mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 16:03:26 +00:00
Clear the alt screen before restoring a hidden-output snapshot
Live capture of a reproduced case showed the frozen/overlapping tab is a cell-level merge INSIDE the renderer's buffer: the pre-hide frame (banner, prompt echo) bleeding through every blank cell of the restored final frame. The rendering stack was innocent — it faithfully painted a merged buffer. The hidden-output restore deliberately skips the destructive clear for alternate-screen snapshots to protect normal-buffer scrollback. But the snapshot's own ?1049h is a no-op on a pane that is already on the alternate screen, and the serialized frame skips blank cells entirely (cursor-forward jumps), so nothing ever erases the pre-hide frame. Fresh terminals enter the alt screen via ?1049h — which clears it — which is why the bug never reproduced in fresh windows. Fix: for alternate-screen snapshots, write \x1b[?1049h\x1b[2J\x1b[H before the snapshot data. \x1b[2J on the alt buffer clears only the alt screen; the normal buffer's scrollback (what the skip was protecting) is untouched. Verified live on the reproduced specimen: writing the same snapshot with this preamble repaired the pane pixel-perfectly. Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -7316,7 +7316,7 @@ describe('connectPanePty', () => {
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
it('preserves scrollback by skipping the destructive clear when restoring an alternate-screen snapshot', async () => {
|
||||
it('clears only the alternate screen when restoring an alternate-screen snapshot', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport('pty-id')
|
||||
const capturedDataCallback: {
|
||||
@@ -7365,6 +7365,19 @@ describe('connectPanePty', () => {
|
||||
'\x1b[2J\x1b[3J\x1b[H',
|
||||
expect.any(Function)
|
||||
)
|
||||
// Why: the snapshot's ?1049h no-ops on an already-alt pane and serialized
|
||||
// frames skip blank cells, so the pre-hide frame bleeds through unless the
|
||||
// alt screen is cleared (without \x1b[3J) before the snapshot paints.
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(
|
||||
'\x1b[?1049h\x1b[2J\x1b[H',
|
||||
expect.any(Function)
|
||||
)
|
||||
const writes = (pane.terminal.write as ReturnType<typeof vi.fn>).mock.calls.map(
|
||||
(call) => call[0]
|
||||
)
|
||||
expect(writes.indexOf('\x1b[?1049h\x1b[2J\x1b[H')).toBeLessThan(
|
||||
writes.indexOf('altscreen-snapshot\r\n')
|
||||
)
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith('altscreen-snapshot\r\n', expect.any(Function))
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
@@ -4393,6 +4393,13 @@ export function connectPanePty(
|
||||
// clearing on restore loses scroll-up after a hidden->visible return.
|
||||
// Mirrors the attach-time guard in pty-transport.ts.
|
||||
writeReplayData('\x1b[2J\x1b[3J\x1b[H')
|
||||
} else {
|
||||
// Why: the snapshot's own ?1049h is a no-op when the pane is already on
|
||||
// the alternate screen, and the serialized frame skips blank cells — so
|
||||
// without clearing the alt screen the pre-hide frame bleeds through
|
||||
// every cell the final frame leaves blank. \x1b[2J on the alt buffer
|
||||
// does not touch the normal buffer's scrollback the TUI returns to.
|
||||
writeReplayData('\x1b[?1049h\x1b[2J\x1b[H')
|
||||
}
|
||||
writeReplayData(snapshot.data)
|
||||
writeReplayData(POST_REPLAY_LIVE_SNAPSHOT_RESET)
|
||||
|
||||
Reference in New Issue
Block a user