diff --git a/src/extensionsIntegrated/Terminal/TerminalInstance.js b/src/extensionsIntegrated/Terminal/TerminalInstance.js index e0afb59b98..2d6539b765 100644 --- a/src/extensionsIntegrated/Terminal/TerminalInstance.js +++ b/src/extensionsIntegrated/Terminal/TerminalInstance.js @@ -41,11 +41,19 @@ define(function (require, exports, module) { /** - * Read terminal theme colors from CSS variables + * Read terminal theme colors from CSS variables. + * @param {Element} [baseEl] - Element to resolve --terminal-* custom + * properties from (CSS custom properties inherit down the DOM tree, + * so this instance picks up whichever ancestor defines them). Pass + * the instance's own container so each embedding context controls + * its own theme — e.g. the bottom Terminal panel's light/dark-aware + * .terminal-panel-container vs. the always-dark AI sidebar's fixed + * palette (see .ai-chat-body-cli in Extn-AIChatPanel.less). Falls + * back to the old global lookup when omitted. * @returns {Object} xterm.js theme object */ - function _getThemeFromCSS() { - const panelEl = document.querySelector('.terminal-panel-container') || document.documentElement; + function _getThemeFromCSS(baseEl) { + const panelEl = baseEl || document.querySelector('.terminal-panel-container') || document.documentElement; const style = getComputedStyle(panelEl); function v(name) { return style.getPropertyValue(name).trim() || undefined; @@ -122,7 +130,7 @@ define(function (require, exports, module) { // Create xterm.js instance this.terminal = new Terminal({ - theme: _getThemeFromCSS(), + theme: _getThemeFromCSS(this.$container[0]), fontFamily: "'Menlo', 'DejaVu Sans Mono', 'Consolas', 'Lucida Console', monospace", fontSize: 13, lineHeight: 1.2, @@ -189,8 +197,11 @@ define(function (require, exports, module) { /** * Spawn the PTY process on the Node side + * @param {Object} [env] - Extra environment variables to merge into the + * PTY's process env (e.g. custom API endpoint overrides). Optional — + * existing callers that omit it are unaffected. */ - TerminalInstance.prototype.spawn = async function () { + TerminalInstance.prototype.spawn = async function (env) { const dims = this.fitAddon.proposeDimensions(); try { const result = await this.nodeConnector.execPeer("createTerminal", { @@ -199,7 +210,8 @@ define(function (require, exports, module) { args: this.shellProfile.args || [], cwd: this.cwd, cols: dims ? dims.cols : 80, - rows: dims ? dims.rows : 24 + rows: dims ? dims.rows : 24, + env: env || undefined }); this.pid = result.pid; this.isAlive = true; @@ -406,7 +418,7 @@ define(function (require, exports, module) { */ TerminalInstance.prototype.updateTheme = function () { if (this.terminal) { - this.terminal.options.theme = _getThemeFromCSS(); + this.terminal.options.theme = _getThemeFromCSS(this.$container ? this.$container[0] : null); } }; diff --git a/src/extensionsIntegrated/Terminal/main.js b/src/extensionsIntegrated/Terminal/main.js index a9e10ae4e1..060136b47f 100644 --- a/src/extensionsIntegrated/Terminal/main.js +++ b/src/extensionsIntegrated/Terminal/main.js @@ -1008,6 +1008,20 @@ define(function (require, exports, module) { exports.CMD_VIEW_TERMINAL = CMD_VIEW_TERMINAL; exports.CMD_NEW_TERMINAL = CMD_NEW_TERMINAL; + /** + * Get the shared "phoenix_terminal" NodeConnector so other extensions + * (e.g. the AI chat panel's embedded CLI terminal) can spawn their own + * independent TerminalInstance without registering a second connector + * on the same id, which throws. Safe to call any time after boot — + * _initNodeConnector() runs unconditionally on AppInit.appReady, before + * any user interaction. + * @return {Object|null} The terminal NodeConnector, or null if this is + * not a native app build (terminal is unavailable there). + */ + exports.getNodeConnector = function () { + return nodeConnector; + }; + if (Phoenix.isTestWindow) { exports._getActiveTerminal = _getActiveTerminal; exports._refreshAllProcesses = _refreshAllProcesses; diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 315082ca2d..1a11e1d1fb 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -2555,6 +2555,8 @@ define({ // AI CHAT PANEL "AI_CHAT_TITLE": "Claude Code", + "AI_CHAT_MODE_CHAT": "Claude Code", + "AI_CHAT_MODE_CLI": "Claude Code CLI", "AI_CHAT_SURPRISE_ME_USER_MSG": "Surprise me!", "AI_CHAT_SURPRISE_ME_LP_HEADING_1": "Setting the stage…", "AI_CHAT_SURPRISE_ME_LP_HEADING_2": "Warming the canvas…", @@ -2713,6 +2715,7 @@ define({ "AI_CHAT_MODEL_DESC_SONNET": "Balanced speed and capability for everyday coding", "AI_CHAT_MODEL_DESC_HAIKU": "Fastest model for quick, simple tasks", "AI_CHAT_MODEL_SELECT_TITLE": "Choose the AI model for this chat", + "AI_CHAT_MODE_SELECT_TITLE": "Switch between the Claude Code chat and an embedded Claude Code CLI terminal", "AI_CHAT_MODEL_SWITCHED_NOTICE": "Switched to {0}. Applies from your next message; the first response may take a moment longer while the cache rebuilds.", "AI_CHAT_INPUT_HINT": "Press {0} to send · {1} for new line", "AI_CHAT_BASH_CONFIRM_TITLE": "Allow command?", @@ -2772,6 +2775,11 @@ define({ "AI_CHAT_NEW_WHILE_STREAMING_MSG": "AI is currently working on a task. Starting a new conversation will stop it. Continue?", "AI_CHAT_RESUME_WHILE_STREAMING_TITLE": "AI is working", "AI_CHAT_RESUME_WHILE_STREAMING_MSG": "AI is currently working on a task. Switching to a previous conversation will stop it. Continue?", + "AI_CHAT_CLI_NEW_CONFIRM_TITLE": "End this CLI session?", + "AI_CHAT_CLI_NEW_CONFIRM_MSG": "This will end the running Claude Code CLI session and start a fresh one. Continue?", + "AI_CHAT_CLI_STALE_PROJECT_MSG": "This Claude Code CLI session is still running in {0} — the open project is now {1}.", + "AI_CHAT_CLI_STALE_PROJECT_SWITCH_BTN": "Switch to “{0}”", + "AI_CHAT_CLI_STALE_PROJECT_STAY_BTN": "Stay on “{0}”", "AI_CHAT_SETTINGS_TITLE": "Claude Code Settings", "AI_SETTINGS_API_KEY": "API Key", "AI_SETTINGS_BASE_URL": "Base URL", diff --git a/src/styles/Extn-AIChatPanel.less b/src/styles/Extn-AIChatPanel.less index 1c8ca97d97..a6a8d63207 100644 --- a/src/styles/Extn-AIChatPanel.less +++ b/src/styles/Extn-AIChatPanel.less @@ -33,6 +33,21 @@ @ai-line-prose: 1.6; // multi-line body text — generous reading rhythm @ai-line-compact: 1.4; // single-line UI elements +// Warning-tone chrome (e.g. the CLI-mode "project switched" banner). Must +// stay fully opaque — it overlays live terminal output, so any see-through +// background would make its text unreadable against arbitrary content +// underneath. Rather than a flat saturated alert-box color (which reads as +// a foreign popup bolted onto the panel), @ai-warning-bg is the panel's own +// dark surface (@bc-ai-sidebar-bg, #252525) warmed toward amber — same +// elevation family, so it still reads as *this panel's* chrome. The amber +// accent (border stripe + icon + emphasized text) carries the "pay +// attention" cue instead of the fill color. The action button intentionally +// uses the panel's own blue accent, not amber — keeping "notice" and +// "actionable" visually distinct, same split the rest of the panel uses. +@ai-warning-bg: #332a1f; +@ai-warning-accent: #e6a23c; +@ai-warning-text: @project-panel-text-1; + .ai-tab-container { display: flex; flex-direction: column; @@ -98,6 +113,21 @@ .ai-chat-title-group { display: flex; align-items: center; + cursor: pointer; + padding: 2px 6px; + border-radius: 6px; + transition: background 0.15s ease; + + &:hover, + &.dropdown-open { + background: rgba(255, 255, 255, 0.06); + } + + .ai-chat-title-chevron { + margin-left: 5px; + font-size: 9px; + opacity: 0.5; + } } .ai-chat-title-icon { @@ -185,6 +215,185 @@ } } +/* ── CLI mode ───────────────────────────────────────────────────────── */ +/* History and the model switcher are chat-UI-only concerns — the embedded + CLI terminal has its own session/model handling. New + Settings stay. */ +.ai-chat-panel.ai-mode-cli { + .ai-model-select, + .ai-history-btn { + display: none; + } +} + +.ai-chat-body-chat, +.ai-chat-body-cli { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + overflow: hidden; +} + +.ai-chat-body-cli { + display: none; + position: relative; + + // Fixed dark terminal palette — deliberately NOT wrapped in a `.dark &` + // guard. TerminalInstance reads --terminal-* off its own container + // (see TerminalInstance.js#_getThemeFromCSS); without these declared + // here it falls back to the bottom Terminal panel's singleton + // .terminal-panel-container (present in the DOM even when that panel + // is never opened), which IS light/dark-theme-aware — flipping the + // embedded CLI terminal to a white background in light theme even + // though this sidebar is always-dark. Values match + // ".dark .terminal-panel-container" in styles/Extn-Terminal.less. + --terminal-background: #1e1e1e; + --terminal-foreground: #cccccc; + --terminal-cursor: #ffffff; + --terminal-selection: rgba(255, 255, 255, 0.2); + --terminal-ansi-black: #000000; + --terminal-ansi-red: #cd3131; + --terminal-ansi-green: #0dbc79; + --terminal-ansi-yellow: #e5e510; + --terminal-ansi-blue: #2472c8; + --terminal-ansi-magenta: #bc3fbc; + --terminal-ansi-cyan: #11a8cd; + --terminal-ansi-white: #e5e5e5; + --terminal-ansi-bright-black: #666666; + --terminal-ansi-bright-red: #f14c4c; + --terminal-ansi-bright-green: #23d18b; + --terminal-ansi-bright-yellow: #f5f543; + --terminal-ansi-bright-blue: #3b8eea; + --terminal-ansi-bright-magenta: #d670d6; + --terminal-ansi-bright-cyan: #29b8db; + --terminal-ansi-bright-white: #ffffff; +} + +.ai-chat-panel.ai-mode-cli { + .ai-chat-body-chat { + display: none; + } + + .ai-chat-body-cli { + display: flex; + } +} + +/* "Project switched" banner over the embedded CLI terminal — a dark, + warm-tinted card in the panel's own elevation family (see @ai-warning-bg) + with a left amber stripe + icon carrying the "pay attention" cue, rather + than a flat saturated alert-box color that would read as a foreign popup + bolted onto the panel. Body text is @ai-text-body (not the smaller + -secondary tier) — this is a message the user needs to actually read, + not incidental chrome. Docked to the BOTTOM, not the top: that's where + the user's eyes/cursor already are while typing into the terminal (the + same spot the chat input box occupies in chat mode), so it can't be + missed the way a top banner easily could be while scrolled/typing. */ +.ai-cli-stale-banner { + position: absolute; + bottom: 0; + left: 0; + right: 0; + z-index: 5; + display: flex; + flex-direction: column; + gap: 8px; + padding: 8px 10px 8px 9px; + background: @ai-warning-bg; + border-left: 3px solid @ai-warning-accent; + box-shadow: 0 -3px 6px rgba(0, 0, 0, 0.35); + font-size: @ai-text-body; + color: @ai-warning-text; + + .ai-cli-stale-banner-row { + display: flex; + align-items: center; + gap: 8px; + } + + .ai-cli-stale-banner-icon { + flex-shrink: 0; + color: @ai-warning-accent; + } + + .ai-cli-stale-banner-text { + flex: 1; + min-width: 0; + white-space: normal; // .sidebar.panel sets nowrap; this needs to wrap + overflow-wrap: break-word; + + // Explicit color: a global `b, strong { color: #333 }` base rule + // (from brackets_patterns_override.less) directly targets , and + // a directly-matching rule always wins over inherited color no + // matter how much higher the ancestor's specificity is — inheriting + // @ai-warning-text from .ai-cli-stale-banner-text here would lose. + b { + color: @ai-warning-accent; + font-weight: 600; + } + } + + // Two explicit, equally-weighted choices rather than one CTA + a small + // close icon — every option here resolves the staleness one way or + // the other, so there's nothing left over to "dismiss". Right-aligned + // and ordered secondary-then-primary, same convention as + // .modal-footer's Cancel/OK (see widgets/Dialogs.js showConfirmDialog) + // — the primary action sits rightmost. flex-wrap lets the pair drop + // to its own line on a narrow sidebar; each button's own max-width + + // ellipsis (below) is what actually bounds an unpredictably long + // project name, not wrapping. + .ai-cli-stale-banner-actions { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 8px; + } + + // Project names are user-controlled and unbounded in length — cap + // both buttons the same width and ellipsis rather than letting one + // balloon the whole banner or wrap into a multi-line button. + .ai-cli-stale-banner-btn, + .ai-cli-stale-banner-btn-secondary { + max-width: 180px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + // Renders as Phoenix's standard .btn.btn-primary (color/hover/active + // come from the shared rule above, keyed off this container class — + // same mechanism the placeholder screens' CTAs use) rather than a + // one-off custom button style. The accent stripe/icon already carries + // "notice", so the button itself stays "actionable" — the panel's + // usual primary-action blue, not amber. + .ai-cli-stale-banner-btn { + padding: 4px 10px !important; + font-size: @ai-text-secondary !important; + } + + // Secondary choice — same transparent/outlined language as + // .ai-placeholder-cta-secondary elsewhere in this panel, sized down + // to match the primary button here instead of that CTA's full-width + // proportions. + .ai-cli-stale-banner-btn-secondary { + padding: 4px 10px; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 4px; + background: transparent; + color: @ai-warning-text; + font-family: inherit; + font-size: @ai-text-secondary; + font-weight: 600; + cursor: pointer; + transition: background-color 0.15s ease, border-color 0.15s ease; + + &:hover { + background: rgba(255, 255, 255, 0.08); + border-color: rgba(255, 255, 255, 0.32); + } + } +} + /* Show header actions on tab container hover */ .ai-tab-container:hover .ai-chat-header-actions { opacity: 1; @@ -273,12 +482,14 @@ } /* ── Session history dropdown ──────────────────────────────────────── */ -/* When history is open, hide chat content and show the dropdown instead */ +/* When history is open, hide chat content and show the dropdown instead. + These live inside .ai-chat-body-chat now (not direct .ai-chat-panel + children), since that wrapper is what CLI mode toggles alongside it. */ .ai-chat-panel.ai-history-open { - > .ai-chat-messages, - > .ai-chat-status, - > .ai-chat-input-area, - > .ai-onboarding-wrap { + .ai-chat-body-chat > .ai-chat-messages, + .ai-chat-body-chat > .ai-chat-status, + .ai-chat-body-chat > .ai-chat-input-area, + .ai-chat-body-chat > .ai-onboarding-wrap { display: none !important; } } @@ -3278,7 +3489,8 @@ selector is more specific than the reset, and `!important` belt-and- braces against any later reset rule that might still apply. */ .ai-chat-panel .ai-unavailable .btn.btn-primary, -.ai-chat-panel .ai-placeholder .btn.btn-primary { +.ai-chat-panel .ai-placeholder .btn.btn-primary, +.ai-chat-panel .ai-cli-stale-banner .btn.btn-primary { display: inline-block; background-color: @bc-primary-btn-bg !important; background-image: none !important; @@ -3308,7 +3520,8 @@ } .dark .ai-chat-panel .ai-unavailable .btn.btn-primary, -.dark .ai-chat-panel .ai-placeholder .btn.btn-primary { +.dark .ai-chat-panel .ai-placeholder .btn.btn-primary, +.dark .ai-chat-panel .ai-cli-stale-banner .btn.btn-primary { background-color: @dark-bc-primary-btn-bg !important; border-color: darken(@dark-bc-primary-btn-bg, 8%) !important; diff --git a/tracking-repos.json b/tracking-repos.json index f58b097035..a661ce4e62 100644 --- a/tracking-repos.json +++ b/tracking-repos.json @@ -1,5 +1,5 @@ { "phoenixPro": { - "commitID": "5886d202574264a2c4c70b4126618b9fae4950ff" + "commitID": "d89085d705cd227280f3a1dd3d4dfbb9a5bef05c" } }