fix(terminal): make startup scanning chunk-invariant

This commit is contained in:
Jinwoo-H
2026-08-12 22:21:23 -04:00
parent 4099540e67
commit 62e4ae7c0b
3 changed files with 90 additions and 18 deletions
+21 -3
View File
@@ -11,6 +11,10 @@ export type ShellReadyScanResult = {
postMarkerBytesObserved: boolean
}
type ShellReadyBoundaryScanResult = ShellReadyScanResult & {
postMarkerOutputIndex: number | null
}
export function createShellReadyScanState(): ShellReadyScanState {
return { matchPos: 0, heldBytes: '' }
}
@@ -22,7 +26,10 @@ export function drainShellReadyHeldBytes(state: ShellReadyScanState): string {
return heldBytes
}
export function scanForShellReady(state: ShellReadyScanState, data: string): ShellReadyScanResult {
export function scanForShellReadyBoundary(
state: ShellReadyScanState,
data: string
): ShellReadyBoundaryScanResult {
let output = ''
for (let i = 0; i < data.length; i += 1) {
@@ -49,7 +56,8 @@ export function scanForShellReady(state: ShellReadyScanState, data: string): She
return {
output: output + remaining,
matched: true,
postMarkerBytesObserved: remaining.length > 0
postMarkerBytesObserved: remaining.length > 0,
postMarkerOutputIndex: output.length
}
} else {
output += state.heldBytes
@@ -64,5 +72,15 @@ export function scanForShellReady(state: ShellReadyScanState, data: string): She
}
}
return { output, matched: false, postMarkerBytesObserved: false }
return {
output,
matched: false,
postMarkerBytesObserved: false,
postMarkerOutputIndex: null
}
}
export function scanForShellReady(state: ShellReadyScanState, data: string): ShellReadyScanResult {
const { postMarkerOutputIndex: _, ...result } = scanForShellReadyBoundary(state, data)
return result
}
+58 -4
View File
@@ -6,6 +6,42 @@ import {
} from './shell-startup-output-scanner'
const READY_MARKER = '\x1b]777;orca-shell-ready\x07'
const IDENTITY_MARKER = '\x1b]777;orca-shell-start:12345\x07'
function scanChunks(chunks: string[]): {
output: string
shellPid: number | null
ready: boolean
postMarkerBytesObserved: boolean
} {
const state = createShellStartupOutputScanState()
let output = ''
let shellPid: number | null = null
let ready = false
let postMarkerBytesObserved = false
for (const chunk of chunks) {
const scanned = scanShellStartupOutput(state, chunk)
output += scanned.output
shellPid ??= scanned.shellPid
ready ||= scanned.ready
postMarkerBytesObserved ||= scanned.postMarkerBytesObserved
}
return { output, shellPid, ready, postMarkerBytesObserved }
}
function expectEverySplitToProduce(
input: string,
expected: Omit<ReturnType<typeof scanChunks>, 'postMarkerBytesObserved'>
): void {
for (let split = 0; split <= input.length; split += 1) {
const { postMarkerBytesObserved: _, ...scanned } = scanChunks([
input.slice(0, split),
input.slice(split)
])
expect(scanned, `split ${split}`).toEqual(expected)
}
}
describe('shell startup output scanner', () => {
it.each([
@@ -22,12 +58,30 @@ describe('shell startup output scanner', () => {
expect(output).toBe('\x1b[?2004hfish> ')
})
it('preserves every post-ready byte at every chunk boundary', () => {
expectEverySplitToProduce(`${READY_MARKER}${IDENTITY_MARKER}prompt`, {
output: `${IDENTITY_MARKER}prompt`,
shellPid: null,
ready: true
})
expectEverySplitToProduce(`${READY_MARKER}\x1b[?2004hfish> `, {
output: '\x1b[?2004hfish> ',
shellPid: null,
ready: true
})
})
it('strips pre-ready markers at every chunk boundary', () => {
expectEverySplitToProduce(`${IDENTITY_MARKER}${READY_MARKER}prompt`, {
output: 'prompt',
shellPid: 12345,
ready: true
})
})
it('strips identity and readiness markers from one chunk', () => {
const state = createShellStartupOutputScanState()
const scanned = scanShellStartupOutput(
state,
`\x1b]777;orca-shell-start:12345\x07${READY_MARKER}prompt`
)
const scanned = scanShellStartupOutput(state, `${IDENTITY_MARKER}${READY_MARKER}prompt`)
expect(scanned).toEqual({
output: 'prompt',
+11 -11
View File
@@ -1,7 +1,7 @@
import {
createShellReadyScanState,
drainShellReadyHeldBytes,
scanForShellReady,
scanForShellReadyBoundary,
type ShellReadyScanState
} from './shell-ready-marker-scanner'
import {
@@ -34,30 +34,30 @@ export function scanShellStartupOutput(
state: ShellStartupOutputScanState,
data: string
): ShellStartupOutputScanResult {
const readiness = scanForShellReadyBoundary(state.ready, data)
const postMarkerOutputIndex = readiness.postMarkerOutputIndex ?? readiness.output.length
const identityInput = readiness.output.slice(0, postMarkerOutputIndex)
let shellPid: number | null = null
let output = identityInput
if (state.identity) {
const identity = scanForShellStartupIdentity(state.identity, data)
data = identity.output
const identity = scanForShellStartupIdentity(state.identity, identityInput)
output = identity.output
shellPid = identity.shellPid
if (shellPid) {
state.identity = null
}
}
const readiness = scanForShellReady(state.ready, data)
let output = readiness.output
let postMarkerBytesObserved = readiness.postMarkerBytesObserved
if (readiness.matched && state.identity) {
const heldOutput = drainShellStartupIdentityHeldBytes(state.identity)
output += heldOutput
postMarkerBytesObserved ||= heldOutput.length > 0
output += drainShellStartupIdentityHeldBytes(state.identity)
state.identity = null
}
output += readiness.output.slice(postMarkerOutputIndex)
return {
output,
shellPid,
ready: readiness.matched,
postMarkerBytesObserved
postMarkerBytesObserved: readiness.postMarkerBytesObserved
}
}
@@ -65,7 +65,7 @@ export function drainShellStartupOutputScanState(state: ShellStartupOutputScanSt
let output = state.identity ? drainShellStartupIdentityHeldBytes(state.identity) : ''
state.identity = null
if (output) {
output = scanForShellReady(state.ready, output).output
output = scanForShellReadyBoundary(state.ready, output).output
}
return output + drainShellReadyHeldBytes(state.ready)
}