diff --git a/src/main/ipc/pty/pane/terminal-spawn-restore.ts b/src/main/ipc/pty/pane/terminal-spawn-restore.ts index 64bd819f568..52254555946 100644 --- a/src/main/ipc/pty/pane/terminal-spawn-restore.ts +++ b/src/main/ipc/pty/pane/terminal-spawn-restore.ts @@ -9,15 +9,17 @@ export function seedHeadlessTerminalFromSpawnResult( result: PtySpawnResult, paneKey: string | null ): void { - // Renderer-owned layout prefers its richer history; main-owned layout must seed the host copy every view restores. - if ( - !runtime || - (!ORCA_LAYOUT_OWNER_IS_MAIN && paneKey && pendingByPaneKey.has(paneKey)) || - (result.isReattach === true && rendererSerializerReadiness.has(result.id)) - ) { + if (!runtime || (result.isReattach === true && rendererSerializerReadiness.has(result.id))) { return } + // Renderer-owned layout prefers its richer history; main-owned layout must seed the host copy every view restores. + const rendererOwnsHistory = Boolean( + !ORCA_LAYOUT_OWNER_IS_MAIN && paneKey && pendingByPaneKey.has(paneKey) + ) if (typeof result.snapshot === 'string' && result.snapshot.length > 0) { + if (rendererOwnsHistory) { + return + } const size = typeof result.snapshotCols === 'number' && typeof result.snapshotRows === 'number' ? { cols: result.snapshotCols, rows: result.snapshotRows } @@ -29,6 +31,8 @@ export function seedHeadlessTerminalFromSpawnResult( ...(result.snapshotTerminalOwner ? { terminalOwner: result.snapshotTerminalOwner } : {}) }) } else if ( + // Seed even when the renderer owns history: its xterm holds only this same payload, and a pane + // revealed after output arrived while hidden repaints from the host copy (STA-14063). result.coldRestore && typeof result.coldRestore.scrollback === 'string' && result.coldRestore.scrollback.length > 0 @@ -47,7 +51,11 @@ export function seedHeadlessTerminalFromSpawnResult( oscLinks: result.coldRestore.oscLinks, preferProviderIfExisting: true }) - } else if (typeof result.replay === 'string' && result.replay.length > 0) { + } else if ( + !rendererOwnsHistory && + typeof result.replay === 'string' && + result.replay.length > 0 + ) { runtime.seedHeadlessTerminal(result.id, result.replay) } } diff --git a/src/main/runtime/orca-runtime-tests/headless-snapshots.spec.ts b/src/main/runtime/orca-runtime-tests/headless-snapshots.spec.ts index 79f6142e3b9..f25fb288f30 100644 --- a/src/main/runtime/orca-runtime-tests/headless-snapshots.spec.ts +++ b/src/main/runtime/orca-runtime-tests/headless-snapshots.spec.ts @@ -4,6 +4,12 @@ import { OrcaRuntimeService } from '../orca-runtime' import { setPlatform } from '../orca-runtime-test-mocks/setup.spec' import type { RuntimeTerminalAgentStatusEvent } from '../terminal/output/terminal-agent-status-events' import type { HeadlessEmulator } from '../../daemon/emulation/headless-emulator' +import { makePaneKey } from '../../../shared/workspace-layout/stable-pane-id' +import { + declarePendingPaneSerializer, + settlePendingPaneSerializer +} from '../../ipc/pty/pane/serializer-state' +import { seedHeadlessTerminalFromSpawnResult } from '../../ipc/pty/pane/terminal-spawn-restore' import { TEST_WORKTREE_ID, @@ -240,6 +246,32 @@ describe('OrcaRuntimeService', () => { expect(runtime.resolveTerminalFileUriHostname(terminal.handle)).toBe('remote-host') }) + it('retains cold-restored history in hidden-output recovery while renderer serialization is pending', async () => { + const runtime = createRuntime() + syncSinglePty(runtime, 'cold-pty') + const paneKey = makePaneKey('tab-1', '44444444-4444-4444-8444-444444444444') + const generation = declarePendingPaneSerializer(paneKey, undefined) + try { + seedHeadlessTerminalFromSpawnResult( + runtime, + { + id: 'cold-pty', + coldRestore: { scrollback: 'restored history\r\n', cwd: '/tmp', cols: 40, rows: 6 } + }, + paneKey + ) + runtime.onPtyData('cold-pty', 'fresh shell prompt $ ', 100) + + const snapshot = await runtime.serializeHiddenOutputRecoveryBuffer('cold-pty', { + scrollbackRows: 100 + }) + expect(snapshot?.data).toContain('restored history') + expect(snapshot?.data).toContain('fresh shell prompt $ ') + } finally { + settlePendingPaneSerializer(paneKey, generation) + } + }) + it('falls back to the renderer snapshot for hidden-output recovery without headless state', async () => { const serializeBuffer = vi.fn().mockResolvedValue({ data: '\x1b[?1049hRenderer TUI\r\nStill running\r\n',