diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 0198b3836..a1b3a157e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -22,7 +22,7 @@ here because the sub-sections below reference it; `inference.done` and exports guards for (`onTurnBoundary` / `onReactorShutdown`) — see the paragraph after the table. The complete set of reactor and stream event types the TUI maps is `PRODUCTION_REACTOR_TYPES` in -`src/tui-opentui/stream-event-map.ts:62-78` (covering `message.received`, +`src/tui/stream-event-map.ts:62-78` (covering `message.received`, `inference.start`, `inference.text.delta`, `inference.tool_call.start` / `.delta` / `.end`, `tool.start`, and `connector.reply`, among others) — treat that as canonical rather than this table or any other doc's partial @@ -79,7 +79,7 @@ In TUI chat mode there is no completion gate — the session stays open across t - Builds a chat-mode agent using the `ChatDirector` - Wires `ask_operator` to an operator-gate event resolved by a modal -- Mounts the OpenTUI host via `mountRunnerHost` (`src/tui-opentui/runner-host.ts`), which mounts `mountProductHost` (`src/tui-opentui/product-host.ts`) over the shell (`src/tui-opentui/shell.ts`) +- 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`) - Bridges reactor events to the OpenTUI host via a plain `EventEmitter` - **Mid-run injection** — When a message arrives while the agent is running, it is queued in an `InjectionQueue`. On the next `inference.done` event (turn boundary), the queue is drained: each queued message is delivered via `agentProxy.deliver()` and a `"mid-run.delivered"` emitter event is fired so the badge count in the App updates. The queue is cleared on session rotation (`/clear`). - **Session rotation** — Uses a serial session-operation queue (`createSessionOperationQueue`, not a boolean flag) so rotation, compaction continuation, and `agentProxy.deliver` never race a concurrent rebuild. Each operation chains onto the tail, ensuring in-flight work completes before the agent is torn down. @@ -210,7 +210,7 @@ Profiles with `orchestrator: true` may themselves call `task` (one hop only): ne **Session records** (`src/subagent/session-store.ts`): each spawn is retained as an inspectable child session (id, profile, description, brief, status, tool activity, transcript entries). Child events land only in this store — not in the parent chat transcript. Live progress still uses the light `onProgress` channel for the status bar. Completed sessions are capped (`maxCompleted`) so a long chat does not grow without bound. -**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-opentui/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-opentui/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-opentui/palette.ts` itself, but the chord replaces it rather than dropping the feature. +**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. 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`). @@ -296,16 +296,16 @@ tool call Approval scopes offered: Allow Once (persist nothing), Allow Always for a file or its directory (file tools), or a command shape (shell). There is intentionally no "all files" rung. -A queued gate's display-dependent timers (auto-deny timeout, tool-budget pause ceiling) arm when the request is actually shown to the operator, not when it is received — a request sitting behind others in the queue does not burn its timeout invisibly. `ask_operator` has the same abort/timeout safety net as the permission gate, so a queued operator question behind a stuck overlay cannot hang a run. Both live in `src/tui-opentui/gate-wire.ts`'s `onPermission`/`onOperator`. +A queued gate's display-dependent timers (auto-deny timeout, tool-budget pause ceiling) arm when the request is actually shown to the operator, not when it is received — a request sitting behind others in the queue does not burn its timeout invisibly. `ask_operator` has the same abort/timeout safety net as the permission gate, so a queued operator question behind a stuck overlay cannot hang a run. Both live in `src/tui/gate-wire.ts`'s `onPermission`/`onOperator`. -### TUI (`src/tui-opentui/`) +### TUI (`src/tui/`) -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-opentui/runner-host.ts`), which mounts `mountProductHost` (`src/tui-opentui/product-host.ts`) over the shell (`src/tui-opentui/shell.ts`). +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`). - **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`. - **Product host** (`product-host.ts`) — Creates the `CliRenderer`, wires the event emitter bridge, model/command catalogs, and chrome pushes. - **Runner host** (`runner-host.ts`) — Runner-facing mount: catalog assembly from live config, chrome pushes on session change, subagent observe resolution, and session teardown (quitting is Ctrl+C twice, owned by the shell). -- **Overlays and pickers** — Resume picker (`src/tui/pick-session.ts`) and session-mode prompt (`src/tui/session-mode-prompt.ts`) use `runListModal` (`src/tui-opentui/list-modal.ts`). Slash-command surfaces (`/model`, `/settings`, `/permissions`, `/plugins`, etc.) route through `openCommandSurface` (`src/tui-opentui/command-surfaces.ts`). +- **Overlays and pickers** — Resume picker (`src/tui/pick-session.ts`) and session-mode prompt (`src/tui/session-mode-prompt.ts`) use `runListModal` (`src/tui/list-modal.ts`). Slash-command surfaces (`/model`, `/settings`, `/permissions`, `/plugins`, etc.) route through `openCommandSurface` (`src/tui/command-surfaces.ts`). - **Auto mode** — Toggled by CLI flags only (`--auto` / `--no-auto`); there is currently no in-session key bound to it. - `@file` mention resolution and image paste are not wired on the OpenTUI send path. diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md index 28252eb04..173bad6d2 100644 --- a/docs/IMPLEMENTATION.md +++ b/docs/IMPLEMENTATION.md @@ -128,7 +128,7 @@ src/ registry.ts Extensible slash-command registry built-in.ts /help, /model, /settings, /permissions, /plugins, /clear, /new, /mcp (connect providers from /model) - tui-opentui/ + tui/ shell.ts Transcript, header, status line, prompt, overlays product-host.ts Creates the CliRenderer, wires the event bridge runner-host.ts Runner-facing mount: catalogs, chrome, quit key @@ -164,7 +164,7 @@ Plan approval is handled separately by `use-gates` (`pendingPlan`), independent - **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. - **Alt+Enter** calls `onSubmit` immediately, pushing the message onto `pendingQueueRef` for drain at the next `connector.reply`. -`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. +`src/tui/stream-event-map.ts` maps reactor events onto the bridge's inbound events, and `src/tui/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. ### @file Mention Resolution @@ -335,11 +335,11 @@ session; that tree re-write is inherent to git and left as residual cost. `agent.stream()` emits `ReactorEmittedEvent` objects. `docs/ARCHITECTURE.md`'s "Reactor Events (Partial)" section names the two turn-boundary/shutdown events the directors guard on; the full set of reactor and stream event types is -`PRODUCTION_REACTOR_TYPES` in `src/tui-opentui/stream-event-map.ts:62-78` — +`PRODUCTION_REACTOR_TYPES` in `src/tui/stream-event-map.ts:62-78` — treat that as canonical rather than this section or any other doc's partial list. -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`. +Mid-run queue/steer/interrupt state is a pure state machine in `src/tui/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/stream.ts`, `PROMPT_HINT`) reads `Enter queue · Alt+Enter steer · Ctrl+C stop`. ### Lifecycle Hooks @@ -408,7 +408,7 @@ Run all three before declaring work complete. - **`tests/fixtures/`** holds fixture repos and comparison assets (e.g. `demo-comparison/`, `multi-file-service/`). - **`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/`. - **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. -- **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. +- **TUI tests** are co-located `*.test.ts` files under `src/tui/` (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. ## Deployment diff --git a/docs/TELEMETRY.md b/docs/TELEMETRY.md index 548901057..c31d85224 100644 --- a/docs/TELEMETRY.md +++ b/docs/TELEMETRY.md @@ -69,7 +69,7 @@ recorded. `auth_provider` is a separate property for that reason: it names which provider's sign-in was rejected (`codex`, `xai`), chosen from a fixed -first-party set in `src/tui-opentui/session-chrome.ts`. No part of the +first-party set in `src/tui/session-chrome.ts`. No part of the provider's rejection message is sent. The mapping is `src/telemetry/classify.ts`, and the tests that feed each diff --git a/docs/TUI.md b/docs/TUI.md index bc82b73cb..0c75822f9 100644 --- a/docs/TUI.md +++ b/docs/TUI.md @@ -2,7 +2,7 @@ This is the normative behavior spec for the terminal UI: Corbits Code, built on OpenTUI (`@opentui/core`). It describes what the shell must do, not how the -OpenTUI cutover got here. The implementation lives in `src/tui-opentui/`; the +OpenTUI cutover got here. The implementation lives in `src/tui/`; the runner that mounts it is `src/tui/runner.ts` (see `docs/ARCHITECTURE.md` for how the TUI fits the rest of the system). A reviewer should be able to hold a PR against this document; someone building a new overlay or picker should be @@ -19,10 +19,10 @@ There is no titlebar, no status strip, and no key-hint row as permanent chrome. The prompt box is the only permanent chrome in the shell: it is anchored at the bottom in every state, and everything else — task/agents strips, notices, banners, the overlay host — is optional and collapses to -zero rows when it has nothing to say (`src/tui-opentui/geometry/zones.ts`). +zero rows when it has nothing to say (`src/tui/geometry/zones.ts`). The transcript is residual: whatever rows remain after chrome and any open overlay belong to it, never the other way around -(`src/tui-opentui/geometry/resolve.ts:resolveGeometry`). +(`src/tui/geometry/resolve.ts:resolveGeometry`). A single **geometry resolver** turns terminal size, zone visibility, and overlay mode into region rects; every zone reads its rect from that resolver @@ -37,7 +37,7 @@ row at a time down to its 3-row base — never the transcript (`COLLAPSE_ORDER` in `zones.ts`). Horizontally, every surface sits inside one shared gutter -(`resolveSideMargin`, `src/tui-opentui/geometry/margins.ts`) so the shell reads +(`resolveSideMargin`, `src/tui/geometry/margins.ts`) so the shell reads as a single column of content rather than stacked panes. The gutter is one column per side at every width that can afford it, and zero below `MARGIN_MIN_COLUMNS` (40), where every column belongs to content. There is no @@ -49,17 +49,17 @@ The prompt box's border carries the metadata that would otherwise cost a titlebar row: the model label sits right-aligned in the top rule; the brand lockup sits at the left of the bottom rule with the working directory and git branch at its right (`AppShell.promptTopRule` / `promptBottomRule`, -`src/tui-opentui/shell.ts`). Both rules cost zero transcript rows because they +`src/tui/shell.ts`). Both rules cost zero transcript rows because they ride the prompt box's own border. While a turn is live the lockup slot swaps the wordmark for a semantic activity word — never the raw tool, MCP server, or plugin identifier that is -actually executing. `resolveTurnLabel` (`src/tui-opentui/session-chrome.ts`) +actually executing. `resolveTurnLabel` (`src/tui/session-chrome.ts`) maps execution onto the closed set `ACTIVITY_STATES` exported from that module (`thinking`, `planning`, `researching`, `building`, `working`, `waiting`, `orchestrating`, `stalled`, `stopping`); that export is the source of truth for what the slot can say, not this list. It is led by a single density cell -(`rampPulse`, `src/tui-opentui/ramp.ts`). The cell, not the word, +(`rampPulse`, `src/tui/ramp.ts`). The cell, not the word, is what says whether the session is healthy, and it carries four states: | State | Cell | Reads as | @@ -91,7 +91,7 @@ every path through both functions behaves exactly as it does for a plain single-agent turn. The stall phase is driven by the watchdog's own silence clock -(`stallLevel`, `src/tui-opentui/stall-watchdog.ts`), so the indicator and the +(`stallLevel`, `src/tui/stall-watchdog.ts`), so the indicator and the abort can never disagree about which runs are stuck. It arms at `STALL_NOTICE_MS` and keeps reading as stalled straight through the abort threshold. Its blink is a bounded burst (`STALL_BLINK_BURST_MS`) that settles @@ -106,7 +106,7 @@ An idle session animates nothing at all: the monitor tick stops entirely rather than repainting an unchanging frame. Color is a small, deliberate palette, not decoration -(`src/tui-opentui/theme.ts`). Dimmed text is a dimmed cream, never a neutral +(`src/tui/theme.ts`). Dimmed text is a dimmed cream, never a neutral gray, so every emphasis level keeps the same warm hue. Orange (`UI.action`) is spent once per screen: it marks the session identity and whatever is currently awaiting a human decision (an approval subject, an @@ -121,7 +121,7 @@ The `task` chrome zone renders a standing panel above the transcript, one row per task the task tool has written (`manage_tasks`) — distinct from the `agents` panel below it. A task is a unit of work with a status; an agent is an executor with its own context and transcript. The two are never merged -into one panel: `formatTasksPanel` (`src/tui-opentui/chrome-state.ts`) and +into one panel: `formatTasksPanel` (`src/tui/chrome-state.ts`) and `formatAgentsPanel` are separate formatters feeding separate zones with separate row types (`TaskPanelRow` vs. `AgentPanelRow`). @@ -157,7 +157,7 @@ costs zero rows. The task tool writes state through `ChatDirectorImpl` (`src/agent/director.ts`), which calls `onTasksChange` on every `manage_tasks` tool call and on session resume (`restoreTasks`). The runner forwards that into the OpenTUI host via -`RunnerHostDeps.chrome`/`subscribeChrome` (`src/tui-opentui/runner-host.ts`): +`RunnerHostDeps.chrome`/`subscribeChrome` (`src/tui/runner-host.ts`): `subscribeChrome` is a required dependency, not optional. The production caller has always passed a real subscription, so this did not fix an observed break; it closes a shape that could have been omitted and would @@ -173,7 +173,7 @@ The `agents` chrome zone renders a standing panel above the transcript, one row per currently-running sub-agent. Each row reads `agentId: description · elapsed · tool`, sourced from the same `agentProgress()` clock/tool/stall computation used to trail a task row in the -transcript (`src/tui-opentui/agent-progress.ts`); the panel does not compute +transcript (`src/tui/agent-progress.ts`); the panel does not compute progress a second way. Past one running agent the panel is led by a fleet summary row (`N agents`, plus `· N stalled` or `· in tools`), counted from the same lane states the rows below render, so header and rows can never disagree. @@ -233,7 +233,7 @@ clock next to the word `stalled` was the original defect — the number the operator watched climbing was unrelated to the word beside it. The panel is bounded to `AGENTS_PANEL_MAX_VISIBLE` rows -(`src/tui-opentui/geometry/zones.ts`); a larger fan-out degrades to a trailing +(`src/tui/geometry/zones.ts`); a larger fan-out degrades to a trailing `+N more` row rather than growing the zone — and therefore the chrome budget — without limit. Its height is requested from the geometry resolver like every other zone, never guessed: the caller passes the exact row count it @@ -301,15 +301,15 @@ operator question mid-run. A blocking surface (permissions, an operator question, the model/provider picker, settings, help, the `/` command list, …) shares one overlay host and one height path — there is no second modal stack with independent row -accounting (`src/tui-opentui/geometry/resolve.ts`, -`src/tui-opentui/shell.ts:openListOverlay`). Opening a second surface either +accounting (`src/tui/geometry/resolve.ts`, +`src/tui/shell.ts:openListOverlay`). Opening a second surface either replaces the one that was open or stacks over it; either way Escape always walks back along a single path to the prompt. An open overlay reserves a real minimum for its own border, title, and at least one content row before anything else — including the transcript floor — is allowed to starve it further. That minimum is `OVERLAY_MIN_ROWS = 3` -(`src/tui-opentui/geometry/zones.ts`) — two border rows plus one content row. +(`src/tui/geometry/zones.ts`) — two border rows plus one content row. The geometry resolver iteratively collapses optional chrome to make room for both the transcript floor and this overlay minimum before it ever accepts a transcript-below-floor outcome; only when nothing is left to collapse does it @@ -319,7 +319,7 @@ actually assigned. Escape dismisses the open overlay and, for a permission or operator prompt, that dismissal **denies** the request rather than leaving it unresolved -(`src/tui-opentui/gate-wire.ts`: both `onPermission`'s and `onOperator`'s +(`src/tui/gate-wire.ts`: both `onPermission`'s and `onOperator`'s `onCancel` handlers resolve the pending promise — as a deny for permissions, as a cancel for the operator question — with an explicit comment that an unresolved gate "hangs the run until the process is killed"). This exists @@ -329,7 +329,7 @@ Escape must always settle the promise it is dismissing. The decision surfaces (permission approval, operator question) are the one framed content in the shell, and they are shaped rather than merely listed -(`src/tui-opentui/overlay-body.ts`): a dithered header (`░▒▓`) carries the +(`src/tui/overlay-body.ts`): a dithered header (`░▒▓`) carries the subject in the action color, a blank row separates it from context, and each choice gets one row with the active choice marked by a solid block (`█`) rather than a background fill. @@ -347,9 +347,9 @@ top of the transcript never lets the wheel move the transcript underneath it. `(current)` is read live from the session's actual active provider/model on every picker open — independent of the recents list, which only moves on an explicit pick and can go stale (`ProductHostConfig.activeModelId`'s doc -comment and `annotateCurrent` in `src/tui-opentui/product-host.ts`). +comment and `annotateCurrent` in `src/tui/product-host.ts`). -The `/` command list specifically (`src/tui-opentui/command-catalog.ts`, +The `/` command list specifically (`src/tui/command-catalog.ts`, `shell.ts:openPalette`/`repaintPalette`): width matches the prompt box — both are painted at the geometry resolver's shared `contentWidth` (`geometry/resolve.ts:assignRects`, `shell.ts:overlayRowWidth`). There is no @@ -366,7 +366,7 @@ shows what was typed, so a second header line would say nothing new `/` at an empty prompt opens the command list, narrowed by name prefix as more is typed; Tab completes the name so arguments can be typed, Enter runs it. Every entry is backed by the live command registry -(`src/tui-opentui/command-catalog.ts:commandItemsFromRegistry`) — there is no +(`src/tui/command-catalog.ts:commandItemsFromRegistry`) — there is no separate palette overlay and no shell-owned action outside the registry. The overlay this reuses is still internally called `"palette"` (`shell.ts`'s `PrimaryOverlayKind`), a naming leftover from when a Ctrl+O command palette @@ -402,7 +402,7 @@ known, accepted cost of the badge rather than an oversight — see `terminalForGeometry`'s doc comment in `shell.ts` for the exact mechanism. The model/provider picker is provider-first -(`src/tui-opentui/product-host.ts:groupModelsForPicker`/`openLevel`): recent +(`src/tui/product-host.ts:groupModelsForPicker`/`openLevel`): recent and favorite provider+model pairs stay flat at the top of the list (already single models, nothing to descend into); every other provider collapses into one top-level group row. Selecting a provider group row descends into that @@ -416,7 +416,7 @@ model. Onboarding (the standalone provider-setup screen, `provider-setup.ts`) and the satellite pickers used for session resume and session-mode selection -(`src/tui-opentui/list-modal.ts:runListModal`) deliberately do not enable DEC +(`src/tui/list-modal.ts:runListModal`) deliberately do not enable DEC mouse reporting (`useMouse: false`, `enableMouseMovement: false` — verified in `mouse-reporting-disabled.test.ts` for both `runListModal` and `runProviderSetup`). This is intentional: these surfaces never need @@ -429,7 +429,7 @@ required. The prompt is a genuine multi-line composing area built on OpenTUI's `TextareaRenderable` rather than its single-line `InputRenderable`, because the single-line widget is hard-wired to one row, no wrapping, and strips -newlines (`src/tui-opentui/prompt-input.ts`). Enter sends; a literal newline +newlines (`src/tui/prompt-input.ts`). Enter sends; a literal newline needs an explicit chord: Ctrl+Enter or Ctrl+J work on every terminal, and Shift+Enter works too on a terminal that negotiates the kitty keyboard protocol (this app requests it — `useKittyKeyboard` in `product-host.ts`) and @@ -507,7 +507,7 @@ can drill into a path without retyping it. A readline-style kill ring backs Ctrl+K/U/W (kill) and Ctrl+Y/Alt+Y (yank/yank-pop) on top of the textarea's native delete bindings, which -otherwise discard what they delete (`src/tui-opentui/prompt-kill-ring.ts`). +otherwise discard what they delete (`src/tui/prompt-kill-ring.ts`). Consecutive kills in the same direction accumulate into one ring entry the way readline does, so a `Ctrl+K Ctrl+K … Ctrl+Y` sequence restores the whole killed run in original order. @@ -551,7 +551,7 @@ first: - **Alt+C** copies a message, tool output, or diff without touching the mouse at all: it opens a copy-selection surface over the transcript (`enterCopyMode`) that resolves through the system clipboard port - (`src/tui-opentui/system-clipboard.ts` — a native helper binary per + (`src/tui/system-clipboard.ts` — a native helper binary per platform, `pbcopy`/`clip`/`wl-copy`/`xclip`/`xsel`, falling back to an OSC 52 escape sequence when no helper is available, e.g. over SSH). @@ -570,7 +570,7 @@ across both contexts. ## Standalone screens -The provider-setup screen (`src/tui-opentui/provider-setup.ts`) runs before +The provider-setup screen (`src/tui/provider-setup.ts`) runs before any session shell exists, so it does not route through the shared geometry resolver — there is no transcript, no prompt box, nothing for that resolver to arbitrate yet. Every direct child of that screen's root is given @@ -586,7 +586,7 @@ Fixture and demo content must never be reachable from a production code path. When a surface's real dependency is missing (e.g. the settings surface opened with no settings data wired in), the surface must produce an honest empty state or a surfaced error — never the hardcoded rows from -`src/tui-opentui/residuals.ts` rendered as if they were real content +`src/tui/residuals.ts` rendered as if they were real content (`overlay-fixture-fallback.test.ts` pins this: a settings surface opened without its dependency must not contain the real settings labels, and must notify the caller instead). diff --git a/docs/audit-fleet-prompt.md b/docs/audit-fleet-prompt.md index 2526ee2d9..6428a44a3 100644 --- a/docs/audit-fleet-prompt.md +++ b/docs/audit-fleet-prompt.md @@ -9,7 +9,7 @@ You are orchestrating a standing audit of Corbits Code's own capabilities. The goal is not to ship a feature — it is to find out what is quietly wrong, what is about to break, and what has drifted from what we believe about it. -**Scope for this run:** ## Ground rules for every agent you spawn diff --git a/docs/tui-opentui-packaging.md b/docs/tui-opentui-packaging.md index f5a986043..195635574 100644 --- a/docs/tui-opentui-packaging.md +++ b/docs/tui-opentui-packaging.md @@ -109,7 +109,7 @@ bun install --frozen-lockfile bun run test:tui # or whatever harness lands with the platform ``` -OpenTUI has since shipped: root `package.json` on `main` depends on `@opentui/core@0.5.1`, `@opentui/solid@0.5.1`, `@opentui/keymap@0.5.1`, and `solid-js@1.9.14`, and `src/tui-opentui/` is the shipping shell (see `docs/TUI.md`). +OpenTUI has since shipped: root `package.json` on `main` depends on `@opentui/core@0.5.1`, `@opentui/solid@0.5.1`, `@opentui/keymap@0.5.1`, and `solid-js@1.9.14`, and `src/tui/` is the shipping shell (see `docs/TUI.md`). ### Solid contributor notes diff --git a/package.json b/package.json index 665c3bef4..2add59760 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "test": "bun test ./src ./tests ./evals", "start": "bun run build && bun ./dist/index.js", "eval:capability": "bun scripts/eval-capability.ts", - "tui:opentui-smoke": "bun ./src/tui-opentui/smoke.ts" + "tui:smoke": "bun ./src/tui/smoke.ts" }, "workspaces": [ ".", diff --git a/src/auth/callback-page.ts b/src/auth/callback-page.ts index afb6f5c52..2902fe4e4 100644 --- a/src/auth/callback-page.ts +++ b/src/auth/callback-page.ts @@ -6,7 +6,7 @@ * sees before returning to the terminal, so it carries the brand rather than a * browser's default serif on white: the mark animates through the same * dithered draw/fill timeline as the boot screen and the TUI landing - * (`tui-opentui/mark-anim.ts`), which is the canvas original the terminal + * (`tui/mark-anim.ts`), which is the canvas original the terminal * approximates rather than a second interpretation of it. * * Everything is inline. The loopback server has no asset route, and a page @@ -33,7 +33,7 @@ const MARK_PATH = // Backgrounds are black/white; element neutrals are cream on dark and charcoal // on light, and never cross over. // The terminal palette, not the print one: the charcoal ground and the stepped -// creams are `tui-opentui/theme.ts` verbatim, so the tab an operator lands on +// creams are `tui/theme.ts` verbatim, so the tab an operator lands on // and the terminal they came from are the same surface. const STYLE = ` :root { diff --git a/src/subagent/fleet-report.ts b/src/subagent/fleet-report.ts index 51cdc2d95..a6f457b17 100644 --- a/src/subagent/fleet-report.ts +++ b/src/subagent/fleet-report.ts @@ -12,7 +12,7 @@ * it back on the next observation. No painting, no store access. */ -import { agentProgress, clockLabel, DEFAULT_STALL_MS } from "../tui-opentui/agent-progress.js"; +import { agentProgress, clockLabel, DEFAULT_STALL_MS } from "../tui/agent-progress.js"; import type { SubAgentSessionStatus } from "./session-store.js"; /** The lane fields a report is written from. `SubAgentSession` satisfies it. */ diff --git a/src/tui-opentui/theme.ts b/src/tui-opentui/theme.ts deleted file mode 100644 index 0e578ad90..000000000 --- a/src/tui-opentui/theme.ts +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Corbits terminal palette — the single source of truth for every color the - * OpenTUI shell paints. - * - * Three rules the rest of the TUI depends on: - * - * 1. Gray never sits on the ground. Dimmed text is a dimmed *cream* - * (`textDim`, `textFaint`) so the warm bias survives at every emphasis - * level. There is deliberately no neutral gray in this file to reach for. - * 2. Orange is spent once per screen. It marks the session and whatever awaits - * a human decision — nothing else. Ongoing status uses the bronze ramp and - * `done` (green) so it never competes with the one thing asking to be - * answered. Diff removals are the sole exception: there the orange is - * content, not chrome, and no decision-marker shares the row. - * 3. The chrome ramp is warm but never saturated. Every bronze sits at or - * below 54% HSL saturation against Breakthrough Orange's 81%, so full - * orange still arrives as an event rather than as another shade of the - * furniture. - * - * The product owner has deliberately dropped Summit Blue from the terminal: - * cool information read as foreign against cream, black and orange chrome. The - * brand's discipline is kept — small palette, roles not decoration, no hue - * without a job — only the cool end of it is replaced by warm structure. - */ - -/** - * Palette values a theme supplies. Call sites paint through `UI`, never - * through a theme directly, so a second theme is a data change here. - */ -export type Theme = { - readonly name: string - /** Terminal ground. Foreground-only discipline means almost nothing fills it. */ - readonly ground: string - /** All body text. Never white, never gray. */ - readonly text: string - /** Secondary text: labels, context lines, chrome. */ - readonly textDim: string - /** Lowest emphasis: comments, concealed markdown syntax. */ - readonly textFaint: string - /** The session mark and anything awaiting a human decision. */ - readonly action: string - readonly actionDim: string - /** Work in progress: ramps, tool verbs, machine output threaded into prose. */ - readonly inFlight: string - /** The tier above body text that still reads as machine: keywords, links, args. */ - readonly inFlightBright: string - /** Document structure: markdown headings and section rules. */ - readonly heading: string - /** Completed and succeeded. */ - readonly done: string -} - -/** - * Brand hues, plus the warm ramp that replaced Summit Blue. - * - * Lowercase because the renderer normalizes hex that way, and tests compare a - * painted span's `fg` against these constants directly. - */ -export const BRAND = { - // Charcoal rather than pure black: it lifts the interface off the host - // terminal's own background and softens the cream's contrast edge, while - // staying dark enough that `textFaint` keeps a readable margin above it. - ground: "#191614", - canvasCream: "#f7ead5", - breakthroughOrange: "#e98428", - breakthroughOrangeDark: "#bf6b20", - ridgeGreen: "#7b9974", -} as const - -// Cream stepped down toward the ground rather than desaturated toward gray, so -// low-emphasis text keeps the same warm hue as full-emphasis text. -const CREAM_DIM = "#a89f91" -const CREAM_FAINT = "#787166" - -// The warm chrome ramp. Three tones so the roles that once shared a blue stay -// separable — they differ in lightness first, hue second, and all three sit -// well under the action orange's saturation. -const BRONZE = "#93733f" // dimmest: motion and machine chrome -const SAND = "#d1ad7d" // brightest: keywords, links, command arguments -const EMBER = "#a97243" // burnt, between the two: document structure - -export const corbitsDark: Theme = { - name: "corbits-dark", - ground: BRAND.ground, - text: BRAND.canvasCream, - textDim: CREAM_DIM, - textFaint: CREAM_FAINT, - action: BRAND.breakthroughOrange, - actionDim: BRAND.breakthroughOrangeDark, - inFlight: BRONZE, - inFlightBright: SAND, - heading: EMBER, - done: BRAND.ridgeGreen, -} - -/** Every theme that ships. A picker would choose from here. */ -export const THEMES = { [corbitsDark.name]: corbitsDark } as const - -/** Semantic roles. Everything outside this file paints through these. */ -export const UI: Theme = corbitsDark - -export type UIColor = Theme[keyof Theme] diff --git a/src/tui-opentui/README.md b/src/tui/README.md similarity index 87% rename from src/tui-opentui/README.md rename to src/tui/README.md index c1e23c929..757698c38 100644 --- a/src/tui-opentui/README.md +++ b/src/tui/README.md @@ -1,6 +1,6 @@ -# tui-opentui +# tui -Platform kit for the OpenTUI shell on the `migration/opentui-tui` branch. Pure TypeScript modules and later Solid surfaces live here; this tree is **not** wired to the `corbits` CLI entry yet (Ink remains production until later migration tasks). +Shipping OpenTUI shell and co-located TUI modules. Pure TypeScript and Solid surfaces live here; the runner mounts this tree as the product shell. ## Modules @@ -68,6 +68,6 @@ appendTranscript(shell, "hello") // stickyScroll follows bottom until operator scrolls up (FOLLOW / PINNED) ``` -Interactive demo (TTY): `bun src/tui-opentui/demo.ts` +Interactive demo (TTY): `bun src/tui/demo.ts` Binding for the shell frame: **core-class** (not Solid) — VNode ScrollBox broke `scrollTop` in the spike; class `ScrollBoxRenderable` is required for sticky/scroll leases. diff --git a/src/tui-opentui/agent-progress.test.ts b/src/tui/agent-progress.test.ts similarity index 100% rename from src/tui-opentui/agent-progress.test.ts rename to src/tui/agent-progress.test.ts diff --git a/src/tui-opentui/agent-progress.ts b/src/tui/agent-progress.ts similarity index 100% rename from src/tui-opentui/agent-progress.ts rename to src/tui/agent-progress.ts diff --git a/src/tui-opentui/chrome-state.test.ts b/src/tui/chrome-state.test.ts similarity index 100% rename from src/tui-opentui/chrome-state.test.ts rename to src/tui/chrome-state.test.ts diff --git a/src/tui-opentui/chrome-state.ts b/src/tui/chrome-state.ts similarity index 100% rename from src/tui-opentui/chrome-state.ts rename to src/tui/chrome-state.ts diff --git a/src/tui-opentui/collapse.test.ts b/src/tui/collapse.test.ts similarity index 100% rename from src/tui-opentui/collapse.test.ts rename to src/tui/collapse.test.ts diff --git a/src/tui-opentui/command-catalog.test.ts b/src/tui/command-catalog.test.ts similarity index 100% rename from src/tui-opentui/command-catalog.test.ts rename to src/tui/command-catalog.test.ts diff --git a/src/tui-opentui/command-catalog.ts b/src/tui/command-catalog.ts similarity index 97% rename from src/tui-opentui/command-catalog.ts rename to src/tui/command-catalog.ts index 99d27a914..e86fd061e 100644 --- a/src/tui-opentui/command-catalog.ts +++ b/src/tui/command-catalog.ts @@ -7,7 +7,7 @@ * setPaletteCatalog(shell, commandItemsFromRegistry(listCommands())) */ -import { sliceToWidth, stringWidth } from "../tui/view/height.js" +import { sliceToWidth, stringWidth } from "./view/height.js" /** Minimal registry shape — matches `listCommands()` entries without importing them. */ export type RegistryCommandSource = { diff --git a/src/tui-opentui/command-display.test.ts b/src/tui/command-display.test.ts similarity index 100% rename from src/tui-opentui/command-display.test.ts rename to src/tui/command-display.test.ts diff --git a/src/tui-opentui/command-display.ts b/src/tui/command-display.ts similarity index 99% rename from src/tui-opentui/command-display.ts rename to src/tui/command-display.ts index 4eef78c93..8659586e9 100644 --- a/src/tui-opentui/command-display.ts +++ b/src/tui/command-display.ts @@ -6,7 +6,7 @@ // grouping is coarser (pipes stay inline) and must never feed back into a // security decision. -import { sliceTailToWidth, sliceToWidth, stringWidth } from "../tui/view/height.js"; +import { sliceTailToWidth, sliceToWidth, stringWidth } from "./view/height.js"; // `&` participates in a redirect when it opens a bash combined redirect // (`&>file`) or duplicates a fd (`2>&1`, `<&-`); only a lone `&` word is the diff --git a/src/tui-opentui/command-surfaces.test.ts b/src/tui/command-surfaces.test.ts similarity index 100% rename from src/tui-opentui/command-surfaces.test.ts rename to src/tui/command-surfaces.test.ts diff --git a/src/tui-opentui/command-surfaces.ts b/src/tui/command-surfaces.ts similarity index 100% rename from src/tui-opentui/command-surfaces.ts rename to src/tui/command-surfaces.ts diff --git a/src/tui-opentui/copy-path.test.ts b/src/tui/copy-path.test.ts similarity index 100% rename from src/tui-opentui/copy-path.test.ts rename to src/tui/copy-path.test.ts diff --git a/src/tui-opentui/copy-path.ts b/src/tui/copy-path.ts similarity index 100% rename from src/tui-opentui/copy-path.ts rename to src/tui/copy-path.ts diff --git a/src/tui-opentui/copy-wire.test.ts b/src/tui/copy-wire.test.ts similarity index 100% rename from src/tui-opentui/copy-wire.test.ts rename to src/tui/copy-wire.test.ts diff --git a/src/tui-opentui/demo.ts b/src/tui/demo.ts similarity index 98% rename from src/tui-opentui/demo.ts rename to src/tui/demo.ts index d2bd0bc4c..08114ecfd 100644 --- a/src/tui-opentui/demo.ts +++ b/src/tui/demo.ts @@ -1,6 +1,6 @@ /** * Interactive OpenTUI product-skin demo (real TTY only). - * Run: bun src/tui-opentui/demo.ts + * Run: bun src/tui/demo.ts * * Wave 7: residual surfaces + observe on shared kit. * Not production CLI. Ink remains production. @@ -61,7 +61,7 @@ const DEMO_RESUME_ITEMS: readonly string[] = [ ] const DEMO_MENTION_ITEMS: readonly string[] = [ - "@src/tui-opentui/shell.ts", + "@src/tui/shell.ts", "@AGENTS.md", "Close mentions", ] @@ -74,7 +74,7 @@ function demoObserveSession(): ObserveSession { lines: [ { role: "system", text: "— child session explore —" }, { role: "user", text: "find every openListOverlay caller" }, - { role: "assistant", text: "Searching src/tui-opentui…" }, + { role: "assistant", text: "Searching src/tui…" }, { role: "tool", text: "grep openListOverlay → 6 hits", meta: "tool.done" }, { role: "assistant", text: "Report ready for parent." }, ], diff --git a/src/tui-opentui/description-zone.test.ts b/src/tui/description-zone.test.ts similarity index 100% rename from src/tui-opentui/description-zone.test.ts rename to src/tui/description-zone.test.ts diff --git a/src/tui-opentui/diff-rows.test.ts b/src/tui/diff-rows.test.ts similarity index 100% rename from src/tui-opentui/diff-rows.test.ts rename to src/tui/diff-rows.test.ts diff --git a/src/tui-opentui/diff.test.ts b/src/tui/diff.test.ts similarity index 100% rename from src/tui-opentui/diff.test.ts rename to src/tui/diff.test.ts diff --git a/src/tui-opentui/diff.ts b/src/tui/diff.ts similarity index 99% rename from src/tui-opentui/diff.ts rename to src/tui/diff.ts index 9e73839de..3a8fd78b3 100644 --- a/src/tui-opentui/diff.ts +++ b/src/tui/diff.ts @@ -12,10 +12,10 @@ * renderer. `shell.ts` maps segments to chunks at paint time. */ -import { describeToolCall } from "../tui/tool-formatter.js" +import { describeToolCall } from "./tool-formatter.js" // The one wrap implementation: a diff row soft-wraps by the same column rules // as every other row, so a wide glyph cannot overflow the gutter here alone. -import { wrapRanges } from "../tui/view/height.js" +import { wrapRanges } from "./view/height.js" import { DIFF_FG, type StreamRow } from "./stream.js" import { toolArgsView } from "./tool-args.js" diff --git a/src/tui-opentui/focus-routing.test.ts b/src/tui/focus-routing.test.ts similarity index 100% rename from src/tui-opentui/focus-routing.test.ts rename to src/tui/focus-routing.test.ts diff --git a/src/tui-opentui/focus/focus-state.test.ts b/src/tui/focus/focus-state.test.ts similarity index 100% rename from src/tui-opentui/focus/focus-state.test.ts rename to src/tui/focus/focus-state.test.ts diff --git a/src/tui-opentui/focus/focus-state.ts b/src/tui/focus/focus-state.ts similarity index 100% rename from src/tui-opentui/focus/focus-state.ts rename to src/tui/focus/focus-state.ts diff --git a/src/tui-opentui/focus/index.ts b/src/tui/focus/index.ts similarity index 100% rename from src/tui-opentui/focus/index.ts rename to src/tui/focus/index.ts diff --git a/src/tui-opentui/focus/types.ts b/src/tui/focus/types.ts similarity index 100% rename from src/tui-opentui/focus/types.ts rename to src/tui/focus/types.ts diff --git a/src/tui-opentui/gate-wire.test.ts b/src/tui/gate-wire.test.ts similarity index 100% rename from src/tui-opentui/gate-wire.test.ts rename to src/tui/gate-wire.test.ts diff --git a/src/tui-opentui/gate-wire.ts b/src/tui/gate-wire.ts similarity index 99% rename from src/tui-opentui/gate-wire.ts rename to src/tui/gate-wire.ts index e6ce86bb9..d1a27aa4a 100644 --- a/src/tui-opentui/gate-wire.ts +++ b/src/tui/gate-wire.ts @@ -24,7 +24,7 @@ import { EXPAND_KEY } from "./stream.js" import type { OperatorGateEvent, PermissionGateEvent, -} from "../tui/gate-events.js" +} from "./gate-events.js" import { createPermissionRequestQueue, wirePermissionGrantReconciliation, diff --git a/src/tui-opentui/geometry.test.ts b/src/tui/geometry.test.ts similarity index 100% rename from src/tui-opentui/geometry.test.ts rename to src/tui/geometry.test.ts diff --git a/src/tui-opentui/geometry/index.ts b/src/tui/geometry/index.ts similarity index 100% rename from src/tui-opentui/geometry/index.ts rename to src/tui/geometry/index.ts diff --git a/src/tui-opentui/geometry/margins.ts b/src/tui/geometry/margins.ts similarity index 100% rename from src/tui-opentui/geometry/margins.ts rename to src/tui/geometry/margins.ts diff --git a/src/tui-opentui/geometry/resolve.ts b/src/tui/geometry/resolve.ts similarity index 100% rename from src/tui-opentui/geometry/resolve.ts rename to src/tui/geometry/resolve.ts diff --git a/src/tui-opentui/geometry/zones.ts b/src/tui/geometry/zones.ts similarity index 100% rename from src/tui-opentui/geometry/zones.ts rename to src/tui/geometry/zones.ts diff --git a/src/tui-opentui/harness.test.ts b/src/tui/harness.test.ts similarity index 100% rename from src/tui-opentui/harness.test.ts rename to src/tui/harness.test.ts diff --git a/src/tui-opentui/harness.ts b/src/tui/harness.ts similarity index 100% rename from src/tui-opentui/harness.ts rename to src/tui/harness.ts diff --git a/src/tui-opentui/history-hydrate.test.ts b/src/tui/history-hydrate.test.ts similarity index 100% rename from src/tui-opentui/history-hydrate.test.ts rename to src/tui/history-hydrate.test.ts diff --git a/src/tui-opentui/history-hydrate.ts b/src/tui/history-hydrate.ts similarity index 99% rename from src/tui-opentui/history-hydrate.ts rename to src/tui/history-hydrate.ts index 066b79594..eb7ed79c9 100644 --- a/src/tui-opentui/history-hydrate.ts +++ b/src/tui/history-hydrate.ts @@ -5,7 +5,7 @@ * paint without a renderer. No OpenTUI or Ink deps. */ -import { validateView, viewToLines } from "../tui/view/index.js" +import { validateView, viewToLines } from "./view/index.js" import { toolCallRow } from "./diff.js" import { toolResultRow } from "./mcp-view.js" import type { StreamRow } from "./stream.js" diff --git a/src/tui-opentui/index.ts b/src/tui/index.ts similarity index 100% rename from src/tui-opentui/index.ts rename to src/tui/index.ts diff --git a/src/tui-opentui/keybindings.test.ts b/src/tui/keybindings.test.ts similarity index 100% rename from src/tui-opentui/keybindings.test.ts rename to src/tui/keybindings.test.ts diff --git a/src/tui-opentui/keybindings.ts b/src/tui/keybindings.ts similarity index 97% rename from src/tui-opentui/keybindings.ts rename to src/tui/keybindings.ts index d07a8b44a..36c990ea9 100644 --- a/src/tui-opentui/keybindings.ts +++ b/src/tui/keybindings.ts @@ -3,7 +3,7 @@ * * This is the source of truth for the help overlay — every row here must * match a real, currently-working chord: either a handler in - * src/tui-opentui/shell.ts's `onKey`/`onEnter` listeners, or a default + * src/tui/shell.ts's `onKey`/`onEnter` listeners, or a default * binding of the prompt's InputRenderable (see `defaultTextareaKeyBindings` * in @opentui/core — Ctrl+B/F/D, Alt+B/F, and arrow motion come from there, * not from shell.ts). Do not hand-transcribe from docs. diff --git a/src/tui-opentui/landing.test.ts b/src/tui/landing.test.ts similarity index 100% rename from src/tui-opentui/landing.test.ts rename to src/tui/landing.test.ts diff --git a/src/tui-opentui/landing.ts b/src/tui/landing.ts similarity index 99% rename from src/tui-opentui/landing.ts rename to src/tui/landing.ts index 9f5bd1813..206e8bf85 100644 --- a/src/tui-opentui/landing.ts +++ b/src/tui/landing.ts @@ -35,7 +35,7 @@ import pkg from "../../package.json" with { type: "json" } import { MARK_LARGE, MARK_MID, MARK_SMALL, type MarkGrid } from "./mark-shape.js" import { renderMark } from "./mark-anim.js" import { UI } from "./theme.js" -import { stringWidth } from "../tui/view/height.js" +import { stringWidth } from "./view/height.js" /** * Left gutter inside the shell's side margin (`resolveSideMargin`, applied to diff --git a/src/tui-opentui/list-modal.test.ts b/src/tui/list-modal.test.ts similarity index 100% rename from src/tui-opentui/list-modal.test.ts rename to src/tui/list-modal.test.ts diff --git a/src/tui-opentui/list-modal.ts b/src/tui/list-modal.ts similarity index 100% rename from src/tui-opentui/list-modal.ts rename to src/tui/list-modal.ts diff --git a/src/tui-opentui/list-viewport.test.ts b/src/tui/list-viewport.test.ts similarity index 100% rename from src/tui-opentui/list-viewport.test.ts rename to src/tui/list-viewport.test.ts diff --git a/src/tui-opentui/list-viewport.ts b/src/tui/list-viewport.ts similarity index 100% rename from src/tui-opentui/list-viewport.ts rename to src/tui/list-viewport.ts diff --git a/src/tui-opentui/live-session-port.test.ts b/src/tui/live-session-port.test.ts similarity index 98% rename from src/tui-opentui/live-session-port.test.ts rename to src/tui/live-session-port.test.ts index 1de856db5..08ac3a86f 100644 --- a/src/tui-opentui/live-session-port.test.ts +++ b/src/tui/live-session-port.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import { createLiveSessionPort } from "./live-session-port" -import type { PendingImageAttachment } from "../tui/image-attachments.js" +import type { PendingImageAttachment } from "./image-attachments.js" import type { QueueItem, QueueKind } from "./session-queue" type Call = diff --git a/src/tui-opentui/live-session-port.ts b/src/tui/live-session-port.ts similarity index 95% rename from src/tui-opentui/live-session-port.ts rename to src/tui/live-session-port.ts index c1240c5a1..a4e7d5a9b 100644 --- a/src/tui-opentui/live-session-port.ts +++ b/src/tui/live-session-port.ts @@ -3,7 +3,7 @@ * host hooks (runner agentProxy send / interrupt / deliver). No React/Ink. */ -import type { PendingImageAttachment } from "../tui/image-attachments.js" +import type { PendingImageAttachment } from "./image-attachments.js" import type { QueueItem, QueueKind } from "./session-queue.js" import type { SessionPort } from "./runtime-bridge.js" diff --git a/src/tui-opentui/lockup.test.ts b/src/tui/lockup.test.ts similarity index 100% rename from src/tui-opentui/lockup.test.ts rename to src/tui/lockup.test.ts diff --git a/src/tui-opentui/lockup.ts b/src/tui/lockup.ts similarity index 99% rename from src/tui-opentui/lockup.ts rename to src/tui/lockup.ts index 80d836976..79bbbd844 100644 --- a/src/tui-opentui/lockup.ts +++ b/src/tui/lockup.ts @@ -37,7 +37,7 @@ import { type MarkCell } from "./mark-anim.js" import { rampFg, rampPulse, type RampPhase, type StallAge } from "./ramp.js" import { UI } from "./theme.js" -import { stringWidth } from "../tui/view/height.js" +import { stringWidth } from "./view/height.js" export const LOCKUP_WORDMARK = "corbits code" diff --git a/src/tui-opentui/log-sink.test.ts b/src/tui/log-sink.test.ts similarity index 100% rename from src/tui-opentui/log-sink.test.ts rename to src/tui/log-sink.test.ts diff --git a/src/tui-opentui/long-log.ts b/src/tui/long-log.ts similarity index 100% rename from src/tui-opentui/long-log.ts rename to src/tui/long-log.ts diff --git a/src/tui-opentui/margins.test.ts b/src/tui/margins.test.ts similarity index 100% rename from src/tui-opentui/margins.test.ts rename to src/tui/margins.test.ts diff --git a/src/tui-opentui/mark-anim.test.ts b/src/tui/mark-anim.test.ts similarity index 100% rename from src/tui-opentui/mark-anim.test.ts rename to src/tui/mark-anim.test.ts diff --git a/src/tui-opentui/mark-anim.ts b/src/tui/mark-anim.ts similarity index 100% rename from src/tui-opentui/mark-anim.ts rename to src/tui/mark-anim.ts diff --git a/src/tui-opentui/mark-shape.ts b/src/tui/mark-shape.ts similarity index 100% rename from src/tui-opentui/mark-shape.ts rename to src/tui/mark-shape.ts diff --git a/src/tui/markdown-parser.test.ts b/src/tui/markdown-parser.test.ts index febab4045..80575cc63 100644 --- a/src/tui/markdown-parser.test.ts +++ b/src/tui/markdown-parser.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { createMemoizedParseMarkdown, parseMarkdown, type StyledSegment } from "./markdown-parser.js"; -import { color } from "./theme.js"; +import { color } from "./semantic-theme.js"; function firstLine(text: string): StyledSegment[] { return parseMarkdown(text)[0] ?? []; diff --git a/src/tui-opentui/markdown-rows.test.ts b/src/tui/markdown-rows.test.ts similarity index 100% rename from src/tui-opentui/markdown-rows.test.ts rename to src/tui/markdown-rows.test.ts diff --git a/src/tui-opentui/mcp-view.test.ts b/src/tui/mcp-view.test.ts similarity index 99% rename from src/tui-opentui/mcp-view.test.ts rename to src/tui/mcp-view.test.ts index 403b603ad..1d94c2ca4 100644 --- a/src/tui-opentui/mcp-view.test.ts +++ b/src/tui/mcp-view.test.ts @@ -8,7 +8,7 @@ import { describe, expect, test } from "bun:test" import { extractMcpRecord, extractMcpRecords, -} from "../tui/mcp-result-format.js" +} from "./mcp-result-format.js" import { toolCallRow } from "./diff" import { withTestRenderer, type Harness } from "./harness" import { mcpStructuredView, toolResultRow } from "./mcp-view" diff --git a/src/tui-opentui/mcp-view.ts b/src/tui/mcp-view.ts similarity index 99% rename from src/tui-opentui/mcp-view.ts rename to src/tui/mcp-view.ts index ba5dbb36f..01d4c7cee 100644 --- a/src/tui-opentui/mcp-view.ts +++ b/src/tui/mcp-view.ts @@ -17,7 +17,7 @@ import { fg as fgChunk, bold as boldChunk, type TextChunk } from "@opentui/core" -import { sliceToWidth, stringWidth } from "../tui/view/height.js" +import { sliceToWidth, stringWidth } from "./view/height.js" import { humanizeMcpTool, isMcpToolName, mcpToolWords, parseMcpToolName } from "../mcp/tool-name.js" import { UI } from "./theme.js" import { @@ -25,8 +25,8 @@ import { extractMcpRecords, recordScalar, type McpRecords, -} from "../tui/mcp-result-format.js" -import { summarizeToolResult } from "../tui/tool-formatter.js" +} from "./mcp-result-format.js" +import { summarizeToolResult } from "./tool-formatter.js" import type { StreamRow, StyledBodyLine } from "./stream.js" export type McpTone = diff --git a/src/tui-opentui/mention-filter.test.ts b/src/tui/mention-filter.test.ts similarity index 93% rename from src/tui-opentui/mention-filter.test.ts rename to src/tui/mention-filter.test.ts index ee303f336..eeb4c42bd 100644 --- a/src/tui-opentui/mention-filter.test.ts +++ b/src/tui/mention-filter.test.ts @@ -48,8 +48,8 @@ describe("filterMentionSuggestions", () => { }) test("directory entries keep their trailing slash", () => { - expect(filterMentionSuggestions(["src/tui-opentui/"], "opentui")).toEqual([ - "src/tui-opentui/", + expect(filterMentionSuggestions(["src/tui/"], "tui")).toEqual([ + "src/tui/", ]) }) }) diff --git a/src/tui-opentui/mention-filter.ts b/src/tui/mention-filter.ts similarity index 100% rename from src/tui-opentui/mention-filter.ts rename to src/tui/mention-filter.ts diff --git a/src/tui-opentui/mention-popup.test.ts b/src/tui/mention-popup.test.ts similarity index 100% rename from src/tui-opentui/mention-popup.test.ts rename to src/tui/mention-popup.test.ts diff --git a/src/tui-opentui/model-catalog.test.ts b/src/tui/model-catalog.test.ts similarity index 100% rename from src/tui-opentui/model-catalog.test.ts rename to src/tui/model-catalog.test.ts diff --git a/src/tui-opentui/model-catalog.ts b/src/tui/model-catalog.ts similarity index 100% rename from src/tui-opentui/model-catalog.ts rename to src/tui/model-catalog.ts diff --git a/src/tui-opentui/mouse-reporting-disabled.test.ts b/src/tui/mouse-reporting-disabled.test.ts similarity index 100% rename from src/tui-opentui/mouse-reporting-disabled.test.ts rename to src/tui/mouse-reporting-disabled.test.ts diff --git a/src/tui-opentui/notice-line.test.ts b/src/tui/notice-line.test.ts similarity index 100% rename from src/tui-opentui/notice-line.test.ts rename to src/tui/notice-line.test.ts diff --git a/src/tui-opentui/notice-line.ts b/src/tui/notice-line.ts similarity index 100% rename from src/tui-opentui/notice-line.ts rename to src/tui/notice-line.ts diff --git a/src/tui-opentui/observe-live.test.ts b/src/tui/observe-live.test.ts similarity index 100% rename from src/tui-opentui/observe-live.test.ts rename to src/tui/observe-live.test.ts diff --git a/src/tui-opentui/observe-map.ts b/src/tui/observe-map.ts similarity index 100% rename from src/tui-opentui/observe-map.ts rename to src/tui/observe-map.ts diff --git a/src/tui/onboarding.ts b/src/tui/onboarding.ts index 7536a5687..3bcb9f31f 100644 --- a/src/tui/onboarding.ts +++ b/src/tui/onboarding.ts @@ -3,7 +3,7 @@ import { buildProviderSubmitHandler } from "./provider-setup-submit.js"; import { loadConfig, type UnconfiguredConfig } from "../config/index.js"; import { globalSettingsPath, loadSettings } from "../config/settings.js"; import { activateHeldTelemetry, telemetryFirstRunPending } from "../telemetry/first-run.js"; -import { runProviderSetup } from "../tui-opentui/provider-setup.js"; +import { runProviderSetup } from "./provider-setup.js"; export async function runOnboarding(config: UnconfiguredConfig): Promise { const settingsPath = config.globalSettingsPath; diff --git a/src/tui-opentui/overlay-body.test.ts b/src/tui/overlay-body.test.ts similarity index 99% rename from src/tui-opentui/overlay-body.test.ts rename to src/tui/overlay-body.test.ts index e2ad9754f..749f2401c 100644 --- a/src/tui-opentui/overlay-body.test.ts +++ b/src/tui/overlay-body.test.ts @@ -19,7 +19,7 @@ const SENTENCE = "The agent wants to run a destructive command on the working tree and this cannot be undone" const LONG_PATH = - "/Users/someone/abklabs/corbits-code/src/tui-opentui/geometry/margins.ts" + "/Users/someone/abklabs/corbits-code/src/tui/geometry/margins.ts" const LONG_URL = "https://registry.internal.example.com/artifactory/api/npm/npm-virtual/package" diff --git a/src/tui-opentui/overlay-body.ts b/src/tui/overlay-body.ts similarity index 99% rename from src/tui-opentui/overlay-body.ts rename to src/tui/overlay-body.ts index 0a51f0839..f0b6a7e20 100644 --- a/src/tui-opentui/overlay-body.ts +++ b/src/tui/overlay-body.ts @@ -14,7 +14,7 @@ */ import { middleEllipsis } from "./command-display.js" -import { prefixIndexForWidth, stringWidth } from "../tui/view/height.js" +import { prefixIndexForWidth, stringWidth } from "./view/height.js" import { UI } from "./theme.js" /** House ordered-dither ramp, sparsest-first, leading the header. */ diff --git a/src/tui-opentui/overlay-fixture-fallback.test.ts b/src/tui/overlay-fixture-fallback.test.ts similarity index 100% rename from src/tui-opentui/overlay-fixture-fallback.test.ts rename to src/tui/overlay-fixture-fallback.test.ts diff --git a/src/tui-opentui/overlay-paint.test.ts b/src/tui/overlay-paint.test.ts similarity index 100% rename from src/tui-opentui/overlay-paint.test.ts rename to src/tui/overlay-paint.test.ts diff --git a/src/tui-opentui/overlays.test.ts b/src/tui/overlays.test.ts similarity index 100% rename from src/tui-opentui/overlays.test.ts rename to src/tui/overlays.test.ts diff --git a/src/tui-opentui/overlays.ts b/src/tui/overlays.ts similarity index 100% rename from src/tui-opentui/overlays.ts rename to src/tui/overlays.ts diff --git a/src/tui-opentui/palette-paint.test.ts b/src/tui/palette-paint.test.ts similarity index 100% rename from src/tui-opentui/palette-paint.test.ts rename to src/tui/palette-paint.test.ts diff --git a/src/tui/pick-session.ts b/src/tui/pick-session.ts index f175878eb..80fa5edb5 100644 --- a/src/tui/pick-session.ts +++ b/src/tui/pick-session.ts @@ -1,6 +1,6 @@ import { COMMAND_NAME } from "../branding.js"; import { listSessions, type SessionSummary } from "../session/index.js"; -import { runListModal } from "../tui-opentui/list-modal.js"; +import { runListModal } from "./list-modal.js"; import { formatRelativeTime } from "./format-relative-time.js"; // Interrupted (cancelled) sessions are prime resume candidates alongside diff --git a/src/tui-opentui/product-host.test.ts b/src/tui/product-host.test.ts similarity index 100% rename from src/tui-opentui/product-host.test.ts rename to src/tui/product-host.test.ts diff --git a/src/tui-opentui/product-host.ts b/src/tui/product-host.ts similarity index 99% rename from src/tui-opentui/product-host.ts rename to src/tui/product-host.ts index 07c144d4b..d0af6fa1b 100644 --- a/src/tui-opentui/product-host.ts +++ b/src/tui/product-host.ts @@ -57,7 +57,7 @@ import type { QueueKind } from "./session-queue.js" import { hydrateHistoryRows } from "./history-hydrate.js" import type { StreamRow } from "./stream.js" -import type { PendingImageAttachment } from "../tui/image-attachments.js" +import type { PendingImageAttachment } from "./image-attachments.js" const PROVIDER_GROUP_PREFIX = "providerGroup:" diff --git a/src/tui-opentui/prompt-attachments.test.ts b/src/tui/prompt-attachments.test.ts similarity index 98% rename from src/tui-opentui/prompt-attachments.test.ts rename to src/tui/prompt-attachments.test.ts index 62f7360b5..3864836e4 100644 --- a/src/tui-opentui/prompt-attachments.test.ts +++ b/src/tui/prompt-attachments.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" -import type { AttachImageResult, PendingImageAttachment } from "../tui/image-attachments.js" +import type { AttachImageResult, PendingImageAttachment } from "./image-attachments.js" import { ingestPathMentions, spliceMentionCompletion, diff --git a/src/tui-opentui/prompt-attachments.ts b/src/tui/prompt-attachments.ts similarity index 98% rename from src/tui-opentui/prompt-attachments.ts rename to src/tui/prompt-attachments.ts index f82bf7067..4a43911ef 100644 --- a/src/tui-opentui/prompt-attachments.ts +++ b/src/tui/prompt-attachments.ts @@ -8,7 +8,7 @@ import { findImagePathMentions, type AttachImageResult, type PendingImageAttachment, -} from "../tui/image-attachments.js" +} from "./image-attachments.js" export type { PendingImageAttachment } diff --git a/src/tui-opentui/prompt-border.test.ts b/src/tui/prompt-border.test.ts similarity index 100% rename from src/tui-opentui/prompt-border.test.ts rename to src/tui/prompt-border.test.ts diff --git a/src/tui-opentui/prompt-border.ts b/src/tui/prompt-border.ts similarity index 99% rename from src/tui-opentui/prompt-border.ts rename to src/tui/prompt-border.ts index 8407e4ad8..27ac13537 100644 --- a/src/tui-opentui/prompt-border.ts +++ b/src/tui/prompt-border.ts @@ -12,7 +12,7 @@ * lockup's exact width is passed in rather than the cells themselves. */ -import { stringWidth } from "../tui/view/height.js" +import { stringWidth } from "./view/height.js" import { renderRamp } from "./ramp.js" import { formatContextPercentLabel } from "../cost/cost-summary.js" diff --git a/src/tui-opentui/prompt-box.test.ts b/src/tui/prompt-box.test.ts similarity index 100% rename from src/tui-opentui/prompt-box.test.ts rename to src/tui/prompt-box.test.ts diff --git a/src/tui-opentui/prompt-chrome.test.ts b/src/tui/prompt-chrome.test.ts similarity index 100% rename from src/tui-opentui/prompt-chrome.test.ts rename to src/tui/prompt-chrome.test.ts diff --git a/src/tui-opentui/prompt-features.test.ts b/src/tui/prompt-features.test.ts similarity index 98% rename from src/tui-opentui/prompt-features.test.ts rename to src/tui/prompt-features.test.ts index c8dfe3e37..7178bb3ea 100644 --- a/src/tui-opentui/prompt-features.test.ts +++ b/src/tui/prompt-features.test.ts @@ -5,7 +5,7 @@ */ import { describe, expect, test } from "bun:test" -import type { PendingImageAttachment } from "../tui/image-attachments.js" +import type { PendingImageAttachment } from "./image-attachments.js" import { withTestRenderer, type Harness } from "./harness" import { acceptOverlaySelection, @@ -447,14 +447,14 @@ describe("@-mention suggestions", () => { // so the source is asked for `src/`, not the whole typed token. setMentionSuggestionSource(shell, async (prefix) => { expect(prefix).toBe("src/") - return ["src/tui/", "src/tui-opentui/", "src/config/"] + return ["src/tui/", "src/telemetry/", "src/config/"] }) - shell.prompt.value = "read @src/tu" + shell.prompt.value = "read @src/t" shell.prompt.cursorOffset = shell.prompt.value.length expect(await openAtMentionSuggestions(shell)).toBe(true) expect(shell.overlayKind).toBe("mentions") - expect(shell.overlayItems).toEqual(["src/tui/", "src/tui-opentui/"]) + expect(shell.overlayItems).toEqual(["src/tui/", "src/telemetry/"]) }) }) diff --git a/src/tui-opentui/prompt-highlight.test.ts b/src/tui/prompt-highlight.test.ts similarity index 100% rename from src/tui-opentui/prompt-highlight.test.ts rename to src/tui/prompt-highlight.test.ts diff --git a/src/tui-opentui/prompt-input.ts b/src/tui/prompt-input.ts similarity index 100% rename from src/tui-opentui/prompt-input.ts rename to src/tui/prompt-input.ts diff --git a/src/tui-opentui/prompt-kill-ring.test.ts b/src/tui/prompt-kill-ring.test.ts similarity index 100% rename from src/tui-opentui/prompt-kill-ring.test.ts rename to src/tui/prompt-kill-ring.test.ts diff --git a/src/tui-opentui/prompt-kill-ring.ts b/src/tui/prompt-kill-ring.ts similarity index 100% rename from src/tui-opentui/prompt-kill-ring.ts rename to src/tui/prompt-kill-ring.ts diff --git a/src/tui-opentui/prompt-recognition.test.ts b/src/tui/prompt-recognition.test.ts similarity index 100% rename from src/tui-opentui/prompt-recognition.test.ts rename to src/tui/prompt-recognition.test.ts diff --git a/src/tui-opentui/prompt-recognition.ts b/src/tui/prompt-recognition.ts similarity index 100% rename from src/tui-opentui/prompt-recognition.ts rename to src/tui/prompt-recognition.ts diff --git a/src/tui-opentui/prompt-rows.test.ts b/src/tui/prompt-rows.test.ts similarity index 100% rename from src/tui-opentui/prompt-rows.test.ts rename to src/tui/prompt-rows.test.ts diff --git a/src/tui-opentui/prompt-rows.ts b/src/tui/prompt-rows.ts similarity index 100% rename from src/tui-opentui/prompt-rows.ts rename to src/tui/prompt-rows.ts diff --git a/src/tui-opentui/prompt-slash-exit.test.ts b/src/tui/prompt-slash-exit.test.ts similarity index 100% rename from src/tui-opentui/prompt-slash-exit.test.ts rename to src/tui/prompt-slash-exit.test.ts diff --git a/src/tui-opentui/provider-connect.test.ts b/src/tui/provider-connect.test.ts similarity index 100% rename from src/tui-opentui/provider-connect.test.ts rename to src/tui/provider-connect.test.ts diff --git a/src/tui-opentui/provider-connect.ts b/src/tui/provider-connect.ts similarity index 96% rename from src/tui-opentui/provider-connect.ts rename to src/tui/provider-connect.ts index 37a9f32ae..9755a6768 100644 --- a/src/tui-opentui/provider-connect.ts +++ b/src/tui/provider-connect.ts @@ -6,7 +6,7 @@ */ import type { Settings } from "../config/settings.js" -import { buildProviderSubmitHandler } from "../tui/provider-setup-submit.js" +import { buildProviderSubmitHandler } from "./provider-setup-submit.js" import { runProviderSetup, type ProviderSetupConfig } from "./provider-setup.js" export type ConnectProviderInput = { diff --git a/src/tui/provider-setup-submit.test.ts b/src/tui/provider-setup-submit.test.ts index 9759b76fb..cbdf1d022 100644 --- a/src/tui/provider-setup-submit.test.ts +++ b/src/tui/provider-setup-submit.test.ts @@ -5,7 +5,7 @@ import { join } from "node:path"; import { buildProviderSubmitHandler } from "./provider-setup-submit.js"; import { loadSettings } from "../config/settings.js"; -import type { ProviderFormValues, SubmitPhase } from "../tui-opentui/provider-setup.js"; +import type { ProviderFormValues, SubmitPhase } from "./provider-setup.js"; const noopSetPhase = (_phase: SubmitPhase): void => {}; diff --git a/src/tui/provider-setup-submit.ts b/src/tui/provider-setup-submit.ts index 6f0ad6991..d278cbf36 100644 --- a/src/tui/provider-setup-submit.ts +++ b/src/tui/provider-setup-submit.ts @@ -6,7 +6,7 @@ import { type Settings, } from "../config/settings.js"; import { validateProviderConnection } from "../provider/validate-connection.js"; -import type { ProviderSetupSubmit } from "../tui-opentui/provider-setup.js"; +import type { ProviderSetupSubmit } from "./provider-setup.js"; /** * The single write path every provider-setup exit takes, shared by first-run diff --git a/src/tui-opentui/provider-setup.test.ts b/src/tui/provider-setup.test.ts similarity index 100% rename from src/tui-opentui/provider-setup.test.ts rename to src/tui/provider-setup.test.ts diff --git a/src/tui-opentui/provider-setup.ts b/src/tui/provider-setup.ts similarity index 99% rename from src/tui-opentui/provider-setup.ts rename to src/tui/provider-setup.ts index 74008c49a..b0c61f1bf 100644 --- a/src/tui-opentui/provider-setup.ts +++ b/src/tui/provider-setup.ts @@ -32,7 +32,7 @@ import { PRODUCT_NAME } from "../branding.js" import { codexProviderName } from "../config/codex-providers.js" import { xaiProviderName } from "../config/xai-providers.js" import { TELEMETRY_NOTICE } from "../telemetry/index.js" -import { wrapLines } from "../tui/view/height.js" +import { wrapLines } from "./view/height.js" import { resolveSideMargin } from "./geometry/margins.js" import { createListViewport, diff --git a/src/tui-opentui/quota-retry.test.ts b/src/tui/quota-retry.test.ts similarity index 100% rename from src/tui-opentui/quota-retry.test.ts rename to src/tui/quota-retry.test.ts diff --git a/src/tui-opentui/quota-retry.ts b/src/tui/quota-retry.ts similarity index 100% rename from src/tui-opentui/quota-retry.ts rename to src/tui/quota-retry.ts diff --git a/src/tui-opentui/ramp-paint.test.ts b/src/tui/ramp-paint.test.ts similarity index 100% rename from src/tui-opentui/ramp-paint.test.ts rename to src/tui/ramp-paint.test.ts diff --git a/src/tui-opentui/ramp.test.ts b/src/tui/ramp.test.ts similarity index 100% rename from src/tui-opentui/ramp.test.ts rename to src/tui/ramp.test.ts diff --git a/src/tui-opentui/ramp.ts b/src/tui/ramp.ts similarity index 100% rename from src/tui-opentui/ramp.ts rename to src/tui/ramp.ts diff --git a/src/tui-opentui/reasoning-fold.test.ts b/src/tui/reasoning-fold.test.ts similarity index 100% rename from src/tui-opentui/reasoning-fold.test.ts rename to src/tui/reasoning-fold.test.ts diff --git a/src/tui-opentui/render-loop.test.ts b/src/tui/render-loop.test.ts similarity index 100% rename from src/tui-opentui/render-loop.test.ts rename to src/tui/render-loop.test.ts diff --git a/src/tui-opentui/residuals.ts b/src/tui/residuals.ts similarity index 100% rename from src/tui-opentui/residuals.ts rename to src/tui/residuals.ts diff --git a/src/tui-opentui/row-click.test.ts b/src/tui/row-click.test.ts similarity index 100% rename from src/tui-opentui/row-click.test.ts rename to src/tui/row-click.test.ts diff --git a/src/tui-opentui/runner-host.test.ts b/src/tui/runner-host.test.ts similarity index 100% rename from src/tui-opentui/runner-host.test.ts rename to src/tui/runner-host.test.ts diff --git a/src/tui-opentui/runner-host.ts b/src/tui/runner-host.ts similarity index 98% rename from src/tui-opentui/runner-host.ts rename to src/tui/runner-host.ts index a8fd66df0..be0301b30 100644 --- a/src/tui-opentui/runner-host.ts +++ b/src/tui/runner-host.ts @@ -40,9 +40,9 @@ import { } from "./shell.js" import type { CostSummary } from "../cost/cost-summary.js" import { watchGitBranch, type FetchBranch } from "./workspace-watch.js" -import type { PromptActionBarModelLabelInput } from "../tui/components/prompt-action-bar-label.js" +import type { PromptActionBarModelLabelInput } from "./components/prompt-action-bar-label.js" import type { ObserveSession } from "./residuals.js" -import type { PendingImageAttachment } from "../tui/image-attachments.js" +import type { PendingImageAttachment } from "./image-attachments.js" import { toolCallRow } from "./diff.js" import { toolResultRow } from "./mcp-view.js" import { pushToolCall, pushToolResult } from "./tool-rows.js" diff --git a/src/tui/runner.ts b/src/tui/runner.ts index c9a25ac33..e8b9062c3 100644 --- a/src/tui/runner.ts +++ b/src/tui/runner.ts @@ -42,9 +42,9 @@ import { type LocalSettings, type PluginConfig, } from "../config/settings.js"; -import { unconnectedProviderChoices } from "../tui-opentui/provider-setup.js"; -import { connectProviderInline } from "../tui-opentui/provider-connect.js"; -import type { SessionModeScope } from "../tui-opentui/command-surfaces.js"; +import { unconnectedProviderChoices } from "./provider-setup.js"; +import { connectProviderInline } from "./provider-connect.js"; +import type { SessionModeScope } from "./command-surfaces.js"; import { resolveWaitForApproval, type ToolWatchdogConfig } from "./tool-execution-watchdog.js"; import { attachApprovalBudget, createGateRequestApproval } from "./request-approval.js"; import { codexProfileFromProviderName } from "../config/codex-providers.js"; @@ -145,7 +145,7 @@ import { scrubSecrets } from "../web/secret-scrub.js"; import { setActiveWebProviderBrand } from "./tool-formatter.js"; import { consumeStream } from "../session/stream-consumer.js"; import { createCycleTextRecorder } from "../session/stream-journal.js"; -import { mountRunnerHost } from "../tui-opentui/runner-host.js"; +import { mountRunnerHost } from "./runner-host.js"; import { attachClipboardImage, setMentionSuggestionSource, @@ -153,13 +153,13 @@ import { setSentMessageHistory, setShellRunState, surfaceSystemNotice, -} from "../tui-opentui/shell.js"; +} from "./shell.js"; import { captureAuthFailure, classifyAgentSendFailure, shouldSettleUiAfterSendFailure, -} from "../tui-opentui/session-chrome.js"; -import { ingestPathMentions } from "../tui-opentui/prompt-attachments.js"; +} from "./session-chrome.js"; +import { ingestPathMentions } from "./prompt-attachments.js"; import { listPathSuggestions } from "./components/at-mention/list.js"; import { resolveAtMentions } from "./mention-resolution.js"; import { diff --git a/src/tui-opentui/runtime-bridge.test.ts b/src/tui/runtime-bridge.test.ts similarity index 100% rename from src/tui-opentui/runtime-bridge.test.ts rename to src/tui/runtime-bridge.test.ts diff --git a/src/tui-opentui/runtime-bridge.ts b/src/tui/runtime-bridge.ts similarity index 99% rename from src/tui-opentui/runtime-bridge.ts rename to src/tui/runtime-bridge.ts index cbd573f59..0c60c2a8e 100644 --- a/src/tui-opentui/runtime-bridge.ts +++ b/src/tui/runtime-bridge.ts @@ -61,7 +61,7 @@ import { turnStateOnSubmit, type TurnState, } from "./turn-state.js" -import type { PendingImageAttachment } from "../tui/image-attachments.js" +import type { PendingImageAttachment } from "./image-attachments.js" import { toolCallRow } from "./diff.js" import { toolResultRow } from "./mcp-view.js" import { diff --git a/src/tui-opentui/runtime-channels.test.ts b/src/tui/runtime-channels.test.ts similarity index 100% rename from src/tui-opentui/runtime-channels.test.ts rename to src/tui/runtime-channels.test.ts diff --git a/src/tui-opentui/runtime-notices.test.ts b/src/tui/runtime-notices.test.ts similarity index 100% rename from src/tui-opentui/runtime-notices.test.ts rename to src/tui/runtime-notices.test.ts diff --git a/src/tui-opentui/runtime-notices.ts b/src/tui/runtime-notices.ts similarity index 100% rename from src/tui-opentui/runtime-notices.ts rename to src/tui/runtime-notices.ts diff --git a/src/tui/semantic-theme.ts b/src/tui/semantic-theme.ts new file mode 100644 index 000000000..9310288ab --- /dev/null +++ b/src/tui/semantic-theme.ts @@ -0,0 +1,131 @@ +export type ColorValue = { hex: string; ansi256: number }; + +export type SemanticRole = + | "brand" + | "accent" + | "success" + | "danger" + | "warning" + | "muted" + | "dim" + | "live" + | "text" + | "emphasis" + | "surface" + | "syntaxKeyword" + | "syntaxString" + | "syntaxComment" + | "syntaxFunction" + | "syntaxNumber" + | "syntaxType" + | "syntaxOperator" + | "syntaxPunctuation" + | "syntaxVariable" + | "markdownHeading" + | "markdownLink" + | "markdownCode" + | "markdownBlockquote" + | "markdownEmphasis" + | "markdownStrong" + | "diffAdded" + | "diffRemoved" + | "diffContext" + | "diffHunkHeader" + | "diffAddedBg" + | "diffRemovedBg" + | "userMessageBg" + | "toolPendingBg" + | "toolSuccessBg" + | "toolErrorBg"; + +const breakthroughOrange: ColorValue = { hex: "#f5933a", ansi256: 173 }; +const summitBlue: ColorValue = { hex: "#7ea2c4", ansi256: 74 }; +const ridgeGreen: ColorValue = { hex: "#94b889", ansi256: 108 }; +const bedrockCharcoal: ColorValue = { hex: "#2b2627", ansi256: 235 }; +// Body prose. A calm warm off-white rather than near-white cream so a wall of +// text does not read as heavy; emphasis and headings sit above it in brightness. +const bodyOffWhite: ColorValue = { hex: "#d0c7bb", ansi256: 250 }; +// The brightest step in the ladder — carries inline emphasis so strong text +// reads as brighter rather than shouting in bold weight. +const emphasisCream: ColorValue = { hex: "#faf1e2", ansi256: 230 }; +const dangerRed: ColorValue = { hex: "#e0594d", ansi256: 167 }; +const mutedGray: ColorValue = { hex: "#a89f96", ansi256: 247 }; +// One step dimmer than muted — used for tool args, collapsed results, thinking gutter. +const dimGray: ColorValue = { hex: "#736c66", ansi256: 243 }; +const heatherPurple: ColorValue = { hex: "#b48ead", ansi256: 139 }; +const duskOrange: ColorValue = { hex: "#d19a66", ansi256: 173 }; +const sandGold: ColorValue = { hex: "#e5c07b", ansi256: 180 }; +// A half-step below muted so punctuation recedes behind identifiers without +// falling all the way to the dim rung used by comments. +const graphiteGray: ColorValue = { hex: "#8a827a", ansi256: 244 }; +// Between body text and emphasis cream — italic emphasis lifts slightly above +// prose while strong text keeps the top brightness rung. +const parchment: ColorValue = { hex: "#e8ddcc", ansi256: 253 }; +const pineShadow: ColorValue = { hex: "#293a28", ansi256: 22 }; +const emberShadow: ColorValue = { hex: "#3d2a28", ansi256: 52 }; +// Cool gray in an otherwise warm palette; kept deliberately so the user's own +// words sit apart from everything the model produces. +const coolGray: ColorValue = { hex: "#45454a", ansi256: 238 }; +// Status-card washes for tool rows: a bare tint over bedrockCharcoal, one step +// up in lightness so the row reads as "tinted" rather than a lit panel. Kept +// close in luminance to the surface color deliberately — a state change +// should be noticeable at a glance, not compete with foreground text. +const pendingWash: ColorValue = { hex: "#21303a", ansi256: 24 }; +const successWash: ColorValue = { hex: "#20291f", ansi256: 22 }; +const errorWash: ColorValue = { hex: "#2e2020", ansi256: 52 }; + +export const palette: Record = { + brand: breakthroughOrange, + accent: summitBlue, + success: ridgeGreen, + danger: dangerRed, + warning: breakthroughOrange, + muted: mutedGray, + dim: dimGray, + // Spinner/streaming indicator color — calm blue rather than brand orange. + live: summitBlue, + text: bodyOffWhite, + emphasis: emphasisCream, + surface: bedrockCharcoal, + syntaxKeyword: heatherPurple, + syntaxString: ridgeGreen, + syntaxComment: dimGray, + syntaxFunction: summitBlue, + syntaxNumber: duskOrange, + syntaxType: sandGold, + syntaxOperator: mutedGray, + syntaxPunctuation: graphiteGray, + syntaxVariable: bodyOffWhite, + markdownHeading: emphasisCream, + markdownLink: summitBlue, + markdownCode: breakthroughOrange, + markdownBlockquote: mutedGray, + markdownEmphasis: parchment, + markdownStrong: emphasisCream, + diffAdded: ridgeGreen, + diffRemoved: dangerRed, + // Carries the full dimmed appearance of unchanged rows; renderers should not + // stack an extra dim attribute on top. + diffContext: dimGray, + diffHunkHeader: summitBlue, + diffAddedBg: pineShadow, + diffRemovedBg: emberShadow, + userMessageBg: coolGray, + toolPendingBg: pendingWash, + toolSuccessBg: successWash, + toolErrorBg: errorWash, +}; + +export function color(role: SemanticRole): string { + return supportsTrueColor() ? palette[role].hex : `ansi256(${color256(role)})`; +} + +export function color256(role: SemanticRole): number { + return palette[role].ansi256; +} + +export function supportsTrueColor(): boolean { + const colorterm = process.env.COLORTERM; + if (colorterm === undefined) return false; + return colorterm === "truecolor" || colorterm === "24bit"; +} diff --git a/src/tui-opentui/session-chrome.test.ts b/src/tui/session-chrome.test.ts similarity index 100% rename from src/tui-opentui/session-chrome.test.ts rename to src/tui/session-chrome.test.ts diff --git a/src/tui-opentui/session-chrome.ts b/src/tui/session-chrome.ts similarity index 100% rename from src/tui-opentui/session-chrome.ts rename to src/tui/session-chrome.ts diff --git a/src/tui/session-mode-prompt.ts b/src/tui/session-mode-prompt.ts index 48a027374..2099e8c64 100644 --- a/src/tui/session-mode-prompt.ts +++ b/src/tui/session-mode-prompt.ts @@ -5,7 +5,7 @@ import { } from "../config/settings.js"; import type { SessionMode } from "../config/session-mode.js"; import { COMMAND_NAME } from "../branding.js"; -import { runListModal } from "../tui-opentui/list-modal.js"; +import { runListModal } from "./list-modal.js"; const OPTIONS: readonly { mode: SessionMode; title: string; description: string }[] = [ { diff --git a/src/tui-opentui/session-queue.test.ts b/src/tui/session-queue.test.ts similarity index 100% rename from src/tui-opentui/session-queue.test.ts rename to src/tui/session-queue.test.ts diff --git a/src/tui-opentui/session-queue.ts b/src/tui/session-queue.ts similarity index 98% rename from src/tui-opentui/session-queue.ts rename to src/tui/session-queue.ts index 851e90392..08acae4fc 100644 --- a/src/tui-opentui/session-queue.ts +++ b/src/tui/session-queue.ts @@ -3,7 +3,7 @@ * Pure data — no paint, no OpenTUI. Shell + demo own delivery and UI flash. */ -import type { PendingImageAttachment } from "../tui/image-attachments.js" +import type { PendingImageAttachment } from "./image-attachments.js" export type QueueKind = "queue" | "steer" diff --git a/src/tui-opentui/shell.test.ts b/src/tui/shell.test.ts similarity index 100% rename from src/tui-opentui/shell.test.ts rename to src/tui/shell.test.ts diff --git a/src/tui-opentui/shell.ts b/src/tui/shell.ts similarity index 99% rename from src/tui-opentui/shell.ts rename to src/tui/shell.ts index a601fa379..1412a82ff 100644 --- a/src/tui-opentui/shell.ts +++ b/src/tui/shell.ts @@ -26,27 +26,27 @@ import { type TextChunk, } from "@opentui/core" -import { isExitCommand } from "../tui/exit-command.js" +import { isExitCommand } from "./exit-command.js" import { composePromptActionBarModelLabel, type PromptActionBarModelLabelInput, -} from "../tui/components/prompt-action-bar-label.js" -import { sliceTailToWidth, sliceToWidth, stringWidth } from "../tui/view/height.js" -import { listPathSuggestions } from "../tui/components/at-mention/list.js" -import { parseAtState } from "../tui/components/at-mention/parse.js" +} from "./components/prompt-action-bar-label.js" +import { sliceTailToWidth, sliceToWidth, stringWidth } from "./view/height.js" +import { listPathSuggestions } from "./components/at-mention/list.js" +import { parseAtState } from "./components/at-mention/parse.js" import { formatAttachmentSummary, readClipboardImage, type ClipboardImageResult, type PendingImageAttachment, -} from "../tui/image-attachments.js" +} from "./image-attachments.js" import { createSentHistoryBrowse, sentHistoryOnEdit, stepSentHistoryDown, stepSentHistoryUp, type SentHistoryBrowse, -} from "../tui/sent-message-history.js" +} from "./sent-message-history.js" import { spliceMentionCompletion } from "./prompt-attachments.js" import { resolvePromptHighlightSpans, diff --git a/src/tui-opentui/smoke.ts b/src/tui/smoke.ts similarity index 83% rename from src/tui-opentui/smoke.ts rename to src/tui/smoke.ts index a101decc4..2f36c9c96 100644 --- a/src/tui-opentui/smoke.ts +++ b/src/tui/smoke.ts @@ -1,6 +1,6 @@ /** * Headless import check for @opentui/core (no TTY required). - * Run: bun ./src/tui-opentui/smoke.ts + * Run: bun ./src/tui/smoke.ts */ import "@opentui/core" import { PLATFORM_VERSION } from "./index" diff --git a/src/tui-opentui/stall-watchdog.test.ts b/src/tui/stall-watchdog.test.ts similarity index 100% rename from src/tui-opentui/stall-watchdog.test.ts rename to src/tui/stall-watchdog.test.ts diff --git a/src/tui-opentui/stall-watchdog.ts b/src/tui/stall-watchdog.ts similarity index 100% rename from src/tui-opentui/stall-watchdog.ts rename to src/tui/stall-watchdog.ts diff --git a/src/tui-opentui/stream-event-map.test.ts b/src/tui/stream-event-map.test.ts similarity index 100% rename from src/tui-opentui/stream-event-map.test.ts rename to src/tui/stream-event-map.test.ts diff --git a/src/tui-opentui/stream-event-map.ts b/src/tui/stream-event-map.ts similarity index 100% rename from src/tui-opentui/stream-event-map.ts rename to src/tui/stream-event-map.ts diff --git a/src/tui-opentui/stream.test.ts b/src/tui/stream.test.ts similarity index 99% rename from src/tui-opentui/stream.test.ts rename to src/tui/stream.test.ts index dafcfff33..c1d3054cf 100644 --- a/src/tui-opentui/stream.test.ts +++ b/src/tui/stream.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { stringWidth } from "../tui/view/height" +import { stringWidth } from "./view/height" import { agentVoicesIn, blockLabel, diff --git a/src/tui-opentui/stream.ts b/src/tui/stream.ts similarity index 99% rename from src/tui-opentui/stream.ts rename to src/tui/stream.ts index 8655a7f22..02a2866a5 100644 --- a/src/tui-opentui/stream.ts +++ b/src/tui/stream.ts @@ -5,7 +5,7 @@ import { SyntaxStyle } from "@opentui/core" -import { stringWidth, wrapLines } from "../tui/view/height.js" +import { stringWidth, wrapLines } from "./view/height.js" import type { DiffView } from "./diff.js" import type { McpStructuredView } from "./mcp-view.js" import { diff --git a/src/tui/syntax-highlight.test.ts b/src/tui/syntax-highlight.test.ts index f99e22475..544609ed0 100644 --- a/src/tui/syntax-highlight.test.ts +++ b/src/tui/syntax-highlight.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test"; import type { StyledSegment } from "./markdown-parser.js"; import { highlightCode } from "./syntax-highlight.js"; -import { color } from "./theme.js"; +import { color } from "./semantic-theme.js"; function segmentFor(lines: StyledSegment[][], text: string): StyledSegment | undefined { return lines.flat().find((seg) => seg.text === text); diff --git a/src/tui/syntax-highlight.ts b/src/tui/syntax-highlight.ts index 261880836..f3715b452 100644 --- a/src/tui/syntax-highlight.ts +++ b/src/tui/syntax-highlight.ts @@ -3,7 +3,7 @@ // already fall back to plain code. import hljs from "highlight.js/lib/common"; import type { StyledSegment } from "./markdown-parser.js"; -import { color, type SemanticRole } from "./theme.js"; +import { color, type SemanticRole } from "./semantic-theme.js"; // Map highlight.js scope classes to the semantic syntax palette. hljs emits a // class attribute whose first token is prefixed `hljs-` and whose optional diff --git a/src/tui-opentui/system-clipboard.test.ts b/src/tui/system-clipboard.test.ts similarity index 100% rename from src/tui-opentui/system-clipboard.test.ts rename to src/tui/system-clipboard.test.ts diff --git a/src/tui-opentui/system-clipboard.ts b/src/tui/system-clipboard.ts similarity index 100% rename from src/tui-opentui/system-clipboard.ts rename to src/tui/system-clipboard.ts diff --git a/src/tui-opentui/teardown.test.ts b/src/tui/teardown.test.ts similarity index 100% rename from src/tui-opentui/teardown.test.ts rename to src/tui/teardown.test.ts diff --git a/src/tui-opentui/teardown.ts b/src/tui/teardown.ts similarity index 100% rename from src/tui-opentui/teardown.ts rename to src/tui/teardown.ts diff --git a/src/tui/theme.ts b/src/tui/theme.ts index 9310288ab..0e578ad90 100644 --- a/src/tui/theme.ts +++ b/src/tui/theme.ts @@ -1,131 +1,102 @@ -export type ColorValue = { hex: string; ansi256: number }; +/** + * Corbits terminal palette — the single source of truth for every color the + * OpenTUI shell paints. + * + * Three rules the rest of the TUI depends on: + * + * 1. Gray never sits on the ground. Dimmed text is a dimmed *cream* + * (`textDim`, `textFaint`) so the warm bias survives at every emphasis + * level. There is deliberately no neutral gray in this file to reach for. + * 2. Orange is spent once per screen. It marks the session and whatever awaits + * a human decision — nothing else. Ongoing status uses the bronze ramp and + * `done` (green) so it never competes with the one thing asking to be + * answered. Diff removals are the sole exception: there the orange is + * content, not chrome, and no decision-marker shares the row. + * 3. The chrome ramp is warm but never saturated. Every bronze sits at or + * below 54% HSL saturation against Breakthrough Orange's 81%, so full + * orange still arrives as an event rather than as another shade of the + * furniture. + * + * The product owner has deliberately dropped Summit Blue from the terminal: + * cool information read as foreign against cream, black and orange chrome. The + * brand's discipline is kept — small palette, roles not decoration, no hue + * without a job — only the cool end of it is replaced by warm structure. + */ -export type SemanticRole = - | "brand" - | "accent" - | "success" - | "danger" - | "warning" - | "muted" - | "dim" - | "live" - | "text" - | "emphasis" - | "surface" - | "syntaxKeyword" - | "syntaxString" - | "syntaxComment" - | "syntaxFunction" - | "syntaxNumber" - | "syntaxType" - | "syntaxOperator" - | "syntaxPunctuation" - | "syntaxVariable" - | "markdownHeading" - | "markdownLink" - | "markdownCode" - | "markdownBlockquote" - | "markdownEmphasis" - | "markdownStrong" - | "diffAdded" - | "diffRemoved" - | "diffContext" - | "diffHunkHeader" - | "diffAddedBg" - | "diffRemovedBg" - | "userMessageBg" - | "toolPendingBg" - | "toolSuccessBg" - | "toolErrorBg"; +/** + * Palette values a theme supplies. Call sites paint through `UI`, never + * through a theme directly, so a second theme is a data change here. + */ +export type Theme = { + readonly name: string + /** Terminal ground. Foreground-only discipline means almost nothing fills it. */ + readonly ground: string + /** All body text. Never white, never gray. */ + readonly text: string + /** Secondary text: labels, context lines, chrome. */ + readonly textDim: string + /** Lowest emphasis: comments, concealed markdown syntax. */ + readonly textFaint: string + /** The session mark and anything awaiting a human decision. */ + readonly action: string + readonly actionDim: string + /** Work in progress: ramps, tool verbs, machine output threaded into prose. */ + readonly inFlight: string + /** The tier above body text that still reads as machine: keywords, links, args. */ + readonly inFlightBright: string + /** Document structure: markdown headings and section rules. */ + readonly heading: string + /** Completed and succeeded. */ + readonly done: string +} -const breakthroughOrange: ColorValue = { hex: "#f5933a", ansi256: 173 }; -const summitBlue: ColorValue = { hex: "#7ea2c4", ansi256: 74 }; -const ridgeGreen: ColorValue = { hex: "#94b889", ansi256: 108 }; -const bedrockCharcoal: ColorValue = { hex: "#2b2627", ansi256: 235 }; -// Body prose. A calm warm off-white rather than near-white cream so a wall of -// text does not read as heavy; emphasis and headings sit above it in brightness. -const bodyOffWhite: ColorValue = { hex: "#d0c7bb", ansi256: 250 }; -// The brightest step in the ladder — carries inline emphasis so strong text -// reads as brighter rather than shouting in bold weight. -const emphasisCream: ColorValue = { hex: "#faf1e2", ansi256: 230 }; -const dangerRed: ColorValue = { hex: "#e0594d", ansi256: 167 }; -const mutedGray: ColorValue = { hex: "#a89f96", ansi256: 247 }; -// One step dimmer than muted — used for tool args, collapsed results, thinking gutter. -const dimGray: ColorValue = { hex: "#736c66", ansi256: 243 }; -const heatherPurple: ColorValue = { hex: "#b48ead", ansi256: 139 }; -const duskOrange: ColorValue = { hex: "#d19a66", ansi256: 173 }; -const sandGold: ColorValue = { hex: "#e5c07b", ansi256: 180 }; -// A half-step below muted so punctuation recedes behind identifiers without -// falling all the way to the dim rung used by comments. -const graphiteGray: ColorValue = { hex: "#8a827a", ansi256: 244 }; -// Between body text and emphasis cream — italic emphasis lifts slightly above -// prose while strong text keeps the top brightness rung. -const parchment: ColorValue = { hex: "#e8ddcc", ansi256: 253 }; -const pineShadow: ColorValue = { hex: "#293a28", ansi256: 22 }; -const emberShadow: ColorValue = { hex: "#3d2a28", ansi256: 52 }; -// Cool gray in an otherwise warm palette; kept deliberately so the user's own -// words sit apart from everything the model produces. -const coolGray: ColorValue = { hex: "#45454a", ansi256: 238 }; -// Status-card washes for tool rows: a bare tint over bedrockCharcoal, one step -// up in lightness so the row reads as "tinted" rather than a lit panel. Kept -// close in luminance to the surface color deliberately — a state change -// should be noticeable at a glance, not compete with foreground text. -const pendingWash: ColorValue = { hex: "#21303a", ansi256: 24 }; -const successWash: ColorValue = { hex: "#20291f", ansi256: 22 }; -const errorWash: ColorValue = { hex: "#2e2020", ansi256: 52 }; +/** + * Brand hues, plus the warm ramp that replaced Summit Blue. + * + * Lowercase because the renderer normalizes hex that way, and tests compare a + * painted span's `fg` against these constants directly. + */ +export const BRAND = { + // Charcoal rather than pure black: it lifts the interface off the host + // terminal's own background and softens the cream's contrast edge, while + // staying dark enough that `textFaint` keeps a readable margin above it. + ground: "#191614", + canvasCream: "#f7ead5", + breakthroughOrange: "#e98428", + breakthroughOrangeDark: "#bf6b20", + ridgeGreen: "#7b9974", +} as const -export const palette: Record = { - brand: breakthroughOrange, - accent: summitBlue, - success: ridgeGreen, - danger: dangerRed, - warning: breakthroughOrange, - muted: mutedGray, - dim: dimGray, - // Spinner/streaming indicator color — calm blue rather than brand orange. - live: summitBlue, - text: bodyOffWhite, - emphasis: emphasisCream, - surface: bedrockCharcoal, - syntaxKeyword: heatherPurple, - syntaxString: ridgeGreen, - syntaxComment: dimGray, - syntaxFunction: summitBlue, - syntaxNumber: duskOrange, - syntaxType: sandGold, - syntaxOperator: mutedGray, - syntaxPunctuation: graphiteGray, - syntaxVariable: bodyOffWhite, - markdownHeading: emphasisCream, - markdownLink: summitBlue, - markdownCode: breakthroughOrange, - markdownBlockquote: mutedGray, - markdownEmphasis: parchment, - markdownStrong: emphasisCream, - diffAdded: ridgeGreen, - diffRemoved: dangerRed, - // Carries the full dimmed appearance of unchanged rows; renderers should not - // stack an extra dim attribute on top. - diffContext: dimGray, - diffHunkHeader: summitBlue, - diffAddedBg: pineShadow, - diffRemovedBg: emberShadow, - userMessageBg: coolGray, - toolPendingBg: pendingWash, - toolSuccessBg: successWash, - toolErrorBg: errorWash, -}; +// Cream stepped down toward the ground rather than desaturated toward gray, so +// low-emphasis text keeps the same warm hue as full-emphasis text. +const CREAM_DIM = "#a89f91" +const CREAM_FAINT = "#787166" -export function color(role: SemanticRole): string { - return supportsTrueColor() ? palette[role].hex : `ansi256(${color256(role)})`; -} +// The warm chrome ramp. Three tones so the roles that once shared a blue stay +// separable — they differ in lightness first, hue second, and all three sit +// well under the action orange's saturation. +const BRONZE = "#93733f" // dimmest: motion and machine chrome +const SAND = "#d1ad7d" // brightest: keywords, links, command arguments +const EMBER = "#a97243" // burnt, between the two: document structure -export function color256(role: SemanticRole): number { - return palette[role].ansi256; +export const corbitsDark: Theme = { + name: "corbits-dark", + ground: BRAND.ground, + text: BRAND.canvasCream, + textDim: CREAM_DIM, + textFaint: CREAM_FAINT, + action: BRAND.breakthroughOrange, + actionDim: BRAND.breakthroughOrangeDark, + inFlight: BRONZE, + inFlightBright: SAND, + heading: EMBER, + done: BRAND.ridgeGreen, } -export function supportsTrueColor(): boolean { - const colorterm = process.env.COLORTERM; - if (colorterm === undefined) return false; - return colorterm === "truecolor" || colorterm === "24bit"; -} +/** Every theme that ships. A picker would choose from here. */ +export const THEMES = { [corbitsDark.name]: corbitsDark } as const + +/** Semantic roles. Everything outside this file paints through these. */ +export const UI: Theme = corbitsDark + +export type UIColor = Theme[keyof Theme] diff --git a/src/tui-opentui/thinking-reveal.test.ts b/src/tui/thinking-reveal.test.ts similarity index 100% rename from src/tui-opentui/thinking-reveal.test.ts rename to src/tui/thinking-reveal.test.ts diff --git a/src/tui-opentui/thinking.ts b/src/tui/thinking.ts similarity index 99% rename from src/tui-opentui/thinking.ts rename to src/tui/thinking.ts index 0f55d19e2..c6bf4a1d5 100644 --- a/src/tui-opentui/thinking.ts +++ b/src/tui/thinking.ts @@ -13,7 +13,7 @@ import { sliceTailToWidth, sliceToWidth, stringWidth, -} from "../tui/view/height.js" +} from "./view/height.js" /** What a settled reasoning row remembers about the thinking it finished. */ export type Thought = { diff --git a/src/tui-opentui/tool-args.ts b/src/tui/tool-args.ts similarity index 97% rename from src/tui-opentui/tool-args.ts rename to src/tui/tool-args.ts index 4bc1d001d..a87f37a03 100644 --- a/src/tui-opentui/tool-args.ts +++ b/src/tui/tool-args.ts @@ -12,9 +12,9 @@ */ import { isMcpToolName } from "../mcp/tool-name.js" -import type { SemanticRole } from "../tui/theme.js" -import { summarizeToolArgs } from "../tui/tool-formatter.js" -import { validateView, viewToLines, type ViewNode } from "../tui/view/index.js" +import type { SemanticRole } from "./semantic-theme.js" +import { summarizeToolArgs } from "./tool-formatter.js" +import { validateView, viewToLines, type ViewNode } from "./view/index.js" import type { StyledBodyLine } from "./stream.js" import { UI } from "./theme.js" diff --git a/src/tui/tool-formatter.ts b/src/tui/tool-formatter.ts index 16a3fd40f..4422375d4 100644 --- a/src/tui/tool-formatter.ts +++ b/src/tui/tool-formatter.ts @@ -1,6 +1,6 @@ import { type } from "arktype"; import { relative, isAbsolute } from "node:path"; -import type { SemanticRole } from "./theme.js"; +import type { SemanticRole } from "./semantic-theme.js"; import { isMcpToolName, humanizeMcpTool } from "../mcp/tool-name.js"; import { formatMcpResult } from "./mcp-result-format.js"; diff --git a/src/tui-opentui/tool-rows.test.ts b/src/tui/tool-rows.test.ts similarity index 100% rename from src/tui-opentui/tool-rows.test.ts rename to src/tui/tool-rows.test.ts diff --git a/src/tui-opentui/tool-rows.ts b/src/tui/tool-rows.ts similarity index 99% rename from src/tui-opentui/tool-rows.ts rename to src/tui/tool-rows.ts index a62aa9ba7..1fb4f62ba 100644 --- a/src/tui-opentui/tool-rows.ts +++ b/src/tui/tool-rows.ts @@ -19,7 +19,7 @@ import { toolResultRow, type ToolResultRowInput, } from "./mcp-view.js" -import { extractMcpRecords } from "../tui/mcp-result-format.js" +import { extractMcpRecords } from "./mcp-result-format.js" import type { StreamRow, StyledBodyLine } from "./stream.js" import { UI } from "./theme.js" diff --git a/src/tui-opentui/tool-subject.test.ts b/src/tui/tool-subject.test.ts similarity index 100% rename from src/tui-opentui/tool-subject.test.ts rename to src/tui/tool-subject.test.ts diff --git a/src/tui-opentui/transcript-anchor.test.ts b/src/tui/transcript-anchor.test.ts similarity index 100% rename from src/tui-opentui/transcript-anchor.test.ts rename to src/tui/transcript-anchor.test.ts diff --git a/src/tui-opentui/transcript-layout.test.ts b/src/tui/transcript-layout.test.ts similarity index 100% rename from src/tui-opentui/transcript-layout.test.ts rename to src/tui/transcript-layout.test.ts diff --git a/src/tui-opentui/transcript-long-log-scroll.test.ts b/src/tui/transcript-long-log-scroll.test.ts similarity index 100% rename from src/tui-opentui/transcript-long-log-scroll.test.ts rename to src/tui/transcript-long-log-scroll.test.ts diff --git a/src/tui-opentui/transcript-panels.test.ts b/src/tui/transcript-panels.test.ts similarity index 100% rename from src/tui-opentui/transcript-panels.test.ts rename to src/tui/transcript-panels.test.ts diff --git a/src/tui-opentui/turn-monitor.test.ts b/src/tui/turn-monitor.test.ts similarity index 100% rename from src/tui-opentui/turn-monitor.test.ts rename to src/tui/turn-monitor.test.ts diff --git a/src/tui-opentui/turn-state.test.ts b/src/tui/turn-state.test.ts similarity index 100% rename from src/tui-opentui/turn-state.test.ts rename to src/tui/turn-state.test.ts diff --git a/src/tui-opentui/turn-state.ts b/src/tui/turn-state.ts similarity index 100% rename from src/tui-opentui/turn-state.ts rename to src/tui/turn-state.ts diff --git a/src/tui/view/height.ts b/src/tui/view/height.ts index 17a0db8cf..1fc470eb8 100644 --- a/src/tui/view/height.ts +++ b/src/tui/view/height.ts @@ -13,7 +13,7 @@ export type RowRange = { start: number; end: number }; * OpenTUI resolves Ambiguous as narrow under both of its width methods * (`wcwidth` and `unicode`), which is why this is `true`. It is passed * explicitly rather than left to Bun's default so the choice is visible and - * greppable, and `src/tui-opentui/width-contract.ts` measures a probe through + * greppable, and `src/tui/width-contract.ts` measures a probe through * OpenTUI's own table at startup so a divergence is caught rather than painted. */ export const AMBIGUOUS_IS_NARROW = true; diff --git a/src/tui/view/lines.ts b/src/tui/view/lines.ts index c7b1cc6f1..7305c423a 100644 --- a/src/tui/view/lines.ts +++ b/src/tui/view/lines.ts @@ -1,5 +1,5 @@ import type { StyledSegment } from "../markdown-parser.js"; -import { color, type SemanticRole } from "../theme.js"; +import { color, type SemanticRole } from "../semantic-theme.js"; import { wrapLines } from "./height.js"; import type { Tone, ViewNode } from "./spec.js"; import { VIEW_GRID_MAX_ROWS } from "./spec.js"; diff --git a/src/tui/view/registry.ts b/src/tui/view/registry.ts index dfb98ba16..e7897b3f7 100644 --- a/src/tui/view/registry.ts +++ b/src/tui/view/registry.ts @@ -1,4 +1,4 @@ -import type { SemanticRole } from "../theme.js"; +import type { SemanticRole } from "../semantic-theme.js"; import type { Tone } from "./spec.js"; export const GAP = 2; diff --git a/src/tui-opentui/wave6.test.ts b/src/tui/wave6.test.ts similarity index 99% rename from src/tui-opentui/wave6.test.ts rename to src/tui/wave6.test.ts index afc2d0858..0db4480fa 100644 --- a/src/tui-opentui/wave6.test.ts +++ b/src/tui/wave6.test.ts @@ -25,7 +25,7 @@ import { toggleTasksPanel, } from "./shell" import { createRecordingClipboard } from "./copy-path" -import { stringWidth } from "../tui/view/height" +import { stringWidth } from "./view/height" import type { PaletteCommand } from "./command-catalog" const CATALOG: readonly PaletteCommand[] = [ diff --git a/src/tui-opentui/wave7.test.ts b/src/tui/wave7.test.ts similarity index 99% rename from src/tui-opentui/wave7.test.ts rename to src/tui/wave7.test.ts index e8dfdf1b8..f2c69013a 100644 --- a/src/tui-opentui/wave7.test.ts +++ b/src/tui/wave7.test.ts @@ -36,7 +36,7 @@ function testObserveSession(): ObserveSession { lines: [ { role: "system", text: "— child session explore —" }, { role: "user", text: "find every openListOverlay caller" }, - { role: "assistant", text: "Searching src/tui-opentui…" }, + { role: "assistant", text: "Searching src/tui…" }, ], } } diff --git a/src/tui-opentui/width-columns.test.ts b/src/tui/width-columns.test.ts similarity index 99% rename from src/tui-opentui/width-columns.test.ts rename to src/tui/width-columns.test.ts index eb6e5ec92..911327127 100644 --- a/src/tui-opentui/width-columns.test.ts +++ b/src/tui/width-columns.test.ts @@ -15,7 +15,7 @@ import { stringWidth, AMBIGUOUS_IS_NARROW, WIDTH_PROBE, -} from "../tui/view/height.js" +} from "./view/height.js" import { middleEllipsis } from "./command-display.js" import { renderDiff } from "./diff.js" import { wrapLanding } from "./landing.js" diff --git a/src/tui-opentui/width-contract.test.ts b/src/tui/width-contract.test.ts similarity index 94% rename from src/tui-opentui/width-contract.test.ts rename to src/tui/width-contract.test.ts index d17602131..4a99e2010 100644 --- a/src/tui-opentui/width-contract.test.ts +++ b/src/tui/width-contract.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" -import { WIDTH_PROBE, stringWidth } from "../tui/view/height.js" +import { WIDTH_PROBE, stringWidth } from "./view/height.js" import { checkWidthContract, measureRendererWidth, diff --git a/src/tui-opentui/width-contract.ts b/src/tui/width-contract.ts similarity index 94% rename from src/tui-opentui/width-contract.ts rename to src/tui/width-contract.ts index 73d6a5185..265bff7bc 100644 --- a/src/tui-opentui/width-contract.ts +++ b/src/tui/width-contract.ts @@ -2,7 +2,7 @@ * Startup check that our column arithmetic and OpenTUI's width table agree. * * Every wrap, pad and truncation budget in the shell is computed with - * `stringWidth` (see `../tui/view/height.ts`), but the cells are actually + * `stringWidth` (see `./view/height.ts`), but the cells are actually * allocated by OpenTUI's native table, negotiated with the terminal at boot. * The two only have to disagree on East Asian Ambiguous characters — which is * most of what the chrome is drawn from — for every border to come out short. @@ -16,7 +16,7 @@ import { resolveRenderLib, type WidthMethod } from "@opentui/core" -import { stringWidth, WIDTH_PROBE } from "../tui/view/height.js" +import { stringWidth, WIDTH_PROBE } from "./view/height.js" export type WidthContractReport = { readonly agrees: boolean diff --git a/src/tui-opentui/workspace-watch.test.ts b/src/tui/workspace-watch.test.ts similarity index 100% rename from src/tui-opentui/workspace-watch.test.ts rename to src/tui/workspace-watch.test.ts diff --git a/src/tui-opentui/workspace-watch.ts b/src/tui/workspace-watch.ts similarity index 100% rename from src/tui-opentui/workspace-watch.ts rename to src/tui/workspace-watch.ts diff --git a/tests/unit/telemetry-product-events.test.ts b/tests/unit/telemetry-product-events.test.ts index 2df7341a4..dde851194 100644 --- a/tests/unit/telemetry-product-events.test.ts +++ b/tests/unit/telemetry-product-events.test.ts @@ -26,7 +26,7 @@ import { createTelemetry, type Telemetry } from "../../src/telemetry/index.js"; import { captureAuthFailure, classifyAgentSendFailure, -} from "../../src/tui-opentui/session-chrome.js"; +} from "../../src/tui/session-chrome.js"; type BatchBody = { batch: { event: string; properties: Record }[]; diff --git a/tests/unit/tui/theme.test.ts b/tests/unit/tui/theme.test.ts index a6c49339f..10700b13a 100644 --- a/tests/unit/tui/theme.test.ts +++ b/tests/unit/tui/theme.test.ts @@ -1,5 +1,5 @@ import { test, expect, afterEach, beforeEach } from "bun:test"; -import { color, color256, palette, supportsTrueColor } from "../../../src/tui/theme.js"; +import { color, color256, palette, supportsTrueColor } from "../../../src/tui/semantic-theme.js"; const originalColorterm = process.env.COLORTERM;