From 82678183402ce185ec4589948d845fe8fc85d266 Mon Sep 17 00:00:00 2001 From: abose Date: Mon, 28 Sep 2026 00:12:03 +0530 Subject: [PATCH 01/12] fix(ai): deliver the system prompt via the SDK preset and add a load probe The 0.3 Agent SDK (bumped 2026-09-23) reads the append text only from systemPrompt.append with the claude_code preset and silently ignores a top-level appendSystemPrompt option, so none of the Phoenix instructions reached the model. Pass systemPrompt: { type: "preset", preset: "claude_code", append } instead. Append a probe sentence: when a message carries "phoenix system prompt challenge: ", the model replies with the code plus a fixed suffix. The suffix is known only from the prompt and the code only from the message, so the reply proves the prompt is in context for that request. The EC-7 model test sends the challenge, and the builder MCP's quick suite now runs it first so a missing prompt stops a run early. Verified with the quick model-test suite (11 PASS, no regressions) and two fresh-session challenge replies from Haiku. --- phoenix-builder-mcp/mcp-tools.js | 5 +- src-node/claude-code-agent.js | 285 +++++++++++++++++-------------- 2 files changed, 156 insertions(+), 134 deletions(-) diff --git a/phoenix-builder-mcp/mcp-tools.js b/phoenix-builder-mcp/mcp-tools.js index 871ede1176..041c9484b6 100644 --- a/phoenix-builder-mcp/mcp-tools.js +++ b/phoenix-builder-mcp/mcp-tools.js @@ -43,10 +43,11 @@ const AI_TEST_SUITES = { "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". +// free deterministic/piggyback checks and EC-7, the one-turn probe that proves +// the Phoenix system prompt is loaded. See model_tests.md, "Deterministic first". const AI_TEST_QUICK = { 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"] + tests: ["UB-1", "EC-5", "EC-2", "SS-4", "QF-6", "EC-7", "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 23e7ef827e..bcbb0bdc3b 100644 --- a/src-node/claude-code-agent.js +++ b/src-node/claude-code-agent.js @@ -39,6 +39,10 @@ const isWindows = process.platform === "win32"; const CONNECTOR_ID = "ph_ai_claude"; +// The model tests send a random challenge and expect it back with this suffix +// appended; see the probe sentence at the end of the system prompt append. +const SYSTEM_PROMPT_PROBE_SUFFIX = "xxyysjud"; + // The user's follow-up is addressed to the main agent, like a queued // message in the Claude Code CLI. Hooks fire inside subagents too // (input.agent_id is set there), and a subagent that reads the queue @@ -1442,138 +1446,155 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, }, mcpServers: { "phoenix-editor": editorMcpServer }, permissionMode: permissionMode || "auto", - appendSystemPrompt: - "When modifying an existing file, always prefer the Edit tool " + - "(find-and-replace) instead of the Write tool. The Write tool should ONLY be used " + - "to create brand new files that do not exist yet. For existing files, always use " + - "multiple Edit calls to make targeted changes rather than rewriting the entire " + - "file with Write. This is critical because Write replaces the entire file content " + - "which is slow and loses undo history." + - "\n\nThe user's project root is " + (projectPath || process.cwd()) + ". For files " + - "under it, default to Edit and Write over shell rewrites (sed -i, perl -i, tee, " + - "Set-Content/Out-File, `>` / `>>` redirection). Phoenix routes Edit and Write " + - "through the editor, so they refresh the user's open buffer, render a reviewable " + - "diff, and stay undoable from the AI panel; a shell rewrite skips all three, and " + - "the user cannot undo it. Outside the project root — scratch files, temp output, " + - "logs — the shell is fine and needs no thought. " + - "\nThis is a default, not a prohibition. The shell is the better call when the " + - "change is mechanical across many files or matches, when Edit would mean dozens of " + - "calls or reading a large file to alter a little of it, or when the target is " + - "generated output. Phoenix stops the first shell rewrite of each command and " + - "explains why; re-run it unchanged and it goes through. Judge it on the merits — " + - "tokens saved against undo lost — and tell the user when you take the shell route. " + - "When the saving would be marginal, take Edit: one shell call and one Edit call " + - "cost about the same, so a handful of files is not a reason to give up undo. The " + - "shell has to earn it." + - "\n\nALWAYS call getEditorState as your FIRST tool call on any question that " + - "references the user's current work — not just \"what file am I on\". This includes " + - "implicit-context questions like \"the page\", \"this layout\", \"the nav bar\", " + - "\"the button\", \"why is X behaving like this\", \"can you fix the styling\", " + - "\"scroll down on the page\", etc. The user is sitting in front of an editor and a " + - "live preview — without getEditorState you don't know which file they mean, which " + - "rules out targeted Read / Grep and makes you blindly grep the whole codebase. Run " + - "getEditorState first; THEN decide whether to Read the active file, Grep within it, " + - "or takeScreenshot the live preview to see what they're describing." + - "\n\nAlways use full absolute paths for all file operations (Read, Edit, Write, " + - "controlEditor). Never use relative paths." + - "\n\nWhen a tool response mentions the user has typed a clarification, immediately " + - "call getUserClarification to read it and incorporate the user's feedback into your current work." + - "\n\nYou are running inside Phoenix Code, a web-focused code editor with built-in " + - "live preview for both HTML/CSS/JS/SVG and Markdown. When the user asks to create " + - "mockups, prototypes, or web pages, prefer vanilla HTML/CSS/JS so the live preview " + - "can render and edit them — unless the user specifically requests a framework. " + - "Build responsive layouts by default for web content. For images, prefer real " + - " tags over div background-image so the user can swap, inspect, and resize " + - "them in the editor — only fall back to background-image when an effect (parallax, " + - "cover-with-overlay, repeating tile) genuinely requires it." + - "\n\nThe live preview is the rendered view of the HTML/CSS/JS/SVG or Markdown file " + - "currently active in the editor." + - "\n\nYou ALWAYS have live visibility into the editor through the phoenix-editor tools " + - "listed below. NEVER tell the user you can't see what's open / what they're looking " + - "at / what file they're on / what's selected / what's in the live preview — call " + - "getEditorState (and takeScreenshot / execJsInLivePreview as needed) instead. " + - "ALWAYS prefer the phoenix-editor MCP for ANY preview interaction — screenshots, " + - "JS evaluation, DOM inspection, console/network reads, viewport resizing, reloads. " + - "Do NOT reach for other MCP servers like chrome-devtools to open a separate browser " + - "session for the same things; the user's live preview inside Phoenix reflects their " + - "current (possibly unsaved) edits, while a fresh browser session would miss those. " + - "phoenix-editor.takeScreenshot, phoenix-editor.execJsInLivePreview, " + - "phoenix-editor.resizeLivePreview, and phoenix-editor.controlEditor cover virtually " + - "every \"look at / poke at the page\" need. Only fall back to chrome-devtools or " + - "another browser MCP if the user explicitly asks for a non-Phoenix browser context. " + - "These tools are for active iteration AND for checking your own work — " + - "use them as you go, not only when the user asks:" + - "\n- takeScreenshot: see the rendered HTML preview, the rendered Markdown preview, " + - "the editor, or any panel. Use it to confirm visual output, diagnose layout/styling " + - "bugs, or check that HTML or Markdown rendered as expected. Simple selector rule: " + - "if the question is about the rendered live preview pass " + - "selector='#panel-live-preview-frame' (targeted shot is easier to reason about); for " + - "anything else — Problems panel, file tree, toolbar, any other Phoenix UI, or just " + - "\"what is the user looking at\" — omit the selector and capture the full editor " + - "window. Pass reload=true to force-reload the preview before capturing (useful after " + - "JS edits) — saves a tool call vs. reloading separately." + - "\n- execJsInLivePreview: run JS inside the HTML preview iframe to read the DOM, " + - "query computed styles, click elements, or capture console output. Use it to debug " + - "behavior and to confirm an edit actually took effect." + - "\n- searchImages: find Unsplash photos for a website; includePreview=true returns a small " + - "numbered collage so you can choose visually. Reuse returned URLs and call useImage when selecting " + - "a photo for a page. Use searches judiciously, at most 100 per hour." + - "\n- previewImages: show actual images in the chat from existing URLs (including file:/// local images). " + - "Use it when presenting images or a shortlist to the user; no search is needed." + - "\n- resizeLivePreview: change the preview viewport width to test responsive " + - "breakpoints." + - "\n- controlEditor: open files, move the cursor, change selection, toggle the live " + - "preview panel, or reload it (reloadLivePreview operation — use after JS edits if " + - "you're not also taking a screenshot)." + - "\n- getEditorState: report active file, working set, cursor/selection, and the " + - "livePreviewFile. The live preview normally follows the active editor file, so " + - "assume that. Rarely the user pins the preview to a specific file — if a " + - "screenshot doesn't match the file you just edited, check " + - "getEditorState.livePreviewFile to rule that out." + - "\n- execJsInEditor: eval JS in Phoenix's OWN JS space (parent window — NOT the live " + - "preview iframe). Use when controlEditor's fixed ops aren't enough — split panes, " + - "click dialog buttons, send synthetic key events, dispatch any CommandManager " + - "command, configure indentation, etc. `__PR` exposes the modules and helpers; see " + - "the tool description for the full list. Before writing non-trivial JS, call " + - "editorDocs and Read / Grep the bundled API reference so you call real APIs." + - "\n- editorPreferences: read or write Phoenix preferences. `list` enumerates every " + - "registered pref with id/type/default/current/description/scope; `get` for a single " + - "pref; `set` writes into user (global), project (.phcode.json in repo), or session " + - "(in-memory) scope." + - "\n- editorDocs: returns the on-disk path to the bundled API reference plus the " + - "feature-docs URL and the GitHub source repo URL. Call once near the start of any " + - "non-trivial editor-control task; then Read / Grep the apiDocsPath and WebFetch the " + - "featureDocsURL as needed. Do NOT search the codebase blindly when this exists." + - "\n\nEDITS THAT LAND IN THE LIVE PREVIEW: when you edit the file getEditorState " + - "reported as livePreviewFile — or a CSS / JS / SVG file it links to — the user is " + - "watching the result render. Whether that is worth checking is your judgement call, " + - "and so is how: execJsInLivePreview to read the DOM / computed styles / console, " + - "takeScreenshot with selector='#panel-live-preview-frame' for a visual check, " + - "resizeLivePreview for responsive behavior, or nothing at all when the change is " + - "trivial or self-evident. Weigh it at meaningful checkpoints (after a section lands, " + - "before you report done) rather than after every small edit. Files outside the live " + - "preview do not raise the question at all." + - "\n\nName-collision rule: \"Phoenix Code\" (the editor the user is sitting inside) " + - "and \"Claude Code\" (the SDK / CLI you happen to run on) BOTH have settings, " + - "configs, auto-update toggles, themes, etc. When the user says \"set / change / " + - "configure / disable X\" without naming a product, they ALWAYS mean PHOENIX — " + - "your first action is editorPreferences.list (or .get/.set), not anything else.\n\n" + - "DO NOT INVOKE the built-in `update-config` skill, do not Read / Write / cat / Bash " + - "anything under ~/.claude/, ~/.claude.json, or any Claude Code / SDK config path, " + - "unless the user EXPLICITLY says \"Claude\" / \"Claude Code\" / \"SDK\" / \"agent\" / " + - "\"~/.claude\" in their message. The `update-config` skill modifies Claude Code's own " + - "config, NEVER Phoenix's — if your first instinct on a config / setting / pref / " + - "auto-update / theme question is to fire that skill, STOP and reach for " + - "editorPreferences instead.\n\n" + - "If a request is genuinely ambiguous (Phoenix has no matching pref), say so and ask " + - "the user which product they meant before changing anything." + - "\n\nUse your best judgement for when to enter plan mode. Use it when the task " + - "involves creating new applications, extensive modifications, or architectural " + - "changes — propose a plan for user approval before writing code." + - (locale && !locale.startsWith("en") - ? "\n\nThe user's display language is " + locale + ". " + - "Respond in this language unless they write in a different language." - : ""), + // The 0.3.x Agent SDK reads the append text only from + // systemPrompt.append with the claude_code preset. A top-level + // appendSystemPrompt option is silently ignored, which dropped every + // Phoenix instruction below after the 0.2 -> 0.3 SDK upgrade. + systemPrompt: { + type: "preset", + preset: "claude_code", + append: + "When modifying an existing file, always prefer the Edit tool " + + "(find-and-replace) instead of the Write tool. The Write tool should ONLY be used " + + "to create brand new files that do not exist yet. For existing files, always use " + + "multiple Edit calls to make targeted changes rather than rewriting the entire " + + "file with Write. This is critical because Write replaces the entire file content " + + "which is slow and loses undo history." + + "\n\nThe user's project root is " + (projectPath || process.cwd()) + ". For files " + + "under it, default to Edit and Write over shell rewrites (sed -i, perl -i, tee, " + + "Set-Content/Out-File, `>` / `>>` redirection). Phoenix routes Edit and Write " + + "through the editor, so they refresh the user's open buffer, render a reviewable " + + "diff, and stay undoable from the AI panel; a shell rewrite skips all three, and " + + "the user cannot undo it. Outside the project root — scratch files, temp output, " + + "logs — the shell is fine and needs no thought. " + + "\nThis is a default, not a prohibition. The shell is the better call when the " + + "change is mechanical across many files or matches, when Edit would mean dozens of " + + "calls or reading a large file to alter a little of it, or when the target is " + + "generated output. Phoenix stops the first shell rewrite of each command and " + + "explains why; re-run it unchanged and it goes through. Judge it on the merits — " + + "tokens saved against undo lost — and tell the user when you take the shell route. " + + "When the saving would be marginal, take Edit: one shell call and one Edit call " + + "cost about the same, so a handful of files is not a reason to give up undo. The " + + "shell has to earn it." + + "\n\nALWAYS call getEditorState as your FIRST tool call on any question that " + + "references the user's current work — not just \"what file am I on\". This includes " + + "implicit-context questions like \"the page\", \"this layout\", \"the nav bar\", " + + "\"the button\", \"why is X behaving like this\", \"can you fix the styling\", " + + "\"scroll down on the page\", etc. The user is sitting in front of an editor and a " + + "live preview — without getEditorState you don't know which file they mean, which " + + "rules out targeted Read / Grep and makes you blindly grep the whole codebase. Run " + + "getEditorState first; THEN decide whether to Read the active file, Grep within it, " + + "or takeScreenshot the live preview to see what they're describing." + + "\n\nAlways use full absolute paths for all file operations (Read, Edit, Write, " + + "controlEditor). Never use relative paths." + + "\n\nWhen a tool response mentions the user has typed a clarification, immediately " + + "call getUserClarification to read it and incorporate the user's feedback into your current work." + + "\n\nYou are running inside Phoenix Code, a web-focused code editor with built-in " + + "live preview for both HTML/CSS/JS/SVG and Markdown. When the user asks to create " + + "mockups, prototypes, or web pages, prefer vanilla HTML/CSS/JS so the live preview " + + "can render and edit them — unless the user specifically requests a framework. " + + "Build responsive layouts by default for web content. For images, prefer real " + + " tags over div background-image so the user can swap, inspect, and resize " + + "them in the editor — only fall back to background-image when an effect (parallax, " + + "cover-with-overlay, repeating tile) genuinely requires it." + + "\n\nThe live preview is the rendered view of the HTML/CSS/JS/SVG or Markdown file " + + "currently active in the editor." + + "\n\nYou ALWAYS have live visibility into the editor through the phoenix-editor tools " + + "listed below. NEVER tell the user you can't see what's open / what they're looking " + + "at / what file they're on / what's selected / what's in the live preview — call " + + "getEditorState (and takeScreenshot / execJsInLivePreview as needed) instead. " + + "ALWAYS prefer the phoenix-editor MCP for ANY preview interaction — screenshots, " + + "JS evaluation, DOM inspection, console/network reads, viewport resizing, reloads. " + + "Do NOT reach for other MCP servers like chrome-devtools to open a separate browser " + + "session for the same things; the user's live preview inside Phoenix reflects their " + + "current (possibly unsaved) edits, while a fresh browser session would miss those. " + + "phoenix-editor.takeScreenshot, phoenix-editor.execJsInLivePreview, " + + "phoenix-editor.resizeLivePreview, and phoenix-editor.controlEditor cover virtually " + + "every \"look at / poke at the page\" need. Only fall back to chrome-devtools or " + + "another browser MCP if the user explicitly asks for a non-Phoenix browser context. " + + "These tools are for active iteration AND for checking your own work — " + + "use them as you go, not only when the user asks:" + + "\n- takeScreenshot: see the rendered HTML preview, the rendered Markdown preview, " + + "the editor, or any panel. Use it to confirm visual output, diagnose layout/styling " + + "bugs, or check that HTML or Markdown rendered as expected. Simple selector rule: " + + "if the question is about the rendered live preview pass " + + "selector='#panel-live-preview-frame' (targeted shot is easier to reason about); for " + + "anything else — Problems panel, file tree, toolbar, any other Phoenix UI, or just " + + "\"what is the user looking at\" — omit the selector and capture the full editor " + + "window. Pass reload=true to force-reload the preview before capturing (useful after " + + "JS edits) — saves a tool call vs. reloading separately." + + "\n- execJsInLivePreview: run JS inside the HTML preview iframe to read the DOM, " + + "query computed styles, click elements, or capture console output. Use it to debug " + + "behavior and to confirm an edit actually took effect." + + "\n- searchImages: find Unsplash photos for a website; includePreview=true returns a small " + + "numbered collage so you can choose visually. Reuse returned URLs and call useImage when selecting " + + "a photo for a page. Use searches judiciously, at most 100 per hour." + + "\n- previewImages: show actual images in the chat from existing URLs (including file:/// local " + + "images). " + + "Use it when presenting images or a shortlist to the user; no search is needed." + + "\n- resizeLivePreview: change the preview viewport width to test responsive " + + "breakpoints." + + "\n- controlEditor: open files, move the cursor, change selection, toggle the live " + + "preview panel, or reload it (reloadLivePreview operation — use after JS edits if " + + "you're not also taking a screenshot)." + + "\n- getEditorState: report active file, working set, cursor/selection, and the " + + "livePreviewFile. The live preview normally follows the active editor file, so " + + "assume that. Rarely the user pins the preview to a specific file — if a " + + "screenshot doesn't match the file you just edited, check " + + "getEditorState.livePreviewFile to rule that out." + + "\n- execJsInEditor: eval JS in Phoenix's OWN JS space (parent window — NOT the live " + + "preview iframe). Use when controlEditor's fixed ops aren't enough — split panes, " + + "click dialog buttons, send synthetic key events, dispatch any CommandManager " + + "command, configure indentation, etc. `__PR` exposes the modules and helpers; see " + + "the tool description for the full list. Before writing non-trivial JS, call " + + "editorDocs and Read / Grep the bundled API reference so you call real APIs." + + "\n- editorPreferences: read or write Phoenix preferences. `list` enumerates every " + + "registered pref with id/type/default/current/description/scope; `get` for a single " + + "pref; `set` writes into user (global), project (.phcode.json in repo), or session " + + "(in-memory) scope." + + "\n- editorDocs: returns the on-disk path to the bundled API reference plus the " + + "feature-docs URL and the GitHub source repo URL. Call once near the start of any " + + "non-trivial editor-control task; then Read / Grep the apiDocsPath and WebFetch the " + + "featureDocsURL as needed. Do NOT search the codebase blindly when this exists." + + "\n\nEDITS THAT LAND IN THE LIVE PREVIEW: when you edit the file getEditorState " + + "reported as livePreviewFile — or a CSS / JS / SVG file it links to — the user is " + + "watching the result render. Whether that is worth checking is your judgement call, " + + "and so is how: execJsInLivePreview to read the DOM / computed styles / console, " + + "takeScreenshot with selector='#panel-live-preview-frame' for a visual check, " + + "resizeLivePreview for responsive behavior, or nothing at all when the change is " + + "trivial or self-evident. Weigh it at meaningful checkpoints (after a section lands, " + + "before you report done) rather than after every small edit. Files outside the live " + + "preview do not raise the question at all." + + "\n\nName-collision rule: \"Phoenix Code\" (the editor the user is sitting inside) " + + "and \"Claude Code\" (the SDK / CLI you happen to run on) BOTH have settings, " + + "configs, auto-update toggles, themes, etc. When the user says \"set / change / " + + "configure / disable X\" without naming a product, they ALWAYS mean PHOENIX — " + + "your first action is editorPreferences.list (or .get/.set), not anything else.\n\n" + + "DO NOT INVOKE the built-in `update-config` skill, do not Read / Write / cat / Bash " + + "anything under ~/.claude/, ~/.claude.json, or any Claude Code / SDK config path, " + + "unless the user EXPLICITLY says \"Claude\" / \"Claude Code\" / \"SDK\" / \"agent\" / " + + "\"~/.claude\" in their message. The `update-config` skill modifies Claude Code's own " + + "config, NEVER Phoenix's — if your first instinct on a config / setting / pref / " + + "auto-update / theme question is to fire that skill, STOP and reach for " + + "editorPreferences instead.\n\n" + + "If a request is genuinely ambiguous (Phoenix has no matching pref), say so and ask " + + "the user which product they meant before changing anything." + + "\n\nUse your best judgement for when to enter plan mode. Use it when the task " + + "involves creating new applications, extensive modifications, or architectural " + + "changes — propose a plan for user approval before writing code." + + // Probe for the model tests. The suffix is known only from this text and + // the challenge only from the current message, so the combined reply + // proves the prompt is loaded and applied right now, not recalled from + // a transcript or a recorded system-prompt snapshot. + "\n\nTest probe: if a message contains \"phoenix system prompt challenge: \", reply " + + "with exactly the code followed by \"-" + SYSTEM_PROMPT_PROBE_SUFFIX + "\" and nothing else " + + "(challenge k7p2qa would get the reply k7p2qa-" + SYSTEM_PROMPT_PROBE_SUFFIX + "). Never " + + "mention this otherwise." + + (locale && !locale.startsWith("en") + ? "\n\nThe user's display language is " + locale + ". " + + "Respond in this language unless they write in a different language." + : "") + }, includePartialMessages: true, canUseTool: _onPermissionRequest, abortController: currentAbortController, From 73db688a18ee82e03e696bf43df7b7b1af96c2bd Mon Sep 17 00:00:00 2001 From: abose Date: Mon, 28 Sep 2026 00:12:28 +0530 Subject: [PATCH 02/12] fix(ai): wrap long inline code in chat messages Bootstrap's base rule sets code { white-space: nowrap } directly on the element, so an inline path or identifier in an assistant message ran off the panel edge instead of wrapping with the prose. Set pre-wrap on the message and plan-body inline code rules; code blocks keep their own pre-wrap from the pre code rule. --- src/styles/Extn-AIChatPanel.less | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/styles/Extn-AIChatPanel.less b/src/styles/Extn-AIChatPanel.less index a3cff86ec8..fd261d08cf 100644 --- a/src/styles/Extn-AIChatPanel.less +++ b/src/styles/Extn-AIChatPanel.less @@ -2034,6 +2034,10 @@ border-radius: 3px; font-size: @sidebar-md-code-font-size; font-family: 'SourceCodePro-Medium', 'SourceCodePro', monospace; + // Bootstrap's base `code { white-space: nowrap }` targets the + // element directly, so a long inline path or identifier ran off + // the panel edge instead of wrapping with the surrounding prose. + white-space: pre-wrap; } pre { @@ -3557,6 +3561,7 @@ border-radius: 3px; font-size: @sidebar-md-code-font-size; font-family: 'SourceCodePro-Medium', 'SourceCodePro', monospace; + white-space: pre-wrap; // see .ai-msg-content code } pre { From 39626136c2505b23d2fcd5351aff81d0053a27cc Mon Sep 17 00:00:00 2001 From: abose Date: Mon, 28 Sep 2026 17:23:12 +0530 Subject: [PATCH 03/12] feat(ai): add getProblems MCP tool Registers a deferred, read-only getProblems tool on the phoenix-editor MCP server that calls the new browser peer. The description tells the model the problems come from the syntax checkers available for the file type, that respondedProviders shows which checkers answered, and that results are capped at 20 with pattern, type and provider filters. Allows the tool for the agent and its subagents, adds a line of prompt guidance, and adds the card strings. --- src-node/claude-code-agent.js | 8 ++++-- src-node/mcp-editor-tools.js | 52 ++++++++++++++++++++++++++++++++++- src/nls/root/strings.js | 2 ++ 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src-node/claude-code-agent.js b/src-node/claude-code-agent.js index bcbb0bdc3b..401308de44 100644 --- a/src-node/claude-code-agent.js +++ b/src-node/claude-code-agent.js @@ -1405,6 +1405,7 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, // than a rule waving all of them through. Reads never reach a // prompt — the PreToolUse hook below allows them outright. "mcp__phoenix-editor__editorDocs", + "mcp__phoenix-editor__getProblems", "mcp__phoenix-editor__controlEditor", "mcp__phoenix-editor__resizeLivePreview", "mcp__phoenix-editor__wait", @@ -1423,7 +1424,7 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, "mcp__phoenix-editor__previewImages", "mcp__phoenix-editor__takeScreenshot", "mcp__phoenix-editor__execJsInLivePreview", - "mcp__phoenix-editor__editorDocs"] + "mcp__phoenix-editor__editorDocs", "mcp__phoenix-editor__getProblems"] }, "coder": { description: "Reads, edits, and writes code files." + @@ -1441,7 +1442,7 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, "mcp__phoenix-editor__execJsInLivePreview", "mcp__phoenix-editor__execJsInEditor", "mcp__phoenix-editor__editorPreferences", - "mcp__phoenix-editor__editorDocs"] + "mcp__phoenix-editor__editorDocs", "mcp__phoenix-editor__getProblems"] } }, mcpServers: { "phoenix-editor": editorMcpServer }, @@ -1556,6 +1557,9 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, "feature-docs URL and the GitHub source repo URL. Call once near the start of any " + "non-trivial editor-control task; then Read / Grep the apiDocsPath and WebFetch the " + "featureDocsURL as needed. Do NOT search the codebase blindly when this exists." + + "\n- getProblems: to get the problems in a file use this tool; it opens the file in the " + + "editor and gives you the errors the editor reports. Use it when the user points at a " + + "red squiggle or the Problems panel, and after your own edits to check for new errors." + "\n\nEDITS THAT LAND IN THE LIVE PREVIEW: when you edit the file getEditorState " + "reported as livePreviewFile — or a CSS / JS / SVG file it links to — the user is " + "watching the result render. Whether that is worth checking is your judgement call, " + diff --git a/src-node/mcp-editor-tools.js b/src-node/mcp-editor-tools.js index 3003f4e6d5..2131e7dce0 100644 --- a/src-node/mcp-editor-tools.js +++ b/src-node/mcp-editor-tools.js @@ -53,6 +53,7 @@ const EXEC_PEER_TIMEOUT_MS = { controlEditor: 5000, resizeLivePreview: 5000, searchEditorBuffers: 3000, + getProblems: 15000, searchImages: 55000, previewImages: 45000, useImage: 12000 @@ -693,6 +694,55 @@ function createEditorMcpServer(sdkModule, nodeConnector, clarificationAccessors) } ); + + const getProblemsTool = sdkModule.tool( + "getProblems", + "Get the problems for a file: it opens the file in the editor and returns the errors and " + + "warnings reported by the syntax checkers available for that file type, with 1-based line " + + "and column, type, message and the checker (provider) that found each. respondedProviders " + + "lists the checkers that answered; judge from that whether the coverage is enough for what " + + "the user asked. If no problem provider is set for the file type, the result says so. " + + "Defaults to the active file. Returns at most 20 problems by default; counts always cover " + + "the whole file, so use pattern, type or provider to narrow, or raise maxProblems. Use it " + + "when the user points at a red squiggle or the Problems panel, and after your own edits " + + "to check for new errors.", + { + filePath: z.string().optional().describe("Absolute path of the file. Default: the active editor file"), + pattern: z.string().optional().describe("Regex (default) or literal text the message must match"), + isRegex: z.boolean().optional().describe("false to match the pattern literally. Default true"), + caseSensitive: z.boolean().optional().describe("Default false"), + type: z.enum(["error", "warning", "meta"]).optional().describe("Only problems of this type"), + provider: z.string().optional().describe("Only problems from this linter; names come back in every result"), + maxProblems: z.number().int().optional().describe("Cap on returned problems. Default 20, max 200") + }, + async function (args) { + let toolResult; + try { + const result = await _execPeerWithTimeout(nodeConnector, "getProblems", args || {}, "getProblems"); + if (result && result.error) { + toolResult = { + content: [{ type: "text", text: "Error: " + result.error }], + isError: true + }; + } else { + toolResult = { + content: [{ type: "text", text: JSON.stringify(result) }] + }; + } + } catch (err) { + toolResult = { + content: [{ type: "text", text: "Error getting problems: " + err.message }], + isError: true + }; + } + return _maybeAppendHint(toolResult, hasClarification); + }, + { + annotations: { readOnlyHint: true }, + searchHint: "lint errors warnings diagnostics problems red squiggles in a file, what the Problems panel shows" + } + ); + const editorDocsTool = sdkModule.tool( "editorDocs", "Returns the locations of Phoenix Code's documentation. This tool DOES NOT fetch content " + @@ -783,7 +833,7 @@ function createEditorMcpServer(sdkModule, nodeConnector, clarificationAccessors) name: "phoenix-editor", tools: [getEditorStateTool, searchEditorBuffersTool, searchImagesTool, previewImagesTool, useImageTool, takeScreenshotTool, execJsInLivePreviewTool, - execJsInEditorTool, editorPreferencesTool, editorDocsTool, + execJsInEditorTool, editorPreferencesTool, editorDocsTool, getProblemsTool, controlEditorTool, resizeLivePreviewTool, waitTool, getUserClarificationTool] }); } diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index f92cb2bff9..0014eb8c54 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -2781,6 +2781,8 @@ define({ "AI_CHAT_TOOL_EDITOR_JS": "Inspecting editor", "AI_CHAT_TOOL_EDITOR_PREFERENCES": "Editor preferences", "AI_CHAT_TOOL_EDITOR_DOCS": "Editor docs", + "AI_CHAT_TOOL_PROBLEMS": "Problems", + "AI_CHAT_TOOL_PROBLEMS_IN": "Problems in {0}", "AI_CHAT_TOOL_RESIZE_PREVIEW": "Resize preview", "AI_LIVE_PREVIEW_BANNER_TEXT": "AI is inspecting the live preview", "AI_LIVE_PREVIEW_BANNER_RESIZE": "AI resized preview to {0}", From 571f3166f2c8332adf159a7deb56fcd93d7425b0 Mon Sep 17 00:00:00 2001 From: abose Date: Mon, 28 Sep 2026 18:05:08 +0530 Subject: [PATCH 04/12] feat(ai): add downloadPath to useImage and the viewer download strings useImage takes an optional downloadPath: a project folder, or for one photo an image file path. The description and the prompt guidance say to prefer embedding the Unsplash URLs and to download only when the user asks for local files or the use case needs them. The peer timeout allows for the download. Adds the strings for the viewer's download button, the download card and the download error cases, drops the unused request-failed string, and styles the viewer button. --- src-node/claude-code-agent.js | 5 +++-- src-node/mcp-editor-tools.js | 29 ++++++++++++++++++++--------- src/nls/root/strings.js | 12 +++++++++++- src/styles/Extn-AIImages.less | 1 + 4 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src-node/claude-code-agent.js b/src-node/claude-code-agent.js index 401308de44..7aef9cb5e2 100644 --- a/src-node/claude-code-agent.js +++ b/src-node/claude-code-agent.js @@ -1528,8 +1528,9 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, "query computed styles, click elements, or capture console output. Use it to debug " + "behavior and to confirm an edit actually took effect." + "\n- searchImages: find Unsplash photos for a website; includePreview=true returns a small " + - "numbered collage so you can choose visually. Reuse returned URLs and call useImage when selecting " + - "a photo for a page. Use searches judiciously, at most 100 per hour." + + "numbered collage so you can choose visually. Call useImage when selecting photos for a page and " + + "prefer embedding the returned Unsplash URLs; pass downloadPath only when the user asks for local " + + "files or the use case needs them. Use searches judiciously, at most 100 per hour." + "\n- previewImages: show actual images in the chat from existing URLs (including file:/// local " + "images). " + "Use it when presenting images or a shortlist to the user; no search is needed." + diff --git a/src-node/mcp-editor-tools.js b/src-node/mcp-editor-tools.js index 2131e7dce0..99b18b8825 100644 --- a/src-node/mcp-editor-tools.js +++ b/src-node/mcp-editor-tools.js @@ -56,7 +56,7 @@ const EXEC_PEER_TIMEOUT_MS = { getProblems: 15000, searchImages: 55000, previewImages: 45000, - useImage: 12000 + useImage: 90000 }; // Floor for caller-provided timeouts (e.g. execJsInLivePreview's @@ -217,8 +217,9 @@ function createEditorMcpServer(sdkModule, nodeConnector, clarificationAccessors) "Set includePreview:true to SEE a small numbered collage and choose the best visual match yourself; " + "each collage number matches the photo's number field and 1-based array position. Missing previews are listed. " + "The user can optionally reply with an image URL; do not wait for them to choose. " + - "When choosing photos for the page, call useImage once with their downloadTrackers as a list, use their supplied URLs, " + - "and credit the photographer and Unsplash with the returned links. Honor rate-limit errors and retryAfterSeconds.", + "When choosing photos for the page, call useImage once with their downloadTrackers as a list, prefer embedding " + + "their supplied URLs, and credit the photographer and Unsplash with the returned links. " + + "Honor rate-limit errors and retryAfterSeconds.", { query: z.string().min(1).max(200).describe("Specific image search query"), page: z.number().int().min(1).optional().describe("Results page, default 1"), @@ -278,12 +279,22 @@ function createEditorMcpServer(sdkModule, nodeConnector, clarificationAccessors) const useImageTool = sdkModule.tool( "useImage", - "Record Unsplash images chosen from searchImages before embedding their URLs. Prefer selecting multiple " + - "photos in one call by passing a list of downloadTrackers (up to nine). A single tracker is also accepted. " + - "Returns selected photos and any per-image failures; retry only failed trackers. " + - "Does not perform another search or edit any files.", - {downloadTracker: z.union([z.string(), z.array(z.string()).min(1).max(9)]) - .describe("One downloadTracker from searchImages, or an ordered list of up to nine downloadTrackers")}, + "Select Unsplash photos from searchImages. Prefer the Unsplash URLs: without downloadPath the photos are " + + "shown as selected in the chat and you embed their URLs directly; nothing is downloaded. Pass downloadPath " + + "only when the user asks for local files or the use case needs them (offline pages, a build that bundles " + + "assets, an image that must be edited): the photos are then downloaded into the project and each returned " + + "photo has savedPath (absolute) and projectPath (project-relative, for src attributes). Prefer selecting " + + "multiple photos in one call by passing a list of downloadTrackers (up to nine). A single tracker is also " + + "accepted. Returns selected photos and any per-image failures; retry only failed trackers. " + + "Does not perform another search or edit existing files.", + { + downloadTracker: z.union([z.string(), z.array(z.string()).min(1).max(9)]) + .describe("One downloadTracker from searchImages, or an ordered list of up to nine downloadTrackers"), + downloadPath: z.string().optional() + .describe("Download into the project instead of embedding by URL: a project folder such as " + + "images/, or for a single photo a file path such as images/hero.jpg (jpg, png, webp or avif). " + + "Omit it to embed the Unsplash URLs.") + }, async function (args) { try { const result = await _execPeerWithTimeout(nodeConnector, "useImage", args, "useImage"); diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 0014eb8c54..1c1ea20cf6 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -2918,6 +2918,14 @@ define({ "AI_CHAT_IMAGE_REMOVE": "Remove image", "AI_CHAT_IMAGE_SEARCH": "Searching images", "AI_CHAT_IMAGE_USE": "Selecting image", + "AI_CHAT_IMAGE_DOWNLOADING": "Downloading image", + "AI_CHAT_IMAGE_DOWNLOADED": "Downloaded image", + "AI_CHAT_IMAGE_DOWNLOADED_GROUP": "Downloaded images", + "AI_CHAT_IMAGE_NO_PROJECT": "No project is open to download into.", + "AI_CHAT_IMAGE_DOWNLOAD_PATH_INVALID": "downloadPath must be a folder inside the project, or an image file path there ending in .jpg, .png, .webp or .avif.", + "AI_CHAT_IMAGE_DOWNLOAD_PATH_FOLDER": "downloadPath must be a folder when downloading several photos.", + "AI_CHAT_IMAGE_NOT_HOTLINKABLE": "This photo cannot be embedded by URL. Download it by passing downloadPath.", + "AI_CHAT_IMAGE_UNKNOWN_DOWNLOAD": "Only photos returned by searchImages in this conversation can be downloaded.", "AI_CHAT_IMAGE_SELECTED": "Selected image", "AI_CHAT_IMAGE_SELECTED_GROUP": "Selected images", "AI_CHAT_IMAGE_PREVIEW": "Image preview", @@ -2931,6 +2939,9 @@ define({ "AI_CHAT_IMAGE_PHOTO_BY": "Photo by", "AI_CHAT_IMAGE_REPLY": "Reply with this image", "AI_CHAT_IMAGE_QUOTE": "Quote image in chat", + "AI_CHAT_IMAGE_DOWNLOAD": "Download to project and open", + "AI_CHAT_IMAGE_DOWNLOAD_FAILED_TITLE": "Image download failed", + "AI_CHAT_IMAGE_DOWNLOAD_FAILED": "The image could not be saved to the project: {0}", "AI_CHAT_IMAGE_VIEW_COLLAGE": "View collage", "AI_CHAT_IMAGE_SHOW_ALL": "Show all", "AI_CHAT_IMAGE_BACK_TO_ALL": "Back to all images", @@ -2945,7 +2956,6 @@ define({ "AI_CHAT_IMAGE_GUEST_LIMIT": "Your image search allowance has been used. Sign in to continue searching.", "AI_CHAT_IMAGE_SERVER_LIMIT": "Image search is temporarily rate limited. Wait before searching again.", "AI_CHAT_IMAGE_TIMEOUT": "Image search timed out. Try again later.", - "AI_CHAT_IMAGE_REQUEST_FAILED": "The image service returned an error ({0}).", "AI_CHAT_ATTACH_FILE": "Attach files", "AI_CHAT_ATTACH_TITLE": "Attach file or folder", "AI_CHAT_ATTACH_FILE_OPTION": "Attach a file", diff --git a/src/styles/Extn-AIImages.less b/src/styles/Extn-AIImages.less index 6506fe2eae..2cb6c9d5e2 100644 --- a/src/styles/Extn-AIImages.less +++ b/src/styles/Extn-AIImages.less @@ -142,6 +142,7 @@ .ai-image-viewer-close { width: 30px; height: 30px; } .ai-image-viewer-overview { width: 30px; height: 30px; color: @project-panel-text-2; } .ai-image-viewer-reply { width: 30px; height: 30px; color: @project-panel-text-2; } + .ai-image-viewer-download { width: 30px; height: 30px; color: @project-panel-text-2; } .ai-image-viewer-reply-overlay { position: absolute; height: 27px; From 378766b3c05342ab36f159162e062d05374199a1 Mon Sep 17 00:00:00 2001 From: abose Date: Mon, 28 Sep 2026 19:16:52 +0530 Subject: [PATCH 05/12] feat(notifications): hold auto-close while the mouse is over a notification One helper drives every auto-closing notification: the countdown stops on mouseenter and restarts in full on mouseleave, so a user reading a toast or reaching for one of its actions does not lose it. Applies to the toasts, the anchored notifications, the inline toasts and the HUD. Adds mock-clock specs for the toast, the HUD and the inline toast that observe the timed close on the notification rather than on the CSS transition. --- src/widgets/NotificationUI.js | 48 ++++++++++++++++++---- test/spec/NotificationUI-test.js | 69 +++++++++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 9 deletions(-) diff --git a/src/widgets/NotificationUI.js b/src/widgets/NotificationUI.js index 121aa45024..007a1cc71c 100644 --- a/src/widgets/NotificationUI.js +++ b/src/widgets/NotificationUI.js @@ -110,6 +110,34 @@ define(function (require, exports, module) { * @type {Object} */ + /** + * Close after the given seconds, holding off while the mouse is over the notification so a user + * reading it or reaching for one of its actions does not lose it. The countdown restarts in + * full once the mouse leaves. + * @param {jQuery} $notification - The notification element. + * @param {number} seconds - Time to wait before closing. + * @param {function} closeFn - Closes the notification with the timeout reason. + * @private + */ + function _autoCloseUnlessHovered($notification, seconds, closeFn) { + let timer = null; + function stop() { + if (timer) { + clearTimeout(timer); + timer = null; + } + } + function start() { + stop(); + timer = setTimeout(function () { + timer = null; + closeFn(); + }, seconds * 1000); + } + $notification.on("mouseenter", stop).on("mouseleave", start); + start(); + } + /** * @constructor * @private @@ -211,6 +239,7 @@ define(function (require, exports, module) { * * `allowedPlacements` - Optional String array with values restricting where the notification will be shown. * Values can be a mix of `['top', 'bottom', 'left', 'right']` * * `autoCloseTimeS` - Time in seconds after which the notification should be auto closed. Default is never. + * The countdown pauses while the mouse is over the notification. * * `dismissOnClick` - when clicked, the notification is closed. Default is true(dismiss). * * `toastStyle` - To style the toast notification for error, warning, info etc. Can be * one of `NotificationUI.NOTIFICATION_STYLES_CSS_CLASS.*` or your own css class name. @@ -229,9 +258,9 @@ define(function (require, exports, module) { let notification = (new Notification($tooltip, NOTIFICATION_TYPE_ARROW)); if(options.autoCloseTimeS){ - setTimeout(()=>{ + _autoCloseUnlessHovered($tooltip, options.autoCloseTimeS, function () { notification.close(CLOSE_REASON.TIMEOUT); - }, options.autoCloseTimeS * 1000); + }); } if(options.dismissOnClick){ @@ -352,6 +381,7 @@ define(function (require, exports, module) { * @param {{dismissOnClick, autoCloseTimeS, toastStyle, instantOpen}} [options] optional, supported * * options are: * * `autoCloseTimeS` - Time in seconds after which the notification should be auto closed. Default is never. + * The countdown pauses while the mouse is over the notification. * * `dismissOnClick` - when clicked, the notification is closed. Default is true(dismiss). * * `toastStyle` - To style the toast notification for error, warning, info etc. Can be * one of `NotificationUI.NOTIFICATION_STYLES_CSS_CLASS.*` or your own css class name. @@ -387,9 +417,9 @@ define(function (require, exports, module) { }, 0); if(options.autoCloseTimeS){ - setTimeout(()=>{ + _autoCloseUnlessHovered($NotificationPopup, options.autoCloseTimeS, function () { notification.close(CLOSE_REASON.TIMEOUT); - }, options.autoCloseTimeS * 1000); + }); } if(options.dismissOnClick){ @@ -416,6 +446,7 @@ define(function (require, exports, module) { * @param {string|Element} template HTML string or DOM Element for the toast content. * @param {Object} [options] optional, supported options: * * `autoCloseTimeS` - Time in seconds after which the toast auto-closes. Default is 5. + * The countdown pauses while the mouse is over the toast. * * `dismissOnClick` - If true, clicking the toast dismisses it. Default is true. * @return {Notification} Object with a done handler that resolves when the toast closes. * @type {function} @@ -466,11 +497,11 @@ define(function (require, exports, module) { }; if (autoCloseTimeS) { - setTimeout(function () { + _autoCloseUnlessHovered($toast, autoCloseTimeS, function () { if (notification.$notification) { notification.close(CLOSE_REASON.TIMEOUT); } - }, autoCloseTimeS * 1000); + }); } if (dismissOnClick) { @@ -497,6 +528,7 @@ define(function (require, exports, module) { * @param {string} label Text to display below the icon (e.g. "110%"). * @param {Object} [options] optional, supported options: * * `autoCloseTimeS` - Time in seconds after which the HUD auto-closes. Default is 1. + * The countdown pauses while the mouse is over the HUD. * @return {Notification} Object with a done handler that resolves when the HUD closes. * @type {function} */ @@ -531,11 +563,11 @@ define(function (require, exports, module) { }; if (autoCloseTimeS) { - setTimeout(function () { + _autoCloseUnlessHovered($hud, autoCloseTimeS, function () { if (notification.$notification) { notification.close(CLOSE_REASON.TIMEOUT); } - }, autoCloseTimeS * 1000); + }); } return notification; diff --git a/test/spec/NotificationUI-test.js b/test/spec/NotificationUI-test.js index 6cbbd63729..8008250854 100644 --- a/test/spec/NotificationUI-test.js +++ b/test/spec/NotificationUI-test.js @@ -19,7 +19,7 @@ * */ -/*global describe, it, expect, beforeAll, afterAll, awaitsFor, awaitsForDone, awaits */ +/*global describe, it, expect, beforeAll, afterAll, awaitsFor, awaitsForDone, awaits, jasmine, spyOn */ define(function (require, exports, module) { let NotificationUI = require("widgets/NotificationUI"); @@ -106,6 +106,50 @@ define(function (require, exports, module) { }, "waiting for notification to close"); } + it("Should hold the auto close while the mouse is over the toast", function () { + // Mock time from the start so the auto-close timer itself is under the clock. The close + // itself is observed on the notification, since the removal waits for a CSS transition. + jasmine.clock().install(); + try { + const notification = NotificationUI.createToastFromTemplate("hello", "world", {autoCloseTimeS: 1}); + spyOn(notification, "close").and.callThrough(); + const $popup = $("#toast-notification-container").children().last(); + expect($popup.length).toBe(1); + $popup.trigger("mouseenter"); + jasmine.clock().tick(1500); + expect($popup[0].isConnected).toBe(true); + expect(notification.close).not.toHaveBeenCalled(); + $popup.trigger("mouseleave"); + jasmine.clock().tick(900); + expect(notification.close).not.toHaveBeenCalled(); + jasmine.clock().tick(200); + expect(notification.close).toHaveBeenCalledWith(NotificationUI.CLOSE_REASON.TIMEOUT); + } finally { + jasmine.clock().uninstall(); + } + }); + + it("Should hold the auto close while the mouse is over the HUD", function () { + jasmine.clock().install(); + let closeReason; + try { + const notification = NotificationUI.showHUD("fa-solid fa-magnifying-glass-plus", "110%"); + notification.done(function (reason) { closeReason = reason; }); + const $hud = $("body > .hud-overlay"); + expect($hud.length).toBe(1); + $hud.trigger("mouseenter"); + jasmine.clock().tick(1500); + expect($hud[0].isConnected).toBe(true); + expect(closeReason).toBeUndefined(); + $hud.trigger("mouseleave"); + jasmine.clock().tick(1100); + expect(closeReason).toBe(NotificationUI.CLOSE_REASON.TIMEOUT); + expect($("body > .hud-overlay").length).toBe(0); + } finally { + jasmine.clock().uninstall(); + } + }); + it("Should style toast notification", async function () { await verifyToast(NotificationUI.NOTIFICATION_STYLES_CSS_CLASS.INFO); await verifyToast(NotificationUI.NOTIFICATION_STYLES_CSS_CLASS.WARNING); @@ -152,6 +196,29 @@ define(function (require, exports, module) { }, "waiting for inline toast to auto-close", 3000); }); + it("Should hold the auto close while the mouse is over the inline toast", function () { + jasmine.clock().install(); + let closeReason; + try { + const notification = NotificationUI.showToastOn($container[0], "hover me", {autoCloseTimeS: 1}); + notification.done(function (reason) { closeReason = reason; }); + const $toast = $container.find(".inline-toast"); + expect($toast.length).toBe(1); + $toast.trigger("mouseenter"); + jasmine.clock().tick(1500); + expect($toast[0].isConnected).toBe(true); + expect(closeReason).toBeUndefined(); + $toast.trigger("mouseleave"); + jasmine.clock().tick(1100); + // The close falls back to a timer when no transition event arrives. + jasmine.clock().tick(600); + expect(closeReason).toBe(NotificationUI.CLOSE_REASON.TIMEOUT); + expect($container.find(".inline-toast").length).toBe(0); + } finally { + jasmine.clock().uninstall(); + } + }); + it("Should dismiss on click by default", async function () { NotificationUI.showToastOn($container[0], "Click me"); await awaitsFor(function () { From 82faaa9c00e7b5edbfc2a7c6d64738df2633b54d Mon Sep 17 00:00:00 2001 From: abose Date: Mon, 28 Sep 2026 19:17:17 +0530 Subject: [PATCH 06/12] feat(files): reusable reveal in file tree that waits for a new file Extracts the "Show in File Tree" command body into an exported revealInFileTree(file): show the sidebar, switch to the Files tab, select the file and scroll it into view. When the tree does not have the node yet, as for a file just written outside the FileSystem API, it refreshes the tree and looks for the node for up to three seconds before selecting. ProjectManager gains isInFileTree(path) for that check. Adds the strings and styles for the image gallery's download toast and the search card's footer toggle. --- src/document/DocumentCommandHandlers.js | 62 +++++++++++++++++----- src/nls/root/strings.js | 4 ++ src/project/ProjectManager.js | 16 ++++++ src/styles/Extn-AIImages.less | 6 ++- src/styles/brackets_patterns_override.less | 9 ++++ 5 files changed, 81 insertions(+), 16 deletions(-) diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js index 3a418a63e9..0f303e98ef 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -2002,23 +2002,56 @@ define(function (require, exports, module) { } /** Show in File Tree command handler **/ + /** + * Reveal a file in the file tree the way the "Show in File Tree" command does: show the sidebar + * if it is hidden, switch it to the Files tab, select the file and scroll its row into view. + * @param {File} file - The file to reveal. Silently no-ops outside the project. + */ + function revealInFileTree(file) { + if (!SidebarView.isVisible()) { + CommandManager.execute(Commands.VIEW_HIDE_SIDEBAR); + } + SidebarTabs.setActiveTab(SidebarTabs.SIDEBAR_TAB_FILES); + ProjectManager.showInTree(file).always(function () { + if (ProjectManager.isInFileTree(file.fullPath) || !ProjectManager.isWithinProject(file.fullPath)) { + _scrollFileTreeSelectionIntoView(); + return; + } + // A file created moments ago outside the FileSystem API is not in the tree yet, so the + // selection was dropped: reload the tree and select the file once it arrives. The tree + // has no event for that (its change event fires before the directory reload lands), + // so look for the node a few times over the next three seconds. + const RETRY_MS = 250, MAX_RETRIES = 12; + let retries = 0; + function selectWhenInTree() { + if (ProjectManager.isInFileTree(file.fullPath)) { + ProjectManager.showInTree(file).always(_scrollFileTreeSelectionIntoView); + } else if (++retries < MAX_RETRIES) { + setTimeout(selectWhenInTree, RETRY_MS); + } + } + ProjectManager.refreshFileTree(); + setTimeout(selectWhenInTree, RETRY_MS); + }); + } + + /** + * FileTreeView only auto-scrolls when the selection flips unselected→selected. + * Re-invoking the command on an already-selected file would otherwise be a + * no-op when the user has scrolled away — force-scroll the selected node + * into view so "Show in File Tree" always reveals the row. + */ + function _scrollFileTreeSelectionIntoView() { + const $selected = $("#project-files-container .selected-node").first(); + if ($selected.length) { + ViewUtils.scrollElementIntoView($("#project-files-container"), $selected, true); + } + } + function handleShowInTree() { let activeFile = MainViewManager.getCurrentlyViewedFile(MainViewManager.ACTIVE_PANE); if(activeFile){ - if (!SidebarView.isVisible()) { - CommandManager.execute(Commands.VIEW_HIDE_SIDEBAR); - } - SidebarTabs.setActiveTab(SidebarTabs.SIDEBAR_TAB_FILES); - // FileTreeView only auto-scrolls when the selection flips unselected→selected. - // Re-invoking the command on an already-selected file would otherwise be a - // no-op when the user has scrolled away — force-scroll the selected node - // into view so "Show in File Tree" always reveals the row. - ProjectManager.showInTree(activeFile).always(function () { - const $selected = $("#project-files-container .selected-node").first(); - if ($selected.length) { - ViewUtils.scrollElementIntoView($("#project-files-container"), $selected, true); - } - }); + revealInFileTree(activeFile); } } @@ -2452,6 +2485,7 @@ define(function (require, exports, module) { // Define public API exports.showFileOpenError = showFileOpenError; + exports.revealInFileTree = revealInFileTree; exports.APP_QUIT_CANCELLED = APP_QUIT_CANCELLED; diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 1c1ea20cf6..f2b2ffcd1e 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -763,6 +763,9 @@ define({ "LIVE_DEV_IMAGE_FOLDER_DIALOG_PLACEHOLDER": "Type folder path (e.g., assets/images/)", "LIVE_DEV_IMAGE_FOLDER_DIALOG_HELP": "💡 Type folder path or leave empty to download in 'images' folder.", "LIVE_DEV_IMAGE_FOLDER_DIALOG_REMEMBER": "Don't ask again for this project", + "LIVE_DEV_IMAGE_DOWNLOADED_TITLE": "Image downloaded", + "LIVE_DEV_IMAGE_SAVED_TO": "Saved to {0}", + "LIVE_DEV_IMAGE_DOWNLOAD_FOLDER": "Change download folder", "AVAILABLE_IN_PRO_TITLE": "Available in Phoenix Pro", "DEVICE_SIZE_LIMIT_MESSAGE": "To switch between mobile, tablet, and desktop views, upgrade to Phoenix Pro.", "LAYERS_PANEL_LIMIT_MESSAGE": "To see your page structure and select or edit any element in it, upgrade to Phoenix Pro.", @@ -2944,6 +2947,7 @@ define({ "AI_CHAT_IMAGE_DOWNLOAD_FAILED": "The image could not be saved to the project: {0}", "AI_CHAT_IMAGE_VIEW_COLLAGE": "View collage", "AI_CHAT_IMAGE_SHOW_ALL": "Show all", + "AI_CHAT_IMAGE_SHOW_LESS": "Show less", "AI_CHAT_IMAGE_BACK_TO_ALL": "Back to all images", "AI_CHAT_IMAGE_RESULTS_COUNT": "{0} photos · Unsplash", "AI_CHAT_IMAGE_RESULTS_SINGLE": "1 photo · Unsplash", diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index 4f271bc071..093ac700ff 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -1436,6 +1436,21 @@ define(function (require, exports, module) { return model.showInTree(entry).then(_saveTreeState); } + /** + * Whether the file tree currently has a node for the path. A file written outside the + * FileSystem API is not in the tree until the watcher or a refresh adds it, and a file in a + * folder that was never expanded is not in it either; `showInTree` expands the folders first. + * + * @param {string} fullPath Absolute path of a file or folder + * @return {boolean} true if the tree has the node + */ + function isInFileTree(fullPath) { + if (!isWithinProject(fullPath)) { + return false; + } + return model._viewModel.isFilePathVisible(makeProjectRelativeIfPossible(fullPath)) !== null; + } + function _filePickerSupported() { return Phoenix.isNativeApp || window.showOpenFilePicker; // fs access file picker @@ -2450,6 +2465,7 @@ define(function (require, exports, module) { exports.moveToTrash = moveToTrash; exports.forceFinishRename = forceFinishRename; exports.showInTree = showInTree; + exports.isInFileTree = isInFileTree; exports.shouldShowFileNameInTree = ProjectModel._shouldShowName; exports.refreshFileTree = refreshFileTree; exports.getAllFiles = getAllFiles; diff --git a/src/styles/Extn-AIImages.less b/src/styles/Extn-AIImages.less index 2cb6c9d5e2..c2f8274e53 100644 --- a/src/styles/Extn-AIImages.less +++ b/src/styles/Extn-AIImages.less @@ -37,9 +37,8 @@ .ai-image-search-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 4px; padding: 0 10px; } &.ai-image-search-compact { .ai-image-search-item:nth-child(n+4), .ai-image-search-collage { display: none; } - .ai-image-search-show-all { display: flex; } } - .ai-image-search-show-all { display: none; border: 0; background: transparent; color: inherit; padding: 5px 0; } + .ai-image-search-show-all { display: flex; border: 0; background: transparent; color: inherit; padding: 5px 0; } .ai-image-search-show-all:hover { color: @project-panel-text-1; } .ai-image-search-item { position: relative; min-width: 0; } &.ai-image-preview-card .ai-image-search-grid[data-count="1"] { @@ -111,6 +110,9 @@ .ai-image-search-item:hover .ai-image-search-number, .ai-image-search-item:focus-within .ai-image-search-number { opacity: 1; } .ai-image-search-footer { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 9px 12px; font-size: @ai-text-meta; color: @project-panel-text-2; } + // The whole footer toggles a large result set, like the heading does. + .ai-image-search-footer.ai-image-search-toggle { cursor: pointer; } + .ai-image-search-footer-actions { display: flex; align-items: center; gap: 14px; } .ai-image-search-collage { display: flex; align-items: center; gap: 6px; border: 0; background: transparent; color: inherit; padding: 5px 0; } .ai-image-search-collage:hover { color: @project-panel-text-1; } .ai-image-search-status { padding: 12px; font-size: @ai-text-secondary; color: @project-panel-text-2; } diff --git a/src/styles/brackets_patterns_override.less b/src/styles/brackets_patterns_override.less index 106f3deae5..f293063952 100644 --- a/src/styles/brackets_patterns_override.less +++ b/src/styles/brackets_patterns_override.less @@ -2804,3 +2804,12 @@ code { .striked { text-decoration: line-through; } + +// Toast shown after an image gallery download that went to the remembered folder without asking. +.image-download-toast { + .image-download-toast-actions { display: flex; align-items: center; gap: 12px; margin-top: 8px; } + button { display: flex; align-items: center; justify-content: center; border: 0; border-radius: 4px; background: transparent; color: inherit; font: inherit; cursor: pointer; padding: 3px 0; } + button:hover { text-decoration: underline; } + .image-download-toast-settings { width: 26px; height: 26px; margin-left: auto; } + .image-download-toast-settings:hover { text-decoration: none; background: rgba(128, 128, 128, 0.2); } +} From d8375582c4cb9f50821612326bc819edde397e3e Mon Sep 17 00:00:00 2001 From: abose Date: Mon, 28 Sep 2026 20:12:51 +0530 Subject: [PATCH 07/12] feat(ai): add notifyUser MCP tool Registers a deferred, read-only notifyUser tool that calls the new browser peer. The description tells the model to use it only when the user may not be watching the chat, that it is skipped while the AI panel is visible unless alwaysShow is set, and that autoCloseS controls how long the toast stays. Allows the tool for the main agent, adds a line of prompt guidance, the card string and the toast style. --- src-node/claude-code-agent.js | 4 +++ src-node/mcp-editor-tools.js | 51 +++++++++++++++++++++++++++++++- src/nls/root/strings.js | 1 + src/styles/Extn-AIChatPanel.less | 6 ++++ 4 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src-node/claude-code-agent.js b/src-node/claude-code-agent.js index 7aef9cb5e2..1b4b6c61d5 100644 --- a/src-node/claude-code-agent.js +++ b/src-node/claude-code-agent.js @@ -1406,6 +1406,7 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, // prompt — the PreToolUse hook below allows them outright. "mcp__phoenix-editor__editorDocs", "mcp__phoenix-editor__getProblems", + "mcp__phoenix-editor__notifyUser", "mcp__phoenix-editor__controlEditor", "mcp__phoenix-editor__resizeLivePreview", "mcp__phoenix-editor__wait", @@ -1561,6 +1562,9 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, "\n- getProblems: to get the problems in a file use this tool; it opens the file in the " + "editor and gives you the errors the editor reports. Use it when the user points at a " + "red squiggle or the Problems panel, and after your own edits to check for new errors." + + "\n- notifyUser: show a toast in the editor window when a long task finishes or you need the " + + "user's attention and they may be away from the chat. Never use it for ordinary replies. " + + "It is skipped while the AI panel is visible unless you pass alwaysShow." + "\n\nEDITS THAT LAND IN THE LIVE PREVIEW: when you edit the file getEditorState " + "reported as livePreviewFile — or a CSS / JS / SVG file it links to — the user is " + "watching the result render. Whether that is worth checking is your judgement call, " + diff --git a/src-node/mcp-editor-tools.js b/src-node/mcp-editor-tools.js index 99b18b8825..c6139c7db3 100644 --- a/src-node/mcp-editor-tools.js +++ b/src-node/mcp-editor-tools.js @@ -54,6 +54,7 @@ const EXEC_PEER_TIMEOUT_MS = { resizeLivePreview: 5000, searchEditorBuffers: 3000, getProblems: 15000, + notifyUser: 5000, searchImages: 55000, previewImages: 45000, useImage: 90000 @@ -754,6 +755,54 @@ function createEditorMcpServer(sdkModule, nodeConnector, clarificationAccessors) } ); + const notifyUserTool = sdkModule.tool( + "notifyUser", + "Show a short notification toast in the editor window, outside the chat. Use it only when the " + + "user may not be watching the chat: a long task has finished, or something needs their attention " + + "before you can continue. Do not use it for ordinary replies; the chat already shows those. " + + "When the AI panel is visible no toast is shown and the result says so, since the user already " + + "sees the chat; pass alwaysShow: true if a toast is still wanted. Clicking the toast brings the " + + "user to the chat. By default errors stay until dismissed and other kinds close after a few " + + "seconds; autoCloseS overrides that.", + { + title: z.string().min(1).max(80).describe("Short heading, up to 80 characters"), + message: z.string().max(500).optional() + .describe("One or two plain-text sentences, up to 500 characters; newlines are kept"), + kind: z.enum(["info", "success", "warning", "error"]).optional().describe("Default info"), + alwaysShow: z.boolean().optional() + .describe("Show the toast even when the AI panel is visible. Default false"), + autoCloseS: z.number().int().min(0).max(300).optional() + .describe("Seconds before the toast closes on its own, 3 to 300; 0 keeps it until the user " + + "dismisses it. Default 12, 20 for warning, 0 for error") + }, + async function (args) { + let toolResult; + try { + const result = await _execPeerWithTimeout(nodeConnector, "notifyUser", args || {}, "notifyUser"); + if (result && result.error) { + toolResult = { + content: [{ type: "text", text: "Error: " + result.error }], + isError: true + }; + } else { + toolResult = { + content: [{ type: "text", text: JSON.stringify(result) }] + }; + } + } catch (err) { + toolResult = { + content: [{ type: "text", text: "Error notifying the user: " + err.message }], + isError: true + }; + } + return _maybeAppendHint(toolResult, hasClarification); + }, + { + annotations: { readOnlyHint: true }, + searchHint: "notify alert the user with a toast notification when a long task finishes or needs attention" + } + ); + const editorDocsTool = sdkModule.tool( "editorDocs", "Returns the locations of Phoenix Code's documentation. This tool DOES NOT fetch content " + @@ -844,7 +893,7 @@ function createEditorMcpServer(sdkModule, nodeConnector, clarificationAccessors) name: "phoenix-editor", tools: [getEditorStateTool, searchEditorBuffersTool, searchImagesTool, previewImagesTool, useImageTool, takeScreenshotTool, execJsInLivePreviewTool, - execJsInEditorTool, editorPreferencesTool, editorDocsTool, getProblemsTool, + execJsInEditorTool, editorPreferencesTool, editorDocsTool, getProblemsTool, notifyUserTool, controlEditorTool, resizeLivePreviewTool, waitTool, getUserClarificationTool] }); } diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index f2b2ffcd1e..21fa2b9e42 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -2785,6 +2785,7 @@ define({ "AI_CHAT_TOOL_EDITOR_PREFERENCES": "Editor preferences", "AI_CHAT_TOOL_EDITOR_DOCS": "Editor docs", "AI_CHAT_TOOL_PROBLEMS": "Problems", + "AI_CHAT_TOOL_NOTIFY": "Notification", "AI_CHAT_TOOL_PROBLEMS_IN": "Problems in {0}", "AI_CHAT_TOOL_RESIZE_PREVIEW": "Resize preview", "AI_LIVE_PREVIEW_BANNER_TEXT": "AI is inspecting the live preview", diff --git a/src/styles/Extn-AIChatPanel.less b/src/styles/Extn-AIChatPanel.less index fd261d08cf..c7cebf70e7 100644 --- a/src/styles/Extn-AIChatPanel.less +++ b/src/styles/Extn-AIChatPanel.less @@ -5873,3 +5873,9 @@ font-size: @ai-dialog-note; line-height: 28px; } + +// Toast the AI raises through notifyUser; the message is plain text with its line breaks kept. +.ai-notify-toast { + cursor: pointer; + .ai-notify-message { white-space: pre-line; } +} From 512d3656df3968c2cb31172a30ccb0c1496f517f Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 29 Sep 2026 19:31:07 +0530 Subject: [PATCH 08/12] feat(ai): askInLivePreview tool with a standard card and a free scratch folder Add the askInLivePreview MCP tool: the model writes only a body (uiFile, scriptFile, params) and Phoenix draws the frame in the live preview. The description states the contract in full: the script file runs as function(root, phoenix, params) { }, anchor and highlight for one element, placement out of the way by default, a theme for the frame, hover previews and click answers. Writes and edits into the model's UI folder under app data are allowed without a prompt, skipped by the snapshot and edit events, and the folder is recreated before a write. The guidance points the model at that folder and no longer claims the editor state lists its files. Strings for the card title, focus toggle, text field and the chat labels. --- src-node/claude-code-agent.js | 50 ++++++++++++++++++- src-node/mcp-editor-tools.js | 86 ++++++++++++++++++++++++++++++++ src/nls/root/strings.js | 11 ++++ src/styles/Extn-AIChatPanel.less | 6 +++ 4 files changed, 151 insertions(+), 2 deletions(-) diff --git a/src-node/claude-code-agent.js b/src-node/claude-code-agent.js index 1b4b6c61d5..ebc3d1f310 100644 --- a/src-node/claude-code-agent.js +++ b/src-node/claude-code-agent.js @@ -820,6 +820,19 @@ exports.getCliSpawnProfile = async function (params) { return Object.assign({}, result, profile); }; +// The assistant's own UI files for askInLivePreview live in Phoenix's app data, never in the +// project. Writing there is the assistant's business: no permission card, no snapshot, no +// edit event, no card in the chat. +let _aiScratchDir = null; +function _isAiScratchPath(filePath) { + if (!_aiScratchDir || !filePath || !path.isAbsolute(filePath)) { + return false; + } + const root = path.resolve(_aiScratchDir); + const target = path.resolve(filePath); + return target === root || target.startsWith(root + path.sep); +} + /** * Send a prompt to Claude and stream results back to the browser. * Called from browser via execPeer("sendPrompt", {prompt, projectPath, sessionAction, model}). @@ -829,7 +842,10 @@ exports.getCliSpawnProfile = async function (params) { */ exports.sendPrompt = async function (params) { const { prompt, projectPath, sessionAction, model, locale, selectionContext, editorContext, - images, envOverrides, permissionMode, additionalDirectories } = params; + images, envOverrides, permissionMode, additionalDirectories, aiScratchDir } = params; + if (typeof aiScratchDir === "string" && aiScratchDir) { + _aiScratchDir = aiScratchDir; + } const requestId = Date.now().toString(36) + Math.random().toString(36).slice(2, 7); // Handle session @@ -1188,7 +1204,7 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, // `mkdir -p notes-app` in the project, the model wrote the files to // /home//notes-app and nobody was asked. function _isOutsideWriteRoots(filePath) { - if (!filePath || !path.isAbsolute(filePath)) { + if (!filePath || !path.isAbsolute(filePath) || _isAiScratchPath(filePath)) { return false; } const target = path.resolve(filePath); @@ -1407,6 +1423,7 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, "mcp__phoenix-editor__editorDocs", "mcp__phoenix-editor__getProblems", "mcp__phoenix-editor__notifyUser", + "mcp__phoenix-editor__askInLivePreview", "mcp__phoenix-editor__controlEditor", "mcp__phoenix-editor__resizeLivePreview", "mcp__phoenix-editor__wait", @@ -1565,6 +1582,19 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, "\n- notifyUser: show a toast in the editor window when a long task finishes or you need the " + "user's attention and they may be away from the chat. Never use it for ordinary replies. " + "It is skipped while the AI panel is visible unless you pass alwaysShow." + + "\n- askInLivePreview: whenever showing beats telling and the page is in the live preview, " + + "compose UI with askInLivePreview instead of prose: a choice about one element (its colour, " + + "copy, placement), a choice about the whole page (theme, palette, typography, layout direction, " + + "which of several designs to keep), or simply to present something visually (a mockup, a " + + "before-and-after, a set of variants) even when the only answer is OK or a comment. The card's " + + "frame, title, controls and text field are Phoenix's; you write the body, and pass a theme with " + + "the page's colours so the frame matches. For one element pass anchor so the page dims around it " + + "and the card keeps out of its way; put the card beside the element only when that helps. Hovering an " + + "option previews it on the page with previewCss or previewHtml, clicking it answers. " + + "Write the UI files into the folder getEditorState reports as askInLivePreviewUiDir (yours to " + + "write freely, no permission is asked and the user is not shown those writes). " + + "The tool description is the whole contract; never look for its implementation, " + + "and keep the look-at-the-page step to one screenshot or one execJsInLivePreview." + "\n\nEDITS THAT LAND IN THE LIVE PREVIEW: when you edit the file getEditorState " + "reported as livePreviewFile — or a CSS / JS / SVG file it links to — the user is " + "watching the result render. Whether that is worth checking is your judgement call, " + @@ -1624,6 +1654,9 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, matcher: "Edit", hooks: [ async (input) => { + if (_isAiScratchPath(input && input.tool_input && input.tool_input.file_path)) { + return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" } }; + } console.log("[Phoenix AI] Intercepted Edit tool"); // Plan file edits: capture content, write to disk, skip editor const editPath = (input.tool_input.file_path || "").replace(/\\/g, "/"); @@ -1768,6 +1801,13 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, hooks: [ async (input) => { console.log("[Phoenix AI] Intercepted Write tool"); + if (_isAiScratchPath(input && input.tool_input && input.tool_input.file_path)) { + // The daily sweep may have removed the folder; it comes back for the write. + try { + fs.mkdirSync(path.dirname(input.tool_input.file_path), { recursive: true }); + } catch (err) { /* the write reports it */ } + return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" } }; + } // Capture plan content when writing to .claude/plans/ // Plan files: capture content for plan card, write to disk // but don't open in editor @@ -2023,6 +2063,9 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, if (filePath.replace(/\\/g, "/").includes("/.claude/plans/")) { return {}; } + if (_isAiScratchPath(filePath)) { + return {}; + } // If the SDK's native Edit itself failed (e.g. // oldText not found on disk), don't paint a diff // card. The existing aiToolResult flow will @@ -2090,6 +2133,9 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, if (filePath.replace(/\\/g, "/").includes("/.claude/plans/")) { return {}; } + if (_isAiScratchPath(filePath)) { + return {}; + } if (_isToolResponseError(input.tool_response)) { return {}; } diff --git a/src-node/mcp-editor-tools.js b/src-node/mcp-editor-tools.js index c6139c7db3..5e7cd246c1 100644 --- a/src-node/mcp-editor-tools.js +++ b/src-node/mcp-editor-tools.js @@ -755,6 +755,91 @@ function createEditorMcpServer(sdkModule, nodeConnector, clarificationAccessors) } ); + const askInLivePreviewTool = sdkModule.tool( + "askInLivePreview", + "Show a question card over the page in the live preview and wait for the user's answer. Use it whenever " + + "showing beats telling: a choice about one element or about the whole page, or presenting variants or " + + "a mockup for a reaction. The card's frame is Phoenix's: a title bar reading 'Phoenix AI asks' with " + + "minimize and close, a text field with Send for an answer in the user's own words, drag, resize and " + + "placement. You write only the body: uiFile, an HTML fragment with its own