From ec21bd7674e78907ccacca846efad198e5cfdbbc Mon Sep 17 00:00:00 2001 From: "hermes-seaeye[bot]" <307254004+hermes-seaeye[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 21:50:23 +0000 Subject: [PATCH] fmt(js): `npm run fix` on merge (#119573) Co-authored-by: github-actions[bot] --- apps/desktop/src/app/contrib/layout-sides.ts | 9 +- .../tree/mode-layout-memory.test.ts | 104 ++++++++++-------- .../renderer/floating-mode-memory.test.tsx | 4 + .../tree/renderer/floating-panes.tsx | 29 +++-- .../tree/renderer/keep-alive-panes.test.tsx | 32 ++++-- .../tree/renderer/keep-alive-panes.tsx | 10 +- .../tree/renderer/narrow-overlays.tsx | 1 + .../pane-shell/tree/renderer/tree-group.tsx | 1 + .../src/components/pane-shell/tree/store.ts | 17 ++- .../src/lib/layout-persistence.test.ts | 1 + apps/desktop/src/lib/layout-persistence.ts | 6 +- 11 files changed, 137 insertions(+), 77 deletions(-) diff --git a/apps/desktop/src/app/contrib/layout-sides.ts b/apps/desktop/src/app/contrib/layout-sides.ts index 1e1e2baca9..3200902998 100644 --- a/apps/desktop/src/app/contrib/layout-sides.ts +++ b/apps/desktop/src/app/contrib/layout-sides.ts @@ -42,10 +42,15 @@ export function bindLayoutSides() { const $leftEdgeOpen = computed([$panesFlipped, $sidebarOpen, $fileBrowserOpen], (flipped, sidebar, files) => flipped ? files : sidebar ) + const $rightEdgeOpen = computed([$panesFlipped, $sidebarOpen, $fileBrowserOpen], (flipped, sidebar, files) => flipped ? sidebar : files ) - bindTreeSideVisibility('left', $leftEdgeOpen, open => ($panesFlipped.get() ? setFileBrowserOpen : setSidebarOpen)(open)) - bindTreeSideVisibility('right', $rightEdgeOpen, open => ($panesFlipped.get() ? setSidebarOpen : setFileBrowserOpen)(open)) + bindTreeSideVisibility('left', $leftEdgeOpen, open => + ($panesFlipped.get() ? setFileBrowserOpen : setSidebarOpen)(open) + ) + bindTreeSideVisibility('right', $rightEdgeOpen, open => + ($panesFlipped.get() ? setSidebarOpen : setFileBrowserOpen)(open) + ) } diff --git a/apps/desktop/src/components/pane-shell/tree/mode-layout-memory.test.ts b/apps/desktop/src/components/pane-shell/tree/mode-layout-memory.test.ts index aaf1eb85b9..28a4749aa4 100644 --- a/apps/desktop/src/components/pane-shell/tree/mode-layout-memory.test.ts +++ b/apps/desktop/src/components/pane-shell/tree/mode-layout-memory.test.ts @@ -28,6 +28,7 @@ async function boot() { ] as const) { registry.register({ id, area: 'panes', data: { placement }, render: () => null }) } + registry.register({ id: 'bots', area: 'panes', @@ -56,6 +57,7 @@ async function boot() { const preset = registry.getArea('layouts').find(p => p.id === id)! presets.applyLayoutPreset(id, preset.data as LayoutNode) } + const snapshot = () => ({ tree: structuredClone(tree.$layoutTree.get()), panes: structuredClone(panes.$paneStates.get()), @@ -114,7 +116,10 @@ it('restores independently customized modes through real pane bindings and reloa // A moved/customized tree, dismissals and an unfinished resize survive too. mode.setInterfaceMode('advanced') app.apply('default') - tree.moveTreePane('files', { groupId: model.findGroupOfPane(tree.$layoutTree.get()!, 'workspace')!.id, pos: 'center' }) + tree.moveTreePane('files', { + groupId: model.findGroupOfPane(tree.$layoutTree.get()!, 'workspace')!.id, + pos: 'center' + }) tree.dismissTreePane('review') const root = tree.$layoutTree.get()! @@ -122,7 +127,10 @@ it('restores independently customized modes through real pane bindings and reloa throw new Error('fixture must have a split') } - tree.setTreeSplitWeights(root.id, root.weights.map((weight, i) => weight + i)) + tree.setTreeSplitWeights( + root.id, + root.weights.map((weight, i) => weight + i) + ) const advanced = app.snapshot() mode.setInterfaceMode('simple') const simple = app.snapshot() @@ -139,47 +147,53 @@ it('restores independently customized modes through real pane bindings and reloa expect(app.model.allPaneIds(app.tree.$layoutTree.get()!)).toContain('new-shared-pane') }) -it.each(['advanced', 'simple'] as const)('keeps legacy %s data and never re-inherits it after migration', async initialMode => { - const { group, split, normalize } = await import('./model') - const legacyTree = split( - 'row', - [group(['sessions', 'bots']), group(['workspace']), group(['files', 'review', 'terminal'])], - [2, 8, 3] - ) - const legacyPanes = { 'chat-sidebar': { open: false, widthOverride: 301 }, 'file-browser': { open: true } } - const raw = JSON.stringify(legacyTree) - window.localStorage.setItem('hermes.desktop.layoutTree.v2', raw) - window.localStorage.setItem('hermes.desktop.paneStates.v1', JSON.stringify(legacyPanes)) - window.localStorage.setItem('hermes.desktop.layoutPreset.active', 'custom') - window.localStorage.setItem('hermes.desktop.hiddenStripTabs.v1', '["bots"]') - if (initialMode === 'simple') { - window.localStorage.setItem('hermes.desktop.interfaceMode.v1', 'simple') +it.each(['advanced', 'simple'] as const)( + 'keeps legacy %s data and never re-inherits it after migration', + async initialMode => { + const { group, split, normalize } = await import('./model') + + const legacyTree = split( + 'row', + [group(['sessions', 'bots']), group(['workspace']), group(['files', 'review', 'terminal'])], + [2, 8, 3] + ) + + const legacyPanes = { 'chat-sidebar': { open: false, widthOverride: 301 }, 'file-browser': { open: true } } + const raw = JSON.stringify(legacyTree) + window.localStorage.setItem('hermes.desktop.layoutTree.v2', raw) + window.localStorage.setItem('hermes.desktop.paneStates.v1', JSON.stringify(legacyPanes)) + window.localStorage.setItem('hermes.desktop.layoutPreset.active', 'custom') + window.localStorage.setItem('hermes.desktop.hiddenStripTabs.v1', '["bots"]') + + if (initialMode === 'simple') { + window.localStorage.setItem('hermes.desktop.interfaceMode.v1', 'simple') + } + + const { mode, tree, panes, layout } = await boot() + expect(mode.$interfaceMode.get()).toBe(initialMode) + expect(panes.$paneStates.get()).toEqual(legacyPanes) + expect(tree.$hiddenStripTabs.get().has('bots')).toBe(true) + // Boot collapse may update the tree, so preserve the settled arrangement. + const settled = structuredClone(tree.$layoutTree.get()) + mode.setInterfaceMode(initialMode === 'simple' ? 'advanced' : 'simple') + expect(tree.$collapsedTreeSides.get().has(layout.sidebarSide())).toBe(!layout.$sidebarOpen.get()) + expect(tree.$collapsedTreeSides.get().has(layout.fileBrowserSide())).toBe(!layout.$fileBrowserOpen.get()) + mode.setInterfaceMode(initialMode) + expect(tree.$layoutTree.get()).toEqual(settled) + expect(panes.$paneStates.get()).toEqual(legacyPanes) + + mode.setInterfaceMode('simple') + tree.setStripTabHidden('bots', false) + panes.setPaneWidthOverride('chat-sidebar', 210) + vi.resetModules() + const reloaded = await boot() + expect(reloaded.tree.$hiddenStripTabs.get().size).toBe(0) + expect(reloaded.panes.$paneStates.get()['chat-sidebar'].widthOverride).toBe(210) + reloaded.mode.setInterfaceMode('advanced') + expect(reloaded.panes.$paneStates.get()).toEqual(legacyPanes) + const restoredTree = reloaded.tree.$layoutTree.get() + const expectedTree = normalize(legacyTree) + assert(restoredTree && expectedTree) + expect(reloaded.model.allPaneIds(restoredTree)).toEqual(reloaded.model.allPaneIds(expectedTree)) } - - const { mode, tree, panes, layout } = await boot() - expect(mode.$interfaceMode.get()).toBe(initialMode) - expect(panes.$paneStates.get()).toEqual(legacyPanes) - expect(tree.$hiddenStripTabs.get().has('bots')).toBe(true) - // Boot collapse may update the tree, so preserve the settled arrangement. - const settled = structuredClone(tree.$layoutTree.get()) - mode.setInterfaceMode(initialMode === 'simple' ? 'advanced' : 'simple') - expect(tree.$collapsedTreeSides.get().has(layout.sidebarSide())).toBe(!layout.$sidebarOpen.get()) - expect(tree.$collapsedTreeSides.get().has(layout.fileBrowserSide())).toBe(!layout.$fileBrowserOpen.get()) - mode.setInterfaceMode(initialMode) - expect(tree.$layoutTree.get()).toEqual(settled) - expect(panes.$paneStates.get()).toEqual(legacyPanes) - - mode.setInterfaceMode('simple') - tree.setStripTabHidden('bots', false) - panes.setPaneWidthOverride('chat-sidebar', 210) - vi.resetModules() - const reloaded = await boot() - expect(reloaded.tree.$hiddenStripTabs.get().size).toBe(0) - expect(reloaded.panes.$paneStates.get()['chat-sidebar'].widthOverride).toBe(210) - reloaded.mode.setInterfaceMode('advanced') - expect(reloaded.panes.$paneStates.get()).toEqual(legacyPanes) - const restoredTree = reloaded.tree.$layoutTree.get() - const expectedTree = normalize(legacyTree) - assert(restoredTree && expectedTree) - expect(reloaded.model.allPaneIds(restoredTree)).toEqual(reloaded.model.allPaneIds(expectedTree)) -}) +) diff --git a/apps/desktop/src/components/pane-shell/tree/renderer/floating-mode-memory.test.tsx b/apps/desktop/src/components/pane-shell/tree/renderer/floating-mode-memory.test.tsx index 0f230140f8..dd9c047ca8 100644 --- a/apps/desktop/src/components/pane-shell/tree/renderer/floating-mode-memory.test.tsx +++ b/apps/desktop/src/components/pane-shell/tree/renderer/floating-mode-memory.test.tsx @@ -10,6 +10,7 @@ it.each([false, true])('restores floating geometry and collapse with keep-alive= const { registry } = await import('@/contrib/registry') const { setInterfaceMode } = await import('@/store/interface-mode') const { FloatingPanes } = await import('./floating-panes') + const dispose = registry.register({ area: 'panes', id: 'card', @@ -17,6 +18,7 @@ it.each([false, true])('restores floating geometry and collapse with keep-alive= data: { placement: 'floating', width: 240, height: 180, lifecycleKeepAlive: keepAlive }, render: () => }) + try { const view = render() const card = () => view.container.querySelector('[data-floating-pane="card"]')! @@ -33,10 +35,12 @@ it.each([false, true])('restores floating geometry and collapse with keep-alive= expect(card().style.left).toBe('37px') expect(card().style.top).toBe('71px') expect(view.getByRole('textbox')).toBeDefined() + if (keepAlive) { expect(view.getByRole('textbox')).toBe(input) expect(input.value).toBe('unsaved') } + act(() => setInterfaceMode('simple')) expect(card().style.left).toBe(simplePosition) expect(view.queryByRole('textbox')).toBeNull() diff --git a/apps/desktop/src/components/pane-shell/tree/renderer/floating-panes.tsx b/apps/desktop/src/components/pane-shell/tree/renderer/floating-panes.tsx index aae3f3b6e2..890b85ca46 100644 --- a/apps/desktop/src/components/pane-shell/tree/renderer/floating-panes.tsx +++ b/apps/desktop/src/components/pane-shell/tree/renderer/floating-panes.tsx @@ -80,10 +80,12 @@ function FloatingPane({ pane }: { pane: Contribution }) { (update: (current: FloatingRect) => FloatingRect) => { const positions = $positions.get() const current = positions[pane.id] + const next = update({ ...anchoredRect(anchor, { width: size.width, height: size.height }, viewport.current), ...current }) + $positions.set({ ...positions, [pane.id]: { x: next.x, y: next.y, collapsed: current?.collapsed } }) }, [pane.id, anchor, size.width, size.height] @@ -124,22 +126,25 @@ function FloatingPane({ pane }: { pane: Contribution }) { event.preventDefault() }, []) - const onPointerMove = useCallback((event: ReactPointerEvent) => { - const from = drag.current + const onPointerMove = useCallback( + (event: ReactPointerEvent) => { + const from = drag.current - if (!from) { - return - } + if (!from) { + return + } - drag.current = { x: event.clientX, y: event.clientY } + drag.current = { x: event.clientX, y: event.clientY } - setRect(current => - clampFloatingRect( - { ...current, x: current.x + event.clientX - from.x, y: current.y + event.clientY - from.y }, - viewport.current + setRect(current => + clampFloatingRect( + { ...current, x: current.x + event.clientX - from.x, y: current.y + event.clientY - from.y }, + viewport.current + ) ) - ) - }, [setRect]) + }, + [setRect] + ) const onPointerUp = useCallback( (event: ReactPointerEvent) => { diff --git a/apps/desktop/src/components/pane-shell/tree/renderer/keep-alive-panes.test.tsx b/apps/desktop/src/components/pane-shell/tree/renderer/keep-alive-panes.test.tsx index 0065868e14..c313e832da 100644 --- a/apps/desktop/src/components/pane-shell/tree/renderer/keep-alive-panes.test.tsx +++ b/apps/desktop/src/components/pane-shell/tree/renderer/keep-alive-panes.test.tsx @@ -27,6 +27,7 @@ import { LayoutTreeRoot } from '.' // A native guest has a lifetime beyond React. DOM identity alone misses a // detach/reparent (even moveBefore destroys Electron's webview guest). const disconnected: HTMLElement[] = [] + class LiveGuest extends HTMLElement { disconnectedCallback() { disconnected.push(this) @@ -37,6 +38,7 @@ customElements.define('pane-live-guest', LiveGuest) // jsdom has no layout. Deliver real observer notifications to the production // shared observer; the browser harness separately checks CSS anchor geometry. const observers = new Set() + class ResizeObserverProbe { targets = new Set() constructor(readonly callback: ResizeObserverCallback) { @@ -55,6 +57,7 @@ class ResizeObserverProbe { function resize(target: HTMLElement, width: number, height: number) { const size = { inlineSize: width, blockSize: height } + const entry: ResizeObserverEntry = { target, contentRect: new DOMRectReadOnly(0, 0, width, height), @@ -108,6 +111,7 @@ function registerPane(id: string, keepAlive = true, data: Record