mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +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>
66 lines
2.2 KiB
TypeScript
66 lines
2.2 KiB
TypeScript
import { spawnSync } from 'node:child_process'
|
|
|
|
/**
|
|
* Locates the fish binary live shell tests run against, and decides how strict
|
|
* they are about finding one.
|
|
*
|
|
* Why the strictness is configurable: on a developer machine without fish these
|
|
* suites must skip, but the `shell contracts` CI job is a required check whose
|
|
* fish lane is the only end-to-end guard for #9993. There, a skip would report
|
|
* green with nothing exercised, so ORCA_REQUIRE_FISH=1 turns it into a failure.
|
|
*/
|
|
const FISH_CANDIDATES = ['fish', '/opt/homebrew/bin/fish', '/usr/local/bin/fish'] as const
|
|
|
|
/** Env var CI sets to make a missing or too-old fish fail instead of skip. */
|
|
export const REQUIRE_FISH_ENV_VAR = 'ORCA_REQUIRE_FISH'
|
|
|
|
export type FishBinaryLookup =
|
|
| { available: true; path: string; majorVersion: number }
|
|
| { available: false; path: null; majorVersion: number; reason: string }
|
|
|
|
export function resolveFishBinary(minMajorVersion = 1): FishBinaryLookup {
|
|
if (process.platform === 'win32') {
|
|
return {
|
|
available: false,
|
|
path: null,
|
|
majorVersion: 0,
|
|
reason: 'fish is not supported on Windows'
|
|
}
|
|
}
|
|
|
|
for (const path of FISH_CANDIDATES) {
|
|
const probe = spawnSync(path, ['--version'], { encoding: 'utf8' })
|
|
if (probe.status !== 0) {
|
|
continue
|
|
}
|
|
const majorVersion = Number(/version (\d+)/.exec(probe.stdout ?? '')?.[1] ?? '0')
|
|
if (majorVersion < minMajorVersion) {
|
|
return {
|
|
available: false,
|
|
path: null,
|
|
majorVersion,
|
|
reason: `fish ${minMajorVersion}+ required, found ${probe.stdout?.trim() || 'an unknown version'} at ${path}`
|
|
}
|
|
}
|
|
return { available: true, path, majorVersion }
|
|
}
|
|
|
|
return { available: false, path: null, majorVersion: 0, reason: 'no fish binary on PATH' }
|
|
}
|
|
|
|
/**
|
|
* The message to fail with when CI demanded fish and did not get it, else null.
|
|
*
|
|
* Assert this in a test that always runs, so the requirement cannot vanish with
|
|
* the suite it guards.
|
|
*/
|
|
export function fishRequirementViolation(
|
|
lookup: FishBinaryLookup,
|
|
env: NodeJS.ProcessEnv = process.env
|
|
): string | null {
|
|
if (lookup.available || env[REQUIRE_FISH_ENV_VAR] !== '1') {
|
|
return null
|
|
}
|
|
return `${REQUIRE_FISH_ENV_VAR}=1 but the live fish tests would skip: ${lookup.reason}`
|
|
}
|