Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 51 additions & 9 deletions apps/desktop/src/main/todo-agent/supervisor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,8 @@ class TodoSupervisor {
kind: parsed.event.kind,
label: parsed.event.label,
text: parsed.event.text,
toolUseId: parsed.event.toolUseId,
parentToolUseId: parsed.event.parentToolUseId,
},
]);
}
Expand Down Expand Up @@ -1008,6 +1010,18 @@ interface ClassifiedEvent {
kind: TodoStreamEventKind;
label: string;
text: string;
/**
* For `tool_use` events this is the tool_use block id.
* For `tool_result` events this is the `tool_use_id` the result
* targets. Undefined for non-tool events.
*/
toolUseId?: string;
/**
* Set when the NDJSON record has a top-level `parent_tool_use_id`,
* i.e. the message was emitted from inside a subagent (Agent/Task
* tool) context.
*/
parentToolUseId?: string;
}

interface ClassifiedLine {
Expand Down Expand Up @@ -1037,6 +1051,13 @@ function classifyStreamJson(payload: unknown): ClassifiedLine {
const type = typeof rec.type === "string" ? (rec.type as string) : "";
const sessionId =
typeof rec.session_id === "string" ? (rec.session_id as string) : null;
// Claude Code sets `parent_tool_use_id` on the top-level NDJSON
// record whenever the message was emitted inside a subagent
// context (i.e. the main session invoked the Task/Agent tool).
const parentToolUseId =
typeof rec.parent_tool_use_id === "string"
? (rec.parent_tool_use_id as string)
: undefined;

if (type === "system" && rec.subtype === "init") {
return {
Expand All @@ -1056,30 +1077,43 @@ function classifyStreamJson(payload: unknown): ClassifiedLine {
return {
...empty,
sessionId,
event: { kind: "assistant_text", label: "Claude", text },
event: {
kind: "assistant_text",
label: "Claude",
text,
parentToolUseId,
},
};
}
const tool = extractToolUseSummary(rec.message);
if (tool) {
return {
...empty,
sessionId,
event: { kind: "tool_use", label: tool.label, text: tool.text },
event: {
kind: "tool_use",
label: tool.label,
text: tool.text,
toolUseId: tool.id,
parentToolUseId,
},
};
}
return empty;
}

if (type === "user") {
const text = extractToolResultText(rec.message);
if (text) {
const result = extractToolResultDetails(rec.message);
if (result) {
return {
...empty,
sessionId,
event: {
kind: "tool_result",
label: "tool result",
text: truncate(text, 400),
text: truncate(result.text, 400),
toolUseId: result.toolUseId,
parentToolUseId,
},
};
}
Expand Down Expand Up @@ -1144,7 +1178,7 @@ function extractAssistantText(message: unknown): string | null {

function extractToolUseSummary(
message: unknown,
): { label: string; text: string } | null {
): { label: string; text: string; id: string | undefined } | null {
if (typeof message !== "object" || message === null) return null;
const content = (message as { content?: unknown }).content;
if (!Array.isArray(content)) return null;
Expand All @@ -1153,22 +1187,29 @@ function extractToolUseSummary(
const rec = part as Record<string, unknown>;
if (rec.type !== "tool_use") continue;
const name = typeof rec.name === "string" ? (rec.name as string) : "tool";
const id = typeof rec.id === "string" ? (rec.id as string) : undefined;
const input = rec.input;
const inputSummary = summarizeToolInput(name, input);
return { label: name, text: inputSummary };
return { label: name, text: inputSummary, id };
}
return null;
}

function extractToolResultText(message: unknown): string | null {
function extractToolResultDetails(
message: unknown,
): { text: string; toolUseId: string | undefined } | null {
if (typeof message !== "object" || message === null) return null;
const content = (message as { content?: unknown }).content;
if (!Array.isArray(content)) return null;
const parts: string[] = [];
let toolUseId: string | undefined;
for (const part of content) {
if (typeof part !== "object" || part === null) continue;
const rec = part as Record<string, unknown>;
if (rec.type === "tool_result") {
if (!toolUseId && typeof rec.tool_use_id === "string") {
toolUseId = rec.tool_use_id as string;
}
const inner = rec.content;
if (typeof inner === "string") {
parts.push(inner);
Expand All @@ -1184,7 +1225,8 @@ function extractToolResultText(message: unknown): string | null {
}
}
const joined = parts.join("\n").trim();
return joined.length > 0 ? joined : null;
if (joined.length === 0) return null;
return { text: joined, toolUseId };
}

function summarizeToolInput(name: string, input: unknown): string {
Expand Down
17 changes: 17 additions & 0 deletions apps/desktop/src/main/todo-agent/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,23 @@ export interface TodoStreamEvent {
text: string;
/** Optional raw payload for the "raw" / debug kind. */
raw?: unknown;
/**
* The Anthropic tool-use block id this event corresponds to.
* - For `tool_use` events: the id of the tool_use content block.
* - For `tool_result` events: the `tool_use_id` the result answers.
* Lets the UI pair tool_use ↔ tool_result by id instead of position,
* which is robust to concurrent / out-of-order SDK emissions.
*/
toolUseId?: string;
/**
* Set on messages emitted from inside a subagent's context (i.e. when
* the main session invoked the `Task`/`Agent` tool). Its value is the
* tool_use id of the parent Agent tool call. The UI uses this to nest
* sub-tool activity under the parent Agent card, matching the VSCode
* Claude Code extension's presentation.
* See: https://docs.claude.com/en/docs/agent-sdk/ (Subagents)
*/
parentToolUseId?: string;
}

export interface TodoStreamUpdate {
Expand Down
Loading
Loading