diff --git a/src/renderer/src/components/terminal-pane/pty-connection.ts b/src/renderer/src/components/terminal-pane/pty-connection.ts index b9a94ad2dfa..331f288ded4 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.ts @@ -7300,10 +7300,18 @@ export function connectPanePty( snapshot.alternateScreen === true && snapshot.frameRestoreAnsi !== undefined && shouldSkipAltFrameForWidthMismatch(snapshot.cols, readProposedTerminalCols(pane)) - for (const replayChunk of buildMainModelSnapshotReplayWrites(snapshot, { - skipAltFrame: skippedAltFrame - })) { - writeReplayData(replayChunk) + // Why: a success frame with no image is the host having nothing to say, not + // proof the pane is empty. The normal-buffer branch opens with + // \x1b[2J\x1b[3J\x1b[H, so applying it would wipe screen AND scrollback and + // leave exactly the blank pane this restore exists to repair. + const snapshotCarriesNoImage = + snapshot.alternateScreen !== true && snapshot.data === '' && !snapshot.scrollbackAnsi + if (!snapshotCarriesNoImage) { + for (const replayChunk of buildMainModelSnapshotReplayWrites(snapshot, { + skipAltFrame: skippedAltFrame + })) { + writeReplayData(replayChunk) + } } // Why: live agents own ?25l/?1004h; a forced ?1004l here would silence focus events until restart (agents enable focus reporting only at startup). writeReplayData( diff --git a/src/renderer/src/components/terminal-pane/remote-hidden-output-restore-outcomes.test.ts b/src/renderer/src/components/terminal-pane/remote-hidden-output-restore-outcomes.test.ts index 0c9b286703c..eca5cd6bf0d 100644 --- a/src/renderer/src/components/terminal-pane/remote-hidden-output-restore-outcomes.test.ts +++ b/src/renderer/src/components/terminal-pane/remote-hidden-output-restore-outcomes.test.ts @@ -638,6 +638,23 @@ describe('remote hidden-output restore outcomes', () => { drive.disposable.dispose() }) + it('[modern] leaves the painted frame alone when the empty snapshot carries no image', async () => { + const serializeBuffer = vi.fn() + const serializeBufferOutcome = vi.fn().mockResolvedValue({ + availability: { kind: 'snapshot' }, + snapshot: { ...HOST_SNAPSHOT, data: '' } + }) + const drive = await connectHiddenRemoteAgentPane(serializeBuffer, serializeBufferOutcome) + vi.useFakeTimers() + driveHiddenBacklogThenReveal(drive) + await flushAsyncTicks(20) + + // \x1b[3J wipes xterm scrollback; a host answer with nothing in it must never + // cost the user the frame they can still see. + expect(drive.writtenChunks().join('')).not.toContain('\x1b[3J') + drive.disposable.dispose() + }) + it('[modern] waits for a reported outcome instead of applying the old elapsed-time deadline', async () => { const pendingOutcome = createDeferred<{ availability: { kind: 'snapshot' }