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
@@ -74,6 +74,14 @@ final class EditRun extends RenderGroup {
/// Tools whose result is a diff the user wants to keep first-class at L1/L2.
bool isDiffTool(String name) => const {'Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'Update'}.contains(name);
/// Tool names that spawn a sub-agent (sidechain): Claude Code emits `Task`,
/// the Agent SDK surface uses `Agent`. An agent spawn is ALWAYS its own
/// first-class collapsing card — it breaks the Activity cluster so a fan-out
/// of N agents reads as N cards, never one merged "Activity / N steps" card
/// (T-342). Each card carries its own folded prompt (T-263) + nested run
/// (T-264); the fold mechanics are unchanged, only the grouping boundary.
bool isAgentTool(String name) => name == 'Task' || name == 'Agent';
/// The file an edit tool-use targets, or null if [it] isn't a same-file edit
/// (used to group consecutive edits, T-296).
String? editFilePath(ConversationItem it) {
@@ -159,6 +167,10 @@ bool _isFoldable(ConversationItem item, FoldLevel level, Map<String, String> too
case AssistantThinkingMessage():
return level != FoldLevel.tools;
case AssistantToolUse(:final name):
// An Agent/Task spawn is always its own first-class card (T-342) — it
// breaks the cluster at every level, including L3, so parallel agents
// never merge into one Activity card.
if (isAgentTool(name)) return false;
// The Edit/Write call stays first-class with its diff at L1/L2.
if (level == FoldLevel.everything) return true;
return !isDiffTool(name);