fix(chat): align transcript columns and block spacing
Transcript rows and cards on one column and one vertical rhythm (grid.md 5.5 and 5.6, teamleaderleo/leo-workspace#1498), plus the composer foot gap.
Leo's words
On the transcript beside Claude Code desktop (2026-10-29):
"Spacing is so bad here. Like, you need to have a little gap between the composer and the, the, the whatever process, and then the whole, like, ran whatever commands thing, and the gap between that, and then everything else. It's like, oh my God. What? Why is it just nothing's aligned? You look at Claude code, everything's fing aligned. We're not aligned at all. We're fing nonsense. We're fing wack as f. Everything is just, oh my God. You know, all these things, like line up. It gets lined up, these things. It's a proper f***ing, like, grid and stuff."
On the working indicator (2026-10-29):
"i think there's this good centering going on here where the thinking is halfway between the user prompt and the composer... though the gap is a bit big in claude code"
On the gap between the composer box and the mode row under it:
"we still have too much of a gap here in our composer"
"wait it's the same"
"hm i still feel like we could go lower tho"
What changes
- One column. Every block's first glyph or 16 icon box sits on
col-left, its words oncol-left + 24(base's meta-row icon column,--cv-row-icon/--cv-tool-text-inset, now on the grid).--cv-insetis on the 4 grid, minimum 16. Disclosure carets stay inline right after their words, as #28018 settled ("we can fit stuff closer"); the rhythm test checks each sits at most one grid gap after its label. - One gap per adjacent pair, most specific first (
transcriptGrid.tstranscriptGap/tailGap, used by both the estimate and the measured placement): waiting rule, then composer, then turn (32 both sides of a user message), then block (24). Gaps never add; no row owns an outer margin. The placed gaps take the Interface Size (--cmux-interface-scale) as the CSS tokens do, read once per pass; a kept placement is reused only at its own scale. - Status rows between two replies (base's
statusEdge, Leo 2026-10-30: "see how claude code has those centered tool calls") stay centred: the block gap on both sides does it, so--cv-status-gap*andSTATUS_GAP*are 0. - Waiting indicator centred. While a turn has no output, the indicator takes the block gap (24) above and below it, in place of the 16 composer gap. Once output arrives the normal gaps apply.
- Composer gap after a message is 24, not 16, so the message's hover action row (glyph tops 4 under the message, no layout height) keeps at least 4 below. The last turn footer's action row uses the same block-gap placement.
- Composer foot gap is
var(--grid-gap-foot)(4, was 8). The mode row's leading mark stays on the + glyph's column (22). - Counts ("Ran 2 commands"): a count summary reads whole (base's
whole), so one ordinary space parts verb and count; tabular figures on the tool row text. A single run's summary keeps its verb out of the column's width (tight). - Cards: header icon 12, text 36, body on text, trailing control on the column's right; the opened agent message's body and the line under it on the text column.
Measured (Chromium, gallery scenes, light and dark, 480 and 1000 px)
Before = base (feat-cmux-next at c033155 plus #27539's tokens) with the same synthetic fixtures; after = this head. Values at 1000 px light; 480 px and dark match within 0.5 except where noted.
| Line | Target | Before px | After px |
|---|---|---|---|
| Turn gap (prompt -> next block) | 32 | 38 | 32 |
| Block gap: header -> message | 24 | 10 | 24 |
| Block gap: message -> "Ran 2 commands" | 24 | 38 | 24 |
| Block gap: "Ran 2 commands" -> live line | 24 | 4 | 24 |
| Paragraph (inner) gap | 12 | 14 | 12 |
| Live line -> composer | 16 | 0 | 16 |
| Waiting indicator: prompt -> indicator | 24 | 36 | 24 |
| Waiting indicator: indicator -> composer | 24 | 0.5 (-0.25 at 480) | 24.5 |
| Last message -> composer (settled) | 24 | n/a (scene added here) | 23.75 |
| Action glyph top under its message | 4 | 6 (center 16) | 4 |
| Room under the action row before the composer | >= 4 | n/a | 3.75 |
| Composer box -> quiet (mode) row | 4 (--grid-gap-foot) | 8 | 4 |
| Mode mark center from box left | 22 | 22 | 22 |
| Count summary verb -> count | one space | verb-column width | one space, tabular figures |
| Every block's first ink from col-left | 0 | 0 | 0 |
Captures
Gallery scenes, synthetic fixtures, Chromium. Before = base with the same fixtures; after = this branch at 1691838 (the last commit after it only changes how a kept placement survives an Interface Size change).
| Scene | Before | After |
|---|---|---|
| grid-rhythm, light, 1000 | ![]() | ![]() |
| grid-rhythm, dark, 480 | ![]() | ![]() |
| grid-waiting, light, 1000 | ![]() | ![]() |
| grid-waiting, dark, 480 | ![]() | ![]() |
| grid-settled, light, 1000 | n/a (scene added here) | ![]() |
| grid-rhythm, light, 1000, 1.25x Interface Size | n/a | ![]() |
Composer (box to the mode row under it, --grid-gap-foot 8 to 4):
| Before | After | |
|---|---|---|
| light, 480 | ![]() | ![]() |
| dark, 480 | ![]() | ![]() |
The captures are full-pane 1x gallery frames (the rhythm test's own screenshots), not 2x element crops.
Gallery: agent-pane.transcript/grid-rhythm, agent-pane.transcript/grid-waiting, agent-pane.transcript/grid-settled, agent-pane.transcript-grid-cards/grid, agent-pane.composer-row/claude-auto
Tests
Focused checks at the exact head 95aace1657478d0a2f40f488b26df06c485cc465, all green: https://github.com/manaflow-ai/cmux/actions/runs/41316593307.
- typecheck 0.
- Units green: continuationSpacing (Interface Size change, and estimated grid rows at 125%), rowEstimates, rowLayoutLinearity, queue-agent-message, transcript-copy, transcript-codex-turn, transcript-row-full-width.
- Browser green: transcript-rhythm (with a 1.25x case and the inline-caret check), agent-pane-tool-group-alignment, transcript-glyph-alignment-browser, composer-claude-split, transcript-hover-fade, message-actions-menu-reveal, transcript-meta-columns-browser, agent-pane-transcript-chevron-column, agent-pane-tool-group-scroll, agent-pane-composer-rest-gap, agent-pane-transcript-spacing, transcript-row-pitch-browser.
grid-lines.test.ts -t transcriptgreen.
Merged feat-cmux-next in four times, each when GitHub reported a conflict (merges, not rebases). Base work on the same rows is kept:
- status rows centred between replies
- the DisclosureRow shell and inline carets (#28018)
- tray-hold dock room and whole count labels
- #30297's 56 rest gap over a floating composer, measured from the newest line (restBand first when a footer follows)
- #29965's one-line row pitch: one grid row, no frame of its own
- #29860's footer centring: a footer over a prompt sits in the middle of the turn gap, and the spacing test keeps its centring check
- #28629's message indent: message cards keep the grid's text column, with its tokens defined in grid terms
One base test was changed: test/agent-pane-transcript-spacing.test.ts (from #30297). It expected a 24px meta row 4px under the bubble. design/grid.md has a message's action row laid in the gap with no layout height and 28 controls ("whose 28 button therefore starts 6 left of the line"; "A message's hover action row ... takes no layout height: it is laid in the gap under its message"). The 28 comes from this PR's own grid commit (dc8161b990), not from the merge.
Row estimates follow the Interface Size: the per-pass scale multiplies the action and tool rows, the bubble padding and sides, the page edge, and the block, list and tool-stack gaps.
Review: Codex gpt-6-astra (high, fast) via review-router, SHIP at 95aace1657478d0a2f40f488b26df06c485cc465 (merge resolution; full PR reviewed at d36abd6 and 36d88ad). It followed BLOCKs at e0c3d9b, 18cf6d8, 1691838, b1de39a, 5d643f0, 84dc1b7, 66ddd56 and 45df014, each fixed in code (the receipt lists them). Follow-ups filed: #30838, #30839, #30938.
Risk: medium. Transcript virtual placement now reads per-row marks for its gaps; the estimate and the measured pass share one function, and the rhythm browser test checks the DOM against both.
Changelog
none
Viewed after image: yes
Agent session: agent session
š¤ Generated with Claude Code













