agent pane: rest the newest message 56px above the composer, with no shift on load

Draft
TE
5d ago
+537-13

Leo (2026-11-01): "in the next build, will we have more space between composer and latest messages? i'd like it... tho maybe not as much as claude code / idk why theirs is so big"

And: "there's currently some oddities where the gap between recent text and composer is bigger than the desired gap (which is rn like barely anything) i hope we reconcile that in the future so there's no shift on load and it's just a load"

Design spec: leo-workspace design/chat-transcript.md (new section "Rest gap above the composer"; this replaces the grid's composer gap token in design/grid.md).

What changes

The space under the chat's newest row (a turn footer's action row, or the running turn's live line) up to the top of the floating bottom stack (the running tray, else the composer card) is now REST_GAP = 56 px. Before, it was 16 px, which was only the last row's own turn gap.

  • model.ts: REST_GAP and restRoom(), which is the room added under the last row on top of the turn gap that row already keeps. A data-rest-gap document attribute overrides the value, but only the gallery sets it.
  • App.tsx: the transcript's room under the last row (dockRoom) is now the stack's overlap plus restRoom(). The follow, held-room, send and lift paths use it. The edge masks, Jump to latest and scroll-into-view still use the stack height alone. The composer, the tray, the gaps between messages and the status rows are unchanged.
  • How 56 was picked: Codex leaves about 2.4 body lines under its action row and Claude Code about 4.3. Leo found Claude Code's gap too big. With our 22.75 px line, Codex works out to about 55 px and Claude Code to about 98 px.

Gallery

agent-pane.composer-rest-gap shows 40, 56 and 72 px at three widths, and each variant's play asserts its own gap. 56 ships. 40 is still tighter than Codex. 72 heads toward Claude Code's size, which Leo said was too much.

Geometry (Chromium and WebKit, same harness, webviews/test/agent-pane-composer-rest-gap.test.ts)

The gap is measured from the newest row's line (its box bottom less its 16 px turn gap) to the first drawn part of the bottom stack.

statebefore (base 86c7515e08 + test)after (fa182687)
rest, chat loaded16 px56 px
while a reply streams16 px; on wrap frames the line went under the stack, down to -24 px56 px; never outside (0, 56]
tray shown (running turn with a subagent)16 px56 px
composer grown two lines15.5 px55.5 px
shift frames on load (gap differs from settled on any painted frame, first paint to settled)0 (plain chat), 0 (running chat with tray)0, 0

Before run (test red in all 6 cases): https://github.com/manaflow-ai/cmux/actions/runs/41290739350. After run (rest-gap 6/6 green): https://github.com/manaflow-ai/cmux/actions/runs/41290760074. The same after run also covers the transcript-spacing, stream-jitter, soft-edges, bottom-stack and queue-scroll-bottom suites. Its only failure is the reply only moves up as a tool call, the working line and the queue card come and go (chromium and webkit), and that same test fails on base too.

Notes

  • Load shift: neither fixture reproduced Leo's load shift on base (0 shift frames). The new test still guards every painted frame from first paint until settled, so a regression would show up there. Follow-up filed to get a repro of the case he saw.
  • Streaming: while a long line wraps, the existing follow glide eases the line up across a few frames, so on those frames the gap dips below 56. Because of that, the streaming check is "the line stays inside the gap and never goes under the stack" instead of an exact 56 on every frame. On base the line went under the stack on those frames. Follow-up filed to hold the gap exactly during the glide.
  • Performance: no new observer, timer or animation loop. restRoom() is read once per transcript mount (useState initializer). No per-token work was added: dockRoom is plain arithmetic on the existing dockSpace measurement.
  • Risk: the space at the bottom of every agent chat gets larger. That is undoable: one constant.

Agent session: agent session