TUI Transcript Rendering and Interaction#
This document is the design of the Ink TUI's transcript display and interaction: how tool calls render, how output folds and expands, how commands and keybindings are declared, and how mid-run prompts are presented. Companion: user-input-requests.md (mid-run questions; their TUI surface is specified here).
1. Goal#
The transcript should show tool calls that collapse without losing information, offer a ctrl+o expanded transcript view, render diffs structurally, queue messages while the agent is busy, and expose keybindings that a user can discover and override.
2. Starting point#
The TUI base provides a vendored hermes-ink cell-grid renderer with mouse
tracking and ScrollBox, four themes with live preview, a BottomBar showing
tokens / context% / cache / permission mode, a command palette (ctrl+k),
fish-style autosuggest, @file completion, per-session drafts, and the
account and channel flows. The gaps this design closes are concentrated in
the transcript itself:
- Tool output folds hard at 6 lines (
Turn.tsxMAX_LINES) with no way to expand it — no keybind, no verbose mode. - Tool args are one truncated line; there is no per-tool rendering, so every tool looks the same.
- There is no diff rendering;
/diffprints rawgit difftext. - Streaming text shows raw markdown source and jumps visually when the final
render lands (
Turn.tsx:123);renderMarkdownis un-memoized under a full-redraw renderer. follow_up_questionandapproval_requestenvelopes are typed inws/client.tsbut dropped without notice, so agent questions time out and theaskpermission mode is unreachable (shift+tab only cycles bypass↔auto)./resumerebuilds the transcript from role and content only, losing tool history (useWsEvents.ts:326-336).- Tool results are matched to calls by tool name, because server stream events carry no call id, so concurrent same-name calls are mis-attributed.
- Busy means input is locked (
submitTextreturns early); there is no message queueing. - The
ui/kit (ModalProvider / Confirm / Form / MultiSelect / Toast) is built but used only by the--demoscreen; the REPL runs a 24-statepickerKindenum.
3. Transcript rendering#
Information density follows Claude Code's approach (file pointers into
references/claude-code-leaked/src are in the references document).
- Tool renderer interface. Each tool gets render hooks (use-line,
progress, result, error) sharing one shell: a status dot (
⏺dim when queued, blinking while running, green on completion, red on error), the bold tool name, a parenthesized arg summary, and the result indented under a⎿gutter. Two glyphs carry all tool state; there are no boxes. - Truncation at 3 lines with
… +N lines (ctrl+o to expand), plus two refinements: if only one line is hidden, show it instead; pre-truncate huge outputs by character count and estimate the remaining line count. - Quantified one-line summaries per tool rather than ellipsis cuts:
Read 52 lines,Added 5 lines, removed 2 linesfollowed by a diff,Found 8 files, andDone (12 calls · 48k tokens · 2m 10s)for sub-runs. - ctrl+o opens a frozen-snapshot transcript screen — a separate Screen state rather than in-place expansion. It freezes the message list, re-renders everything expanded, and shows exit hints in the footer; ctrl+e inside it shows everything with no truncation at all.
- A composite spinner line:
✻ verb… (esc to interrupt · 42s · ↓ 3.2k tokens), with progressive width gating. The spinner and the token stats already exist separately; this merges them. - Queued messages while busy: typing during a run queues the message (dim, above the input), ↑ recalls it for editing, and the queue flushes between turns.
Diff rendering is written in-house: line numbers, add/remove coloring, three
context lines, used by edit-style tool results and by /diff. Markdown
rendering goes through marked-terminal and is memoized per turn, which
matters under a renderer that redraws every frame.
The renderer itself is not replaced. The vendored hermes-ink already has mouse tracking and ScrollBox, so switching to OpenTUI buys nothing here.
4. Command and interaction architecture#
Interaction structure follows opencode (pointers into
references/opencode/packages/opencode/src are in the references document).
- One command registry as the single source of truth. Each command is
declared once (name, title, category, keybind, slash-name, enabled), and
that declaration drives keybindings, the ctrl+k palette, slash commands,
and the live key hints in footers. A single table is what keeps registry
and handlers from drifting apart —
/branchimplemented but unlisted,/memorylisted but stubbed. - A keybind definitions table with user overrides. Defaults and
descriptions are declared once and generate the config schema, which fits
the existing schema-driven settings design. Unknown keys are an error;
"none"disables a binding. - Question and permission prompts replace the input box through a
three-way slot (Prompt | QuestionPrompt | ApprovalPrompt) instead of a
modal. The transcript stays visible and scrollable and esc keeps a single
clear meaning. This is the TUI surface of user-input-requests.md, handling
follow_up_questionandapproval_requestand making theaskpermission mode reachable. - In-row danger confirmation for destructive picker actions: pressing the key again confirms and the row turns red, instead of a nested confirm layer.
REPL pickers move from the pickerKind enum onto the ModalProvider/Form kit;
the change is mechanical and can be done one picker at a time.
5. Server support#
Two pieces of the design need data the server does not currently send:
- Tool stream events carry a
call_id, so the TUI matches results by id rather than by name and concurrent same-name calls are attributed correctly. conversation_loadedcarries tool blocks, so/resumerestores tool history.
Both touch _event_parsing.py and dispatcher event emission, which is shared
with the event-bus work.
6. Constraints#
- The cell-grid renderer redraws everything per frame, so the heavier per-turn rendering above depends on memoizing markdown and diff output.
- ctrl+o is safe as a global key: terminal flow control uses ctrl+s and ctrl+q.
Appendix: Implementation Status#
Research is complete; implementation has not started. The intended landing order is:
- P0 — transcript density (pure TUI, no server changes): tool render
shell, 3-line truncation, the ctrl+o transcript screen, the diff
component, memoized
renderMarkdown. Accepted when a run with mixed tools reads as two-line entries, ctrl+o shows everything, an edit shows a colored diff, and long bash output folds with an accurate +N count. - P1 — interaction: queued messages with ↑ editing, the composite
spinner/status line, command registry unification, the keybind definitions
table with
~/.openprogramoverrides, and?shortcut help generated from the same table. - P2 — server-dependent items:
call_idin tool stream events, tool blocks inconversation_loaded, question/approval prompts in the input slot, and the REPL picker migration.