Agent chat: a run of messages to agents is one group, "Messaged <target> 3 times"
Leo, 2026-10-29, on Claude desktop's transcript: "this would be decent ui for cmux-next... tho i dont mind what we have now. i just think we can do a little bit better"
Gallery: agent-pane.message-groups/running, agent-pane.message-groups/done, agent-pane.message-groups/done-open, agent-pane.message-groups/mixed, agent-pane.message-groups/before-running, agent-pane.message-groups/before-open, agent-pane.message-groups/look-guide-running, agent-pane.message-groups/look-guide-open, agent-pane.message-groups/look-list-running, agent-pane.message-groups/look-list-open
What changes
Two or more messages to other agents in a row (Claude Code's SendMessage, a shell acpmux send, a cmux-subsessions send_message, in any mix) now draw as one group (toolGroups.ts messages, MessageGroupRow.tsx):
- One line that sums them up. While a message goes out: the present tense, its target and the time since the first one left ("Messaging Model picker spruce-up 11s"). Once all went: the past tense with a count ("Messaged Model picker spruce-up 3 times", or "Messaged 3 agents" when the targets differ). The verb sits in the turn's verb column (#27386); the target takes the text colour at the row's weight, the rest stays muted.
- One quiet bordered card, a row per message (each message's own existing row: verb, target, first line, caret), split by hairlines in the transcript's card-ring colour.
- A row opens in place to the whole message as Markdown, not JSON (unchanged). A shell
acpmux send/send_messagerow now has the same hover Copy a SendMessage row already had, so every sent message has exactly one Copy. - The group starts open while a message goes out and closed once all went; the reader's choice persists across the virtualized transcript. Find in Chat opens it.
A lone message still draws as its own row. Other work between two messages splits them. A message that failed or never said it went out keeps its own row with its failure, outside any group, so a closed group never reads "Messaged ..." over a hidden failure. With the Codex-style runs on (the default), the message in flight leaves its run for the live line, which now says "Sending message to " for a Claude SendMessage too (it did for acpmux send and send_message); the running group is what shows with those runs off. Coordination cards stay in sight in a folded run, as the messages did.
Kept: our colours, glass, type scale, and the spinner as the working mark. Not touched: the composer, the rail, the model picker.
Variants (gallery, theme.messageGroupLook)
card(picked, the default): the bordered card with hairlines, as the reference.guide: the rows on the tree guide an edit group's rows hang from, no card.list: the hairlines with no ring, flush under the line.off: no group, each message its own row: the "before".
card wins: it reads as one unit at a glance in a long transcript, and its hairlines separate rows whose first lines are similar; guide reads as nested edits and list loses the group's edge when the rows above are also muted.
Home Channels draws these rows too; its pane catalog (gen-strings.mjs home-channels-pane) carries the new messages.group.* keys.
Timer, observers, per-token cost
- The elapsed counter is
useNow(running)(SubagentGroup.tsx): one 1 s interval while a message in the group goes out and the pane is visible. Stop condition: the last message went (runningfalse), the pane hid (visibilitychange), or the row unmounted. Cleanup:clearIntervalplus removing thevisibilitychangelistener in the effect cleanup.MessageGroupRow.test.tsxasserts the interval is live while sending and gone once the last message went. - No new observers or animation loops. Expand and collapse are instant on the click (
useAnchoredCollapsewithanimates: () => false, the header still anchored). - No new O(rows) work per token: grouping is one pass over the run's own entries inside the existing per-row
toolGroupsmemo; the tick re-renders only the group's line (its rows are the same elements, so React skips them).
UI review checklist (design/ui-review-checklist.md, skills/aiforui)
Applied:
- Motion gate: a group toggle is seen tens of times a day, so it does not animate; open and close take effect on the click, with no fade-in.
- Subtraction: the word repeated on every row ("Sent to", the envelope) stays on the rows as their verb; the head names the count once. No narration labels; no second Copy or Open control.
- Stable layout: the timer uses
tabular-nums; the gap before it is one token (--ds-space-2); the chevron stays on the row's trailing column; the target truncates before it. - Spacing and surfaces: padding, gaps and radius from tokens (
--ds-space-1/2/3,--ds-radius-card); one hairline ring, no card inside a card, no new fill. - Type: one weight in the row kind; the target is the text colour, not bold.
- Every state: running, done (same target), done (mixed targets), opened row, queued rows keep their delivery word inside the group, failed ones stay outside it, both themes in the gallery.
- Agent working mark: the existing spinner; no other indicator restyled.
Needs a human: VoiceOver reading of the group line; a real trackpad hover on Copy.
Prior art checked
Open: #26591 (show every item when expanding a tool group), #27611 (plain "Ran 2 commands", regular sub-session rows), #27602 (waiting messages from agents, received side), #26629 (fold completed dialogue), #26696 (tool output inline), #27074 (dim background activity). None groups sent agent messages; this PR touches none of their lines beyond TurnRows.tsx's groupNode switch. Merged: #24075 / #24443 (tool calls consolidate; coordination cards kept out of groups), #23738 (sent message row), #24291 (SendMessage names its subagent), #27386 (verb column).
Tests
toolGroups.test.ts: consecutive messages of every kind form one group; a lone one does not; other work, a sub-session start and a failed send split runs.messageLiveLine.test.ts: with the Codex-style runs on, a SendMessage in flight leaves its run and the live line names its recipient.MessageGroupRow.test.tsx: the line in each state; a click opens the card in the same commit and closes it; the clock runs only while a message goes out; theofflook draws the old rows.
Agent session: agent session
š¤ Generated with Claude Code