claude: give each spawned subagent its own collapsing card (T-342)

A fan-out of N agents (Task/Agent) merged into one shared "Activity / N
steps" cluster — groupConversation folded an Agent spawn like any Bash/
Read call. Now an Agent spawn is a cluster boundary, rendering as its own
first-class collapsing card (reusing the existing sticky-agent path: folded
prompt T-263 + nested run T-264), while adjacent non-agent foldables keep
clustering into the normal Activity card.

Two changes:
- activity_cluster: a shared isAgentTool() predicate; _isFoldable returns
  false for agent spawns at every level (incl. L3), so parallel agents
  never merge. Only the grouping boundary changes; fold mechanics are
  unchanged.
- conversation_view: harden resolveOwner. Its nearest-preceding-agent
  fallback is safe with one agent but mis-routes under a parallel fan-out
  (an unattributable item lands in whichever agent was emitted last —
  a sibling's card). With >1 agent, drop the fallback so the item orphans
  (rendered inline) instead of cross-attributed. The T-338 direct route
  (parent_tool_use_id) still attributes interleaved items correctly.

Tests: two consecutive agents → two cards (not one cluster); agent breaks
a sibling cluster; agents first-class at L3; regression — consecutive
Bash still one cluster; interleaved parallel-agent runs route to their own
card; an unattributable item orphans instead of being swept into the last
agent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 14:57:21 +02:00
co-authored by Claude Opus 4.8
parent ce200765cc
commit 4c33a85bf0
7 changed files with 135 additions and 4 deletions
+13 -4
View File
@@ -205,6 +205,15 @@ class _ConversationViewState extends State<ConversationView> {
if (it is AssistantToolUse) toolUseIds.add(it.toolUseId);
}
// With more than one agent in the turn (a parallel fan-out), the
// "nearest preceding agent" fallback is unsafe: an item with no
// parent_tool_use_id and no rooted parentUuid chain would mis-file into
// whichever agent was emitted last — landing in a SIBLING agent's card.
// Drop the fallback in that case so an unattributable item orphans
// (rendered inline) rather than cross-attributed (T-342). A single agent
// has only one possible owner, so the fallback stays safe there.
final multipleAgents = agentByToolUseId.length > 1;
AssistantToolUse? resolveOwner(ConversationItem item, AssistantToolUse? nearest) {
// Direct route: stream-json hands us the spawning Agent's tool-use id on
// the item itself (T-338) — no chain to walk.
@@ -223,7 +232,7 @@ class _ConversationViewState extends State<ConversationView> {
if (sidechainByUuid[parent] != true) break; // left the run's chain
cur = parent;
}
return nearest;
return multipleAgents ? null : nearest;
}
final owned = <String>{};
@@ -371,9 +380,9 @@ class _ConversationViewState extends State<ConversationView> {
/// used for the "claude" message card's stripe + label.
const claudeAccent = Color(0xFFD97757);
/// The tool names that launch a sub-agent (sidechain). Claude Code emits
/// `Task`; the Agent SDK surface uses `Agent` — accept both (T-263).
bool _isAgentTool(String name) => name == 'Task' || name == 'Agent';
/// The tool names that launch a sub-agent (sidechain) — shared with the
/// grouping pass so "is this an agent spawn?" has one definition (T-342).
bool _isAgentTool(String name) => isAgentTool(name);
/// Open a governance/ticket record clicked in the conversation (T-279) in its
/// context-pane reader, reusing the existing `selection` MessageBus addressing