From a34ee275148954afda8ff2b471a0dcfe13642355 Mon Sep 17 00:00:00 2001 From: abose Date: Sun, 22 Feb 2026 17:07:22 +0530 Subject: [PATCH 1/5] fix: cap inline selection context to 500 chars, send preview for large selections Selections over 500 chars no longer send full text to the AI prompt. Instead, a head/tail preview (first and last 3 lines, trimmed to 80 chars each) is sent with file path and line range, instructing the AI to use the Read tool for full content. --- src-node/claude-code-agent.js | 24 +++++++++++++++++++----- src/core-ai/AIChatPanel.js | 22 +++++++++++++++++++--- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src-node/claude-code-agent.js b/src-node/claude-code-agent.js index ee724402a2..5c87afe05d 100644 --- a/src-node/claude-code-agent.js +++ b/src-node/claude-code-agent.js @@ -147,11 +147,25 @@ exports.sendPrompt = async function (params) { // Prepend selection context to the prompt if available let enrichedPrompt = prompt; - if (selectionContext && selectionContext.selectedText) { - enrichedPrompt = - "The user has selected the following text in " + selectionContext.filePath + - " (lines " + selectionContext.startLine + "-" + selectionContext.endLine + "):\n" + - "```\n" + selectionContext.selectedText + "\n```\n\n" + prompt; + if (selectionContext) { + if (selectionContext.selectedText) { + enrichedPrompt = + "The user has selected the following text in " + selectionContext.filePath + + " (lines " + selectionContext.startLine + "-" + selectionContext.endLine + "):\n" + + "```\n" + selectionContext.selectedText + "\n```\n\n" + prompt; + } else { + let previewSnippet = ""; + if (selectionContext.selectionPreview) { + previewSnippet = "\nPreview of selection:\n```\n" + + selectionContext.selectionPreview + "\n```\n"; + } + enrichedPrompt = + "The user has selected lines " + selectionContext.startLine + "-" + + selectionContext.endLine + " in " + selectionContext.filePath + + ". Use the Read tool with offset=" + (selectionContext.startLine - 1) + + " and limit=" + (selectionContext.endLine - selectionContext.startLine + 1) + + " to read the selected content if needed." + previewSnippet + "\n" + prompt; + } } // Run the query asynchronously — don't await here so we return requestId immediately diff --git a/src/core-ai/AIChatPanel.js b/src/core-ai/AIChatPanel.js index 396371f250..98565388b5 100644 --- a/src/core-ai/AIChatPanel.js +++ b/src/core-ai/AIChatPanel.js @@ -506,15 +506,31 @@ define(function (require, exports, module) { // Gather selection context if available and not dismissed let selectionContext = null; if (_lastSelectionInfo && !_selectionDismissed && _lastSelectionInfo.selectedText) { + const MAX_INLINE_SELECTION = 500; + const MAX_PREVIEW_LINES = 3; + const MAX_PREVIEW_LINE_LEN = 80; let selectedText = _lastSelectionInfo.selectedText; - if (selectedText.length > 10000) { - selectedText = selectedText.slice(0, 10000); + let selectionPreview = null; + if (selectedText.length > MAX_INLINE_SELECTION) { + const lines = selectedText.split("\n"); + const headLines = lines.slice(0, MAX_PREVIEW_LINES).map(function (l) { + return l.length > MAX_PREVIEW_LINE_LEN ? l.slice(0, MAX_PREVIEW_LINE_LEN) + "..." : l; + }); + const tailLines = lines.length > MAX_PREVIEW_LINES * 2 + ? lines.slice(-MAX_PREVIEW_LINES).map(function (l) { + return l.length > MAX_PREVIEW_LINE_LEN ? l.slice(0, MAX_PREVIEW_LINE_LEN) + "..." : l; + }) + : []; + selectionPreview = headLines.join("\n") + + (tailLines.length ? "\n...\n" + tailLines.join("\n") : ""); + selectedText = null; } selectionContext = { filePath: _lastSelectionInfo.filePath, startLine: _lastSelectionInfo.startLine, endLine: _lastSelectionInfo.endLine, - selectedText: selectedText + selectedText: selectedText, + selectionPreview: selectionPreview }; } From 78ee1cb57853f2b0c12a651d4e8a5f60d78f1dcd Mon Sep 17 00:00:00 2001 From: abose Date: Sun, 22 Feb 2026 17:44:21 +0530 Subject: [PATCH 2/5] feat: add live preview banner and resizeLivePreview MCP tool Show a frosted-glass banner on the live preview toolbar when AI inspects via execJsInLivePreview, with mode save/restore and reference counting. Add resizeLivePreview MCP tool that accepts a pixel width, clamps via WorkspaceManager, and reports back actual width with a clamped notice when the editor can't accommodate the requested size. --- src-node/claude-code-agent.js | 3 +- src-node/mcp-editor-tools.js | 32 ++++- src/core-ai/AIChatPanel.js | 6 + src/core-ai/aiPhoenixConnectors.js | 191 +++++++++++++++++++++++++++++ src/core-ai/main.js | 4 + src/nls/root/strings.js | 3 + 6 files changed, 237 insertions(+), 2 deletions(-) diff --git a/src-node/claude-code-agent.js b/src-node/claude-code-agent.js index 5c87afe05d..9c70da4b06 100644 --- a/src-node/claude-code-agent.js +++ b/src-node/claude-code-agent.js @@ -236,7 +236,8 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale) "mcp__phoenix-editor__getEditorState", "mcp__phoenix-editor__takeScreenshot", "mcp__phoenix-editor__execJsInLivePreview", - "mcp__phoenix-editor__controlEditor" + "mcp__phoenix-editor__controlEditor", + "mcp__phoenix-editor__resizeLivePreview" ], mcpServers: { "phoenix-editor": editorMcpServer }, permissionMode: "acceptEdits", diff --git a/src-node/mcp-editor-tools.js b/src-node/mcp-editor-tools.js index 767243041d..d0161796f6 100644 --- a/src-node/mcp-editor-tools.js +++ b/src-node/mcp-editor-tools.js @@ -170,9 +170,39 @@ function createEditorMcpServer(sdkModule, nodeConnector) { } ); + const resizeLivePreviewTool = sdkModule.tool( + "resizeLivePreview", + "Resize the live preview panel to a specific width for responsive testing. " + + "Provide a width in pixels based on the target device (e.g. 390 for a phone, 768 for a tablet, 1440 for desktop).", + { + width: z.number().describe("Target width in pixels") + }, + async function (args) { + try { + const result = await nodeConnector.execPeer("resizeLivePreview", { + width: args.width + }); + if (result.error) { + return { + content: [{ type: "text", text: "Error: " + result.error }], + isError: true + }; + } + return { + content: [{ type: "text", text: JSON.stringify(result) }] + }; + } catch (err) { + return { + content: [{ type: "text", text: "Error resizing live preview: " + err.message }], + isError: true + }; + } + } + ); + return sdkModule.createSdkMcpServer({ name: "phoenix-editor", - tools: [getEditorStateTool, takeScreenshotTool, execJsInLivePreviewTool, controlEditorTool] + tools: [getEditorStateTool, takeScreenshotTool, execJsInLivePreviewTool, controlEditorTool, resizeLivePreviewTool] }); } diff --git a/src/core-ai/AIChatPanel.js b/src/core-ai/AIChatPanel.js index 98565388b5..b361100984 100644 --- a/src/core-ai/AIChatPanel.js +++ b/src/core-ai/AIChatPanel.js @@ -639,6 +639,7 @@ define(function (require, exports, module) { "mcp__phoenix-editor__takeScreenshot": { icon: "fa-solid fa-camera", color: "#c084fc", label: Strings.AI_CHAT_TOOL_SCREENSHOT }, "mcp__phoenix-editor__execJsInLivePreview": { icon: "fa-solid fa-eye", color: "#66bb6a", label: Strings.AI_CHAT_TOOL_LIVE_PREVIEW_JS }, "mcp__phoenix-editor__controlEditor": { icon: "fa-solid fa-code", color: "#6bc76b", label: Strings.AI_CHAT_TOOL_CONTROL_EDITOR }, + "mcp__phoenix-editor__resizeLivePreview": { icon: "fa-solid fa-arrows-left-right", color: "#66bb6a", label: Strings.AI_CHAT_TOOL_RESIZE_PREVIEW }, TodoWrite: { icon: "fa-solid fa-list-check", color: "#66bb6a", label: Strings.AI_CHAT_TOOL_TASKS } }; @@ -1467,6 +1468,11 @@ define(function (require, exports, module) { summary: Strings.AI_CHAT_TOOL_LIVE_PREVIEW_JS, lines: input.code ? input.code.split("\n").slice(0, 20) : [] }; + case "mcp__phoenix-editor__resizeLivePreview": + return { + summary: Strings.AI_CHAT_TOOL_RESIZE_PREVIEW, + lines: input.width ? [input.width + "px"] : [] + }; case "TodoWrite": { const todos = input.todos || []; const completed = todos.filter(function (t) { return t.status === "completed"; }).length; diff --git a/src/core-ai/aiPhoenixConnectors.js b/src/core-ai/aiPhoenixConnectors.js index 0038673236..ceac9b12a3 100644 --- a/src/core-ai/aiPhoenixConnectors.js +++ b/src/core-ai/aiPhoenixConnectors.js @@ -36,9 +36,11 @@ define(function (require, exports, module) { FileSystem = require("filesystem/FileSystem"), LiveDevProtocol = require("LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol"), LiveDevMain = require("LiveDevelopment/main"), + LivePreviewConstants = require("LiveDevelopment/LivePreviewConstants"), WorkspaceManager = require("view/WorkspaceManager"), SnapshotStore = require("core-ai/AISnapshotStore"), EventDispatcher = require("utils/EventDispatcher"), + StringUtils = require("utils/StringUtils"), Strings = require("strings"); // filePath → previous content before edit, for undo/snapshot support @@ -47,6 +49,134 @@ define(function (require, exports, module) { // Last screenshot base64 data, for displaying in tool indicators let _lastScreenshotBase64 = null; + // Banner / live preview mode state + let _activeExecJsCount = 0; + let _savedLivePreviewMode = null; + let _bannerDismissed = false; + let _bannerEl = null; + let _bannerStyleInjected = false; + let _bannerAutoHideTimer = null; + + /** + * Inject banner CSS once into the document head. + */ + function _injectBannerStyles() { + if (_bannerStyleInjected) { + return; + } + _bannerStyleInjected = true; + const style = document.createElement("style"); + style.textContent = + "@keyframes ai-banner-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }" + + ".ai-lp-banner {" + + " position: absolute; top: 0; left: 0; right: 0; bottom: 0;" + + " display: flex; align-items: center; justify-content: center; gap: 8px;" + + " background: rgba(24,24,28,0.52);" + + " backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);" + + " z-index: 10; border-radius: 3px;" + + " font-size: 12px; color: #e0e0e0; pointer-events: auto;" + + " transition: opacity 0.3s ease;" + + "}" + + ".ai-lp-banner .ai-lp-banner-icon {" + + " color: #66bb6a; animation: ai-banner-pulse 1.5s ease-in-out infinite;" + + "}" + + ".ai-lp-banner .ai-lp-banner-close {" + + " position: absolute; right: 6px; top: 50%; transform: translateY(-50%);" + + " background: none; border: none; color: #aaa; cursor: pointer;" + + " font-size: 14px; padding: 2px 5px; line-height: 1;" + + "}" + + ".ai-lp-banner .ai-lp-banner-close:hover { color: #fff; }"; + document.head.appendChild(style); + } + + /** + * Show a banner overlay on the live preview toolbar. + * @param {string} text - Banner message text + */ + function _showBanner(text) { + if (_bannerDismissed) { + return; + } + _injectBannerStyles(); + const toolbar = document.getElementById("live-preview-plugin-toolbar"); + if (!toolbar) { + return; + } + // Ensure toolbar can host absolutely positioned children + if (getComputedStyle(toolbar).position === "static") { + toolbar.style.position = "relative"; + } + if (_bannerEl && _bannerEl.parentNode) { + // Update text on existing banner + const textSpan = _bannerEl.querySelector(".ai-lp-banner-text"); + if (textSpan) { + textSpan.textContent = text; + } + _bannerEl.style.opacity = "1"; + return; + } + const banner = document.createElement("div"); + banner.className = "ai-lp-banner"; + banner.innerHTML = + '' + + '' + text.replace(/' + + ''; + banner.querySelector(".ai-lp-banner-close").addEventListener("click", function () { + _bannerDismissed = true; + _hideBanner(); + }); + toolbar.appendChild(banner); + _bannerEl = banner; + } + + /** + * Hide and remove the banner overlay with a fade-out transition. + */ + function _hideBanner() { + if (!_bannerEl) { + return; + } + _bannerEl.style.opacity = "0"; + const el = _bannerEl; + setTimeout(function () { + if (el.parentNode) { + el.parentNode.removeChild(el); + } + }, 300); + _bannerEl = null; + } + + /** + * Called when an execJsInLivePreview call starts. Increments the active + * count, saves mode and shows banner on first call. + */ + function _onExecJsStart() { + _activeExecJsCount++; + if (_activeExecJsCount === 1) { + _savedLivePreviewMode = LiveDevMain.getCurrentMode(); + if (_savedLivePreviewMode !== LivePreviewConstants.LIVE_PREVIEW_MODE) { + LiveDevMain.setMode(LivePreviewConstants.LIVE_PREVIEW_MODE); + } + _bannerDismissed = false; + _showBanner(Strings.AI_LIVE_PREVIEW_BANNER_TEXT); + } + } + + /** + * Called when an execJsInLivePreview call finishes. Decrements the count + * and restores mode / hides banner when all calls are done. + */ + function _onExecJsDone() { + _activeExecJsCount = Math.max(0, _activeExecJsCount - 1); + if (_activeExecJsCount === 0) { + if (_savedLivePreviewMode && _savedLivePreviewMode !== LivePreviewConstants.LIVE_PREVIEW_MODE) { + LiveDevMain.setMode(_savedLivePreviewMode); + } + _savedLivePreviewMode = null; + _hideBanner(); + } + } + // --- Editor state --- /** @@ -354,13 +484,16 @@ define(function (require, exports, module) { */ function execJsInLivePreview(params) { const deferred = new $.Deferred(); + _onExecJsStart(); function _evaluate() { LiveDevProtocol.evaluate(params.code) .done(function (evalResult) { + _onExecJsDone(); deferred.resolve({ result: JSON.stringify(evalResult) }); }) .fail(function (err) { + _onExecJsDone(); deferred.resolve({ error: (err && err.message) || String(err) || "evaluate() failed" }); }); } @@ -397,6 +530,7 @@ define(function (require, exports, module) { const timeoutTimer = setTimeout(function () { if (settled) { return; } cleanup(); + _onExecJsDone(); deferred.resolve({ error: "Timed out waiting for live preview connection (30s)" }); }, TIMEOUT); @@ -495,6 +629,62 @@ define(function (require, exports, module) { return deferred.promise(); } + // --- Live preview resize --- + + /** + * Resize the live preview panel to a specific width in pixels. + * @param {Object} params - { width: number } + * @return {$.Promise} resolves with { actualWidth } or { error } + */ + function resizeLivePreview(params) { + const deferred = new $.Deferred(); + + if (!params.width) { + deferred.resolve({ error: "Provide 'width' as a number in pixels" }); + return deferred.promise(); + } + + const targetWidth = params.width; + const label = targetWidth + "px"; + + // Ensure live preview panel is open + const panel = WorkspaceManager.getPanelForID("live-preview-panel"); + if (!panel || !panel.isVisible()) { + CommandManager.execute("file.liveFilePreview"); + } + + // Give the panel a moment to open, then resize + setTimeout(function () { + WorkspaceManager.setPluginPanelWidth(targetWidth); + + // Read back actual width from the toolbar + const toolbar = document.getElementById("live-preview-plugin-toolbar"); + const actualWidth = toolbar ? toolbar.offsetWidth : targetWidth; + + // Show brief banner + _bannerDismissed = false; + _showBanner(StringUtils.format(Strings.AI_LIVE_PREVIEW_BANNER_RESIZE, label)); + if (_bannerAutoHideTimer) { + clearTimeout(_bannerAutoHideTimer); + } + _bannerAutoHideTimer = setTimeout(function () { + _hideBanner(); + _bannerAutoHideTimer = null; + }, 3000); + + const result = { actualWidth: actualWidth }; + if (actualWidth !== targetWidth) { + result.clamped = true; + result.note = "Requested " + targetWidth + "px but the editor window can only " + + "accommodate " + actualWidth + "px. The user needs to increase the editor " + + "window size to allow a wider preview."; + } + deferred.resolve(result); + }, 100); + + return deferred.promise(); + } + exports.getEditorState = getEditorState; exports.takeScreenshot = takeScreenshot; exports.getFileContent = getFileContent; @@ -504,6 +694,7 @@ define(function (require, exports, module) { exports.getLastScreenshot = getLastScreenshot; exports.execJsInLivePreview = execJsInLivePreview; exports.controlEditor = controlEditor; + exports.resizeLivePreview = resizeLivePreview; EventDispatcher.makeEventDispatcher(exports); }); diff --git a/src/core-ai/main.js b/src/core-ai/main.js index 48bab8a91a..794aae4f8a 100644 --- a/src/core-ai/main.js +++ b/src/core-ai/main.js @@ -58,6 +58,10 @@ define(function (require, exports, module) { return PhoenixConnectors.controlEditor(params); }; + exports.resizeLivePreview = async function (params) { + return PhoenixConnectors.resizeLivePreview(params); + }; + AppInit.appReady(function () { SidebarTabs.addTab("ai", "AI", "fa-solid fa-wand-magic-sparkles", { priority: 200 }); diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index f6b9db8f8d..b912d6fd09 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1826,6 +1826,9 @@ define({ "AI_CHAT_TOOL_SCREENSHOT_LIVE_PREVIEW": "live preview", "AI_CHAT_TOOL_SCREENSHOT_FULL_PAGE": "full page", "AI_CHAT_TOOL_LIVE_PREVIEW_JS": "Inspecting preview", + "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}", "AI_CHAT_TOOL_CONTROL_EDITOR": "Editor", "AI_CHAT_TOOL_TASKS": "Tasks", "AI_CHAT_TOOL_TASKS_SUMMARY": "{0} of {1} tasks done", From 401de36e5c3e1cf229d7d54f5d0cd4b6a3ab2cfb Mon Sep 17 00:00:00 2001 From: abose Date: Sun, 22 Feb 2026 17:57:34 +0530 Subject: [PATCH 3/5] feat: add copy-to-clipboard button on AI chat code blocks --- src/core-ai/AIChatPanel.js | 33 ++++++++++++++++++++++++++++++++ src/nls/root/strings.js | 2 ++ src/styles/Extn-AIChatPanel.less | 26 +++++++++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/src/core-ai/AIChatPanel.js b/src/core-ai/AIChatPanel.js index b361100984..4e50b66d88 100644 --- a/src/core-ai/AIChatPanel.js +++ b/src/core-ai/AIChatPanel.js @@ -1191,6 +1191,7 @@ define(function (require, exports, module) { try { $target.html(marked.parse(_segmentText, { breaks: true, gfm: true })); _enhanceColorCodes($target); + _addCopyButtons($target); } catch (e) { $target.text(_segmentText); } @@ -1257,6 +1258,35 @@ define(function (require, exports, module) { }); } + /** + * Inject a copy-to-clipboard button into each
 block inside the given container.
+     * Idempotent: skips 
 elements that already have a .ai-copy-btn.
+     */
+    function _addCopyButtons($container) {
+        $container.find("pre").each(function () {
+            const $pre = $(this);
+            if ($pre.find(".ai-copy-btn").length) {
+                return;
+            }
+            const $btn = $('');
+            $btn.on("click", function (e) {
+                e.stopPropagation();
+                const $code = $pre.find("code");
+                const text = ($code.length ? $code[0] : $pre[0]).textContent;
+                Phoenix.app.copyToClipboard(text);
+                const $icon = $btn.find("i");
+                $icon.removeClass("fa-copy").addClass("fa-check");
+                $btn.attr("title", Strings.AI_CHAT_COPIED_CODE);
+                setTimeout(function () {
+                    $icon.removeClass("fa-check").addClass("fa-copy");
+                    $btn.attr("title", Strings.AI_CHAT_COPY_CODE);
+                }, 1500);
+            });
+            $pre.append($btn);
+        });
+    }
+
     function _appendToolIndicator(toolName, toolId) {
         // Remove thinking indicator on first content
         if (!_hasReceivedContent) {
@@ -1628,6 +1658,9 @@ define(function (require, exports, module) {
             // Finalize: remove ai-stream-target class so future messages get their own container
             $messages.find(".ai-stream-target").removeClass("ai-stream-target");
 
+            // Ensure copy buttons are present on all code blocks
+            _addCopyButtons($messages);
+
             // Mark all active tool indicators as done
             _finishActiveTools();
         }
diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js
index b912d6fd09..3e86cf24df 100644
--- a/src/nls/root/strings.js
+++ b/src/nls/root/strings.js
@@ -1862,6 +1862,8 @@ define({
     "AI_CHAT_CONTEXT_SELECTION": "Selection L{0}-L{1}",
     "AI_CHAT_CONTEXT_CURSOR": "Line {0}",
     "AI_CHAT_CONTEXT_LIVE_PREVIEW": "Live Preview",
+    "AI_CHAT_COPY_CODE": "Copy",
+    "AI_CHAT_COPIED_CODE": "Copied!",
 
     // demo start - Phoenix Code Playground - Interactive Onboarding
     "DEMO_SECTION1_TITLE": "Edit in Live Preview",
diff --git a/src/styles/Extn-AIChatPanel.less b/src/styles/Extn-AIChatPanel.less
index d086b11a44..707433a147 100644
--- a/src/styles/Extn-AIChatPanel.less
+++ b/src/styles/Extn-AIChatPanel.less
@@ -163,6 +163,7 @@
             border-radius: 4px;
             overflow-x: auto;
             margin: 6px 0;
+            position: relative;
 
             code {
                 background: none;
@@ -172,6 +173,31 @@
                 line-height: 1.5;
                 color: @project-panel-text-1;
             }
+
+            .ai-copy-btn {
+                position: absolute;
+                top: 4px;
+                right: 4px;
+                background: rgba(255, 255, 255, 0.08);
+                border: 1px solid rgba(255, 255, 255, 0.1);
+                border-radius: 4px;
+                color: rgba(255, 255, 255, 0.5);
+                cursor: pointer;
+                padding: 2px 6px;
+                font-size: 12px;
+                line-height: 1;
+                opacity: 0;
+                transition: opacity 0.15s ease, background 0.15s ease, color 0.15s ease;
+
+                &:hover {
+                    background: rgba(255, 255, 255, 0.15);
+                    color: rgba(255, 255, 255, 0.85);
+                }
+            }
+
+            &:hover .ai-copy-btn {
+                opacity: 1;
+            }
         }
 
         ul, ol {

From dd6a9dbe20491870e174c3673763c9f37ca48cbf Mon Sep 17 00:00:00 2001
From: abose 
Date: Sun, 22 Feb 2026 22:30:41 +0530
Subject: [PATCH 4/5] feat: add wait MCP tool, extend live preview banner
 duration, fix table cursor
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- Add `wait` MCP tool (0.1–60s) with countdown UI in AI chat panel
- Auto-approve `wait` tool in allowedTools list
- Show "Waiting Xs" countdown that becomes "Done waiting Xs" on completion
- Keep live preview banner visible for 5s after exec/resize (was instant/3s)
- Cancel pending auto-hide timer when new exec batch starts
- Fix text cursor on table cells (th/td) in AI chat messages
---
 src-node/claude-code-agent.js      |  3 ++-
 src-node/mcp-editor-tools.js       | 20 +++++++++++++++++++-
 src/core-ai/AIChatPanel.js         | 20 ++++++++++++++++++++
 src/core-ai/aiPhoenixConnectors.js | 16 ++++++++++++++--
 src/nls/root/strings.js            |  3 +++
 src/styles/Extn-AIChatPanel.less   |  1 +
 6 files changed, 59 insertions(+), 4 deletions(-)

diff --git a/src-node/claude-code-agent.js b/src-node/claude-code-agent.js
index 9c70da4b06..c76f7ece71 100644
--- a/src-node/claude-code-agent.js
+++ b/src-node/claude-code-agent.js
@@ -237,7 +237,8 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale)
             "mcp__phoenix-editor__takeScreenshot",
             "mcp__phoenix-editor__execJsInLivePreview",
             "mcp__phoenix-editor__controlEditor",
-            "mcp__phoenix-editor__resizeLivePreview"
+            "mcp__phoenix-editor__resizeLivePreview",
+            "mcp__phoenix-editor__wait"
         ],
         mcpServers: { "phoenix-editor": editorMcpServer },
         permissionMode: "acceptEdits",
diff --git a/src-node/mcp-editor-tools.js b/src-node/mcp-editor-tools.js
index d0161796f6..66d64ec284 100644
--- a/src-node/mcp-editor-tools.js
+++ b/src-node/mcp-editor-tools.js
@@ -200,9 +200,27 @@ function createEditorMcpServer(sdkModule, nodeConnector) {
         }
     );
 
+    const waitTool = sdkModule.tool(
+        "wait",
+        "Wait for a specified number of seconds before continuing. " +
+        "Useful for waiting after DOM changes, animations, live preview updates, or resize operations " +
+        "before taking a screenshot or inspecting state. Maximum 60 seconds.",
+        {
+            seconds: z.number().min(0.1).max(60).describe("Number of seconds to wait (0.1–60)")
+        },
+        async function (args) {
+            const ms = Math.round(args.seconds * 1000);
+            await new Promise(function (resolve) { setTimeout(resolve, ms); });
+            return {
+                content: [{ type: "text", text: "Waited " + args.seconds + " seconds." }]
+            };
+        }
+    );
+
     return sdkModule.createSdkMcpServer({
         name: "phoenix-editor",
-        tools: [getEditorStateTool, takeScreenshotTool, execJsInLivePreviewTool, controlEditorTool, resizeLivePreviewTool]
+        tools: [getEditorStateTool, takeScreenshotTool, execJsInLivePreviewTool, controlEditorTool,
+            resizeLivePreviewTool, waitTool]
     });
 }
 
diff --git a/src/core-ai/AIChatPanel.js b/src/core-ai/AIChatPanel.js
index 4e50b66d88..a786b82cd8 100644
--- a/src/core-ai/AIChatPanel.js
+++ b/src/core-ai/AIChatPanel.js
@@ -640,6 +640,7 @@ define(function (require, exports, module) {
         "mcp__phoenix-editor__execJsInLivePreview": { icon: "fa-solid fa-eye", color: "#66bb6a", label: Strings.AI_CHAT_TOOL_LIVE_PREVIEW_JS },
         "mcp__phoenix-editor__controlEditor":       { icon: "fa-solid fa-code", color: "#6bc76b", label: Strings.AI_CHAT_TOOL_CONTROL_EDITOR },
         "mcp__phoenix-editor__resizeLivePreview":   { icon: "fa-solid fa-arrows-left-right", color: "#66bb6a", label: Strings.AI_CHAT_TOOL_RESIZE_PREVIEW },
+        "mcp__phoenix-editor__wait":                { icon: "fa-solid fa-hourglass-half", color: "#adb9bd", label: Strings.AI_CHAT_TOOL_WAIT },
         TodoWrite: { icon: "fa-solid fa-list-check", color: "#66bb6a", label: Strings.AI_CHAT_TOOL_TASKS }
     };
 
@@ -1376,6 +1377,20 @@ define(function (require, exports, module) {
             $tool.find(".ai-tool-header").on("click", function () {
                 $tool.toggleClass("ai-tool-expanded");
             }).css("cursor", "pointer");
+        } else if (toolName === "mcp__phoenix-editor__wait" && toolInput && toolInput.seconds) {
+            // Countdown timer: update label every second
+            const totalSec = Math.ceil(toolInput.seconds);
+            let remaining = totalSec;
+            const $label = $tool.find(".ai-tool-label");
+            const countdownId = setInterval(function () {
+                remaining--;
+                if (remaining <= 0) {
+                    clearInterval(countdownId);
+                    $label.text(StringUtils.format(Strings.AI_CHAT_TOOL_WAITED, totalSec));
+                } else {
+                    $label.text(StringUtils.format(Strings.AI_CHAT_TOOL_WAITING, remaining));
+                }
+            }, 1000);
         } else if (toolName === "mcp__phoenix-editor__takeScreenshot") {
             const $detail = $('
'); $tool.append($detail); @@ -1503,6 +1518,11 @@ define(function (require, exports, module) { summary: Strings.AI_CHAT_TOOL_RESIZE_PREVIEW, lines: input.width ? [input.width + "px"] : [] }; + case "mcp__phoenix-editor__wait": + return { + summary: StringUtils.format(Strings.AI_CHAT_TOOL_WAITING, input.seconds || "?"), + lines: [] + }; case "TodoWrite": { const todos = input.todos || []; const completed = todos.filter(function (t) { return t.status === "completed"; }).length; diff --git a/src/core-ai/aiPhoenixConnectors.js b/src/core-ai/aiPhoenixConnectors.js index ceac9b12a3..75c1c64db4 100644 --- a/src/core-ai/aiPhoenixConnectors.js +++ b/src/core-ai/aiPhoenixConnectors.js @@ -153,6 +153,11 @@ define(function (require, exports, module) { function _onExecJsStart() { _activeExecJsCount++; if (_activeExecJsCount === 1) { + // Cancel any pending auto-hide from a previous exec batch + if (_bannerAutoHideTimer) { + clearTimeout(_bannerAutoHideTimer); + _bannerAutoHideTimer = null; + } _savedLivePreviewMode = LiveDevMain.getCurrentMode(); if (_savedLivePreviewMode !== LivePreviewConstants.LIVE_PREVIEW_MODE) { LiveDevMain.setMode(LivePreviewConstants.LIVE_PREVIEW_MODE); @@ -173,7 +178,14 @@ define(function (require, exports, module) { LiveDevMain.setMode(_savedLivePreviewMode); } _savedLivePreviewMode = null; - _hideBanner(); + // Keep the banner visible briefly so the user can read it + if (_bannerAutoHideTimer) { + clearTimeout(_bannerAutoHideTimer); + } + _bannerAutoHideTimer = setTimeout(function () { + _hideBanner(); + _bannerAutoHideTimer = null; + }, 5000); } } @@ -670,7 +682,7 @@ define(function (require, exports, module) { _bannerAutoHideTimer = setTimeout(function () { _hideBanner(); _bannerAutoHideTimer = null; - }, 3000); + }, 5000); const result = { actualWidth: actualWidth }; if (actualWidth !== targetWidth) { diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 3e86cf24df..1dabc4f942 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1862,6 +1862,9 @@ define({ "AI_CHAT_CONTEXT_SELECTION": "Selection L{0}-L{1}", "AI_CHAT_CONTEXT_CURSOR": "Line {0}", "AI_CHAT_CONTEXT_LIVE_PREVIEW": "Live Preview", + "AI_CHAT_TOOL_WAIT": "Wait", + "AI_CHAT_TOOL_WAITING": "Waiting {0}s", + "AI_CHAT_TOOL_WAITED": "Done waiting {0}s", "AI_CHAT_COPY_CODE": "Copy", "AI_CHAT_COPIED_CODE": "Copied!", diff --git a/src/styles/Extn-AIChatPanel.less b/src/styles/Extn-AIChatPanel.less index 707433a147..c9ed5e3bb0 100644 --- a/src/styles/Extn-AIChatPanel.less +++ b/src/styles/Extn-AIChatPanel.less @@ -242,6 +242,7 @@ max-width: 50%; white-space: nowrap; overflow-wrap: normal; + cursor: text; } th { From 7a7b8a649e3673b9a85bab7a414faa29af049fcc Mon Sep 17 00:00:00 2001 From: abose Date: Sun, 22 Feb 2026 23:00:56 +0530 Subject: [PATCH 5/5] fix: enforce minimum 100px editor width when resizing plugin panels Both setPluginPanelWidth() and the drag-based Resizer now account for sidebar width so the editor area cannot be squeezed below 100px. --- src/view/WorkspaceManager.js | 6 +++-- test/spec/MainViewManager-integ-test.js | 33 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/view/WorkspaceManager.js b/src/view/WorkspaceManager.js index c56bbb4d95..c11bc2e0a5 100644 --- a/src/view/WorkspaceManager.js +++ b/src/view/WorkspaceManager.js @@ -160,7 +160,8 @@ define(function (require, exports, module) { } }); - $mainToolbar.data("maxsize", window.innerWidth*.75); + var sidebarWidth = $("#sidebar").outerWidth() || 0; + $mainToolbar.data("maxsize", Math.min(window.innerWidth * 0.75, window.innerWidth - sidebarWidth - 100)); } @@ -442,7 +443,8 @@ define(function (require, exports, module) { // Respect min/max constraints var minSize = currentlyShownPanel.minWidth || 0; var minToolbarWidth = minSize + pluginIconsBarWidth; - var maxToolbarWidth = window.innerWidth * 0.75; + var sidebarWidth = $("#sidebar").outerWidth() || 0; + var maxToolbarWidth = Math.min(window.innerWidth * 0.75, window.innerWidth - sidebarWidth - 100); newToolbarWidth = Math.max(newToolbarWidth, minToolbarWidth); newToolbarWidth = Math.min(newToolbarWidth, maxToolbarWidth); diff --git a/test/spec/MainViewManager-integ-test.js b/test/spec/MainViewManager-integ-test.js index 1d9d96e36a..16e61040a7 100644 --- a/test/spec/MainViewManager-integ-test.js +++ b/test/spec/MainViewManager-integ-test.js @@ -1238,6 +1238,39 @@ define(function (require, exports, module) { expect(panelContentWidth).toEqual(targetWidth); }); }); + + it("should preserve at least 100px for the editor area", function () { + pluginPanel.show(); + + // Request an absurdly large width that would squeeze the editor to near-zero + WorkspaceManager.setPluginPanelWidth(testWindow.innerWidth); + + const $mainToolbar = _$("#main-toolbar"); + const sidebarWidth = _$("#sidebar").outerWidth() || 0; + const toolbarWidth = $mainToolbar.width(); + const editorWidth = testWindow.innerWidth - sidebarWidth - toolbarWidth; + + // Editor area must retain at least 100px + expect(editorWidth).toBeGreaterThanOrEqual(100); + }); + + it("should set drag-based maxsize to preserve at least 100px for the editor", function () { + pluginPanel.show(); + + // Trigger a layout recompute so updateResizeLimits runs + WorkspaceManager.recomputeLayout(true); + + const $mainToolbar = _$("#main-toolbar"); + const sidebarWidth = _$("#sidebar").outerWidth() || 0; + const maxSize = $mainToolbar.data("maxsize"); + + // maxSize must leave at least 100px for the editor + const editorWidthAtMax = testWindow.innerWidth - sidebarWidth - maxSize; + expect(editorWidthAtMax).toBeGreaterThanOrEqual(100); + + // maxSize should also not exceed 75% of window width + expect(maxSize).toBeLessThanOrEqual(testWindow.innerWidth * 0.75); + }); }); }); });