fix(renderer): retry failed remote pane layout pushes

This commit is contained in:
Brennan Benson
2026-08-03 22:05:57 -07:00
parent 5d4e6b9511
commit bef37d562a
3 changed files with 44 additions and 18 deletions
@@ -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)
@@ -36,7 +36,7 @@ function makeLayout(overrides: Partial<TerminalLayoutSnapshot> = {}): 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)
})
})
@@ -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
}
}
}