diff --git a/src/renderer/src/components/terminal-pane/TerminalPane.tsx b/src/renderer/src/components/terminal-pane/TerminalPane.tsx index cb881533aeb..fc2da132f37 100644 --- a/src/renderer/src/components/terminal-pane/TerminalPane.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalPane.tsx @@ -1069,14 +1069,6 @@ function TerminalPane( } }, [tabId, setTabLayout, worktreeId]) - // Why: the dedupe cache mirrors what one host holds for one tab, so drop it when either changes or the pane unmounts. - useEffect(() => { - const pusher = remotePaneLayoutPusherRef.current - return () => { - pusher?.reset() - } - }, [tabId, worktreeId]) - const clearPaneScrollback = useCallback( (pane: ManagedPane): void => { clearedScrollbackLeafIdsRef.current.add(pane.leafId) diff --git a/src/renderer/src/components/terminal-pane/remote-pane-layout-push.test.ts b/src/renderer/src/components/terminal-pane/remote-pane-layout-push.test.ts index 6318e3b7037..ef9a0d63597 100644 --- a/src/renderer/src/components/terminal-pane/remote-pane-layout-push.test.ts +++ b/src/renderer/src/components/terminal-pane/remote-pane-layout-push.test.ts @@ -36,7 +36,7 @@ function makeLayout(overrides: Partial = {}): TerminalLa describe('createRemotePaneLayoutPusher', () => { beforeEach(() => { - updateWebRuntimePaneLayout.mockClear() + updateWebRuntimePaneLayout.mockReset().mockResolvedValue(true) }) it('pushes once across 100 persists of an unchanged layout', () => { @@ -125,11 +125,33 @@ describe('createRemotePaneLayoutPusher', () => { expect(updateWebRuntimePaneLayout).toHaveBeenCalledTimes(3) }) - it('re-pushes after reset so a remount re-establishes host geometry', () => { + it('does not carry a cached layout across worktrees', () => { const pusher = createRemotePaneLayoutPusher() pusher.push({ worktreeId: 'wt-1', tabId: 'tab-1', layout: makeLayout() }) - pusher.reset() + pusher.push({ worktreeId: 'wt-2', tabId: 'tab-1', layout: makeLayout() }) + expect(updateWebRuntimePaneLayout).toHaveBeenCalledTimes(2) + }) + + it('retries an unchanged layout after a failed push', async () => { + updateWebRuntimePaneLayout.mockResolvedValueOnce(false) + const pusher = createRemotePaneLayoutPusher() + const input = { worktreeId: 'wt-1', tabId: 'tab-1', layout: makeLayout() } + + pusher.push(input) + await Promise.resolve() + pusher.push(input) + + expect(updateWebRuntimePaneLayout).toHaveBeenCalledTimes(2) + }) + + it('re-pushes after a remount re-establishes host geometry', () => { + const pusher = createRemotePaneLayoutPusher() pusher.push({ worktreeId: 'wt-1', tabId: 'tab-1', layout: makeLayout() }) + createRemotePaneLayoutPusher().push({ + worktreeId: 'wt-1', + tabId: 'tab-1', + layout: makeLayout() + }) expect(updateWebRuntimePaneLayout).toHaveBeenCalledTimes(2) }) }) diff --git a/src/renderer/src/components/terminal-pane/remote-pane-layout-push.ts b/src/renderer/src/components/terminal-pane/remote-pane-layout-push.ts index aa6e7fd0f7d..506bd7ea42f 100644 --- a/src/renderer/src/components/terminal-pane/remote-pane-layout-push.ts +++ b/src/renderer/src/components/terminal-pane/remote-pane-layout-push.ts @@ -4,7 +4,6 @@ import { updateWebRuntimePaneLayout } from '@/runtime/web-runtime-session' export type RemotePaneLayoutPusher = { push: (input: { worktreeId: string; tabId: string; layout: TerminalLayoutSnapshot }) => void - reset: () => void } /** @@ -13,23 +12,36 @@ export type RemotePaneLayoutPusher = { * Dedupe against the last push so unchanged layouts cost no remote round trip. */ export function createRemotePaneLayoutPusher(): RemotePaneLayoutPusher { - let lastPushed: { tabId: string; snapshot: TerminalLayoutSnapshot } | null = null + let lastAttempt: { + id: number + worktreeId: string + tabId: string + snapshot: TerminalLayoutSnapshot + } | null = null + let nextAttemptId = 0 return { push: ({ worktreeId, tabId, layout }) => { - if (lastPushed?.tabId === tabId && terminalLayoutEqual(lastPushed.snapshot, layout)) { + if ( + lastAttempt?.worktreeId === worktreeId && + lastAttempt.tabId === tabId && + terminalLayoutEqual(lastAttempt.snapshot, layout) + ) { return } - lastPushed = { tabId, snapshot: layout } + const attempt = { id: ++nextAttemptId, worktreeId, tabId, snapshot: layout } + lastAttempt = attempt void updateWebRuntimePaneLayout({ worktreeId, tabId, root: layout.root, expandedLeafId: layout.expandedLeafId, ...(layout.titlesByLeafId ? { titlesByLeafId: layout.titlesByLeafId } : {}) + }).then((updated) => { + // Why: a disconnected or timed-out push carried no information, so the next persist must retry it. + if (!updated && lastAttempt?.id === attempt.id) { + lastAttempt = null + } }) - }, - reset: () => { - lastPushed = null } } }