mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
feat(desktop): tool panels collapse to a persistent rail, ✕ removes them
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.
This commit is contained in:
parent
ec926ce89e
commit
798e602a8e
3 changed files with 118 additions and 12 deletions
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
<div
|
||||
|
|
@ -319,10 +332,9 @@ export function TreeGroup({
|
|||
key={paneId}
|
||||
onClick={event => {
|
||||
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({
|
|||
<button
|
||||
aria-label={node.minimized ? t.zones.restore : t.zones.minimize}
|
||||
className="mx-1 grid size-5 shrink-0 place-items-center self-center rounded-md text-(--ui-text-tertiary) opacity-0 transition-opacity hover:bg-(--ui-control-hover-background) hover:text-foreground focus-visible:opacity-100 group-hover/pane-header:opacity-100"
|
||||
onClick={() => toggleTreeGroupMinimized(node.id, !node.minimized)}
|
||||
onClick={toggleCollapse}
|
||||
onPointerDown={e => e.stopPropagation()}
|
||||
type="button"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -171,6 +171,20 @@ export function registerPaneOpener(paneId: string, open: () => void) {
|
|||
paneOpeners[paneId] = open
|
||||
}
|
||||
|
||||
// TOOL PANELS (terminal, logs, …): their toggle COLLAPSES the zone to a rail
|
||||
// (tab stays) instead of hiding it, and the tab's ✕ REMOVES it (vs a session
|
||||
// tile, whose ✕ closes the session). Membership tells the renderer which
|
||||
// semantics a tab gets. See bindPaneCollapse in the controller.
|
||||
const collapsePanes = new Set<string>()
|
||||
|
||||
export function markCollapsePane(paneId: string) {
|
||||
collapsePanes.add(paneId)
|
||||
}
|
||||
|
||||
export function isCollapsePane(paneId: string): boolean {
|
||||
return collapsePanes.has(paneId)
|
||||
}
|
||||
|
||||
const resetHandlers = new Set<() => void>()
|
||||
|
||||
/** Run during a layout reset, BEFORE generic adoption — lets an owner
|
||||
|
|
@ -954,6 +968,67 @@ export function toggleTreeGroupMinimized(groupId: string, minimized: boolean) {
|
|||
}
|
||||
}
|
||||
|
||||
/** The group hosting `paneId`, or null. */
|
||||
function paneGroup(paneId: string) {
|
||||
const tree = $layoutTree.get()
|
||||
|
||||
return tree ? findGroupOfPane(tree, paneId) : null
|
||||
}
|
||||
|
||||
/** Collapse/restore a pane's ZONE to a minimized rail — its tab stays visible.
|
||||
* Store-driven (one-way): a tool panel's $open store mirrors here via
|
||||
* bindPaneCollapse, so a toggle collapses rather than hides. */
|
||||
export function setPaneCollapsed(paneId: string, collapsed: boolean) {
|
||||
const group = paneGroup(paneId)
|
||||
|
||||
if (group && Boolean(group.minimized) !== collapsed) {
|
||||
toggleTreeGroupMinimized(group.id, collapsed)
|
||||
|
||||
if (!collapsed) {
|
||||
revealTreePane(paneId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Restore a minimized tool pane the truthful way — through its store opener
|
||||
* when bound (keeps ⌃`/titlebar toggles in sync), else just un-minimize +
|
||||
* front. Used by the rail (tab / whole-rail click) and the header chevron. */
|
||||
export function restoreTreePane(paneId: string) {
|
||||
const open = paneOpeners[paneId]
|
||||
|
||||
if (open) {
|
||||
open()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const group = paneGroup(paneId)
|
||||
|
||||
if (group) {
|
||||
toggleTreeGroupMinimized(group.id, false)
|
||||
activateTreePane(group.id, paneId)
|
||||
}
|
||||
}
|
||||
|
||||
/** Collapse a tool pane through its store closer (truthful), else minimize the
|
||||
* zone directly. Gated on isCollapsePane so a non-tool pane's closer (a tile's
|
||||
* REMOVES it) is never mistaken for a collapse. */
|
||||
export function collapseTreePane(paneId: string) {
|
||||
const close = paneClosers[paneId]
|
||||
|
||||
if (isCollapsePane(paneId) && close) {
|
||||
close()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const group = paneGroup(paneId)
|
||||
|
||||
if (group) {
|
||||
toggleTreeGroupMinimized(group.id, true)
|
||||
}
|
||||
}
|
||||
|
||||
/** Hide/show a zone's header entirely (double-click gesture). */
|
||||
export function setTreeGroupHeaderHidden(groupId: string, headerHidden: boolean) {
|
||||
const tree = $layoutTree.get()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue