diff --git a/gulpfile.js/index.js b/gulpfile.js/index.js
index 9eec937fa..e134fb1d7 100644
--- a/gulpfile.js/index.js
+++ b/gulpfile.js/index.js
@@ -610,10 +610,9 @@ function containsRegExpExcludingEmpty(str) {
const minifyablePaths = [
'src/extensionsIntegrated/phoenix-pro/browser-context'
];
-
function _minifyBrowserContextFile(fileContent) {
const minified = terser.minify(fileContent, {
- mangle: true,
+ mangle: false,
compress: {
unused: false
},
@@ -690,7 +689,9 @@ function inlineTextRequire(file, content, srcDir, isDevBuild = true) {
throw `Error inlining ${requireStatement} in ${file}: Regex: ${detectedRegEx}`+
"\nRegular expression of the form /*/ is not allowed for minification please use RegEx constructor";
}
- content = content.replaceAll(requireStatement, `${JSON.stringify(textContent)}`);
+ // Escape $ in replacement to prevent special replacement patterns ($&, $1, etc.)
+ const safeReplacement = JSON.stringify(textContent).replaceAll("$", "$$$$");
+ content = content.replaceAll(requireStatement, safeReplacement);
}
}
diff --git a/src-node/claude-code-agent.js b/src-node/claude-code-agent.js
index f9fe0cd8b..108bad9d1 100644
--- a/src-node/claude-code-agent.js
+++ b/src-node/claude-code-agent.js
@@ -27,9 +27,12 @@
*/
const { execSync } = require("child_process");
+const fs = require("fs");
const path = require("path");
const { createEditorMcpServer } = require("./mcp-editor-tools");
+const isWindows = process.platform === "win32";
+
const CONNECTOR_ID = "ph_ai_claude";
const CLARIFICATION_HINT =
@@ -54,6 +57,18 @@ const TEXT_STREAM_THROTTLE_MS = 50;
// Pending question resolver — used by AskUserQuestion hook
let _questionResolve = null;
+// Pending plan resolver — used by ExitPlanMode stream interception
+let _planResolve = null;
+
+// Stores rejection feedback when user rejects a plan
+let _planRejectionFeedback = null;
+
+// Stores the last plan content written to .claude/plans/
+let _lastPlanContent = null;
+
+// Flag set when user approves a plan
+let _planApproved = false;
+
// Queued clarification from the user (typed while AI is streaming)
// Shape: { text: string, images: [{mediaType, base64Data}] } or null
let _queuedClarification = null;
@@ -71,9 +86,45 @@ async function getQueryFn() {
}
/**
- * Find the user's globally installed Claude CLI, skipping node_modules copies.
+ * Find the user's globally installed Claude CLI on Windows.
*/
-function findGlobalClaudeCli() {
+function _findGlobalClaudeCliWin() {
+ const userHome = process.env.USERPROFILE || process.env.HOME || "";
+ const locations = [
+ path.join(userHome, ".local", "bin", "claude.exe"),
+ path.join(process.env.APPDATA || "", "npm", "claude.cmd"),
+ path.join(process.env.LOCALAPPDATA || "", "Programs", "claude", "claude.exe")
+ ];
+
+ // Try 'where claude' first to find claude in PATH
+ try {
+ const allPaths = execSync("where claude", { encoding: "utf8" })
+ .trim()
+ .split("\r\n")
+ .filter(p => p && !p.includes("node_modules"));
+ if (allPaths.length > 0) {
+ console.log("[Phoenix AI] Found global Claude CLI at:", allPaths[0]);
+ return allPaths[0];
+ }
+ } catch {
+ // where failed, try manual locations
+ }
+
+ // Check common Windows locations
+ for (const loc of locations) {
+ if (loc && fs.existsSync(loc)) {
+ console.log("[Phoenix AI] Found global Claude CLI at:", loc);
+ return loc;
+ }
+ }
+
+ return null;
+}
+
+/**
+ * Find the user's globally installed Claude CLI on macOS/Linux.
+ */
+function _findGlobalClaudeCliLinuxMac() {
const locations = [
"/usr/local/bin/claude",
"/usr/bin/claude",
@@ -108,10 +159,20 @@ function findGlobalClaudeCli() {
}
}
- console.log("[Phoenix AI] Global Claude CLI not found");
return null;
}
+/**
+ * Find the user's globally installed Claude CLI, skipping node_modules copies.
+ */
+function findGlobalClaudeCli() {
+ const claudePath = isWindows ? _findGlobalClaudeCliWin() : _findGlobalClaudeCliLinuxMac();
+ if (!claudePath) {
+ console.log("[Phoenix AI] Global Claude CLI not found");
+ }
+ return claudePath;
+}
+
/**
* Check whether Claude CLI is available.
* Called from browser via execPeer("checkAvailability").
@@ -119,14 +180,22 @@ function findGlobalClaudeCli() {
exports.checkAvailability = async function () {
try {
const claudePath = findGlobalClaudeCli();
- if (claudePath) {
- // Also verify the SDK can be imported
- await getQueryFn();
- return { available: true, claudePath: claudePath };
+ if (!claudePath) {
+ return { available: false, claudePath: null, error: "Claude Code CLI not found" };
+ }
+ // Check if user is logged in
+ let loggedIn = false;
+ try {
+ const authOutput = execSync(claudePath + " auth status", {
+ encoding: "utf8",
+ timeout: 10000
+ });
+ const authStatus = JSON.parse(authOutput);
+ loggedIn = authStatus.loggedIn === true;
+ } catch (e) {
+ // auth status failed — treat as not logged in
}
- // No global CLI found — try importing SDK anyway (it might find its own)
- await getQueryFn();
- return { available: true, claudePath: null };
+ return { available: true, claudePath: claudePath, loggedIn: loggedIn };
} catch (err) {
return { available: false, claudePath: null, error: err.message };
}
@@ -200,8 +269,9 @@ exports.cancelQuery = async function () {
currentAbortController = null;
// Clear session so next query starts fresh instead of resuming a killed session
currentSessionId = null;
- // Clear any pending question
+ // Clear any pending question or plan
_questionResolve = null;
+ _planResolve = null;
_queuedClarification = null;
return { success: true };
}
@@ -220,6 +290,18 @@ exports.answerQuestion = async function (params) {
return { success: true };
};
+/**
+ * Receive the user's response to a proposed plan.
+ * Called from browser via execPeer("answerPlan", {approved, feedback}).
+ */
+exports.answerPlan = async function (params) {
+ if (_planResolve) {
+ _planResolve(params);
+ _planResolve = 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.
@@ -230,6 +312,7 @@ exports.resumeSession = async function (params) {
currentAbortController = null;
}
_questionResolve = null;
+ _planResolve = null;
_queuedClarification = null;
currentSessionId = params.sessionId;
return { success: true };
@@ -339,6 +422,7 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale,
"AskUserQuestion", "Task",
"TodoRead", "TodoWrite",
"WebFetch", "WebSearch",
+ "EnterPlanMode", "ExitPlanMode",
"mcp__phoenix-editor__getEditorState",
"mcp__phoenix-editor__takeScreenshot",
"mcp__phoenix-editor__execJsInLivePreview",
@@ -386,6 +470,10 @@ 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\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." +
(locale && !locale.startsWith("en")
? "\n\nThe user's display language is " + locale + ". " +
"Respond in this language unless they write in a different language."
@@ -494,6 +582,31 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale,
hooks: [
async (input) => {
console.log("[Phoenix AI] Intercepted Write tool");
+ // Capture plan content when writing to .claude/plans/
+ // Don't open plan files in editor — shown in plan card UI
+ const writePath = input.tool_input.file_path || "";
+ if (writePath.includes("/.claude/plans/")) {
+ _lastPlanContent = input.tool_input.content || "";
+ console.log("[Phoenix AI] Captured plan content:",
+ _lastPlanContent.length + "ch");
+ if (_queuedClarification) {
+ return {
+ hookSpecificOutput: {
+ hookEventName: "PreToolUse",
+ permissionDecision: "deny",
+ permissionDecisionReason:
+ "Plan file saved." + CLARIFICATION_HINT
+ }
+ };
+ }
+ return {
+ hookSpecificOutput: {
+ hookEventName: "PreToolUse",
+ permissionDecision: "deny",
+ permissionDecisionReason: "Plan file saved."
+ }
+ };
+ }
const myToolId = toolCounter; // capture before any await
const edit = {
file: input.tool_input.file_path,
@@ -870,6 +983,45 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale,
toolId: toolCounter,
toolInput: toolInput
});
+
+ // ExitPlanMode: show plan to user and wait for approval
+ // Plan text comes from a prior Write to .claude/plans/ (captured in hook)
+ if (activeToolName === "ExitPlanMode") {
+ const planText = toolInput.plan || _lastPlanContent || "";
+ _lastPlanContent = null;
+ if (planText) {
+ _log("ExitPlanMode plan detected (" + planText.length + "ch), sending to browser");
+ nodeConnector.triggerPeer("aiPlanProposed", {
+ requestId: requestId,
+ plan: planText
+ });
+ // Pause stream processing until user approves/rejects
+ const planResponse = await new Promise((resolve, reject) => {
+ _planResolve = resolve;
+ if (signal.aborted) {
+ _planResolve = null;
+ reject(new Error("Aborted"));
+ return;
+ }
+ const onAbort = () => {
+ _planResolve = null;
+ reject(new Error("Aborted"));
+ };
+ signal.addEventListener("abort", onAbort, { once: true });
+ });
+ if (!planResponse.approved) {
+ _log("Plan rejected by user, aborting");
+ currentAbortController.abort();
+ _planRejectionFeedback = planResponse.feedback || "";
+ } else {
+ _log("Plan approved by user, will send proceed prompt");
+ _planApproved = true;
+ }
+ } else {
+ _log("ExitPlanMode with no plan content, skipping UI");
+ }
+ }
+
activeToolName = null;
activeToolIndex = null;
activeToolInputJson = "";
@@ -908,6 +1060,32 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale,
_log("Complete: tools=" + toolCounter, "edits=" + editCount,
"textDeltas=" + textDeltaCount, "textSent=" + textStreamSendCount);
+ // Check if plan was approved — send follow-up to proceed with implementation
+ if (_planApproved) {
+ _planApproved = false;
+ _log("Plan approved, sending proceed prompt");
+ nodeConnector.triggerPeer("aiComplete", {
+ requestId: requestId,
+ sessionId: currentSessionId,
+ planApproved: true
+ });
+ return;
+ }
+
+ // Check if stream ended due to plan rejection (abort + break)
+ if (_planRejectionFeedback !== null) {
+ const feedback = _planRejectionFeedback;
+ _planRejectionFeedback = null;
+ _log("Plan rejected, sending revision request");
+ nodeConnector.triggerPeer("aiComplete", {
+ requestId: requestId,
+ sessionId: currentSessionId,
+ planRejected: true,
+ planFeedback: feedback
+ });
+ return;
+ }
+
// Signal completion
nodeConnector.triggerPeer("aiComplete", {
requestId: requestId,
@@ -920,6 +1098,20 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale,
const isAbort = signal.aborted || /abort/i.test(errMsg);
if (isAbort) {
+ // Check if this was a plan rejection — if so, send feedback as follow-up
+ if (_planRejectionFeedback !== null) {
+ const feedback = _planRejectionFeedback;
+ _planRejectionFeedback = null;
+ _log("Plan rejected, sending revision request");
+ // Don't clear session — resume with feedback
+ nodeConnector.triggerPeer("aiComplete", {
+ requestId: requestId,
+ sessionId: currentSessionId,
+ planRejected: true,
+ planFeedback: feedback
+ });
+ return;
+ }
_log("Cancelled");
// Send sessionId so browser side can save partial history for later resume
const cancelledSessionId = currentSessionId;
diff --git a/src/command/DefaultMenus.js b/src/command/DefaultMenus.js
index c803191e3..6361f3cb9 100644
--- a/src/command/DefaultMenus.js
+++ b/src/command/DefaultMenus.js
@@ -231,7 +231,9 @@ define(function (require, exports, module) {
menu.addMenuItem(Commands.TOGGLE_RULERS);
menu.addMenuDivider();
menu.addMenuItem(Commands.VIEW_TOGGLE_PROBLEMS);
- menu.addMenuItem(Commands.VIEW_TERMINAL);
+ if (Phoenix.isNativeApp) {
+ menu.addMenuItem(Commands.VIEW_TERMINAL);
+ }
menu.addMenuItem(Commands.VIEW_TOGGLE_INSPECTION);
/*
diff --git a/src/extensionsIntegrated/Terminal/TerminalInstance.js b/src/extensionsIntegrated/Terminal/TerminalInstance.js
index fa131a706..90d21e26d 100644
--- a/src/extensionsIntegrated/Terminal/TerminalInstance.js
+++ b/src/extensionsIntegrated/Terminal/TerminalInstance.js
@@ -108,6 +108,12 @@ define(function (require, exports, module) {
this._webglAddon = null;
this._lastDpr = null;
+ // Promise that resolves when the shell sends its first output (prompt ready)
+ this._firstDataResolve = null;
+ this.firstDataReceived = new Promise(function (resolve) {
+ this._firstDataResolve = resolve;
+ }.bind(this));
+
// Bound event handlers for cleanup
this._onTerminalData = this._onTerminalData.bind(this);
this._onTerminalExit = this._onTerminalExit.bind(this);
@@ -217,6 +223,10 @@ define(function (require, exports, module) {
TerminalInstance.prototype._onTerminalData = function (_event, eventData) {
if (eventData.id === this.id && this.terminal) {
this.terminal.write(eventData.data);
+ if (this._firstDataResolve) {
+ this._firstDataResolve();
+ this._firstDataResolve = null;
+ }
}
};
diff --git a/src/extensionsIntegrated/Terminal/main.js b/src/extensionsIntegrated/Terminal/main.js
index e8bdd2846..2a5bcd554 100644
--- a/src/extensionsIntegrated/Terminal/main.js
+++ b/src/extensionsIntegrated/Terminal/main.js
@@ -547,8 +547,25 @@ define(function (require, exports, module) {
* If the panel is visible and the active terminal is focused and there
* are 2+ terminals, cycles to the next one. Otherwise just shows and
* focuses the active terminal.
+ *
+ * @param {Object} [options] - Optional settings
+ * @param {string} [options.shellCommand] - A shell command to execute in a new terminal.
+ * When provided, always creates a fresh terminal and types the command into it.
*/
- async function _showTerminal() {
+ async function _showTerminal(options) {
+ if (options && options.shellCommand) {
+ await _createNewTerminal();
+ const active = _getActiveTerminal();
+ if (active && active.isAlive) {
+ // Wait for the shell to output its prompt before sending the command.
+ await active.firstDataReceived;
+ nodeConnector.execPeer("writeTerminal", {
+ id: active.id,
+ data: options.shellCommand + "\r"
+ });
+ }
+ return;
+ }
if (terminalInstances.length === 0) {
await _createNewTerminal();
return;
diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js
index b19a4a78a..18d6cd7dc 100644
--- a/src/nls/root/strings.js
+++ b/src/nls/root/strings.js
@@ -240,8 +240,8 @@ define({
"LIVE_DEV_IMAGE_FOLDER_DIALOG_PLACEHOLDER": "Type folder path (e.g., assets/images/)",
"LIVE_DEV_IMAGE_FOLDER_DIALOG_HELP": "💡 Type folder path or leave empty to download in 'images' folder.",
"LIVE_DEV_IMAGE_FOLDER_DIALOG_REMEMBER": "Don't ask again for this project",
- "DEVICE_SIZE_LIMIT_TITLE": "Responsive Preview Limit Reached",
- "DEVICE_SIZE_LIMIT_MESSAGE": "Free accounts get a few responsive previews per day. Upgrade to Phoenix Pro for unlimited responsive previews across all device sizes.",
+ "DEVICE_SIZE_LIMIT_TITLE": "Available in Phoenix Pro",
+ "DEVICE_SIZE_LIMIT_MESSAGE": "Phoenix Pro lets you preview your page at the screen sizes defined in your CSS.",
"IMAGE_SEARCH_LIMIT_TITLE": "Image search limit reached",
"IMAGE_SEARCH_LIMIT_MESSAGE": "You’ve used all {0} image searches for this month.
Start a paid Phoenix Pro plan to remove trial limits and continue searching.",
"IMAGE_SEARCH_LIMIT_MESSAGE_THROTTLE": "Image search is temporarily unavailable due to high demand.
Start a paid Phoenix Pro plan to remove trial limits and continue searching.",
@@ -1829,15 +1829,23 @@ define({
"AI_UPSELL_DIALOG_MESSAGE": "You’ve discovered {0}. To proceed, you’ll need an AI subscription or credits.",
// AI CHAT PANEL
- "AI_CHAT_TITLE": "AI Assistant",
+ "AI_CHAT_TITLE": "Claude Code",
"AI_CHAT_NEW_SESSION_TITLE": "Start a new conversation",
"AI_CHAT_NEW_BTN": "New",
"AI_CHAT_THINKING": "Thinking...",
"AI_CHAT_PLACEHOLDER": "Ask Claude...",
"AI_CHAT_SEND_TITLE": "Send message",
"AI_CHAT_STOP_TITLE": "Stop generation (Esc)",
- "AI_CHAT_CLI_NOT_FOUND": "Claude CLI Not Found",
- "AI_CHAT_CLI_INSTALL_MSG": "Install the Claude CLI to use AI features:
npm install -g @anthropic-ai/claude-code
Then run claude login to authenticate.",
+ "AI_CHAT_CLI_NOT_FOUND": "Claude Code Not Installed",
+ "AI_CHAT_CLI_INSTALL_MSG": "Claude Code CLI must be installed on your system to use AI features in {APP_NAME}. Learn more",
+ "AI_CHAT_CLI_INSTALL_BTN": "Install Claude Code",
+ "AI_CHAT_CLI_INSTALLING": "Installing…",
+ "AI_CHAT_CLI_INSTALLING_MSG": "Installing Claude Code, please wait. This may take a while...",
+ "AI_CHAT_CLI_RESTART_NOTE": "Restart {APP_NAME} after installation completes.",
+ "AI_CHAT_CLAUDE_LOGIN_TITLE": "Setup Claude Code",
+ "AI_CHAT_CLAUDE_LOGIN_MSG": "Claude Code is installed but needs to be configured. Learn more",
+ "AI_CHAT_CLAUDE_LOGIN_BTN": "Setup Claude Code",
+ "AI_CHAT_ADD_PROVIDER_BTN": "Add Custom Provider",
"AI_CHAT_RETRY": "Retry",
"AI_CHAT_DESKTOP_ONLY": "AI features require the Phoenix desktop app. Download it to get started.",
"AI_CHAT_DOWNLOAD_BTN": "Download Desktop App",
@@ -1911,6 +1919,11 @@ define({
"AI_CHAT_TOOL_QUESTION": "Question",
"AI_CHAT_TOOL_TASK": "Subagent",
"AI_CHAT_TOOL_TASK_NAME": "Subagent: {0}",
+ "AI_CHAT_TOOL_PLANNING": "Planning",
+ "AI_CHAT_PLAN_TITLE": "Proposed Plan",
+ "AI_CHAT_PLAN_APPROVE": "Approve",
+ "AI_CHAT_PLAN_REVISE": "Revise",
+ "AI_CHAT_PLAN_FEEDBACK_PLACEHOLDER": "What would you like changed?",
"AI_CHAT_QUESTION_OTHER": "Type a custom answer\u2026",
"AI_CHAT_IMAGE_LIMIT": "Maximum {0} images allowed",
"AI_CHAT_IMAGE_REMOVE": "Remove image",
diff --git a/src/styles/Extn-AIChatPanel.less b/src/styles/Extn-AIChatPanel.less
index 0a30db345..721f9de7c 100644
--- a/src/styles/Extn-AIChatPanel.less
+++ b/src/styles/Extn-AIChatPanel.less
@@ -39,17 +39,37 @@
background-color: @bc-sidebar-bg;
color: @project-panel-text-1;
font-size: @sidebar-content-font-size;
+ container-type: inline-size;
}
/* ── Header ─────────────────────────────────────────────────────────── */
.ai-chat-header {
display: flex;
align-items: center;
- justify-content: space-between;
+ justify-content: center;
+ position: relative;
padding: 10px 10px 9px 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
flex-shrink: 0;
+ .ai-chat-title-group {
+ display: flex;
+ align-items: center;
+ }
+
+ .ai-chat-title-icon {
+ display: inline-flex;
+ align-items: center;
+ margin-right: 5px;
+ opacity: 0.6;
+ color: @project-panel-text-2;
+
+ svg {
+ width: 16px;
+ height: 16px;
+ }
+ }
+
.ai-chat-title {
font-weight: 400;
font-size: @label-font-size;
@@ -58,84 +78,53 @@
}
.ai-chat-header-actions {
+ position: absolute;
+ right: 10px;
display: flex;
align-items: center;
gap: 2px;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.5s ease;
}
- .ai-history-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- background: none;
- border: none;
- color: @project-panel-text-2;
- font-size: @menu-item-font-size;
- width: 26px;
- height: 26px;
- border-radius: 3px;
- cursor: pointer;
- opacity: 0.7;
- transition: opacity 0.15s ease, background-color 0.15s ease;
-
- &:hover {
- opacity: 1;
- background-color: rgba(255, 255, 255, 0.06);
- }
-
- &.active {
- opacity: 1;
- background-color: rgba(255, 255, 255, 0.08);
- }
- }
-
+ .ai-history-btn,
.ai-settings-btn {
display: flex;
align-items: center;
justify-content: center;
- background: none;
- border: none;
color: @project-panel-text-2;
font-size: @menu-item-font-size;
width: 26px;
height: 26px;
- border-radius: 3px;
cursor: pointer;
- opacity: 0;
- transition: opacity 0.15s ease, background-color 0.15s ease;
- pointer-events: none;
-
- &:hover {
- opacity: 1;
- background-color: rgba(255, 255, 255, 0.06);
- }
}
.ai-new-session-btn {
display: flex;
align-items: center;
gap: 4px;
- background: none;
- border: none;
color: @project-panel-text-2;
font-size: @menu-item-font-size;
+ height: 26px;
padding: 0 8px;
- border-radius: 3px;
cursor: pointer;
- opacity: 0.7;
- transition: opacity 0.15s ease, background-color 0.15s ease;
-
- &:hover {
- opacity: 1;
- background-color: rgba(255, 255, 255, 0.06);
- }
}
}
-/* Show settings gear on tab container hover */
-.ai-tab-container:hover .ai-settings-btn {
- opacity: 0.7;
+/* Show header actions on tab container hover */
+.ai-tab-container:hover .ai-chat-header-actions {
+ opacity: 1;
pointer-events: auto;
+ transition: opacity 0.15s ease;
+}
+
+
+/* Left-align title when sidebar is narrow to free space for action buttons */
+@container (max-width: 380px) {
+ .ai-chat-header {
+ justify-content: flex-start;
+ }
}
/* ── Session history dropdown ──────────────────────────────────────── */
@@ -418,6 +407,7 @@
code {
background-color: rgba(255, 255, 255, 0.08);
+ color: @project-panel-text-1;
padding: 1px 4px;
border-radius: 3px;
font-size: @sidebar-small-font-size;
@@ -1086,6 +1076,182 @@
}
}
+/* ── Plan card (ExitPlanMode) ───────────────────────────────────────── */
+.ai-msg-plan {
+ margin-bottom: 8px;
+ border: 1px solid rgba(107, 158, 255, 0.25);
+ border-radius: 6px;
+ background-color: rgba(107, 158, 255, 0.04);
+ overflow: hidden;
+}
+
+.ai-plan-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 12px;
+ background-color: rgba(107, 158, 255, 0.08);
+ border-bottom: 1px solid rgba(107, 158, 255, 0.15);
+ color: #6b9eff;
+ font-size: @sidebar-content-font-size;
+ font-weight: 600;
+}
+
+.ai-plan-body {
+ padding: 10px 12px;
+ font-size: @sidebar-content-font-size;
+ color: @project-panel-text-1;
+ line-height: 1.5;
+ white-space: normal;
+ word-wrap: break-word;
+ max-height: 400px;
+ overflow-y: auto;
+
+ p, ul, ol, pre {
+ margin-bottom: 8px;
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ code {
+ background: rgba(255, 255, 255, 0.06);
+ padding: 1px 4px;
+ border-radius: 3px;
+ font-size: 0.9em;
+ }
+
+ pre {
+ background: rgba(0, 0, 0, 0.2);
+ padding: 8px;
+ border-radius: 4px;
+ overflow-x: auto;
+
+ code {
+ background: none;
+ padding: 0;
+ }
+ }
+}
+
+.ai-plan-actions {
+ display: flex;
+ gap: 8px;
+ padding: 8px 12px;
+ border-top: 1px solid rgba(107, 158, 255, 0.1);
+}
+
+.ai-plan-approve-btn {
+ background: rgba(76, 175, 80, 0.15);
+ border: 1px solid rgba(76, 175, 80, 0.35);
+ color: #4caf50;
+ font-size: @sidebar-content-font-size;
+ padding: 5px 14px;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: background-color 0.15s ease;
+
+ &:hover:not(:disabled) {
+ background: rgba(76, 175, 80, 0.25);
+ }
+
+ &.selected {
+ background: rgba(76, 175, 80, 0.2);
+ border-color: rgba(76, 175, 80, 0.5);
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: default;
+ }
+}
+
+.ai-plan-revise-btn {
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ color: @project-panel-text-2;
+ font-size: @sidebar-content-font-size;
+ padding: 5px 14px;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: background-color 0.15s ease;
+
+ &:hover:not(:disabled) {
+ background: rgba(255, 255, 255, 0.08);
+ }
+
+ &.selected {
+ background: rgba(107, 158, 255, 0.1);
+ border-color: rgba(107, 158, 255, 0.3);
+ color: #6b9eff;
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: default;
+ }
+}
+
+.ai-plan-feedback {
+ padding: 8px 12px;
+ border-top: 1px solid rgba(107, 158, 255, 0.1);
+ display: flex;
+ gap: 6px;
+ align-items: stretch;
+}
+
+.ai-plan-feedback-input {
+ flex: 1;
+ min-width: 0;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid rgba(255, 255, 255, 0.1) !important;
+ border-radius: 4px;
+ color: @project-panel-text-1;
+ font-size: @sidebar-content-font-size;
+ padding: 6px 10px !important;
+ margin: 0 !important;
+ line-height: 1.4;
+ height: auto !important;
+ min-height: 32px;
+ max-height: 80px;
+ resize: vertical;
+ box-sizing: content-box;
+ outline: none !important;
+ box-shadow: none !important;
+
+ &:focus {
+ border-color: @bc-btn-border-focused !important;
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+}
+
+.ai-plan-feedback-send {
+ background: rgba(107, 158, 255, 0.15);
+ border: 1px solid rgba(107, 158, 255, 0.3);
+ color: #6b9eff;
+ padding: 0 10px;
+ border-radius: 4px;
+ cursor: pointer;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background-color 0.15s ease;
+
+ &:hover:not(:disabled) {
+ background: rgba(107, 158, 255, 0.25);
+ }
+
+ &:disabled {
+ opacity: 0.4;
+ cursor: default;
+ }
+}
+
/* ── Queued clarification bubble (static, above input) ─────────────── */
.ai-queued-msg {
border: 1px dashed rgba(255, 255, 255, 0.15);
@@ -1564,6 +1730,8 @@
padding: 2rem;
text-align: center;
color: @project-panel-text-2;
+ overflow: hidden;
+ min-width: 0;
.ai-unavailable-icon {
font-size: 2rem;
@@ -1583,6 +1751,18 @@
line-height: 1.5;
margin-bottom: 12px;
opacity: 0.6;
+ white-space: normal;
+
+ .ai-learn-more-link {
+ color: @project-panel-text-2;
+ text-decoration: underline;
+ cursor: pointer;
+ opacity: 1;
+
+ &:hover {
+ color: @project-panel-text-1;
+ }
+ }
}
.ai-retry-btn {
@@ -1618,6 +1798,33 @@
}
}
+.ai-install-screen {
+ .ai-install-icon {
+ display: inline-flex;
+ margin-bottom: 12px;
+ opacity: 0.5;
+ color: @project-panel-text-2;
+
+ svg {
+ width: 48px;
+ height: 48px;
+ }
+ }
+
+ .ai-install-btn,
+ .ai-claude-login-btn {
+ margin-bottom: 10px;
+ }
+
+ .ai-install-restart-note {
+ font-size: @sidebar-small-font-size;
+ color: @project-panel-text-2;
+ opacity: 0.6;
+ margin-top: 8px;
+ white-space: normal;
+ }
+}
+
/* ── AI Settings Dialog ────────────────────────────────────────────── */
.ai-settings-dialog {
.ai-settings-section-label {
diff --git a/tracking-repos.json b/tracking-repos.json
index 1cebaf842..50016da60 100644
--- a/tracking-repos.json
+++ b/tracking-repos.json
@@ -1,5 +1,5 @@
{
"phoenixPro": {
- "commitID": "e1b145089cb7c242689fdeec3cc138445b9928e2"
+ "commitID": "ca8641e99f5954e259d1408d8bda62a5a16d4384"
}
}