Skip to content

feat(zcode): explain a ZCode build that has no terminal UI - #22730

Merged
nwparker merged 3 commits into
mainfrom
nwparker/zcode-missing-tui-notice
Sep 25, 2026
Merged

nwparker merged 3 commits into
mainfrom
nwparker/zcode-missing-tui-notice

Conversation

@nwparker

@nwparker nwparker commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 4 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​75 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​75
Prod 14 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​226 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​226

ELI5

ZCode has two front ends that share one brain: a desktop app that draws its own window, and a zcode terminal command that draws a text UI. The desktop app ships the brain without the text UI, because it never needs one.

If that's the zcode on your PATH, everything looks fine — it prints its version, runs one-shot prompts, passes its own doctor check — right up until Orca asks it for a session, which is the one thing it can't do. You get a Node stack trace in the pane and it looks like Orca is broken.

This makes Orca recognise that specific failure and say what's actually wrong.

What Changed

Before: launching ZCode with the desktop app's bundled runtime left a dead pane containing:

Error: Cannot find package '@zcode/tui' imported from /Applications/ZCode.app/Contents/Resources/glm/zcode.cjs

Orca's own signals all said success — agent detected, hooks installed (enabled: true, all seven events) — so the only reasonable read was "Orca's ZCode integration is broken."

After: the pane's first output is matched and the user gets a notice instead:

This ZCode build has no terminal UI
Orca's hooks installed correctly — the zcode on your PATH just cannot open a session. The ZCode desktop app bundles the agent runtime without its terminal UI. Install a zcode that ships the TUI, then run zcode outside Orca to confirm.

Mechanism. startZCodeMissingTuiWatcher subscribes to one freshly spawned pane's PTY data via the existing single-PTY sidecar and disposes itself the moment it matches or the budget runs out.

Why

Two decisions worth reviewing:

It keys on the failure, not the success. A build that has the TUI but no TTY prints TUI requires an interactive terminal. — matching that would be the obvious inversion, but ZCode localizes it (TUI 需要交互式终端。 in zh-CN), so the rule would silently miss every non-English user. Node's module-resolution error is not translated and names the package directly, so that's the anchor. Both the ESM (Cannot find package) and CJS (Cannot find module) spellings are covered because which one a build hits depends on how it was bundled.

It is scoped, not global. Matching every chunk of every pane would put a regex on the hottest path in the app. A missing module fails before the runtime renders anything, so the watcher runs only for a pane Orca launched as zcode, and only across the first 8 KiB. A healthy pane pays nothing after startup.

Alternative considered: draft #16227 approached the same problem by making launchCmd a multi-line /bin/sh -c that branches on zcode --version output. I did not take that route — it keys on the zcode-app-cli version prefix, which is one distribution's package name, so it would reject a first-party CLI built from zai-org/ZCode (that one prints a bare 0.16.9). It is also macOS-only and hardcodes /Applications/ZCode.app. Keying on the failure is distribution-neutral and platform-independent. Credit to @guanbear for identifying the problem first.

Linked Issue

Follow-up to #22464. Reported during live testing by @JWu527 — #22464 (comment)

Visual Proof

The user-visible change is a toast replacing an unexplained stack trace. Reproducing it needs a ZCode desktop install, so the evidence here is the recorded PTY capture of the real failure plus tests pinned to it, rather than a staged screenshot:

src/main/runtime/__fixtures__/zcode-missing-tui.txt (112 bytes, exitCode: 1), captured with config/scripts/capture-agent-pty-transcript.mjs per docs/reference/agent-pty-transcript-capture.md; scrub check clean.

Measured on this machine, both builds present:

Build zcode with stdin closed ms
Desktop bundle (no TUI) Cannot find package '@zcode/tui' ~438
Built from zai-org/ZCode (has TUI) TUI requires an interactive terminal. ~722

Deterministic 3/3 each way. --version and zcode doctor are identical in shape between the two, which is why neither works as a probe and why the rule reads the startup failure instead.

Testing

pnpm tc clean. Changed-code quality gate 0 findings. 17 tests across three suites.

  • src/shared/zcode-missing-tui.test.ts — matches the recorded fixture byte for byte and the CJS spelling; does not match the healthy no-TTY message, its zh-CN translation, an unrelated missing package, or prose that merely mentions @zcode/tui.
  • src/renderer/src/components/terminal-pane/zcode-missing-tui-watcher.test.ts — fires once on the fixture, unsubscribes after firing, rejoins the error when it straddles a chunk boundary, stays silent for a healthy pane and unsubscribes at the byte budget, stays silent on the no-TTY message, and is safe to dispose twice.

Platforms: developed and measured on macOS with both builds installed. The rule itself is platform-independent (no paths, no shell), but it is untested on a Windows or Linux ZCode install.

  • I manually tested these changes locally
  • Automated tests added/updated

AI Disclosure

Implemented with Claude (Opus 5) via Claude Code.

Agent skill upstream boundary

  • Not applicable — no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation copied or translated.

Notes

  • Stacked on feat(agents): add first-class ZCode harness #22464 — it needs zcode to exist as a TuiAgent. Merge that first.
  • Performance: one subscription per launched ZCode pane, released on match or after 8 KiB. Nothing added to the shared PTY data path.
  • Security / cross-platform: no new process spawn, no new file read, no shell. Pure string matching on output Orca already receives.
  • i18n: both strings added to all six locales.
  • There is a companion docs section on the supported-agents page (shipped in feat(agents): add first-class ZCode harness #22464) for anyone who hits this before updating.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • Visual/behavioural evidence attached, or reasoned substitute
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered
  • pnpm typecheck, tests, and changed-code quality gate pass locally

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

verify:localization-runtime-catalog fails on this head, so the pnpm lint gate blocks the merge. One root cause, detailed inline.

Reviewed changes

  • Failure rule — isZCodeMissingTuiOutput keys on Node's un-localized module-resolution error (Cannot find package|module '@zcode/tui') instead of ZCode's translated no-TTY message, so it works across locales.
  • Bounded watcher — one subscribeToPtyData sidecar subscription per freshly spawned zcode pane, scanning the first 8 KiB with a chunk-boundary carry, self-disposing on match or budget.
  • Wiring — started only when the pane's launchAgent is zcode, replacing any prior subscription on re-spawn.
  • Copy + evidence — toast strings added to all six locales; recorded PTY fixture plus unit tests pinned to it.

ℹ️ Nitpicks

  • No test exercises the launchAgent === 'zcode' gate in fresh-spawn-start.ts. The watcher tests mock subscribeToPtyData, so a regression that starts the watcher for every pane (defeating the "nothing on the shared path" claim) would still pass; an assertion that a non-zcode spawn registers no sidecar would pin it.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

id: 'zcode-missing-tui',
description: translate(
'auto.components.terminal.pane.zcode.missing.tui.description',
"Orca's hooks installed correctly — the `zcode` on your PATH just cannot open a session. The ZCode desktop app bundles the agent runtime without its terminal UI. Install a `zcode` that ships the TUI, then run `zcode` outside Orca to confirm."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This inline default wraps zcode in backticks, but the en.json value for the same key does not, so i18next can no longer rebuild the description from the call-site default. verify:localization-runtime-catalog therefore requires the entry to ship in en-runtime-required.json — which this PR does not add, so pnpm lint fails (verified on this head).

Technical details
# Runtime English catalog gate fails on the mismatched description

## Affected sites
- `src/renderer/src/components/terminal-pane/zcode-missing-tui-watcher.ts:67-70` — fallback has `` `zcode` ``.
- `src/renderer/src/i18n/locales/en.json:3260` — catalog value has `zcode` with no backticks.
- `src/renderer/src/i18n/en-runtime-required.json` — missing `auto.components.terminal.pane.zcode.missing.tui.description`.

## Evidence
`node config/scripts/generate-runtime-required-english-catalog.mjs` exits 1 with:

```
Entries i18next cannot rebuild from a call site default, but that are not shipped:
  auto.components.terminal.pane.zcode.missing.tui.description
```

`collectRuntimeRequiredKeys` marks a key required when a literal call-site default differs from the catalog value, because the catalog value is what ships today. Since `en.json` is the translator source and the non-English catalogs carry the no-backtick text, English currently renders the backticked fallback while every other locale renders the catalog text.

## Required outcome
English and the five locale catalogs agree, and `pnpm run verify:localization-runtime-catalog` exits 0.

## Suggested approach
Run `pnpm run sync:localization-runtime-catalog` to ship the `en.json` value in `en-runtime-required.json` (recommended, keeps the fallback/catalog distinction intentional), or remove the backticks from the inline fallback so the entry is no longer required.

// in its first chunk, so the scan is bounded to one freshly launched pane's startup
// instead of every chunk of every pane. See zcode-missing-tui-watcher.
session.disposeZCodeMissingTuiWatcher?.()
session.disposeZCodeMissingTuiWatcher = startZCodeMissingTuiWatcher(resolvedPtyId)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The returned disposer is stored on the session, but session-reconcile-dispose.ts dispose() never calls it. A zcode pane that is torn down before a match or before the 8 KiB budget leaves its subscribeToPtyData sidecar and the ref-counted delivery-interest hold registered for that pty id.

Technical details
# ZCode watcher subscription is not released on session dispose

## Affected sites
- `src/renderer/src/components/terminal-pane/pty-connection/fresh-spawn-start.ts:144-145` — stores `session.disposeZCodeMissingTuiWatcher`, only ever called here (on re-spawn).
- `src/renderer/src/components/terminal-pane/pty-connection/session-reconcile-dispose.ts:177-312` — `dispose()` releases every other sidecar/timer but not this one.

## Required outcome
Disposing a pane releases the watcher (`session.disposeZCodeMissingTuiWatcher?.()`), so `subscribeToPtyData`'s unsubscribe runs and `acquirePtyDeliveryInterest` is released even when no match or budget was reached.

## Why it matters
`subscribeToPtyData` acquires a delivery-interest hold; without the unsubscribe, `setPtyDeliveryInterest(ptyId, false)` is never sent, so main keeps delivering hidden bytes for that (reusable) pty id. Severity is low — the match path and the 8 KiB budget dispose the watcher in the common case — but the gap is unbounded per disposed zcode pane.

@nwparker
nwparker marked this pull request as draft September 25, 2026 01:30
@nwparker

Copy link
Copy Markdown
Contributor Author

Moved to draft — the visual proof shows this does not work

I set out to attach before/after screenshots and they disproved the change. Posting them rather than quietly fixing, because the negative result is the useful part.

Setup: real ZCode desktop bundle (/Applications/ZCode.app/.../glm/zcode.cjs) on PATH as zcode, Orca launched from each branch in turn, same repo, same steps — create a workspace with the ZCode agent.

Before (nwparker/harness-zcode) — the trap, exactly as reported:

before.png

After (this branch) — identical. No notice:

after.png

Why it doesn't fire

I instrumented the wiring and the watcher with temporary console.logs and reran the whole flow. Neither probe produced a single line, so the .then() block I hooked in fresh-spawn-start.ts never executes for this pane. My integration point is simply wrong — the pane reaches a live PTY by a different route.

The unit tests pass because they call startZCodeMissingTuiWatcher directly with a mocked subscribeToPtyData. They prove the watcher and the classifier are correct; they prove nothing about whether anything ever calls the watcher. That gap is on me.

What is still good

  • src/shared/zcode-missing-tui.ts and its tests are sound and transcript-backed. The classifier correctly matches the recorded failure and correctly rejects the healthy no-TTY message and its zh-CN translation.
  • The measurements stand: --version and zcode doctor cannot distinguish the two builds; only the startup failure can.
  • The recorded fixture is real evidence and worth keeping either way.

What needs to change

The watcher has to attach somewhere every pane actually passes through. Two leads:

  1. ipc-pty-session-handlers.ts:127 registers ptyDataHandlers for every pane regardless of connect path — but it has no agent context, so the ZCode scoping would need to come from elsewhere.
  2. That same function calls drainPreHandlerPtyData, i.e. data arriving before a handler registers is buffered and replayed for the primary handler. Sidecars get no such replay. Since this failure lands ~438ms after spawn, a late sidecar may miss it entirely even once attached at the right place.

Lead 2 suggests the sidecar approach may be wrong in principle here, not just misplaced.

Draft until it demonstrably fires on a real pane, with a screenshot that differs from the before.

#22464 is unaffected — it is green, rebased, and independently confirmed on a live account.

@nwparker

Copy link
Copy Markdown
Contributor Author

There is a smarter way, and the source explains why it is sound

Pushed d4d7cca — the stream watcher is gone, replaced by a direct probe. Reading zai-org/ZCode turned this from a heuristic into a structural guarantee.

Why nothing cheaper works

--version and doctor cannot tell the builds apart, and now I know it is by construction rather than by accident: the TUI is only ever touched on the tui command path. run.ts routes doctor, login, plugins, skills and the rest without going near it, so every one of them succeeds identically on a build that has no terminal UI at all. doctor --json does expose runtime.sea, but that only separates SEA from node-bundle — not TUI-present from TUI-absent.

Why the probe's answer is unambiguous

Two facts from the source combine:

  1. tui-command.ts:28 — runTuiCommand calls loadTuiRuntime() before anything else.
  2. The TTY check lives inside runTui, i.e. after that module has already loaded.

So with stdin at EOF the two outcomes cannot overlap:

Build What happens Output
No TUI fails inside loadTuiRuntime Cannot find package '@zcode/tui'
Has TUI loads, then declines the missing TTY TUI requires an interactive terminal.

The module error is present exactly when the terminal UI is absent — ordering guarantees it.

Why it holds for every distribution

build.mjs:14 marks @zcode/tui an esbuild external, so it is never inlined into zcode.cjs — a node-bundle install must resolve it as a real package. build-sea.mjs calls collectSeaTuiAssets unconditionally, so a SEA build always embeds it and tui-runtime-loader.ts extracts it from the SEA archive. The desktop app's bundle has neither path available. All three shapes classify correctly, with no knowledge of any particular distribution's package name.

Verified against real builds, not mocks

interactive-capability.live.test.ts runs the probe against both ZCode builds installed on this machine:

Build Verdict
Desktop app bundled runtime missing-tui
CLI built from zai-org/ZCode interactive
Command that does not exist unknown (fails open)

All three in 1.6s. It skips automatically where those builds are absent.

Why this design and not the last one

The previous attempt lost a race it could not win: the failure lands ~440ms after spawn, and a sidecar subscriber gets no replay of data that arrived before it attached. Asking the question directly removes the race entirely, answers once per run, caches only definitive answers, and — the real win — can run before Orca opens a pane, so the user need never see a dead terminal.

Still draft: the probe is proven, but nothing calls it yet. The remaining work is surfacing, which is now a small, race-free wiring job rather than a hunt through the connection state machine.

@nwparker
nwparker marked this pull request as ready for review September 25, 2026 08:58
@nwparker

Copy link
Copy Markdown
Contributor Author

It fires now — out of draft

e1162e39 wires the probe to terminal tab creation, which is where a ZCode launch is first known and runs before the pane connects. Verified in the running app against the real desktop bundle:

after-notice.png

Same failure as before, but now with an explanation beside it instead of a bare stack trace.

Compare against the earlier attempt on this PR, where before and after were pixel-identical. The difference is the mechanism: that one watched a live stream and lost a race it could not win; this one asks the question directly, before the terminal exists.

A caught mistake worth recording

My first run of this verification launched Claude, not ZCode — the agent picker had reverted to its default and I did not check. The pane failed differently and there was no notice, which I could easily have read as "still broken". I redid it asserting the picker reads ZCode before creating the workspace. Worth flagging for anyone reproducing: confirm the agent actually took.

Final shape

  • src/shared/zcode-missing-tui.ts — classifier + capability type, keyed on Node's untranslated module error
  • src/main/zcode/interactive-capability.ts — cached probe over the sanctioned runProcess, fails open
  • preflight:zcodeInteractiveCapability — IPC beside the other CLI-capability answers; the web stub returns unknown, since a paired client should not judge the host's install
  • src/renderer/src/components/terminal-pane/zcode-missing-tui-notice.ts — advisory toast, never blocks a launch

pnpm tc clean, 60 tests passing across the touched trees, changed-code quality gate 0 findings. Still stacked on #22464.

@nwparker
nwparker force-pushed the nwparker/harness-zcode branch from 53e9c6f to 67ec117 Compare September 25, 2026 09:02
@nwparker
nwparker force-pushed the nwparker/zcode-missing-tui-notice branch from e1162e3 to 68ac9d4 Compare September 25, 2026 09:04

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The probe always asks the machine running Orca's main process, so a ZCode pane on an SSH or WSL worktree is judged by the wrong zcode. Two inline notes below; the PR body also still describes the earlier watcher implementation.

Reviewed changes

  • Capability probe (src/main/zcode/interactive-capability.ts) — spawns the installed zcode with no args and empty stdin, classifies output as missing-tui / interactive / unknown, and caches a definitive answer for the run.
  • Failure rule (src/shared/zcode-missing-tui.ts) — regex anchored on Node's un-localized module-resolution error, plus a recorded PTY fixture.
  • Wiring — preflight:zcodeInteractiveCapability IPC handler, preload bridge, web-host stub returning unknown, and a toast fired from createTab when launchAgent === 'zcode'.
  • i18n — title/description keys added to all six locale catalogs.
  • Tests — shared regex suite (runs) and a live probe suite (skipped unless two machine-specific binaries exist).

ℹ️ The PR description describes an implementation that is no longer in the diff

The body sells startZCodeMissingTuiWatcher, subscribing to PTY data "across the first 8 KiB", and a zcode-missing-tui-watcher.test.ts suite; none of those exist at head e1162e39. What shipped is a preflight probe plus an on-demand toast, and the testing section's "17 tests across three suites" is actually 9 tests across 2 suites (3 of them skipped). The rationale a reviewer is being asked to evaluate ("keys on the failure, not the success") still holds, but the mechanism paragraphs and test inventory do not.

Technical details
# Stale PR body

## Affected sites
- PR #22730 body — "Mechanism", "It is scoped, not global", and "Testing" sections all
  reference the removed watcher implementation.

## Required outcome
- Rewrite the mechanism and testing sections against `e1162e39` so a reviewer evaluates the
  probe (and its local-vs-remote and command-resolution tradeoffs) rather than the watcher.

## Suggested approach (optional)
- Keep the design rationale on why the rule keys on the module error; replace the watcher
  paragraph with the probe's spawn/classify/cache flow, and correct the test counts.

ℹ️ Nitpicks

  • The live suite is skipped in CI (bothPresent needs two /tmp/zcode-bin-* paths), so the probe's classify and timeout branches have no automated coverage; only the regex is pinned. Mocking runProcess would let a unit test exercise probe directly.
  • _resetZCodeInteractiveCapabilityForTests is exported from a production module (an established pattern in this repo), so no action needed.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@@ -133,6 +134,11 @@ export function createTerminalTabCreationActions(
// Why: mark click-caused (not work-caused) spawns so updateTabPtyId skips the activity/sortEpoch bump that would reorder Recent/Smart on click.
...(options?.pendingActivationSpawn ? { pendingActivationSpawn: true } : {})
}
if (options?.launchAgent === 'zcode') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createTab already knows the pane's execution host — remoteConnectionId and isWslWorktree are computed at lines 99-101 — but warnIfZCodeCannotOpenSession() probes the machine running Orca's main process regardless. On an SSH or WSL worktree this shows a false notice when only the local binary lacks the TUI, and silently misses the real failure when only the remote one does. At minimum, skip the local probe for remote/WSL panes; the better fix is to ask the execution host (the same way detectRemoteAgents/detectWslCommandsOnPath do).

Suggested change
if (options?.launchAgent === 'zcode') {
if (options?.launchAgent === 'zcode' && !isRemoteWorktree && !isWslWorktree) {


/** Whether this `zcode` can open a session. Answered once per Orca run. */
export function readZCodeInteractiveCapability(
command = 'zcode'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The probe always spawns bare zcode, so a user whose agentCmdOverrides.zcode points at a working binary is still judged by whatever zcode is on PATH, and an install resolvable only via detectCommandsInInstallDirs (as detectInstalledAgents handles) is missed. The command parameter also is not part of the cache key at line 71, so a non-default call would poison the run's cached verdict. Resolve the command the pane will actually launch and key the cache by it.

Suggested change
command = 'zcode'
export function readZCodeInteractiveCapability(
command = 'zcode'
): Promise<ZCodeInteractiveCapability> {

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new code changes — the branch was rebased onto the advanced stacked base (e1162e39 → 68ac9d4e), and a diff of the two PR diffs shows only locale-file line-number shifts. The prior review's findings still stand.

Reviewed changes (delta since review 5315767283)

  • Rebase only — the three commits were re-created (df92fe83, 3049a5c8, 68ac9d4e); all 18 files and the +226/−0 prod / +75/−0 test counts are identical, and the changed hunks are byte-for-byte the same apart from shifted en/es/fr/ja/ko/zh.json line numbers from the base advance.
  • The local-host probe and agentCmdOverrides/cache-key concerns from the first review are unaffected by the rebase.

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@nwparker
nwparker changed the base branch from nwparker/harness-zcode to main September 25, 2026 09:18
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
…hing 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.
…ssion

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.
@nwparker
nwparker force-pushed the nwparker/zcode-missing-tui-notice branch from 68ac9d4 to 65d4fe0 Compare September 25, 2026 09:19
@coderabbitai

coderabbitai Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nwparker
nwparker merged commit 69839c2 into main Sep 25, 2026
30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant