Design systemDraft
Tool row
Every tool call in a transcript is one quiet line: an icon, a past-tense verb, a mono summary and what it cost. A run of calls folds into one row that opens to them. Drawn on "Session, phone and desktop", "Session · the composer, like Claude Code" and "Plan approval and modes".
| Surface | Implementing file | Status |
|---|---|---|
| Deck | deck/chat/core/grouping.js, deck/chat/core/tool-detail.js, deck/chat/chat.css (work/chat) | partial |
| App | apps/app/src/session/Rows.tsx RunRow (work/mobile) | partial |
| Lumen | local/capsule/native/Sources/UI/CapsuleView.swift ToolRows (work/capsule-pro) | partial |
Anatomy
A flex row, gap 8, no border, no fill, --text-2:
- Icon, 16, from the stroke set: file (Read, Edited, Wrote), terminal (Ran, Shell), search
(Searched), globe (Fetched), agents (sub-agent), chev-r when the row opens to detail. Running:
the spinner (14, 1.5 ring
--rule-strong, arc--text-2) replaces the icon. - Verb, base size: Read, Edited, Wrote, Ran, Searched, Fetched, Running, Editing.
- Summary, JetBrains Mono 12/16,
--text-2, one line, ellipsis in the middle for paths. - Right meta, pushed right, meta size:
+12 −4(--text-2), "0:18" elapsed while running (--text-2), "9 s" or "moved to background" (--label), "you · 0.2 s" for a!line.
Folded run. Consecutive calls fold: chev-r, "Read 6 files, searched 2 patterns", right meta the total time. Running: "Running npm run build · 0:42". A run of one stays a plain row. A plan, a todo list and a Handoff row (below) never fold, alone or into a run beside ordinary tool calls: each always renders as its own line, in the flow, never absorbed into a summary.
Expanded detail. Opening a row shows, indented to the summary, a block on --code-bg with a
1 px --rule top border: the full command and output (terminal type), or the diff (see diff), or
key and value pairs for other tools. A ! shell row always shows its output, capped at 12 lines
with "Show all".
Variants
- Plain (one call), folded run, running, failed, opened.
- Shell line you ran with
!: verb "Shell", meta "you · 0.2 s", then one line under the block: "Ran in ~/work/harlow-legal. kit sees the output on its next turn." - Handoff (a session calling
team_askor@role, ADR 0031/teammates): the teammate's own avatar tile (avatar.md), never the generic sub-agent icon, since this row is naming a specific teammate, not an anonymous sub-agent call. Summary line, left to right: verb "Asked" while it waits, "Replied" once it lands, the teammate's role name in--text600 (not mono, it's a name: "design"), then a "Teammate" tag (chip.md, avatar.md's tile-is-never-bare rule), then the rest of the line in--text-2: "Asked designTeammateto make the intake form calmer." Always its own row (see Folded run, above: never absorbed into a run summary), always present in the flow; "collapsed" here only ever means the reply detail is shut, the row itself is never hidden. Opening it shows the reply. The one difference from every other row's detail: it's not code or a diff, so it does not sit on--code-bgin mono, it renders as plain turn prose (turn.md's Prose spec, read size,--text), because a teammate's reply is words, not a tool's output. No colour change to the row, the tile or the detail on account of which teammate answered (avatar.md, "no colour per agent"): distinct-without-colour is the tile, the name and the tag, together, every time. Right meta while waiting: nothing (no elapsed timer; a teammate's own pace is its business, not a thing to watch tick up). Failed (refused, timed out): crossed circle, meta "no answer", same as any other failed row.
Sizes
| Desktop | Phone | |
|---|---|---|
| Row height | 28 (--control-xs) | 32 (--control-sm), hit area 44 |
| Verb | base 13/18 | base 13/18 |
| Summary | mono 12/16 | mono 12/16 |
| Detail block | mono 12/18, padding 8 12 | same, scrolls sideways inside |
States
- Default.
--text-2, no fill. - Hover (desktop, rows that open). Fill
--hover, radius--radius-field(8). - Focus. 2 px
--focusoutline, offset 2. - Running. Spinner, "Running" or "Editing", bone is never used for the text; the elapsed time counts up once a second so quiet work never looks stalled.
- Failed. Crossed circle icon in
--text, verb stays, meta "failed · exit 1". Never violet, never a border. - Canceled (turn stopped). Meta "stopped", icon unchanged,
--label. - Opened. Chevron rotated 90; detail below.
- Background. Right meta "moved to background"; the task continues in the composer's background pill.
Keyboard and touch
- A row that opens is a
button: Enter or Space toggles. Arrow keys do not move between rows. - Deep links (
?tool=<id>) open the row and scroll it into view. - Phone: tap anywhere on the row; long press copies the summary.
Motion
Open and close with a --motion-panel (220) height reveal (grid rows 0fr to 1fr), chevron at
--motion-tap (120). The spinner stops under reduced motion and shows a still ring.
Copy
Verbs past tense when done, present participle while running. Counts in words: "Edited 3 files, ran 2 commands". Never tool names from the SDK (no "Bash", "TodoWrite"), never capitals.
Accessibility
- Row:
buttonwitharia-expandedwhen it opens, else a plainlistitem. - Label: verb plus the full summary ("Ran npm test, 18 seconds").
- The spinner has
aria-hidden; the row carries "running" in its label. - Summary
--text-2on--bgpasses AA at 12.
Gaps
Deck (work/chat)
-
.cv-toolis a bordered card (38 high,--panel, radius 8); spec: a borderless 28 line. - Failed uses
--beacon-inkand a beacon border; spec: crossed circle in--text. - Running state word is lowercase in the signal colour; spec: spinner plus
--text-2verb. - Meta is 11 px; spec: 12.
App (work/mobile)
- No icon, no elapsed timer, no detail; "Hide"/"Show" text instead of a chevron.
- Status mark dot instead of the icon.
Lumen (work/capsule-pro)
- SF Symbols instead of the stroke set; no folding, no detail.