Skip to content

Commit a9a669e

Browse files
committed
docs(tui): verify TUI.md against the terminal UI as it behaves
1 parent 9f37404 commit a9a669e

2 files changed

Lines changed: 25 additions & 15 deletions

File tree

‎docs/ARCHITECTURE.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,11 @@ In TUI chat mode there is no completion gate — the session stays open across t
7676
- Both settings files and the project/global grant store (`.corbits/permissions.json`) are on the secret-guard denylist for path-keyed tools, so the agent cannot `read_file` its own credentials or persist standing auto-approvals. Shell commands that reference them still require explicit operator approval.
7777
- Credential-surface ownership: each auth store module enumerates its own files (`*_AUTH_FILENAME` / `MCP_AUTH_DIRNAME`), the data-only registry in `src/auth/credential-surface.ts` turns them into denylist patterns, `secret-guard-plugin.ts` owns matching (lexical plus realpath), and `@mention` resolution consumes the resolved check — never the registry directly. A new `*-auth.json` token store is denied only once its store module exports its filename and the registry lists it; the coverage test scans store sources for `*-auth.json` literals (registered dirnames get an includes-check instead) and fails the build until both exist.
7878

79-
### TUI Runner (`src/tui/runner.ts`)
79+
### TUI Runner (`src/tui/runner/`)
8080

