fix(chat): align transcript columns and block spacing

Merged
TE
4d ago
+1219-356

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 on col-left + 24 (base's meta-row icon column, --cv-row-icon / --cv-tool-text-inset, now on the grid). --cv-inset is 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.ts transcriptGap/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* and STATUS_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.

LineTargetBefore pxAfter px
Turn gap (prompt -> next block)323832
Block gap: header -> message241024
Block gap: message -> "Ran 2 commands"243824
Block gap: "Ran 2 commands" -> live line24424
Paragraph (inner) gap121412
Live line -> composer16016
Waiting indicator: prompt -> indicator243624
Waiting indicator: indicator -> composer240.5 (-0.25 at 480)24.5
Last message -> composer (settled)24n/a (scene added here)23.75
Action glyph top under its message46 (center 16)4
Room under the action row before the composer>= 4n/a3.75
Composer box -> quiet (mode) row4 (--grid-gap-foot)84
Mode mark center from box left222222
Count summary verb -> countone spaceverb-column widthone space, tabular figures
Every block's first ink from col-left000

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).

SceneBeforeAfter
grid-rhythm, light, 1000beforeafter
grid-rhythm, dark, 480beforeafter
grid-waiting, light, 1000beforeafter
grid-waiting, dark, 480beforeafter
grid-settled, light, 1000n/a (scene added here)after
grid-rhythm, light, 1000, 1.25x Interface Sizen/aafter

Composer (box to the mode row under it, --grid-gap-foot 8 to 4):

BeforeAfter
light, 480composer before lightcomposer after light
dark, 480composer before darkcomposer after dark

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 transcript green.

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