From ab81f04faedeb33271e0d1c96c8a3530cfc772fa Mon Sep 17 00:00:00 2001 From: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com> Date: Sun, 19 Apr 2026 04:23:52 +0000 Subject: [PATCH] persist sidebar collapsed groups across navigation and restart (#722) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: persist sidebar collapsed groups across navigation and restart The sidebar's collapsed-group state was stored as local React state in WorktreeList, so the Sidebar unmount/remount triggered by navigating into Settings discarded it — returning to the main view re-expanded every previously collapsed group. Lift the state into the UI slice alongside groupBy/sortBy/filterRepoIds and persist it via the existing ui:set IPC so collapse state also survives app restarts. * fix: clear collapsed groups when switching groupBy mode Prevents stale keys from accumulating across mode switches and avoids cross-mode key collisions that could leave groups unexpectedly collapsed. --------- Co-authored-by: Jinwoo-H --- src/renderer/src/App.tsx | 1 + .../src/components/sidebar/WorktreeList.tsx | 16 ++----------- src/renderer/src/store/slices/ui.ts | 24 ++++++++++++++++++- src/shared/constants.ts | 1 + src/shared/types.ts | 1 + 5 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 646c32fda7a..2c6913f7a20 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -190,6 +190,7 @@ function App(): React.JSX.Element { sortBy: 'name', showActiveOnly: false, filterRepoIds: [], + collapsedGroups: [], uiZoomLevel: 0, editorFontZoomLevel: 0, worktreeCardProperties: [...DEFAULT_WORKTREE_CARD_PROPERTIES], diff --git a/src/renderer/src/components/sidebar/WorktreeList.tsx b/src/renderer/src/components/sidebar/WorktreeList.tsx index 8644e9297d9..a1f6ba43215 100644 --- a/src/renderer/src/components/sidebar/WorktreeList.tsx +++ b/src/renderer/src/components/sidebar/WorktreeList.tsx @@ -597,20 +597,8 @@ const WorktreeList = React.memo(function WorktreeList() { // the active navigation surface, so any modal should clear and disable them. const { showHints } = useModifierHint(activeModal === 'none') - // Collapsed group state - const [collapsedGroups, setCollapsedGroups] = useState>(new Set()) - - const toggleGroup = useCallback((key: string) => { - setCollapsedGroups((prev) => { - const next = new Set(prev) - if (next.has(key)) { - next.delete(key) - } else { - next.add(key) - } - return next - }) - }, []) + const collapsedGroups = useAppStore((s) => s.collapsedGroups) + const toggleGroup = useAppStore((s) => s.toggleCollapsedGroup) // Build flat row list for rendering const rows: Row[] = useMemo( diff --git a/src/renderer/src/store/slices/ui.ts b/src/renderer/src/store/slices/ui.ts index cceb27df84d..1d446a85010 100644 --- a/src/renderer/src/store/slices/ui.ts +++ b/src/renderer/src/store/slices/ui.ts @@ -122,6 +122,8 @@ export type UISlice = { setShowActiveOnly: (v: boolean) => void filterRepoIds: string[] setFilterRepoIds: (ids: string[]) => void + collapsedGroups: Set + toggleCollapsedGroup: (key: string) => void worktreeCardProperties: WorktreeCardProperty[] toggleWorktreeCardProperty: (prop: WorktreeCardProperty) => void statusBarItems: StatusBarItem[] @@ -225,7 +227,13 @@ export const createUISlice: StateCreator = (set, get) setSearchQuery: (q) => set({ searchQuery: q }), groupBy: 'none', - setGroupBy: (g) => set({ groupBy: g }), + // Why: group keys are mode-specific (e.g. repo id vs PR status), so + // collapsed state from one mode is meaningless in another. Clearing + // also prevents unbounded accumulation of stale keys across mode switches. + setGroupBy: (g) => { + window.api.ui.set({ collapsedGroups: [] }).catch(console.error) + set({ groupBy: g, collapsedGroups: new Set() }) + }, sortBy: 'name', setSortBy: (s) => set({ sortBy: s }), @@ -236,6 +244,19 @@ export const createUISlice: StateCreator = (set, get) filterRepoIds: [], setFilterRepoIds: (ids) => set({ filterRepoIds: ids }), + collapsedGroups: new Set(), + toggleCollapsedGroup: (key) => + set((s) => { + const next = new Set(s.collapsedGroups) + if (next.has(key)) { + next.delete(key) + } else { + next.add(key) + } + window.api.ui.set({ collapsedGroups: [...next] }).catch(console.error) + return { collapsedGroups: next } + }), + worktreeCardProperties: [...DEFAULT_WORKTREE_CARD_PROPERTIES], toggleWorktreeCardProperty: (prop) => set((s) => { @@ -307,6 +328,7 @@ export const createUISlice: StateCreator = (set, get) // worktree list stable across restarts instead of silently widening it. showActiveOnly: ui.showActiveOnly, filterRepoIds: (ui.filterRepoIds ?? []).filter((repoId) => validRepoIds.has(repoId)), + collapsedGroups: new Set(ui.collapsedGroups ?? []), uiZoomLevel: ui.uiZoomLevel ?? 0, editorFontZoomLevel: ui.editorFontZoomLevel ?? 0, worktreeCardProperties: ui.worktreeCardProperties ?? [...DEFAULT_WORKTREE_CARD_PROPERTIES], diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 91127cc6c06..0d1ce960251 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -169,6 +169,7 @@ export function getDefaultUIState(): PersistedUIState { sortBy: 'name', showActiveOnly: false, filterRepoIds: [], + collapsedGroups: [], uiZoomLevel: 0, editorFontZoomLevel: 0, worktreeCardProperties: [...DEFAULT_WORKTREE_CARD_PROPERTIES], diff --git a/src/shared/types.ts b/src/shared/types.ts index 4ac2061e3f8..6804e401929 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -692,6 +692,7 @@ export type PersistedUIState = { sortBy: 'name' | 'smart' | 'recent' | 'repo' showActiveOnly: boolean filterRepoIds: string[] + collapsedGroups: string[] uiZoomLevel: number editorFontZoomLevel: number worktreeCardProperties: WorktreeCardProperty[]