summaryrefslogtreecommitdiffhomepage
path: root/packages/frontend/src/lib/sidebar-storage.ts
AgeCommit message (Collapse)Author
2026-06-04chore: genesis — remove all files to rebuild from scratch (arch rewrite)Adam Malczewski
2026-05-28feat(frontend): persist sidebar panel layout across browser refreshes via ↵Adam Malczewski
localStorage Carries the in-app sidebar layout (which views are open and in what order) across page reloads. Closes the natural follow-up to the tab- restore feature in d2e2e67: tabs survive, but until now the sidebar panels (Chat Settings / Tasks / Skills / Tools / etc.) reset to a single default panel on every load. Scope (explicitly bounded by the user): - Persistence target: localStorage. Matches the precedent for UI preferences (`dispatch-theme`, `dispatch-api-url`). Per-device layout; no backend round-trip. - sidebarOpen (the Header button that hides the whole sidebar column) is NOT persisted; always starts open on every load. - No drag-to-reorder UI added — persistence captures whatever order the user established via the existing add/remove buttons. Implementation: • New `packages/frontend/src/lib/sidebar-storage.ts` — pure functions `loadSidebarPanels(): string[]` and `saveSidebarPanels(selected: string[]): void`. localStorage key is `dispatch-sidebar-panels` (canonical `dispatch-` prefix). `loadSidebarPanels` is defensive against every failure mode (missing key, malformed JSON, non-array root, non-string entries, empty-after-filter, localStorage.getItem throwing under SecurityError). Returns a fresh array on every call so mutations by the caller don't pollute the module-level default constant. `saveSidebarPanels` swallows storage errors (quota / disabled / SecurityError) — best-effort. • `packages/frontend/src/lib/components/SidebarPanel.svelte`: seed the `panels: $state` from `loadSidebarPanels().map(s => ({ id: nextId++, selected: s }))` and add a `$effect` that calls `saveSidebarPanels(panels.map(p => p.selected))` whenever `panels` changes. The session-ephemeral `id` field is regenerated on every mount; only the `selected` strings round-trip. • Existing addPanel / remove / dropdown handlers untouched — they all reassign `panels` (`panels = [...panels, ...]`, `panels = panels.filter(...)`, `panels = panels.map(...)`), which triggers the new $effect. The minimum-one-panel invariant (X button hidden on idx 0) is preserved at the UI layer and reinforced by the loader's empty-fallback to the default layout. Tests: 15 new in `packages/frontend/tests/sidebar-storage.test.ts` — load with empty / valid / malformed / non-array / null / mixed-type / empty-after-filter / throwing-getItem; save round-trip; save error swallowing; overwrite semantics; empty-save / load-fallback; mutation isolation. Frontend total: 59 tests (was 44; +15). API 31, core 168 unchanged. Typecheck clean (svelte-check 0 errors), biome clean (126 files). Gemini code review (yolo mode, prompt-level write restriction to report.md only): SHIP, no findings.