mirror of
https://github.com/stablyai/orca.git
synced 2026-10-07 00:02:29 +00:00
* feat(zcode): explain a ZCode build that has no terminal UI ZCode ships one agent runtime behind two front ends. The desktop app bundles it without `@zcode/tui`, because it draws its own window in Electron. Put that bundle on PATH as `zcode` and it answers `--version`, runs `-p` headlessly, and passes `zcode doctor` — so Orca detects it, launches it, and installs hooks against it, all successfully. Only the interactive session fails, leaving a bare Node stack trace in the pane that reads as a broken Orca integration. Watch a freshly launched ZCode pane's first output and replace that with an explanation: Orca's hooks are fine, this `zcode` just cannot open a session, install one that ships the TUI. The rule keys on Node's own module-resolution error rather than on the healthy build's "TUI requires an interactive terminal." message, because ZCode localizes the latter (`TUI 需要交互式终端。` in zh-CN) and matching it would miss every non-English user. Node's error is not translated and names the package. Scoped so it costs a healthy pane nothing: it runs only for a pane Orca launched as `zcode`, and only over the first 8 KiB, because a module-resolution failure happens before the runtime renders anything. Evidence: `src/main/runtime/__fixtures__/zcode-missing-tui.txt`, a recorded PTY capture of the desktop bundle refusing to start, per docs/reference/agent-pty-transcript-capture.md. Reported-by: JWu527 * refactor(zcode): ask the CLI if it can open a session instead of watching for the failure The stream watcher this replaces never fired. Before/after screenshots were identical and instrumentation showed the hook never ran, so the sidecar was both misplaced and racing a failure that lands ~440ms after spawn. Replace it with a direct question, answered once per run and cached. Reading zai-org/ZCode shows why running it is the only way to ask, and why the answer is unambiguous. `--version` and `doctor` are byte-identical in shape between a build that has the terminal UI and one that does not, because the TUI is only ever touched on the `tui` command path. There, `runTuiCommand` calls `loadTuiRuntime()` before anything else, and `runTui` checks for a TTY only after that module is already loaded. So with stdin at EOF: - no TUI -> fails in the loader -> Node's module-resolution error - has TUI -> loads, then declines -> "TUI requires an interactive terminal." The module error is therefore present exactly when the terminal UI is absent. All three shipping shapes land correctly: an npm/node-bundle install resolves `@zcode/tui` as a real package (esbuild marks it external, so it is never inlined), a SEA build always carries it as embedded assets, and the desktop app's bundled runtime carries neither. Verified against both real builds on this machine rather than a mock: the desktop bundle answers `missing-tui`, a CLI built from source answers `interactive`, and a command that does not exist answers `unknown` — the probe fails open so an unrelated spawn failure never accuses a working CLI. * feat(zcode): warn at launch when the installed zcode cannot open a session Wires the capability probe to the one place a ZCode launch is first known: terminal tab creation, which runs before the pane connects, so the explanation reaches the screen alongside the failure rather than after it. - main exposes the cached probe over `preflight:zcodeInteractiveCapability`, beside the other "what can the installed CLIs do" answers - the web preload stub answers `unknown`, because a paired client has no business deciding anything about the host's CLI install - the renderer notice is advisory: a probe that cannot run never blocks a launch Verified in the running app against the real desktop bundle: creating a ZCode workspace now shows "This ZCode build has no terminal UI" next to the stack trace, where before the trace stood alone.
85 lines
3.3 KiB
TypeScript
85 lines
3.3 KiB
TypeScript
import { runProcess } from '../../shared/child-process/run-process'
|
|
import {
|
|
isZCodeMissingTuiOutput,
|
|
type ZCodeInteractiveCapability
|
|
} from '../../shared/zcode-missing-tui'
|
|
|
|
/**
|
|
* Asking a `zcode` build whether it can open a session, before Orca opens a pane for it.
|
|
*
|
|
* Why a deliberate probe and not a rule over the pane's output: the failure lands within
|
|
* about half a second of spawn, so anything watching a live stream races it. Asking the
|
|
* question directly is race-free, answerable once, and can run before the user ever sees a
|
|
* terminal.
|
|
*
|
|
* Why running it is the only way to ask: ZCode's `--version` and `doctor` are identical
|
|
* between a build that has the TUI and one that does not (measured on both), because the
|
|
* TUI is only ever touched on the `tui` command path — `runTuiCommand` calls
|
|
* `loadTuiRuntime()` before anything else, and every other subcommand returns without it.
|
|
*
|
|
* That same ordering is what makes the answer unambiguous. With no TTY:
|
|
* - a build WITHOUT the TUI fails in `loadTuiRuntime` → Node's module-resolution error
|
|
* - a build WITH the TUI loads, then `runTui` rejects the missing TTY
|
|
* so the module error is present exactly when the terminal UI is absent.
|
|
*
|
|
* All three shipping shapes land correctly: an npm/node-bundle install resolves
|
|
* `@zcode/tui` as a real package (it is an esbuild external, never inlined); a SEA build
|
|
* always carries the TUI as embedded assets; and the desktop app's bundled runtime carries
|
|
* neither, which is the case worth catching.
|
|
*/
|
|
export type { ZCodeInteractiveCapability }
|
|
|
|
// Why bounded: the answer arrives in well under a second on both builds measured. A hang
|
|
// means something unexpected, and an unexpected build must not be called broken.
|
|
const PROBE_TIMEOUT_MS = 6_000
|
|
|
|
let cached: ZCodeInteractiveCapability | undefined
|
|
let inFlight: Promise<ZCodeInteractiveCapability> | undefined
|
|
|
|
export function _resetZCodeInteractiveCapabilityForTests(): void {
|
|
cached = undefined
|
|
inFlight = undefined
|
|
}
|
|
|
|
async function probe(command: string): Promise<ZCodeInteractiveCapability> {
|
|
try {
|
|
const result = await runProcess({
|
|
program: command,
|
|
args: [],
|
|
// Why an explicit empty stdin: the child gets a pipe at EOF rather than a terminal, so
|
|
// a build that HAS the TUI declines instead of taking the probe interactive.
|
|
input: '',
|
|
timeoutMs: PROBE_TIMEOUT_MS
|
|
})
|
|
if (result.timedOut) {
|
|
return 'unknown'
|
|
}
|
|
return isZCodeMissingTuiOutput(`${result.stderr}\n${result.stdout}`)
|
|
? 'missing-tui'
|
|
: 'interactive'
|
|
} catch {
|
|
// Why fail open: a spawn that never ran says nothing about the build. Reporting
|
|
// 'missing-tui' here would accuse a perfectly good CLI on an unrelated failure.
|
|
return 'unknown'
|
|
}
|
|
}
|
|
|
|
/** Whether this `zcode` can open a session. Answered once per Orca run. */
|
|
export function readZCodeInteractiveCapability(
|
|
command = 'zcode'
|
|
): Promise<ZCodeInteractiveCapability> {
|
|
if (cached !== undefined) {
|
|
return Promise.resolve(cached)
|
|
}
|
|
inFlight ??= probe(command).then((verdict) => {
|
|
// Why only remember a definitive answer: 'unknown' is usually transient (a timeout, a
|
|
// spawn refused under load), and caching it would suppress the notice for the session.
|
|
if (verdict !== 'unknown') {
|
|
cached = verdict
|
|
}
|
|
inFlight = undefined
|
|
return verdict
|
|
})
|
|
return inFlight
|
|
}
|