mirror of
https://github.com/stablyai/orca.git
synced 2026-11-04 02:57:36 +00:00
fix(terminal): a woken pane's new prompt starts on its own line when reattached (#33494)
After Wake the window paints the restored history and then blanks its viewport, so the new shell's prompt starts on a clean screen. The daemon's own copy of the screen, seeded with the same history, skipped that step. A later reattach (a reload, a second window, the phone) therefore showed the new prompt beside the dead shell's last one: `p7/sub$ orca@awin:~/projects/p7$` (awin verify-11). The seed now ends with the same viewport blanking the window writes, built by one shared function, after the process-boundary ground, which still never moves the cursor. Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -103,13 +103,23 @@ describe('process boundary ground at a cold restore', () => {
|
||||
expect(checkpoint.snapshotAnsi).not.toContain('\x1b[?66h')
|
||||
})
|
||||
|
||||
it('keeps the cursor where the recovered history left it', () => {
|
||||
it('grounds without moving the cursor', () => {
|
||||
const live = emulator(DAEMON_SESSION_SCROLLBACK_ROWS)
|
||||
write(live, 'OLD_OUTPUT_1\r\n$ ')
|
||||
write(live, PROCESS_BOUNDARY_GROUND)
|
||||
write(live, 'X')
|
||||
expect(live.getBufferTailLines(24)[1]).toBe('$ X')
|
||||
})
|
||||
|
||||
// awin verify-11: a reattach after Wake showed the new prompt beside the dead shell's (`$ $`).
|
||||
it("starts the new shell's prompt on its own line, keeping the recovered history", () => {
|
||||
const live = seededLive(checkpointOfDeadProcess(''))
|
||||
write(live, 'NEW_SHELL_PROMPT')
|
||||
|
||||
const rows = live.getBufferTailLines(24)
|
||||
expect(rows[0]).toBe('OLD_OUTPUT_1')
|
||||
expect(rows[2]).toBe('$ NEW_SHELL_PROMPT')
|
||||
const rows = live.getBufferTailLines(60).filter((row) => row.length > 0)
|
||||
expect(rows.slice(0, 2)).toEqual(['OLD_OUTPUT_1', 'OLD_OUTPUT_2'])
|
||||
expect(rows.some((row) => row.trim() === '$')).toBe(true)
|
||||
expect(rows.at(-1)).toBe('NEW_SHELL_PROMPT')
|
||||
})
|
||||
|
||||
it('never lets the new shell complete a torn escape from the dead process', () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
PROCESS_BOUNDARY_GROUND,
|
||||
RESET_GRAPHIC_RENDITION
|
||||
} from '../../shared/terminal-mode-reset-profiles'
|
||||
import { buildFreshShellViewportBlankingSequence } from '../../shared/terminal-fresh-shell-viewport'
|
||||
import type { ColdRestoreInfo } from './terminal-history-cold-restore-info'
|
||||
import type { TerminalModes } from './types'
|
||||
|
||||
@@ -20,6 +21,7 @@ const ARMED_MODES: TerminalModes = {
|
||||
|
||||
// Assert seed placement here; terminal-mode-reset-profiles.test.ts pins the reset bytes.
|
||||
const GROUND = PROCESS_BOUNDARY_GROUND
|
||||
const FRESH_VIEWPORT = buildFreshShellViewportBlankingSequence(24)
|
||||
|
||||
function restoreInfo(overrides: Partial<ColdRestoreInfo> = {}): ColdRestoreInfo {
|
||||
return {
|
||||
@@ -42,7 +44,8 @@ describe('getRecoveredHistorySeedSegments', () => {
|
||||
expect(segments).toEqual([
|
||||
`${RESET_GRAPHIC_RENDITION}\x1b[?1003h\x1b[?1006h`,
|
||||
'user@host ~ $ \x1b[?1003h',
|
||||
GROUND
|
||||
GROUND,
|
||||
FRESH_VIEWPORT
|
||||
])
|
||||
})
|
||||
|
||||
@@ -51,7 +54,7 @@ describe('getRecoveredHistorySeedSegments', () => {
|
||||
getRecoveredHistorySeedSegments(
|
||||
restoreInfo({ modes: { ...ARMED_MODES, alternateScreen: true } })
|
||||
)
|
||||
).toEqual([`${RESET_GRAPHIC_RENDITION}user@host ~ $ `, GROUND])
|
||||
).toEqual([`${RESET_GRAPHIC_RENDITION}user@host ~ $ `, GROUND, FRESH_VIEWPORT])
|
||||
})
|
||||
|
||||
it('stays empty when there is no recovered normal buffer', () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
PROCESS_BOUNDARY_GROUND,
|
||||
RESET_GRAPHIC_RENDITION
|
||||
} from '../../shared/terminal-mode-reset-profiles'
|
||||
import { buildFreshShellViewportBlankingSequence } from '../../shared/terminal-fresh-shell-viewport'
|
||||
|
||||
// Reset modes after seeding the daemon too, or downstream snapshots (including mobile) re-arm dead TUI state.
|
||||
|
||||
@@ -10,7 +11,11 @@ export function getRecoveredHistorySeedSegments(restoreInfo: ColdRestoreInfo): r
|
||||
if (restoreInfo.modes.alternateScreen) {
|
||||
const normalBuffer = restoreInfo.scrollbackAnsi || restoreInfo.snapshotAnsi
|
||||
return normalBuffer
|
||||
? [`${RESET_GRAPHIC_RENDITION}${normalBuffer}`, PROCESS_BOUNDARY_GROUND]
|
||||
? [
|
||||
`${RESET_GRAPHIC_RENDITION}${normalBuffer}`,
|
||||
PROCESS_BOUNDARY_GROUND,
|
||||
freshShellViewport(restoreInfo)
|
||||
]
|
||||
: []
|
||||
}
|
||||
const recovered = [restoreInfo.rehydrateSequences, restoreInfo.snapshotAnsi].filter(
|
||||
@@ -25,6 +30,13 @@ export function getRecoveredHistorySeedSegments(restoreInfo: ColdRestoreInfo): r
|
||||
return [
|
||||
`${RESET_GRAPHIC_RENDITION}${firstRecovered}`,
|
||||
...remainingRecovered,
|
||||
PROCESS_BOUNDARY_GROUND
|
||||
PROCESS_BOUNDARY_GROUND,
|
||||
freshShellViewport(restoreInfo)
|
||||
]
|
||||
}
|
||||
|
||||
// Why: the window blanks its viewport after painting a restore, but the daemon's seeded screen
|
||||
// did not, so a reattach showed the new shell's prompt beside the old one (`$ $`).
|
||||
function freshShellViewport(restoreInfo: ColdRestoreInfo): string {
|
||||
return buildFreshShellViewportBlankingSequence(restoreInfo.rows)
|
||||
}
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import {
|
||||
} from '../../../../shared/terminal-mode-reset-profiles'
|
||||
import { Terminal } from '@xterm/headless'
|
||||
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch'
|
||||
import { buildFreshShellViewportBlankingSequence } from './terminal-restored-viewport'
|
||||
import { buildFreshShellViewportBlankingSequence } from '../../../../shared/terminal-fresh-shell-viewport'
|
||||
import {
|
||||
flushAsyncTicks,
|
||||
writeHeadlessTerminal,
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import {
|
||||
POST_REPLAY_REATTACH_RESET_KEEP_MOUSE,
|
||||
buildKittyKeyboardRestore
|
||||
} from '../../../../../shared/terminal-mode-reset-profiles'
|
||||
import { buildFreshShellViewportBlankingSequence } from '../terminal-restored-viewport'
|
||||
import { buildFreshShellViewportBlankingSequence } from '../../../../../shared/terminal-fresh-shell-viewport'
|
||||
import { flushTerminalOutput } from '@/lib/pane-manager/pane-terminal-output-scheduler'
|
||||
import {
|
||||
getTerminalScrollIntentKind,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Terminal } from '@xterm/headless'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { buildFreshShellViewportBlankingSequence } from './terminal-restored-viewport'
|
||||
import { buildFreshShellViewportBlankingSequence } from '../../../../shared/terminal-fresh-shell-viewport'
|
||||
|
||||
function writeTerminal(term: Terminal, data: string): Promise<void> {
|
||||
return new Promise((resolve) => term.write(data, resolve))
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
import type { RefObject } from 'react'
|
||||
|
||||
export type RestoredViewportBlankingPanesRef = RefObject<Set<number>>
|
||||
|
||||
export function buildFreshShellViewportBlankingSequence(rows: number): string {
|
||||
const viewportRows = Math.max(1, Math.floor(Number.isFinite(rows) ? rows : 24))
|
||||
// Why: newline scrolling preserves restored rows in xterm scrollback; CSI S
|
||||
// drops them. Reset margins first so stale TUI scroll regions cannot trap it.
|
||||
return `\x1b[?6l\x1b[r\x1b[${viewportRows};1H${'\r\n'.repeat(viewportRows)}\x1b[H`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Scrolls a restored screen into scrollback and homes the cursor, so a fresh shell's first prompt
|
||||
* starts on a blank viewport instead of beside the dead shell's last prompt.
|
||||
*/
|
||||
export function buildFreshShellViewportBlankingSequence(rows: number): string {
|
||||
const viewportRows = Math.max(1, Math.floor(Number.isFinite(rows) ? rows : 24))
|
||||
// Why: newline scrolling preserves restored rows in xterm scrollback; CSI S
|
||||
// drops them. Reset margins first so stale TUI scroll regions cannot trap it.
|
||||
return `\x1b[?6l\x1b[r\x1b[${viewportRows};1H${'\r\n'.repeat(viewportRows)}\x1b[H`
|
||||
}
|
||||
Reference in New Issue
Block a user