diff --git a/phoenix-builder-mcp/mcp-tools.js b/phoenix-builder-mcp/mcp-tools.js index 6a9ff65ec7..871ede1176 100644 --- a/phoenix-builder-mcp/mcp-tools.js +++ b/phoenix-builder-mcp/mcp-tools.js @@ -38,14 +38,15 @@ const AI_TEST_SUITES = { "unsaved-buffers": "suite-unsaved-buffers.md", "self-sufficiency": "suite-self-sufficiency.md", "bug-fixing": "suite-bug-fixing.md", + "questions": "suite-questions.md", "plan-mode": "suite-plan-mode.md", "permissions": "suite-permissions.md" }; // The four model runs that have caught every regression seen so far, plus the // free deterministic/piggyback checks. See model_tests.md, "Deterministic first". const AI_TEST_QUICK = { - suites: ["editor-context", "unsaved-buffers", "self-sufficiency", "bug-fixing"], - tests: ["UB-1", "EC-5", "EC-2", "SS-4", "EC-1", "UB-2", "SS-1", "BF-1"] + suites: ["editor-context", "unsaved-buffers", "self-sufficiency", "bug-fixing", "questions"], + tests: ["UB-1", "EC-5", "EC-2", "SS-4", "QF-6", "EC-1", "UB-2", "SS-1", "BF-1", "QF-1"] }; function _gitInfo(cwd) { diff --git a/src-node/claude-code-agent.js b/src-node/claude-code-agent.js index 82aea2933d..9c70c91b81 100644 --- a/src-node/claude-code-agent.js +++ b/src-node/claude-code-agent.js @@ -1115,6 +1115,9 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, // SDK tool_use id (e.g. "toolu_01...") → our sequential toolCounter so a // tool_result block can be mapped back to its indicator on the browser. const _toolUseIdToCounter = {}; + // tool_use id → SDK tool name, so a tool_result can be interpreted in the + // light of which tool produced it (see the AskUserQuestion note below). + const _toolUseIdToName = {}; // Set true once the user clicks "Allow & Switch to Edit Mode" on a // plan-mode write confirmation. Subsequent Edit/Write attempts in the same // turn skip the prompt and use the cached "allow" decision so a multi-edit @@ -2396,6 +2399,7 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, toolCounter++; if (block.id) { _toolUseIdToCounter[block.id] = toolCounter; + _toolUseIdToName[block.id] = block.name; } _log("Subagent tool:", block.name, "#" + toolCounter, "parent=#" + (parentToolId !== undefined ? parentToolId : "?")); @@ -2598,6 +2602,7 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, // correlate later tool_result blocks back to the indicator. if (event.content_block.id) { _toolUseIdToCounter[event.content_block.id] = activeToolCounter; + _toolUseIdToName[event.content_block.id] = activeToolName; } _log("Tool start:", activeToolName, "#" + activeToolCounter); nodeConnector.triggerPeer("aiProgress", { @@ -2713,10 +2718,22 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, // on the corresponding tool indicator (errored vs ran). const counterId = _toolUseIdToCounter[block.tool_use_id]; if (counterId !== undefined) { + // A question is answered by DENYING the tool call and + // handing the user's answer back as the denial reason + // (see the AskUserQuestion PreToolUse hook). The CLI + // reports every deny as an error result, so without + // this the panel painted a red "failed" badge on a + // question the user had just answered normally — and + // counted every question as a tool error in metrics. + // The deny is our transport, not a failure; an + // unanswered question means the user cancelled or + // stopped the turn, which is not a failure either. + const resultToolName = _toolUseIdToName[block.tool_use_id]; + const isAnsweredByDeny = resultToolName === "AskUserQuestion"; nodeConnector.triggerPeer("aiToolResult", { requestId: requestId, toolId: counterId, - isError: !!block.is_error, + isError: !!block.is_error && !isAnsweredByDeny, preview: preview }); }