From d9186afdc0937f111202a5bc0bb071a1b23ea9e7 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 9 Sep 2026 22:35:54 +0300 Subject: [PATCH] fix(ui): preserve sidebar width when reopening Opening the sidebar reset its width to the reduced resize minimum. Keep visibility independent from the persisted width and restore the 280px initial default. Validated five sidebar regression tests, UI type-check and UI lint. --- packages/ui/src/stores/DOCUMENTATION.md | 4 ++ .../ui/src/stores/useUIStore.sidebar.test.ts | 52 +++++++++++++++++++ packages/ui/src/stores/useUIStore.ts | 42 ++------------- 3 files changed, 61 insertions(+), 37 deletions(-) create mode 100644 packages/ui/src/stores/useUIStore.sidebar.test.ts 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) => {