diff --git a/src-node/claude-code-agent.js b/src-node/claude-code-agent.js index 00f9e5d973..ee724402a2 100644 --- a/src-node/claude-code-agent.js +++ b/src-node/claude-code-agent.js @@ -378,12 +378,18 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale) let accumulatedText = ""; let lastStreamTime = 0; - // Tool input tracking + // Tool input tracking (parent-level) let activeToolName = null; let activeToolIndex = null; let activeToolInputJson = ""; let lastToolStreamTime = 0; + // Sub-agent tool tracking + let subagentToolName = null; + let subagentToolIndex = null; + let subagentToolInputJson = ""; + let lastSubagentToolStreamTime = 0; + // Trace counters (logged at tool/query completion, not per-delta) let toolDeltaCount = 0; let toolStreamSendCount = 0; @@ -406,94 +412,184 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale) // Handle streaming events if (message.type === "stream_event") { const event = message.event; + const isSubagent = !!message.parent_tool_use_id; + + if (isSubagent) { + // --- Sub-agent events --- + + // Sub-agent tool use start + if (event.type === "content_block_start" && + event.content_block?.type === "tool_use") { + subagentToolName = event.content_block.name; + subagentToolIndex = event.index; + subagentToolInputJson = ""; + toolCounter++; + lastSubagentToolStreamTime = 0; + _log("Subagent tool start:", subagentToolName, "#" + toolCounter); + nodeConnector.triggerPeer("aiProgress", { + requestId: requestId, + toolName: subagentToolName, + toolId: toolCounter, + phase: "tool_use" + }); + } - // Tool use start — send initial indicator - if (event.type === "content_block_start" && - event.content_block?.type === "tool_use") { - activeToolName = event.content_block.name; - activeToolIndex = event.index; - activeToolInputJson = ""; - toolCounter++; - toolDeltaCount = 0; - toolStreamSendCount = 0; - lastToolStreamTime = 0; - _log("Tool start:", activeToolName, "#" + toolCounter); - nodeConnector.triggerPeer("aiProgress", { - requestId: requestId, - toolName: activeToolName, - toolId: toolCounter, - phase: "tool_use" - }); - } + // Sub-agent tool input streaming + if (event.type === "content_block_delta" && + event.delta?.type === "input_json_delta" && + event.index === subagentToolIndex) { + subagentToolInputJson += event.delta.partial_json; + const now = Date.now(); + if (subagentToolInputJson && + now - lastSubagentToolStreamTime >= TEXT_STREAM_THROTTLE_MS) { + lastSubagentToolStreamTime = now; + nodeConnector.triggerPeer("aiToolStream", { + requestId: requestId, + toolId: toolCounter, + toolName: subagentToolName, + partialJson: subagentToolInputJson + }); + } + } - // Accumulate tool input JSON and stream preview - if (event.type === "content_block_delta" && - event.delta?.type === "input_json_delta" && - event.index === activeToolIndex) { - activeToolInputJson += event.delta.partial_json; - toolDeltaCount++; - const now = Date.now(); - if (activeToolInputJson && - now - lastToolStreamTime >= TEXT_STREAM_THROTTLE_MS) { - lastToolStreamTime = now; - toolStreamSendCount++; - nodeConnector.triggerPeer("aiToolStream", { + // Sub-agent tool block complete + if (event.type === "content_block_stop" && + event.index === subagentToolIndex && + subagentToolName) { + if (subagentToolInputJson) { + nodeConnector.triggerPeer("aiToolStream", { + requestId: requestId, + toolId: toolCounter, + toolName: subagentToolName, + partialJson: subagentToolInputJson + }); + } + let toolInput = {}; + try { + toolInput = JSON.parse(subagentToolInputJson); + } catch (e) { + // ignore parse errors + } + _log("Subagent tool done:", subagentToolName, "#" + toolCounter, + "json=" + subagentToolInputJson.length + "ch"); + nodeConnector.triggerPeer("aiToolInfo", { requestId: requestId, + toolName: subagentToolName, toolId: toolCounter, - toolName: activeToolName, - partialJson: activeToolInputJson + toolInput: toolInput }); + subagentToolName = null; + subagentToolIndex = null; + subagentToolInputJson = ""; } - } - // Tool block complete — flush final stream preview and send details - if (event.type === "content_block_stop" && - event.index === activeToolIndex && - activeToolName) { - // Final flush of tool stream (bypasses throttle) - if (activeToolInputJson) { - toolStreamSendCount++; - nodeConnector.triggerPeer("aiToolStream", { + // Sub-agent text deltas — stream as regular text + if (event.type === "content_block_delta" && + event.delta?.type === "text_delta") { + accumulatedText += event.delta.text; + textDeltaCount++; + const now = Date.now(); + if (now - lastStreamTime >= TEXT_STREAM_THROTTLE_MS) { + lastStreamTime = now; + textStreamSendCount++; + nodeConnector.triggerPeer("aiTextStream", { + requestId: requestId, + text: accumulatedText + }); + accumulatedText = ""; + } + } + } else { + // --- Parent-level events (unchanged) --- + + // Tool use start — send initial indicator + if (event.type === "content_block_start" && + event.content_block?.type === "tool_use") { + activeToolName = event.content_block.name; + activeToolIndex = event.index; + activeToolInputJson = ""; + toolCounter++; + toolDeltaCount = 0; + toolStreamSendCount = 0; + lastToolStreamTime = 0; + _log("Tool start:", activeToolName, "#" + toolCounter); + nodeConnector.triggerPeer("aiProgress", { requestId: requestId, - toolId: toolCounter, toolName: activeToolName, - partialJson: activeToolInputJson + toolId: toolCounter, + phase: "tool_use" }); } - let toolInput = {}; - try { - toolInput = JSON.parse(activeToolInputJson); - } catch (e) { - // ignore parse errors + + // Accumulate tool input JSON and stream preview + if (event.type === "content_block_delta" && + event.delta?.type === "input_json_delta" && + event.index === activeToolIndex) { + activeToolInputJson += event.delta.partial_json; + toolDeltaCount++; + const now = Date.now(); + if (activeToolInputJson && + now - lastToolStreamTime >= TEXT_STREAM_THROTTLE_MS) { + lastToolStreamTime = now; + toolStreamSendCount++; + nodeConnector.triggerPeer("aiToolStream", { + requestId: requestId, + toolId: toolCounter, + toolName: activeToolName, + partialJson: activeToolInputJson + }); + } } - _log("Tool done:", activeToolName, "#" + toolCounter, - "deltas=" + toolDeltaCount, "sent=" + toolStreamSendCount, - "json=" + activeToolInputJson.length + "ch"); - nodeConnector.triggerPeer("aiToolInfo", { - requestId: requestId, - toolName: activeToolName, - toolId: toolCounter, - toolInput: toolInput - }); - activeToolName = null; - activeToolIndex = null; - activeToolInputJson = ""; - } - // Stream text deltas (throttled) - if (event.type === "content_block_delta" && - event.delta?.type === "text_delta") { - accumulatedText += event.delta.text; - textDeltaCount++; - const now = Date.now(); - if (now - lastStreamTime >= TEXT_STREAM_THROTTLE_MS) { - lastStreamTime = now; - textStreamSendCount++; - nodeConnector.triggerPeer("aiTextStream", { + // Tool block complete — flush final stream preview and send details + if (event.type === "content_block_stop" && + event.index === activeToolIndex && + activeToolName) { + // Final flush of tool stream (bypasses throttle) + if (activeToolInputJson) { + toolStreamSendCount++; + nodeConnector.triggerPeer("aiToolStream", { + requestId: requestId, + toolId: toolCounter, + toolName: activeToolName, + partialJson: activeToolInputJson + }); + } + let toolInput = {}; + try { + toolInput = JSON.parse(activeToolInputJson); + } catch (e) { + // ignore parse errors + } + _log("Tool done:", activeToolName, "#" + toolCounter, + "deltas=" + toolDeltaCount, "sent=" + toolStreamSendCount, + "json=" + activeToolInputJson.length + "ch"); + nodeConnector.triggerPeer("aiToolInfo", { requestId: requestId, - text: accumulatedText + toolName: activeToolName, + toolId: toolCounter, + toolInput: toolInput }); - accumulatedText = ""; + activeToolName = null; + activeToolIndex = null; + activeToolInputJson = ""; + } + + // Stream text deltas (throttled) + if (event.type === "content_block_delta" && + event.delta?.type === "text_delta") { + accumulatedText += event.delta.text; + textDeltaCount++; + const now = Date.now(); + if (now - lastStreamTime >= TEXT_STREAM_THROTTLE_MS) { + lastStreamTime = now; + textStreamSendCount++; + nodeConnector.triggerPeer("aiTextStream", { + requestId: requestId, + text: accumulatedText + }); + accumulatedText = ""; + } } } } diff --git a/src-node/mcp-editor-tools.js b/src-node/mcp-editor-tools.js index 52372d8c4b..767243041d 100644 --- a/src-node/mcp-editor-tools.js +++ b/src-node/mcp-editor-tools.js @@ -42,7 +42,9 @@ function createEditorMcpServer(sdkModule, nodeConnector) { const getEditorStateTool = sdkModule.tool( "getEditorState", "Get the current Phoenix editor state: active file, working set (open files), live preview file, " + - "and cursor/selection info (current line text with surrounding context, or selected text). " + + "cursor/selection info (current line text with surrounding context, or selected text), " + + "and the currently selected element in the live preview (tag, selector, text preview) if any. " + + "The live preview selected element may differ from the editor cursor — use execJsInLivePreview to inspect it further. " + "Long lines are trimmed to 200 chars and selections to 10K chars — use the Read tool for full content.", {}, async function () { diff --git a/src/core-ai/aiPhoenixConnectors.js b/src/core-ai/aiPhoenixConnectors.js index 41a716e50d..0038673236 100644 --- a/src/core-ai/aiPhoenixConnectors.js +++ b/src/core-ai/aiPhoenixConnectors.js @@ -54,6 +54,7 @@ define(function (require, exports, module) { * Called from the node-side MCP server via execPeer. */ function getEditorState() { + const deferred = new $.Deferred(); const activeFile = MainViewManager.getCurrentlyViewedFile(MainViewManager.ACTIVE_PANE); const workingSet = MainViewManager.getWorkingSet(MainViewManager.ALL_PANES); @@ -138,7 +139,37 @@ define(function (require, exports, module) { } } - return result; + // If live preview is connected, query the selected element (best-effort) + if (LiveDevProtocol.getConnectionIds().length > 0) { + const LP_SELECTED_EL_JS = + "(function(){" + + "var el=window.__current_ph_lp_selected;" + + "if(!el||!el.isConnected)return null;" + + "var tag=el.tagName.toLowerCase();" + + "var id=el.id?'#'+el.id:'';" + + "var cls=el.className&&typeof el.className==='string'?" + + "'.'+el.className.trim().split(/\\s+/).join('.'):" + + "'';" + + "var text=el.textContent||'';" + + "if(text.length>80)text=text.slice(0,80)+'...';" + + "text=text.replace(/\\n/g,' ').trim();" + + "return{tag:tag,selector:tag+id+cls,textPreview:text};" + + "})()"; + LiveDevProtocol.evaluate(LP_SELECTED_EL_JS) + .done(function (evalResult) { + if (evalResult && evalResult.tag) { + result.livePreviewSelectedElement = evalResult; + } + deferred.resolve(result); + }) + .fail(function () { + deferred.resolve(result); + }); + } else { + deferred.resolve(result); + } + + return deferred.promise(); } // --- Screenshot --- diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 21a76ca79b..f6b9db8f8d 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1825,7 +1825,7 @@ define({ "AI_CHAT_TOOL_SCREENSHOT_OF": "Screenshot of {0}", "AI_CHAT_TOOL_SCREENSHOT_LIVE_PREVIEW": "live preview", "AI_CHAT_TOOL_SCREENSHOT_FULL_PAGE": "full page", - "AI_CHAT_TOOL_LIVE_PREVIEW_JS": "Live Preview JS", + "AI_CHAT_TOOL_LIVE_PREVIEW_JS": "Inspecting preview", "AI_CHAT_TOOL_CONTROL_EDITOR": "Editor", "AI_CHAT_TOOL_TASKS": "Tasks", "AI_CHAT_TOOL_TASKS_SUMMARY": "{0} of {1} tasks done", diff --git a/src/styles/Extn-AIChatPanel.less b/src/styles/Extn-AIChatPanel.less index 9118461205..d086b11a44 100644 --- a/src/styles/Extn-AIChatPanel.less +++ b/src/styles/Extn-AIChatPanel.less @@ -409,7 +409,7 @@ /* ── TodoWrite task list widget ────────────────────────────────────── */ .ai-todo-list { - padding: 2px 8px 4px 28px; + padding: 2px 8px 4px 0; } .ai-todo-item { @@ -434,6 +434,9 @@ .ai-todo-content { color: @project-panel-text-2; line-height: 1.4; + white-space: normal; + overflow-wrap: break-word; + min-width: 0; &.completed { text-decoration: line-through;