Skip to content

Commit 8fde6d6

Browse files
committed
feat: deepen AI chat editor integration with context chips and live preview tools
- Add context bar above chat input showing selection/cursor position and live preview status as dismissable chips - Send selected text as context with chat prompts so Claude sees what the user is looking at - Add execJsInLivePreview MCP tool to execute JS in the live preview iframe - Enrich getEditorState with cursor/selection info and surrounding lines - Add human-readable labels for MCP tools (Editor state, Screenshot, Live Preview JS) instead of raw mcp__ identifiers - Show captured screenshots inline as collapsible thumbnails - Guide Claude to prefer specific selectors for screenshots - Remove maxTurns cap so agent runs to completion - Namespace all event listeners with .off before .on to prevent leaks
1 parent 7b381ff commit 8fde6d6

7 files changed

Lines changed: 593 additions & 17 deletions

File tree

src-node/claude-code-agent.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ exports.checkAvailability = async function () {
129129
* aiProgress, aiTextStream, aiToolEdit, aiError, aiComplete
130130
*/
131131
exports.sendPrompt = async function (params) {
132-
const { prompt, projectPath, sessionAction, model, locale } = params;
132+
const { prompt, projectPath, sessionAction, model, locale, selectionContext } = params;
133133
const requestId = Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
134134

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

146146
currentAbortController = new AbortController();
147147

148+
// Prepend selection context to the prompt if available
149+
let enrichedPrompt = prompt;
150+
if (selectionContext && selectionContext.selectedText) {
151+
enrichedPrompt =
152+
"The user has selected the following text in " + selectionContext.filePath +
153+
" (lines " + selectionContext.startLine + "-" + selectionContext.endLine + "):\n" +
154+
"```\n" + selectionContext.selectedText + "\n```\n\n" + prompt;
155+
}
156+
148157
// Run the query asynchronously — don't await here so we return requestId immediately
149-
_runQuery(requestId, prompt, projectPath, model, currentAbortController.signal, locale)
158+
_runQuery(requestId, enrichedPrompt, projectPath, model, currentAbortController.signal, locale)
150159
.catch(err => {
151160
console.error("[Phoenix AI] Query error:", err);
152161
});
@@ -207,11 +216,12 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale)
207216

208217
const queryOptions = {
209218
cwd: projectPath || process.cwd(),
210-
maxTurns: 10,
219+
maxTurns: undefined,
211220
allowedTools: [
212221
"Read", "Edit", "Write", "Glob", "Grep",
213222
"mcp__phoenix-editor__getEditorState",
214-
"mcp__phoenix-editor__takeScreenshot"
223+
"mcp__phoenix-editor__takeScreenshot",
224+
"mcp__phoenix-editor__execJsInLivePreview"
215225
],
216226
mcpServers: { "phoenix-editor": editorMcpServer },
217227
permissionMode: "acceptEdits",

src-node/mcp-editor-tools.js

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@
2121
/**
2222
* MCP server factory for exposing Phoenix editor context to Claude Code.
2323
*
24-
* Provides two tools:
24+
* Provides three tools:
2525
* - getEditorState: returns active file, working set, and live preview file
2626
* - takeScreenshot: captures a screenshot of the Phoenix window as base64 PNG
27+
* - execJsInLivePreview: executes JS in the live preview iframe
2728
*
2829
* Uses the Claude Code SDK's in-process MCP server support (createSdkMcpServer / tool).
2930
*/
@@ -40,7 +41,9 @@ const { z } = require("zod");
4041
function createEditorMcpServer(sdkModule, nodeConnector) {
4142
const getEditorStateTool = sdkModule.tool(
4243
"getEditorState",
43-
"Get the current Phoenix editor state: active file, working set (open files), and live preview file.",
44+
"Get the current Phoenix editor state: active file, working set (open files), live preview file, " +
45+
"and cursor/selection info (current line text with surrounding context, or selected text). " +
46+
"Long lines are trimmed to 200 chars and selections to 10K chars — use the Read tool for full content.",
4447
{},
4548
async function () {
4649
try {
@@ -59,8 +62,12 @@ function createEditorMcpServer(sdkModule, nodeConnector) {
5962

6063
const takeScreenshotTool = sdkModule.tool(
6164
"takeScreenshot",
62-
"Take a screenshot of the Phoenix Code editor window. Returns a PNG image.",
63-
{ selector: z.string().optional().describe("Optional CSS selector to capture a specific element") },
65+
"Take a screenshot of the Phoenix Code editor window. Returns a PNG image. " +
66+
"Prefer capturing specific regions instead of the full page: " +
67+
"use selector '#panel-live-preview-frame' for the live preview content, " +
68+
"or '.editor-holder' for the code editor area. " +
69+
"Only omit the selector when you need to see the full application layout.",
70+
{ 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.") },
6471
async function (args) {
6572
try {
6673
const result = await nodeConnector.execPeer("takeScreenshot", {
@@ -84,9 +91,41 @@ function createEditorMcpServer(sdkModule, nodeConnector) {
8491
}
8592
);
8693

94+
const execJsInLivePreviewTool = sdkModule.tool(
95+
"execJsInLivePreview",
96+
"Execute JavaScript in the live preview iframe (the page being previewed), NOT in Phoenix itself. " +
97+
"Auto-opens the live preview panel if it is not already visible. Code is evaluated via eval() in " +
98+
"the global scope of the previewed page. Note: eval() is synchronous — async/await is NOT supported. " +
99+
"Only available when an HTML file is selected in the live preview — does not work for markdown or " +
100+
"other non-HTML file types. Use this to inspect or manipulate the user's live-previewed web page " +
101+
"(e.g. document.title, DOM queries).",
102+
{ code: z.string().describe("JavaScript code to execute in the live preview iframe") },
103+
async function (args) {
104+
try {
105+
const result = await nodeConnector.execPeer("execJsInLivePreview", {
106+
code: args.code
107+
});
108+
if (result.error) {
109+
return {
110+
content: [{ type: "text", text: "Error: " + result.error }],
111+
isError: true
112+
};
113+
}
114+
return {
115+
content: [{ type: "text", text: result.result || "undefined" }]
116+
};
117+
} catch (err) {
118+
return {
119+
content: [{ type: "text", text: "Error executing JS in live preview: " + err.message }],
120+
isError: true
121+
};
122+
}
123+
}
124+
);
125+
87126
return sdkModule.createSdkMcpServer({
88127
name: "phoenix-editor",
89-
tools: [getEditorStateTool, takeScreenshotTool]
128+
tools: [getEditorStateTool, takeScreenshotTool, execJsInLivePreviewTool]
90129
});
91130
}
92131

0 commit comments

Comments
 (0)