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
9 changes: 9 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@
- **Exception — Markdown viewer iframe** (`src-mdviewer/`): Has its own i18n system. Strings go in `src-mdviewer/src/locales/en.json` (root), not `src/nls/`. Other locale files in that folder are auto-translated by GitHub Actions. Use `t("key")` / `tp("key", { param })` from `src-mdviewer/src/core/i18n.js`.
- Never compare `$(el).text()` against English strings for logic — use data attributes or CSS classes instead.

## File I/O APIs — which to use

Phoenix has two parallel file APIs. Pick the right one for the situation:

- **`Phoenix.VFS.readFileAsync(path, encoding)` / `Phoenix.VFS.writeFileAsync(path, content, encoding)` / `Phoenix.VFS.unlinkAsync(path)`** — for raw app data (config files, session JSONs, caches, snapshots). No size cap. `unlinkAsync` removes non-empty directories recursively.
- **`FileSystem.getFileForPath(path).read/.write/.unlink`** (and `getDirectoryForPath`) — *only* for files that may be opened as documents in the editor. Goes through the document layer (mtime tracking, dirty-buffer reconciliation). Has a 16 MB cap on reads/writes.

If a file is purely app-internal data and never edited by the user as a document, use the VFS APIs. Mixing them on the same file leads to mtime confusion and surprise size limits.

## 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
91 changes: 88 additions & 3 deletions src-node/claude-code-agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,63 @@ function _isToolResponseError(toolResponse) {
return false;
}

// Bash commands the agent can run without prompting the user in Edit
// Mode. Mirrors the CLI's default "permissions.allow" set
// (cli.js:2925) plus a small handful of universally read-only shell
// utilities. The safety belt in _isSafeReadOnlyBash splits on
// `;` / `&&` / `||` and checks every segment, so chaining safe
// commands (e.g. `git status && git log`, `sleep 1; echo done`)
// works while `git status; rm -rf /` correctly falls through.
const _SAFE_BASH_PATTERNS = [
// git read-only
/^git\s+status(\s|$)/,
/^git\s+log(\s|$)/,
/^git\s+diff(\s|$)/,
/^git\s+show(\s|$)/,
/^git\s+branch(\s|$)/,
/^git\s+ls-files(\s|$)/,
/^git\s+rev-parse(\s|$)/,
/^git\s+remote\s+show(\s|$)/,
/^git\s+--version$/,
// generic read-only shell
/^ls(\s|$)/,
/^pwd$/,
/^echo(\s|$)/,
/^which\s/,
/^cat(\s|$)/,
/^head(\s|$)/,
/^tail(\s|$)/,
/^wc(\s|$)/,
/^file\s/,
/^stat\s/,
// numeric-only sleep — no `sleep $(...)` since process substitution
// is rejected separately, but be explicit so `sleep $VAR` also fails.
/^sleep\s+\d+(\.\d+)?$/,
// version probes
/^node\s+--version$/,
/^npm\s+--version$/,
/^yarn\s+--version$/,
/^pnpm\s+--version$/
];

