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}
+
+
+
+
+
+
+
+ {m.status === "queued"
+ ? `${m.peerName} is busy — reply will arrive when they're free`
+ : `Waiting for ${m.peerName}…`}
+
+
+
+
+ ))}
+ >
+ );
+}
+
function NormalMessage({ msg }: { msg: CommMessage }) {
return (