Skip to content

Commit f62211d

Browse files
Cut the interactive TUI over to OpenTUI (#333)
* Document TUI layout and scroll platform rebuild plan Capture the OpenTUI foundation plan and product brief with locked interaction contract, branch hard cutover, and constitution scope so dispatch and Linear can share one source of truth. * Write TUI layout constitution and chrome zone registry Lock geometry ownership, residual transcript floor, zone budgets, and the kill list so Platform work has one implementer contract before any OpenTUI shell lands. * Write TUI interaction contract for queue steer and focus Document locked Enter queue, Alt+Enter tool-boundary steer, Ctrl+C interrupt, palette chord, focus tree, and scroll lease so migration implements one input model. * Write Ink freeze policy and overflow ticket triage Restrict Ink to true P0 daily-use patches and route layout or scroll classes into the OpenTUI platform instead of new chrome geometry. * Document OpenTUI migration branch hard cutover Require a single migration branch, full acceptance before merge, and scrap-on-fail so Ink and OpenTUI never ship as dual paint paths. * Record OpenTUI Bun spike go decision and evidence Prove isolated core and Solid install, sticky ScrollBox, focus, and Enter Alt+Enter Ctrl+C key shapes so binding and packaging can proceed. * Record OpenTUI Solid plus core binding decision Choose Solid with core class APIs and keymap for the migration branch, grounded in the Bun spike go evidence and OpenCode peer alignment. * Document OpenTUI install CI and packaging plan Describe macOS and Linux contributor install, optional native packages, and CI matrix gates so packaging is clear before root dependency cutover. * Link product and architecture docs to TUI constitution pack Point operators and implementers from PRODUCT and ARCHITECTURE into the layout constitution, interaction contract, freeze policy, and cutover docs. * Point spike report at the OpenTUI binding ADR Link the go report to the Solid plus core decision so binding evidence and the ADR stay one hop apart for implementers. * Add OpenTUI deps and tui-opentui scaffold Platform kit root for migration/opentui-tui: core, solid, keymap 0.5.1. Ink CLI entry unchanged. * Add zone registry and geometry resolver Pure resolveGeometry for OpenTUI shell: zone budgets, collapse order, transcript floors (idle 12 / inset 8), prompt and overlay caps. * Add focus tree and scroll lease state machine Immutable focus stack with overlay > observe > shell priority and a single scroll lease owner for wheel/page routing. * Add shared list viewport kit Windowing helpers for keep-active-visible lists (permissions, models, settings consumers). * Add OpenTUI headless test harness createHarness / withTestRenderer wrap createTestRenderer with cleanup and named chords for Enter, Alt+Enter, and Ctrl+C. * Add OpenTUI app shell frame Header, sticky transcript ScrollBox, prompt Input, and status bar owned by createAppShell. Geometry resolver and focus lease drive layout and Tab focus. * Prove OpenTUI shell sticky scroll and key chords Integration tests cover follow/pin, focus lease, Tab, Enter/Alt+Enter/Ctrl+C. Wave 2 platform status note and dispatch tree recorded. * Add session queue and stream row models Pure queue/steer/interrupt state machine and role-styled transcript formatters for the Wave 3 product skin. * Skin OpenTUI shell as a fake Corbits session Wire transcript roles, bordered prompt with binding hint, local queue/steer/interrupt keys, and an inset permission overlay on the Wave 2 platform. Demo and headless product-skin tests included. * Wire OpenTUI shell to a fixture-driven session bridge Map reactor-like events into stream rows, route queue/steer/interrupt through a SessionPort, and cover the path with headless fixture tests. Ink entry stays production. * Add primary overlays on the shared OpenTUI list kit Permissions, operator question, and model/provider picker open on the focus stack + list viewport + geometry host, with headless open/navigate/Esc and resize-floor coverage. Ink entry stays production. * Add Wave 6 palette, long-log windowing, chrome zones, and copy Command palette on Ctrl+O (reclaimed from tool-expand), windowed transcript paint past collapse threshold, measured goal/task/agents chrome via the zone registry, and Alt+C keyboard copy for stream rows. * Add Wave 7 residual surfaces, observe, and cutover readiness Settings/help/plugins/resume/mentions share the list kit; subagent observe swaps stream + lease with Esc restore. Acceptance §10 scored and blockers listed without authorizing Ink delete or main merge. * Add Level 1 live session bridges for OpenTUI cutover Wire injectable SessionPort, production stream event map, overlay onAccept callbacks, registry-backed palette catalog, and chrome state helpers so the product host can bind real runner/session APIs without fixtures. * Wire residual overlays for host-injected catalogs and accept actions Settings/help/plugins/resume/mentions openers take optional items, itemIds, and onAccept; fixtures remain defaults when the host omits data. Shell hooks gain residual kind callbacks so product-host can bind toggles and resume. * Add live subagent observe: host-pushed child rows + parent restore appendObserveStreamRow for live child stream while parent appends route to the parent snapshot; leave/Esc restores parent lease. * Let Alt+C open a copy-mode picker instead of a one-shot stub OpenTUI Alt+C now freezes selectable stream targets in the inset overlay (Ink parity), defaults to the last non-system row, and reports via status flash so copy never mutates the transcript. Fixes CL-5415. * Add OpenTUI product host and pure cutover bridges Land mountProductHost plus gate, catalog, history, observe, and chrome helpers with unit tests so production can leave Ink. Runner still mounts Ink; wire next. * Derive the OpenTUI help catalog from the keymap table * Extract gate emitter wiring into a disposable module * Drive subagent observe from host-supplied sessions * Source the help catalog from an OpenTUI-owned binding table * Cover the product host mount lifecycle with headless tests * Add prompt kill ring and yank chords to the OpenTUI shell * Mount the OpenTUI host as the interactive entry path * Port onboarding, session picker, and mode prompt to OpenTUI * Mount slash-command surfaces on the OpenTUI overlay kit * Delete the Ink renderer tree * Remove Ink dependencies and dead renderer modules * Restore command registration, slash dispatch, and telemetry disclosure * Re-score the acceptance corpus against the shipped OpenTUI path * Wire orphaned TUI features into the OpenTUI host Four strands of work that were left unwired by the renderer cutover: Prompt input: Ctrl+P attaches a clipboard image, with an attachment channel threaded shell -> bridge -> port -> runner -> agent so images reach inference as content blocks. Pasted image paths are detected and attached inline. Sent-message recall on Up/Down persists across resumes. Typing @ opens path suggestions and resolves mentions on the send path. Model picker: recents, favorites, provider grouping and the Go-on-Zen billing warning, sourced from real settings and the billing detector. Approval display: chained commands render as numbered segments and long payloads collapse to inspectable placeholders, expandable in the overlay and mirrored untruncated to the transcript. Code-consuming segments are never collapsed. Deletes seven modules the OpenTUI renderer supersedes or makes obsolete: observe-chrome, chrome-zones, copy, stdin-filter, sync-output, osc8 and styled-segment-props. * Render assistant markdown and restore turn progress, quota retry, and stall recovery * Restore MCP result tables, exit command, and the prompt model bar * Render edit tool diffs and install OpenTUI native platform packages * Fix live transcript streaming, overlay row overflow, and slash command entry * Rebuild the TUI as a prompt-first surface on the Corbits palette * Lay out the transcript as bubbles with tool glyphs and collapsed skills * Carry session metadata in the prompt border and rebuild the landing hero * Grow the prompt into a wrapping multi-line composer and make pickers toggle * Collapse tool arguments and reasoning behind one expand key Tool calls show a human summary of their arguments instead of raw JSON — a view tree reads as its shape, a shell call as its command, an edit as its file. Expanding renders the real structure rather than pretty-printed JSON, reusing the view renderer with a palette resolver so it paints in the product's own tones. Reasoning streams as a single windowed line while a turn thinks, then settles to an elapsed-time phrase chosen from a band. Phrases report only how long reasoning took, never what it concluded. Skills, summarized calls and settled reasoning all collapse behind the same expand key, and every collapsed row says it can be opened. Fixes wrapped tool bodies escaping to column 0: continuation lines now wrap to the body column and stay inside the shell gutter. Also drops the mountain glyph from the prompt border. One row has no vertical range for a silhouette — the wide ridge read as a lump and the three-cell token as an anonymous peak — so the slot is the word alone and the mark keeps its full expression on the landing. Adds a bottom margin row, and makes the brand yield rather than starve the workspace path. * Describe what a selection does before it is chosen * Anchor the transcript to the prompt and put row actions behind Alt * Move the running indicator into the border and reduce context to a percent * Summarize tool results and put the cost figure behind a setting * Fold a tool result into its call row and quiet the reasoning line * Keep plugin warnings out of the frame after the rebase * Fold a turn's reasoning into one row and give the exit notice a lifetime * Stop negotiating the kitty keyboard protocol * Negotiate the kitty keyboard protocol the way OpenCode does * Re-enable CI on push and pull request * Humanize MCP tool names and stop the server-name doubling Exa ships tool names like web_search_exa that already carry the server as a suffix, so namespacing them produced 'Exa: web search exa'. Strip a leading or trailing word matching the server before title-casing both halves. * Remove the unused alt-screen helper No callers since the OpenTUI cutover, and it would fight the renderer's own alternate-screen handling if it were ever wired up. * Add Ctrl+Enter as a newline fallback and drop any-motion mouse tracking Shift+Enter is unreportable unless the terminal negotiates the kitty keyboard protocol, so a terminal that does not send a bare CR and the newline is lost. Ctrl+Enter arrives everywhere. Alt+Enter is deliberately not bound: the shell owns it for steering. Nothing reads hover, so any-motion tracking was one report per cell traversed for no consumer. Press and drag reporting stay, which click-to-expand and the transcript drag-scroll both need. * Size the permission overlay to its content and stop headings flashing raw The overlay asked for a share of the terminal rather than a row per option, so a three-choice gate filled the screen and a nine-choice gate was no taller. It now asks for its item count and lets the resolver cap and scroll it. Choices lost their blank spacer row so the list reads as one list. The gate wrote its transcript row at open time, directly above an overlay saying the same thing. That row is now written once the operator decides, and carries the choice. A row ending in a bare '####' is not yet a heading, so the marker painted as literal text until the title arrived and the line re-laid out underneath text already on screen. Streaming rows now withhold a trailing heading-marker-only line. Observe mode focused the prompt while the focus tree said observe owned keys, so keystrokes landed silently in the parent prompt where a stray Enter would have sent them. * Pin CI to the bun version the tests are written against 1.2.19 has no test.serial, so whole integration files failed to load and took their tests with them. * Free every renderable in a discarded subtree instead of only its root OpenTUI releases a renderable's native TextBuffer in its own destroy(), which detaches children without destroying them. Clearing the landing, repainting the transcript window or disposing a shell therefore stranded every descendant's buffer, and a full test run exhausted the native allocator: 32 'Failed to create TextBuffer' errors on a dev machine and 76 renderer-dependent failures on a 2-core CI runner. * Fix the defects the review panel found Ctrl+D quit unconditionally, so deleting a character mid-edit ended the session. It now needs a focused, empty prompt. Esc from transcript browse did nothing; it returns to the prompt. Batched identical tool calls dropped every result after the first, because the coalesced run row cleared its pending flag on the first answer. A run row now tracks what it is still owed. The existing test passed because it drove a different path than the app. The control-character stripper let bidirectional overrides through, so text in the approval overlay could read as one thing and run as another. Model output reached the screen unsanitized, including sequences split across deltas. Width math used UTF-16 code units where columns were required, most visibly in the wrap that lays out what the operator is approving. The Ambiguous-width contract with the renderer is now stated and checked at startup. Onboarding truncated a pasted API key at a thousand characters without saying so. Provider failures rendered as raw SDK strings; they now name the recovery. The stall watchdog said nothing for fifteen minutes. Four emitter channels had no listeners, so lifecycle hooks, subagent progress, MCP status and grant confirmations were silently absent. The standalone binary excluded its own native module and could not start away from a node_modules tree. Paint entry points now stand down once the shell is disposed. A poll or a resolved continuation that outlives the renderer would otherwise write into freed buffers. * Bound ripgrep output, isolate perf spans, and stop resume dropping blocks The grep byte cap never bounded anything: on a breach the handler settled with everything it had accumulated, 153 KB against a 200 byte limit in one measured run. The notice fired, so it read as enforced. Collection now owns the bytes behind three explicit settle points where exactly one wins, and truncates on a line boundary. The perf span test was not order-dependent. Every perf file cleared the store after itself and assumed it began empty, but the store is process-global and one process runs them all. Resumed sessions dropped view, plan and tasks blocks with no marker. The loose history-block shape discarded their payloads before anything could paint them. Replaying all 49 sessions on this machine recovers 6 blocks that previously vanished. * Answer operator questions, and cap search output on hosts without ripgrep A gate arriving while another overlay was open was dropped: the host refuses a second non-palette open, so the handler returned without resolving and the run blocked forever with nothing on screen. Gates now queue and take the host when it frees. An approval and a question in the same turn is enough to hit it. The overlay had no way to type an answer, though the tool has always promised one and OperatorResult already carried it. There is now an answer field, and a question with no options opens straight into it rather than offering a chooser with nothing to choose. The GitHub runner has no ripgrep, so CI exercised the fallback walker for every search test — and the fallback never capped its output. An unbounded grep could reach the model on any host without rg. The cap now belongs to the plugin, and CI installs ripgrep so the real path is covered. * Make the suite state its own preconditions Tests were reading the developer's machine and passing there while failing on a runner. The launch tests resolved configuration from the real global settings — the provider env vars they set are read by nothing — and the theme tests assert hex, which only appears when COLORTERM says truecolor. One test wrote into the real home directory: session state lands under the home root, not the sandboxed cwd, so goal-state left project directories behind on every run. Its home is now an override, matching its sibling in session/. A preload clears the terminal and CORBITS variables so a test that needs one must set it, forces telemetry off so no path can write an installation id into a real settings file, and fails loudly when ripgrep is missing — without it the search tools quietly run their fallback and the suite stays green. Bun snapshots os.homedir() at process start, so assigning process.env.HOME in a test does nothing. Sandboxing goes through the config and cwd flags instead.
1 parent 2228578 commit f62211d

366 files changed

Lines changed: 42546 additions & 30459 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
name: CI
22

3-
# Temporarily disabled: GitHub Actions billing/spending limit blocks runs
4-
# (jobs fail before steps start). Re-enable push/pull_request triggers when
5-
# org billing is fixed.
6-
#
7-
# Previous triggers:
8-
# push:
9-
# branches: [main]
10-
# pull_request:
113
on:
4+
push:
5+
branches: [main]
6+
pull_request:
127
workflow_dispatch:
138

149
jobs:
@@ -26,7 +21,12 @@ jobs:
2621
- name: Setup Bun
2722
uses: oven-sh/setup-bun@v2
2823
with:
29-
bun-version: "1.2.19"
24+
bun-version: "1.3.14"
25+
26+
# The runner image has no ripgrep, so the grep plugin silently exercised
27+
# its fallback walker and left the ripgrep path untested.
28+
- name: Install ripgrep
29+
run: sudo apt-get install -y ripgrep
3030

3131
- name: Install dependencies
3232
run: bun install --frozen-lockfile

briefs/tui-rebuild-opentui.md

Lines changed: 308 additions & 0 deletions
Large diffs are not rendered by default.

bun.lock

Lines changed: 207 additions & 69 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bunfig.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[test]
2+
preload = ["./tests/preload.ts"]
23
pathIgnorePatterns = [
34
"tests/fixtures/**",
45
"eval/tasks/**",

docs/ARCHITECTURE.md

Lines changed: 21 additions & 53 deletions
Large diffs are not rendered by default.

docs/IMPLEMENTATION.md

Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ CLI binary: `corbits` (`./dist/index.js`). Version lives in `package.json` only.
1919
| `@intx/tools-posix` | workspace | `createPosixTools`, `ToolPlugin` middleware — sandboxed shell/file tools |
2020
| `@intx/types` | workspace | Runtime types (`ReactorDirector`, `ReactorState`, `ToolDefinition`, `ToolCall`, `ToolResult`, …) |
2121
| `@intx/storage-isogit` | workspace | Git-backed context persistence |
22+
| `@opentui/core` | 0.5.1 | Terminal UI renderer |
23+
| `@opentui/keymap` | 0.5.1 | OpenTUI keybinding support |
24+
| `@opentui/solid` | 0.5.1 | Solid bindings for OpenTUI |
25+
| `solid-js` | 1.9.14 | Reactive primitives used by the OpenTUI bindings |
2226
| `arktype` | catalog ^2.1.29 | Runtime validation |
2327

2428
Other Interchange workspace packages (`@intx/inference-discovery`, `@intx/mime`, `@intx/log`, `@intx/crypto-node`) are pulled transitively via the above.
@@ -29,11 +33,6 @@ Other Interchange workspace packages (`@intx/inference-discovery`, `@intx/mime`,
2933
|---|---|---|
3034
| `@intx/inference-testing` | workspace | Deterministic agent-loop test harness |
3135
| `@types/bun` | 1.3.9 | Bun types |
32-
| `ink` | ^7.0.4 | Terminal UI framework |
33-
| `react` | ^19.2.6 | TUI component model |
34-
| `@types/react` | ^19.2.15 | React types |
35-
| `ink-testing-library` | ^4.0.0 | TUI test utilities |
36-
| `react-devtools-core` | ^7.0.1 | React devtools |
3736
| `ws` | ^8.21.0 | WebSocket support |
3837
| `typescript` | 5.9.3 | Type checking |
3938
| `typescript-language-server` | ^4.3.4 | TS/JS language server for the `lsp` tool (`bin/check-env` checks for it) |
@@ -117,36 +116,33 @@ src/
117116
file-mutation-lock.ts Serialize mutations per file for verify
118117
lsp-hint-plugin.ts TS/JS LSP setup hint on unavailable server
119118
tui/
120-
app.tsx Root full-screen layout
121-
runner.tsx Chat-mode agent setup + Ink render (alt-screen)
122-
use-stream.ts Event stream → React state (AgentStatus machine)
119+
runner.ts Chat-mode agent setup; mounts the OpenTUI host
120+
onboarding.ts First-run provider setup entry
121+
pick-session.ts Resume picker (via runListModal)
122+
session-mode-prompt.ts Session-mode prompt (via runListModal)
123+
turns-to-blocks.ts Stored turns → typed content blocks (resume hydration)
123124
tool-formatter.ts Human-readable tool args/results
124125
markdown-parser.ts Markdown rendering
125-
keymap-table.ts Keybindings
126126
theme.ts Colors
127127
commands/
128128
registry.ts Extensible slash-command registry
129129
built-in.ts /help, /model, /settings, /permissions, /plugins,
130130
/clear, /new, /mcp (connect providers from /model)
131-
components/
132-
header.tsx, event-log.tsx, chat-input.tsx, status-bar.tsx, task-view.tsx,
133-
at-mention/, operator-modal.tsx, permission-modal.tsx,
134-
permissions-manager.tsx, plugins-manager.tsx, settings-overlay.tsx,
135-
agent-modal.tsx, exit-confirm.tsx, help-overlay.tsx, hook-panel.tsx,
136-
codex-login-modal.tsx, mcp-auth-prompt.tsx,
137-
onboarding-animation.tsx, in-flight-indicator.tsx, modal-stack.tsx
138-
hooks/
139-
use-gates.ts, use-keymap.ts, use-layout-geometry.ts, use-mcp-status.ts,
140-
use-mouse-scroll.ts, use-provider-manager.ts,
141-
use-scroll.ts, use-spinner.ts, use-terminal-size.ts
142-
stdin-filter.ts Strips SGR mouse sequences before Ink parses input
131+
tui-opentui/
132+
shell.ts Transcript, header, status line, prompt, overlays
133+
product-host.ts Creates the CliRenderer, wires the event bridge
134+
runner-host.ts Runner-facing mount: catalogs, chrome, quit key
135+
list-modal.ts Shared list-picker overlay (runListModal)
136+
command-surfaces.ts Slash-command surface routing (openCommandSurface)
137+
command-catalog.ts, model-catalog.ts, chrome-state.ts, palette.ts,
138+
provider-setup.ts Onboarding provider setup flow
143139
docs/
144140
PRODUCT.md, ARCHITECTURE.md, IMPLEMENTATION.md, HOOKS.md, MCP.md, PLUGINS.md
145141
```
146142

147143
### Auto Mode
148144

149-
Auto mode defaults **on** (`config.auto = true` from `loadConfig`; pass `--no-auto` to start off, or `--auto` to force on). SHIFT+TAB (wired through `use-keymap`'s `cycleMode` action to `onToggleAuto` in `src/tui/app.tsx`) toggles it for the rest of the session; enabling prints a one-line envelope reminder via `commandMessage`. The permission gate reads the flag (`getAuto`/`setAuto` in `src/permission/gate.ts`) on the next tool call.
145+
Auto mode defaults **on** (`config.auto = true` from `loadConfig`; pass `--no-auto` to start off, or `--auto` to force on). It is toggled only via those CLI flags — there is currently no in-session key bound to it. The permission gate reads the flag (`getAuto`/`setAuto` in `src/permission/gate.ts`) on the next tool call.
150146

151147
When auto is on, the gate auto-allows workspace file tools in `AUTO_ALLOWED_TOOLS` and any `run_shell` that does not match the auto-shell policy. The policy (`autoShellRuleForCall` / `AUTO_SHELL_RULES` in `src/permission/auto-shell-policy.ts`) peels wrappers via `expandShellSubjects` (`bash`/`sh`/`zsh -c`, `xargs`, transparent prefixes), then applies:
152148

@@ -167,11 +163,11 @@ Plan approval is handled separately by `use-gates` (`pendingPlan`), independent
167163
- **Enter** calls `onInterrupt`. `App.handleInterrupt` calls `requestStop()` synchronously — which calls `sendAbortRef.current.abort()` — before `resolveAtMentions` yields, ensuring the abort signal reaches the in-flight HTTP request before any async work begins.
168164
- **Alt+Enter** calls `onSubmit` immediately, pushing the message onto `pendingQueueRef` for drain at the next `connector.reply`.
169165

170-
Token event batching in `use-stream.ts`: `TOKEN_EVENTS` (`inference.text.delta`, `inference.thinking.delta`, `inference.tool_call.delta`) set `pendingRenderRef.current = true`; a 33ms `setInterval` converts pending flags into `setTick` calls. All other events call `setTick` directly.
166+
`src/tui-opentui/stream-event-map.ts` maps reactor events onto the bridge's inbound events, and `src/tui-opentui/turn-state.ts` tracks the turn's status. `src/tui/turns-to-blocks.ts` hydrates a resumed session's stored turns into the same content blocks.
171167

172168
### @file Mention Resolution
173169

174-
`@<path>` tokens in chat input are resolved to file contents before delivery to the agent. `resolveAtMentions` (in `app.tsx`) scans the submitted text for `@<path>` patterns, resolves each against the workspace (blocking absolute paths, `..` escapes, and sensitive files), and inlines file contents as fenced code blocks or directory summaries. Unresolvable paths pass through as a short inline warning so the agent knows the mention could not be expanded. Limits: 5 mentions per message, 200 kB per file, 400 kB total.
170+
`@-mention` resolution and image paste are not wired on the OpenTUI send path.
175171

176172
## Configuration
177173

@@ -289,7 +285,7 @@ Providers and credentials are read exclusively from settings files: the global `
289285
| `--force` | false | Override an existing run state |
290286
| `--dangerously-skip-permissions` | false | Auto-allow anything not denied by the authorization layer |
291287
| `--auto` | true (default) | Force auto mode on (workspace writes + unconstrained shell without prompts) |
292-
| `--no-auto` | false | Start with auto mode off (ask on every consequential action); SHIFT+TAB still toggles live |
288+
| `--no-auto` | false | Start with auto mode off (ask on every consequential action); no in-session key toggles it |
293289
| `--no-workflow` | false | Deprecated no-op; workflows are manual slash commands only |
294290
| `--help` || Show help |
295291

@@ -345,7 +341,7 @@ session; that tree re-write is inherent to git and left as residual cost.
345341
- `inference.error` / `reactor.error` — parse/inference and fatal errors
346342
- `reactor.done` — loop completion
347343

348-
Mid-run queue steering is entirely in `app.tsx`: `queuedCount` tracks `pendingQueueRef` depth; the input chrome shows `N queued · Enter steer · Alt+Enter queue` while processing.
344+
Mid-run queue/steer/interrupt state is a pure state machine in `src/tui-opentui/session-queue.ts` (interaction contract §3): `enqueue` (kind `"queue"`) and `enqueueSteer` (kind `"steer"`) share one pending pool, drained steer-first, then queue, both FIFO within their class. The prompt hint (`src/tui-opentui/stream.ts`, `PROMPT_HINT`) reads `Enter queue · Alt+Enter steer · Ctrl+C stop`.
349345

350346
### Lifecycle Hooks
351347

@@ -367,7 +363,7 @@ See `docs/PLUGINS.md` for the full design. Summary:
367363
- Every installable plugin exports a `manifest` (`{ id, name, kind, description?, credentials? }`) with `kind` one of `web | command | tool`. A workflow is just a slash command, so there is no separate workflow/agent kind.
368364
- Plugins are auto-discovered from `plugins/`, `<cwd>/.corbits/plugins/`, and `~/.corbits/plugins/`, plus any explicit file/dir paths in `settings.pluginPaths`. When `settings.discoverClaudePlugins` is true, plugins listed in `~/.claude/plugins/installed_plugins.json` are also loaded (install paths only; still require enable). The `/plugins` UI's "add by path" action (`a`) loads a plugin from anywhere on disk, validates its manifest, and persists the path. Discovery resolves relative imports to absolute first (`loadPluginEntry`). Project-local plugins require per-cwd trust (`~/.corbits/trust/<hash>.json`); path plugins use global path trust (`~/.corbits/trust/path-plugins.json`) so they keep working across project directories. Untrusted origins load metadata-only until granted.
369365

370-
- **Explicit enable:** nothing is wired in until `settings.plugins[id].enabled` is true. `command``registerCommandPlugins` registers slash commands (live on enable); `tool``resolveToolPlugins` instantiates `createToolPlugin(credentials)` and appends the tools to the posix toolset assembled in `src/tui/runner.tsx` (via `tools.ts` helpers). `web``web_search`/`web_fetch` are now always-on core built-ins (`src/tools/web-search.ts`, `src/tools/web-fetch.ts`), not plugin-backed; a discovered `kind: "web"` plugin is retained for brand-display resolution only (`resolveWebProviderFromPlugins`/`webBrand` in `src/web/plugin-provider.ts`) and no longer supplies the tool implementation.
366+
- **Explicit enable:** nothing is wired in until `settings.plugins[id].enabled` is true. `command``registerCommandPlugins` registers slash commands (live on enable); `tool``resolveToolPlugins` instantiates `createToolPlugin(credentials)` and appends the tools to the posix toolset assembled in `src/tui/runner.ts` (via `tools.ts` helpers). `web``web_search`/`web_fetch` are now always-on core built-ins (`src/tools/web-search.ts`, `src/tools/web-fetch.ts`), not plugin-backed; a discovered `kind: "web"` plugin is retained for brand-display resolution only (`resolveWebProviderFromPlugins`/`webBrand` in `src/web/plugin-provider.ts`) and no longer supplies the tool implementation.
371367
- **Tool consent:** a `tool` plugin runs in-process, so it is wired in only when enabled AND `consented`. The `/plugins` UI prompts a one-time y/n consent recorded in `settings.plugins[id].consented`.
372368
- Configure via `/plugins`, which writes `settings.plugins` (enabled / consented / credentials), `settings.web`, and `settings.pluginPaths` to the global settings file. Credentials live in the global file because it carries secrets — the project-local settings file rejects credential keys. When a web plugin is active its tool calls render under its brand (e.g. "Exa Search"). Example: `{ "web": "exa", "plugins": { "exa": { "enabled": true, "credentials": { "apiKey": "..." } } } }`.
373369

@@ -391,7 +387,7 @@ Corbits Code v0.3 memory and stall hardening is implemented under `src/`, `tests
391387
|---|---|
392388
| **Status** | Not applicable on the default path; deferred until real audit persistence is enabled |
393389
| **Risk** | A live audit collector that buffers full tool results in memory until `flush()` on checkpoint/shutdown can grow without bound on long, checkpoint-sparse runs. |
394-
| **Why Corbits Code-only scope cannot close it** | Production agent setup wires `noopAuditStore()` from `@intx/agent/testing` in `src/tui/runner.tsx` and `src/subagent/index.ts`. No `AuditCollector` from `@intx/inference` is instantiated, so bounding `completed` retention in `audit-collector` does not change shipped behavior today. |
390+
| **Why Corbits Code-only scope cannot close it** | Production agent setup wires `noopAuditStore()` from `@intx/agent/testing` in `src/tui/runner.ts` and `src/subagent/index.ts`. No `AuditCollector` from `@intx/inference` is instantiated, so bounding `completed` retention in `audit-collector` does not change shipped behavior today. |
395391
| **Upstream owner** | `@intx/inference` audit collector (`audit-collector` module): opportunistic flush or capped result bodies while preserving metadata. |
396392
| **Future Corbits Code work** | If settings later select a persistent audit store, add a bounded wrapper or configuration in `src/` and re-run hardening tests; until then, document the noop path only. |
397393

@@ -414,7 +410,7 @@ Run all three before declaring work complete.
414410
- **`tests/fixtures/`** holds fixture repos and comparison assets (e.g. `demo-comparison/`, `multi-file-service/`).
415411
- **`tests/integration/`** holds the reactor permission / multi-turn harness (scripted models via `@intx/inference-testing`). **`tests/e2e/`** (fixture-repo runs) is still planned. Until e2e exists, broader harness coverage also lives in co-located `*.test.ts` files and `tests/unit/`.
416412
- **Capability evals** (`evals/capability/`) are **not** the integration harness: they drive the product path (`corbits exec` / `runExec`) with real models against fixture copies and objective `verify.sh` graders. Case format + loader tests live under `evals/capability/`; run with `bun run eval:capability` (see `evals/capability/README.md`). Use `--baseline` to detect improve/regress across models or commits.
417-
- **TUI tests** use `ink-testing-library` with mock `EventEmitter`s to simulate real-time event streams; they verify stream-hook accumulation, event-log formatting/filtering, keyboard handling, and cost formatting. `bun run test:tui` preloads `tests/setup/tui-preload.ts`, which mocks `yoga-layout` onto its TLA-free `/load` entry point. Under Bun 1.3.14, `--isolate`'s fresh-global-per-file evaluation hits a TLA/TDZ regression (fixed on Bun's `main` via oven-sh/bun#32437, not yet in a stable release) that leaves `ink`'s default `yoga-layout` import in its temporal dead zone, crashing every TUI test file; the preload works around it without giving up `--isolate`'s per-file timer/handle isolation, which the TUI tests rely on. Drop the preload once a stable Bun release ships that fix.
413+
- **TUI tests** are co-located `*.test.ts` files under `src/tui/` and `src/tui-opentui/` (e.g. `shell.test.ts`, `runner-host.test.ts`, `stream.test.ts`), run as part of `bun test` along with everything else; there is no separate `test:tui` script or test-setup preload.
418414

419415
## Deployment
420416

docs/PLUGINS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ Five mechanisms, three loading models, one manifest that only governs one kind.
9090

9191
| Mechanism | Entry contract | Loads via | Config | Manifest | UI |
9292
|---|---|---|---|---|---|
93-
| ToolPlugin (`@intx/tools-posix`) | `ToolPlugin` | wired in `src/tui/runner.tsx` / `tools.ts` || no | no |
93+
| ToolPlugin (`@intx/tools-posix`) | `ToolPlugin` | wired in `src/tui/runner.ts` / `tools.ts` || no | no |
9494
| WorkflowPlugin | `plugin` / default | `settings.workflowPlugins: string[]``loadWorkflowPlugins` | specifier array | no | no |
9595
| AgentPlugin | `plugin` / default | `settings.agentPlugins: string[]``loadAgentPlugins` | specifier array | no | no |
9696
| CommandPlugin | `commandPlugin` | directory discovery | discovery only | no | no |
@@ -102,7 +102,7 @@ Concrete problems, with file references:
102102
*specifier arrays*; command/web load from *directory discovery* (plus the new
103103
`pluginPaths`). Same concept, two code paths.
104104
2. **A dead path.** `src/plugins/loader.ts` captures `workflowPlugin` from a
105-
discovered module, but `src/tui/runner.tsx` only registers `commandPlugin`
105+
discovered module, but `src/tui/runner.ts` only registers `commandPlugin`
106106
from discovered modules — a discovered workflow plugin is silently dropped.
107107
3. **Manifest governs only web.** `kind: "workflow" | "command"` exist in the
108108
type (`src/plugins/manifest.ts`) but nothing routes by them; command plugins
@@ -233,7 +233,7 @@ shape.
233233

234234
- `kind: "tool"` plugins export `createToolPlugin(credentials)` and contribute
235235
posix `ToolPlugin`s, resolved in `src/plugins/tool-plugins.ts` and wired into
236-
the toolset in `src/tui/runner.tsx` and `tools.ts` (appended last, so they cannot
236+
the toolset in `src/tui/runner.ts` and `tools.ts` (appended last, so they cannot
237237
shadow core middleware).
238238
- A tool plugin is wired in only when **enabled AND consented**. Enabling one in
239239
`/plugins` prompts a one-time y/n consent recorded in `settings.plugins[id]`.

0 commit comments

Comments
 (0)