Default workspace sidebar grouping to status

Default workspace sidebar grouping to status and keep group-by none organized with Pinned and All sections.
This commit is contained in:
Neil 2026-05-19 12:35:16 -07:00 committed by GitHub
parent 535052e2b8
commit 0d7e91002e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 53 additions and 18 deletions

View File

@ -233,7 +233,7 @@ describe('Store', () => {
const store = await createStore()
const ui = store.getUI()
expect(ui.sidebarWidth).toBe(280)
expect(ui.groupBy).toBe('repo')
expect(ui.groupBy).toBe('workspace-status')
expect(ui.lastActiveRepoId).toBeNull()
expect(ui.dismissedUpdateVersion).toBeNull()
expect(ui.lastUpdateCheckAt).toBeNull()
@ -1116,7 +1116,7 @@ describe('Store', () => {
store.updateUI({ sidebarWidth: 400 })
const ui = store.getUI()
expect(ui.sidebarWidth).toBe(400)
expect(ui.groupBy).toBe('repo') // default preserved
expect(ui.groupBy).toBe('workspace-status') // default preserved
expect(ui.dismissedUpdateVersion).toBeNull()
})

View File

@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import {
ALL_GROUP_META,
buildRows,
getGroupKeyForWorktree,
getLineageGroupKey,
@ -55,8 +56,8 @@ describe('getPRGroupKey', () => {
})
describe('getGroupKeyForWorktree', () => {
it('returns no group key for the ungrouped mode', () => {
expect(getGroupKeyForWorktree('none', worktree, repoMap, null)).toBeNull()
it('returns the all group key for the ungrouped mode', () => {
expect(getGroupKeyForWorktree('none', worktree, repoMap, null)).toBe('all')
})
it('returns a workspace-status key only in status grouping mode', () => {
@ -71,32 +72,46 @@ describe('buildRows with pinned worktrees', () => {
const unpinned1 = { ...worktree, id: 'wt-1', displayName: 'alpha' }
const unpinned2 = { ...worktree, id: 'wt-2', displayName: 'beta' }
it('emits a Pinned header followed by pinned items in groupBy none', () => {
it('emits Pinned and All headers in groupBy none', () => {
const rows = buildRows('none', [unpinned1, pinned, unpinned2], repoMap, null, new Set())
expect(rows[0]).toMatchObject({ type: 'header', key: 'pinned', label: 'Pinned', count: 1 })
expect(rows[1]).toMatchObject({ type: 'item', worktree: { id: 'wt-pinned' } })
expect(rows[2]).toMatchObject({ type: 'header', key: 'all', label: 'All', count: 2 })
expect(rows[2]).toMatchObject({ type: 'header', icon: ALL_GROUP_META.icon })
})
it('renders a flat list without status headers in groupBy none', () => {
it('groups all worktrees under All in groupBy none', () => {
const rows = buildRows('none', [unpinned1, unpinned2], repoMap, null, new Set())
expect(rows).toMatchObject([
{ type: 'header', key: 'all', label: 'All', count: 2 },
{ type: 'item', worktree: { id: 'wt-1' } },
{ type: 'item', worktree: { id: 'wt-2' } }
])
})
it('keeps pinned worktrees above the flat list', () => {
it('keeps pinned worktrees above the All group', () => {
const rows = buildRows('none', [unpinned1, pinned, unpinned2], repoMap, null, new Set())
expect(rows).toMatchObject([
{ type: 'header', key: 'pinned', count: 1 },
{ type: 'item', worktree: { id: 'wt-pinned' } },
{ type: 'header', key: 'all', count: 2 },
{ type: 'item', worktree: { id: 'wt-1' } },
{ type: 'item', worktree: { id: 'wt-2' } }
])
})
it('collapses the All group in groupBy none', () => {
const rows = buildRows('none', [unpinned1, pinned, unpinned2], repoMap, null, new Set(['all']))
expect(rows).toMatchObject([
{ type: 'header', key: 'pinned', count: 1 },
{ type: 'item', worktree: { id: 'wt-pinned' } },
{ type: 'header', key: 'all', count: 2 }
])
})
it('emits status headers for unpinned worktrees in groupBy workspace-status', () => {
const rows = buildRows(
'workspace-status',

View File

@ -1,5 +1,5 @@
/* eslint-disable max-lines -- Why: sidebar row construction keeps every grouping mode in one pure module so reveal, virtualized rendering, and tests share the same flat row contract. */
import { CircleX, Folder, Pin } from 'lucide-react'
import { CircleX, Folder, List, Pin } from 'lucide-react'
import type React from 'react'
import type {
Repo,
@ -105,6 +105,14 @@ export const PINNED_GROUP_META = {
icon: Pin
} as const
export const ALL_GROUP_KEY = 'all'
export const ALL_GROUP_META = {
label: 'All',
tone: 'text-foreground',
icon: List
} as const
export const MISSING_PARENT_GROUP_META = {
label: 'Missing parent'
} as const
@ -373,11 +381,23 @@ export function buildRows(
const unpinned = pinnedIds.size > 0 ? worktrees.filter((w) => !pinnedIds.has(w.id)) : worktrees
if (groupBy === 'none') {
appendWorktreeRows(result, unpinned, repoMap, lineageById, worktreeMap, {
nestLineage,
showLineageContext: nestLineage,
collapsedGroups
})
if (unpinned.length > 0) {
result.push({
type: 'header',
key: ALL_GROUP_KEY,
label: ALL_GROUP_META.label,
count: unpinned.length,
tone: ALL_GROUP_META.tone,
icon: ALL_GROUP_META.icon
})
if (!collapsedGroups.has(ALL_GROUP_KEY)) {
appendWorktreeRows(result, unpinned, repoMap, lineageById, worktreeMap, {
nestLineage,
showLineageContext: nestLineage,
collapsedGroups
})
}
}
return result
}
@ -513,7 +533,7 @@ export function getGroupKeyForWorktree(
workspaceStatuses: readonly WorkspaceStatusDefinition[] = cloneDefaultWorkspaceStatuses()
): string | null {
if (groupBy === 'none') {
return null
return ALL_GROUP_KEY
}
if (groupBy === 'workspace-status') {
return getWorkspaceStatusGroupKey(getWorkspaceStatus(worktree, workspaceStatuses))

View File

@ -52,7 +52,7 @@ describe('startup UI hydration fallback', () => {
expect(hydratePersistedUI).toHaveBeenCalledTimes(1)
expect(hydratePersistedUI.mock.calls[0][0].sidebarWidth).toBe(280)
expect(hydratePersistedUI.mock.calls[0][0].groupBy).toBe('repo')
expect(hydratePersistedUI.mock.calls[0][0].groupBy).toBe('workspace-status')
expect(hydratePersistedUI.mock.calls[0][0].sortBy).toBe('name')
})

View File

@ -34,7 +34,7 @@ export function getStartupErrorFallbackUI(uiHydrated: boolean): PersistedUIState
lastActiveWorktreeId: null,
sidebarWidth: 280,
rightSidebarWidth: 350,
groupBy: 'repo',
groupBy: 'workspace-status',
sortBy: 'name',
showActiveOnly: false,
hideDefaultBranchWorkspace: false,

View File

@ -770,7 +770,7 @@ export const createUISlice: StateCreator<AppState, [], [], UISlice> = (set, get)
return { trustedOrcaHooks: next }
}),
groupBy: 'repo',
groupBy: 'workspace-status',
// 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.

View File

@ -338,7 +338,7 @@ export function getDefaultUIState(): PersistedUIState {
lastActiveWorktreeId: null,
sidebarWidth: 280,
rightSidebarWidth: 350,
groupBy: 'repo',
groupBy: 'workspace-status',
sortBy: 'recent',
showActiveOnly: false,
hideDefaultBranchWorkspace: false,