Agent pane: another agent's "sender -> receiver:" message is its card, not the person's bubble
Leo: "this needs to be rendered better... it is now right"
Leo-ask: 51cd858ac0b0e899 (relayed by claude-sr2, render agent messages)
What was wrong
In claude-sr2's chat, summary-panel's report showed up as a plain grey user bubble, so it looked as if Leo had typed it. The raw summary-panel -> claude-sr2: prefix was still in the text.
The real record, from claude-sr2's acpmux event log (2026-10-22 04:22Z), is a user_message:
{"client": "acpmux-peer#01a161", "steer": true, "promptId": "…", "turnId": "…",
"text": "summary-panel -> claude-sr2: Confirmed. On feat-cmux-next today, …"}
It has no _meta.provenance and no fromAgent. When acpmux send is relayed from another machine (acpmux-peer#…) or run at a shell (acpmux-cli#…), it records nothing about the sender. The only provenance acpmux sets for another session is session_message, and only when the sending connection proves it runs inside a session on this daemon. incomingPrompt already recognised [name] … and From name: … on that path, but not the arrow form that agents use.
Wire formats checked
| How it arrives | What the pane gets | Read by |
|---|---|---|
cmux agent message | agent_message provenance and the rendered [cmux agent message] from … envelope | already handled (renderedBody) |
acpmux send inside a session on this daemon | session_message / subsession_message provenance | already handled |
| Batched agent messages (busy chat) | agent_digest provenance and [agent message i of N] from X lines | already handled |
acpmux send at a shell, or relayed from a peer | only client: acpmux-cli#… / acpmux-peer#… and the text | [name] and From name: already handled. X -> Y: is new |
| SendMessage between Claude Code sessions | <cross-session-message> in an adopted chat's prompt (client: "adopt") | already handled. Watched chats (client: "watch", hub/watch.rs) now get the same reading |
| cmux-forum | announcement provenance and context blocks: rules, not messages | not touched |
The fix (parser only)
-
incomingPrompt(text, msg, context?).contextgives this chat's names andisChat(name)for chats in the session list.direct.tspasses the summary'sname/shortName, and matches listed chats by name, short name, or the bare name of a peer chat listed as<peer>/<name>. -
X -> Y: body(also→, and no spaces around the arrow) is an agent's message only if two things hold:- The record carries nothing from the person's own pane: the client is
acpmux-cli#/acpmux-peer#, or the chat is adopted or watched. - X is a listed chat, or Y is this chat (case-insensitive) and X reads as an agent's name (
summary-panel,coord-deputy; notleo).
The card then names X, drops the address line, and renders the body as Markdown. The chip opens the sender through the existing
useSenderChat. - The record carries nothing from the person's own pane: the client is
-
When a sender is listed only after its prompt was read (a later list page, a new chat), the rows are read again once. This is bounded: it happens only when a newly listed chat names a sender that an addressed prompt was still waiting on. Until the summary arrives (a kept tail's replay), this chat's own name comes from its listed entry.
-
When provenance already names the sender, a leading
sender -> receiver:line is dropped from the body, the same way[name]andFrom name:are, but only when the sender looks like an agent name (coord-deputy,summary-panel). -
Client
watchis read the same asadopt, so a watched chat's Claude Code envelopes, reminders and task notifications no longer show raw.
Why a strict prefix fallback and not metadata
The message carries no metadata that names an agent, and the same client prefix also covers the person typing at the acpmux TUI. So the text is all there is. The fallback only fires when the record isn't from the pane, the arrow is the first thing in the message, both names are single words, a colon and a body follow, and the message is sent by a listed chat or is addressed to this chat by an agent-like name. A user who types foo -> bar: … in the pane is never matched, because the client gate rejects it. At the TUI it doesn't match either: neither name is this chat or a listed one. Prose such as then a -> b: see doesn't match because it isn't at the start, and a-->b: is not an address. Without a context (a caller that knows no chats), the arrow form never matches.
The real fix for metadata is acpmux recording a session_message provenance for peer-relayed sends (see follow-ups).
What is left: the person, at the acpmux TUI or a shell, typing <hyphenated-name> -> <this chat's name>: … or naming a listed chat as the sender would get a card. The Codex review called this transport-not-authorship. It is accepted here because the record has nothing else to go on. The alternative is today's behaviour, where every peer-relayed agent message shows as the person's bubble. The acpmux follow-up below removes the guess.
Tests
incomingMessage.test.ts:- positive cases:
->,→, no spaces, body on the next line, cli and peer clients, a listed sender, adopt and watch; - negatives: pane clients;
foo -> bar:anda -> b: seefrom the TUI; the arrow mid-sentence; multi-word names; no colon; no body; a URL; no context; an adopted typed prompt behind a reminder; - the provenance path drops the address line only for agent-like names;
- a watched chat's
<cross-session-message>.
- positive cases:
test/agent-pane-addressed-message.test.ts: runs the recorded steer throughAcpmuxDirectClient, with this chat namedclaude-sr2andhuge-blue/summary-panelin the list. It fails on base (the row is a bubble with the raw prefix) and passes here. A second test lists the sender after the prompt was read (session_changed) and checks that the prompt becomes its card. It fails if the re-read is removed.- Run on huge-blue: 174 related webview tests pass. tsc shows no errors in the changed files; the remaining errors are in base files (generated strings, composer-row-layout, home-cloud-rows).
vp fmtandvp lintare clean.
Before / after
Gallery entry agent-pane.addressed-message / relayed runs the real incomingPrompt on the recorded text. Playwright Chromium on huge-blue, dark. "Before" is the same fixture with base incomingMessage.ts.
| Before (base parser) | After |
|---|---|
![]() | ![]() |
Before: summary-panel's report is a grey bubble on the right that starts with summary-panel -> claude-sr2:. After: it is summary-panel's card, with the chip and no address line. The person's own foo -> bar: stays their bubble in both.
Follow-ups (not in this PR)
-
acpmux: give a peer-relayed
acpmux sendasession_messageprovenance (server/peer_forward.rsalready forwardsfromAgentin one case). The pane could then stop relying on the text. -
acpmux sidebar line (
hub/prompt_line.rs) for watched chats:begin_watched_turninhub/watch.rssetslast_promptfrom the raw text without callingprompt_line;prompt_linereads envelopes only foradopt;prompt_linedoes not drop Claude Code's "Another Claude session sent a message:" line before the envelope.
It is Rust and a different surface, so it is left out here.
Risk: medium. This is a behaviour change in how prompts are classified; the review is from a different model (Codex, xhigh).
Agent session: Air-Blue
🤖 Generated with Claude Code

