mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
fix(desktop): panel layout toggle bugs — terminal reveal, side collapse in column-root layouts, zone-menu restore (#65162)
* fix(desktop): un-minimize zone when revealing a collapsed tool panel `revealTreePane()` fronted the pane's tab but never un-minimized its zone when the pane lived in a shared zone (terminal + logs, or a tool panel stacked with the workspace). The shared-zone branch of `setPaneCollapsed` routes opens through `revealTreePane` instead of `toggleTreeGroupMinimized`, so the zone stayed collapsed and the terminal appeared to "close but not open" on Ctrl+` — and tab clicks needed a double-click because the first click's `restoreTreePane` → opener → listener → `revealTreePane` chain didn't un-minimize either. The fix: `revealTreePane` now restores a minimized zone before fronting the pane, chaining the un-minimize + activate into a single `commit` so the second op sees the updated tree. * fix(desktop): side collapse in column-root layouts + restore after zone-menu minimize Two follow-up fixes for the panel layout: 1. Ctrl+B / Ctrl+J sidebar toggles didn't work in the Terminal deck (and Quad) layout. The side-collapse system (treeSideOfPane, paneRootSide, layoutHasRootSide, and the renderer's semanticSides) only operated on the ROOT split when it was a row — but Terminal deck's root is a column, so the side columns (sessions/files) nested inside a child row were invisible to the collapse system. Extracted a rootRow() helper that finds the row containing main (the root itself for row layouts, or the row child of a column root), and a rootRow prop propagated through TreeNode → TreeSplit so semanticSides fires on the right split regardless of root orientation. 2. Clicking the "logs" tab in a minimized terminal/logs zone needed a double-click when the zone was minimized via the zone menu (right-click → minimize), not the toggle. restoreTreePane called the opener ($logsOpen.set(true)) and returned early — but when the store was already true, nanostores don't fire the listener, so setPaneCollapsed/revealTreePane never ran and the zone stayed minimized. Now restoreTreePane also un-minimizes directly + calls revealTreePane when the zone is still minimized after the opener runs.
This commit is contained in:
parent
b6c11a35ac
commit
c1945d410b
4 changed files with 110 additions and 18 deletions
|
|
@ -71,7 +71,7 @@ export function LayoutTreeRoot({ children }: { children?: ReactNode }) {
|
|||
display: none;
|
||||
}
|
||||
`}</style>
|
||||
<TreeNode node={tree} root />
|
||||
<TreeNode node={tree} root rootRow={tree.type === 'split' && tree.orientation === 'row'} />
|
||||
<NarrowOverlays />
|
||||
<TreeEditBar />
|
||||
<ZoneEditor />
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import { TreeSplit } from './tree-split'
|
|||
|
||||
/** Dispatch a layout node to its renderer — the split/group recursion point.
|
||||
* `root` marks the tree's top split (side collapse applies only there).
|
||||
* `rootRow` marks the row split that owns the side columns — usually the root
|
||||
* itself, but in a column-root layout (Terminal deck, Quad) it's the row
|
||||
* child holding sessions/workspace/files. Side collapse (⌘B/⌘J) applies here.
|
||||
* `parentAxis` is the containing split's orientation — a group collapses
|
||||
* ALONG that axis, so it picks the minimized form (row → vertical rail,
|
||||
* column → horizontal header). `railSide` is which half of that row the
|
||||
|
|
@ -13,15 +16,17 @@ export function TreeNode({
|
|||
node,
|
||||
parentAxis,
|
||||
railSide,
|
||||
root
|
||||
root,
|
||||
rootRow
|
||||
}: {
|
||||
node: LayoutNode
|
||||
parentAxis?: 'column' | 'row'
|
||||
railSide?: 'left' | 'right'
|
||||
root?: boolean
|
||||
rootRow?: boolean
|
||||
}) {
|
||||
return node.type === 'split' ? (
|
||||
<TreeSplit node={node} root={root} />
|
||||
<TreeSplit node={node} root={root} rootRow={rootRow} />
|
||||
) : (
|
||||
<TreeGroup node={node} parentAxis={parentAxis} railSide={railSide} />
|
||||
)
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ function useSubtreeOverrides(paneIds: readonly string[]): TrackContext['override
|
|||
return useSyncExternalStore(cb => $paneStates.listen(cb), snapshot, snapshot)
|
||||
}
|
||||
|
||||
export function TreeSplit({ node, root }: { node: SplitNode; root?: boolean }) {
|
||||
export function TreeSplit({ node, root, rootRow }: { node: SplitNode; root?: boolean; rootRow?: boolean }) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const panes = useContributions('panes')
|
||||
const hiddenPanes = useStore($hiddenTreePanes)
|
||||
|
|
@ -80,6 +80,21 @@ export function TreeSplit({ node, root }: { node: SplitNode; root?: boolean }) {
|
|||
const horizontal = node.orientation === 'row'
|
||||
const axis = node.orientation
|
||||
|
||||
// When the root is a column (Terminal deck, Quad), the root ROW — the one
|
||||
// the side-collapse system operates on — is a row child containing main.
|
||||
// Propagate `rootRow` to that child so its `semanticSides` fires.
|
||||
const childRootRow = (child: LayoutNode): boolean => {
|
||||
if (!root || horizontal) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (child.type !== 'split' || child.orientation !== 'row') {
|
||||
return false
|
||||
}
|
||||
|
||||
return allPaneIds(child).some(id => paneChrome(paneFor(id)).placement === 'main')
|
||||
}
|
||||
|
||||
// A pane leaves the grid when its contribution isn't registered (yet) — a
|
||||
// runtime plugin's pane collapses until the plugin loads, then appears; no
|
||||
// placeholder flash — when a chrome toggle hides it, or when the viewport
|
||||
|
|
@ -353,7 +368,9 @@ export function TreeSplit({ node, root }: { node: SplitNode; root?: boolean }) {
|
|||
// ⌘B owns the sessions column and ⌘J the other side columns — by pane
|
||||
// placement, NOT position, so a ⌘\ flip moves the columns without
|
||||
// rewiring the toggles (main parity). In edit mode sides stay visible.
|
||||
const semanticSides = root && horizontal && collapsedSides.size > 0 && !editMode
|
||||
// `rootRow` covers both a row root (Default, Focus) and a row nested inside
|
||||
// a column root (Terminal deck, Quad) — wherever the side columns live.
|
||||
const semanticSides = rootRow && horizontal && collapsedSides.size > 0 && !editMode
|
||||
|
||||
const sideGone = (i: number) => {
|
||||
if (!semanticSides) {
|
||||
|
|
@ -463,7 +480,12 @@ export function TreeSplit({ node, root }: { node: SplitNode; root?: boolean }) {
|
|||
/>
|
||||
)}
|
||||
{!narrowCollapsed && (
|
||||
<TreeNode node={child} parentAxis={axis} railSide={horizontal ? railSideFor(i) : undefined} />
|
||||
<TreeNode
|
||||
node={child}
|
||||
parentAxis={axis}
|
||||
railSide={horizontal ? railSideFor(i) : undefined}
|
||||
rootRow={rootRow || childRootRow(child)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@ import {
|
|||
setGroupHeaderHidden as setGroupHeaderHiddenOp,
|
||||
setGroupMinimized,
|
||||
setSplitWeights as setSplitWeightsOp,
|
||||
splitGroupZone as splitGroupZoneOp
|
||||
splitGroupZone as splitGroupZoneOp,
|
||||
type SplitNode
|
||||
} from './model'
|
||||
import { rootChildSide } from './renderer/track-model'
|
||||
|
||||
|
|
@ -357,18 +358,52 @@ export function removeTreePane(paneId: string) {
|
|||
}
|
||||
}
|
||||
|
||||
/** The layout's root ROW — the split that contains main + the side columns.
|
||||
* Usually the root itself (Default, Focus); in a column-root layout (Terminal
|
||||
* deck, Quad) it's the row child that holds sessions/workspace/files. Returns
|
||||
* null when the tree has no row split with side-eligible panes. */
|
||||
function rootRow(): SplitNode | null {
|
||||
const tree = $layoutTree.get()
|
||||
|
||||
if (!tree || tree.type !== 'split') {
|
||||
return null
|
||||
}
|
||||
|
||||
if (tree.orientation === 'row') {
|
||||
return tree
|
||||
}
|
||||
|
||||
// Column root: find the row child that contains the main pane — that's the
|
||||
// row the side-collapse system operates on (sessions left, files right).
|
||||
const panes = registry.getArea('panes')
|
||||
|
||||
const hasMain = (node: LayoutNode): boolean => {
|
||||
if (node.type === 'group') {
|
||||
return node.panes.some(id =>
|
||||
(panes.find(p => p.id === id)?.data as { placement?: string } | undefined)?.placement === 'main'
|
||||
)
|
||||
}
|
||||
|
||||
return node.children.some(hasMain)
|
||||
}
|
||||
|
||||
return tree.children.find(child => child.type === 'split' && child.orientation === 'row' && hasMain(child)) as
|
||||
| SplitNode
|
||||
| undefined ?? null
|
||||
}
|
||||
|
||||
/** Which root-row side a pane currently lives in, or null when it's nested
|
||||
* with main (dragged into the middle) — where a side collapse can't hide it.
|
||||
* Lets side-bound closers (files/sessions) fall back to dismissal. */
|
||||
export function paneRootSide(paneId: string): null | TreeSide {
|
||||
const tree = $layoutTree.get()
|
||||
const row = rootRow()
|
||||
|
||||
if (tree?.type !== 'split' || tree.orientation !== 'row') {
|
||||
if (!row) {
|
||||
return null
|
||||
}
|
||||
|
||||
const panes = registry.getArea('panes')
|
||||
const child = tree.children.find(c => allPaneIds(c).includes(paneId))
|
||||
const child = row.children.find(c => allPaneIds(c).includes(paneId))
|
||||
|
||||
return child ? rootChildSide(child, id => panes.find(p => p.id === id)) : null
|
||||
}
|
||||
|
|
@ -453,15 +488,15 @@ export function setTreeSideCollapsed(side: TreeSide, collapsed: boolean) {
|
|||
* reuses `rootChildSide`, so it tracks a ⌘\ flip / drag like the toggles do.
|
||||
*/
|
||||
export function layoutHasRootSide(side: TreeSide): boolean {
|
||||
const tree = $layoutTree.get()
|
||||
const row = rootRow()
|
||||
|
||||
if (tree?.type !== 'split' || tree.orientation !== 'row') {
|
||||
if (!row) {
|
||||
return false
|
||||
}
|
||||
|
||||
const panes = registry.getArea('panes')
|
||||
|
||||
return tree.children.some(child => rootChildSide(child, id => panes.find(p => p.id === id)) === side)
|
||||
return row.children.some(child => rootChildSide(child, id => panes.find(p => p.id === id)) === side)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -515,13 +550,13 @@ export function bindTreeSideVisibility(
|
|||
* panes) wherever it sits, ⌘J ⇔ the other side columns. Null for the main
|
||||
* column (never side-collapsed). */
|
||||
export function treeSideOfPane(paneId: string): TreeSide | null {
|
||||
const tree = $layoutTree.get()
|
||||
const row = rootRow()
|
||||
|
||||
if (!tree || tree.type !== 'split' || tree.orientation !== 'row') {
|
||||
if (!row) {
|
||||
return null
|
||||
}
|
||||
|
||||
const child = tree.children.find(node => allPaneIds(node).includes(paneId))
|
||||
const child = row.children.find(node => allPaneIds(node).includes(paneId))
|
||||
|
||||
if (!child) {
|
||||
return null
|
||||
|
|
@ -574,8 +609,26 @@ export function revealTreePane(paneId: string) {
|
|||
const tree = $layoutTree.get()
|
||||
const group = tree ? findGroupOfPane(tree, paneId) : null
|
||||
|
||||
if (tree && group && group.active !== paneId) {
|
||||
commit(setActivePaneOp(tree, group.id, paneId))
|
||||
if (tree && group) {
|
||||
// A minimized zone must be restored — "reveal" means show the pane, not
|
||||
// just front its tab behind a collapsed rail. Without this, a tool panel
|
||||
// (terminal/logs) in a shared zone stays minimized after its toggle opens
|
||||
// it: setPaneCollapsed's shared-zone branch calls revealTreePane instead
|
||||
// of toggleTreeGroupMinimized, so the zone never un-minimizes and the
|
||||
// pane appears to "close but not open" on ctrl-` / tab click.
|
||||
let next = tree
|
||||
|
||||
if (group.minimized) {
|
||||
next = setGroupMinimized(next, group.id, false)
|
||||
}
|
||||
|
||||
if (group.active !== paneId) {
|
||||
next = setActivePaneOp(next, group.id, paneId)
|
||||
}
|
||||
|
||||
if (next !== tree) {
|
||||
commit(next)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1039,6 +1092,18 @@ export function restoreTreePane(paneId: string) {
|
|||
if (open) {
|
||||
open()
|
||||
|
||||
// The opener may be a no-op — the store was already true (zone minimized
|
||||
// via the zone menu, not the toggle). nanostores don't fire listeners on
|
||||
// a same-value .set(), so the bindPaneCollapse listener never runs and
|
||||
// the zone stays minimized. Un-minimize directly when that happens.
|
||||
const group = paneGroup(paneId)
|
||||
|
||||
if (group?.minimized) {
|
||||
toggleTreeGroupMinimized(group.id, false)
|
||||
}
|
||||
|
||||
revealTreePane(paneId)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue