mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +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>
135 lines
4.4 KiB
TypeScript
135 lines
4.4 KiB
TypeScript
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
import { __resetShellStartupEnvCache } from '../main/pty/shell-startup-env'
|
|
import { resolveOpenCodeSourceConfigDir, resolvePiSourceAgentDir } from './plugin-overlay-env'
|
|
|
|
describe('plugin overlay env source resolution', () => {
|
|
let homeDir: string
|
|
|
|
beforeEach(() => {
|
|
homeDir = mkdtempSync(join(tmpdir(), 'relay-plugin-overlay-env-'))
|
|
__resetShellStartupEnvCache()
|
|
})
|
|
|
|
afterEach(() => {
|
|
rmSync(homeDir, { recursive: true, force: true })
|
|
__resetShellStartupEnvCache()
|
|
})
|
|
|
|
it.skipIf(process.platform === 'win32')(
|
|
'uses zsh startup exports before inherited public overlay env',
|
|
() => {
|
|
mkdirSync(join(homeDir, 'company-opencode'), { recursive: true })
|
|
mkdirSync(join(homeDir, 'company-pi'), { recursive: true })
|
|
writeFileSync(
|
|
join(homeDir, '.zshrc'),
|
|
[
|
|
'export OPENCODE_CONFIG_DIR="$HOME/company-opencode"',
|
|
'export PI_CODING_AGENT_DIR="$HOME/company-pi"'
|
|
].join('\n')
|
|
)
|
|
|
|
const env = {
|
|
HOME: homeDir,
|
|
OPENCODE_CONFIG_DIR: '/tmp/inherited-opencode-overlay',
|
|
PI_CODING_AGENT_DIR: '/tmp/inherited-pi-overlay'
|
|
}
|
|
|
|
expect(resolveOpenCodeSourceConfigDir(env, '/bin/zsh')).toBe(
|
|
join(homeDir, 'company-opencode')
|
|
)
|
|
expect(resolvePiSourceAgentDir(env, '/bin/zsh', 'pi')).toBe(join(homeDir, 'company-pi'))
|
|
}
|
|
)
|
|
|
|
it.skipIf(process.platform === 'win32')(
|
|
'discovers overlay sources from a custom zsh ZDOTDIR',
|
|
() => {
|
|
const zshDir = join(homeDir, '.config', 'zsh')
|
|
mkdirSync(zshDir, { recursive: true })
|
|
writeFileSync(join(homeDir, '.zshenv'), 'export ZDOTDIR="$HOME/.config/zsh"\n')
|
|
writeFileSync(join(zshDir, '.zshrc'), 'export OPENCODE_CONFIG_DIR="$HOME/opencode-src"\n')
|
|
|
|
expect(
|
|
resolveOpenCodeSourceConfigDir(
|
|
{
|
|
HOME: homeDir,
|
|
OPENCODE_CONFIG_DIR: '/tmp/inherited-opencode-overlay'
|
|
},
|
|
'/bin/zsh'
|
|
)
|
|
).toBe(join(homeDir, 'opencode-src'))
|
|
}
|
|
)
|
|
|
|
it('keeps explicit original-source env ahead of startup hints', () => {
|
|
writeFileSync(join(homeDir, '.zshrc'), 'export OPENCODE_CONFIG_DIR="$HOME/company-opencode"\n')
|
|
|
|
expect(
|
|
resolveOpenCodeSourceConfigDir(
|
|
{
|
|
HOME: homeDir,
|
|
ORCA_OPENCODE_SOURCE_CONFIG_DIR: '/remote/original-opencode',
|
|
OPENCODE_CONFIG_DIR: '/tmp/inherited-opencode-overlay'
|
|
},
|
|
'/bin/zsh'
|
|
)
|
|
).toBe('/remote/original-opencode')
|
|
})
|
|
|
|
it.skipIf(process.platform === 'win32')('resolves Prime from its independent env keys', () => {
|
|
writeFileSync(
|
|
join(homeDir, '.zshrc'),
|
|
'export PRIME_AGENT_CODING_AGENT_DIR="$HOME/company-prime"\n'
|
|
)
|
|
|
|
expect(
|
|
resolvePiSourceAgentDir(
|
|
{ HOME: homeDir, PRIME_AGENT_CODING_AGENT_DIR: '/tmp/inherited-prime' },
|
|
'/bin/zsh',
|
|
'prime-agent'
|
|
)
|
|
).toBe(join(homeDir, 'company-prime'))
|
|
expect(
|
|
resolvePiSourceAgentDir(
|
|
{
|
|
HOME: homeDir,
|
|
ORCA_PRIME_AGENT_SOURCE_AGENT_DIR: '/remote/original-prime',
|
|
PRIME_AGENT_CODING_AGENT_DIR: '/tmp/inherited-prime'
|
|
},
|
|
'/bin/zsh',
|
|
'prime-agent'
|
|
)
|
|
).toBe('/remote/original-prime')
|
|
})
|
|
|
|
// Why: the session env is the only place a fish user's XDG_CONFIG_HOME shows up
|
|
// (config.fish exports it, so no GUI-launched process inherits it). Dropping it
|
|
// here would scan ~/.config and disagree with the same lookup on the main side.
|
|
it.skipIf(process.platform === 'win32')(
|
|
'reads fish config under the session XDG_CONFIG_HOME',
|
|
() => {
|
|
const configHome = join(homeDir, 'xdg')
|
|
mkdirSync(join(configHome, 'fish'), { recursive: true })
|
|
writeFileSync(
|
|
join(configHome, 'fish', 'config.fish'),
|
|
'set -gx OPENCODE_CONFIG_DIR "$HOME/company-opencode"\n'
|
|
)
|
|
mkdirSync(join(homeDir, '.config', 'fish'), { recursive: true })
|
|
writeFileSync(
|
|
join(homeDir, '.config', 'fish', 'config.fish'),
|
|
'set -gx OPENCODE_CONFIG_DIR /wrong-default-config-home\n'
|
|
)
|
|
|
|
expect(
|
|
resolveOpenCodeSourceConfigDir(
|
|
{ HOME: homeDir, XDG_CONFIG_HOME: configHome },
|
|
'/opt/homebrew/bin/fish'
|
|
)
|
|
).toBe(join(homeDir, 'company-opencode'))
|
|
}
|
|
)
|
|
})
|