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
114 changes: 98 additions & 16 deletions src/extensionsIntegrated/Terminal/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -329,17 +329,19 @@
* @param {Object} shell Shell profile to use.
* @param {string} [cwdOverride] Directory to use instead of the project root.
* @param {string} [projectPath] Owning project when restarting during a project switch.
* @param {Object} [processRun] Set when `shell` is a single process run without a shell
* (see _runProcessInNewTerminal): its env and the exit report the caller asked for.
* @return {Promise<TerminalInstance|undefined>} The new terminal, if a shell is available.
*/
async function _createNewTerminalWithShell(shell, cwdOverride, projectPath) {
async function _createNewTerminalWithShell(shell, cwdOverride, projectPath, processRun) {

Check failure on line 336 in src/extensionsIntegrated/Terminal/main.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 19 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaFtJn26HB9E3Nf5d5o0&open=AaFtJn26HB9E3Nf5d5o0&pullRequest=3291
if (!shell) {
console.error("Terminal: No shell available");
Metrics.countEvent(Metrics.EVENT_TYPE.TERMINAL, "new", "noShell");
return;
}
// Metric: a new terminal session was created, keyed by shell family.
Metrics.countEvent(Metrics.EVENT_TYPE.TERMINAL, "new",
_shellMetricLabel(shell.name));
processRun ? "process" : _shellMetricLabel(shell.name));

// Get cwd: use override if provided, otherwise fall back to project root
const projectRoot = ProjectManager.getProjectRoot();
Expand All @@ -356,6 +358,7 @@
const instance = new TerminalInstance(nodeConnector, shell, cwd);
// Project ownership is independent of directories the user visits in the shell.
instance.projectPath = projectPath || (projectRoot ? projectRoot.fullPath : null);
instance.processRun = processRun || null;

// Set up callbacks
instance.onTitleChanged = _onTerminalTitleChanged;
Expand Down Expand Up @@ -393,10 +396,59 @@
try { instance.fitAddon.fit(); } catch (e) { /* not ready */ }

// Spawn PTY process
await instance.spawn();
const spawned = await instance.spawn(processRun ? processRun.env : undefined);
if (processRun) {
processRun.started = spawned;
if (!spawned) {
_reportProcessExit(processRun, null);
}
}
return instance;
}

/**
* Tell whoever started a process tab how it ended, once.
* @param {?Object} processRun The tab's run, or null for a shell tab.
* @param {?number} exitCode The process exit code, or null when it never ran or its tab was closed first.
*/
function _reportProcessExit(processRun, exitCode) {

Check warning on line 414 in src/extensionsIntegrated/Terminal/main.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move function '_reportProcessExit' to the outer scope.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaFtJn26HB9E3Nf5d5o1&open=AaFtJn26HB9E3Nf5d5o1&pullRequest=3291
if (!processRun || processRun.reported) {
return;
}
processRun.reported = true;
if (typeof processRun.onProcessExit === "function") {
try {
processRun.onProcessExit(exitCode);
} catch (err) {
console.error("Terminal: onProcessExit handler failed:", err);
}
}
}

/**
* Run one executable as a new tab's own process, with no shell around it, so the
* caller learns how it ended. The tab stays open after the exit to show its output.
* @param {{path: string, args: (Array<string>|undefined), name: (string|undefined),
* env: (Object|undefined)}} spec What to run; env is merged into the inherited environment.
* @param {function(?number)} [onProcessExit] Called once with the exit code, or null when
* the process could not start or its tab was closed before it exited.
* @return {Promise<?{terminalId: string}>} The new tab, or null when the process did not start.
*/
async function _runProcessInNewTerminal(spec, onProcessExit) {
const processRun = {env: spec.env, onProcessExit: onProcessExit, started: false, reported: false};
if (!spec.path || _restartingTerminals) {
_reportProcessExit(processRun, null);
return null;
}
const profile = {
name: spec.name || spec.path.split(/[\\/]/).pop(),
path: spec.path,
args: spec.args || []
};
const instance = await _createNewTerminalWithShell(profile, undefined, undefined, processRun);
return instance && processRun.started ? {terminalId: instance.id} : null;
}

