diff --git a/src/main/runtime/terminal/surface/main-terminal-surfaces.ts b/src/main/runtime/terminal/surface/main-terminal-surfaces.ts index eeb2c130d5b..ff9b31c86fd 100644 --- a/src/main/runtime/terminal/surface/main-terminal-surfaces.ts +++ b/src/main/runtime/terminal/surface/main-terminal-surfaces.ts @@ -165,7 +165,7 @@ class MainSurfacePublisher { this.schedule({ force: true }) } - /** A layout operation may have moved a pane; main's panes are installed once per task. */ + /** Deferred so a connected flip, noted inside syncWindowGraph's record update, cannot recurse into it. */ private schedule(opts: { force?: boolean } = {}): void { this.forceNext ||= opts.force === true if (this.scheduled) { diff --git a/src/main/runtime/terminal/surface/terminal-surface-replies-main.test.ts b/src/main/runtime/terminal/surface/terminal-surface-replies-main.test.ts index 703c11d8ba2..b2451a9e69f 100644 --- a/src/main/runtime/terminal/surface/terminal-surface-replies-main.test.ts +++ b/src/main/runtime/terminal/surface/terminal-surface-replies-main.test.ts @@ -11,6 +11,7 @@ import { SURFACE_PTYS, SURFACE_TAB_ID, captureSurfaceReplies, + mountedSurfaceGraph, recordedSurfaceReplies, surfaceRuntime } from './terminal-surface-replies.test-fixture' @@ -212,4 +213,41 @@ describe('terminal replies from main (switch on)', () => { paneKey: `${SURFACE_TAB_ID}:${HEADLESS_SECOND_LEAF_ID}` }) }) + + it('reinstall after a graph sync reconnects a terminal, never inside it', async () => { + const { runtime } = surfaceRuntime() + runtime.attachWindow(1) + runtime.syncWindowGraph(1, mountedSurfaceGraph()) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: ptysById is the runtime's PTY registry; the test edits the record it keeps. + const { ptysById } = runtime as unknown as { ptysById: Map } + // Disconnected, not exited: the graph's leaf loop records it connected again. + ptysById.get('pty-a')!.connected = false + const installs: { depth: number; connected: boolean | undefined }[] = [] + let depth = 0 + const sync = runtime.syncWindowGraph.bind(runtime) + vi.spyOn(runtime, 'syncWindowGraph').mockImplementation((windowId, graph) => { + installs.push({ depth, connected: ptysById.get('pty-a')?.connected }) + depth += 1 + try { + return sync(windowId, graph) + } finally { + depth -= 1 + } + }) + + runtime.syncWindowGraph(1, mountedSurfaceGraph()) + expect(installs).toHaveLength(1) + await Promise.resolve() + + // One deferred reinstall, outside the sync, after the record says connected. + expect(installs).toEqual([ + { depth: 0, connected: false }, + { depth: 0, connected: true } + ]) + const listed = await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`) + expect(listed.terminals.map(({ ptyId, connected }) => ({ ptyId, connected }))).toEqual([ + { ptyId: 'pty-a', connected: true }, + { ptyId: 'pty-b', connected: true } + ]) + }) })