fix(desktop): layout reset reopens collapsed sidebars

resetLayoutTree() claims to "restore everything" but never reopened collapsed
SIDES. A sidebar hidden before a reset survived it, so the next ⌘B toggled from
that stale-hidden state into a SHOW — and the user's hide never persisted across
reload. Reopen every bound side through its store so the toggles stay truthful.

Adds a store-level regression test (real modules, re-import = reload) covering
hide→reload and the reset→hide→reload repro.
This commit is contained in:
Brooklyn Nicholson 2026-07-15 03:30:25 -04:00
parent c18edf4ac0
commit b5aef05e2c
2 changed files with 72 additions and 0 deletions

View file

@ -1148,6 +1148,14 @@ export function resetLayoutTree() {
resetHandlers.forEach(fn => fn())
// Everything still missing (plugin panes) adopts by placement.
adoptContributedPanes()
// "Restore everything" includes collapsed SIDES: reopen every bound side
// (through its store, so $sidebarOpen / the toggles stay truthful). Without
// this a sidebar hidden before the reset silently survives it, flipping the
// next ⌘B into a SHOW — so hiding never appears to persist.
for (const side of Object.keys(sideOpeners) as TreeSide[]) {
sideOpeners[side]?.(true)
}
}
// Dev hook for automation.

View file

@ -0,0 +1,64 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
// Ground-truth repro for "hiding the sidebar doesn't persist on reload": drive
// the REAL stores, then re-import them (fresh module state reading persisted
// localStorage) to simulate a ⌃R reload. `bind` mirrors the controller wiring.
async function loadStores() {
const layout = await import('./layout')
const tree = await import('@/components/pane-shell/tree/store')
return {
layout,
tree,
bind: () => tree.bindTreeSideVisibility('left', layout.$sidebarOpen, layout.setSidebarOpen),
leftCollapsed: () => tree.$collapsedTreeSides.get().has('left')
}
}
const reload = () => vi.resetModules() // fresh modules; localStorage is the carry-over
describe('sidebar collapse persistence', () => {
beforeEach(() => {
window.localStorage.clear()
vi.resetModules()
})
it('restores a hidden sidebar after a reload', async () => {
const s1 = await loadStores()
s1.bind()
s1.layout.setSidebarOpen(false)
expect(s1.leftCollapsed()).toBe(true)
reload()
const s2 = await loadStores()
expect(s2.layout.$sidebarOpen.get()).toBe(false) // persisted open:false survives
s2.bind()
expect(s2.leftCollapsed()).toBe(true) // and re-collapses
})
// The reported repro: a sidebar HIDDEN before a reset must be reopened by the
// reset ("restore everything"); otherwise the stale-hidden state flips the
// next ⌘B into a SHOW, and the user's hide never persists.
it('reset reopens a hidden sidebar, so a later hide persists across reload', async () => {
const s1 = await loadStores()
const { group, split } = await import('@/components/pane-shell/tree/model')
s1.tree.declareDefaultTree(split('row', [group(['sessions']), group(['workspace'])], [1, 3]))
s1.bind()
s1.layout.setSidebarOpen(false) // hidden BEFORE the reset
expect(s1.leftCollapsed()).toBe(true)
s1.tree.resetLayoutTree() // ⌘⇧ reset — restores everything, sidebar shown again
expect(s1.layout.$sidebarOpen.get()).toBe(true)
expect(s1.leftCollapsed()).toBe(false)
s1.layout.toggleSidebarOpen() // ⌘B now genuinely hides
expect(s1.layout.$sidebarOpen.get()).toBe(false)
reload()
const s2 = await loadStores()
expect(s2.layout.$sidebarOpen.get()).toBe(false)
s2.bind()
expect(s2.leftCollapsed()).toBe(true)
})
})