diff --git a/src-node/claude-code-agent.js b/src-node/claude-code-agent.js index 4d2350f513..3bda06f8b9 100644 --- a/src-node/claude-code-agent.js +++ b/src-node/claude-code-agent.js @@ -824,7 +824,7 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, }, allowedTools: [ "Read", "Edit", "Write", "Glob", "Grep", "Bash", - "AskUserQuestion", "Task", + "AskUserQuestion", "Task", "Agent", "TodoRead", "TodoWrite", "TaskCreate", "TaskUpdate", "TaskList", "TaskGet", "WebFetch", "WebSearch", @@ -1612,6 +1612,49 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, _log("Session:", currentSessionId); } + // Subagent tool extraction. The SDK delivers the parent + // agent's tool calls as a stream of stream_event messages + // (content_block_start / content_block_delta / content_block_ + // stop), but the new Agent dispatcher's *subagent* tool calls + // arrive batched on a single assistant message with + // parent_tool_use_id set — there is no streaming path. We have + // to fish them out here, otherwise the UI sees the parent + // Agent card finish and then nothing until the subagent + // returns. Each tool_use block emits aiProgress + aiToolInfo + // back-to-back (no streaming preview — the SDK never gave us + // one); the tool_use id is registered in _toolUseIdToCounter + // so the existing tool_result handler routes the response + // back to the right indicator card. + if (message.type === "assistant" && + message.parent_tool_use_id && + message.message && Array.isArray(message.message.content)) { + const parentToolId = _toolUseIdToCounter[message.parent_tool_use_id]; + for (const block of message.message.content) { + if (block && block.type === "tool_use") { + toolCounter++; + if (block.id) { + _toolUseIdToCounter[block.id] = toolCounter; + } + _log("Subagent tool:", block.name, "#" + toolCounter, + "parent=#" + (parentToolId !== undefined ? parentToolId : "?")); + nodeConnector.triggerPeer("aiProgress", { + requestId: requestId, + toolName: block.name, + toolId: toolCounter, + parentToolId: parentToolId, + phase: "tool_use" + }); + nodeConnector.triggerPeer("aiToolInfo", { + requestId: requestId, + toolName: block.name, + toolId: toolCounter, + parentToolId: parentToolId, + toolInput: block.input || {} + }); + } + } + } + // Per-turn token usage: each SDKAssistantMessage carries the // wrapped Anthropic API message whose `.usage` reflects what // that single turn consumed. Useful for diagnosing runaway diff --git a/src/styles/Extn-AIChatPanel.less b/src/styles/Extn-AIChatPanel.less index b3bf169495..2811ed0fe0 100644 --- a/src/styles/Extn-AIChatPanel.less +++ b/src/styles/Extn-AIChatPanel.less @@ -1126,6 +1126,39 @@ } } +/* ── Subagent step rows ────────────────────────────────────────────── + Compact one-line entries inside an Agent dispatcher card showing + what the subagent is doing (Read foo.json, Searched: *.md, etc). + Indented with a left border so the nesting reads clearly. */ +.ai-subagent-steps { + margin-top: 4px; + padding-left: 8px; + border-left: 2px solid fade(@project-panel-text-2, 25%); +} + +.ai-subagent-step { + display: flex; + align-items: center; + gap: 6px; + padding: 2px 0; + font-size: @ai-text-secondary; +} + +.ai-subagent-step-icon { + width: 14px; + text-align: center; + flex-shrink: 0; + font-size: @ai-text-secondary; +} + +.ai-subagent-step-label { + color: @project-panel-text-2; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} + @keyframes ai-spin { to { transform: rotate(360deg); } } diff --git a/tracking-repos.json b/tracking-repos.json index 2ad56dbe5e..731d64b832 100644 --- a/tracking-repos.json +++ b/tracking-repos.json @@ -1,5 +1,5 @@ { "phoenixPro": { - "commitID": "21edc7c2963b6b73076dfa7b362a76ae71db7879" + "commitID": "79aeedfe7690aa5a166dd8e5b988900f6bf71468" } }