Skip to content
Merged

Ai #3205

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
178 changes: 154 additions & 24 deletions src/extensionsIntegrated/Terminal/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@
let originalDefaultShellName = null; // System-detected default shell name
let _focusToastShown = false; // Show focus hint toast only once per session
let _clearHintShown = false; // Show clear buffer hint toast only once per session
let _projectPath = null;
let _restartingTerminals = false;
let $panel, $contentArea, $shellDropdown, $flyoutList;

/**
Expand Down Expand Up @@ -186,7 +188,7 @@
const shells = ShellProfiles.getShells();
const defaultShell = ShellProfiles.getDefaultShell();
$shellDropdown.empty();
for (const shell of shells) {
shells.forEach(function (shell) {
const isSelected = defaultShell && defaultShell.name === shell.name;
const $check = $('<span class="shell-check"></span>');
if (isSelected) {
Expand All @@ -197,6 +199,9 @@
.append($check)
.append($('<span></span>').text(shell.name));
$item.on("click", function () {
if (_restartingTerminals) {
return;
}
_hideShellDropdown();
ShellProfiles.setDefaultShell(shell.name);
_populateShellDropdown();
Expand All @@ -209,7 +214,7 @@
_createNewTerminalWithShell(shell);
});
$shellDropdown.append($item);
}
});
}

/**
Expand Down Expand Up @@ -272,6 +277,9 @@
* Create a new terminal with the default shell
*/
async function _createNewTerminal(cwdOverride) {
if (_restartingTerminals) {
return;
}
const shell = ShellProfiles.getDefaultShell();
return _createNewTerminalWithShell(shell, cwdOverride);
}
Expand All @@ -286,17 +294,12 @@
if (cwd.startsWith(tauriPrefix)) {
cwd = Phoenix.fs.getTauriPlatformPath(cwd);
}
if (cwd.length > 1 && (cwd.endsWith("/") || cwd.endsWith("\\"))) {
if (cwd.length > 1 && !/^[a-z]:[\\/]$/i.test(cwd) && (cwd.endsWith("/") || cwd.endsWith("\\"))) {
cwd = cwd.slice(0, -1);
}
return cwd;
}

/**
* Create a new terminal with a specific shell profile
* @param {Object} shell - Shell profile to use
* @param {string} [cwdOverride] - Optional VFS path to use as cwd instead of project root
*/
/**
* Map an OS shell name (e.g. "powershell.exe", "bash.exe") to a short
* family label so the metrics server's per-event length budget stays
Expand All @@ -315,7 +318,14 @@
return n || "unknown";
}

async function _createNewTerminalWithShell(shell, cwdOverride) {
/**
* Create a terminal using a shell profile and an optional VFS directory.
* @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.
* @return {Promise<TerminalInstance|undefined>} The new terminal, if a shell is available.
*/
async function _createNewTerminalWithShell(shell, cwdOverride, projectPath) {
if (!shell) {
console.error("Terminal: No shell available");
Metrics.countEvent(Metrics.EVENT_TYPE.TERMINAL, "new", "noShell");
Expand All @@ -326,18 +336,20 @@
_shellMetricLabel(shell.name));

// Get cwd: use override if provided, otherwise fall back to project root
const projectRoot = ProjectManager.getProjectRoot();
let cwd;
if (cwdOverride) {
cwd = _toNativePath(cwdOverride);
} else {
const projectRoot = ProjectManager.getProjectRoot();
if (projectRoot) {

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'If' statement should not be the only statement in 'else' block

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaCuhpgtOzNUJwVYOxjw&open=AaCuhpgtOzNUJwVYOxjw&pullRequest=3205
cwd = _toNativePath(projectRoot.fullPath);
}
}

// Create instance
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);

// Set up callbacks
instance.onTitleChanged = _onTerminalTitleChanged;
Expand Down Expand Up @@ -376,6 +388,120 @@

// Spawn PTY process
await instance.spawn();
return instance;
}

/** Remove the project-switch notice without changing any terminal session. */
function _hideProjectBanner() {
if ($contentArea) {
$contentArea.find(".terminal-project-banner").remove();
}
}

