From 798e602a8e0323269275851f1140e77f7cd99104 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Tue, 14 Jul 2026 21:06:40 -0400 Subject: [PATCH] =?UTF-8?q?feat(desktop):=20tool=20panels=20collapse=20to?= =?UTF-8?q?=20a=20persistent=20rail,=20=E2=9C=95=20removes=20them?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Terminal and logs now follow the IntelliJ/VS-Code tool-window model: their toggle (⌃`, ⌘K) COLLAPSES the zone to a rail with the tab still showing instead of hiding it outright, so "toggle" and "the tab bar" stop fighting. Restore routes through the pane's store opener (rail click / chevron) so the shortcut and titlebar toggle stay truthful; the tab's ✕ dismisses the panel (comes back via its toggle), while a session tile's ✕ still closes the session. New store primitives (setPaneCollapsed / restoreTreePane / collapseTreePane + a collapse-pane registry) via a bindPaneCollapse in the controller. --- apps/desktop/src/app/contrib/controller.tsx | 27 ++++++- .../pane-shell/tree/renderer/tree-group.tsx | 28 +++++-- .../src/components/pane-shell/tree/store.ts | 75 +++++++++++++++++++ 3 files changed, 118 insertions(+), 12 deletions(-) diff --git a/apps/desktop/src/app/contrib/controller.tsx b/apps/desktop/src/app/contrib/controller.tsx index ff9452388..537ef8804 100644 --- a/apps/desktop/src/app/contrib/controller.tsx +++ b/apps/desktop/src/app/contrib/controller.tsx @@ -14,6 +14,7 @@ import { declareDefaultTree, dismissTreePane, dockPaneBeside, + markCollapsePane, mirrorLayoutTree, paneRootSide, registerLayoutResetHandler, @@ -21,6 +22,7 @@ import { registerPaneOpener, resetLayoutTree, revealTreePane, + setPaneCollapsed, setTreePaneHidden, watchContributedPanes } from '@/components/pane-shell/tree/store' @@ -412,6 +414,23 @@ function bindPaneVisibility( } } +// TOOL PANELS (terminal, logs): like bindPaneVisibility but the toggle COLLAPSES +// the zone to a persistent rail (tab stays) instead of hiding it — the +// IntelliJ/VS-Code tool-window model. Restore routes back through `open` (rail +// click / chevron) so ⌃`/the button stay truthful; the tab's ✕ removes it. +function bindPaneCollapse( + paneId: string, + $open: { get(): boolean; listen(fn: (open: boolean) => void): void }, + close: () => void, + open: () => void +) { + markCollapsePane(paneId) + setPaneCollapsed(paneId, !$open.get()) + $open.listen(isOpen => setPaneCollapsed(paneId, !isOpen)) + registerPaneCloser(paneId, close) + registerPaneOpener(paneId, open) +} + // SIDES have one source of truth: the TREE. The legacy $panesFlipped flag is // DERIVED from where the sessions zone actually sits (TitlebarControls maps // its left/right buttons through it), so dragging sessions across — or @@ -467,9 +486,9 @@ bindPaneVisibility( computed([$reviewOpen, $hasWorkspace], (open, workspace) => open && workspace), closeReview ) -// ⌃` / statusbar toggle — the terminal zone follows takeover instead of -// being forced on (PTYs stay alive while hidden; see PersistentTerminal). -bindPaneVisibility( +// ⌃` / statusbar toggle — the terminal COLLAPSES to a rail (tab stays), not +// hides; PTYs stay alive while collapsed (see PersistentTerminal). +bindPaneCollapse( 'terminal', $terminalTakeover, () => setTerminalTakeover(false), @@ -489,7 +508,7 @@ bindPaneVisibility('preview', $previewVisible, closeRightRail) // Logs are optional chrome: off by default, toggled from ⌘K, persisted. const $logsOpen = persistentAtom('hermes.desktop.logsOpen', false, Codecs.bool) -bindPaneVisibility( +bindPaneCollapse( 'logs', $logsOpen, () => $logsOpen.set(false), diff --git a/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.tsx b/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.tsx index 22dd8f74b..8f146b382 100644 --- a/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.tsx +++ b/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.tsx @@ -34,7 +34,11 @@ import { $treeDragging, activateTreePane, closeTreePane, + collapseTreePane, + dismissTreePane, + isCollapsePane, moveTreePane, + restoreTreePane, SESSION_TILE_DRAG, setTreeGroupHeaderHidden, splitTreeZone, @@ -254,6 +258,15 @@ export function TreeGroup({ // MAIN strands the whole app behind a strip. const minimizable = !shown.some(id => paneChrome(paneFor(id)).uncloseable) + // Tab ✕: a tool panel (terminal/logs) is REMOVED from the layout (comes back + // via its toggle); everything else routes through its Close (a session tile + // closes the session, a store-bound pane collapses). + const closeTab = (paneId: string) => (isCollapsePane(paneId) ? dismissTreePane(paneId) : closeTreePane(paneId)) + + // Collapse/restore a tool panel (or plain minimize elsewhere) — the header + // chevron + tap gesture, routed so ⌃`/the titlebar toggle stay truthful. + const toggleCollapse = () => (node.minimized ? restoreTreePane(activeId) : collapseTreePane(activeId)) + // Same menu on the header strip and the edit veil — one prop bag. const zoneMenu = { closable, @@ -299,7 +312,7 @@ export function TreeGroup({ // Strip line faces the content the zone collapsed away from. railSide === 'right' ? PANE_TAB_STRIP_LINE_LEFT : PANE_TAB_STRIP_LINE_RIGHT )} - onClick={() => toggleTreeGroupMinimized(node.id, false)} + onClick={() => restoreTreePane(activeId)} title={t.zones.restore} >
{ event.stopPropagation() - toggleTreeGroupMinimized(node.id, false) - activateTreePane(node.id, paneId) + restoreTreePane(paneId) }} - onClose={closeable ? () => closeTreePane(paneId) : undefined} + onClose={closeable ? () => closeTab(paneId) : undefined} role="tab" side={railSide} vertical @@ -359,7 +371,7 @@ export function TreeGroup({ startPaneDrag( activeId, e, - () => minimizable && toggleTreeGroupMinimized(node.id, !node.minimized), + () => minimizable && toggleCollapse(), undefined, hideHeaderDoubleTap ) @@ -383,7 +395,7 @@ export function TreeGroup({ aria-selected={isActive} data-tree-tab={paneId} key={paneId} - onClose={closeable ? () => closeTreePane(paneId) : undefined} + onClose={closeable ? () => closeTab(paneId) : undefined} onPointerDown={e => startPaneDrag( paneId, @@ -394,7 +406,7 @@ export function TreeGroup({ // — overloading the active tab made double-click a // minimize/restore/hide lottery. if (node.minimized) { - toggleTreeGroupMinimized(node.id, false) + restoreTreePane(paneId) } activateTreePane(node.id, paneId) @@ -419,7 +431,7 @@ export function TreeGroup({