8181
- Builds a chat-mode agent using the `ChatDirector`
8282
- Wires `ask_operator` to an operator-gate event resolved by a modal
83-
- Mounts the OpenTUI host via `mountRunnerHost` (`src/tui/runner-host.ts`), which mounts `mountProductHost` (`src/tui/product-host.ts`) over the shell (`src/tui/shell.ts`)
83+
- Mounts the OpenTUI host via `mountRunnerHost` (`src/tui/runner/host.ts`), which mounts `mountProductHost` (`src/tui/product-host.ts`) over the shell (`src/tui/shell/`)
8484
- Bridges reactor events to the OpenTUI host via a plain `EventEmitter`
8585
- **Mid-run injection** — Shell `session-queue` items drain at the parent
8686
`tool.boundary` through `SessionPort.deliver`. Production `routeQueuedDelivery`
@@ -314,7 +314,7 @@ Every shipped specialist is a **director package** — a prompt-first `DirectorP
314314

315315
**Wait mailbox** (`src/subagent/agent-fleet.ts` `FleetMailbox`): per-install overlay over that session store. Wait JSON is a projection of stored lifecycle plus mailbox membership, pin, collected, and optional interrupt override — not a second terminal store. Mailbox `register` pins an uncollected result (honored by prune); past `MAX_FLEET_RECORDS` the oldest never-collected pin is compacted to a tombstone. Operator cancel projects wait status `interrupted`.
316316

317-
**Observe (OpenTUI)**: `shell.ts:enterSubagentObserve` swaps the transcript for a child's stream (live while running, historical when done) without stealing the parent reactor; child events are mapped to stream rows by `src/tui/observe-map.ts`. Esc leaves observe and restores the parent transcript. Parent Esc/stop and `/clear` still call `cancelAll` so live children close (`agent.close`) instead of continuing after the parent stops. The host-injection point that resolves a live session (`onObserveRequest` → `observeSessionFromSubAgents`, `src/tui/runner-host.ts`, picking the newest running child else the most recent session of any status) is triggered by Alt+O (`shell.ts:observeActiveSubagent`) — the command palette action that used to call it is gone along with `src/tui/palette.ts` itself, but the chord replaces it rather than dropping the feature.
317+
**Observe (OpenTUI)**: `shell/observe.ts:enterSubagentObserve` swaps the transcript for a child's stream (live while running, historical when done) without stealing the parent reactor; child events are mapped to stream rows by `src/tui/observe-map.ts`. Esc leaves observe and restores the parent transcript. Parent Esc/stop and `/clear` still call `cancelAll` so live children close (`agent.close`) instead of continuing after the parent stops. The host-injection point that resolves a live session (`onObserveRequest` → `observeSessionFromSubAgents`, `src/tui/runner-host.ts`, picking the newest running child else the most recent session of any status) is triggered by Alt+O (`shell.ts:observeActiveSubagent`) — the command palette action that used to call it is gone along with `src/tui/shell/palette.ts` itself, but the chord replaces it rather than dropping the feature.
318318

319319
Data-only agent plugins (`src/plugins/data-only-agent.ts`) synthesize `agentPlugin.agents[]` from `agents/*.md` or flat `*.md` in the plugin directory, with optional co-located `skills/`. `loadPluginEntry` tries JS entrypoints first, then falls back to this layout (`/plugins` add-by-path supports filesystem completion via `listPathSuggestions`).
320320

@@ -413,7 +413,7 @@ A queued gate's display-dependent timers (auto-deny timeout, tool-budget pause c
413413

414414
### TUI (`src/tui/`)
415415

416-
OpenTUI (`@opentui/core`) is the shipping shell; the Ink/React tree has been deleted from the repo. The runner (`src/tui/runner.ts`) mounts the host via `mountRunnerHost` (`src/tui/runner-host.ts`), which mounts `mountProductHost` (`src/tui/product-host.ts`) over the shell (`src/tui/shell.ts`).
416+
OpenTUI (`@opentui/core`) is the shipping shell; the Ink/React tree has been deleted from the repo. The runner (`src/tui/runner/`) mounts the host via `mountRunnerHost` (`src/tui/runner/host.ts`), which mounts `mountProductHost` (`src/tui/product-host.ts`) over the shell (`src/tui/shell/`).
417417

418418
- **Shell** (`shell.ts`) — Owns the transcript window, header, status line, prompt, overlay/palette stack, and layout/relayout (`applyLayout`, `relayout`). Transcript rows are appended via `appendStreamRow`/`appendObserveStreamRow`; focus moves between prompt and transcript via `applyFocus`/`toggleShellFocus`.
419419
- **Product host** (`product-host.ts`) — Creates the `CliRenderer`, wires the event emitter bridge, model/command catalogs, and chrome pushes.

‎docs/TUI.md‎

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22

33
This is the normative behavior spec for the terminal UI: Corbits Code, built on
44
OpenTUI (`@opentui/core`). It describes what the shell must do, not how the
5-
OpenTUI cutover got here. The implementation lives in `src/tui/`; the
6-
runner that mounts it is `src/tui/runner.ts` (see `docs/ARCHITECTURE.md` for
7-
how the TUI fits the rest of the system). A reviewer should be able to hold a
5+
OpenTUI cutover got here. The implementation lives in `src/tui/`; the runner that mounts it is `mountRunnerHost` in `src/tui/runner/host.ts` (see `docs/ARCHITECTURE.md` for how the TUI fits the rest of the system). A reviewer should be able to hold a
86
PR against this document; someone building a new overlay or picker should be
97
able to build it correctly from this document alone.
108

@@ -28,8 +26,7 @@ A single **geometry resolver** turns terminal size, zone visibility, and
2826
overlay mode into region rects; every zone reads its rect from that resolver
2927
instead of computing its own height from `process.stdout.rows`. On an 80×24
3028
terminal with nothing optional showing, the transcript floor is 12 rows
31-
(`IDLE_TRANSCRIPT_FLOOR`); with an inset overlay open the floor drops to a
32-
proposed 8 rows (`OVERLAY_TRANSCRIPT_FLOOR`) so the log stays glanceable
29+
(`IDLE_TRANSCRIPT_FLOOR`); with an inset overlay open the floor drops to 8 rows (`OVERLAY_TRANSCRIPT_FLOOR`) so the log stays glanceable
3330
underneath a permission prompt. When space is scarce, collapse follows a
3431
fixed order — transient banners first, then settings/plugin notices, then
3532
task/agents strips, then progress, then the prompt itself shrinks one
@@ -337,6 +334,13 @@ a single tally. Errors clip to `OUTCOME_CHARS`/`MAX_UPDATE_CHARS` on the
337334
"one update is one row" rule. `fleetDigest()` is the on-demand counterpart
338335
for `/status` or an operator question mid-run.
339336

337+
### Subagent observe
338+
339+
Alt+O swaps the transcript for a running child's stream without stealing the
340+
parent reactor (`enterSubagentObserve`, `src/tui/shell/observe.ts`); Esc
341+
leaves observe and restores the parent transcript. With no live subagent
342+
session, the chord paints a system row saying so instead of opening anything.
343+
340344
## How pop-ups should feel
341345

342346
A blocking surface (permissions, an operator question, the model/provider
@@ -370,7 +374,7 @@ below the prompt floor (`PROMPT_BASE_ROWS`). An unanswerable approval
370374
deadlocks the session; a cramped prompt does not. An overlay must never paint
371375
past the box it was actually assigned, and its border must always close.
372376

373-
Escape dismisses the open overlay and, for a permission or operator prompt,
377+
Escape dismisses the open overlay, or leaves subagent observe when observe holds the transcript, and, for a permission or operator prompt,
374378
that dismissal **denies** the request rather than leaving it unresolved
375379
(`src/tui/gate-wire.ts`: both `onPermission`'s and `onOperator`'s
376380
`onCancel` handlers resolve the pending promise — as a deny for permissions,
@@ -445,7 +449,7 @@ leading marker column and no per-row kind column; the selected row is marked
445449
by text color only (`paintPaletteList` in `overlay-view.ts`: "the highlighted row
446450
already stands out by sitting under the cursor, so a leading `>` and a grey
447451
block would both be saying the same thing twice"). Rows stay name-only
448-
(`/help`, `/model`); the focused command's registry description paints in the
452+
(`/help`, `/model`) except plugin-supplied commands, which append their origin marker (`/name [bundled]` for bundled repo commands, `/name [<origin>]` otherwise); the focused command's registry description paints in the
449453
shared two-line description zone under the list (`openListOverlay({ describe })`,
450454
`paintDescriptionZone` in `overlay-view.ts`). A missing or blank `description` still
451455
reserves the zone (rule plus two blank lines); it does not collapse. Built-ins
@@ -469,6 +473,7 @@ lives in the prompt — list chrome is in How selectors should work above.
469473
When the prefix matches nothing, the list stays open and paints a
470474
`(no matches)` row (CL-6699: a close-and-reopen refresh would drain a
471475
queued gate); Enter then dismisses and leaves the prompt as typed (`/z`).
476+
Elsewhere, an empty list paints `(no choices)` inside the body chrome with zero list rows (`OVERLAY_EMPTY_STATE`).
472477
Every entry is backed by the live command registry
473478
(`src/tui/command-catalog.ts:commandItemsFromRegistry`) — there is no
474479
separate palette overlay and no shell-owned action outside the registry. The
@@ -486,14 +491,14 @@ in `SHELL_SHORTCUTS` documents that in place of a dedicated `?` row.
486491
`/plugins` lists every discovered plugin in a flat list. The title line carries
487492
how-to hints (`Esc cancel · Enter toggle · Alt+A add path · Alt+X remove`,
488493
falling back to shorter forms as the terminal narrows). Enter toggles
489-
enablement. Alt+A adds a plugin by path. Alt+X removes a user, project, or
494+
enablement (blocked pre-trust). Alt+C opens credentials, Alt+V verifies, Alt+T trusts, Alt+A adds a plugin by path, Alt+W picks the web provider. Alt+X removes a user, project, or
490495
path plugin (owned user/project installs, including a path-origin plugin
491496
whose directory sits under those roots, confirm before deleting from disk).
492497
Every remove writes `enabled: false` rather than dropping `settings.plugins[id]`
493498
so in-session command gating holds; disk and unique `pluginPaths` entries are
494499
still removed so the plugin is gone after restart. Bundled Corbits plugins
495500
cannot be uninstalled — Alt+X disables them instead and they stay listed.
496-
Claude marketplace installs write `enabled: false` and never delete `~/.claude`.
501+
Plugin rows carry their origin marker (`pluginOriginMarker`): `[bundled]` for repo-origin bundled plugins, `[<origin>]` otherwise. Claude marketplace installs write `enabled: false` and never delete `~/.claude`.
497502

498503
The idle landing paints two doors beside the mark, keys aligned so the
499504
descriptions share a column (`LANDING_HINTS` in `src/tui/landing.ts`): `/`
@@ -558,6 +563,12 @@ pair as the default (global `defaultProvider` + that provider's `defaultModel`
558563
disable, **Alt+R** remove — never bare letters. A remove confirm drops those
559564
manage hints so the footer is the default Esc/Enter pair.
560565

566+
Each row carries its live state (`mcpRowLabel`): `name — connecting`,
567+
`name — connected · N tools`, `name — needs auth`, `name — failed`,
568+
`name — disabled`. Enter authorizes, retries, or re-enables depending on that
569+
state; Alt+A adds, Alt+D disables, Alt+R removes. An empty catalog paints a
570+
`No MCP servers configured` row above `Close mcp`.
571+
561572
The list itself never nests by provider, but connecting a new provider is not
562573
a flat-list row either: the picker used to grow a "connect →" row per
563574
not-yet-configured provider kind, filtered out once that kind had any
@@ -613,8 +624,7 @@ The prompt is a genuine multi-line composing area built on OpenTUI's
613624
`TextareaRenderable` rather than its single-line `InputRenderable`, because
614625
the single-line widget is hard-wired to one row, no wrapping, and strips
615626
newlines (`src/tui/prompt-input.ts`). Enter sends; a literal newline
616-
needs an explicit chord: Ctrl+Enter or Ctrl+J work on every terminal, and
617-
Shift+Enter works too on a terminal that negotiates the kitty keyboard
627+
needs an explicit chord: Ctrl+Enter and Ctrl+J (`linefeed`) are the portable pair — they work on every terminal — while Shift+Enter needs a terminal that negotiates the kitty keyboard
618628
protocol (this app requests it — `useKittyKeyboard` in `product-host.ts`) and
619629
reports the modifier back. A plain terminal sends the same bare `\r` for
620630
Enter and Shift+Enter, so on those Shift+Enter silently does nothing — driven

0 commit comments

Comments
 (0)