Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions src-node/claude-code-agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -222,7 +236,9 @@ 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",
"mcp__phoenix-editor__wait"
],
mcpServers: { "phoenix-editor": editorMcpServer },
permissionMode: "acceptEdits",
Expand Down
50 changes: 49 additions & 1 deletion src-node/mcp-editor-tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,57 @@ 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
};
}
}
);

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]
tools: [getEditorStateTool, takeScreenshotTool, execJsInLivePreviewTool, controlEditorTool,
resizeLivePreviewTool, waitTool]
});
}

Expand Down
81 changes: 78 additions & 3 deletions src/core-ai/AIChatPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
}

Expand Down Expand Up @@ -623,6 +639,8 @@ 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 },
"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 }
};

Expand Down Expand Up @@ -1174,6 +1192,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);
}
Expand Down Expand Up @@ -1240,6 +1259,35 @@ define(function (require, exports, module) {
});
}

/**
* Inject a copy-to-clipboard button into each <pre> block inside the given container.
* Idempotent: skips <pre> 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 = $('<button class="ai-copy-btn" title="' + Strings.AI_CHAT_COPY_CODE + '">' +
'<i class="fa-solid fa-copy"></i></button>');
$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) {
Expand Down Expand Up @@ -1329,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 = $('<div class="ai-tool-detail"></div>');
$tool.append($detail);
Expand Down Expand Up @@ -1451,6 +1513,16 @@ 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 "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;
Expand Down Expand Up @@ -1606,6 +1678,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();
}
Expand Down
Loading
Loading