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({