diff --git a/src-node/claude-code-agent.js b/src-node/claude-code-agent.js index ace042841c..40eb387683 100644 --- a/src-node/claude-code-agent.js +++ b/src-node/claude-code-agent.js @@ -1151,6 +1151,36 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, } } } + + // Tool results come back as user-typed messages with content blocks + // of type tool_result. Log isError + content size so we can correlate + // a "Tool done" (input stream) with what Claude actually saw as the reply. + if (message.type === "user" && message.message && Array.isArray(message.message.content)) { + for (const block of message.message.content) { + if (block && block.type === "tool_result") { + let len = 0; + let preview = ""; + if (typeof block.content === "string") { + len = block.content.length; + preview = block.content.slice(0, 120); + } else if (Array.isArray(block.content)) { + for (const c of block.content) { + if (c && c.type === "text" && typeof c.text === "string") { + len += c.text.length; + if (!preview) { preview = c.text.slice(0, 120); } + } else if (c && c.type === "image" && typeof c.data === "string") { + len += c.data.length; + if (!preview) { preview = "[image " + c.data.length + "ch]"; } + } + } + } + _log("Tool result:", block.tool_use_id || "?", + "isError=" + !!block.is_error, + "len=" + len + "ch", + preview ? ("preview=" + JSON.stringify(preview)) : ""); + } + } + } } // Flush any remaining accumulated text diff --git a/src-node/mcp-editor-tools.js b/src-node/mcp-editor-tools.js index 17b9ca4e57..4896a598f1 100644 --- a/src-node/mcp-editor-tools.js +++ b/src-node/mcp-editor-tools.js @@ -35,6 +35,36 @@ const CLARIFICATION_HINT = "IMPORTANT: The user has typed a follow-up clarification while you were working." + " Call the getUserClarification tool to read it before proceeding."; +// Per-tool safety-net budgets for the browser round-trip. The node connector +// is reliable in practice, so these should never fire during normal use — +// they exist so a stalled promise chain (live preview wedged, etc.) surfaces +// a deterministic error to Claude instead of the handler hanging forever. +// Tools whose runtime is bounded by user-supplied code (execJsInLivePreview) +// intentionally have no timeout — the code is allowed to run as long as it takes. +const EXEC_PEER_TIMEOUT_MS = { + getEditorState: 5000, + takeScreenshot: 15000, + controlEditor: 5000, + resizeLivePreview: 5000 +}; + +function _execPeerWithTimeout(nodeConnector, fn, args, label) { + const ms = EXEC_PEER_TIMEOUT_MS[fn]; + const call = nodeConnector.execPeer(fn, args); + if (!ms) { + return call; // no timeout configured for this tool + } + let timer; + const timeout = new Promise(function (_resolve, reject) { + timer = setTimeout(function () { + reject(new Error(label + " timed out after " + ms + "ms")); + }, ms); + }); + return Promise.race([call, timeout]).finally(function () { + clearTimeout(timer); + }); +} + /** * Append a clarification hint to an MCP tool result if the user has queued a message. */ @@ -70,7 +100,7 @@ function createEditorMcpServer(sdkModule, nodeConnector, clarificationAccessors) async function () { let result; try { - const state = await nodeConnector.execPeer("getEditorState", {}); + const state = await _execPeerWithTimeout(nodeConnector, "getEditorState", {}, "getEditorState"); result = { content: [{ type: "text", text: JSON.stringify(state) }] }; @@ -107,11 +137,11 @@ function createEditorMcpServer(sdkModule, nodeConnector, clarificationAccessors) async function (args) { let toolResult; try { - const result = await nodeConnector.execPeer("takeScreenshot", { + const result = await _execPeerWithTimeout(nodeConnector, "takeScreenshot", { selector: args.selector || undefined, purePreview: args.purePreview || false, filePath: args.filePath || undefined - }); + }, "takeScreenshot"); if (result.filePath) { toolResult = { content: [{ type: "text", text: "Screenshot saved to: " + result.filePath }] @@ -148,9 +178,9 @@ function createEditorMcpServer(sdkModule, nodeConnector, clarificationAccessors) async function (args) { let toolResult; try { - const result = await nodeConnector.execPeer("execJsInLivePreview", { + const result = await _execPeerWithTimeout(nodeConnector, "execJsInLivePreview", { code: args.code - }); + }, "execJsInLivePreview"); if (result.error) { toolResult = { content: [{ type: "text", text: "Error: " + result.error }], @@ -202,7 +232,7 @@ function createEditorMcpServer(sdkModule, nodeConnector, clarificationAccessors) for (const op of args.operations) { console.log("[Phoenix AI] controlEditor:", op.operation, op.filePath); try { - const result = await nodeConnector.execPeer("controlEditor", op); + const result = await _execPeerWithTimeout(nodeConnector, "controlEditor", op, "controlEditor:" + op.operation); results.push(result); if (!result.success) { hasError = true; @@ -234,9 +264,9 @@ function createEditorMcpServer(sdkModule, nodeConnector, clarificationAccessors) async function (args) { let toolResult; try { - const result = await nodeConnector.execPeer("resizeLivePreview", { + const result = await _execPeerWithTimeout(nodeConnector, "resizeLivePreview", { width: args.width - }); + }, "resizeLivePreview"); if (result.error) { toolResult = { content: [{ type: "text", text: "Error: " + result.error }], diff --git a/src/styles/Extn-AIChatPanel.less b/src/styles/Extn-AIChatPanel.less index 10f73f8bd6..60903bfdcd 100644 --- a/src/styles/Extn-AIChatPanel.less +++ b/src/styles/Extn-AIChatPanel.less @@ -77,9 +77,9 @@ line-height: 19px; } - .ai-chat-header-actions { + .ai-chat-header-actions, + .ai-chat-header-left-actions { position: absolute; - right: 10px; display: flex; align-items: center; gap: 2px; @@ -88,6 +88,14 @@ transition: opacity 0.5s ease; } + .ai-chat-header-actions { + right: 10px; + } + + .ai-chat-header-left-actions { + left: 10px; + } + .ai-history-btn, .ai-settings-btn { display: flex; @@ -113,7 +121,8 @@ } /* Show header actions on tab container hover */ -.ai-tab-container:hover .ai-chat-header-actions { +.ai-tab-container:hover .ai-chat-header-actions, +.ai-tab-container:hover .ai-chat-header-left-actions { opacity: 1; pointer-events: auto; transition: opacity 0.15s ease;