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.
This commit is contained in:
Bohdan Triapitsyn
2026-09-09 22:35:54 +03:00
parent d419fe9248
commit d9186afdc0
3 changed files with 61 additions and 37 deletions
+4
View File
@@ -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
@@ -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);
});
+5 -37
View File
@@ -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<string, ContextPanelDirectoryState>;
contextRailOrder: string[];
/** Surface ids the user hid from the context rail; stored as the hidden set
@@ -1184,8 +1183,7 @@ export const useUIStore = create<UIStore>()(
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<UIStore>()(
},
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) => {