diff --git a/src/main/runtime/hidden-output-restored-provider-snapshot.test.ts b/src/main/runtime/hidden-output-restored-provider-snapshot.test.ts new file mode 100644 index 00000000000..bcca4491fca --- /dev/null +++ b/src/main/runtime/hidden-output-restored-provider-snapshot.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, it, vi } from 'vitest' +import { createRuntime, syncSinglePty } from './orca-runtime-test-fixtures.spec' + +describe('hidden-output recovery after provider reattach', () => { + it('uses retained provider modes instead of the pre-attach redraw suffix', async () => { + const runtime = createRuntime() + const serializeProviderBuffer = vi.fn(async () => ({ + data: '\x1b[?1049hRetained TUI', + cols: 100, + rows: 30, + seq: 1000, + source: 'headless' as const, + alternateScreen: true + })) + runtime.setPtyController({ + write: () => true, + kill: () => true, + getForegroundProcess: async () => null, + serializeProviderBuffer + }) + syncSinglePty(runtime, 'pty-1') + runtime.onPtyData('pty-1', '\x1b[HRedraw without the original alternate-screen entry', 60) + runtime.synchronizePtyOutputSequenceFromProvider('pty-1', { + value: 1000, + generation: 'continued' + }) + + const snapshot = await runtime.serializeHiddenOutputRecoveryBuffer('pty-1', { + scrollbackRows: 5000 + }) + + expect(snapshot).toMatchObject({ data: '\x1b[?1049hRetained TUI', alternateScreen: true }) + expect(serializeProviderBuffer).toHaveBeenCalledWith('pty-1', { scrollbackRows: 5000 }) + }) + + it('keeps the renderer fallback for providers without retained snapshots', async () => { + const runtime = createRuntime() + runtime.onPtyData('pty-1', 'partial redraw', 14) + runtime.synchronizePtyOutputSequenceFromProvider('pty-1', { + value: 1000, + generation: 'continued' + }) + const serializeBuffer = vi.fn(async () => ({ + data: '\x1b[?1049hRenderer TUI', + cols: 100, + rows: 30 + })) + runtime.setPtyController({ + write: () => true, + kill: () => true, + getForegroundProcess: async () => null, + hasRendererSerializer: () => true, + serializeBuffer + }) + + await expect(runtime.serializeHiddenOutputRecoveryBuffer('pty-1')).resolves.toMatchObject({ + data: '\x1b[?1049hRenderer TUI', + source: 'renderer' + }) + }) + + it('keeps an authoritative main model without polling the provider', async () => { + const runtime = createRuntime() + const serializeProviderBuffer = vi.fn(async () => null) + runtime.setPtyController({ + write: () => true, + kill: () => true, + getForegroundProcess: async () => null, + serializeProviderBuffer + }) + runtime.onPtyData('pty-1', '\x1b[?1049hLive TUI', 20) + + await expect(runtime.serializeHiddenOutputRecoveryBuffer('pty-1')).resolves.toMatchObject({ + alternateScreen: true, + source: 'headless' + }) + expect(serializeProviderBuffer).not.toHaveBeenCalled() + }) +}) diff --git a/src/main/runtime/orca-runtime-serialize-main-terminal-buffer.ts b/src/main/runtime/orca-runtime-serialize-main-terminal-buffer.ts index ed777c1f7d1..6559dbfd349 100644 --- a/src/main/runtime/orca-runtime-serialize-main-terminal-buffer.ts +++ b/src/main/runtime/orca-runtime-serialize-main-terminal-buffer.ts @@ -47,6 +47,10 @@ export class OrcaRuntimeWithSerializeMainTerminalBuffer extends OrcaRuntimeWithA pendingEscapeTailAnsi?: string terminalOwner?: 'shell' } | null> { + const restoredSnapshot = await this.serializePreferredRestoredTerminalBuffer(ptyId, opts) + if (restoredSnapshot) { + return restoredSnapshot + } const headlessSnapshot = await this.serializeHeadlessTerminalBuffer(ptyId, { ...opts, includeEmpty: true diff --git a/src/main/runtime/orca-runtime-serialize-terminal-buffer-from-available-state.ts b/src/main/runtime/orca-runtime-serialize-terminal-buffer-from-available-state.ts index e031dc1b6f5..8969841359b 100644 --- a/src/main/runtime/orca-runtime-serialize-terminal-buffer-from-available-state.ts +++ b/src/main/runtime/orca-runtime-serialize-terminal-buffer-from-available-state.ts @@ -23,18 +23,9 @@ export class OrcaRuntimeWithSerializeTerminalBufferFromAvailableState extends Or kittyKeyboardFlags?: number terminalOwner?: 'shell' } | null> { - if (this.providerSnapshotPreferredPtys.has(ptyId)) { - // Why: pre-attach stream bytes only form a suffix of restored state. A - // sequenced provider snapshot safely reconciles live bytes; renderer is - // the fallback when an older provider cannot expose that boundary. - const providerSnapshot = await this.serializeProviderTerminalBuffer(ptyId, opts) - if (providerSnapshot) { - return providerSnapshot - } - const rendererSnapshot = await this.serializeRendererTerminalBuffer(ptyId, opts) - if (rendererSnapshot) { - return rendererSnapshot - } + const restoredSnapshot = await this.serializePreferredRestoredTerminalBuffer(ptyId, opts) + if (restoredSnapshot) { + return restoredSnapshot } const headlessSnapshot = await this.serializeHeadlessTerminalBuffer(ptyId, opts) if (headlessSnapshot) { @@ -58,6 +49,20 @@ export class OrcaRuntimeWithSerializeTerminalBufferFromAvailableState extends Or : rendererSnapshot } + protected async serializePreferredRestoredTerminalBuffer( + ptyId: string, + opts: { scrollbackRows?: number } = {} + ) { + if (!this.providerSnapshotPreferredPtys.has(ptyId)) { + return null + } + // Pre-attach bytes are only a suffix; older providers can fall back to the renderer. + return ( + (await this.serializeProviderTerminalBuffer(ptyId, opts)) ?? + (await this.serializeRendererTerminalBuffer(ptyId, opts)) + ) + } + async serializeRendererTerminalBuffer( ptyId: string, opts: { scrollbackRows?: number } = {}