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" } }