diff --git a/src-node/claude-code-agent.js b/src-node/claude-code-agent.js index fac1e11ac8..ace042841c 100644 --- a/src-node/claude-code-agent.js +++ b/src-node/claude-code-agent.js @@ -60,6 +60,9 @@ let _questionResolve = null; // Pending plan resolver — used by ExitPlanMode stream interception let _planResolve = null; +// Pending bash confirmation resolver — used by Bash PreToolUse hook (Edit Mode) +let _bashConfirmResolve = null; + // Stores rejection feedback when user rejects a plan let _planRejectionFeedback = null; @@ -209,7 +212,7 @@ exports.checkAvailability = async function () { * aiProgress, aiTextStream, aiToolEdit, aiError, aiComplete */ exports.sendPrompt = async function (params) { - const { prompt, projectPath, sessionAction, model, locale, selectionContext, images, envOverrides } = params; + const { prompt, projectPath, sessionAction, model, locale, selectionContext, images, envOverrides, permissionMode } = params; const requestId = Date.now().toString(36) + Math.random().toString(36).slice(2, 7); // Handle session @@ -252,7 +255,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) + _runQuery(requestId, enrichedPrompt, projectPath, model, currentAbortController.signal, locale, images, envOverrides, permissionMode) .catch(err => { console.error("[Phoenix AI] Query error:", err); }); @@ -272,6 +275,7 @@ exports.cancelQuery = async function () { // Clear any pending question or plan _questionResolve = null; _planResolve = null; + _bashConfirmResolve = null; _queuedClarification = null; return { success: true }; } @@ -302,6 +306,18 @@ exports.answerPlan = async function (params) { return { success: true }; }; +/** + * Receive the user's response to a bash confirmation prompt (Edit Mode). + * Called from browser via execPeer("answerBashConfirm", {allowed}). + */ +exports.answerBashConfirm = async function (params) { + if (_bashConfirmResolve) { + _bashConfirmResolve(params); + _bashConfirmResolve = null; + } + return { success: true }; +}; + /** * Resume a previous session by setting the session ID. * The next sendPrompt call will use queryOptions.resume with this session ID. @@ -313,6 +329,7 @@ exports.resumeSession = async function (params) { } _questionResolve = null; _planResolve = null; + _bashConfirmResolve = null; _queuedClarification = null; currentSessionId = params.sessionId; return { success: true }; @@ -370,7 +387,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) { +async function _runQuery(requestId, prompt, projectPath, model, signal, locale, images, envOverrides, permissionMode) { let editCount = 0; let toolCounter = 0; let queryFn; @@ -455,7 +472,7 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, } }, mcpServers: { "phoenix-editor": editorMcpServer }, - permissionMode: "acceptEdits", + permissionMode: 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 " + @@ -470,13 +487,18 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, "controlEditor). Never use relative paths." + "\n\nWhen a tool response mentions the user has typed a clarification, immediately " + "call getUserClarification to read it and incorporate the user's feedback into your current work." + + "\n\nYou are running inside Phoenix Code, a web-focused code editor with a built-in " + + "live preview for HTML/CSS/JS. When the user asks to create mockups, prototypes, " + + "or web pages, prefer vanilla HTML/CSS/JS so the live preview can render and " + + "edit them — unless the user specifically requests a framework. " + + "Build responsive layouts by default for web content." + "\n\nWhen planning, consider if verification is needed. takeScreenshot can " + "capture the full editor, specific panels, the code area, or the live preview. " + "For HTML/CSS/JS with live preview, execJsInLivePreview can run JS in the " + "browser to confirm behavior." + - "\n\nFor tasks that involve creating new applications, extensive modifications, " + - "or architectural changes, enter plan mode first to propose a plan " + - "for user approval before writing code." + + "\n\nUse your best judgement for when to enter plan mode. Use it when the task " + + "involves creating new applications, extensive modifications, or architectural " + + "changes — propose a plan for user approval before writing code." + (locale && !locale.startsWith("en") ? "\n\nThe user's display language is " + locale + ". " + "Respond in this language unless they write in a different language." @@ -491,6 +513,39 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, hooks: [ async (input) => { console.log("[Phoenix AI] Intercepted Edit tool"); + // Plan file edits: capture content, write to disk, skip editor + const editPath = (input.tool_input.file_path || "").replace(/\\/g, "/"); + if (editPath.includes("/.claude/plans/")) { + try { + let content = ""; + if (fs.existsSync(input.tool_input.file_path)) { + content = fs.readFileSync(input.tool_input.file_path, "utf8"); + } + if (input.tool_input.old_string && input.tool_input.new_string) { + content = content.replace(input.tool_input.old_string, input.tool_input.new_string); + } + const dir = path.dirname(input.tool_input.file_path); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(input.tool_input.file_path, content, "utf8"); + _lastPlanContent = content; + console.log("[Phoenix AI] Captured plan edit content:", content.length + "ch"); + } catch (err) { + console.warn("[Phoenix AI] Failed to edit plan file:", err.message); + } + let planReason = "Plan file updated."; + if (_queuedClarification) { + planReason += CLARIFICATION_HINT; + } + return { + hookSpecificOutput: { + hookEventName: "PreToolUse", + permissionDecision: "deny", + permissionDecisionReason: planReason + } + }; + } const myToolId = toolCounter; // capture before any await const edit = { file: input.tool_input.file_path, @@ -658,6 +713,48 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, } ] }, + { + matcher: "Bash", + hooks: [ + async (input) => { + if (permissionMode !== "acceptEdits") { + // Plan mode: SDK handles. Full Auto: allow freely. + return {}; + } + // Edit Mode: ask user confirmation before running bash + const command = input.tool_input.command || ""; + console.log("[Phoenix AI] Bash confirmation requested:", command.slice(0, 80)); + nodeConnector.triggerPeer("aiBashConfirm", { + requestId: requestId, + command: command, + toolId: toolCounter + }); + const response = await new Promise((resolve, reject) => { + _bashConfirmResolve = resolve; + if (signal.aborted) { + _bashConfirmResolve = null; + reject(new Error("Aborted")); + return; + } + const onAbort = () => { + _bashConfirmResolve = null; + reject(new Error("Aborted")); + }; + signal.addEventListener("abort", onAbort, { once: true }); + }); + if (response.allowed) { + return {}; + } + return { + hookSpecificOutput: { + hookEventName: "PreToolUse", + permissionDecision: "deny", + permissionDecisionReason: "User denied this command." + } + }; + } + ] + }, { matcher: "AskUserQuestion", hooks: [ diff --git a/src/editor/EditorManager.js b/src/editor/EditorManager.js index 4dd00f2807..36e613070b 100644 --- a/src/editor/EditorManager.js +++ b/src/editor/EditorManager.js @@ -495,7 +495,6 @@ define(function (require, exports, module) { * removed. For example, after a dialog with editable text is closed. */ function focusEditor() { - DeprecationWarning.deprecationWarning("Use MainViewManager.focusActivePane() instead of EditorManager.focusEditor().", true); MainViewManager.focusActivePane(); } diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 113f66fa69..078fae2389 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1926,6 +1926,14 @@ define({ "AI_CHAT_PLAN_REVISE": "Revise", "AI_CHAT_PLAN_FEEDBACK_PLACEHOLDER": "What would you like changed?", "AI_CHAT_PLAN_REVISE_DEFAULT": "Please revise the plan.", + "AI_CHAT_MODE_PLAN": "Plan Mode", + "AI_CHAT_MODE_EDIT": "Edit Mode", + "AI_CHAT_MODE_FULL_AUTO": "Full Auto", + "AI_CHAT_BASH_CONFIRM_TITLE": "Allow command?", + "AI_CHAT_BASH_ALLOW": "Allow", + "AI_CHAT_BASH_DENY": "Deny", + "AI_CHAT_BASH_ALLOWED": "Command allowed", + "AI_CHAT_BASH_DENIED": "Command denied", "AI_CHAT_CODE_DEFAULT_LANG": "text", "AI_CHAT_CODE_COLLAPSE": "Collapse", "AI_CHAT_CODE_EXPAND": "Expand", @@ -1934,6 +1942,7 @@ define({ "AI_CHAT_PREVIEW_OPEN": "Preview", "AI_CHAT_PREVIEW_VIEWING": "Previewing", "AI_CHAT_QUESTION_OTHER": "Type a custom answer\u2026", + "AI_CHAT_QUESTION_SUBMIT": "Submit", "AI_CHAT_IMAGE_LIMIT": "Maximum {0} images allowed", "AI_CHAT_IMAGE_REMOVE": "Remove image", "AI_CHAT_ATTACH_FILE": "Attach files", diff --git a/src/styles/Extn-AIChatPanel.less b/src/styles/Extn-AIChatPanel.less index 0aee240594..35ff4c8f50 100644 --- a/src/styles/Extn-AIChatPanel.less +++ b/src/styles/Extn-AIChatPanel.less @@ -1022,7 +1022,10 @@ } .ai-question-submit { - align-self: flex-end; + display: flex; + align-items: center; + gap: 5px; + margin-left: auto; background: none; border: 1px solid rgba(76, 175, 80, 0.3); color: rgba(76, 175, 80, 0.85); @@ -1088,6 +1091,7 @@ display: flex; align-items: center; justify-content: center; + gap: 5px; transition: background-color 0.15s ease, color 0.15s ease; &:hover:not(:disabled) { @@ -1408,6 +1412,92 @@ } } +/* ── Bash confirmation card ─────────────────────────────────────────── */ +.ai-bash-confirm { + border: 1px solid rgba(243, 156, 18, 0.3); + border-radius: 8px; + background: rgba(243, 156, 18, 0.06); + margin: 8px 0; + overflow: hidden; + + .ai-bash-confirm-header { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + font-weight: 600; + font-size: @sidebar-content-font-size; + color: #f39c12; + border-bottom: 1px solid rgba(243, 156, 18, 0.15); + } + + .ai-bash-confirm-body { + padding: 8px 12px; + + pre { + margin: 0; + padding: 8px; + border-radius: 4px; + background: rgba(0, 0, 0, 0.2); + color: #e0e0e0; + font-size: @sidebar-content-font-size; + white-space: pre-wrap; + word-break: break-all; + } + } + + .ai-bash-confirm-actions { + display: flex; + gap: 8px; + padding: 8px 12px; + border-top: 1px solid rgba(243, 156, 18, 0.15); + } + + .ai-bash-allow-btn { + background: rgba(76, 175, 80, 0.15); + border: 1px solid rgba(76, 175, 80, 0.35); + color: #81c784; + padding: 4px 14px; + border-radius: 4px; + cursor: pointer; + font-size: @sidebar-xs-font-size; + + &:hover { + background: rgba(76, 175, 80, 0.25); + } + } + + .ai-bash-deny-btn { + background: rgba(231, 76, 60, 0.15); + border: 1px solid rgba(231, 76, 60, 0.35); + color: #e57373; + padding: 4px 14px; + border-radius: 4px; + cursor: pointer; + font-size: @sidebar-xs-font-size; + + &:hover { + background: rgba(231, 76, 60, 0.25); + } + } + +} + +.ai-bash-result { + padding: 4px 10px; + border-radius: 4px; + font-size: @sidebar-xs-font-size; + margin: 4px 0; + + &.ai-bash-result-allowed { + color: #81c784; + } + + &.ai-bash-result-denied { + color: #e57373; + } +} + /* ── Queued clarification bubble (static, above input) ─────────────── */ .ai-queued-msg { border: 1px dashed rgba(255, 255, 255, 0.15); @@ -1620,11 +1710,50 @@ } } + .ai-permission-bar { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + cursor: pointer; + user-select: none; + + &:hover { + background: rgba(255, 255, 255, 0.04); + border-radius: 4px; + } + + .ai-permission-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + + &.mode-auto { + background-color: #e74c3c; + } + + &.mode-edit { + background-color: #f39c12; + } + + &.mode-plan { + background-color: #3498db; + } + } + + .ai-permission-label { + font-size: @sidebar-xs-font-size; + color: @project-panel-text-2; + line-height: 1; + } + } + .ai-chat-context-bar { display: none; flex-wrap: wrap; - gap: 4px; - padding: 0 4px 4px 4px; + gap: 6px; + padding: 0 4px 6px 4px; &.has-chips { display: flex; diff --git a/tracking-repos.json b/tracking-repos.json index 66abb0da14..f26946e066 100644 --- a/tracking-repos.json +++ b/tracking-repos.json @@ -1,5 +1,5 @@ { "phoenixPro": { - "commitID": "21f226b54906adc5bd2cb0dd0d68d587dc9d22fe" + "commitID": "4ca7bc6eea7ad4c94fbf321ae2c854f533fb8bd2" } }