fix(term): stop daemon emulator from replying to xterm queries (#1024)
This commit is contained in:
parent
5315e87a81
commit
0a9187909d
|
|
@ -138,22 +138,6 @@ describe('HeadlessEmulator', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('onData callback', () => {
|
||||
it('fires onData for terminal query responses', async () => {
|
||||
const responses: string[] = []
|
||||
emulator = new HeadlessEmulator({
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
onData: (data) => responses.push(data)
|
||||
})
|
||||
|
||||
// DA1 query — xterm.js will respond with a device attributes string
|
||||
await emulator.write('\x1b[c')
|
||||
|
||||
expect(responses.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('terminal modes', () => {
|
||||
it('tracks bracketed paste mode', async () => {
|
||||
emulator = new HeadlessEmulator({ cols: 80, rows: 24 })
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ export type HeadlessEmulatorOptions = {
|
|||
cols: number
|
||||
rows: number
|
||||
scrollback?: number
|
||||
onData?: (data: string) => void
|
||||
}
|
||||
|
||||
const DEFAULT_SCROLLBACK = 5000
|
||||
|
|
@ -57,9 +56,17 @@ export class HeadlessEmulator {
|
|||
this.serializer = new SerializeAddon()
|
||||
this.terminal.loadAddon(this.serializer)
|
||||
|
||||
if (opts.onData) {
|
||||
this.terminal.onData(opts.onData)
|
||||
}
|
||||
// Why no onData wiring: this emulator exists purely for state tracking
|
||||
// (snapshots, cwd, mode flags). It MUST NOT respond to terminal query
|
||||
// sequences (DA1/DA2, DSR, OSC 10/11/12, DECRPM). The emulator parses
|
||||
// data in-process synchronously before `handleSubprocessData` forwards
|
||||
// it to the renderer over IPC, so any reply it emits would land on the
|
||||
// shell's stdin ahead of the renderer's xterm reply and win the race.
|
||||
// The renderer is the authoritative responder (it has the real theme,
|
||||
// cursor position, and paste mode); a daemon-side reply would be a
|
||||
// double-reply with wrong values. OSC 11 was the visible casualty:
|
||||
// Claude Code's /theme auto always saw the emulator's default-black
|
||||
// background regardless of Orca's configured terminal theme.
|
||||
}
|
||||
|
||||
write(data: string): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -156,6 +156,27 @@ describe('Session', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('emulator does not reply to terminal queries', () => {
|
||||
// Why: daemon emulator parses in-process synchronously — before
|
||||
// handleSubprocessData forwards bytes to the renderer over IPC — so any
|
||||
// auto-reply it emits races ahead of the renderer's xterm and clobbers
|
||||
// it with default-xterm values (no theme, stale cursor). The renderer is
|
||||
// the authoritative responder; a daemon-side reply to any query is a bug.
|
||||
it.each([
|
||||
['OSC 11 background-color', '\x1b]11;?\x07'],
|
||||
['DA1 device-attributes', '\x1b[c'],
|
||||
['DSR cursor-position', '\x1b[6n']
|
||||
])('does not reply to %s query', async (_label, query) => {
|
||||
createSession({ shellReadySupported: false })
|
||||
subprocess.simulateData(query)
|
||||
// xterm.js fires terminal.write's completion callback via a microtask;
|
||||
// two resolved-promise awaits flush any nested scheduling.
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
expect(subprocess.written).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('shell readiness gating', () => {
|
||||
it('buffers writes during pending state', () => {
|
||||
createSession({ shellReadySupported: true })
|
||||
|
|
|
|||
|
|
@ -54,16 +54,11 @@ export class Session {
|
|||
this.emulator = new HeadlessEmulator({
|
||||
cols: size.cols,
|
||||
rows: size.rows,
|
||||
scrollback: opts.scrollback,
|
||||
// Why: xterm.js generates query responses (DA1, DSR) asynchronously.
|
||||
// If the subprocess has already exited, writing to it would hit a dead
|
||||
// NAPI handle. Check session state before forwarding.
|
||||
onData: (data) => {
|
||||
if (this._state === 'exited' || this._disposed) {
|
||||
return
|
||||
}
|
||||
opts.subprocess.write(data)
|
||||
}
|
||||
scrollback: opts.scrollback
|
||||
// No onData wiring: the daemon-side emulator must never reply to
|
||||
// terminal query sequences. The renderer's xterm is the authoritative
|
||||
// responder; any daemon reply races ahead via in-process parsing and
|
||||
// clobbers the renderer's answer. See the comment in HeadlessEmulator.
|
||||
})
|
||||
|
||||
if (opts.shellReadySupported) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue