diff --git a/packages/ui/src/stores/DOCUMENTATION.md b/packages/ui/src/stores/DOCUMENTATION.md index 6f893f51..c765dded 100644 --- a/packages/ui/src/stores/DOCUMENTATION.md +++ b/packages/ui/src/stores/DOCUMENTATION.md @@ -42,6 +42,10 @@ refresh attempt per opening, so a failed first load cannot create a retry loop. ### UI state stores +Sidebar visibility and its persisted width are independent. Opening or closing +the sidebar never writes a width; only resizing changes the saved choice. +The initial width is separate from the component's minimum resize width. + `useCommitSelectionStore.ts` shares the selected commit between desktop/mobile Changes and walkthrough. Choices are session-only and keyed by runtime, directory, and checked-out branch, with at most 100 remembered choices. The picker history diff --git a/packages/ui/src/stores/useUIStore.sidebar.test.ts b/packages/ui/src/stores/useUIStore.sidebar.test.ts new file mode 100644 index 00000000..726dd34a --- /dev/null +++ b/packages/ui/src/stores/useUIStore.sidebar.test.ts @@ -0,0 +1,52 @@ +import { afterEach, expect, test } from 'bun:test'; +import { useUIStore } from './useUIStore'; + +const originalOptions = useUIStore.persist.getOptions(); +const originalState = useUIStore.getState(); + +afterEach(() => { + useUIStore.persist.setOptions(originalOptions); + useUIStore.setState(originalState, true); +}); + +test('the initial sidebar width is independent of its resize minimum', () => { + expect(useUIStore.getInitialState().sidebarWidth).toBe(280); +}); + +for (const width of [168, 280, 360]) { + test(`reopening a persisted ${width}px sidebar preserves its width`, async () => { + useUIStore.persist.setOptions({ storage: { + getItem: () => ({ version: originalOptions.version, state: { sidebarWidth: width, isSidebarOpen: true } }), + setItem: () => undefined, + removeItem: () => undefined, + } }); + useUIStore.setState(useUIStore.getInitialState(), true); + await useUIStore.persist.rehydrate(); + + const actions = useUIStore.getState(); + actions.toggleSidebar(); + expect(useUIStore.getState().isSidebarOpen).toBe(false); + expect(useUIStore.getState().sidebarWidth).toBe(width); + actions.toggleSidebar(); + expect(useUIStore.getState().isSidebarOpen).toBe(true); + expect(useUIStore.getState().sidebarWidth).toBe(width); + + actions.setSidebarOpen(false); + actions.setSidebarOpen(true); + expect(useUIStore.getState().sidebarWidth).toBe(width); + const openState = useUIStore.getState(); + actions.setSidebarOpen(true); + expect(useUIStore.getState()).toBe(openState); + }); +} + +test('a manual resize stays authoritative across repeated visibility changes', () => { + useUIStore.setState(useUIStore.getInitialState(), true); + const actions = useUIStore.getState(); + actions.setSidebarWidth(420); + actions.setSidebarOpen(false); + actions.setSidebarOpen(true); + actions.toggleSidebar(); + actions.toggleSidebar(); + expect(useUIStore.getState().sidebarWidth).toBe(420); +}); diff --git a/packages/ui/src/stores/useUIStore.ts b/packages/ui/src/stores/useUIStore.ts index e2af3cd4..cbf9de35 100644 --- a/packages/ui/src/stores/useUIStore.ts +++ b/packages/ui/src/stores/useUIStore.ts @@ -217,7 +217,7 @@ const CONTEXT_PANEL_MAX_WIDTH = 1400; /** Per surface, not per panel: see clampContextPanelTabs. */ const CONTEXT_PANEL_MAX_TABS = 12; const CONTEXT_PANEL_MAX_LABEL_LENGTH = 120; -const LEFT_SIDEBAR_MIN_WIDTH = 168; +const LEFT_SIDEBAR_DEFAULT_WIDTH = 280; /** Separates browser tabs opened in the same millisecond. */ let browserTabSequence = 0; @@ -762,7 +762,6 @@ interface UIStore { multiRunLauncherPrefillPrompt: string; isSidebarOpen: boolean; sidebarWidth: number; - hasManuallyResizedLeftSidebar: boolean; contextPanelByDirectory: Record; contextRailOrder: string[]; /** Surface ids the user hid from the context rail; stored as the hidden set @@ -1184,8 +1183,7 @@ export const useUIStore = create()( isMultiRunLauncherOpen: false, multiRunLauncherPrefillPrompt: '', isSidebarOpen: true, - sidebarWidth: LEFT_SIDEBAR_MIN_WIDTH, - hasManuallyResizedLeftSidebar: false, + sidebarWidth: LEFT_SIDEBAR_DEFAULT_WIDTH, contextPanelByDirectory: {}, contextRailOrder: [], contextRailHiddenSurfaces: [], @@ -1347,45 +1345,15 @@ export const useUIStore = create()( }, toggleSidebar: () => { - set((state) => { - const newOpen = !state.isSidebarOpen; - - if (newOpen && !state.hasManuallyResizedLeftSidebar) { - return { - isSidebarOpen: newOpen, - sidebarWidth: LEFT_SIDEBAR_MIN_WIDTH, - }; - } - return { isSidebarOpen: newOpen }; - }); + set((state) => ({ isSidebarOpen: !state.isSidebarOpen })); }, setSidebarOpen: (open) => { - set((state) => { - if (state.isSidebarOpen === open) { - if (!open) { - return state; - } - if (!state.hasManuallyResizedLeftSidebar && state.sidebarWidth !== LEFT_SIDEBAR_MIN_WIDTH) { - return { - isSidebarOpen: open, - sidebarWidth: LEFT_SIDEBAR_MIN_WIDTH, - }; - } - return state; - } - if (open && !state.hasManuallyResizedLeftSidebar) { - return { - isSidebarOpen: open, - sidebarWidth: LEFT_SIDEBAR_MIN_WIDTH, - }; - } - return { isSidebarOpen: open }; - }); + set((state) => state.isSidebarOpen === open ? state : { isSidebarOpen: open }); }, setSidebarWidth: (width) => { - set({ sidebarWidth: width, hasManuallyResizedLeftSidebar: true }); + set({ sidebarWidth: width }); }, setContextRailOrder: (order) => {