diff --git a/src/extensions/default/Git/src/Panel.js b/src/extensions/default/Git/src/Panel.js
index 3a90cac8e..a8c4d7c9b 100644
--- a/src/extensions/default/Git/src/Panel.js
+++ b/src/extensions/default/Git/src/Panel.js
@@ -1240,7 +1240,7 @@ define(function (require, exports) {
var $panelHtml = $(panelHtml);
$panelHtml.find(".git-available, .git-not-available").hide();
- gitPanel = WorkspaceManager.createBottomPanel("main-git.panel", $panelHtml, 100, Strings.GIT_PANEL_TITLE);
+ gitPanel = WorkspaceManager.createBottomPanel("main-git.panel", $panelHtml, 100, Strings.GIT_PANEL_TITLE, {iconClass: "fa-brands fa-git-alt"});
$gitPanel = gitPanel.$panel;
const resizeObserver = new ResizeObserver(_panelResized);
resizeObserver.observe($gitPanel[0]);
diff --git a/src/extensionsIntegrated/CustomSnippets/main.js b/src/extensionsIntegrated/CustomSnippets/main.js
index 973c994cd..d533e3770 100644
--- a/src/extensionsIntegrated/CustomSnippets/main.js
+++ b/src/extensionsIntegrated/CustomSnippets/main.js
@@ -59,7 +59,7 @@ define(function (require, exports, module) {
*/
function _createPanel() {
customSnippetsPanel = WorkspaceManager.createBottomPanel(PANEL_ID, $snippetsPanel, PANEL_MIN_SIZE,
- Strings.CUSTOM_SNIPPETS_PANEL_TITLE);
+ Strings.CUSTOM_SNIPPETS_PANEL_TITLE, {iconClass: "fa-solid fa-code"});
UIHelper.init(customSnippetsPanel);
customSnippetsPanel.show();
diff --git a/src/extensionsIntegrated/DisplayShortcuts/main.js b/src/extensionsIntegrated/DisplayShortcuts/main.js
index 0b8f49678..ceb6e1ca5 100644
--- a/src/extensionsIntegrated/DisplayShortcuts/main.js
+++ b/src/extensionsIntegrated/DisplayShortcuts/main.js
@@ -479,7 +479,7 @@ define(function (require, exports, module) {
// AppInit.htmlReady() has already executed before extensions are loaded
// so, for now, we need to call this ourself
panel = WorkspaceManager.createBottomPanel(TOGGLE_SHORTCUTS_ID, $(s), 300,
- Strings.KEYBOARD_SHORTCUT_PANEL_TITLE);
+ Strings.KEYBOARD_SHORTCUT_PANEL_TITLE, {iconClass: "fa-solid fa-keyboard"});
panel.hide();
$shortcutsPanel = $("#shortcuts-panel");
diff --git a/src/extensionsIntegrated/Terminal/TerminalInstance.js b/src/extensionsIntegrated/Terminal/TerminalInstance.js
index 90d21e26d..e0afb59b9 100644
--- a/src/extensionsIntegrated/Terminal/TerminalInstance.js
+++ b/src/extensionsIntegrated/Terminal/TerminalInstance.js
@@ -39,14 +39,6 @@ define(function (require, exports, module) {
let _nextId = 0;
- // Shortcuts that should be passed to the editor, not the terminal
- const EDITOR_SHORTCUTS = [
- {ctrlKey: true, shiftKey: true, key: "p"}, // Command Palette
- {ctrlKey: true, key: "p"}, // Quick Open
- {ctrlKey: true, key: "b"}, // Toggle sidebar
- {ctrlKey: true, key: "Tab"}, // Next tab
- {ctrlKey: true, shiftKey: true, key: "Tab"} // Previous tab
- ];
/**
* Read terminal theme colors from CSS variables
@@ -267,16 +259,6 @@ define(function (require, exports, module) {
return false;
}
- for (const shortcut of EDITOR_SHORTCUTS) {
- const ctrlMatch = shortcut.ctrlKey ? ctrlOrMeta : !ctrlOrMeta;
- const shiftMatch = shortcut.shiftKey ? event.shiftKey : !event.shiftKey;
- const keyMatch = event.key.toLowerCase() === shortcut.key.toLowerCase();
-
- if (ctrlMatch && shiftMatch && keyMatch) {
- return false; // Don't let xterm handle it
- }
- }
-
return true; // Let xterm handle it
};
diff --git a/src/extensionsIntegrated/Terminal/main.js b/src/extensionsIntegrated/Terminal/main.js
index fda71c4aa..b55323403 100644
--- a/src/extensionsIntegrated/Terminal/main.js
+++ b/src/extensionsIntegrated/Terminal/main.js
@@ -115,7 +115,7 @@ define(function (require, exports, module) {
};
$panel = $(Mustache.render(panelHTML, templateVars));
- panel = WorkspaceManager.createBottomPanel(PANEL_ID, $panel, PANEL_MIN_SIZE);
+ panel = WorkspaceManager.createBottomPanel(PANEL_ID, $panel, PANEL_MIN_SIZE, undefined, {iconClass: "fa-solid fa-terminal"});
// Override focus() so Shift+Escape can transfer focus to the terminal
panel.focus = function () {
@@ -147,37 +147,7 @@ define(function (require, exports, module) {
// Dropdown chevron button toggles shell selector
$panel.find(".terminal-flyout-dropdown-btn").on("click", _onDropdownButtonClick);
- // When the terminal is focused, prevent Phoenix keybindings from
- // stealing keys that should go to the shell (e.g. Ctrl+L for clear).
- // The EDITOR_SHORTCUTS list in TerminalInstance.js already defines which
- // Ctrl combos should pass through to Phoenix; everything else should
- // reach xterm/the PTY.
- KeyBindingManager.addGlobalKeydownHook(function (event) {
- if (event.type !== "keydown") {
- return false;
- }
- // Only intercept when a terminal textarea is focused
- const el = document.activeElement;
- if (!el || !$contentArea[0].contains(el)) {
- return false;
- }
- // Let the terminal handle Ctrl/Cmd key combos that aren't
- // reserved for the editor (those are handled by TerminalInstance's
- // _customKeyHandler which returns false for them).
- const ctrlOrMeta = event.ctrlKey || event.metaKey;
- const key = event.key.toLowerCase();
- if (ctrlOrMeta && !event.shiftKey && key === "l") {
- _showClearBufferHintToast();
- return true; // Block Phoenix, let xterm handle Ctrl+L
- }
- // Ctrl+K (Cmd+K on mac): clear terminal scrollback
- if (ctrlOrMeta && !event.shiftKey && key === "k") {
- event.preventDefault();
- _clearActiveTerminal();
- return true;
- }
- return false;
- });
+ _setupPhoenixShortcuts();
// Refresh process info when the tab bar gains focus or mouse enters
$panel.find(".terminal-tab-bar").on("mouseenter", _refreshAllProcesses);
@@ -251,6 +221,22 @@ define(function (require, exports, module) {
* Show/hide the shell dropdown
*/
function _showShellDropdown() {
+ // Move dropdown out of the flyout and append to the terminal body
+ // so it isn't clipped by the tab bar's overflow: hidden.
+ // Position it above the actions row, aligned to the right.
+ const $body = $panel.find(".terminal-body");
+ const $actions = $panel.find(".terminal-flyout-actions");
+ const actionsRect = $actions[0].getBoundingClientRect();
+ const bodyRect = $body[0].getBoundingClientRect();
+
+ $shellDropdown.appendTo($body);
+ $shellDropdown.css({
+ position: "absolute",
+ bottom: (bodyRect.bottom - actionsRect.top) + "px",
+ right: "0",
+ left: "auto",
+ top: "auto"
+ });
$shellDropdown.removeClass("forced-hidden");
// Close on outside click
setTimeout(function () {
@@ -642,6 +628,69 @@ define(function (require, exports, module) {
_refreshAllProcesses();
}
+ /**
+ * Set up keyboard shortcut routing so that when the terminal is focused,
+ * all keys go to the terminal except shortcuts bound to specific Phoenix
+ * commands (e.g. toggle terminal, keyboard nav overlay).
+ */
+ function _setupPhoenixShortcuts() {
+ // Commands whose shortcuts should pass through to Phoenix
+ // even when the terminal is focused.
+ const PASSTHROUGH_COMMANDS = [
+ Commands.VIEW_TERMINAL,
+ Commands.CMD_KEYBOARD_NAV_UI_OVERLAY
+ ];
+
+ // Build a set of shortcut strings, rebuilt when bindings change.
+ let passthroughShortcuts = new Set();
+ function rebuild() {
+ passthroughShortcuts = new Set();
+ for (const cmdId of PASSTHROUGH_COMMANDS) {
+ for (const binding of KeyBindingManager.getKeyBindings(cmdId)) {
+ if (binding.key) {
+ passthroughShortcuts.add(binding.key);
+ }
+ }
+ }
+ }
+ rebuild();
+ KeyBindingManager.on(KeyBindingManager.EVENT_KEY_BINDING_ADDED, rebuild);
+ KeyBindingManager.on(KeyBindingManager.EVENT_KEY_BINDING_REMOVED, rebuild);
+
+ KeyBindingManager.addGlobalKeydownHook(function (event, shortcut) {
+ if (event.type !== "keydown") {
+ return false;
+ }
+ const el = document.activeElement;
+ if (!el || !$contentArea[0].contains(el)) {
+ return false;
+ }
+
+ 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") {
+ event.preventDefault();
+ _clearActiveTerminal();
+ return true;
+ }
+
+ // Show clear buffer hint on Ctrl+L
+ if (ctrlOrMeta && !event.shiftKey && key === "l") {
+ _showClearBufferHintToast();
+ }
+
+ // Let Phoenix handle shortcuts bound to passthrough commands
+ if (shortcut && passthroughShortcuts.has(shortcut)) {
+ return false;
+ }
+
+ // Block Phoenix from handling everything else — let xterm get it
+ return true;
+ });
+ }
+
/**
* Update all terminal themes (after editor theme change)
*/
diff --git a/src/language/CodeInspection.js b/src/language/CodeInspection.js
index 027cfd1ad..afad847b0 100644
--- a/src/language/CodeInspection.js
+++ b/src/language/CodeInspection.js
@@ -1240,7 +1240,7 @@ define(function (require, exports, module) {
Editor.registerGutter(CODE_INSPECTION_GUTTER, CODE_INSPECTION_GUTTER_PRIORITY);
// Create bottom panel to list error details
var panelHtml = Mustache.render(PanelTemplate, Strings);
- problemsPanel = WorkspaceManager.createBottomPanel("errors", $(panelHtml), 100, Strings.CMD_VIEW_TOGGLE_PROBLEMS);
+ problemsPanel = WorkspaceManager.createBottomPanel("errors", $(panelHtml), 100, Strings.CMD_VIEW_TOGGLE_PROBLEMS, {iconClass: "fa-solid fa-triangle-exclamation"});
$problemsPanel = $("#problems-panel");
$fixAllBtn = $problemsPanel.find(".problems-fix-all-btn");
$fixAllBtn.click(()=>{
diff --git a/src/search/SearchResultsView.js b/src/search/SearchResultsView.js
index e79a86116..0c8a8c0d1 100644
--- a/src/search/SearchResultsView.js
+++ b/src/search/SearchResultsView.js
@@ -82,7 +82,7 @@ define(function (require, exports, module) {
const self = this;
let panelHtml = Mustache.render(searchPanelTemplate, {panelID: panelID});
- this._panel = WorkspaceManager.createBottomPanel(panelName, $(panelHtml), 100, title);
+ this._panel = WorkspaceManager.createBottomPanel(panelName, $(panelHtml), 100, title, {iconClass: "fa-solid fa-magnifying-glass"});
this._$summary = this._panel.$panel.find(".title");
this._$table = this._panel.$panel.find(".table-container");
this._$previewEditor = this._panel.$panel.find(".search-editor-preview");
diff --git a/src/styles/Extn-BottomPanelTabs.less b/src/styles/Extn-BottomPanelTabs.less
index b65d181ae..afe16fabf 100644
--- a/src/styles/Extn-BottomPanelTabs.less
+++ b/src/styles/Extn-BottomPanelTabs.less
@@ -144,12 +144,68 @@
}
}
+/* Tab icon: hidden by default, shown when tabs are collapsed */
+.bottom-panel-tab-icon {
+ display: none;
+ font-size: 1rem;
+ width: 1rem;
+ text-align: center;
+ pointer-events: none;
+}
+
+/* Override any FA class specificity (e.g. fa-brands sets font-size: 1.2em) */
+i.panel-titlebar-icon.panel-titlebar-icon {
+ font-size: 1rem;
+}
+
+img.panel-titlebar-icon {
+ width: 1rem;
+ height: 1rem;
+ vertical-align: middle;
+}
+
+.default-panel-btn i.panel-titlebar-icon {
+ font-size: 20px;
+}
+
+.default-panel-btn img.panel-titlebar-icon {
+ width: 20px;
+ height: 20px;
+}
+
+
.bottom-panel-tab-title {
display: inline-flex;
align-items: center;
pointer-events: none;
}
+/* Drag-and-drop tab reordering */
+.bottom-panel-tab-dragging {
+ opacity: 0.5;
+}
+
+/* Collapsed tab bar: show icons, hide titles for tabs that have icons */
+.bottom-panel-tabs-collapsed {
+ .bottom-panel-tab-icon {
+ display: inline-flex;
+ }
+ .bottom-panel-tab-icon ~ .bottom-panel-tab-title {
+ display: none;
+ }
+ /* Increase spacing in collapsed mode */
+ .bottom-panel-tab {
+ padding: 0 0.6rem 0 0.8rem;
+ }
+ .bottom-panel-tab-close-btn {
+ margin-left: 0.8rem;
+ }
+ /* Only show close button on the active tab to prevent accidental clicks */
+ .bottom-panel-tab:not(.active) .bottom-panel-tab-close-btn {
+ visibility: hidden;
+ }
+}
+
.bottom-panel-tab-close-btn {
margin-left: 0.55rem;
border-radius: 3px;
diff --git a/src/styles/Extn-Terminal.less b/src/styles/Extn-Terminal.less
index 3d2a1033b..8743e14ca 100644
--- a/src/styles/Extn-Terminal.less
+++ b/src/styles/Extn-Terminal.less
@@ -314,11 +314,8 @@
display: inline;
}
-/* ─── Shell dropdown (pops above the actions row) ─── */
+/* ─── Shell dropdown (positioned on $panel to avoid flyout overflow clip) ─── */
.terminal-shell-dropdown {
- position: absolute;
- bottom: 100%;
- right: 0;
min-width: 180px;
background: var(--terminal-tab-bg);
border: 1px solid var(--terminal-border);
diff --git a/src/view/DefaultPanelView.js b/src/view/DefaultPanelView.js
index 5dff454db..8bbdfd52b 100644
--- a/src/view/DefaultPanelView.js
+++ b/src/view/DefaultPanelView.js
@@ -43,17 +43,12 @@ define(function (require, exports, module) {
label: Strings.CMD_VIEW_TOGGLE_PROBLEMS || "Problems",
commandID: Commands.VIEW_TOGGLE_PROBLEMS
},
- {
- id: "search",
- icon: "fa-solid fa-magnifying-glass",
- label: Strings.CMD_FIND_IN_FILES || "Find in Files",
- commandID: Commands.CMD_FIND_IN_FILES
- },
{
id: "git",
- icon: "fa-solid fa-code-branch",
+ icon: "fa-brands fa-git-alt",
label: Strings.GIT_PANEL_TITLE || "Git",
- commandID: Commands.CMD_GIT_TOGGLE_PANEL
+ commandID: Commands.CMD_GIT_TOGGLE_PANEL,
+ nativeOnly: true
},
{
id: "snippets",
@@ -104,7 +99,7 @@ define(function (require, exports, module) {
.attr("data-command", btn.commandID)
.attr("data-btn-id", btn.id)
.attr("title", btn.label);
- let $icon = $('').addClass(btn.icon);
+ let $icon = $('').addClass(btn.icon);
let $label = $('').text(btn.label);
$button.append($icon).append($label);
$buttonsRow.append($button);
@@ -130,15 +125,16 @@ define(function (require, exports, module) {
/**
* Show or hide buttons based on current state.
- * The Problems button is always shown since the panel now displays
- * meaningful content regardless of error state.
+ * On desktop, Git is always shown. On browser, it depends on availability.
* @private
*/
function _updateButtonVisibility() {
if (!_$panel) {
return;
}
- _$panel.find('.default-panel-btn[data-btn-id="git"]').toggle(_isGitAvailable());
+ if (!Phoenix.isNativeApp) {
+ _$panel.find('.default-panel-btn[data-btn-id="git"]').toggle(_isGitAvailable());
+ }
}
/**
@@ -165,7 +161,8 @@ define(function (require, exports, module) {
WorkspaceManager.DEFAULT_PANEL_ID,
_$panel,
undefined,
- Strings.BOTTOM_PANEL_DEFAULT_TITLE
+ Strings.BOTTOM_PANEL_DEFAULT_TITLE,
+ {iconSvg: "styles/images/app-drawer.svg"}
);
// Button click handler: execute the command to open the target panel.
@@ -177,21 +174,6 @@ define(function (require, exports, module) {
}
});
- const iconHTML = '';
-
- /**
- * Inject the app-drawer icon into the Quick Access tab title.
- * Called each time the panel is shown because the tab DOM is rebuilt.
- */
- function _addTabIcon() {
- const $tabTitle = $('#bottom-panel-tab-bar .bottom-panel-tab[data-panel-id="'
- + WorkspaceManager.DEFAULT_PANEL_ID + '"] .bottom-panel-tab-title');
- if ($tabTitle.length && !$tabTitle.find(".app-drawer-tab-icon").length) {
- $tabTitle.prepend(iconHTML);
- }
- }
-
// The app-drawer button is defined in index.html; set its title here.
const $drawerBtn = $("#app-drawer-button")
.attr("title", Strings.BOTTOM_PANEL_DEFAULT_TITLE);
@@ -210,7 +192,6 @@ define(function (require, exports, module) {
_panel.hide();
} else {
_updateButtonVisibility();
- _addTabIcon();
}
$drawerBtn.toggleClass("selected-button", panelID === WorkspaceManager.DEFAULT_PANEL_ID);
});
diff --git a/src/view/PanelView.js b/src/view/PanelView.js
index 5f07df975..3b3134454 100644
--- a/src/view/PanelView.js
+++ b/src/view/PanelView.js
@@ -139,10 +139,42 @@ define(function (require, exports, module) {
* Call this when tabs are added, removed, or renamed.
* @private
*/
+ /**
+ * Build a tab element for a panel.
+ * @param {Panel} panel
+ * @param {boolean} isActive
+ * @return {jQueryObject}
+ * @private
+ */
+ function _buildTab(panel, isActive) {
+ let title = panel._tabTitle || _getPanelTitle(panel.panelID, panel.$panel);
+ let $tab = $('