/** Show one notice for all bottom-panel terminals, naming the current project. */
function _showProjectBanner() {
_hideProjectBanner();
const root = ProjectManager.getProjectRoot();
if (!root || !terminalInstances.some(inst => inst.projectPath !== root.fullPath)) {
return;
}
const $banner = $('<div class="terminal-project-banner" role="status"></div>');
$banner.append($('<div class="terminal-project-message"></div>')
.text(StringUtils.format(Strings.TERMINAL_PROJECT_CHANGED, root.name)));
const path = _toNativePath(root.fullPath);
$banner.append($('<div class="terminal-project-path"></div>')
.text(StringUtils.format(Strings.TERMINAL_PROJECT_RESTART_PATH, path)).attr("title", path));
const $actions = $('<div class="terminal-project-actions"></div>');
$actions.append($('<button class="btn terminal-project-keep"></button>')
.text(Strings.TERMINAL_PROJECT_KEEP).on("click", function () {
_hideProjectBanner();
const active = _getActiveTerminal();
if (active) {
active.focus();
}
}));
$actions.append($('<button class="btn btn-primary terminal-project-restart"></button>')
.text(Strings.TERMINAL_PROJECT_RESTART).attr("title", Strings.TERMINAL_PROJECT_RESTART_WARNING)
.on("click", _restartTerminalsInProject));
$actions.find("button").prop("disabled", _restartingTerminals);
$banner.append($actions);
$contentArea.append($banner);
}

/** Notify on actual project changes; shell navigation and panel toggles leave sessions alone. */
function _onProjectOpen() {
const root = ProjectManager.getProjectRoot();
const path = root ? root.fullPath : null;
if (path !== _projectPath) {
_projectPath = path;
_showProjectBanner();
}
}

/**
* Query child processes using the same platform-specific lookup as terminal tabs.
* @return {Promise<string[]>} Active child process names.
*/
async function _getActiveProcesses() {
const results = await Promise.all(terminalInstances.filter(inst => inst.isAlive).map(function (inst) {
return nodeConnector.execPeer("getTerminalProcess", {id: inst.id})
.catch(function () { return {process: ""}; });
}));
return results.filter(result => result.process && !_isShellProcess(result.process))
.map(result => result.process);
}

/** Restart all bottom-panel tabs in the selected project, preserving shells, order and selection. */
async function _restartTerminalsInProject() {
const root = ProjectManager.getProjectRoot();
if (_restartingTerminals || !root || !terminalInstances.length) {
return;
}
const path = root.fullPath;
_restartingTerminals = true;
$contentArea.find(".terminal-project-actions button").prop("disabled", true);
try {
const activeProcesses = await _getActiveProcesses();
if (activeProcesses.length) {
const dialog = Dialogs.showModalDialog(DefaultDialogs.DIALOG_ID_INFO,
Strings.TERMINAL_RESTART_CONFIRM_TITLE,
Strings.TERMINAL_RESTART_CONFIRM_MSG, [
{className: Dialogs.DIALOG_BTN_CLASS_NORMAL, id: Dialogs.DIALOG_BTN_CANCEL, text: Strings.CANCEL},
{className: Dialogs.DIALOG_BTN_CLASS_PRIMARY, id: Dialogs.DIALOG_BTN_OK,
text: Strings.TERMINAL_PROJECT_RESTART}
]);
if (await dialog.getPromise() !== Dialogs.DIALOG_BTN_OK) {
return;
}
}
// A project can change while process lookup or confirmation is pending.
const currentRoot = ProjectManager.getProjectRoot();
if (!currentRoot || currentRoot.fullPath !== path) {
return;
}
const profiles = terminalInstances.map(inst => inst.shellProfile);
const activeIndex = terminalInstances.findIndex(inst => inst.id === activeTerminalId);
await _disposeAllAsync();
activeTerminalId = null;
_updateFlyout();
const replacements = [];
for (const profile of profiles) {
replacements.push(await _createNewTerminalWithShell(profile, path, path));
}
if (replacements[activeIndex]) {
_activateTerminal(replacements[activeIndex].id);
}
// Keep a notice for a newer project selected during the restart.
if (_projectPath !== path) {
_showProjectBanner();
}
} catch (err) {
console.error("Terminal: Failed to restart terminals:", err);
_showProjectBanner();
} finally {
_restartingTerminals = false;
$contentArea.find(".terminal-project-actions button").prop("disabled", false);
}
}

/**
Expand All @@ -400,7 +526,10 @@
/**
* Close a terminal instance, confirming first if a child process is running
*/
async function _closeTerminal(id) {

Check failure on line 529 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 17 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaCuhpgtOzNUJwVYOxjx&open=AaCuhpgtOzNUJwVYOxjx&pullRequest=3205
if (_restartingTerminals) {
return;
}
const idx = terminalInstances.findIndex(t => t.id === id);
if (idx === -1) {
return;
Expand Down Expand Up @@ -433,6 +562,9 @@
instance.dispose();
terminalInstances.splice(idx, 1);
delete processInfo[id];
if ($contentArea.find(".terminal-project-banner").length) {
_showProjectBanner();
}
Metrics.countEvent(Metrics.EVENT_TYPE.TERMINAL, "close", "user");

// If we closed the active terminal, activate another
Expand All @@ -447,6 +579,7 @@

// If no terminals left, hide the panel
if (terminalInstances.length === 0) {
_hideProjectBanner();
panel.hide();

}
Expand Down Expand Up @@ -708,22 +841,23 @@
return false;
}
const el = document.activeElement;
if (!el || !$contentArea[0].contains(el)) {
if (!el || !$(el).closest(".terminal-instance-container").length) {
return false;
}

const inBottomPanel = $contentArea[0].contains(el);
const ctrlOrMeta = event.ctrlKey || event.metaKey;
const key = event.key.toLowerCase();

// Ctrl+K (Cmd+K on mac): clear terminal scrollback
if (ctrlOrMeta && !event.shiftKey && key === "k") {
if (inBottomPanel && ctrlOrMeta && !event.shiftKey && key === "k") {
event.preventDefault();
_clearActiveTerminal();
return true;
}

// Show clear buffer hint on Ctrl+L
if (ctrlOrMeta && !event.shiftKey && key === "l") {
if (inBottomPanel && ctrlOrMeta && !event.shiftKey && key === "l") {
_showClearBufferHintToast();
}

Expand Down Expand Up @@ -800,6 +934,7 @@
}
terminalInstances = [];
processInfo = {};
_hideProjectBanner();
}

/**
Expand Down Expand Up @@ -922,23 +1057,18 @@
_initNodeConnector();
_createPanel();
_createToolbarButton();
const root = ProjectManager.getProjectRoot();
_projectPath = root ? root.fullPath : null;
ProjectManager.on("projectOpen.terminal", _onProjectOpen);

// Gate user-initiated panel close (X button): confirm if needed, then
// dispose all terminals. Programmatic hide() just collapses the panel
// without disposing terminals.
panel.registerOnCloseRequestedHandler(async function () {
// Query all terminals in parallel to avoid sequential 2s waits on Windows
const aliveInstances = terminalInstances.filter(inst => inst.isAlive);
const results = await Promise.all(aliveInstances.map(function (inst) {
return nodeConnector.execPeer("getTerminalProcess", {id: inst.id})
.catch(function () { return {process: ""}; });
}));
const activeProcesses = [];
for (const result of results) {
if (result.process && !_isShellProcess(result.process)) {
activeProcesses.push(result.process);
}
if (_restartingTerminals) {
return false;
}
const activeProcesses = await _getActiveProcesses();

let title, message, confirmText;
const count = terminalInstances.length;
Expand Down
7 changes: 7 additions & 0 deletions src/nls/root/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -2230,6 +2230,13 @@ define({
"TERMINAL_FOCUS_HINT": "Press {0} to switch between editor and terminal",
"TERMINAL_CLEAR": "Clear Terminal",
"TERMINAL_CLEAR_BUFFER_HINT": "💡 Press {0} to clear terminal buffer",
"TERMINAL_PROJECT_CHANGED": "Project changed to {0}. Existing terminals have kept their locations.",
"TERMINAL_PROJECT_RESTART_PATH": "Restart in: {0}",
"TERMINAL_PROJECT_RESTART_WARNING": "Restarting opens every terminal in this folder, stops running processes and clears terminal output.",
"TERMINAL_PROJECT_KEEP": "Keep Terminals",
"TERMINAL_PROJECT_RESTART": "Restart All in This Project",
"TERMINAL_RESTART_CONFIRM_TITLE": "Restart All Terminals?",
"TERMINAL_RESTART_CONFIRM_MSG": "Terminals have active processes. Restarting will stop them and clear terminal output. Continue?",
"EXTENDED_COMMIT_MESSAGE": "EXTENDED",
"GETTING_STAGED_DIFF_PROGRESS": "Getting diff of staged files\u2026",
"GIT_COMMIT": "Git commit\u2026",
Expand Down
34 changes: 34 additions & 0 deletions src/styles/Extn-Terminal.less
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,40 @@
overflow: hidden;
}

.terminal-project-banner {
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 5;
box-sizing: border-box;
max-height: 100%;
overflow: auto;
padding: 8px 10px;
background: var(--terminal-toolbar-bg);
color: var(--terminal-foreground);
border-top: 1px solid var(--terminal-border);
border-left: 3px solid var(--terminal-ansi-yellow);
box-shadow: 0 -3px 6px rgba(0, 0, 0, 0.2);
white-space: normal;

.terminal-project-path {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 4px 0;
font-weight: 600;
}

.terminal-project-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
margin-top: 8px;
}
}

.terminal-instance-container {
position: absolute;
top: 0;
Expand Down
Loading
Loading