From b60abae4bd7e44ec4f4b785350887d2bac1263b5 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 7 Apr 2026 10:06:46 +0530 Subject: [PATCH 1/8] feat: git panel always on desktop, shell dropdown fix, tools cleanup - Git panel tab now always visible on desktop apps even for non-git projects, showing Init/Clone buttons - Removed Find in Files button from the Tools default panel launcher - Shell dropdown now appended to terminal body to avoid overflow clipping from the tab bar --- src/extensions/default/Git/src/Panel.js | 5 +++++ src/extensionsIntegrated/Terminal/main.js | 16 ++++++++++++++++ src/styles/Extn-Terminal.less | 5 +---- src/view/DefaultPanelView.js | 16 ++++++---------- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/extensions/default/Git/src/Panel.js b/src/extensions/default/Git/src/Panel.js index 3a90cac8e9..3b578cc991 100644 --- a/src/extensions/default/Git/src/Panel.js +++ b/src/extensions/default/Git/src/Panel.js @@ -1389,6 +1389,11 @@ define(function (require, exports) { $gitPanel.find(".git-not-available").show(); Utils.enableCommand(Constants.CMD_GIT_INIT, true); Utils.enableCommand(Constants.CMD_GIT_CLONE, true); + // On desktop, always show the git panel tab so users can + // init/clone even when the project is not a git repo. + if (Phoenix.isNativeApp) { + gitPanel.setVisible(true); + } } else { Main.$icon.addClass("warning"); toggle(false); diff --git a/src/extensionsIntegrated/Terminal/main.js b/src/extensionsIntegrated/Terminal/main.js index fda71c4aa8..02eabb8200 100644 --- a/src/extensionsIntegrated/Terminal/main.js +++ b/src/extensionsIntegrated/Terminal/main.js @@ -251,6 +251,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 () { diff --git a/src/styles/Extn-Terminal.less b/src/styles/Extn-Terminal.less index 3d2a1033b2..8743e14cab 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 5dff454dbf..36d27b570e 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", label: Strings.GIT_PANEL_TITLE || "Git", - commandID: Commands.CMD_GIT_TOGGLE_PANEL + commandID: Commands.CMD_GIT_TOGGLE_PANEL, + nativeOnly: true }, { id: "snippets", @@ -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()); + } } /** From 2292fa9d133d7f78599119cf29c406c4b7ea4477 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 7 Apr 2026 10:28:32 +0530 Subject: [PATCH 2/8] feat(terminal): route all keyboard events to terminal when focused MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Route all keyboard events to the terminal when it has focus instead of whitelisting individual keys. Only Command Palette (Ctrl+P) and F4 are passed back to Phoenix. Removed redundant EDITOR_SHORTCUTS from TerminalInstance.js — single source of truth is now PHOENIX_SHORTCUTS in main.js. --- .../Terminal/TerminalInstance.js | 18 --------- src/extensionsIntegrated/Terminal/main.js | 39 ++++++++++++------- 2 files changed, 26 insertions(+), 31 deletions(-) diff --git a/src/extensionsIntegrated/Terminal/TerminalInstance.js b/src/extensionsIntegrated/Terminal/TerminalInstance.js index 90d21e26d2..e0afb59b98 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 02eabb8200..e4e73bb262 100644 --- a/src/extensionsIntegrated/Terminal/main.js +++ b/src/extensionsIntegrated/Terminal/main.js @@ -147,11 +147,13 @@ 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. + // When the terminal is focused, route all keyboard events to the + // terminal instead of letting Phoenix keybindings intercept them. + // Only a few essential shortcuts are passed back to Phoenix. + const PHOENIX_SHORTCUTS = [ + {ctrlKey: true, key: "p"}, // Command Palette + {key: "f4"} // Switch terminals + ]; KeyBindingManager.addGlobalKeydownHook(function (event) { if (event.type !== "keydown") { return false; @@ -161,22 +163,33 @@ define(function (require, exports, module) { 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; + + // Show clear buffer hint on Ctrl+L + if (ctrlOrMeta && !event.shiftKey && key === "l") { + _showClearBufferHintToast(); + } + + // Let Phoenix handle these specific shortcuts + for (const shortcut of PHOENIX_SHORTCUTS) { + const ctrlMatch = shortcut.ctrlKey ? ctrlOrMeta : !ctrlOrMeta; + const shiftMatch = shortcut.shiftKey ? event.shiftKey : !event.shiftKey; + if (ctrlMatch && shiftMatch && key === shortcut.key.toLowerCase()) { + return false; // Let Phoenix handle it + } + } + + // Block Phoenix from handling everything else — let xterm get it + return true; }); // Refresh process info when the tab bar gains focus or mouse enters From a6314e6258d99af22d0560548f30dfb26cdc3236 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 7 Apr 2026 10:35:57 +0530 Subject: [PATCH 3/8] refactor(terminal): dynamic keyboard passthrough from command bindings Replace hardcoded PHOENIX_SHORTCUTS and EDITOR_SHORTCUTS with a dynamic set built from KeyBindingManager bindings for VIEW_TERMINAL and CMD_KEYBOARD_NAV_UI_OVERLAY. Rebuilds automatically when user changes keybindings. Extract logic into _setupPhoenixShortcuts(). --- src/extensionsIntegrated/Terminal/main.js | 108 +++++++++++++--------- 1 file changed, 64 insertions(+), 44 deletions(-) diff --git a/src/extensionsIntegrated/Terminal/main.js b/src/extensionsIntegrated/Terminal/main.js index e4e73bb262..02e26b7470 100644 --- a/src/extensionsIntegrated/Terminal/main.js +++ b/src/extensionsIntegrated/Terminal/main.js @@ -147,50 +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, route all keyboard events to the - // terminal instead of letting Phoenix keybindings intercept them. - // Only a few essential shortcuts are passed back to Phoenix. - const PHOENIX_SHORTCUTS = [ - {ctrlKey: true, key: "p"}, // Command Palette - {key: "f4"} // Switch terminals - ]; - 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; - } - - 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 these specific shortcuts - for (const shortcut of PHOENIX_SHORTCUTS) { - const ctrlMatch = shortcut.ctrlKey ? ctrlOrMeta : !ctrlOrMeta; - const shiftMatch = shortcut.shiftKey ? event.shiftKey : !event.shiftKey; - if (ctrlMatch && shiftMatch && key === shortcut.key.toLowerCase()) { - return false; // Let Phoenix handle it - } - } - - // Block Phoenix from handling everything else — let xterm get it - return true; - }); + _setupPhoenixShortcuts(); // Refresh process info when the tab bar gains focus or mouse enters $panel.find(".terminal-tab-bar").on("mouseenter", _refreshAllProcesses); @@ -671,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) */ From a74069dc10ec32dc43a16fd3910dc0d917834b30 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 7 Apr 2026 16:20:31 +0530 Subject: [PATCH 4/8] feat: collapse bottom panel tabs to icons on overflow Add icon support to bottom panel tabs via options object in createBottomPanel(). Tabs collapse to icons when the tab bar overflows. Supports both FA icon classes and SVG image paths. Panels without icons get a generic fallback icon. Uses panel-titlebar-icon class to override FA specificity issues. --- src/extensions/default/Git/src/Panel.js | 2 +- .../CustomSnippets/main.js | 2 +- .../DisplayShortcuts/main.js | 2 +- src/extensionsIntegrated/Terminal/main.js | 2 +- src/language/CodeInspection.js | 2 +- src/search/SearchResultsView.js | 2 +- src/styles/Extn-BottomPanelTabs.less | 40 ++++++++++ src/view/DefaultPanelView.js | 23 +----- src/view/PanelView.js | 78 +++++++++++++++---- src/view/WorkspaceManager.js | 7 +- 10 files changed, 117 insertions(+), 43 deletions(-) diff --git a/src/extensions/default/Git/src/Panel.js b/src/extensions/default/Git/src/Panel.js index 3b578cc991..6630b76a61 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 973c994cdc..d533e37701 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 0b8f49678c..ceb6e1ca50 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/main.js b/src/extensionsIntegrated/Terminal/main.js index 02e26b7470..b55323403c 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 () { diff --git a/src/language/CodeInspection.js b/src/language/CodeInspection.js index 027cfd1ad9..afad847b08 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 e79a86116b..0c8a8c0d16 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 b65d181aee..17580c6d8d 100644 --- a/src/styles/Extn-BottomPanelTabs.less +++ b/src/styles/Extn-BottomPanelTabs.less @@ -144,12 +144,52 @@ } } +/* 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; } +/* 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; + } +} + .bottom-panel-tab-close-btn { margin-left: 0.55rem; border-radius: 3px; diff --git a/src/view/DefaultPanelView.js b/src/view/DefaultPanelView.js index 36d27b570e..8bbdfd52ba 100644 --- a/src/view/DefaultPanelView.js +++ b/src/view/DefaultPanelView.js @@ -45,7 +45,7 @@ define(function (require, exports, module) { }, { 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, nativeOnly: true @@ -99,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); @@ -161,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. @@ -173,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); @@ -206,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 5f07df9758..c59db9fb9f 100644 --- a/src/view/PanelView.js +++ b/src/view/PanelView.js @@ -139,6 +139,34 @@ 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 = $('
') + .toggleClass('active', isActive) + .attr('data-panel-id', panel.panelID); + const opts = panel._options; + if (opts.iconClass) { + $tab.append($('') + .addClass(opts.iconClass)); + } else if (opts.iconSvg) { + $tab.append($('') + .attr("src", opts.iconSvg)); + } else { + // Fallback generic icon for panels without a custom icon + $tab.append($('')); + } + $tab.append($('').text(title)); + $tab.append($('×').attr('title', Strings.CLOSE)); + return $tab; + } + function _updateBottomPanelTabBar() { if (!_$tabsOverflow) { return; @@ -150,14 +178,7 @@ define(function (require, exports, module) { if (!panel) { return; } - let title = panel._tabTitle || _getPanelTitle(panelId, panel.$panel); - let isActive = (panelId === _activeId); - let $tab = $('
') - .toggleClass('active', isActive) - .attr('data-panel-id', panelId); - $tab.append($('').text(title)); - $tab.append($('×').attr('title', Strings.CLOSE)); - _$tabsOverflow.append($tab); + _$tabsOverflow.append(_buildTab(panel, panelId === _activeId)); }); // Re-append the "+" button at the end (after all tabs) @@ -165,6 +186,7 @@ define(function (require, exports, module) { _$tabsOverflow.append(_$addBtn); _updateAddButtonVisibility(); } + _checkTabOverflow(); } /** @@ -199,13 +221,7 @@ define(function (require, exports, module) { if (!panel) { return; } - let title = panel._tabTitle || _getPanelTitle(panelId, panel.$panel); - let isActive = (panelId === _activeId); - let $tab = $('
') - .toggleClass('active', isActive) - .attr('data-panel-id', panelId); - $tab.append($('').text(title)); - $tab.append($('×').attr('title', Strings.CLOSE)); + let $tab = _buildTab(panel, panelId === _activeId); // Insert before the "+" button so it stays at the end if (_$addBtn && _$addBtn.parent().length) { @@ -214,6 +230,7 @@ define(function (require, exports, module) { _$tabsOverflow.append($tab); } _updateAddButtonVisibility(); + _checkTabOverflow(); } /** @@ -228,6 +245,22 @@ define(function (require, exports, module) { } _$tabsOverflow.find('.bottom-panel-tab[data-panel-id="' + panelId + '"]').remove(); _updateAddButtonVisibility(); + _checkTabOverflow(); + } + + /** + * Check if the tab bar is overflowing and collapse tabs to icons if so. + * Only collapses tabs that have an icon available. + * @private + */ + function _checkTabOverflow() { + if (!_$tabBar) { + return; + } + // Remove collapsed state first to measure true width + _$tabBar.removeClass("bottom-panel-tabs-collapsed"); + const isOverflowing = _$tabsOverflow[0].scrollWidth > _$tabsOverflow[0].clientWidth; + _$tabBar.toggleClass("bottom-panel-tabs-collapsed", isOverflowing); } /** @@ -281,10 +314,19 @@ define(function (require, exports, module) { * @param {string} id Unique panel identifier. * @param {string=} title Optional display title for the tab bar. */ - function Panel($panel, id, title) { + /** + * @param {jQueryObject} $panel + * @param {string} id + * @param {string=} title + * @param {Object=} options + * @param {string=} options.iconClass FontAwesome class string (e.g. "fa-solid fa-terminal"). + * @param {string=} options.iconSvg Path to an SVG icon (e.g. "styles/images/icon.svg"). + */ + function Panel($panel, id, title, options) { this.$panel = $panel; this.panelID = id; this._tabTitle = _getPanelTitle(id, $panel, title); + this._options = options || {}; _panelMap[id] = this; } @@ -573,6 +615,10 @@ define(function (require, exports, module) { _toggleMaximize(); }); + // Re-check tab overflow when the tab bar resizes (e.g. window resize) + const tabBarResizeObserver = new ResizeObserver(_checkTabOverflow); + tabBarResizeObserver.observe(_$tabsOverflow[0]); + // Restore maximize state from preferences (survives reload). _isMaximized = PreferencesManager.getViewState(PREF_BOTTOM_PANEL_MAXIMIZED) === true; diff --git a/src/view/WorkspaceManager.js b/src/view/WorkspaceManager.js index e7babe09ae..02394d80ef 100644 --- a/src/view/WorkspaceManager.js +++ b/src/view/WorkspaceManager.js @@ -265,13 +265,16 @@ define(function (require, exports, module) { * attribute, for use as a preferences key. * @param {number=} minSize @deprecated No longer used. Pass `undefined`. * @param {string=} title Display title shown in the bottom panel tab bar. + * @param {Object=} options Optional settings: + * - {string} iconClass FontAwesome class string (e.g. "fa-solid fa-terminal"). + * - {string} iconSvg Path to an SVG icon (e.g. "styles/images/icon.svg"). * @return {!Panel} */ - function createBottomPanel(id, $panel, minSize, title) { + function createBottomPanel(id, $panel, minSize, title, options) { $bottomPanelContainer.append($panel); $panel.hide(); updateResizeLimits(); - let bottomPanel = new PanelView.Panel($panel, id, title); + let bottomPanel = new PanelView.Panel($panel, id, title, options); panelIDMap[id] = bottomPanel; return bottomPanel; } From 36382bfe790cdd1d33c87fa33537adb8e129d85a Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 7 Apr 2026 16:25:10 +0530 Subject: [PATCH 5/8] fix: increase spacing between icon and close button in collapsed tabs Add more padding and margin between the tab icon and close button when the tab bar is in collapsed/icon mode to prevent accidental close clicks. --- src/styles/Extn-BottomPanelTabs.less | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/styles/Extn-BottomPanelTabs.less b/src/styles/Extn-BottomPanelTabs.less index 17580c6d8d..dc7ed326ad 100644 --- a/src/styles/Extn-BottomPanelTabs.less +++ b/src/styles/Extn-BottomPanelTabs.less @@ -188,6 +188,13 @@ img.panel-titlebar-icon { .bottom-panel-tab-icon ~ .bottom-panel-tab-title { display: none; } + /* Increase spacing between icon and close button to prevent accidental clicks */ + .bottom-panel-tab { + padding: 0 0.6rem 0 0.8rem; + } + .bottom-panel-tab-close-btn { + margin-left: 0.8rem; + } } .bottom-panel-tab-close-btn { From a9302c1c84ba229645385e60cb7e2687ca0814c3 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 7 Apr 2026 16:34:26 +0530 Subject: [PATCH 6/8] fix: revert git panel auto-show on non-git repos The setVisible(true) call in disable("not-repo") changed toolbar icon behavior and broke git integration tests. Git visibility in the Tools launcher is already handled by DefaultPanelView. --- src/extensions/default/Git/src/Panel.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/extensions/default/Git/src/Panel.js b/src/extensions/default/Git/src/Panel.js index 6630b76a61..a8c4d7c9bb 100644 --- a/src/extensions/default/Git/src/Panel.js +++ b/src/extensions/default/Git/src/Panel.js @@ -1389,11 +1389,6 @@ define(function (require, exports) { $gitPanel.find(".git-not-available").show(); Utils.enableCommand(Constants.CMD_GIT_INIT, true); Utils.enableCommand(Constants.CMD_GIT_CLONE, true); - // On desktop, always show the git panel tab so users can - // init/clone even when the project is not a git repo. - if (Phoenix.isNativeApp) { - gitPanel.setVisible(true); - } } else { Main.$icon.addClass("warning"); toggle(false); From 83b1a4c7e206c1ae743ff6a03befff02b2aea6ef Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 7 Apr 2026 16:41:03 +0530 Subject: [PATCH 7/8] fix: hide close button on inactive tabs in collapsed icon mode Use visibility:hidden so inactive tabs keep their spacing but prevent accidental close clicks. Users must activate a tab first before closing it when in collapsed mode. --- src/styles/Extn-BottomPanelTabs.less | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/styles/Extn-BottomPanelTabs.less b/src/styles/Extn-BottomPanelTabs.less index dc7ed326ad..4f9a4745dc 100644 --- a/src/styles/Extn-BottomPanelTabs.less +++ b/src/styles/Extn-BottomPanelTabs.less @@ -188,13 +188,17 @@ img.panel-titlebar-icon { .bottom-panel-tab-icon ~ .bottom-panel-tab-title { display: none; } - /* Increase spacing between icon and close button to prevent accidental clicks */ + /* 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 { From 7dca2da75b29811e07f45458f940d7d241b5bc89 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 7 Apr 2026 16:56:11 +0530 Subject: [PATCH 8/8] feat: drag-and-drop tab reordering and tooltip in collapsed mode Extract drag-and-drop into _initDragAndDrop() with vertical line indicator matching file tab bar UX. Fix Tools button losing click handler after tab rebuild by using detach() before empty(). Show tooltip on hover in collapsed icon mode only. --- src/styles/Extn-BottomPanelTabs.less | 5 ++ src/view/PanelView.js | 114 ++++++++++++++++++++++++++- 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/src/styles/Extn-BottomPanelTabs.less b/src/styles/Extn-BottomPanelTabs.less index 4f9a4745dc..afe16fabfa 100644 --- a/src/styles/Extn-BottomPanelTabs.less +++ b/src/styles/Extn-BottomPanelTabs.less @@ -180,6 +180,11 @@ img.panel-titlebar-icon { 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 { diff --git a/src/view/PanelView.js b/src/view/PanelView.js index c59db9fb9f..3b3134454f 100644 --- a/src/view/PanelView.js +++ b/src/view/PanelView.js @@ -148,7 +148,7 @@ define(function (require, exports, module) { */ function _buildTab(panel, isActive) { let title = panel._tabTitle || _getPanelTitle(panel.panelID, panel.$panel); - let $tab = $('
') + let $tab = $('
') .toggleClass('active', isActive) .attr('data-panel-id', panel.panelID); const opts = panel._options; @@ -171,6 +171,10 @@ define(function (require, exports, module) { if (!_$tabsOverflow) { return; } + // Detach the add button before emptying to preserve its event handlers + if (_$addBtn) { + _$addBtn.detach(); + } _$tabsOverflow.empty(); _openIds.forEach(function (panelId) { @@ -248,6 +252,103 @@ define(function (require, exports, module) { _checkTabOverflow(); } + /** + * Set up drag-and-drop tab reordering on the bottom panel tab bar. + * Uses a vertical line indicator matching the file tab bar UX. + * @private + */ + function _initDragAndDrop() { + let draggedTab = null; + let $indicator = $('
'); + $("body").append($indicator); + + function getDropPosition(targetTab, mouseX) { + const rect = targetTab.getBoundingClientRect(); + return mouseX < rect.left + rect.width / 2; + } + + function updateIndicator(targetTab, insertBefore) { + if (!targetTab) { + $indicator.hide(); + return; + } + const rect = targetTab.getBoundingClientRect(); + $indicator.css({ + position: "fixed", + left: (insertBefore ? rect.left : rect.right) + "px", + top: rect.top + "px", + height: rect.height + "px", + width: "2px", + zIndex: 10001 + }).show(); + } + + function cleanup() { + if (draggedTab) { + $(draggedTab).removeClass("bottom-panel-tab-dragging"); + } + draggedTab = null; + $indicator.hide(); + _$tabBar.find(".bottom-panel-tab").removeClass("drag-target"); + } + + _$tabBar.on("dragstart", ".bottom-panel-tab", function (e) { + draggedTab = this; + e.originalEvent.dataTransfer.effectAllowed = "move"; + e.originalEvent.dataTransfer.setData("text/plain", "panel-tab"); + $(this).addClass("bottom-panel-tab-dragging"); + }); + + _$tabBar.on("dragend", ".bottom-panel-tab", function () { + setTimeout(cleanup, 50); + }); + + _$tabBar.on("dragover", ".bottom-panel-tab", function (e) { + if (!draggedTab || this === draggedTab) { + return; + } + e.preventDefault(); + e.originalEvent.dataTransfer.dropEffect = "move"; + _$tabBar.find(".bottom-panel-tab").removeClass("drag-target"); + $(this).addClass("drag-target"); + updateIndicator(this, getDropPosition(this, e.originalEvent.clientX)); + }); + + _$tabBar.on("dragleave", ".bottom-panel-tab", function (e) { + const related = e.originalEvent.relatedTarget; + if (!$(this).is(related) && !$(this).has(related).length) { + $(this).removeClass("drag-target"); + } + }); + + _$tabBar.on("drop", ".bottom-panel-tab", function (e) { + e.preventDefault(); + e.stopPropagation(); + if (!draggedTab || this === draggedTab) { + cleanup(); + return; + } + let draggedId = $(draggedTab).data("panel-id"); + let targetId = $(this).data("panel-id"); + let fromIdx = _openIds.indexOf(draggedId); + let toIdx = _openIds.indexOf(targetId); + if (fromIdx === -1 || toIdx === -1) { + cleanup(); + return; + } + const insertBefore = getDropPosition(this, e.originalEvent.clientX); + _openIds.splice(fromIdx, 1); + let newIdx = _openIds.indexOf(targetId); + if (!insertBefore) { + newIdx++; + } + _openIds.splice(newIdx, 0, draggedId); + cleanup(); + _updateBottomPanelTabBar(); + _updateActiveTabHighlight(); + }); + } + /** * Check if the tab bar is overflowing and collapse tabs to icons if so. * Only collapses tabs that have an icon available. @@ -261,6 +362,15 @@ define(function (require, exports, module) { _$tabBar.removeClass("bottom-panel-tabs-collapsed"); const isOverflowing = _$tabsOverflow[0].scrollWidth > _$tabsOverflow[0].clientWidth; _$tabBar.toggleClass("bottom-panel-tabs-collapsed", isOverflowing); + // Show tooltip on hover only in collapsed mode (title text is hidden) + _$tabBar.find(".bottom-panel-tab").each(function () { + const $tab = $(this); + if (isOverflowing) { + $tab.attr("title", $tab.find(".bottom-panel-tab-title").text()); + } else { + $tab.removeAttr("title"); + } + }); } /** @@ -584,6 +694,8 @@ define(function (require, exports, module) { } }); + _initDragAndDrop(); + // "+" button opens the default/quick-access panel _$addBtn.on("click", function (e) { e.stopPropagation();