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
26 changes: 19 additions & 7 deletions src/extensionsIntegrated/Terminal/TerminalInstance.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,19 @@


/**
* 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) {

Check warning on line 55 in src/extensionsIntegrated/Terminal/TerminalInstance.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move function '_getThemeFromCSS' to the outer scope.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaAKUDzu67TddhEa7gRo&open=AaAKUDzu67TddhEa7gRo&pullRequest=3100
const panelEl = baseEl || document.querySelector('.terminal-panel-container') || document.documentElement;
const style = getComputedStyle(panelEl);
function v(name) {
return style.getPropertyValue(name).trim() || undefined;
Expand Down Expand Up @@ -122,7 +130,7 @@

// 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,
Expand Down Expand Up @@ -189,8 +197,11 @@

/**
* 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", {
Expand All @@ -199,7 +210,8 @@
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;
Expand Down Expand Up @@ -406,7 +418,7 @@
*/
TerminalInstance.prototype.updateTheme = function () {
if (this.terminal) {
this.terminal.options.theme = _getThemeFromCSS();
this.terminal.options.theme = _getThemeFromCSS(this.$container ? this.$container[0] : null);
}
};

Expand Down
14 changes: 14 additions & 0 deletions src/extensionsIntegrated/Terminal/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions src/nls/root/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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…",
Expand Down Expand Up @@ -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?",
Expand Down Expand Up @@ -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",
Expand Down
227 changes: 220 additions & 7 deletions src/styles/Extn-AIChatPanel.less
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {

Check warning on line 272 in src/styles/Extn-AIChatPanel.less

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Duplicate selector ".ai-chat-panel.ai-mode-cli", first used at line 221

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaAKUD6I67TddhEa7gRp&open=AaAKUD6I67TddhEa7gRp&pullRequest=3100
.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 <b>, 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;
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion tracking-repos.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"phoenixPro": {
"commitID": "5886d202574264a2c4c70b4126618b9fae4950ff"
"commitID": "d89085d705cd227280f3a1dd3d4dfbb9a5bef05c"
}
}
Loading