persist sidebar collapsed groups across navigation and restart (#722)

* 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 <jinwoo0825@gmail.com>
This commit is contained in:
Kelvin Amoaba
2026-04-19 00:23:52 -04:00
committed by GitHub
co-authored by Jinwoo-H
parent 5d985a6563
commit ab81f04fae
5 changed files with 28 additions and 15 deletions
+1
View File
@@ -190,6 +190,7 @@ function App(): React.JSX.Element {
sortBy: 'name',
showActiveOnly: false,
filterRepoIds: [],
collapsedGroups: [],
uiZoomLevel: 0,
editorFontZoomLevel: 0,
worktreeCardProperties: [...DEFAULT_WORKTREE_CARD_PROPERTIES],
@@ -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<Set<string>>(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(
+23 -1
View File
@@ -122,6 +122,8 @@ export type UISlice = {
setShowActiveOnly: (v: boolean) => void
filterRepoIds: string[]
setFilterRepoIds: (ids: string[]) => void
collapsedGroups: Set<string>
toggleCollapsedGroup: (key: string) => void
worktreeCardProperties: WorktreeCardProperty[]
toggleWorktreeCardProperty: (prop: WorktreeCardProperty) => void
statusBarItems: StatusBarItem[]
@@ -225,7 +227,13 @@ export const createUISlice: StateCreator<AppState, [], [], UISlice> = (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<string>() })
},
sortBy: 'name',
setSortBy: (s) => set({ sortBy: s }),
@@ -236,6 +244,19 @@ export const createUISlice: StateCreator<AppState, [], [], UISlice> = (set, get)
filterRepoIds: [],
setFilterRepoIds: (ids) => set({ filterRepoIds: ids }),
collapsedGroups: new Set<string>(),
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<AppState, [], [], UISlice> = (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],
+1
View File
@@ -169,6 +169,7 @@ export function getDefaultUIState(): PersistedUIState {
sortBy: 'name',
showActiveOnly: false,
filterRepoIds: [],
collapsedGroups: [],
uiZoomLevel: 0,
editorFontZoomLevel: 0,
worktreeCardProperties: [...DEFAULT_WORKTREE_CARD_PROPERTIES],
+1
View File
@@ -692,6 +692,7 @@ export type PersistedUIState = {
sortBy: 'name' | 'smart' | 'recent' | 'repo'
showActiveOnly: boolean
filterRepoIds: string[]
collapsedGroups: string[]
uiZoomLevel: number
editorFontZoomLevel: number
worktreeCardProperties: WorktreeCardProperty[]