/** Remove the project-switch notice without changing any terminal session. */
function _hideProjectBanner() {
if ($contentArea) {
Expand Down Expand Up @@ -491,12 +543,14 @@
// A restart replaces every instance, and the new ones are issued
// fresh ids, so a name looked up by id would not find them. Carry
// the names across with the shells: the tabs are the same tabs to
// the user, only pointed at the new project.
const carried = terminalInstances.map(inst => ({
// the user, only pointed at the new project. A tab running a single
// process (no shell) ends with the restart rather than running again.
const shellTabs = terminalInstances.filter(inst => !inst.processRun);
const carried = shellTabs.map(inst => ({
profile: inst.shellProfile,
customName: inst.customName
}));
const activeIndex = terminalInstances.findIndex(inst => inst.id === activeTerminalId);
const activeIndex = shellTabs.findIndex(inst => inst.id === activeTerminalId);
await _disposeAllAsync();
activeTerminalId = null;
_updateFlyout();
Expand All @@ -511,6 +565,10 @@
if (replacements[activeIndex]) {
_activateTerminal(replacements[activeIndex].id);
}
if (!replacements.length) {
_hideProjectBanner();
panel.hide();
}
// Keep a notice for a newer project selected during the restart.
if (_projectPath !== path) {
_showProjectBanner();
Expand Down Expand Up @@ -579,6 +637,8 @@
}
}

// Disposing stops exit events, so a process still running reports as closed.
_reportProcessExit(instance.processRun, null);
instance.dispose();
terminalInstances.splice(idx, 1);
delete processInfo[id];
Expand Down Expand Up @@ -748,7 +808,9 @@
* for it to come back.
*/
function _saveTabNames() {
// A process tab would come back as a plain shell, so its name is not kept.
StateManager.set(STATE_TAB_NAMES, terminalInstances
.filter(function (inst) { return !inst.processRun; })
.map(function (inst) { return inst.customName; })
.filter(Boolean));
}
Expand Down Expand Up @@ -892,6 +954,10 @@
*/
function _onTerminalProcessExit(id, exitCode) {
delete processInfo[id];
const instance = terminalInstances.find(inst => inst.id === id);
if (instance) {
_reportProcessExit(instance.processRun, exitCode);
}
_updateFlyout();
// Metric: terminal process exited on its own (e.g. user typed
// "exit"). Distinct from "user" close above, which records the
Expand All @@ -901,16 +967,6 @@
exitCode === 0 ? "ok" : "err");
}

/**
* Show the terminal panel. Creates a new terminal if none exist.
* 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.
*/
/**
* Open the tabs the user named last time, all of them, in the order they
* were in.
Expand Down Expand Up @@ -944,7 +1000,28 @@
}
}

/**
* Show the terminal panel. Creates a new terminal if none exist.
* 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.
* @param {{path: string, args: (Array<string>|undefined), name: (string|undefined),
* env: (Object|undefined)}} [options.process] - Run this executable as a new tab's own
* process instead of a shell (takes precedence over shellCommand). The tab is not saved
* or restored, and a project restart closes it instead of running it again.
* @param {function(?number)} [options.onProcessExit] - With `process`: called once with
* its exit code, or null if it could not start or its tab was closed before it exited.
* @return {Promise<?{terminalId: string}>} With `process`: the new tab, or null if the
* process did not start. Otherwise undefined.
*/
async function _showTerminal(options) {
if (options && options.process) {

Check warning on line 1022 in src/extensionsIntegrated/Terminal/main.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaFtJn26HB9E3Nf5d5o2&open=AaFtJn26HB9E3Nf5d5o2&pullRequest=3291
return _runProcessInNewTerminal(options.process, options.onProcessExit);
}
if (options && options.shellCommand) {
await _createNewTerminal();
const active = _getActiveTerminal();
Expand Down Expand Up @@ -1120,6 +1197,7 @@
*/
function _disposeAll() {
for (const inst of terminalInstances) {
_reportProcessExit(inst.processRun, null);
inst.dispose();
}
terminalInstances = [];
Expand All @@ -1133,6 +1211,10 @@
* and acknowledged by the Node side.
*/
async function _disposeAllAsync() {
// Report first, so a killed process can never be read as having finished on its own.
for (const inst of terminalInstances) {
_reportProcessExit(inst.processRun, null);
}
const killPromises = terminalInstances
.filter(function (inst) { return inst.isAlive && !inst._disposed; })
.map(function (inst) {
Expand Down
5 changes: 3 additions & 2 deletions src/nls/root/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -2549,8 +2549,8 @@ define({
"PROMO_PRO_WHATS_NEW_MESSAGE": "Thanks for supporting {0}. Here’s what’s new in this update:",
"PROMO_CARD_LIVE_PREVIEW": "Edit and style in Live Preview",
"PROMO_CARD_LIVE_PREVIEW_MESSAGE": "Change text, colors and fonts, rearrange elements, and try different screen sizes. Your code updates as you go.",
"PROMO_CARD_AI": "Build and refine with AI",
"PROMO_CARD_AI_MESSAGE": "Turn an idea into a page, then select an element or a piece of code and ask AI to make changes.",
"PROMO_CARD_AI": "Your AI Agents, Now Visual in Phoenix Pro",
"PROMO_CARD_AI_MESSAGE": "Bring your existing Claude Code or ChatGPT subscription into Phoenix Code, and unlock a whole new way to build and refine websites visually.",
"PROMO_CARD_LAYERS": "Shape your page with Layers",
"PROMO_CARD_LAYERS_MESSAGE": "Explore your page structure, insert elements, and edit classes and styles. See every change in Live Preview.",
"PROMO_CARD_MARKDOWN": "Write Markdown like a document",
Expand Down Expand Up @@ -3041,6 +3041,7 @@ define({
"AI_CHAT_AUTH_ERROR_NOTICE": "Claude Code is signed out or your login has expired.",
"AI_CHAT_AUTH_ERROR_BTN": "Sign in to Claude",
"AI_CHAT_AUTH_ERROR_HINT": "Finish signing in on the page that opens in your browser, then send your message again.",
"AI_CHAT_AUTH_LOGIN_FAILED": "Could not start Claude sign-in. Check your Claude Code installation and try again.",
"AI_CHAT_MODEL_DEFAULT": "Default",
"AI_CHAT_MODEL_DEFAULT_DESC_CURRENT": "Currently {0}",
"AI_CHAT_MODEL_DEFAULT_DESC": "Recommended — uses your Claude Code model setting",
Expand Down
13 changes: 5 additions & 8 deletions src/styles/Extn-AIChatPanel.less
Original file line number Diff line number Diff line change
Expand Up @@ -1293,6 +1293,10 @@
}

.ai-start-surprise-open {
.ai-start-surprise-dismiss {
border-left: 0;
}

.ai-start-surprise-choice {
display: block;
animation: ai-start-choice-open 0.22s ease-out;
Expand Down Expand Up @@ -1450,19 +1454,12 @@
}

/* Home over a chat that has started: the chat's card leads back to it,
with the same arrow as the CLIs. The Surprise Me demo would play into
that chat, so it is not offered. */
with the same arrow as the CLIs. Demos start a separate chat. */
.ai-start-row[data-mode="chat"] .ai-start-go {
display: none;
}

&.ai-start-chat-running {
// The compact layout shows these with more specific rules.
.ai-start-surprise,
.ai-start-footer.ai-start-demo {
display: none !important;
}

.ai-start-row[data-mode="chat"] {
.ai-start-check {
display: none;
Expand Down
Loading
Loading