Agent pane: show agent pair pauses with Resume
Summary
Pairs with #21975, which adds the acpmux agent pair guard. When that guard pauses two agent chats that keep messaging each other without progress, it writes mux records to the transcripts: agent_pair_paused, agent_pair_held, agent_pair_resumed, agent_pair_released and agent_pair_dropped. No client draws them yet, so the other agent's messages just stop arriving. Nothing says why, and the person has no way to let the held messages through.
The agent pane now shows a pause as a quiet inline row, styled like the compaction and failover notes:
- The row names the peer and gives one sentence per reason:
alternating: "Paused messages with Reviewer: the two agents went back and forth 10 times without progress".repeating: "...the same message was sent 4 times in a row without progress".cap: "...the two agents exchanged 21 messages within 10 minutes with no message from you". It usescount, andwindowSecswritten out as a long unit in the pane language ("10 Minuten" in German).release_failed: "...a held message could not be delivered", with the error as the sentence's tooltip.
- Under it: "2 messages held", a Resume button, and Show held messages. That disclosure lists each held preview, clamped to 3 lines and 200 characters, at most 20 of them, then "and N more".
- Held messages are keyed by prompt id, so a resent message counts once.
agent_pair_releasedtakes a message off the list and counts it as delivered.agent_pair_droppedcounts it apart, as "1 not delivered". - A re-pause after a failed release is a row of its own. It takes over what the pause before it still held. The pane replays the same records live and on reload, so both draw the same rows.
- After
agent_pair_resumed(from the button, or the person's next prompt), the row is one static line: "Resumed messages with Reviewer, 2 delivered", plus "1 not delivered" when acpmux dropped any. Nothing on the row animates. - Resume sends
_acpmux/agent_pair_resume {sessionId, peerSessionId}throughchat.agentPair.resumeto the direct client.sessionIdis the chat on screen (actingSessionId), like the sibling actions.resumeAgentPairis the same method #21975 adds at the same spot, so whichever PR lands second has a trivial conflict.- A second click before the row redraws sends nothing.
- If the request fails, the row says "Couldn't resume" and the button works again.
- The counts and the failure are an
<output>live region. The toggle hasaria-expandedandaria-controlspointing at its list. - A held message never draws as a user prompt. A pause stays outside a finished turn's "Worked for" fold, because it is waiting on the person.
The reducer is its own module (conversation/agentPairs.ts), so direct.ts and App.tsx only gain wiring lines.
Testing
All commands run from webviews/. Red, then green, with the same command, bun test test/agent-pane-agent-pair.test.tsx test/agent-pane-agent-pair-row.test.tsx:
-
Review fixes. The failing tests are 24cbf5f1dea5: 0 pass, 13 fail. Among the failures:
- The cap read as back and forth.
- A resent message counted twice.
- Released and dropped records were ignored.
- A release_failed re-pause drew "0 times" with nothing held.
- Resume during a switch hold named the incoming chat (
…c3) instead of the one on screen. - The transcript row sent Resume twice on a double click.
With the fix, e61d091511e1: 13 pass, 0 fail.
-
Cap copy. The test commit is 978bd6ecf0e5. Its red run was 1 pass, 1 fail: the main file failed to load because
spelledWindowwas missing. With the fix, 0f19b9ed386f: 14 pass, 0 fail. After the red commit I corrected two test expectations: the repeating sentence's full text ("...without progress"), and the live region queried asoutput, [role='status']once lint asked for<output>. -
Held list and delivered count (an earlier pass). The red run was 00cb6c17b4f6: 2 pass, 5 fail. The green run was 15b6128dd63c: 7 pass. The first commit's tests landed with its code, so they have no red run of their own.
Coverage:
- The reducer runs through a fake daemon (
test/support/agentPairDaemon.ts): live records, the deduped resends, released and dropped, arelease_failedre-pause live and on reload, the cap with its window, unknown peers, andactingSessionIdduring a switch hold. - Render tests check each reason's sentence, the live region, toggle and
aria-controls, the clamp and the 20-item cap, the resumed line with its delivered and dropped counts, a double click, and failed-Resume retry. test/agent-pane-agent-pair-row.test.tsxclicks Resume on the realVirtualTranscriptrow, so the click goes throughchat.agentPair.resumeto_acpmux/agent_pair_resumeon the fake daemon.
Other checks:
bun test test/agent-pane-compaction-rows.test.ts test/agent-pane-session-start.test.ts src/agent-session/acpmux/conversation: 41 pass (on e61d091511e1).bun run typecheckandbun x vp checkpass on 0f19b9ed386f.bun test test/gallery-coverage.test.ts test/gallery-env.test.ts test/pane-english.test.ts: 10 pass, 1 fail. The failure is the coverage check, which lists the same five unrelated components with or without this PR's source changes (AttachmentStrip,MobilePairingPage,AppearanceBar,NumberListEditor,DialogButton). The newPauseCircleicon is allowlisted like its siblings.- No native build.
Record shapes were read from #21975's cmux-tui/crates/acpmux/src/hub/agent_pairs.rs at 93fae49a9b62. That PR is still changing, so they may move.
Not run, and known gaps
Resume works in the app only once #21975 lands. #21975 carries the pane bridge allowlist entry for _acpmux/agent_pair_resume (Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AcpmuxPaneMethods.swift, default deny). Without it, the click ends in "Couldn't resume". The person's next prompt in the chat still resumes the pair on the hub side.
The hub accepts the method only from a person-proved connection (conn.is_person()). For the pane, that is the native AcpmuxPaneSocket, which answers the daemon's initialize personChallenge with the app's person key. So Resume goes through the bridge, never through a socket the page opens.
This branch does not carry #21975's spec or generated methods.ts entry. direct.ts sends the method by its name string, "_acpmux/agent_pair_resume", as #21975's own copy does. On a base without #21975 the records never appear, so no row draws and nothing else changes. I have not run this against a live acpmux with #21975.
Known limit: reopening a chat reads only a tail page. The rows are built from the records the pane has read, and opening a chat reads only the newest page of its log. So on reopen:
- A
release_failedrow can lose what it carried over, when the pause and held records before it are outside that page. - A pause row can be missing entirely, when its records are older than the page.
A follow-up can read the older agent pair records with _acpmux/events {beforeSeq, kinds: [agent_pair_*]}, the way fetchTurnOpener reads a turn's opener. Or #21975 can carry the still-held list on the release_failed record.
Known acpmux-side edge. If the daemon restarts mid-delivery, restore_agent_pairs holds the rest again without a new agent_pair_paused. Those messages stay on the resumed row, where they are not shown.
Changelog
Added: When cmux pauses two agents that keep messaging each other without progress, the chat says so, lists the held messages, and offers Resume.
Proof
Gallery entry agent-pane.agent-pair-note has these variants: paused, held-list (a play step opens Show held messages), repeating, cap, release-failed and resumed. I checked the first pass's screenshots locally from the gallery dev server. I have not viewed the variants added since.
Checklist
- Behavior changes have added or updated tests, or Testing says why not
- Localization audited:
- 14 new keys in
webviews/src/agent-session/acpmux/Localizable.xcstringscover all 21 pane locales:agentPair.paused.alternating,.repeating,.cap,.releaseFailed,agentPair.held,agentPair.dropped,agentPair.moreHeld,agentPair.resume,agentPair.resumed,agentPair.resumedDelivered,agentPair.showHeld,agentPair.hideHeld,agentPair.resumeFailedandagentPair.peerFallback. - The seven count strings have each locale's plural forms.
- English is
translated; the other locales are machine translated and markedneeds_review. scripts/cmux-next/check-l10n.shreports 0 errors.- The cap's
{window}is anIntl.NumberFormatlong unit in the pane language, so it needs no catalog string.
- 14 new keys in
🤖 Generated with Claude Code