Guides
The terminal theme
The transcript drawn the way the CLI draws it - a character grid, no boxes, diffs as full-width bands - and the three knobs an embedder gets.
SessionPanel draws a turn one of two ways. cards is the chat convention: bubbles, bordered
tool cards, generous gaps - right for a wide dashboard where the transcript is the page.
terminal is the other one, and it is what the Claude Code CLI actually looks like.
<SessionPanel
client={client}
sessionId={sessionId}
transcriptVariant='terminal'
/>
That is the whole opt-in. Everything below is optional.
What it is
Not a restyled card. terminal is a separate renderer (components/terminal/) that the panel
mounts instead of the card components, so nothing in the card path has a terminal branch in it.
Two rules hold the rendering up:
- Horizontal measures are
ch. Onechin a monospace face is exactly one cell, so a gutter is2chand an indent level is2ch, and text lands on a column rather than near one. A wrapped line hangs under its own first character because the body is its own grid column - not because a padding and a negative text-indent were tuned against each other. - Vertical measures are whole multiples of one line. The space between blocks is a blank line, the way it is in a terminal. Nothing has margins, so no two blocks can disagree about the rhythm.
Everything is built from three primitives - a row (gutter cell + body cell), a blank line, and a
full-bleed band - and the palette is the terminal’s own (--term-*), not the app’s. Re-theme the
surrounding application and the transcript still looks like the CLI, which is the point.
Markdown goes through the renderer’s own component map, and a diff renders protocol’s FilePatch
with the engine’s line numbers: this code has never read the file, so a number it computed
would look authoritative and point at the wrong line.
The character cell
terminalMetrics sets the cell, in whole pixels:
<SessionPanel
transcriptVariant='terminal'
terminalMetrics={{ fontSize: 13, lineHeight: 18 }} // the CLI's own, and the default
/>
Whole pixels is not a nicety. A line height of 1.5 × 13px is 19.5px, and every second row of a
long transcript then renders on a half-pixel: the text visibly softens and the diff bands show a
seam along their edge. TerminalSurface rounds what it is handed for exactly this reason.
One prop rather than two per surface, because the panel mounts three terminal surfaces - the transcript, the pending approval/question prompts, and the composer - each in a different part of its flex column. Hand two of them different numbers and the caret lands on a different column from the text above it, which is the one failure this theme exists to prevent.
The VS Code extension follows editor.fontSize / editor.lineHeight here, so the panel, the
editor and the integrated terminal all draw at one size.
Affordances
A terminal cannot highlight the row under your cursor and cannot put a copy button on a block of output. A web view can do both for free, and they are genuinely useful - so both are on by default:
<SessionPanel transcriptVariant='terminal' affordances={false} /> {/* the pure article */}
<SessionPanel transcriptVariant='terminal' affordances={{ hover: true, actions: false }} />
The rule that keeps this honest is that each one costs no layout. The hover fill is a
background; an action button is an overlay one line tall, absolutely positioned so it displaces
nothing. Turn them all off and every glyph is on exactly the same cell it was on - which is what
makes false a real option rather than a degraded mode, and why a new affordance may not be added
as anything that occupies space.
What it changes elsewhere in the panel
- The composer becomes the CLI’s prompt:
>in the gutter cell every transcript row’s marker sits in, so what you type starts on the column what you are reading starts on. Docked flush to the panel edges - no radius, no shadow - with one rule along the top that turns accent while anything inside has focus, and+/↵characters in place of the round pills. - Approvals and questions are keyboard-first rows rather than dialogs: one question at a time
behind a chip strip, ending in a review step, answerable entirely from
↑↓, the digits andesc. No boxes means the affordance has to be carried by the keyboard, which is what a terminal does anyway. transcriptFontstops meaning anything. A terminal has one line height and is monospace by construction. It is inert rather than broken underterminal- a host that offers it as a setting should say so, or hide it. (The dashboard hides it; the VS Code extension documents it as Cards-only.)
Where it is already on
The VS Code extension’s dock defaults to it (workerdeck.transcriptVariant), the dashboard offers
it as Agent view style → Terminal in Settings, and the reference embedding
(apps/embedded) runs it in a 26rem rail - the densest thing there is.
The iOS app has it too, as Settings → Style → Terminal, but it shares no code with the above: it is a native Swift renderer over a Swift port of the same rules (the two folds, the cell/wrap model, the strings that are the heights). One thing is deliberately inverted there and worth knowing if you are porting this yourself - on the web the browser wraps and the calculator predicts how many lines that will be, while on iOS the planner wraps and the renderer draws the lines it returned, so a row’s height is exact by construction rather than by a prediction that is 99% right. If you own the renderer, that is the better trade, and it costs nothing: the wrap was computed to measure the row anyway.