diff --git a/canvas/src/components/tabs/chat/AgentCommsPanel.tsx b/canvas/src/components/tabs/chat/AgentCommsPanel.tsx index 268953ce..fc327ea0 100644 --- a/canvas/src/components/tabs/chat/AgentCommsPanel.tsx +++ b/canvas/src/components/tabs/chat/AgentCommsPanel.tsx @@ -472,6 +472,7 @@ function GroupedCommsView({ ), )} +
@@ -560,6 +561,83 @@ function PeerTabButton({ ); } +/** WaitingBubbles renders one "typing" bubble per peer that has an + * in-flight outbound delegation — i.e., the most recent outbound + * message to that peer is still pending or queued and no later inbound + * reply has arrived. Mirrors the bouncing-dots indicator in ChatTab so + * the operator sees the same visual cue regardless of whether they're + * watching their own chat or a peer thread. + * + * Why "per peer" not "one global": when multiple delegations are in + * flight to different peers (common during fan-out), one shared + * spinner under-reports — the user can't tell whether ALL peers are + * still working or only the visible ones. Per-peer matches Slack-style + * typing indicators and keeps the signal honest. + * + * Why we look at the LAST per-peer message: once a peer replies (an + * "in" bubble lands), the corresponding "out" bubble is no longer the + * tail — even if status hasn't been mutated to "completed", the inbound + * reply means the wait is over. Looking at the tail collapses both + * cases into one rule. + */ +function WaitingBubbles({ visible }: { visible: CommMessage[] }) { + // Group by peer, keep only the chronologically-last message per peer, + // emit a bubble when that tail is an outbound pending/queued. + const tailByPeer = new Map(); + for (const m of visible) { + const prev = tailByPeer.get(m.peerId); + if (!prev || m.timestamp > prev.timestamp) tailByPeer.set(m.peerId, m); + } + const waitingPeers = Array.from(tailByPeer.values()).filter( + (m) => m.flow === "out" && (m.status === "pending" || m.status === "queued"), + ); + if (waitingPeers.length === 0) return null; + return ( + <> + {waitingPeers.map((m) => ( +
+
+
→ To {m.peerName}
+ +
+
+ ))} + + ); +} + function NormalMessage({ msg }: { msg: CommMessage }) { return (