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
8 changes: 8 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@
- No trailing whitespace.
- Use `const` and `let` instead of `var`.

## Translations / i18n
- All user-visible strings must go in `src/nls/root/strings.js` — never hardcode English in source files.
- Use `const Strings = require("strings");` then `Strings.KEY_NAME`.
- For parameterized strings use `StringUtils.format(Strings.KEY, arg0, arg1)` with `{0}`, `{1}` placeholders.
- Keys use UPPER_SNAKE_CASE grouped by feature prefix (e.g. `AI_CHAT_*`).
- Only `src/nls/root/strings.js` (English) needs manual edits — other locales are auto-translated by GitHub Actions.
- Never compare `$(el).text()` against English strings for logic — use data attributes or CSS classes instead.

## Phoenix MCP (Desktop App Testing)

Use `exec_js` to run JS in the Phoenix browser runtime. jQuery `$()` is global. `brackets.test.*` exposes internal modules (DocumentManager, CommandManager, ProjectManager, FileSystem, EditorManager). Always `return` a value from `exec_js` to see results. Prefer reusing an already-running Phoenix instance (`get_phoenix_status`) over launching a new one.
Expand Down
26 changes: 21 additions & 5 deletions src-node/claude-code-agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

const { execSync } = require("child_process");
const path = require("path");
const { createEditorMcpServer } = require("./mcp-editor-tools");

const CONNECTOR_ID = "ph_ai_claude";

Expand All @@ -40,6 +41,9 @@ let currentSessionId = null;
// Active query state
let currentAbortController = null;

// Lazily-initialized in-process MCP server for editor context
let editorMcpServer = null;

// Streaming throttle
const TEXT_STREAM_THROTTLE_MS = 50;

Expand Down Expand Up @@ -125,7 +129,7 @@ exports.checkAvailability = async function () {
* aiProgress, aiTextStream, aiToolEdit, aiError, aiComplete
*/
exports.sendPrompt = async function (params) {
const { prompt, projectPath, sessionAction, model } = params;
const { prompt, projectPath, sessionAction, model, locale } = params;
const requestId = Date.now().toString(36) + Math.random().toString(36).slice(2, 7);

// Handle session
Expand All @@ -142,7 +146,7 @@ exports.sendPrompt = async function (params) {
currentAbortController = new AbortController();

// Run the query asynchronously — don't await here so we return requestId immediately
_runQuery(requestId, prompt, projectPath, model, currentAbortController.signal)
_runQuery(requestId, prompt, projectPath, model, currentAbortController.signal, locale)
.catch(err => {
console.error("[Phoenix AI] Query error:", err);
});
Expand Down Expand Up @@ -176,13 +180,16 @@ exports.destroySession = async function () {
/**
* Internal: run a Claude SDK query and stream results back to the browser.
*/
async function _runQuery(requestId, prompt, projectPath, model, signal) {
async function _runQuery(requestId, prompt, projectPath, model, signal, locale) {
let editCount = 0;
let toolCounter = 0;
let queryFn;

try {
queryFn = await getQueryFn();
if (!editorMcpServer) {
editorMcpServer = createEditorMcpServer(queryModule, nodeConnector);
}
} catch (err) {
nodeConnector.triggerPeer("aiError", {
requestId: requestId,
Expand All @@ -201,15 +208,24 @@ async function _runQuery(requestId, prompt, projectPath, model, signal) {
const queryOptions = {
cwd: projectPath || process.cwd(),
maxTurns: 10,
allowedTools: ["Read", "Edit", "Write", "Glob", "Grep"],
allowedTools: [
"Read", "Edit", "Write", "Glob", "Grep",
"mcp__phoenix-editor__getEditorState",
"mcp__phoenix-editor__takeScreenshot"
],
mcpServers: { "phoenix-editor": editorMcpServer },
permissionMode: "acceptEdits",
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.",
"which is slow and loses undo history." +
(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,
abortController: currentAbortController,
hooks: {
Expand Down
93 changes: 93 additions & 0 deletions src-node/mcp-editor-tools.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* GNU AGPL-3.0 License
*
* Copyright (c) 2021 - present core.ai . All rights reserved.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
* for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
*
*/

/**
* MCP server factory for exposing Phoenix editor context to Claude Code.
*
* Provides two tools:
* - getEditorState: returns active file, working set, and live preview file
* - takeScreenshot: captures a screenshot of the Phoenix window as base64 PNG
*
* Uses the Claude Code SDK's in-process MCP server support (createSdkMcpServer / tool).
*/

const { z } = require("zod");

/**
* Create an in-process MCP server exposing editor context tools.
*
* @param {Object} sdkModule - The imported @anthropic-ai/claude-code ESM module
* @param {Object} nodeConnector - The NodeConnector instance for communicating with the browser
* @returns {McpSdkServerConfigWithInstance} MCP server config ready for queryOptions.mcpServers
*/
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.",
{},
async function () {
try {
const state = await nodeConnector.execPeer("getEditorState", {});
return {
content: [{ type: "text", text: JSON.stringify(state) }]
};
} catch (err) {
return {
content: [{ type: "text", text: "Error getting editor state: " + err.message }],
isError: true
};
}
}
);

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") },
async function (args) {
try {
const result = await nodeConnector.execPeer("takeScreenshot", {
selector: args.selector || undefined
});
if (result.base64) {
return {
content: [{ type: "image", data: result.base64, mimeType: "image/png" }]
};
}
return {
content: [{ type: "text", text: result.error || "Screenshot failed" }],
isError: true
};
} catch (err) {
return {
content: [{ type: "text", text: "Error taking screenshot: " + err.message }],
isError: true
};
}
}
);

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

exports.createEditorMcpServer = createEditorMcpServer;
12 changes: 11 additions & 1 deletion src-node/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 8 additions & 7 deletions src-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,16 @@
},
"IMPORTANT!!": "Adding things here will bloat up the package size",
"dependencies": {
"@anthropic-ai/claude-code": "^1.0.0",
"@expo/sudo-prompt": "^9.3.2",
"@phcode/fs": "^4.0.2",
"open": "^10.1.0",
"npm": "11.8.0",
"ws": "^8.17.1",
"cross-spawn": "^7.0.6",
"lmdb": "^3.5.1",
"mime-types": "^2.1.35",
"cross-spawn": "^7.0.6",
"npm": "11.8.0",
"open": "^10.1.0",
"which": "^2.0.1",
"@expo/sudo-prompt": "^9.3.2",
"@anthropic-ai/claude-code": "^1.0.0"
"ws": "^8.17.1",
"zod": "^3.25.76"
}
}
}
1 change: 1 addition & 0 deletions src/brackets.js
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ define(function (require, exports, module) {
SidebarTabs: require("view/SidebarTabs"),
SidebarView: require("project/SidebarView"),
WorkingSetView: require("project/WorkingSetView"),
AISnapshotStore: require("core-ai/AISnapshotStore"),
doneLoading: false
};

Expand Down
Loading
Loading