mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 16:02:24 +00:00
fish arms `CSI ?2031h` before painting each prompt and withdraws it when it hands the tty to a child — a ~1ms window. Orca answered that subscribe with `CSI ?997;Nn` across a 1-3ms renderer hop, so the reply landed after the withdrawal and was read as stdin by the next child, corrupting `brew`/`npx` `[y/N]` prompts. The reply is not stale by Orca's own view when written (measured staleReplies: 0), so no suppress-the-stale-reply scheme can close this — the information needed to suppress does not exist yet. Nothing asked for the reply either. The Contour spec says a terminal "should only send out the DSR when the palette has been updated"; Ghostty (Termio.zig:729 — force=true reachable only from the ?996n DSR), iTerm2 (VT100Terminal.m:995 — flag only) and xterm.js (InputHandler.ts:2035 — flag only) all emit nothing on the DECSET. So stop entering the race: record the subscription, answer nothing. Of 17 real programs measured under a pty, only fish, tmux, claude and opencode subscribe; none block on a reply, and answering produces one redundant palette re-query and zero rendering difference. tmux is the only one that sends `?996n`, which Orca still answers. - Subscribes are record-only at all four emitters (live scan, hidden-gate fact, parked byte watcher, parked responder — the last is deleted, it only replied). - `?996n` answers, the subscription registry, and the theme-flip push are unchanged. `paneLastThemeMode` is still seeded at subscribe so the next appearance re-apply is not read as a flip. - Replay grammar carries `?2031l` alongside `?2031h`, so a late-attaching remote client no longer registers a subscription the TUI already retired. Also closes fish-integration gaps found alongside: `unset` (which fish lacks) becomes `set -e` on paths parsed by the client's login shell, `config.fish` is parsed for agent-home detection, and bracketed-paste startup delivery is made consistent across local/daemon/relay. Regression test drives real fish 4.7.1 under node-pty and asserts on what the child process reads; it fails against pre-fix code with the exact payload from the issue. CI installs fish 4 and fails loudly rather than skipping. Closes #9993 Co-authored-by: Orca <help@stably.ai>
69 lines
2.5 KiB
TypeScript
69 lines
2.5 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
buildStartupCommandSubmission,
|
|
isBracketedPasteSafeShell
|
|
} from './startup-command-submission'
|
|
|
|
describe('buildStartupCommandSubmission', () => {
|
|
it('appends the submit byte to a single-line command unchanged', () => {
|
|
expect(
|
|
buildStartupCommandSubmission('claude', { submit: '\n', bracketedPasteSafe: true })
|
|
).toBe('claude\n')
|
|
expect(
|
|
buildStartupCommandSubmission('claude', { submit: '\r', bracketedPasteSafe: true })
|
|
).toBe('claude\r')
|
|
})
|
|
|
|
it('preserves a caller-supplied trailing submit byte on single-line commands', () => {
|
|
expect(
|
|
buildStartupCommandSubmission('claude\n', { submit: '\r', bracketedPasteSafe: true })
|
|
).toBe('claude\n')
|
|
})
|
|
|
|
it('treats a CRLF-terminated single-line command as single-line', () => {
|
|
expect(
|
|
buildStartupCommandSubmission('claude\r\n', { submit: '\r', bracketedPasteSafe: true })
|
|
).toBe('claude\r\n')
|
|
})
|
|
|
|
it('wraps a multiline command in bracketed paste with a trailing submit byte', () => {
|
|
const command = "claude 'first\nsecond'"
|
|
expect(buildStartupCommandSubmission(command, { submit: '\n', bracketedPasteSafe: true })).toBe(
|
|
`\x1b[200~${command}\x1b[201~\n`
|
|
)
|
|
})
|
|
|
|
it('strips a trailing submit byte before bracket-wrapping the multiline body', () => {
|
|
const body = "claude 'first\nsecond'"
|
|
expect(
|
|
buildStartupCommandSubmission(`${body}\n`, { submit: '\r', bracketedPasteSafe: true })
|
|
).toBe(`\x1b[200~${body}\x1b[201~\r`)
|
|
})
|
|
|
|
it('keeps the raw path for multiline commands when bracketed paste is unsafe', () => {
|
|
const command = 'echo one\necho two'
|
|
expect(
|
|
buildStartupCommandSubmission(command, { submit: '\n', bracketedPasteSafe: false })
|
|
).toBe(`${command}\n`)
|
|
})
|
|
})
|
|
|
|
describe('isBracketedPasteSafeShell', () => {
|
|
it('always trusts bash and zsh', () => {
|
|
for (const shellName of ['bash', 'zsh', 'BASH']) {
|
|
expect(isBracketedPasteSafeShell({ shellName, waitsForShellReady: false })).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('trusts fish only behind the shell-ready barrier', () => {
|
|
expect(isBracketedPasteSafeShell({ shellName: 'fish', waitsForShellReady: true })).toBe(true)
|
|
expect(isBracketedPasteSafeShell({ shellName: 'fish', waitsForShellReady: false })).toBe(false)
|
|
})
|
|
|
|
it('rejects shells with no known bracketed-paste line editor', () => {
|
|
for (const shellName of ['sh', 'dash', 'nu', 'powershell.exe', '']) {
|
|
expect(isBracketedPasteSafeShell({ shellName, waitsForShellReady: true })).toBe(false)
|
|
}
|
|
})
|
|
})
|