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
19 changes: 15 additions & 4 deletions src-node/claude-code-agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ exports.checkAvailability = async function () {
* aiProgress, aiTextStream, aiToolEdit, aiError, aiComplete
*/
exports.sendPrompt = async function (params) {
const { prompt, projectPath, sessionAction, model, locale } = params;
const { prompt, projectPath, sessionAction, model, locale, selectionContext } = params;
const requestId = Date.now().toString(36) + Math.random().toString(36).slice(2, 7);

// Handle session
Expand All @@ -145,8 +145,17 @@ exports.sendPrompt = async function (params) {

currentAbortController = new AbortController();

// 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;
}

// Run the query asynchronously — don't await here so we return requestId immediately
_runQuery(requestId, prompt, projectPath, model, currentAbortController.signal, locale)
_runQuery(requestId, enrichedPrompt, projectPath, model, currentAbortController.signal, locale)
.catch(err => {
console.error("[Phoenix AI] Query error:", err);
});
Expand Down Expand Up @@ -207,11 +216,13 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale)

const queryOptions = {
cwd: projectPath || process.cwd(),
maxTurns: 10,
maxTurns: undefined,
allowedTools: [
"Read", "Edit", "Write", "Glob", "Grep",
"mcp__phoenix-editor__getEditorState",
"mcp__phoenix-editor__takeScreenshot"
"mcp__phoenix-editor__takeScreenshot",
"mcp__phoenix-editor__execJsInLivePreview",
"mcp__phoenix-editor__controlEditor"
],
mcpServers: { "phoenix-editor": editorMcpServer },
permissionMode: "acceptEdits",
Expand Down
94 changes: 89 additions & 5 deletions src-node/mcp-editor-tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@
/**
* MCP server factory for exposing Phoenix editor context to Claude Code.
*
* Provides two tools:
* Provides three tools:
* - getEditorState: returns active file, working set, and live preview file
* - takeScreenshot: captures a screenshot of the Phoenix window as base64 PNG
* - execJsInLivePreview: executes JS in the live preview iframe
*
* Uses the Claude Code SDK's in-process MCP server support (createSdkMcpServer / tool).
*/
Expand All @@ -40,7 +41,9 @@ const { z } = require("zod");
function createEditorMcpServer(sdkModule, nodeConnector) {
const getEditorStateTool = sdkModule.tool(
"getEditorState",
"Get the current Phoenix editor state: active file, working set (open files), and live preview file.",
"Get the current Phoenix editor state: active file, working set (open files), live preview file, " +
"and cursor/selection info (current line text with surrounding context, or selected text). " +
"Long lines are trimmed to 200 chars and selections to 10K chars — use the Read tool for full content.",
{},
async function () {
try {
Expand All @@ -59,8 +62,12 @@ function createEditorMcpServer(sdkModule, nodeConnector) {

const takeScreenshotTool = sdkModule.tool(
"takeScreenshot",
"Take a screenshot of the Phoenix Code editor window. Returns a PNG image.",
{ selector: z.string().optional().describe("Optional CSS selector to capture a specific element") },
"Take a screenshot of the Phoenix Code editor window. Returns a PNG image. " +
"Prefer capturing specific regions instead of the full page: " +
"use selector '#panel-live-preview-frame' for the live preview content, " +
"or '.editor-holder' for the code editor area. " +
"Only omit the selector when you need to see the full application layout.",
{ selector: z.string().optional().describe("CSS selector to capture a specific element. Use '#panel-live-preview-frame' for the live preview, '.editor-holder' for the code editor.") },
async function (args) {
try {
const result = await nodeConnector.execPeer("takeScreenshot", {
Expand All @@ -84,9 +91,86 @@ function createEditorMcpServer(sdkModule, nodeConnector) {
}
);

const execJsInLivePreviewTool = sdkModule.tool(
"execJsInLivePreview",
"Execute JavaScript in the live preview iframe (the page being previewed), NOT in Phoenix itself. " +
"Auto-opens the live preview panel if it is not already visible. Code is evaluated via eval() in " +
"the global scope of the previewed page. Note: eval() is synchronous — async/await is NOT supported. " +
"Only available when an HTML file is selected in the live preview — does not work for markdown or " +
"other non-HTML file types. Use this to inspect or manipulate the user's live-previewed web page " +
"(e.g. document.title, DOM queries).",
{ code: z.string().describe("JavaScript code to execute in the live preview iframe") },
async function (args) {
try {
const result = await nodeConnector.execPeer("execJsInLivePreview", {
code: args.code
});
if (result.error) {
return {
content: [{ type: "text", text: "Error: " + result.error }],
isError: true
};
}
return {
content: [{ type: "text", text: result.result || "undefined" }]
};
} catch (err) {
return {
content: [{ type: "text", text: "Error executing JS in live preview: " + err.message }],
isError: true
};
}
}
);

const controlEditorTool = sdkModule.tool(
"controlEditor",
"Control the Phoenix editor: open/close files, navigate to lines, and select text ranges. " +
"Accepts an array of operations to batch multiple actions in one call. " +
"All line and ch (column) parameters are 1-based.\n\n" +
"Operations:\n" +
"- open: Open a file in the active pane. Params: filePath\n" +
"- close: Close a file (force, no save prompt). Params: filePath\n" +
"- openInWorkingSet: Open a file and pin it to the working set. Params: filePath\n" +
"- setSelection: Open a file and select a range. Params: filePath, startLine, startCh, endLine, endCh\n" +
"- setCursorPos: Open a file and set cursor position. Params: filePath, line, ch",
{
operations: z.array(z.object({
operation: z.enum(["open", "close", "openInWorkingSet", "setSelection", "setCursorPos"]),
filePath: z.string().describe("Absolute path to the file"),
startLine: z.number().optional().describe("Start line (1-based) for setSelection"),
startCh: z.number().optional().describe("Start column (1-based) for setSelection"),
endLine: z.number().optional().describe("End line (1-based) for setSelection"),
endCh: z.number().optional().describe("End column (1-based) for setSelection"),
line: z.number().optional().describe("Line number (1-based) for setCursorPos"),
ch: z.number().optional().describe("Column (1-based) for setCursorPos")
})).describe("Array of editor operations to execute sequentially")
},
async function (args) {
const results = [];
let hasError = false;
for (const op of args.operations) {
try {
const result = await nodeConnector.execPeer("controlEditor", op);
results.push(result);
if (!result.success) {
hasError = true;
}
} catch (err) {
results.push({ success: false, error: err.message });
hasError = true;
}
}
return {
content: [{ type: "text", text: JSON.stringify(results) }],
isError: hasError
};
}
);

return sdkModule.createSdkMcpServer({
name: "phoenix-editor",
tools: [getEditorStateTool, takeScreenshotTool]
tools: [getEditorStateTool, takeScreenshotTool, execJsInLivePreviewTool, controlEditorTool]
});
}

Expand Down
Loading
Loading