function _isSafeReadOnlyBash(rawCmd) {
const cmd = (rawCmd || "").trim();
if (!cmd) { return false; }
// Reject command/process substitution, redirection, and pipes —
// these can hide arbitrary commands or send output to dangerous
// places. Backticks, `$(...)`, `<`, `>`, `|`. Plain `$VAR` is
// allowed (substitution-without-command).
if (/[`<>|]|\$\(/.test(cmd)) { return false; }
// Split on `;`, `&&`, `||` and verify EVERY segment matches a safe
// pattern. Quotes around delimiters are not handled — a command
// like `echo "a; b"` will split mid-string and fail safe-check
// (which is fine: false negatives are OK, false positives are not).
const segments = cmd.split(/\s*(?:;|&&|\|\|)\s*/).filter(Boolean);
return segments.every(function (seg) {
return _SAFE_BASH_PATTERNS.some(function (rx) { return rx.test(seg); });
});
}

/**
* Lazily import the ESM @anthropic-ai/claude-code module.
*/
Expand Down Expand Up @@ -248,7 +305,7 @@ exports.checkAvailability = async function () {
* aiProgress, aiTextStream, aiToolEdit, aiError, aiComplete
*/
exports.sendPrompt = async function (params) {
const { prompt, projectPath, sessionAction, model, locale, selectionContext, images, envOverrides, permissionMode } = params;
const { prompt, projectPath, sessionAction, model, locale, selectionContext, images, envOverrides, permissionMode, additionalDirectories } = params;
const requestId = Date.now().toString(36) + Math.random().toString(36).slice(2, 7);

// Handle session
Expand Down Expand Up @@ -291,7 +348,7 @@ exports.sendPrompt = async function (params) {
}

// Run the query asynchronously — don't await here so we return requestId immediately
_runQuery(requestId, enrichedPrompt, projectPath, model, currentAbortController.signal, locale, images, envOverrides, permissionMode)
_runQuery(requestId, enrichedPrompt, projectPath, model, currentAbortController.signal, locale, images, envOverrides, permissionMode, additionalDirectories)
.catch(err => {
console.error("[Phoenix AI] Query error:", err);
});
Expand Down Expand Up @@ -452,7 +509,7 @@ exports.clearClarification = async function () {
/**
* Internal: run a Claude SDK query and stream results back to the browser.
*/
async function _runQuery(requestId, prompt, projectPath, model, signal, locale, images, envOverrides, permissionMode) {
async function _runQuery(requestId, prompt, projectPath, model, signal, locale, images, envOverrides, permissionMode, additionalDirectories) {
// Sync the runtime mutable that hooks read for permission decisions —
// setPermissionMode (peer) updates this same variable when the user
// cycles modes mid-stream.
Expand Down Expand Up @@ -520,8 +577,23 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale,
_hookErrorTimer = null;
}

// Validate the user-attached extra directories the browser sent.
// Drop entries that aren't absolute, don't exist, or duplicate cwd.
// Returns undefined for empty results so the SDK ignores the option
// rather than seeing a literal []. Each sendPrompt rebuilds this
// list, so adding/removing in the UI takes effect on the next turn.
const _cwdForValidation = projectPath || process.cwd();
const validatedExtraDirs = (Array.isArray(additionalDirectories)
? additionalDirectories.filter(function (p) {
if (typeof p !== "string" || !path.isAbsolute(p)) { return false; }
if (p === _cwdForValidation) { return false; }
try { return fs.existsSync(p); } catch (e) { return false; }
})
: []);

const queryOptions = {
cwd: projectPath || process.cwd(),
additionalDirectories: validatedExtraDirs.length ? validatedExtraDirs : undefined,
maxTurns: undefined,
stderr: (data) => {
console.log("[AI stderr]", data);
Expand Down Expand Up @@ -901,6 +973,19 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale,
}
// Edit Mode: ask user confirmation before running bash
const command = input.tool_input.command || "";
// Skip prompting for well-known read-only commands
// that mirror the Claude Code CLI's default safe
// patterns. Cuts down on prompt fatigue during
// typical "look around the repo" turns.
if (_isSafeReadOnlyBash(command)) {
console.log("[Phoenix AI] Auto-allowing safe bash:", command.slice(0, 80));
return {
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "allow"
}
};
}
console.log("[Phoenix AI] Bash confirmation requested:", command.slice(0, 80));
nodeConnector.triggerPeer("aiBashConfirm", {
requestId: requestId,
Expand Down
11 changes: 11 additions & 0 deletions src/nls/root/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -2223,6 +2223,11 @@ define({
"AI_CHAT_FILE_NOT_FOUND_MSG": "Could not open <span class=\"dialog-filename\">{0}</span>. The file may have been moved or deleted.",
"AI_CHAT_UNDO_RESTORE_WARNING_TITLE": "AI Undo & Restore",
"AI_CHAT_UNDO_RESTORE_WARNING_BODY": "This will only undo changes made by the AI. Changes made outside the AI won’t be restored and may be lost. For full version history, use version control like Git.",
"AI_CHAT_FULL_AUTO_WARNING_TITLE": "Switch to Full Auto Mode?",
"AI_CHAT_FULL_AUTO_WARNING_BODY": "Full Auto mode lets the AI run any tool — Bash commands, file edits, file deletions, web fetches — without asking you first.<br><br>This is convenient for trusted scratch projects, but can be risky: a misjudged step could overwrite or delete files, run a destructive shell command, or push unintended changes. Use version control (Git) so you can recover if something goes wrong.<br><br>Only enable Full Auto in projects you trust. You can switch back to Edit Mode at any time using <kbd>Shift+Tab</kbd> or by clicking the mode bar.",
"AI_CHAT_FULL_AUTO_WARNING_PROCEED": "Enable Full Auto",
"AI_CHAT_ONBOARDING_REVIEW_PROMPT": "Ready to build",
"AI_CHAT_ONBOARDING_SEND": "Send",
"AI_CHAT_SHOW_DIFF": "Show diff",
"AI_CHAT_HIDE_DIFF": "Hide diff",
"AI_CHAT_DIFF_MORE_TITLE": "Diff options",
Expand All @@ -2249,6 +2254,8 @@ define({
"AI_CHAT_TOOL_TASK_NAME": "Subagent: {0}",
"AI_CHAT_TOOL_PLANNING": "Planning",
"AI_CHAT_PLAN_TITLE": "Proposed Plan",
"AI_CHAT_PLAN_MAXIMIZE": "Open plan in full screen",
"AI_CHAT_PLAN_CLOSE_FULLSCREEN": "Close (Esc)",
"AI_CHAT_PLAN_APPROVE": "Approve",
"AI_CHAT_PLAN_REVISE": "Revise",
"AI_CHAT_PLAN_FEEDBACK_PLACEHOLDER": "What would you like changed?",
Expand Down Expand Up @@ -2284,6 +2291,10 @@ define({
"AI_CHAT_IMAGE_LIMIT": "Maximum {0} images allowed",
"AI_CHAT_IMAGE_REMOVE": "Remove image",
"AI_CHAT_ATTACH_FILE": "Attach files",
"AI_CHAT_ATTACH_TITLE": "Attach file or folder",
"AI_CHAT_ATTACH_FILE_OPTION": "Attach a file",
"AI_CHAT_ATTACH_FOLDER": "Add folder as context",
"AI_CHAT_ATTACH_FOLDER_PICK_TITLE": "Choose folder to add as context",
"AI_CHAT_SCREENSHOT_TITLE": "Take Screenshot",
"AI_CHAT_SCREENSHOT_LIVE_PREVIEW": "Live Preview",
"AI_CHAT_SCREENSHOT_AREA": "Select Area",
Expand Down
Loading
Loading