diff --git a/AGENTS.md b/AGENTS.md index 9c445f7db..747fbe988 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -70,6 +70,7 @@ Interchange is the standard library for this repo, consumed as published `@intx/ ## Reference - `docs/ARCHITECTURE.md` — reactor loop, events, directors, workflows, plugin chain, permission system +- `docs/TUI.md` — terminal UI behavior spec: layout, overlays, selectors, palette, prompt box, scrolling - `docs/IMPLEMENTATION.md` — runtime, dependencies, config resolution, settings precedence, CLI flags, state persistence, eval harness - `docs/PRODUCT.md` — what we're building and why - `docs/HOOKS.md` — lifecycle hooks diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md index c1345cc89..cc622fc78 100644 --- a/docs/PRODUCT.md +++ b/docs/PRODUCT.md @@ -36,12 +36,9 @@ $ corbits "Add JWT auth to the API" A full-screen terminal interface: a pinned header (session title and workflow progress), a scrollable event log, modals for permission prompts and operator questions, and a chat input for follow-up turns. -**Layout and interaction contracts** (OpenTUI is the shipping shell): - -- Layout constitution: `docs/tui-layout-constitution.md` (chrome budget, geometry ownership, kill list) -- Interaction contract: `docs/tui-interaction-contract.md` (queue / steer / interrupt, keys, palette) -- Migration cutover: `docs/tui-migration-cutover.md` -- Epic plan: `docs/plans/tui-layout-scroll-platform.md` · product brief: `briefs/tui-rebuild-opentui.md` +**Behavior spec** (OpenTUI is the shipping shell): `docs/TUI.md` — layout, +chrome budget, overlays, selectors, palette, prompt box, and scroll/mouse +behavior. ### Exec mode (non-TUI product path) diff --git a/docs/TUI.md b/docs/TUI.md new file mode 100644 index 000000000..ed1068407 --- /dev/null +++ b/docs/TUI.md @@ -0,0 +1,333 @@ +# Corbits Code — TUI Behavior Spec + +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 +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 +able to build it correctly from this document alone. + +Internally and in code, a blocking surface is an "overlay." Never use that +word in anything the operator reads — hint lines, status flashes, help text, +titles. The operator sees "permissions," "model / provider," "command +palette," a plain question — never the word "overlay." + +## How it should look + +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 — goal/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`). +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`). + +A single **geometry resolver** turns terminal size, zone visibility, and +overlay mode into region rects; every zone reads its rect from that resolver +instead of computing its own height from `process.stdout.rows`. On an 80×24 +terminal with nothing optional showing, the transcript floor is 12 rows +(`IDLE_TRANSCRIPT_FLOOR`); with an inset overlay open the floor drops to a +proposed 8 rows (`OVERLAY_TRANSCRIPT_FLOOR`) so the log stays glanceable +underneath a permission prompt. When space is scarce, collapse follows a +fixed order — transient banners first, then settings/plugin notices, then +goal/task/agents strips, then progress, then the prompt itself shrinks one +row at a time down to its 3-row base — never the transcript +(`COLLAPSE_ORDER` in `zones.ts`). + +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 +ride the prompt box's own border. + +Color is a small, deliberate palette, not decoration +(`src/tui-opentui/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 +active choice) — nothing else competes with it. Ongoing, non-decision status +uses the bronze/sand/ember chrome ramp and green (`UI.done`) for completion. +The one deliberate exception is diff removals, where orange is content (the +removed line), not a decision marker, and no decision-marker shares that row. + +## How pop-ups should feel + +A blocking surface (permissions, an operator question, the model/provider +picker, settings, help, the command palette, …) 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 +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. +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 +fall back to best effort (`resolveGeometry`'s collapse loop in +`geometry/resolve.ts`). An overlay must never paint past the box it was +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 +`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 +because an earlier version could abandon the awaited promise on Escape and +leave the session parked with no recovery path short of killing the process; +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 +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. + +## How selectors should work + +Every list surface — permissions, the operator question, the model picker, +the command palette, resume/session-mode pickers, settings — shares one list +viewport kit: shared windowing, keep-active-visible, and page/jump behavior. +There is exactly one scroll lease at a time; keyboard paging and the mouse +wheel both follow whichever surface currently holds it, so a modal open on +top of the transcript never lets the wheel move the transcript underneath it. + +"Current" is never inferred. For the model picker, the row marked +`(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`). + +The command palette specifically (`src/tui-opentui/palette.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 +leading marker column and no per-row kind column; the selected row is marked +by text color only (`paintPaletteList` in `shell.ts`: "the highlighted row +already stands out by sitting under the cursor, so a leading `>` and a grey +block would both be saying the same thing twice"). The palette also paints +with no title rule — the filter row (`> query`) directly under the box +already shows what was typed, so a second header line would say nothing new +(`repaintPalette`). + +## Slash commands and pickers + +`Ctrl+O` opens the command palette from anywhere in the shell (reclaimed from +the Ink-era tool-expand chord); `/` at an empty prompt opens the same +palette narrowed to registry slash commands. Every user-facing slash command +has a palette twin. Palette entries are either "residual" product actions +owned by the shell (open permissions, switch model, toggle a chrome zone, +copy, toggle mouse capture, help, insert a mention, observe a subagent) or +"command" entries backed by the live command registry +(`src/tui-opentui/palette.ts`). + +The model/provider picker is provider-first +(`src/tui-opentui/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 +provider's models; selecting a model dispatches the switch. Escape at the +model level returns to the provider level rather than closing the picker +outright (`openLevel(group.rows, onCancel)` passes the parent `openModels` +reopen as the child level's `onCancel`); only Escape at the provider level +closes the picker. Recent/favorite rows and the active provider's group row +both get a `(current)` suffix when they match the session's live active +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 +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 +click-to-expand or drag-to-scroll, so leaving mouse reporting off lets the +terminal's own text selection and copy work by default, with no Alt+M dance +required. + +## The prompt box + +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 +needs an explicit chord (Shift+Enter or Ctrl+Enter where the terminal reports +the modifier via the kitty keyboard protocol, Ctrl+J everywhere else, since a +plain terminal cannot report Shift+Enter at all). Alt+Enter is claimed by the +shell before the textarea ever sees it, as the mid-run "steer" action. + +Up/Down are caret motion first inside a multi-line buffer. History recall +only fires when the caret is already at the first or last wrapped row of the +buffer — i.e., has nowhere further to go +(`promptCaretAtFirstRow`/`promptCaretAtLastRow` in `prompt-input.ts`, +consumed in `shell.ts`'s key handler). This is deliberate, not incidental: +with DEC mouse reporting on, a terminal translates a wheel tick into the same +arrow-key byte sequence as a real keypress, so scroll and history navigation +cannot both be arrow-driven at the same time without one shadowing the +other. That is also why the main shell routes the mouse wheel to the +transcript rather than the prompt even when the wheel event hits the prompt's +own hit-tested region (`routePromptWheelToTranscript`, `shell.ts`) — arrow +keys stay history/caret, wheel stays transcript scroll, and the two never +collide. + +Bracketed paste is the primary paste path; a fallback heuristic (a printable +character immediately followed by Enter inside one keystroke burst) detects a +paste replayed as raw keystrokes on a terminal that never sends a real +`paste` event, so pasted multi-line text does not get split into multiple +sent messages. Once a real `paste` event has fired even once, the fallback +heuristic is permanently skipped for the rest of the session +(`shell.ts`, the `sawBracketedPaste` guard). + +@-mention path completion opens a popup keyed off the `@token` under the +cursor (`openAtMentionSuggestions`, `shell.ts`); every keystroke re-queries, +and a generation counter discards a slower, stale query's results if a newer +one already landed. Directory picks re-open one level down so the operator +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`). +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. + +The prompt repaints on every keystroke (`onFrame` in `shell.ts` calls +`syncPromptRows`/`syncTranscriptSpacer`/`syncNoticeAfterLayout` every frame, +not on a debounce) — anything added to the prompt's paint path must stay +cheap, because it runs at typing speed. + +Ctrl+C interrupts a busy run (or clears a non-empty idle prompt); a second +Ctrl+C within a 2-second window (`CTRL_C_EXIT_WINDOW_MS`) quits — this +replaced an Ink-era yes/no exit-confirm modal with the same intent (an +explicit second confirmation) without adding a modal (`handleCtrlC`, +`shell.ts`). + +## Overflows, scrolling, and key macros + +The main session shell owns the mouse. With DEC mouse reporting on (the +default), the wheel scrolls the transcript, clicking a collapsed tool row or +diff arrow expands it in place, and dragging inside the transcript scrolls it +— none of that needs a modifier key. The cost of holding the mouse this way +is that native terminal drag-select is unavailable while reporting is on: +the terminal hands drag events to the app instead of running its own +selection. Two chords cover that gap without needing the mouse released +first: + +- **Alt+M** toggles DEC mouse reporting off and back on + (`toggleMouseCapture`, `shell.ts`). Off, the terminal's own drag-select + and copy work exactly as in any other terminal program; the status flash + names the trade both ways ("Mouse released · drag to select and copy as + usual · Alt+M to click rows" / "Mouse captured · click to expand, drag to + scroll · Alt+M to select text again"). +- **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 + platform, `pbcopy`/`clip`/`wl-copy`/`xclip`/`xsel`, falling back to an OSC + 52 escape sequence when no helper is available, e.g. over SSH). + +Arrow keys never scroll anything — inside the prompt they are caret motion +or, at the buffer's edges, prompt-history recall; inside an open overlay's +list they move the active selection. Only the mouse wheel and the modal's +own page keys (PgUp/PgDn) move a scroll position, and only the surface +holding the current scroll lease responds to them. + +`Ctrl+G` (the Emacs/readline "abort" chord) cancels the most recently queued +mid-run message. `Tab` toggles focus between the prompt and the transcript. +`e` (with Alt/Option) expands a collapsed row — a collapsed permission +payload while an overlay owns focus, or a collapsed transcript row (tool +output, a long diff) while the transcript does — one expand idiom shared +across both contexts. + +## Standalone screens + +The provider-setup screen (`src/tui-opentui/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 +`flexShrink: 0` (verified at the five top-level row containers in +`provider-setup.ts`). Without that, OpenTUI's flex layout compresses +single-line rows into each other on a short terminal and garbles the text. +Any future standalone screen in this class — one that mounts its own +renderer ahead of the main shell — must follow the same rule. + +## Fixture and demo data + +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 +(`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). + +## Structured logging never paints the screen + +Every logger in the process — including loggers inside vendored dependencies +Corbits does not control — is routed to a file sink +(`src/logging/sink.ts:installFileLogSink`, installed as the first statement +in the process entry point) instead of the console. `@intx/log` installs a +console sink as a side effect of its own first import, so this must run +before any other Corbits code executes: the TUI holds the alternate screen +for the rest of the process, and anything landing on the real terminal +mid-frame corrupts the paint. Anything the operator must actually see goes +through the transcript (`appendStreamRow`) or a chrome notice — never a log +line. + +## Test-harness blind spots + +The OpenTUI headless renderer used by the automated test suite is not a +terminal. It cannot observe: + +- **Real paint.** Tests assert on the shell's in-memory row/rect state, not + on what a terminal emulator actually draws to a screen buffer. +- **Modifier reporting.** Whether a real terminal can report Shift+Enter, + Alt+letter, or similar modifier combinations depends on the terminal + negotiating the kitty keyboard protocol (or an equivalent) with the actual + host terminal emulator — the headless harness has no such negotiation to + fail or succeed at. +- **The system clipboard.** `system-clipboard.ts`'s helper-binary spawns and + OSC 52 fallback are exercised with mocked spawn functions in tests; no + test round-trips through a real `pbcopy`/`xclip`/terminal clipboard. +- **Terminal-owned text selection.** Native drag-select only exists once DEC + mouse reporting is off and a real terminal emulator is running; there is + no terminal emulator in the test harness to select text in. + +Concretely, whole defect classes — a DEC mouse-reporting toggle that silently +no-ops, an Alt+key chord a given terminal never actually delivers, a +clipboard write that fails silently on a machine with no clipboard helper +installed, native drag-select that never re-engages after Alt+M — are +invisible to the automated suite by construction. A green `bun run test` is +evidence the pure logic and the headless paint model behave; it is not +evidence any of the above works in a real terminal. Changes touching mouse +reporting, modifier chords, clipboard, or terminal-owned selection need a +manual run in a real terminal before being called done. + +## Open questions + +The following normative-shaped statements could not be verified against +source in the time available and are left here as open questions rather than +asserted as fact: + +- Whether every collapse-order edge case in `geometry/resolve.ts` (e.g. the + tiny-terminal, sub-24-row path) has a corresponding test that pins the + exact row counts, or whether some of that path is only exercised + indirectly. +- Whether the `(current)` marking on a provider *group* row + (`withGroupMark` in `openModels`, `product-host.ts`) is reachable and + correct in every case where the active model's provider itself has no + favorites/recents entry — the code path exists but was not traced through + a live picker session. +- Full coverage of which chords are guaranteed deliverable on every terminal + emulator Corbits Code targets (Shift+Enter and Alt+letter reporting depend + on kitty-protocol negotiation the harness cannot test — see Test-harness + blind spots above); this document states what the code does when a chord + *is* delivered, not which terminals reliably deliver it. diff --git a/docs/plans/opentui-platform-wave2.md b/docs/plans/opentui-platform-wave2.md deleted file mode 100644 index 55cc742ab..000000000 --- a/docs/plans/opentui-platform-wave2.md +++ /dev/null @@ -1,43 +0,0 @@ -# OpenTUI Platform — Wave 2 status - -**Branch:** `migration/opentui-tui` -**Date:** 2026-08-05 -**Status:** Platform skeleton landed; not wired to production CLI - -## What landed - -| Piece | Path | Notes | -|---|---|---| -| Deps | `package.json` | `@opentui/core` / `solid` / `keymap` **0.5.1** + `solid-js` | -| Geometry | `src/tui-opentui/geometry/` | Zone registry, collapse, floors (idle 12 / inset 8) | -| Focus + scroll lease | `src/tui-opentui/focus/` | overlay > observe > shell; one lease | -| List viewport | `src/tui-opentui/list-viewport.ts` | keep-active-visible windowing | -| Harness | `src/tui-opentui/harness.ts` | headless `createTestRenderer` + chords | -| App shell | `src/tui-opentui/shell.ts` | header · sticky transcript · prompt · status (core **class** API) | -| Demo | `src/tui-opentui/demo.ts` | `bun src/tui-opentui/demo.ts` on a real TTY | - -## Binding - -**core class API** for scroll/focus leases (spike: VNode ScrollBox broke `scrollTop`). Solid remains the ADR composition path for denser surfaces later; shell wave used class API to stay typecheck-clean under root React `jsxImportSource`. - -## Verify - -```bash -bun test ./src/tui-opentui -bun run typecheck -bun run build -bun run tui:opentui-smoke -``` - -## Not done (next waves) - -1. Wire shell into a migration-only entry (still no dual-release flag on main) -2. Transcript surface (long-log window, real stream host) -3. Prompt queue / steer / interrupt product wiring -4. Overlay host + list kit consumers (permissions, model picker) -5. Solid declarative chrome where it helps -6. Delete Ink only at full epic gate - -## Production entry - -`src/index.ts` / `src/tui/runner` remain **Ink**. Platform kit is importable but not the operator path yet. diff --git a/docs/plans/opentui-spike-report.md b/docs/plans/opentui-spike-report.md deleted file mode 100644 index a4ea2ac99..000000000 --- a/docs/plans/opentui-spike-report.md +++ /dev/null @@ -1,137 +0,0 @@ -# OpenTUI spike report — GO / NO-GO - -**Verdict: GO** - -**Date:** 2026-08-05 -**Runtime:** Bun 1.3.14 on darwin arm64 -**Packages:** `@opentui/core@0.5.1`, `@opentui/solid@0.5.1` -**Spike tree (local only):** `tmp/opentui-spike/` -**Root `package.json`:** unchanged (no OpenTUI dependency added) - -This report is the committed evidence artifact for the TUI layout/scroll platform plan. Final binding ADR remains a downstream decision; this spike supplies inputs only. - ---- - -## 1. What was proven - -| Checkpoint | Result | Evidence | -|---|---|---| -| Install under Bun (isolated tree) | PASS | `bun add @opentui/core` → 0.5.1; native `@opentui/core-darwin-arm64` / `libopentui.dylib` | -| Native renderer via FFI | PASS | `createTestRenderer` from `@opentui/core/testing` paints memory buffer | -| Mini shell layout (header + ScrollBox + prompt) | PASS | Flex column; no manual row math; resize 60×20 → 80×12 keeps HEADER + STATUS | -| Sticky stream bottom | PASS | Seeded lines 1–50; viewport shows ~034–050 only | -| Scroll-up pin (sticky pause) | PASS | `scrollTop` held at 21 after append while scrolled up | -| Return-to-bottom | PASS | `scrollTo(MAX)` reveals appended line | -| Focus lease (prompt vs scroll) | PASS | `InputRenderable.focus()` / `ScrollBoxRenderable.focus()` both succeed | -| Enter / Alt+Enter / Ctrl+C distinct | PASS | See key shapes below | -| Input ENTER submit path | PASS | `InputRenderableEvents.ENTER` value `hello-spike` | -| Solid package loads | PASS | `@opentui/solid` exports `render` / `testRender` | -| Windows | Untested | Non-blocking for this spike | -| Linux / musl | Untested on host | Optional deps declared for x64/arm64 musl | - -Headless verifier: `tmp/opentui-spike/verify.ts` — **16/16 PASS**. -Interactive shell: `tmp/opentui-spike/index.ts` (`bun run start` on a real TTY). - ---- - -## 2. Key event shapes (actionable) - -Observed via `createTestRenderer` mock input → `renderer.keyInput` `keypress`: - -| Chord | `name` | `ctrl` | `meta` | `option` | `sequence` / `raw` | -|---|---|---|---|---|---| -| Enter | `return` | false | false | false | `\r` | -| Alt+Enter | `return` | false | **true** | false | `\u001b\r` | -| Ctrl+C | `c` | **true** | false | false | `\u0003` | - -Notes for Corbits steering/interrupt design: - -- Canonical identity for Enter is **`return`**, not `enter`. Component keybindings alias `enter` → `return`; raw `keyInput` handlers must check `return` (or both). -- Alt+Enter is cleanly separable as `name === "return" && (meta || option)`. -- On this mock/mac path Alt surfaces as **`meta: true`**, not `option: true`. Real Kitty-protocol terminals may also set `option`; match either. -- Ctrl+C is `name === "c" && ctrl`. Renderer supports `exitOnCtrlC: true` (default) or manual handling when false. -- `InputRenderable` consumes Enter for submit (`ENTER` event) when focused; app-level Alt+Enter should be handled on `keyInput` (or keymap) before/around input so it is not treated as submit. - ---- - -## 3. Binding recommendation inputs (not an ADR) - -| Binding | Spike result | DX notes | -|---|---|---| -| **Core** | Full mini shell + headless suite green | Class API (`*Renderable`) is reliable for scroll/focus. Construct factories (`ScrollBox({…})`) paint fine but VNode proxies broke `scrollTop` access in one path — prefer class API for imperative scroll control. | -| **Solid** | Install + import green; OpenCode peer uses Solid | Needs `jsxImportSource: "@opentui/solid"` + `bunfig.toml` preload `@opentui/solid/preload`. Install warned `incorrect peer dependency solid-js@1.9.14` but package resolved. Fine-grained reactivity fits stream-heavy UIs. | -| **React** | Not exercised in this spike | Docs: `createRoot(renderer)` + `@opentui/react` jsxImportSource. Closest to current Corbits Ink/React muscle memory; higher render-cost risk than Solid for dense streams. | - -**Recommended binding hint:** **Solid + core + keymap** (OpenCode-aligned), with core class API for low-level scroll/focus leases. Revisit if team velocity strongly favors React reuse; do not block GO on React sample. - -**Binding ADR (downstream of this report):** `docs/adr/opentui-binding.md` — decision **Solid + core + keymap**. - - -`@opentui/keymap` not installed in this spike; treat as next packaging/spike item when wiring host key chords. - ---- - -## 4. Packaging / FFI / platform risks - -- **Native Zig core via Bun FFI** — works on this host. Node path needs Node ≥ 26.4.0 + `--experimental-ffi` (Corbits is Bun-first; low risk). -- **Optional platform packages** ship per OS/arch including **musl** (`core-linux-*-musl`) and Windows. CI matrix should assert the correct optional dep resolves (darwin arm64/x64, linux glibc + musl arm64/x64). -- **Standalone / Homebrew packaging** — native dylib must be included or resolved at install; verify Corbits release pipeline before cutover. -- **Tree-sitter wasm assets** ship under `@opentui/core` (markdown/code components); size/packaging impact if those components are used. -- **Solid peer pin** — install warning on solid-js 1.9.14; pin compatible peer when productionizing. -- **Construct vs class API** — document team convention early (class for imperative scroll/focus). - ---- - -## 5. Mini shell shape (reference) - -``` -┌─ header (flexShrink: 0) ─────────────────────────────┐ -│ OpenTUI spike · … │ -├─ ScrollBox stickyScroll + stickyStart: "bottom" ─────┤ -│ flexGrow: 1 · no manual row budget │ -│ stream lines … │ -├─ prompt region (flexShrink: 0) ──────────────────────┤ -│ status / focus lease │ -│ Input prompt │ -└──────────────────────────────────────────────────────┘ -``` - -Sticky behavior matches product need: auto-follow until operator scrolls up; return-to-bottom resumes follow. - ---- - -## 6. Explicit GO / NO-GO - -### GO — proceed with OpenTUI as TUI substrate - -Reasons: - -1. Clean install on Bun with native renderer in under ~1s. -2. Flex layout + sticky ScrollBox eliminate the current chrome-row-math failure class. -3. Focus and key distinguishability cover queue-vs-interrupt chords. -4. Headless `createTestRenderer` enables automated layout/key regression tests without a host TTY. -5. Solid binding installs and matches the OpenCode production peer stack. - -### Not claimed (out of scope / residual risk) - -- Production wiring into Corbits entry or root deps -- Full React binding exercise -- Windows interactive run -- Linux/musl CI green -- Keymap host integration -- Performance under multi-hour stream load -- Migration of existing Ink surfaces - ---- - -## 7. How to re-run - -```bash -cd tmp/opentui-spike -bun install -bun run verify # headless evidence (primary) -bun run start # interactive mini shell (real TTY) -``` - -Local evidence JSON: `tmp/opentui-spike/verify-evidence.json` -Task logs (dispatch): `verification.log`, `spike-run.log` under the OpenTUI spike task directory. diff --git a/docs/plans/tui-layout-scroll-platform.md b/docs/plans/tui-layout-scroll-platform.md deleted file mode 100644 index c39b804a0..000000000 --- a/docs/plans/tui-layout-scroll-platform.md +++ /dev/null @@ -1,439 +0,0 @@ -# TUI rebuild: OpenTUI shell, layout platform, Amp-class calm - -**Status:** plan (source of truth for Linear project *TUI layout and scroll platform*) -**Product brief:** `briefs/tui-rebuild-opentui.md` -**Project:** https://linear.app/abklabs/project/tui-layout-and-scroll-platform-2f172c54fa83 -**Issues:** CL-5364–CL-5388 + CL-5391–CL-5400 (follow-ups from this plan) - -This document is written **before** treating the Linear backlog as shippable work. Tickets are expanded from here, not the reverse. - ---- - -## 1. Problem - -The Corbits Code TUI feels random because it is a **systems failure**, not a pile of unrelated bugs. - -### What users hit - -- Scroll and overflow break under long sessions, dense approvals, stacked chrome, and small terminals. -- Mid-run steering is inverted vs Amp (Enter interrupts; Alt+Enter queues) and hard to discover. -- Discovery is split across slash commands, a help overlay, and tribal key knowledge. -- Linear is full of closed and open tickets in the **same failure class**: guessed row budgets, per-surface scroll, dual overlay stacks. - -### Root cause (architecture) - -| Concern | Today | Failure | -|---|---|---| -| Fixed chrome | `chrome-zones.ts` constants (`header:2`, `prompt:3`, `status:2`, …) sum to a fixed `CHROME_ROWS` | Constants drift from paint | -| Variable chrome | `chrome-geometry.ts` + `extraChromeRows` (goal, task, agents, banners, prompt growth) | Stacks until transcript is 1 row | -| Overlay height | Hardcoded budgets in `use-layout-geometry.ts` (permissions 6/20, operator 7, help 16, …) | Reuse of wrong budget (e.g. settings ← permissions) | -| Paint | Ink `flexGrow` on the log **and** manual row subtraction | Two systems claim the same height | -| Scroll | `use-scroll`, `use-scroll-window`, prompt-local offset, agents strip window, subagent fork, manual slices in agent/settings/plugins/resume | N owners, no lease | -| Focus | Boolean soup in `app.tsx` + per-modal `useInput` | Keys/wheel race | -| Overlays | `modal-stack.tsx` **and** `overlay-stack.tsx` | Inconsistent height accounting; absolute-positioned settings/slash | - -**Effective equation today:** - -``` -visibleRows = terminal.rows - - CHROME_ROWS - - overlayHeuristic - - extraChromeRows -``` - -Paint does not obey this equation exactly. Overflow tickets are the equation lying. - -### Teardown inventory (surfaces) - -| Surface | Layout | Scroll | -|---|---|---| -| Event log | Residual rows from geometry hooks | `useScroll` + `useTranscriptLayout` + `useMouseScroll` | -| Chat prompt | Cap ~40% / max height 8 | Local window (`prompt-layout`) | -| Permission / operator | Modal-local height | `useScrollWindow` | -| Agent modal | Modal list | Manual slice | -| Mentions | Fixed maxHeight | `useScrollWindow` | -| Permissions / settings / plugins / resume | Overlay panes | Local window each | -| Subagent session | Full-screen fork | Own offset (parallel to main log) | -| Agents strip | Extra chrome row | Horizontal only | - -Tests cover `use-scroll-window` and `prompt-layout` thinly. Missing: chrome-zones/geometry, use-scroll, mouse gating, overlay→shrink integration, size matrix. - ---- - -## 2. Decision (locked) - -1. **Target substrate: OpenTUI** (`@opentui/core` + binding chosen in spike). Same family as OpenCode. -2. **Do not** build a long-lived geometry/scroll platform on Ink and rewrite later. -3. **Ink policy:** only true P0 daily-use blockers get minimal patches; no new chrome features on Ink (CL-5367). -4. **UX north star: Amp-class calm**, implemented on OpenTUI — not a copy of Amp's product surface. -5. **Peers for comparison:** Amp (feel), OpenCode (stack), Claude Code / pi (interaction baselines). pi-tui is **not** the destination stack. - ---- - -## 3. Product definition - -### Who - -Terminal-first developers already on Corbits or evaluating agent CLIs. They abandon tools that fight them mid-stream. - -### Good 30-minute session - -1. Screen paints once; prompt ready; no thrash on first stream token. -2. Stream + tools auto-follow unless operator scrolled up (clear “follow live”). -3. Mid-run type → **Enter queues** (badge); interrupt is a distinct labeled chord. -4. Permission overlay measures remaining box; focus returns cleanly. -5. Orchestrator: agents strip thin; observe intentional; Esc leaves. -6. Scroll back, keyboard copy path works, quit without alt-screen garbage. - -### Success (human-visible) - -- Calm layout under stream + modal open/close. -- Queue-default steering; interrupt discoverable. -- Command palette finds permissions/model without memorizing slash. -- Chrome budget held on 80×24 (≥ ~12 log rows idle). -- Keyboard copy of a message/tool/diff without relying on terminal drag-select. -- Fewer “TUI broken / scroll wrong / accidental interrupt” session-killers. - ---- - -## 4. Amp: steal / adapt / ignore - -| Amp idea | Verdict | -|---|---| -| Solid TUI foundation, no flicker, smooth stream scroll, overlays | **Steal** via OpenTUI | -| Command palette (Ctrl+O) as primary discovery | **Steal** — today Ctrl+O expands tool output; rebind | -| Queue-by-default mid-run; interrupt special | **Steal** — invert Enter vs Alt+Enter | -| Steer at tool boundary vs hard interrupt | **Adapt** — queue + interrupt first; deep steer later | -| One visual system | **Steal** — one theme | -| Keymap customize | **Adapt** — discoverable v1; settings remap later | -| Plugin UI notify/confirm/input/select | **Adapt** — TUI-only v1 | -| Remote control / web mirror / multiplayer | **Ignore** this project | -| Perfect mouse selection | **Ignore** — ship deliberate keyboard copy (learn Amp scar) | -| Unlimited historical render | **Ignore** — long-log strategy required (learn Amp lag scar) | - -Sources: [Owner's Manual](https://ampcode.com/manual), [Look Ma, No Flicker](https://ampcode.com/news/look-ma-no-flicker), [Command Palette](https://ampcode.com/news/command-palette), [Amp Rebuilt](https://ampcode.com/news/neo). - ---- - -## 5. Principles (constitution) - -1. **Transcript pays the rent.** Fixed chrome is thin. Dense UI is modal or collapsed. -2. **Measure, do not guess.** One layout owner; no parallel magic constants that must match paint. -3. **One scroll owner (lease) at a time.** Keys and wheel follow the same focus tree. -4. **One list viewport kit.** Models, permissions, agents, approval options, settings lists share windowing + keep-active-visible + page/jump. -5. **One overlay host.** Kill the ModalStack vs OverlayStack split in the design. -6. **Visual quiet.** State via text and color; no glyph zoo. One theme. -7. **Queue-default steering.** Interrupt is loud and rare. -8. **Palette discovers.** Slash remains a power path, not the only path. -9. **Platform before features.** No new chrome until shell + kit land. -10. **Hard min transcript rows** on 24-row terminals; chrome priority when space is scarce. - -### Chrome budget (v1 numbers) - -| Zone | Idle rows (target) | Notes | -|---|---|---| -| Header | ≤ 2 | Profile / workflow chip | -| Progress | 0 or 1–2 | Only while active / workflow | -| Model / action bar | 1 | Above prompt | -| Prompt (bordered) | 3+ content growth capped | Cap fraction of terminal; scroll internally | -| Status | 1–2 | Under prompt | -| Goal / task / agents / plugin | 0–1 each, collapsed default | Dense detail → modal or expand | -| **Transcript** | **≥ 12 on 80×24 idle** | Non-negotiable floor | - -Dense content never becomes an unbounded permanent strip. - -### Interaction contract (v1) — **LOCKED** - -| Intent | Binding | Notes | -|---|---|---| -| Command palette | **Ctrl+O** | Reclaim from tool-expand | -| Queue message mid-run | **Enter** (when agent busy) | Badge count on prompt; does **not** stop the agent | -| Steer ASAP | **Alt+Enter** | Deliver at next **tool boundary** (Amp-like steer) | -| Interrupt now | **Ctrl+C** | Hard stop current run (must not be plain Enter) | -| Help / keymap | Palette + `/help` | Tables must match | -| Copy path | **Alt+C** (existing direction) | Message/tool/diff; not mouse-drag | -| Esc | Pop focus stack | Overlay → prompt; never silent no-op | -| Wheel | Active scroll lease only | Disabled for transcript when modal owns focus | - -**Queue drain:** messages deliver at **tool boundary** (next tool result / ASAP opportunity), not only full idle. Enter enqueues; Alt+Enter steers (priority ASAP at tool boundary). Exact micro-semantics (queue vs steer priority when both pending) live in CL-5394 constitution. - -**Breaking change:** today's Enter-interrupt / Alt+Enter-queue is inverted and expanded (Ctrl+C = interrupt). Document once; no legacy toggle in v1 (educate on hint line + release note). - ---- - -## 6. Target architecture - -``` -┌─────────────────────────────────────────┐ -│ OpenTUI app shell (single layout owner) │ -│ header │ transcript │ prompt │ status │ -│ overlay host (modal mode) │ -└─────────────────────────────────────────┘ - │ │ - geometry contract focus tree + scroll lease - │ │ - zone registry list viewport kit - (declared heights) (shared windowing) -``` - -### Contracts (define before porting surfaces) - -**Geometry** - -- Inputs: terminal size, declared chrome zones (measured or fixed-with-test), overlay mode. -- Outputs: region rects (x, y, width, height) for header, transcript, prompt, status, overlay. -- Forbidden: leaf components subtracting magic constants from `process.stdout.rows`. - -**Scroll** - -- Content model + **measured row heights** (or OpenTUI-native scroll that owns measurement). -- Pin policy: follow-tail vs user-pinned; jump-to-bottom affordance. -- Scroll unit must not be “virtual log line index that changes height under wrap.” - -**Focus** - -- Tree: overlay host > entered subagent > prompt/transcript shell. -- Exactly one scroll lease; keyboard and wheel share it. -- Restore previous focus on close. - -**Long log (LOCKED ideal)** - -- Keep a hard working set: only content near the viewport is fully expanded/rendered. -- When the operator scrolls **up**, material that leaves the bottom of the viewport **collapses/hides**; material scrolling into view at the top **expands/shows**. -- Symmetric when scrolling down: off-screen content collapses; in-view content is real. -- Goal: multi-thousand-line sessions stay interactive without rendering the entire history at full fidelity. -- Numeric N / window size set during CL-5399 with a laptop budget (interactive scroll after multi-k lines). - -### Kill list (must not reappear on OpenTUI) - -- Guessed fixed row tables that drift from paint -- Parallel geometry hook + paint tree heights -- Absolute overlays without layout-owned clip -- N independent scroll hooks without a lease -- Unbounded stacked chrome with only `max(1, …)` floor -- Reusing one overlay budget for another surface - -### Non-goals (this project) - -- Web workbench / portal UI -- Amp remote control / multi-thread / web plugin mirror -- Inference, permission policy, or agent-loop rewrites (except TUI wiring) -- Theme marketplace -- Perfect native drag-select -- Building on pi-tui or staying on Ink as destination -- Full keymap settings editor (unless free with table) -- **Dual-release Ink + OpenTUI** — no shipping two paint paths; migration is branch-hard-cutover -- **Windows** as a v1 support target (do not block on it) - ---- - -## 7. Migration strategy (branch hard cutover) — **LOCKED** - -**Not** a dual-entry product flag. **Not** shipping OpenTUI until the whole epic is ready. - -1. **Spike** OpenTUI on Bun (FFI, install, binding) → go/no-go. Spike *is* the start of the migration branch attempt. -2. **Constitution** locks budget, focus, scroll, palette, queue/steer/interrupt (docs + acceptance scenarios). -3. **Single migration branch** builds the full OpenTUI shell + platform + all primary surfaces. -4. **Gate:** whole epic acceptance corpus green → merge and ship. If it fails, **scrap the branch** and stay on Ink (do not land half). -5. **Platform** on that branch: shell + geometry + list kit + focus/scroll lease + harness. -6. **Migrate critical path** on that branch: transcript → prompt → approvals/operator → pickers → agents/goal chrome → permissions/settings/help → palette. -7. **Quiet UI** on that branch after geometry rules exist. -8. **Remove Ink** as part of the same cutover (not a later flag flip on main). -9. **Bar** before merge: size matrix, peer pass (incl. Amp), long-session smoke. - -### Strangler order (on the migration branch) - -``` -app chrome shell - → transcript + scroll lease + long-log window - → prompt + queue/steer/interrupt - → overlay host (permissions, operator, model) - → palette - → agents strip / goal / task zones - → settings / help / residual - → delete Ink path - → merge only when acceptance corpus green -``` - -### Rollback / failure mode - -- **Before merge:** scrap migration branch; main stays Ink. -- **After merge:** normal git revert of the merge if catastrophic; no permanent dual paint path. -- Never ship “OpenTUI shell with Ink fallback” as a product mode. - -### Platform support (LOCKED) - -| Priority | Platform | Packaging / Bar | -|---|---|---| -| **#1** | **macOS** | Must pass; primary development target | -| **#2** | **Linux** | Must pass for CI/server users | -| — | **Windows** | Do not block v1; best-effort only if free | - ---- - -## 8. Milestone map - -| Milestone | Intent | Gate | -|---|---|---| -| **0. Renderer** | OpenTUI spike, binding, packaging plan, comparison note | Go/no-go written | -| **1. Constitution** | Budget, ownership, freeze policy, interaction contract | Reviewed + locked | -| **2. Platform** | Shell, geometry, list kit, focus/scroll, harness | Unit + harness green | -| **3. Migration** | All primary surfaces on branch; hard cutover | Acceptance corpus green; Ink deleted on merge | -| **4. Quiet UI** | Collapse chrome, glyph quiet, dense→modal | Budget held on 80×24 | -| **5. Bar** | Size matrix (macOS #1, Linux #2), peers (Amp/OpenCode/Claude/pi), long smoke | Acceptance signed | - ---- - -## 9. Issue index (Linear) - -### 0. Renderer - -| ID | Title | Plan role | -|---|---|---| -| CL-5365 | Spike OpenTUI on Bun | Go/no-go evidence | -| CL-5366 | Binding choice | Decision record from spike | -| CL-5368 | Renderer comparison note | Fold into spike decision (not floating research) | -| CL-5370 | Install/CI/packaging | Plan early; implement after go | - -### 1. Constitution - -| ID | Title | Plan role | -|---|---|---| -| CL-5364 | Layout constitution | Principles + contracts | -| CL-5367 | Freeze Ink chrome | Policy + open-ticket triage | -| CL-5369 | Chrome zone registry | Budget numbers + registry | - -**Filed from this plan:** CL-5395 focus design · CL-5394 interaction contract · CL-5398 dual-entry · CL-5397 palette · CL-5393 follow-tail · CL-5399 long-log · CL-5391 copy path · CL-5396 ticket triage · CL-5400 plan lock · CL-5392 residual overlays. - -### 2. Platform - -| ID | Title | Plan role | -|---|---|---| -| CL-5377 | App shell | Frame owner | -| CL-5372 | Geometry resolver | Measured regions | -| CL-5376 | List viewport kit | Shared lists | -| CL-5374 | Input ownership impl | Focus tree + scroll lease | -| CL-5373 | Test harness | Raise priority; before kit claims CI | - -### 3. Migration - -| ID | Title | Plan role | -|---|---|---| -| CL-5375 | Transcript / event log | Critical path | -| CL-5371 | Prompt box | + queue-default wiring | -| CL-5382 | Approvals / operator | Overlay host consumer | -| CL-5380 | Model/provider pickers | List kit consumer | -| CL-5379 | Agents / goal / task chrome | Zone model | -| CL-5384 | Permissions UI | List kit + budget | -| CL-5381 | Remove Ink | Endgate | - -**Missing surfaces to cover in expansion:** settings, help, status/header, subagent session view, plugins manager, session resume, mention list, streaming markdown host. - -### 4. Quiet UI - -| ID | Title | Plan role | -|---|---|---| -| CL-5383 | Collapse mode/goal/task | Defaults | -| CL-5385 | Dense → modal | Product rule enforcement | -| CL-5378 | Glyph quiet | Polish after structure | - -### 5. Bar - -| ID | Title | Plan role | -|---|---|---| -| CL-5386 | Size matrix | Also early regression, not only end | -| CL-5387 | Peer pass | **Add Amp** to checklist | -| CL-5388 | Long-session smoke | Ghosting / overpaint / resize | - ---- - -## 10. Acceptance corpus (scenarios) - -These replace “fix overflow bugs one by one.” Each must pass on OpenTUI before cutover. - -1. **Starved chrome:** goal + tasks + agents + active progress on 24 rows → transcript still ≥ min floor; expand/collapse works. -2. **Permission list:** 30 options; keep-active-visible; wheel only on list; close restores prompt. -3. **Operator question:** long prompt text + many choices; no overpaint into status. -4. **Prompt expand:** multi-line paste; internal scroll; transcript does not vanish. -5. **Stream follow:** continuous tool output; auto-follow; scroll up pins; jump-to-bottom returns. -6. **Queue mid-run:** type + Enter while busy → badge; agent continues; Alt+Enter steers at tool boundary; Ctrl+C interrupts. -7. **Palette:** Ctrl+O → open permissions → Esc → prompt focused. -8. **Copy path:** Alt+C style flow copies last assistant message without mouse. -9. **Subagent observe:** enter child, scroll independently, Esc to parent; parent lease restored. -10. **Resize mid-session:** 80×24 ↔ 120×40; no ghost lines; prompt row stable. -11. **Settings / help:** open/close; no residual absolute paint. -12. **Long log:** multi-thousand lines; scroll remains interactive (define numeric budget in CL long-log). - -Map each scenario to automated harness where possible; manual for terminal-specific paint. - ---- - -## 11. Related failure-class tickets (triage) - -Absorb into platform rather than deep Ink fixes (unless true P0): - -- Provider picker scroll (e.g. CL-5363) -- Goal resume / mode chrome overflow (e.g. CL-5199, CL-5196) -- Permissions row budget -- Scroll hygiene / line-granular scroll historical class - -Policy: **minimal Ink patch only if daily-use blocker**; otherwise link as related to migration consumer tickets. - ---- - -## 12. Decisions (LOCKED 2026-08-05) - -| # | Topic | Decision | -|---|---|---| -| 1 | Mid-run **Enter** | **Queue** (badge); does not stop agent | -| 2 | Mid-run **Alt+Enter** | **Steer** ASAP at next **tool boundary** | -| 3 | **Ctrl+C** | **Interrupt** current run (hard stop) | -| 4 | Queue drain | **Tool boundary** (not only full idle/turn end) | -| 5 | OpenTUI **binding** | **Spike decides** (React vs Solid vs core) — CL-5365/5366 | -| 6 | OS support | **macOS #1**, **Linux #2**, **Windows do not block** | -| 7 | Long log | Viewport working set: scroll up → collapse off-bottom / show into-top; numeric N in CL-5399 | -| 8 | Cutover | **Branch hard cutover** — no dual-release Ink+OpenTUI; ship only when whole epic works; scrap branch on fail | - -### Still for spike / constitution polish (not product direction) - -- Exact queue vs steer priority when both pending (implement CL-5394 with tool-boundary delivery). -- Numeric long-log window size and collapse thresholds (CL-5399). -- Binding ADR after spike evidence (CL-5366). -- Terminal matrix within macOS/Linux (iTerm2, Ghostty, Apple Terminal, tmux, common Linux terms). - ---- - -## 13. Work sequence (do this order) - -1. Land this plan + product brief (decisions locked above). -2. Expand Linear issues with plan links, blockers, kill-list, acceptance corpus (done for core set). -3. Constitution pack (CL-5364/5369/5394/5395) + Renderer spike (CL-5365) in parallel. -4. Open migration branch after go; build full epic on branch. -5. No merge until acceptance corpus + Bar minimum green. -6. No Quiet UI chrome redesign until Migration geometry exists on branch. -7. Run size matrix on each migration PR on the branch (continuous, not end-only). - ---- - -## 14. References - -### Constitution pack - -- Layout constitution: `docs/tui-layout-constitution.md` (principles, zone registry, geometry contract, kill list) -- Interaction contract: `docs/tui-interaction-contract.md` (queue / steer / interrupt, keys, palette) -- Ink freeze: `docs/tui-ink-freeze.md` (what may still patch on Ink before cutover) -- Migration cutover: `docs/tui-migration-cutover.md` (branch hard cutover, merge gate) - -### Product / architecture entry points - -- Product brief: `briefs/tui-rebuild-opentui.md` -- Product UX claims: `docs/PRODUCT.md` (TUI section) -- Architecture: `docs/ARCHITECTURE.md` (TUI subsection; Ink today, OpenTUI target) -- Spike report: `docs/plans/opentui-spike-report.md` - -### Code ownership and peers - -- Current ownership: `src/tui/chrome-zones.ts`, `chrome-geometry.ts`, `hooks/use-layout-geometry.ts`, `hooks/use-scroll*.ts`, `components/event-log.tsx`, `app.tsx`, `modal-stack.tsx`, `overlay-stack.tsx` -- Amp: https://ampcode.com/manual -- OpenTUI / OpenCode: peer stack reference -- pi-tui: comparison only (`@earendil-works/pi-tui`) diff --git a/docs/tui-cutover-readiness.md b/docs/tui-cutover-readiness.md deleted file mode 100644 index 30151ae33..000000000 --- a/docs/tui-cutover-readiness.md +++ /dev/null @@ -1,234 +0,0 @@ -# TUI cutover readiness — post-cutover re-score - -**Branch:** `migration/opentui-tui` -**Policy:** `docs/tui-migration-cutover.md` — hard cutover, no dual-ship. -**Scope of this doc:** what shipped, what was verified and how, and what parity is -still missing. It does **not** authorize a merge; it tells a reviewer exactly what -they would be merging. The merge question is live, so §"What a reviewer would be -merging today" states that plainly. - -> **Last verified against the code: 2026-08-06.** -> Method: every claim below was re-checked by reading the module and symbol it -> names at branch HEAD (`f1a189c`), plus a full `bun run typecheck` and `bun test` -> on macOS. Claims that could not be demonstrated in the code were deleted, not -> softened. Nothing here is scored on a real-TTY run — see §"Real-terminal -> coverage". A prior revision of this doc carried five "blocking" items that had -> already been fixed; they are listed in §"Closed since the last revision" so a -> reader can see the history without mistaking it for the present. - -## Status summary - -The cutover is done. OpenTUI is the only renderer. - -| Claim | State | How it was checked | -|-------|-------|--------------------| -| Interactive CLI renders with OpenTUI | yes | `src/index.ts` → `runTUI` (`src/tui/runner.ts`) → `mountRunnerHost` (`src/tui-opentui/runner-host.ts`) | -| Onboarding on OpenTUI | yes | `src/tui/onboarding.ts` → `runProviderSetup` (`src/tui-opentui/provider-setup.ts`) | -| Resume picker + session-mode prompt on OpenTUI | yes | `src/tui/pick-session.ts`, `src/tui/session-mode-prompt.ts` → `runListModal` (`src/tui-opentui/list-modal.ts`) | -| Ink tree deleted | yes | no `.tsx` file remains under `src/`; no `from "ink"` import anywhere | -| Ink/React deps removed | yes | `ink`, `react`, `yoga-layout` and friends are absent from `package.json` | -| Build clean | yes | `bun run build` bundles `src/index.ts` | -| Typecheck clean | yes | `bun run typecheck` — no output | -| Suite green | yes, one known unrelated baseline failure | `bun test` — 4217 pass / 1 fail across 315 files; the failure is `src/perf/permission-subagent-spans.test.ts` ("records allow decision when operator approves a shell ask"), pre-existing and unrelated to the renderer | - -### What "verified" can and cannot mean here - -There is **no automated test that mounts `runTUI`**. Every OpenTUI test — including -the ones that exercise production wiring — mounts the `@opentui/core` test renderer, -not a real TTY. So the strongest automated evidence available is: - -- **production-wired** — the test drives the same module the CLI drives - (`product-host.ts`, `runner-host.ts`, `gate-wire.ts`, `command-surfaces.ts`, - `stream-event-map.ts`, `provider-setup.ts`, `list-modal.ts`), with the production - event/emitter shapes, on the headless renderer. -- **platform-kit only** — the test drives `shell.ts` / `overlays.ts` / `geometry.ts` - with fixture data. Real paint on a real terminal is not covered. - -## Real-terminal coverage - -**No scenario in this document has been verified on a real TTY.** Every acceptance -result below comes from the headless test renderer. The manual size-matrix -checklist at the end of this doc is **still unsigned**. Two of the twelve -acceptance scenarios (resize, and paint under an expanded prompt) are -terminal-specific by nature and cannot be closed any other way. Treat the -automated corpus as evidence that the wiring is correct, not that the shell paints -correctly on a user's terminal. - -## What a reviewer would be merging today - -- A complete, single-renderer OpenTUI shell that is the only interactive path: - onboarding, resume, session-mode prompt, transcript, prompt, overlays, palette, - permissions and operator gates, subagent observe, settings, plugins, hooks. -- Full slash-command parity with the deleted Ink registry: `registerBuiltInCommands` - is on the production path, typed `/` commands parse, and the palette is a second - route to the same catalog. -- `@`-mentions, image attachments, sent-message recall, and a keyboard copy path. -- Four known open items, all in §Blocking or §Behavioral deltas below: mouse - selection policy (CL-5540), paste in real terminals (CL-5541), test-renderer - lifetime in CI (CL-5539), and Shift+Enter on terminals that do not report the - modifier. -- An unsigned manual size matrix. - -## Acceptance corpus (`docs/plans/tui-layout-scroll-platform.md` §10) — re-scored - -| # | Scenario | Result | Evidence | Level | -|---|----------|--------|----------|-------| -| 1 | Starved chrome: goal + tasks + agents + progress on 24 rows → transcript ≥ floor | PASS | `geometry.test.ts` (collapse order, idle floor ≥ 12, prompt floor reclaim); `chrome-state.test.ts` maps the live governor/task/agent snapshots the runner passes | production-wired for the chrome mapping, platform-kit for the layout solve | -| 2 | Permission list: 30 options, keep-active-visible, wheel scoped, close restores prompt | PASS | `overlays.test.ts` (30 options, keep-active-visible, Esc restores); `gate-wire.test.ts` (`permission.gate` opens the overlay and resolves the real approval callback) | production-wired | -| 3 | Operator question: long body + many choices, no overpaint into status | PASS | `overlays.test.ts` operator; `gate-wire.test.ts` `operator.gate` end-to-end through the emitter | production-wired | -| 4 | Prompt expand: multi-line paste, internal scroll, transcript survives | PARTIAL | `geometry.test.ts` covers prompt growth and floor reclaim; `prompt-features.test.ts` covers bracketed paste into the live prompt. The user-reported paste failure in a real terminal (CL-5541) is not reproduced by any test | platform-kit only | -| 5 | Stream follow: continuous tool output, auto-follow, scroll pins, jump-to-bottom | PASS | `shell.test.ts` (sticky tail, scroll-up pin, append does not yank viewport); `stream-event-map.test.ts` maps real reactor events; `product-host.test.ts` paints emitter events into the shell | production-wired | -| 6 | Queue mid-run: Enter queues, Alt+Enter steers at tool boundary, Ctrl+C interrupts | PASS | `session-queue.test.ts`, `runtime-bridge.test.ts` (queued item delivers at `tool.boundary`), `live-session-port.test.ts` (forwards to the runner's `send` / `deliver` / `interrupt`) | production-wired | -| 7 | Palette: Ctrl+O → open permissions → Esc → prompt focused | PASS | `wave6.test.ts` palette open/stack/Esc; `command-catalog.test.ts` + `command-surfaces.test.ts` for the real dispatch | production-wired | -| 8 | Copy path: Alt+C copies last assistant message without mouse | PASS | `wave6.test.ts` copy mode (freeze targets, default last, navigate, Esc cancels); `copy-path.test.ts`, `system-clipboard.test.ts` | production-wired for selection/formatting; the OS clipboard write itself is behind a port | -| 9 | Subagent observe: enter child, scroll independently, Esc to parent, lease restored | PASS | `observe-live.test.ts` (host-supplied session, mapped production child events, Esc restores parent transcript + focus lease); `runner-host.test.ts` `observeSessionFromSubAgents` picks the newest running session | production-wired | -| 10 | Resize mid-session 80×24 ↔ 120×40, no ghost lines, prompt row stable | PARTIAL | `overlays.test.ts` resize keeps floors; `geometry.test.ts` 120×40 accrues residual to transcript. Ghost-line/paint behavior is terminal-specific and untested | platform-kit only; needs a manual run | -| 11 | Settings / help: open/close, no residual absolute paint | PASS | `wave7.test.ts` residual surfaces; `command-surfaces.test.ts` drives the settings, permissions, plugins and hooks surfaces against a live settings snapshot | production-wired | -| 12 | Long log: multi-thousand lines, scroll stays interactive | PASS | `long-log.test.ts` (window slice is O(window), not O(total)); `wave6.test.ts` multi-thousand append stays windowed | platform-kit only, but the budget is asserted numerically | -| 13 | Model/provider picker | PASS | `model-catalog.test.ts` maps the runner's real provider config; `overlays.test.ts` model picker accept; `runner-host.test.ts` routes `models` | production-wired for select-and-apply only — connect / usage / re-auth panes do not exist (see gaps) | -| 14 | Onboarding / resume / session-mode prompt | PASS | `provider-setup.test.ts` (field flow, secret masking, connection-test failure, save-anyway, OAuth login step, Ctrl+C aborts), `list-modal.test.ts` (accept, arrows, Esc, Ctrl+C) | production-wired — these are the modules the CLI mounts | - -No scenario is marked PASS on the strength of a real-terminal run, because none was -performed. - -## Blocking — a normal user will hit these - -1. **Paste is reported broken in a real terminal (CL-5541).** Bracketed paste - verifies in the harness (`prompt-features.test.ts`) and Ctrl+V / Ctrl+P image - attach is wired (`attachClipboardImage` in `src/tui-opentui/shell.ts`, bound in - the shell's key handler), but the reported real-terminal failure is not - reproduced or explained. Until someone pastes into a real TTY, treat text paste - as unverified. -2. **Mouse selection policy (CL-5540).** DEC mouse reporting defaults on in - the main shell (`useMouse` in `src/tui-opentui/product-host.ts:218`), so - wheel scroll and click-to-expand work out of the box. The cost is native - text selection, which the terminal cannot perform while reporting is on; - `Alt+M` (`toggleMouseCapture` in `src/tui-opentui/shell.ts:4006`) hands the - mouse back for that. The satellite pickers (`list-modal.ts`, - `provider-setup.ts`) keep reporting off and are unaffected. This is the - settled decision, not a pending tradeoff. -3. **Shift+Enter does not insert a newline on terminals that do not report the - modifier.** `Ctrl+Enter` and `Ctrl+J` are the working newline chords and the - help catalog says so (`src/tui-opentui/keybindings.ts`). The kitty keyboard - protocol path was verified end to end, so this is terminal reporting, not a - decode defect — but a user on a plain terminal who reaches for Shift+Enter will - send the message. -4. **The manual size matrix is unsigned.** See §Real-terminal coverage. This is a - process gap, not a code gap, but it is the single largest unknown in this - document. - -## CI risk - -**CL-5539 — test renderers are never freed.** `withTestRenderer` -(`src/tui-opentui/harness.ts:157`) destroys the renderer in a `finally`, but 15 -call sites across 9 test files still call `createHarness` directly and never -destroy: `runner-host.test.ts` (6), `provider-setup.test.ts` (2), and one each in -`list-modal.test.ts`, `copy-wire.test.ts`, `harness.test.ts`, -`focus-routing.test.ts`, `product-host.test.ts`, `reasoning-fold.test.ts`, -`zz-paste-probe.test.ts`. On a fast macOS machine the current run emits zero -`Failed to create renderer` errors, so the leak is latent locally; on a 2-core CI -runner it has turned into a cascade of failures. Fix in flight. Do not read a -green local run as evidence CI is safe. - -## Missing surfaces - -1. **In-session provider re-auth is unmounted.** `runProviderSetup` handles OAuth - during onboarding (`OAUTH_STEPS` in `src/tui-opentui/provider-setup.ts:78`), but - an expired profile mid-session has no re-auth surface. - `src/tui-opentui/provider-connect.ts` has no importer at all. -2. **Model surface is select-only.** `openCommandSurface`'s `models` case - (`src/tui-opentui/command-surfaces.ts:915`) delegates to `openModels` and - nothing else — no connect, usage, or re-auth panes. -3. **Tasks view is unmounted.** `applyCommandResult`'s `view` case - (`src/tui/runner.ts:1796`) prints "not available in this renderer yet". No - built-in command currently returns a `view` result, so nothing reaches it today, - but the surface does not exist. -4. **Non-model modals print a placeholder.** `applyCommandResult`'s `modal` case - (`src/tui/runner.ts:1790`) routes `agent` to the model picker and reports every - other modal as unavailable. - -## Behavioral deltas against the deleted Ink shell - -1. **Shift+Tab is unimplemented**, and with it the in-session auto-mode toggle. - Nothing in `src/tui-opentui/` binds it; the only references are the stale - comments at `src/config/index.ts:388` and `src/permission/gate.ts:253`. Auto - mode is settable only via `--auto` / `--no-auto` at launch. -2. **Session mode writes global scope only.** `promptSessionModeIfUnset` - (`src/tui/session-mode-prompt.ts`) calls `saveGlobalSettings`; Ink also offered a - per-repo local scope. -3. **Permissions list is flat with Enter-to-revoke** - (`command-surfaces.ts:518`, title "permissions · Enter revokes"), where Ink - grouped by scope and used `d` to delete. -4. **Markdown flickers mildly while streaming.** The one deterministic cause — a - bare `####` painting as literal text before its heading text arrives — is fixed - with a regression test (`markdown-rows.test.ts:126`). Residual flicker is - reported but not characterized. - -## Orphaned modules - -The renderer rewrite moved most of the former `src/tui` platform code into -`src/tui-opentui`. What is genuinely unreferenced by any production path today: - -- `src/tui/kill-ring.ts` — superseded by `src/tui-opentui/prompt-kill-ring.ts`. -- `src/tui-opentui/provider-connect.ts` — the unmounted re-auth surface above. -- `src/tui-opentui/demo.ts`, `smoke.ts`, `harness.ts` — developer entry points, not - product code. -- `src/tui-opentui/observe-map.ts` — observe mapping now lives in `runner-host.ts` - (`observeSessionFromSubAgents`, line 187). - -Everything else the previous revision listed as orphaned has since been either -rewired or deleted. This list was regenerated mechanically on 2026-08-06 by -resolving every `src/tui/**` and `src/tui-opentui/**` module against its -non-test importers; regenerate it the same way rather than editing it by hand. - -## Closed since the last revision - -Kept only so a reader who saw the previous revision does not re-file these. All -were verified fixed on 2026-08-06. - -| Was claimed | Actual state | -|---|---| -| Built-in slash commands are not registered | `runner.ts:84` imports `registerBuiltInCommands`; `runner.ts:351` calls it inside `setUpCommandRegistry`, which `runner.ts:398` invokes. All 15 built-ins register, including `hooks`. | -| Typed `/` commands do not parse and are sent to the model | `runner.ts:271-272` intercepts the leading `/`; `shell.ts` opens a completion list at an empty prompt (see the `/` row in `keybindings.ts`). | -| `@`-mention resolution is unwired | `ingestPathMentions` at `runner.ts:1821`, `resolveAtMentions` at `runner.ts:1822`, `setMentionSuggestionSource` at `runner.ts:2099`. | -| Image attachments are gone; `/paste-image` reports "not available" | `runner.ts:1799` routes `paste-image` to `attachClipboardImage`; `readClipboardImage` feeds `shell.pendingAttachments` (`shell.ts:824`), bound to Ctrl+V and Ctrl+P. | -| Help catalog documents Ctrl+D as "delete character under cursor" | Correct: the host claims no quit key, so Ctrl+D is the prompt default. Quitting is Ctrl+C twice. | -| There is no exit-confirm step | Ctrl+C arms a 2-second exit window and quits on a second press (`CTRL_C_EXIT_WINDOW_MS`, `shell.ts:3843`). | -| `quota-retry.ts` / `stall-watchdog.ts` have no production importer | Both moved to `src/tui-opentui/` and are imported by `runtime-bridge.ts:35,40`; `product-host.ts:232` opts the host into their timers. | -| Sent-message history recall is unimplemented | `shell.ts:45` imports `src/tui/sent-message-history.ts`; `runner.ts` appends every sent prompt and Up/Down recall is in the help catalog. | -| Plugins manager is an enable/disable toggle only | `command-surfaces.ts` carries a credential-entry pane (`openCredentialsPane`, line 562), trust state, and a web-provider override. | -| Two dozen `src/tui` modules are orphaned | Regenerated; see §Orphaned modules. Four modules remain. | - -## Size matrix - -| Platform | Role | How to run | -|----------|------|------------| -| macOS (darwin) | Primary interactive | run `corbits` in a real TTY at 80×24 and 120×40 | -| Linux CI | Headless gate | `bun test ./src/tui-opentui` — `@opentui/core` test renderer, no TTY | -| Geometry pure | Any | `geometry.test.ts` — no `process.stdout`; explicit columns/rows | - -**Floors (constitution)** - -- Idle closed overlay: transcript ≥ 12 on 80×24 -- Inset overlay open: transcript ≥ 8 -- Residual rows accrue to transcript, not chrome - -Manual checklist — **not yet signed off by an operator**: - -- [ ] 80×24 idle: status visible; prompt not clipped -- [ ] 80×24 permissions open: list scrollable; Esc restores -- [ ] 80×24 palette over permissions: Esc ×2 restores prompt -- [ ] 120×40: extra rows land in transcript -- [ ] Observe enter/leave: parent stream restored -- [ ] Ctrl+C interrupts a run, and twice in a row exits cleanly; Ctrl+D only deletes a character -- [ ] Resize mid-session leaves no ghost rows -- [ ] Paste multi-line text into the prompt (CL-5541) -- [ ] Drag-select and copy transcript text with the mouse; `Alt+M` restores click-to-expand (CL-5540) - -## Related - -- Plan: `docs/plans/tui-layout-scroll-platform.md` (§5, §7, §10, §12) -- Cutover policy: `docs/tui-migration-cutover.md` -- Constitution: `docs/tui-layout-constitution.md` -- Interaction contract: `docs/tui-interaction-contract.md` -- Code: `src/tui-opentui/**`, `src/tui/runner.ts` diff --git a/docs/tui-ink-freeze.md b/docs/tui-ink-freeze.md deleted file mode 100644 index bbe943106..000000000 --- a/docs/tui-ink-freeze.md +++ /dev/null @@ -1,5 +0,0 @@ -# Ink freeze policy (superseded) - -**Status:** historical — Ink has been deleted from the repo; the OpenTUI cutover is complete. - -This document described the freeze rules that governed changes to the Ink-based TUI during the OpenTUI migration. The Ink/React tree no longer exists in this repo (no `.tsx` files remain), so the policy no longer applies. See `docs/tui-cutover-readiness.md` for the current state of the OpenTUI shell. diff --git a/docs/tui-interaction-contract.md b/docs/tui-interaction-contract.md deleted file mode 100644 index b3002ba61..000000000 --- a/docs/tui-interaction-contract.md +++ /dev/null @@ -1,341 +0,0 @@ -# TUI interaction contract - -**Status:** constitution (locked product bindings + implementable focus/scroll design) -**Source plan:** `docs/plans/tui-layout-scroll-platform.md` §5, §12 -**Product brief:** `briefs/tui-rebuild-opentui.md` -**Siblings:** `docs/tui-layout-constitution.md`, `docs/tui-cutover-readiness.md` - -This document is the interaction constitution for the OpenTUI shell. It locks mid-run send semantics, discovery chords, focus ownership, and scroll lease rules so implementers do not re-open product decisions or invent focus/scroll races. - -The cutover has landed and OpenTUI is the shipping renderer, but not every binding below is implemented — `docs/tui-cutover-readiness.md` records which ones are missing (Shift+Tab, and Shift+Enter on terminals that do not report the modifier) and where runtime behavior deviates (Ctrl+C interrupts, and quits on a second press inside a two-second window). - ---- - -## 1. Purpose - -Operators abandon tools that fight them mid-stream. Corbits today inverts market-leader muscle memory (Enter interrupts; Alt+Enter queues) and splits discovery across slash, help, and tribal knowledge. Multiple scroll hooks and boolean focus flags race keys and wheel. - -This contract fixes the **operator-facing** half of that failure class: - -1. Mid-run send is **queue-default**; interrupt is loud and rare. -2. One discovery chord owns the command palette. -3. Exactly one focus owner and one scroll lease at a time; Esc always pops something real. - -Geometry budgets and long-log window sizes live in sibling constitution docs. This file owns **keys, queue/steer/interrupt, focus tree, and scroll lease**. - ---- - -## 2. Locked binding table - -Do not reopen these product decisions. They match the plan’s locked table. - -| Intent | Binding | Notes | -|---|---|---| -| Queue message mid-run | **Enter** (agent busy, non-empty submit) | Badge count on prompt; does **not** stop the agent | -| Steer ASAP | **Alt+Enter** | Deliver at next **tool boundary** | -| Interrupt now | **Ctrl+C** | Hard stop current run; must not be plain Enter | -| Command palette | **Ctrl+O** | Reclaim from today’s tool-expand chord | -| Help / keymap | Palette entry + `/help` | Tables must match handlers | -| Copy path | **Alt+C** (existing direction) | Message / tool / diff; not mouse-drag | -| Expand a collapsed body | **e** | One idiom: collapsed approval payloads while the overlay owns focus, collapsed skill rows while the transcript does | -| Esc | Pop focus stack | Overlay → prior focus; never silent no-op | -| Wheel | Active scroll lease only | Transcript wheel off when modal owns lease | -| Newline in prompt | **Shift+Enter** (Alt+Enter when idle if terminal maps it) | Mid-run Alt+Enter is steer, not newline | - -**No legacy toggle** for “Enter interrupts” in v1. Educate once via hint line + release note. - ---- - -## 3. Queue, steer, interrupt - -### 3.1 Definitions - -| Term | Meaning | -|---|---| -| **Idle** | No in-flight parent inference/tool work and no running child agent work that blocks drain | -| **Busy** | Parent run active and/or child work that holds the drain gate | -| **Queue** | Operator message accepted while busy; stored for later delivery; agent continues | -| **Steer** | Operator message accepted while busy; marked priority for ASAP delivery at the next tool boundary | -| **Interrupt** | Abort the current run immediately; discard pending deliveries; do not auto-replay the stopped turn | -| **Tool boundary** | ASAP safe injection point: after a tool result lands (`tool.done` / equivalent), before the next inference that would otherwise continue the prior plan; also when the run reaches true idle if no earlier boundary fired | - -### 3.2 Enter — queue (default mid-run send) - -When the agent is **busy** and the prompt has a submittable payload: - -1. Enter enqueues the message (FIFO among queue-class items). -2. Prompt clears; badge increments. -3. The running agent is **not** stopped, aborted, or re-prompted. -4. Delivery waits for a **tool boundary** (or idle if the run ends without tools). - -When **idle**, Enter is normal send (immediate delivery), same as today for an idle prompt. - -Empty Enter is a no-op (no phantom queue entry). Slash commands still dispatch immediately when the field starts with `/` (busy or idle); they are not queued as chat. - -### 3.3 Alt+Enter — steer - -When the agent is **busy** and the prompt has a submittable payload: - -1. Alt+Enter accepts a **steer** item (priority class). -2. Prompt clears; badge increments (same badge pool as queue unless UI later splits counts). -3. The agent is **not** hard-stopped. -4. At the next **tool boundary**, steer items drain **before** any plain queue items. - -When **idle**, Alt+Enter is not a second send path: treat as newline if the terminal delivers meta+return that way, otherwise no-op for empty fields. Steer only exists while busy. - -### 3.4 Ctrl+C — interrupt - -While a run is active (parent busy, including entered-subagent observe of a running parent/child session as implemented by the shell): - -1. Ctrl+C **interrupts** the current run (hard stop / `requestStop` class). -2. All pending queue **and** steer items are **discarded**. -3. In-flight tools settle to a terminal stopped state; no silent no-op. -4. Hint line and keymap must name this as stop/interrupt, not “exit”. - -When **idle**: - -- Non-empty prompt: clear the prompt (or existing clear-input path). -- Empty prompt: existing exit-with-confirm path (app quit is not interrupt). - -Interrupt must never be bound to plain Enter. - -### 3.5 Queue vs steer priority (locked micro-semantics) - -When both classes are pending at a tool boundary: - -1. Drain **all steer items first**, oldest-first (FIFO within steer). -2. Then drain **queue items**, oldest-first (FIFO within queue). -3. Drain **one message per boundary** into a new turn (same “one outbound send at a time” rule as today’s drain), unless an implementer proves multi-inject is safe under the reactor — default is **one per boundary**. -4. After that send starts, further pending items wait for the next boundary or idle. - -Rationale: steer is “ASAP course correction”; queue is “when you get a chance.” Mixing them in a single FIFO would bury steers behind earlier casual queues. - -### 3.6 Drain policy - -| Event | Drain action | -|---|---| -| Tool boundary while busy | Prefer steer head, else queue head; one item | -| Run becomes idle (no child work holding gate) | Drain remaining steers then queues, one at a time as each send completes | -| Interrupt (Ctrl+C) | Clear both classes; badge → 0 | -| New session / clear session | Clear both classes | -| Permission / operator modal open | **Do not** inject mid-modal; boundary waits until the run can accept a user message again | - -Drain is **tool-boundary**, not “only full idle.” Waiting only for `connector.reply` / full turn idle is the **current** behavior and is insufficient for the target. - -### 3.7 Badge and clear minimum - -**Badge** - -- Show a single count of pending deliveries: `steer_count + queue_count`. -- When count > 0, prompt chrome includes the count (e.g. `3 queued`). -- Optional later: split `1 steer · 2 queued`; not required for v1. - -**Clear (minimum)** - -| Action | Behavior | -|---|---| -| Clear last | Drop the most recently accepted pending item (steer or queue, by enqueue time) | -| Clear all | Drop every pending item; badge → 0 | -| Interrupt | Implies clear all | - -Discovery: palette entries “Clear last queued” / “Clear all queued” are required for v1 discoverability. A prompt-local chord may be added later; do not steal Esc (Esc is focus-stack only). - -Editing an in-place queued item (reorder, rewrite) is **deferred**; clear last + retype is enough for v1. - -### 3.8 Hint line truth - -While busy, the action/hint line **must** name all three intents in operator language, for example: - -`Enter queue · Alt+Enter steer · Ctrl+C stop` - -When pending count > 0, prefix the count. Empty-field busy state still shows the chords (discoverability without typing). Help overlay, keymap table, and palette descriptions must match these bindings — one source of truth table feeds all three surfaces. - ---- - -## 4. Command palette (Ctrl+O) - -| Rule | Detail | -|---|---| -| Chord | **Ctrl+O** opens the command palette | -| Scope | Searchable actions: slash twins, panels, model/settings/permissions entry, queue clear, help | -| Reclaim | Tool-output expand **must not** keep Ctrl+O. Expand moves to a dedicated non-palette chord later (candidate: keep tool-row local expand / dedicated binding — **not** Ctrl+O). OpenTUI shell (`src/tui-opentui`) implements Ctrl+O → palette as of Wave 6. Residual surfaces (settings/help/plugins/resume/mentions) and observe are palette actions as of Wave 7. | -| Esc | Closes palette and restores prior focus (normally prompt; if palette stacked over a primary overlay, Esc restores that overlay first). Esc while observing a subagent leaves observe and restores the parent stream + lease. | -| Slash | Remains a power path; every user-facing slash entry has a palette twin | -| Stack | Palette may open above an existing primary overlay (permissions / operator / model). Esc pops one frame at a time. | - -Palette open takes the **palette** focus target (overlay priority slot) and the scroll lease for its own list. - ---- - -## 5. Focus tree - -### 5.1 Priority (high → low) - -``` - ┌─────────────────────┐ - │ Overlay host │ palette, permission, help, - │ (modal / manager) │ settings, operator question, … - └──────────┬──────────┘ - │ Esc pops - ┌──────────▼──────────┐ - │ Entered subagent │ observe child session - │ observe view │ - └──────────┬──────────┘ - │ Esc leaves observe - ┌──────────▼──────────┐ - │ Shell │ prompt + transcript (+ thin strips) - │ default focus: │ prompt owns typing; - │ prompt │ transcript owns scroll when leased - └─────────────────────┘ -``` - -### 5.2 Rules - -1. **Exactly one focus owner** receives non-reserved keys at a time. -2. **Overlay host** always wins over subagent observe and shell. -3. **Entered subagent** wins over shell; parent prompt does not steal typing while observing. -4. **Shell default** is the prompt field. Transcript is not a separate “mode” for typing; it holds the scroll lease when no overlay/subagent list owns it. -5. **Esc** pops exactly one level: - - Overlay → previous focus (usually prompt; if observe was under an overlay, restore observe). - - Observe → parent shell (prompt focused). - - Shell with open thin panel (tasks/hooks strip expanded) → collapse panel, stay on prompt. - - Shell with empty stack → existing clear-prompt / no-op policy (never a silent dead key when something is dismissible). -6. Opening B while A is open either **replaces** A or **stacks** A under B; either way Esc returns along a single path to the prompt. No orphan focus. -7. Closing any overlay **restores** the focus node recorded on open (not a hardcoded “always prompt” if observe was active underneath — restore the recorded prior). - -### 5.3 Focus vs global chords - -These chords are handled at the shell/keymap layer even when the prompt is not the text owner, unless an overlay fully captures input for its own confirm flow: - -| Chord | When overlay open | When observe open | When shell | -|---|---|---|---| -| Esc | Pop overlay | Leave observe | Pop panel / clear policy | -| Ctrl+C | Overlay-local cancel if any; else interrupt/exit policy | Interrupt if running; else exit policy | Interrupt if running; else clear/exit | -| Ctrl+O | Close or replace with palette (single stack) | Open palette above observe | Open palette | -| Wheel / PgUp / PgDn | Active lease only | Child transcript lease | Shell transcript lease | - ---- - -## 6. Scroll lease - -### 6.1 One owner - -At any moment exactly one surface holds the **scroll lease**. Keyboard page/line scroll **and** mouse wheel both follow that lease. There is no parallel “wheel on transcript while keys scroll a modal.” - -### 6.2 Lease assignment (high → low) - -| Priority | Surface | When | -|---|---|---| -| 1 | Overlay list / body | Overlay host focused (permissions, palette, settings, help, …) | -| 2 | Entered subagent transcript | Observe view active, no overlay | -| 3 | Prompt internal window | Prompt multi-line content overflows **and** caret navigation needs in-prompt scroll; short-lived, returns to transcript when not needed | -| 4 | Main transcript | Default shell lease | - -Agents strip horizontal navigation is not a vertical scroll lease. - -### 6.3 Rules - -1. **Grant:** focus enter on a scrollable surface grants the lease; previous owner releases. -2. **Release:** focus leave restores the prior lease from the focus stack. -3. **Wheel:** ignored by non-lease surfaces (no dual-scroll). -4. **Follow vs pin (transcript):** auto-follow while at bottom; operator scroll-up pins; show a clear “follow live” / jump-to-bottom affordance; reattach restores follow. -5. **Modal open:** transcript does not consume wheel or page keys; list kit owns them. -6. **Subagent observe:** child transcript has its own offset; Esc restores parent lease and parent offset (parent must not jump to bottom solely because observe closed unless parent was already following). - -### 6.4 Implementer checklist - -- [ ] Single lease token in shell state (not N independent hooks fighting `useInput`). -- [ ] List viewport kit used by palette, permissions, agents, settings (shared windowing + keep-active-visible). -- [ ] Mouse scroll gated on lease id. -- [ ] Tests: modal open → wheel does not move transcript; Esc → transcript lease restored; observe Esc → parent offset stable. - ---- - -## 7. Historical: Ink vs this contract - -The Ink shell has been deleted. This table is kept only to explain why the current -bindings differ from muscle memory built on the old shell. - -| Concern | Old (Ink) | Target (this contract) | -|---|---|---| -| Enter while busy | Interrupt + send (`steerOnEnter` path) | **Queue** (no stop) | -| Alt+Enter while busy | Queue follow-up | **Steer** at tool boundary | -| Interrupt chord | Enter (busy) / Ctrl+C also stops | **Ctrl+C** only for hard stop | -| Queue drain | Idle / `connector.reply` when not processing | **Tool boundary** ASAP | -| Ctrl+O | Expand tool output (visible area) | **Command palette** | -| Ctrl+C idle | Clear input or exit confirm | Unchanged family (clear / exit confirm) | -| Focus | Boolean soup + per-modal `useInput` | Focus tree + restore stack | -| Scroll | N owners (`use-scroll`, windows, modal slices) | One lease | -| Hint line | “Enter steer · Alt+Enter queue” | “Enter queue · Alt+Enter steer · Ctrl+C stop” | -| Keymap help | Ctrl+O = expand tool; Ctrl+C = exit | Must match target table | - -Breaking change is intentional. No compatibility toggle in v1. - ---- - -## 8. Acceptance scenarios - -These are the operator-visible checks for this contract (subset of the plan acceptance corpus). - -### A. Queue mid-run - -1. Start a long-running agent turn with tools. -2. Type a follow-up; press **Enter**. -3. Expect: badge ≥ 1; agent continues; no abort. -4. At next tool boundary (or idle), queued message delivers as a new user turn. - -### B. Steer mid-run - -1. While busy, type a correction; press **Alt+Enter**. -2. Expect: badge increments; agent not hard-stopped. -3. At next tool boundary, steer delivers **before** any earlier plain-queue items still pending. - -### C. Queue vs steer priority - -1. While busy: Enter message A (queue), then Alt+Enter message B (steer). -2. At the next boundary: B delivers first; A remains pending (or delivers on a later boundary after B’s turn starts). - -### D. Interrupt - -1. While busy (with or without pending items), press **Ctrl+C**. -2. Expect: run stops; badge → 0; pending discarded; no silent no-op. -3. Plain Enter never interrupts. - -### E. Palette - -1. **Ctrl+O** opens palette. -2. Find permissions (or model); open it; **Esc** returns focus to prompt (or prior focus). -3. Ctrl+O does not expand tool output. - -### F. Focus + scroll lease - -1. Open a tall permission/options list; wheel/page only moves the list. -2. Esc closes; transcript lease restored; prompt accepts typing. -3. Enter subagent observe; scroll child log; Esc → parent; parent lease restored. - -### G. Hint / help truth - -1. While busy, hint line names queue, steer, and stop with the locked chords. -2. Help/keymap/palette copy matches the locked table. - ---- - -## 9. Non-goals (this contract) - -- Keymap settings editor / user remap UI (discoverable fixed map first). -- Amp-style selective reorder of queue items beyond clear last / clear all. -- Multi-message inject at a single boundary. -- Mouse click-to-focus everywhere. - ---- - -## 10. Related docs - -| Doc | Owns | -|---|---| -| `docs/plans/tui-layout-scroll-platform.md` | Plan + locked decisions source | -| `briefs/tui-rebuild-opentui.md` | Product intent and acceptance narrative | -| `docs/tui-layout-constitution.md` | Chrome budget, geometry ownership | -| `docs/tui-cutover-readiness.md` | Post-cutover state: which bindings are implemented, which are not | - -When implementation lands, the keymap truth table, hint line, and palette labels must be generated from or checked against **this** binding table so help cannot drift again. diff --git a/docs/tui-layout-constitution.md b/docs/tui-layout-constitution.md deleted file mode 100644 index 5a6ea4f1e..000000000 --- a/docs/tui-layout-constitution.md +++ /dev/null @@ -1,284 +0,0 @@ -# TUI layout constitution - -**Status:** locked for Platform wave (OpenTUI rebuild) -**Scope:** layout principles, chrome zone registry, geometry contract, kill list -**Not this doc:** keybindings and mid-run semantics (`docs/tui-interaction-contract.md`), Ink freeze policy (`docs/tui-ink-freeze.md`), migration cutover (`docs/tui-migration-cutover.md`) - -This constitution is the implementer contract for shell geometry on the OpenTUI migration branch. Product direction and epic sequencing live in `briefs/tui-rebuild-opentui.md` and `docs/plans/tui-layout-scroll-platform.md`. Do not reopen locked product decisions here. - ---- - -## 1. Purpose - -The Corbits Code TUI fails today as a **systems** problem: guessed row budgets, dual height owners (manual subtraction vs paint flex), dual overlay stacks, and unbounded optional chrome that starves the event log. - -This document locks: - -1. Layout principles every surface must obey. -2. A chrome zone registry with row budgets and collapse rules. -3. A single geometry contract (who measures, who owns residual height). -4. A kill list of patterns that must not reappear on OpenTUI. - -Implementers without external ticket context should still be able to build shell, geometry, and chrome zones from this file alone. - ---- - -## 2. Principles - -These are hard contracts, not preferences. - -1. **Transcript pays the rent.** Fixed chrome stays thin. Dense UI is modal or collapsed by default. Optional strips never stack without bound. -2. **Measure, do not guess.** One layout owner produces region rects. Leaf components consume those rects. They do not invent heights or subtract magic constants from terminal size. -3. **One geometry equation.** Paint and reservation use the same measured heights. Parallel “hook math” that can drift from the paint tree is forbidden. -4. **Residual transcript.** After fixed chrome, optional chrome (within budget), and the active overlay region are accounted for, remaining height belongs to the transcript. The transcript is never reduced below the hard floor by optional chrome (see §4). -5. **One overlay host.** Blocking surfaces share one host and one height path. No second stack with separate row accounting. -6. **One scroll owner (lease) at a time.** Keyboard and wheel follow the same focus tree. Details of lease handoff live with focus design; layout only ensures each region has a well-defined rect to scroll inside. -7. **One list viewport kit.** Models, permissions, agents, approval options, settings lists share windowing, keep-active-visible, and page/jump. Layout provides the box; the kit fills it. -8. **Modals measure the remaining box.** Overlays size against the layout-owned overlay region (or full-shell modal mode), not against raw `stdout.rows`. -9. **Hard min transcript rows** on 24-row terminals; **chrome priority** when space is scarce (collapse order in §3.3). -10. **Platform before features.** No new permanent chrome zones until shell + geometry + registry land on the migration branch. -11. **Visual quiet (layout implication).** State via text and color density, not extra permanent rows of glyph chrome. -12. **Long log is a working set.** Only near-viewport content is fully expanded/rendered. Off-viewport material collapses. Numeric window size is set with long-log work, not ad hoc per surface. - ---- - -## 3. Chrome zone registry - -### 3.1 Reference terminal - -Budgets are validated against **80×24** as the hard floor and **120×40** as the common laptop size. macOS is primary; Linux is secondary; Windows does not block v1. - -**Non-negotiable:** on **80×24 idle** (no overlay, optional strips collapsed), the transcript region is **≥ 12 rows**. - -### 3.2 Zone table - -| Zone | Role | Idle target (rows) | Min | Max | Collapse / resize rules | Owner | -|---|---|---|---|---|---|---| -| **progress** | In-flight phase / workflow line | 0 when idle; 1–2 when active or workflow chip | 0 | 2 | **0** when inactive and no workflow chip. Shown only while agent is active or a workflow chip needs it. | Shell | -| **progress_divider** | Hairline above prompt stack | 0–1 | 0 | 1 | Present only when the prompt stack is painted as a distinct block. Prefer absorbing into prompt chrome if a hairline is free on OpenTUI. | Shell | -| **notice** | Transient state above the prompt: queue depth, interrupt latch, pinned scroll, a flash, the live turn's density ramp | 0 | 0 | 1 | Present only while a segment is off its default. Never a permanent strip and never a filled bar. | Shell | -| **prompt** | Bordered input (content + borders) | 3 base | 3 | Cap: **≤ 40% of terminal rows**, and never so large that transcript falls below floor when only prompt grows | Content growth **scrolls inside** the prompt box. Cap fraction is of full terminal height. Multi-line paste must not steal the log permanently. | Shell + prompt | -| **goal** | Goal / acceptance strip | 0 default | 0 | **1** collapsed; expanded dense detail → **modal** | Default collapsed or hidden. Implementing phase may show a compact 1-row chip. Full criteria lists are modal, not an unbounded strip. | Zone registry consumer | -| **task** | Work checklist strip | 0 default | 0 | **1** collapsed; expand → **modal** or temporary expand capped at **proposed 5** content rows then modal | Default off or 1-row summary. Full checklist is not a permanent multi-row tenant. | Zone registry consumer | -| **agents** | Orchestrator agents strip | 0 in single-agent; 0–1 collapsed in orchestrator | 0 | **1** collapsed; observe/expand → modal or dedicated region under overlay host | Single-agent sessions pay **0**. Orchestrator default is a thin strip, never a permanent multi-row roster. | Zone registry consumer | -| **plugin_banner** | Plugin / MCP auth / thin notices | 0 | 0 | **1** each class, dismissible or timed | Never stacks multiple multi-line plugin UIs into chrome. Dense plugin admin is overlay. | Shell banners | -| **command_banner** | Command feedback | 0 | 0 | **1–2**, short-lived | Auto-dismiss or Esc; not a permanent zone. | Shell banners | -| **settings_notice** | Settings diagnostics | 0 | 0 | **proposed ≤ 3** rows then collapse remainder into modal/settings | Multi-line dump of every diagnostic is forbidden in chrome. | Shell banners | -| **transcript** | Event log (residual) | **≥ 12** on 80×24 idle | **12** on 24-row idle; **proposed ≥ 8** when overlay open on 24-row | Remaining height | Always residual after higher-priority zones. Optional chrome may not push idle transcript below floor. | Geometry owner | -| **overlay_host** | Single modal / blocking surface | 0 when closed | 0 | Layout-owned region: **proposed ≤ 70% of terminal rows**, and never eliminates the prompt stack entirely on 24-row | One primary blocking overlay at a time. Open replaces or stacks with a single Esc path back to prompt. Height is measured for the active surface inside the host box (list kit + wrap measure), not a per-surface magic constant table outside the host. | Overlay host | - -**Proposed (not yet paint-proven) defaults** are marked **proposed**. Locked floors: progress 0 or 1–2, prompt exactly 3 (capped growth), notice 0–1, optional strips 0–1 collapsed, transcript ≥ 12 on 80×24 idle. - -There is no titlebar, no status strip and no key-hint row. The prompt box is the product: it is anchored at the bottom in every state and it is the *only* permanent chrome. Its border carries the metadata — the model label right-aligned in the top rule, the brand lockup at the left of the bottom rule and the working directory with git branch at its right — so both cost zero rows and the rule breaks around each label. Keys are discoverable from the landing screen and the command palette. No zone paints a full-width background fill. - -### 3.3 Chrome priority when space is scarce - -When `terminal.rows` cannot host all desired chrome without violating the transcript floor, collapse in this order (first cut first): - -1. Temporary banners (`command_banner`, timed notices) — drop or force-dismiss. -2. `settings_notice` / `plugin_banner` — collapse to 0 or single-line “N notices” chip. -3. Expanded `goal` / `task` / `agents` — force collapsed (0–1) or push dense content to modal. -4. `progress` — if idle, already 0; if active, prefer 1 row over 2. -5. `progress_divider` — drop if still short. -6. `notice` — last transient chrome to shrink (0 as soon as it has nothing to say). -7. `prompt` — never below 3; growth reclaims only via internal scroll (already capped). - -In full-shell modal mode the overlay carries its own keys in its title row; no chrome row survives for it. - -**Overlays:** opening a blocking overlay may shrink the transcript below the idle floor, but must leave a **proposed ≥ 8** row transcript (or hide transcript entirely only in full-shell modal mode, where the overlay owns the residual box and the prompt remains reachable on Esc). Full-shell modal mode is explicit (e.g. model picker, settings), not the default for thin permission prompts. - -### 3.4 Idle budget check (80×24) - -Example idle layout that satisfies the floor: - -| Zone | Rows | -|---|---| -| notice | 0 | -| prompt | 3 | -| optional strips | 0 | -| progress | 0 | -| **subtotal chrome** | **3** | -| **transcript residual** | **21** (≥ 12) | - -With progress (2) + thin agents strip (1): chrome 6 → transcript 18. Goal+task+agents all expanded as multi-row strips is **out of constitution** — those expansions must modalize or collapse under §3.3. - -### 3.5 Today vs target (Ink reference only) - -Ink ownership today (do not extend; do not reimplement on OpenTUI): - -| Concern | Today | Target | -|---|---|---| -| Fixed chrome | Constants in `src/tui/chrome-zones.ts` summed into a fixed chrome total | Declared zones in registry; measured or fixed-with-test | -| Variable chrome | `src/tui/chrome-geometry.ts` extra rows (goal, task, plugins, banners, prompt growth) | Same registry; collapse rules; dense → modal | -| Overlay height | Heuristics in `src/tui/hooks/use-layout-geometry.ts` | Overlay host measures content inside a layout-owned rect | -| Paint vs math | Flex grow **and** manual subtraction | One owner; paint consumes rects | -| Overlay stacks | Modal stack **and** overlay stack | One host | - ---- - -## 4. Geometry contract - -### 4.1 Single owner - -The **app shell geometry resolver** is the only module allowed to turn terminal size + zone declarations + overlay mode into region rects. - -| Input | Source | -|---|---| -| Terminal size | Runtime resize events (`columns`, `rows`) | -| Declared chrome zones | Zone registry (§3): fixed or measured heights | -| Optional zone visibility | Session state (goal active, tasks present, orchestrator mode, banners) | -| Overlay mode | Closed · inset (shrink transcript) · full-shell modal | -| Content measures | Prompt wrap height (capped); overlay body measure (list kit / wrap) | - -| Output | Meaning | -|---|---| -| Region rects | `{ x, y, width, height }` for transcript, prompt stack, notice, overlay host, and each active optional strip | -| Transcript residual height | Explicit; not re-derived in leaves | -| Scroll lease region | Which rect currently owns wheel/page keys (focus tree decides *who*; geometry provides *where*) | - -### 4.2 Forbidden in leaves - -Leaf components **must not**: - -- Read `process.stdout.rows` / columns and subtract magic constants. -- Maintain a private “overlay budget” table that other surfaces reuse incorrectly. -- Assume full terminal height for a modal without subtracting layout-owned chrome. -- Call `flexGrow` (or OpenTUI equivalent) in a way that competes with the residual transcript assignment. -- Grow optional chrome without going through the registry and collapse rules. - -### 4.3 Residual transcript equation - -Conceptual (implementation may use measured zone heights rather than a single sum of constants): - -```text -chromeHeight = sum(visible zone heights from registry, after collapse) -overlayHeight = 0 | measured overlay host region -transcriptHeight = terminal.rows - chromeHeight - overlayHeight - -assert transcriptHeight >= idleFloor when overlay closed and only allowed chrome -assert transcriptHeight >= overlayFloor when inset overlay (proposed ≥ 8 on 24-row) -``` - -`idleFloor` = **12** on 24-row terminals. On taller terminals, idle floor remains **12** minimum (do not spend extra rows on chrome by default; extra rows accrue to the transcript). - -**Proposed** on terminals shorter than 24: still attempt floor of `max(6, rows - maxChromeForTiny)` and refuse to open non-essential optional strips; exact tiny-terminal matrix is Bar work. - -### 4.4 Resize - -On every resize: - -1. Re-run the geometry resolver with new `columns` / `rows`. -2. Re-apply collapse rules if the transcript would breach the floor. -3. Re-measure overlay body within the new host rect (lists re-window; keep-active-visible). -4. No ghost lines: previous absolute paint outside the new rects is invalid — host clears/clips. - -Prompt base row count stays stable across resize; only wrap width and internal scroll change. - -### 4.5 Overlay host modes - -| Mode | Transcript | Prompt box | Use | -|---|---|---|---| -| **Closed** | Residual full | Visible | Default session | -| **Inset** | Shrinks; ≥ overlay floor | Visible | Permission, operator question, thin confirms | -| **Full-shell modal** | Hidden or minimal | Hidden or minimal; Esc restores | Model/settings/help-class managers | - -Exactly one primary blocking surface. Opening B while A is open either replaces A or pushes a stack with a single Esc path that always returns to the prompt. No orphan focus, no second geometry path. - -### 4.6 Prompt growth - -- Base bordered prompt: **3** rows (borders + one content line). -- Content may grow with wrap, **capped at 40% of terminal rows**. -- Growth is **internal scroll**, not unbounded chrome. -- Extra prompt rows count against chrome only up to the cap; they still cannot violate the transcript idle floor when no overlay is open — if both cannot be satisfied, prompt stays at base/internal scroll and does not expand further. - -### 4.7 Measurement rules - -- Prefer **measure after layout** (OpenTUI-native) over hand-maintained constant tables. -- Where a fixed height is used for a zone, it must be **fixed-with-test**: a unit or harness check fails if paint height drifts from the declared budget. -- Overlay bodies: measure wrapped text and list windows inside the host width; do not copy another surface’s row constant. -- Scroll units for the transcript must be based on **measured row heights** (or framework-owned scroll measurement), not virtual indices that change height under wrap. - ---- - -## 5. Kill list - -These patterns caused the current failure class. They **must not reappear** on OpenTUI. - -1. **Guessed fixed row tables that drift from paint** — constants that are not the same values the shell actually paints, with no test coupling them. -2. **Parallel geometry hook + paint tree heights** — two systems claiming the same vertical space (manual subtraction *and* flex residual). -3. **Absolute overlays without layout-owned clip** — surfaces positioned outside the geometry resolver’s rects. -4. **N independent scroll hooks without a lease** — each modal/list/log owning wheel and keys with no single focus/scroll owner. -5. **Unbounded stacked chrome with only `max(1, …)` as floor** — optional strips that grow until the log is unusable. -6. **Reusing one overlay budget for another surface** — e.g. settings height derived from permissions constants. -7. **Dual overlay stacks** — separate modal and overlay accounting paths. -8. **Leaf magic constants from terminal size** — components computing their own `rows - K`. -9. **Dense permanent strips** — full goal criteria, full task lists, full agent rosters as always-on multi-row chrome. -10. **Idle progress spacers** — reserving progress rows when nothing is painted. - ---- - -## 6. Acceptance scenarios this constitution enables - -Full corpus and harness mapping live in `docs/plans/tui-layout-scroll-platform.md` (acceptance scenarios section). Layout-critical scenarios that must stay true under this constitution: - -| Scenario | Layout requirement | -|---|---| -| **Starved chrome** | Goal + tasks + agents + active progress on 24 rows → transcript still ≥ floor; expand/collapse works via §3.3 | -| **Permission list** | Host measures list; keep-active-visible; wheel on list lease; close restores prompt rect/focus | -| **Operator question** | Long question + many choices fit host box; no overpaint into the prompt border | -| **Prompt expand** | Multi-line paste; internal scroll; transcript does not vanish | -| **Stream follow** | Transcript residual stable under append; no layout thrash | -| **Resize mid-session** | 80×24 ↔ 120×40; resolver re-runs; no ghost lines; prompt base stable | -| **Settings / help** | Full-shell or host modal; open/close leaves no residual absolute paint | -| **Long log** | Working-set render inside transcript rect; interactive scroll (numeric N elsewhere) | - -Interaction scenarios (queue, palette, copy, subagent observe) depend on this geometry but are specified in the interaction contract and focus design, not here. - ---- - -## 7. Related docs - -| Doc | Role | -|---|---| -| `briefs/tui-rebuild-opentui.md` | Product brief (Amp-class calm, queue-default, palette, chrome rent metaphor) | -| `docs/plans/tui-layout-scroll-platform.md` | Epic plan: architecture, migration hard cutover, full acceptance corpus | -| `docs/tui-interaction-contract.md` | Keys, queue/steer/interrupt, palette chord (sibling constitution) | -| `docs/tui-ink-freeze.md` | What may still patch on Ink before cutover | -| `docs/tui-migration-cutover.md` | Branch hard cutover, platforms, merge bar | -| `docs/PRODUCT.md` | Product UX claims (update when cutover ships; do not treat mid-run Enter semantics there as target) | -| `docs/ARCHITECTURE.md` | System architecture; TUI runner ownership today | - -### Ink paths (reference only — do not grow) - -- `src/tui/chrome-zones.ts` — fixed zone row budgets today -- `src/tui/chrome-geometry.ts` — variable chrome row math today -- `src/tui/hooks/use-layout-geometry.ts` — transcript/overlay resolver today -- `src/tui/components/modal-stack.tsx`, `src/tui/components/overlay-stack.tsx` — dual stacks today - ---- - -## 8. Proposed defaults still open for paint proof - -These are **proposed** in this constitution so Platform can implement without waiting. Spike/harness may tighten numbers; they may not violate locked floors. - -| Item | Proposed default | Rationale | -|---|---|---| -| Transcript idle floor (24-row) | **12** | Locked by plan/brief | -| Transcript inset-overlay floor (24-row) | **8** | Keeps log glanceable under permission/operator | -| Prompt height cap | **40% of terminal rows** | Matches current prompt-layout intent; internal scroll | -| Overlay host max (inset) | **≤ 70% of terminal rows** | Leaves room for the prompt stack | -| Goal / task / agents collapsed | **0–1 row each** | Plan budget; dense → modal | -| Task temporary expand cap | **5 content rows then modal** | Prevents checklist starvation of log | -| Settings notice chrome cap | **≤ 3 rows** | Diagnostics dump belongs in settings | -| Tiny terminal (< 24 rows) min transcript | **≥ 6** with optional strips forced off | Degraded but usable | -| Collapse priority | §3.3 order | Makes “chrome priority when scarce” implementable | - -Numeric long-log working-set size is **not** set here; it belongs with long-log platform work. - ---- - -## 9. Change control - -- Locked floors and kill list change only with an explicit product/plan update, not drive-by PR edits. -- Proposed numbers may be refined when the geometry harness proves paint heights. -- New permanent chrome zones require a registry row, a collapse rule, and an idle 80×24 budget proof before merge. diff --git a/docs/tui-migration-cutover.md b/docs/tui-migration-cutover.md deleted file mode 100644 index 0b2b13675..000000000 --- a/docs/tui-migration-cutover.md +++ /dev/null @@ -1,136 +0,0 @@ -# TUI migration cutover policy - -**Status:** locked -**Source of truth:** `docs/plans/tui-layout-scroll-platform.md` (migration strategy and locked cutover decision) -**Product brief:** `briefs/tui-rebuild-opentui.md` - -This document is the operator-facing cutover policy for moving Corbits Code from Ink to OpenTUI. It does not invent product direction; it restates the locked plan decisions so merge and scrap rules are unambiguous. - ---- - -## 1. Policy statement - -Migration is a **branch hard cutover**. - -- One migration branch carries the full OpenTUI shell, platform, and primary surfaces. -- Main stays on Ink until the whole epic is ready to ship. -- Merge only when the acceptance corpus and Bar gates are green. -- On failure before merge: **scrap the branch**; do not land half. -- **No dual-release.** Do not ship two paint paths (Ink and OpenTUI) as product modes. -- **No dual-entry product flag.** Runtime flags that select Ink vs OpenTUI for end users are not a ship path. -- Remove Ink as part of the same cutover merge — not a later flag flip on main. - ---- - -## 2. Branch lifecycle - -| Phase | What happens | Exit | -|---|---|---| -| **Spike** | OpenTUI on Bun (FFI, install, binding). Spike starts the migration-branch attempt. | Go / no-go written | -| **Constitution** | Lock budget, focus, scroll, palette, queue/steer/interrupt (docs + acceptance scenarios). | Reviewed and locked | -| **Full epic on branch** | Platform (shell, geometry, list kit, focus/scroll lease, harness) + all primary surfaces + Quiet UI after geometry rules exist. | Surfaces complete on branch | -| **Bar** | Size matrix, peer pass, long-session smoke on the branch. | Bar minimum green | -| **Merge or scrap** | Merge only when gates pass; otherwise scrap and stay on Ink. | Ship OpenTUI or remain Ink | - -### Strangler order (on the migration branch only) - -``` -app chrome shell - → transcript + scroll lease + long-log window - → prompt + queue/steer/interrupt - → overlay host (permissions, operator, model) - → palette - → agents strip / goal / task zones - → settings / help / residual - → delete Ink path - → merge only when acceptance corpus green -``` - -Main does not receive partial OpenTUI surfaces. Work lands on the migration branch; cutover is the merge that deletes Ink. - ---- - -## 3. Merge checklist - -Do not merge until every item is true: - -### Acceptance corpus - -All scenarios in the plan acceptance corpus pass on OpenTUI (automated harness where possible; manual for terminal-specific paint). Summary of required classes: - -1. Starved chrome on 24 rows (transcript floor held; expand/collapse works) -2. Permission list (keep-active-visible; wheel scoped; close restores prompt) -3. Operator question (long text + many choices; no overpaint into status) -4. Prompt expand (multi-line paste; transcript does not vanish) -5. Stream follow (auto-follow; pin on scroll up; jump-to-bottom) -6. Queue mid-run (Enter queues; Alt+Enter steers; Ctrl+C interrupts) -7. Palette open → permissions → Esc restores prompt focus -8. Keyboard copy path without mouse drag-select -9. Subagent observe (independent scroll; Esc restores parent lease) -10. Resize mid-session (no ghost lines; prompt row stable) -11. Settings / help open/close without residual absolute paint -12. Long log remains interactive under multi-thousand-line load - -Full scenario text lives in `docs/plans/tui-layout-scroll-platform.md` (acceptance corpus section). - -### Bar gates - -- **Size matrix** green on **macOS** (primary) and **Linux** (required for CI/server users). Run continuously on migration PRs, not only at the end. -- **Peer pass** complete (Amp feel, OpenCode stack notes, Claude Code / pi interaction baselines as comparison — not copy targets). -- **Long-session smoke** clean (ghosting, overpaint, resize under sustained stream). - -### Cutover completeness - -- Ink path removed on the migration branch (same merge as OpenTUI ship). -- No product-facing Ink fallback or dual paint mode remains. -- Packaging/install path for OpenTUI is documented and CI-green on required platforms. - ---- - -## 4. Rollback and failure modes - -| When | Action | Outcome | -|---|---|---| -| **Before merge** | Scrap the migration branch | Main stays Ink; no half-landed OpenTUI | -| **After merge (catastrophic)** | Normal git revert of the merge commit | Return to Ink; do not leave a permanent dual paint path | -| **Never** | Ship “OpenTUI shell with Ink fallback” as a product mode | Forbidden | - -Scrap means: abandon the branch work as the ship vehicle; do not merge incomplete surfaces “behind a flag.” Lessons from the spike and constitution docs may still inform a later attempt; the incomplete paint path does not ship. - ---- - -## 5. Platform gates - -| Priority | Platform | Role at cutover | -|---|---|---| -| **#1** | **macOS** | Must pass; primary development target | -| **#2** | **Linux** | Must pass (CI and server users) | -| — | **Windows** | Non-blocking for v1; best-effort only if free | - -Windows failures do not block merge. macOS or Linux acceptance/Bar failures do. - ---- - -## 6. Explicit non-goals - -- Dual-release Ink + OpenTUI in production -- Dual-entry product flag as the migration vehicle -- Shipping OpenTUI before the whole epic is ready -- Landing half the surfaces on main “to de-risk” -- Treating Windows as a v1 must-pass platform - -Ink policy until cutover: only true P0 daily-use blockers get minimal patches; no new chrome features on Ink. That freeze is separate from this cutover doc; see the plan and constitution. - ---- - -## 7. Related docs - -| Doc | Role | -|---|---| -| `docs/plans/tui-layout-scroll-platform.md` | Plan source of truth (migration strategy, acceptance corpus, locked decisions) | -| `briefs/tui-rebuild-opentui.md` | Product brief (cutover locked with platform priorities) | -| `docs/tui-layout-constitution.md` | Layout constitution (budget, ownership, interaction) — when present | -| `docs/ARCHITECTURE.md` | Current system architecture (Ink TUI today) | -| `docs/PRODUCT.md` | Product UX claims (TUI section) | - -When constitution and cutover disagree with the plan on locked decisions, the plan wins until deliberately re-locked. diff --git a/docs/tui-opentui-packaging.md b/docs/tui-opentui-packaging.md index 615a95976..f5a986043 100644 --- a/docs/tui-opentui-packaging.md +++ b/docs/tui-opentui-packaging.md @@ -1,7 +1,7 @@ # OpenTUI install / CI / packaging plan **Status:** plan (docs only; CI not yet changed) -**Spike verdict:** GO — `docs/plans/opentui-spike-report.md` +**Spike verdict:** GO **Spike packages:** `@opentui/core@0.5.1`, `@opentui/solid@0.5.1` **Runtime:** Bun (`package.json` engines: `bun >= 1.2`) **Platform priority:** macOS #1 · Linux #2 · Windows non-blocking @@ -109,7 +109,7 @@ bun install --frozen-lockfile bun run test:tui # or whatever harness lands with the platform ``` -**Do not** add `@opentui/*` to root `package.json` on **main** until cutover policy allows it (`docs/tui-migration-cutover.md`). On branch `migration/opentui-tui`, root already depends on `@opentui/core@0.5.1`, `@opentui/solid@0.5.1`, `@opentui/keymap@0.5.1`, and `solid-js@1.9.14` (scaffold under `src/tui-opentui/`; not wired to the `corbits` CLI yet). +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`). ### Solid contributor notes @@ -236,10 +236,7 @@ bun run start | Doc | Role | |---|---| -| `docs/plans/opentui-spike-report.md` | Spike GO evidence, install notes, binding hint | -| `docs/plans/tui-layout-scroll-platform.md` | Epic plan; packaging is Renderer milestone | -| `docs/tui-migration-cutover.md` | Branch hard cutover; no dual-release packaging | -| `docs/tui-layout-constitution.md` | Geometry contracts once shell lands | -| `docs/tui-ink-freeze.md` | Ink maintenance-only while OpenTUI builds | +| `docs/TUI.md` | Shipping shell behavior spec (layout, geometry, overlays, prompt) | +| `docs/adr/opentui-binding.md` | ADR: why OpenTUI, core-class vs Solid binding | Spike re-run evidence lives under `tmp/opentui-spike/` (local; not a root dependency). diff --git a/docs/tui-tty-signoff.md b/docs/tui-tty-signoff.md deleted file mode 100644 index 029605901..000000000 --- a/docs/tui-tty-signoff.md +++ /dev/null @@ -1,100 +0,0 @@ -# Real-terminal sign-off — v0.2.90 - -Every acceptance result on the OpenTUI cutover so far comes from the headless -test renderer. That renderer cannot see paint, real modifier reporting, the -system clipboard, or terminal-owned selection, so a whole class of defect is -invisible to it by construction. - -This is not a theoretical gap. During the cutover, every genuine defect was -found by running the app or capturing the pty byte stream, and none were found -by the suite — including a standalone binary that could not start, a clipboard -that had never written to the system clipboard, and a quit key that fired -mid-edit. Two separate false "blocking" verdicts came from trusting documents -instead. - -So this checklist is signed by a human at a real terminal, or it is not signed. - -**Signed by:** ______________________ **Date:** ______________ - -**Build under test:** `bun run build:bin` → `dist/corbits`, run from a -directory with no adjacent `node_modules`. - -**Terminal:** ______________________ **Version:** ______________ - -Run the whole list at **80x24**, then again at **120x40**. A row passes only if -it passes at both sizes. - -## Launch and layout - -| # | Check | 80x24 | 120x40 | -|---|---|---|---| -| 1 | Binary starts from an empty directory | ☐ | ☐ | -| 2 | Landing screen paints; the mark and hint row are intact | ☐ | ☐ | -| 3 | Resize the window mid-session: no overlap, no stuck rows, no horizontal scroll | ☐ | ☐ | -| 4 | Resize while an overlay is open | ☐ | ☐ | - -## Input - -| # | Check | 80x24 | 120x40 | -|---|---|---|---| -| 5 | Ctrl+Enter inserts a newline; the prompt grows and then scrolls at 40vh | ☐ | ☐ | -| 6 | Enter sends; the message is not split at a newline | ☐ | ☐ | -| 7 | Ctrl+D deletes the character under the cursor and never exits | ☐ | ☐ | -| 8 | Ctrl+C interrupts a run; twice quits, and the terminal is restored | ☐ | ☐ | -| 9 | Paste a short API key into onboarding | ☐ | ☐ | -| 10 | Paste a key longer than 1000 characters — it must not truncate | ☐ | ☐ | -| 11 | Paste multi-line text into the prompt: arrives whole, does not send early | ☐ | ☐ | -| 12 | Ctrl+V with an image on the clipboard attaches it | ☐ | ☐ | -| 13 | Typing works immediately on every surface without clicking first | ☐ | ☐ | - -## Selection and copy - -| # | Check | 80x24 | 120x40 | -|---|---|---|---| -| 15 | Drag-select transcript text with the mouse, no modifier | ☐ | ☐ | -| 16 | CMD+C copies the selection; paste it elsewhere to confirm | ☐ | ☐ | -| 17 | Alt+M takes the mouse; click-to-expand works | ☐ | ☐ | -| 18 | Alt+M again returns selection to the terminal | ☐ | ☐ | -| 19 | Alt+C copy mode writes to the system clipboard | ☐ | ☐ | - -## Streaming and transcript - -| # | Check | 80x24 | 120x40 | -|---|---|---|---| -| 20 | Stream a reply containing `####` headings — no literal markers, no shaking | ☐ | ☐ | -| 21 | Stream a reply with a code fence and a table | ☐ | ☐ | -| 22 | Run the same tool 4+ times in one turn: one folded row, no orphan results | ☐ | ☐ | -| 23 | Non-ASCII output (CJK, em dashes, arrows) does not overflow or mis-wrap | ☐ | ☐ | -| 24 | A session past 500 rows still streams smoothly | ☐ | ☐ | - -## Permissions and failure - -| # | Check | 80x24 | 120x40 | -|---|---|---|---| -| 25 | Approval overlay is sized to its content, not the terminal | ☐ | ☐ | -| 26 | The gate content is not printed twice | ☐ | ☐ | -| 27 | The decision is recorded in the transcript after choosing | ☐ | ☐ | -| 28 | Force a crash: the terminal is restored and the process exits | ☐ | ☐ | -| 29 | Resume a session created before the cutover | ☐ | ☐ | - -## Known open at sign-off - -State whether each is still true, so the release notes match reality. - -- **Shift+Enter** does not insert a newline on terminals that do not report the - modifier. Ctrl+Enter and Ctrl+J do. Verified as terminal reporting, not a - decode defect — the kitty path works end to end. -- **Markdown flicker while streaming.** The deterministic cause (a bare `####` - painting as literal text) is fixed and guarded. A milder flicker remains from - the async highlighter repainting raw source before the concealed form lands. -- **CL-5551**, transcript rows are retained for the life of the process. The - 600-block cap died with the deleted Ink stream state and was not ported. - -## Sign-off - -Do not tag the release until every row above is checked at both sizes, or until -an unchecked row is deliberately accepted and named in the release notes. - -Anything found here is worth more than anything found by the suite. If a row -fails, say what you saw rather than what you expected — the failures that cost -the most this cycle were the ones described from intent instead of observation. diff --git a/src/tui-opentui/geometry/zones.ts b/src/tui-opentui/geometry/zones.ts index 21ab1c1e5..66f2f6dda 100644 --- a/src/tui-opentui/geometry/zones.ts +++ b/src/tui-opentui/geometry/zones.ts @@ -1,5 +1,5 @@ // Chrome zone registry for the OpenTUI shell. -// Source of truth: docs/tui-layout-constitution.md §3 zone table + §3.3 collapse order. +// Source of truth: docs/TUI.md "How it should look" (zone table + collapse order). // Pure data — no process.stdout, no paint framework. /** Constitution zone ids (snake_case matches the registry table). */ @@ -139,7 +139,7 @@ export const PROMPT_IDLE_ROWS = PROMPT_IDLE_INPUT_ROWS + PROMPT_BORDER_ROWS; /** * Collapse order when transcript would breach the floor (first cut first). - * Matches docs/tui-layout-constitution.md §3.3. + * Matches docs/TUI.md "How it should look" collapse order. */ export const COLLAPSE_ORDER = [ "command_banner",