Files
orca/src/preload/api/preflight-api.ts
T
Neil 69839c253e feat(zcode): explain a ZCode build that has no terminal UI (#22730)
* 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.
2026-09-25 02:28:40 -07:00

62 lines
2.4 KiB
TypeScript

import type { ZCodeInteractiveCapability } from '../../shared/zcode-missing-tui'
import type { ProjectExecutionRuntimeResolution } from '../../shared/project-execution-runtime'
import type {
PathSource,
ShellHydrationFailureReason
} from '../../shared/shell-path-hydration-types'
export type PreflightStatus = {
git: { installed: boolean }
gh: { installed: boolean; authenticated: boolean }
/** Optional — older preload payloads predating GitLab support omit it; consumers gate on `glab?.installed`. */
glab?: { installed: boolean; authenticated: boolean }
bitbucket?: { configured: boolean; authenticated: boolean; account: string | null }
azureDevOps?: {
configured: boolean
authenticated: boolean
account: string | null
baseUrl: string | null
tokenConfigured: boolean
}
gitea?: {
configured: boolean
authenticated: boolean
account: string | null
baseUrl: string | null
tokenConfigured: boolean
}
}
export type RefreshAgentsResult = {
agents: string[]
addedPathSegments: string[]
shellHydrationOk: boolean
/** Drives agent_picks `on_path:false` triage (dashboard 1562016). `'shell_hydrate'` = detection saw the user's
* full shell PATH; `'sync_seed_only'` = hydration failed and detection ran against the `patchPackagedProcessPath` seed list. */
pathSource: PathSource
/** Classified hydration outcome: `'none'` on success, else a failure mode when `shellHydrationOk` is false. */
pathFailureReason: ShellHydrationFailureReason
}
export type PreflightRuntimeContext = {
wslDistro?: string | null
wslDefault?: boolean
projectRuntime?: ProjectExecutionRuntimeResolution
}
export type PreflightApi = {
check: (args?: PreflightRuntimeContext & { force?: boolean }) => Promise<PreflightStatus>
detectAgents: (args?: PreflightRuntimeContext) => Promise<string[]>
/** Whether the installed `zcode` can open a session; cached in main per run. */
zcodeInteractiveCapability: () => Promise<ZCodeInteractiveCapability>
refreshAgents: (args?: PreflightRuntimeContext) => Promise<RefreshAgentsResult>
detectRemoteAgents: (args: { connectionId: string }) => Promise<string[]>
detectRemoteWindowsTerminalCapabilities: (args: { connectionId: string }) => Promise<{
wslAvailable: boolean
wslDistros: string[]
pwshAvailable: boolean
gitBashAvailable: boolean
hostPlatform: NodeJS.Platform | null
}>
}