Skip to content

Commit 352c74c

Browse files
authored
Support subagent transcript viewing for Agent tool calls (#76)
## Summary Claude Code renamed its subagent tool from `Task` to `Agent`. This adds `Agent` handling across parser, taxonomy, frontend, and subagent transcript rendering so it works identically to `Task`. ## Key changes - Parse `agent_progress` events to map Agent tool calls to subagent session IDs - Normalize `Agent` to `Task` category in tool taxonomy - Extend `SubagentInline` rendering and content-parser regex to cover Agent - Add Agent tool name mapping and normalization tests
1 parent 7792ff0 commit 352c74c

12 files changed

Lines changed: 76 additions & 10 deletions

File tree

frontend/src/lib/components/content/ToolBlock.svelte

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
generateFallbackContent,
99
} from "../../utils/tool-params.js";
1010
11+
/** Returns true for tool names that represent a subagent call ("Task" or "Agent"). */
12+
function isSubagentTool(name: string | undefined): boolean {
13+
return name === "Task" || name === "Agent";
14+
}
15+
1116
interface Props {
1217
content: string;
1318
label?: string;
@@ -33,7 +38,7 @@
3338
3439
/** For Task tool calls, extract key metadata fields */
3540
let taskMeta = $derived.by(() => {
36-
if (toolCall?.tool_name !== "Task" || !inputParams)
41+
if (!isSubagentTool(toolCall?.tool_name) || !inputParams)
3742
return null;
3843
const meta: { label: string; value: string }[] = [];
3944
if (inputParams.subagent_type) {
@@ -107,13 +112,13 @@
107112
});
108113
109114
let taskPrompt = $derived(
110-
toolCall?.tool_name === "Task"
115+
isSubagentTool(toolCall?.tool_name)
111116
? inputParams?.prompt ?? null
112117
: null,
113118
);
114119
115120
let subagentSessionId = $derived(
116-
toolCall?.tool_name === "Task"
121+
isSubagentTool(toolCall?.tool_name)
117122
? toolCall?.subagent_session_id ?? null
118123
: null,
119124
);

frontend/src/lib/utils/content-parser.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,14 @@ const THINKING_LEGACY_RE =
2727
/\[Thinking\]\n?([\s\S]*?)(?=\n\[|\n\n|$)/g;
2828

2929
const TOOL_NAMES =
30-
"Tool|Read|Write|Edit|Bash|Glob|Grep|Other|TaskCreate|TaskUpdate|TaskGet|TaskList|Task|Skill|" +
30+
"Tool|Read|Write|Edit|Bash|Glob|Grep|Other|TaskCreate|TaskUpdate|TaskGet|TaskList|Task|Agent|Skill|" +
3131
"SendMessage|Question|Todo List|Entering Plan Mode|" +
3232
"Exiting Plan Mode|exec_command|shell_command|" +
3333
"write_stdin|apply_patch|shell|parallel|view_image|" +
3434
"request_user_input|update_plan";
3535

3636
const TOOL_ALIASES: Record<string, string> = {
37+
Agent: "Task",
3738
exec_command: "Bash",
3839
shell_command: "Bash",
3940
write_stdin: "Bash",

frontend/src/lib/utils/tool-params.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export function generateFallbackContent(
106106
toolName: string,
107107
params: Params,
108108
): string | null {
109-
if (toolName === "Task") return null;
109+
if (toolName === "Task" || toolName === "Agent") return null;
110110
if (toolName === "Edit") {
111111
const lines: string[] = [];
112112
if (params.old_string != null) {

internal/parser/claude.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,19 @@ func ParseClaudeSession(
116116
continue
117117
}
118118

119+
// Collect agent_progress events for subagent mapping.
120+
// Claude Code v2.1+ emits these instead of queue-operation for Agent tool calls.
121+
if entryType == "progress" {
122+
if gjson.Get(line, "data.type").Str == "agent_progress" {
123+
tuid := gjson.Get(line, "parentToolUseID").Str
124+
agentID := gjson.Get(line, "data.agentId").Str
125+
if tuid != "" && agentID != "" {
126+
subagentMap[tuid] = "agent-" + agentID
127+
}
128+
}
129+
continue
130+
}
131+
119132
if entryType != "user" && entryType != "assistant" {
120133
continue
121134
}
@@ -495,7 +508,7 @@ func annotateSubagentSessions(
495508
for i := range messages {
496509
for j := range messages[i].ToolCalls {
497510
tc := &messages[i].ToolCalls[j]
498-
if tc.ToolName == "Task" && tc.ToolUseID != "" {
511+
if (tc.ToolName == "Task" || tc.ToolName == "Agent") && tc.ToolUseID != "" {
499512
if sid, ok := subagentMap[tc.ToolUseID]; ok {
500513
tc.SubagentSessionID = sid
501514
}

internal/parser/claude_subagent_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,18 @@ func TestSubagentSessionIDMapping(t *testing.T) {
6262
{ToolUseID: "toolu_xyz789", ToolName: "Task", Category: "Task", SubagentSessionID: "agent-cafebabe"},
6363
},
6464
},
65+
{
66+
name: "Agent Tool Name",
67+
lines: []string{
68+
`{"type":"user","timestamp":"2024-01-01T10:00:00Z","uuid":"u1","message":{"content":"hello"},"cwd":"/tmp"}`,
69+
`{"type":"assistant","timestamp":"2024-01-01T10:00:01Z","uuid":"u2","parentUuid":"u1","message":{"content":[{"type":"tool_use","id":"toolu_agent1","name":"Agent","input":{"description":"explore codebase","subagent_type":"general-purpose","prompt":"find all API endpoints"}}]}}`,
70+
`{"type":"queue-operation","operation":"enqueue","timestamp":"2024-01-01T10:00:01Z","sessionId":"test-session","content":"{\"task_id\":\"agent789\",\"tool_use_id\":\"toolu_agent1\",\"description\":\"explore codebase\",\"task_type\":\"local_agent\"}"}`,
71+
`{"type":"user","timestamp":"2024-01-01T10:00:05Z","uuid":"u3","parentUuid":"u2","message":{"content":[{"type":"tool_result","tool_use_id":"toolu_agent1","content":"found 5 endpoints"}]}}`,
72+
},
73+
wantTools: []ParsedToolCall{
74+
{ToolUseID: "toolu_agent1", ToolName: "Agent", Category: "Task", SubagentSessionID: "agent-agent789"},
75+
},
76+
},
6577
{
6678
name: "Non-Task Tool Unchanged",
6779
lines: []string{
@@ -98,6 +110,19 @@ func TestSubagentSessionIDMapping(t *testing.T) {
98110
{ToolUseID: "toolu_01XYZ", ToolName: "Task", Category: "Task", SubagentSessionID: "agent-beef4567"},
99111
},
100112
},
113+
{
114+
name: "Agent Progress Events",
115+
lines: []string{
116+
`{"type":"user","timestamp":"2024-01-01T10:00:00Z","uuid":"u1","message":{"content":"hello"},"cwd":"/tmp"}`,
117+
`{"type":"assistant","timestamp":"2024-01-01T10:00:01Z","uuid":"u2","parentUuid":"u1","message":{"content":[{"type":"tool_use","id":"toolu_bdrk_01Wt5","name":"Agent","input":{"description":"wiki search","subagent_type":"wiki-search","prompt":"search wiki"}}]}}`,
118+
`{"type":"progress","timestamp":"2024-01-01T10:00:02Z","parentToolUseID":"toolu_bdrk_01Wt5","data":{"type":"agent_progress","agentId":"a78243c84a44ebcd4","message":{"type":"user","message":{"role":"user","content":[{"type":"text","text":"search wiki"}]}}}}`,
119+
`{"type":"progress","timestamp":"2024-01-01T10:00:03Z","parentToolUseID":"toolu_bdrk_01Wt5","data":{"type":"agent_progress","agentId":"a78243c84a44ebcd4","message":{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"found it"}]}}}}`,
120+
`{"type":"user","timestamp":"2024-01-01T10:00:05Z","uuid":"u3","parentUuid":"u2","message":{"content":[{"type":"tool_result","tool_use_id":"toolu_bdrk_01Wt5","content":"wiki results"}]}}`,
121+
},
122+
wantTools: []ParsedToolCall{
123+
{ToolUseID: "toolu_bdrk_01Wt5", ToolName: "Agent", Category: "Task", SubagentSessionID: "agent-a78243c84a44ebcd4"},
124+
},
125+
},
101126
{
102127
name: "Multiple Subagents",
103128
lines: []string{

internal/parser/codex.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ func codexCategoryDetail(
374374
return fmt.Sprintf("%s in %s", pattern, path)
375375
}
376376
return firstNonEmpty(pattern, path)
377-
case "Task":
377+
case "Task", "Agent":
378378
desc := codexArgValue(args, "description")
379379
agent := codexArgValue(args, "subagent_type")
380380
if desc != "" && agent != "" {

internal/parser/codex_parser_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,22 @@ func TestParseCodexSession_FunctionCalls(t *testing.T) {
9494
assertToolCalls(t, msgs[1].ToolCalls, []ParsedToolCall{{ToolName: "write_stdin", Category: "Bash"}})
9595
})
9696

97+
t.Run("Agent function call normalizes to Task category", func(t *testing.T) {
98+
content := testjsonl.JoinJSONL(
99+
testjsonl.CodexSessionMetaJSON("fc-agent", "/tmp", "user", tsEarly),
100+
testjsonl.CodexMsgJSON("user", "explore code", tsEarlyS1),
101+
testjsonl.CodexFunctionCallArgsJSON("Agent", map[string]any{
102+
"description": "explore codebase",
103+
"subagent_type": "Explore",
104+
}, tsEarlyS5),
105+
)
106+
sess, msgs := runCodexParserTest(t, "test.jsonl", content, false)
107+
assert.Equal(t, "codex:fc-agent", sess.ID)
108+
assert.Equal(t, 2, len(msgs))
109+
assert.Contains(t, msgs[1].Content, "[Task: explore codebase (Explore)]")
110+
assertToolCalls(t, msgs[1].ToolCalls, []ParsedToolCall{{ToolName: "Agent", Category: "Task"}})
111+
})
112+
97113
t.Run("function call no name skipped", func(t *testing.T) {
98114
content := testjsonl.JoinJSONL(
99115
testjsonl.CodexSessionMetaJSON("fc-2", "/tmp", "user", tsEarly),

internal/parser/content.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ func formatToolUse(block gjson.Result) string {
162162
skill = input.Get("name").Str
163163
}
164164
return fmt.Sprintf("[Skill: %s]", skill)
165-
case "Task":
165+
case "Task", "Agent":
166166
return formatTask(input)
167167
case "Skill":
168168
return fmt.Sprintf("[Skill: %s]", input.Get("skill").Str)

internal/parser/parser_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,11 @@ func TestFormatToolUseVariants(t *testing.T) {
374374
`{"type":"tool_use","name":"Task","input":{"description":"explore","subagent_type":"Explore"}}`,
375375
"[Task: explore (Explore)]",
376376
},
377+
{
378+
"Agent",
379+
`{"type":"tool_use","name":"Agent","input":{"description":"explore","subagent_type":"Explore"}}`,
380+
"[Task: explore (Explore)]",
381+
},
377382
{
378383
"EnterPlanMode",
379384
`{"type":"tool_use","name":"EnterPlanMode","input":{}}`,

internal/parser/taxonomy.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func NormalizeToolCategory(rawName string) string {
1818
return "Grep"
1919
case "Glob":
2020
return "Glob"
21-
case "Task":
21+
case "Task", "Agent":
2222
return "Task"
2323
case "Skill":
2424
return "Tool"

0 commit comments

Comments
 (0)