feat: hide default branch workspace (#1249)

Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
This commit is contained in:
Yasin Kavakli 2026-05-04 00:55:43 +02:00 committed by GitHub
parent 66d596fe11
commit 7edcdca699
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 444 additions and 14 deletions

View File

@ -135,6 +135,7 @@ function App(): React.JSX.Element {
const groupBy = useAppStore((s) => s.groupBy)
const sortBy = useAppStore((s) => s.sortBy)
const showActiveOnly = useAppStore((s) => s.showActiveOnly)
const hideDefaultBranchWorkspace = useAppStore((s) => s.hideDefaultBranchWorkspace)
const filterRepoIds = useAppStore((s) => s.filterRepoIds)
const persistedUIReady = useAppStore((s) => s.persistedUIReady)
const rightSidebarWidth = useAppStore((s) => s.rightSidebarWidth)
@ -330,6 +331,7 @@ function App(): React.JSX.Element {
groupBy: 'none',
sortBy: 'recent',
showActiveOnly: false,
hideDefaultBranchWorkspace: false,
filterRepoIds: [],
collapsedGroups: [],
uiZoomLevel: 0,
@ -460,6 +462,7 @@ function App(): React.JSX.Element {
groupBy,
sortBy,
showActiveOnly,
hideDefaultBranchWorkspace,
filterRepoIds
})
}, 150)
@ -472,6 +475,7 @@ function App(): React.JSX.Element {
groupBy,
sortBy,
showActiveOnly,
hideDefaultBranchWorkspace,
filterRepoIds
])

View File

@ -16,6 +16,7 @@ import { parseGitHubIssueOrPRNumber, parseGitHubIssueOrPRLink } from '@/lib/gith
import { getLinkedWorkItemSuggestedName } from '@/lib/new-workspace'
import type { LinkedWorkItemSummary } from '@/lib/new-workspace'
import { sortWorktreesSmart } from '@/components/sidebar/smart-sort'
import { isDefaultBranchWorkspace } from '@/components/sidebar/visible-worktrees'
import StatusIndicator from '@/components/sidebar/StatusIndicator'
import { cn } from '@/lib/utils'
import { getWorktreeStatus, getWorktreeStatusLabel } from '@/lib/worktree-status'
@ -157,6 +158,7 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
const browserTabsByWorktree = useAppStore((s) => s.browserTabsByWorktree)
const browserPagesByWorkspace = useAppStore((s) => s.browserPagesByWorkspace)
const sshConnectionStates = useAppStore((s) => s.sshConnectionStates)
const hideDefaultBranchWorkspace = useAppStore((s) => s.hideDefaultBranchWorkspace)
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
@ -176,7 +178,24 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
const hasQuery = deferredQuery.trim().length > 0
const sortedWorktrees = useMemo(() => {
const visibleWorktrees = allWorktrees.filter((worktree) => !worktree.isArchived)
const visibleWorktrees = allWorktrees.filter((worktree) => {
if (worktree.isArchived) {
return false
}
// Why: keep the jump palette aligned with the sidebar. If the user
// opted to hide the default-branch workspace, surfacing it here via
// Cmd+J would reintroduce the entry they asked to remove.
// Drift warning: this check must stay in lockstep with the sidebar's
// filter in computeVisibleWorktreeIds (visible-worktrees.ts). Both
// surfaces share isDefaultBranchWorkspace so the predicate can't drift,
// but adding a new filter axis (e.g. a second toggle) here would need
// the matching change in the sidebar pipeline — otherwise Cmd+J and
// the sidebar will show different lists.
if (hideDefaultBranchWorkspace && isDefaultBranchWorkspace(worktree)) {
return false
}
return true
})
// Why: on empty query, show pure recency (matches sidebar's 'recent' sort
// rationale in smart-sort.ts) so Cmd+J is a predictable "jump back to what
// I was just on" surface. Typing swaps in smart-sort to rank matches.
@ -188,11 +207,15 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
(a, b) =>
b.lastActivityAt - a.lastActivityAt || a.displayName.localeCompare(b.displayName)
)
}, [allWorktrees, tabsByWorktree, repoMap, prCache, hasQuery])
}, [allWorktrees, tabsByWorktree, repoMap, prCache, hasQuery, hideDefaultBranchWorkspace])
const browserSortedWorktrees = useMemo(() => {
// Why: browser-tab search is explicitly cross-worktree, so it must keep
// indexing live browser pages even when their owning worktree is archived.
// indexing live browser pages even when their owning worktree is archived
// or hidden by the default-branch-workspace setting. A user who opened a
// tab on the default-branch worktree before toggling hide-on should still
// be able to Cmd+J back to it — the setting hides the *workspace row*,
// not the browser tabs that live inside it.
return sortWorktreesSmart(allWorktrees, tabsByWorktree, repoMap, prCache)
}, [allWorktrees, tabsByWorktree, repoMap, prCache])

View File

@ -1,5 +1,5 @@
import React, { useCallback } from 'react'
import { Activity, ListFilter, FolderPlus, X } from 'lucide-react'
import { Activity, GitBranch, ListFilter, FolderPlus, X } from 'lucide-react'
import { useAppStore } from '@/store'
import { Button } from '@/components/ui/button'
import {
@ -17,6 +17,8 @@ import RepoDotLabel from '@/components/repo/RepoDotLabel'
const SidebarFilter = React.memo(function SidebarFilter() {
const showActiveOnly = useAppStore((s) => s.showActiveOnly)
const setShowActiveOnly = useAppStore((s) => s.setShowActiveOnly)
const hideDefaultBranchWorkspace = useAppStore((s) => s.hideDefaultBranchWorkspace)
const setHideDefaultBranchWorkspace = useAppStore((s) => s.setHideDefaultBranchWorkspace)
const filterRepoIds = useAppStore((s) => s.filterRepoIds)
const setFilterRepoIds = useAppStore((s) => s.setFilterRepoIds)
const repos = useAppStore((s) => s.repos)
@ -37,14 +39,19 @@ const SidebarFilter = React.memo(function SidebarFilter() {
() => setShowActiveOnly(!showActiveOnly),
[showActiveOnly, setShowActiveOnly]
)
const handleToggleHideDefaultBranch = useCallback(
() => setHideDefaultBranchWorkspace(!hideDefaultBranchWorkspace),
[hideDefaultBranchWorkspace, setHideDefaultBranchWorkspace]
)
const canFilterRepos = repos.length > 1
// Why: derive from the current repos list so stale IDs in filterRepoIds
// (e.g. lingering after a repo is removed) don't inflate the active-filter
// count or falsely signal an applied filter.
const selectedRepos = canFilterRepos ? repos.filter((r) => filterRepoIds.includes(r.id)) : []
const hasRepoFilter = selectedRepos.length > 0
const hasAnyFilter = showActiveOnly || hasRepoFilter
const activeFilterCount = (showActiveOnly ? 1 : 0) + selectedRepos.length
const hasAnyFilter = showActiveOnly || hideDefaultBranchWorkspace || hasRepoFilter
const activeFilterCount =
(showActiveOnly ? 1 : 0) + (hideDefaultBranchWorkspace ? 1 : 0) + selectedRepos.length
return (
<DropdownMenu>
@ -79,7 +86,6 @@ const SidebarFilter = React.memo(function SidebarFilter() {
</TooltipContent>
</Tooltip>
<DropdownMenuContent align="end" className="min-w-[12rem]">
<DropdownMenuLabel>Status</DropdownMenuLabel>
<DropdownMenuCheckboxItem
checked={showActiveOnly}
onCheckedChange={handleToggleActive}
@ -88,6 +94,14 @@ const SidebarFilter = React.memo(function SidebarFilter() {
<Activity className="size-3.5 text-muted-foreground" />
Active only
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={hideDefaultBranchWorkspace}
onCheckedChange={handleToggleHideDefaultBranch}
onSelect={(event) => event.preventDefault()}
>
<GitBranch className="size-3.5 text-muted-foreground" />
Hide default branch
</DropdownMenuCheckboxItem>
{canFilterRepos && (
<>
<DropdownMenuSeparator />
@ -110,6 +124,7 @@ const SidebarFilter = React.memo(function SidebarFilter() {
<DropdownMenuItem
onSelect={() => {
setShowActiveOnly(false)
setHideDefaultBranchWorkspace(false)
setFilterRepoIds([])
}}
>

View File

@ -28,7 +28,12 @@ import {
buildRows,
getGroupKeyForWorktree
} from './worktree-list-groups'
import { computeVisibleWorktreeIds, setVisibleWorktreeIds } from './visible-worktrees'
import {
computeClearFilterActions,
computeVisibleWorktreeIds,
setVisibleWorktreeIds,
sidebarHasActiveFilters
} from './visible-worktrees'
import { useModifierHint } from '@/hooks/useModifierHint'
import { activateAndRevealWorktree } from '@/lib/worktree-activation'
@ -440,6 +445,7 @@ const WorktreeList = React.memo(function WorktreeList() {
const groupBy = useAppStore((s) => s.groupBy)
const sortBy = useAppStore((s) => s.sortBy)
const showActiveOnly = useAppStore((s) => s.showActiveOnly)
const hideDefaultBranchWorkspace = useAppStore((s) => s.hideDefaultBranchWorkspace)
const filterRepoIds = useAppStore((s) => s.filterRepoIds)
const openModal = useAppStore((s) => s.openModal)
const activeView = useAppStore((s) => s.activeView)
@ -632,6 +638,7 @@ const WorktreeList = React.memo(function WorktreeList() {
tabsByWorktree,
browserTabsByWorktree,
activeWorktreeId,
hideDefaultBranchWorkspace,
repoMap
})
return ids.map((id) => worktreeMap.get(id)).filter((w): w is Worktree => w != null)
@ -639,6 +646,7 @@ const WorktreeList = React.memo(function WorktreeList() {
filterRepoIds,
showActiveOnly,
activeWorktreeId,
hideDefaultBranchWorkspace,
repoMap,
tabsByWorktree,
browserTabsByWorktree,
@ -721,14 +729,32 @@ const WorktreeList = React.memo(function WorktreeList() {
[openModal]
)
const hasFilters = !!(showActiveOnly || filterRepoIds.length)
// Why: hideDefaultBranchWorkspace is counted as a filter here so the
// empty-sidebar escape hatch (Clear Filters button below) is reachable when
// it's the only reason the list is empty — otherwise a user whose only
// worktree is a default-branch row and who just toggled hide on would see
// "No worktrees found" with no way back short of reopening the filter menu.
const filterState = useMemo(
() => ({ showActiveOnly, filterRepoIds, hideDefaultBranchWorkspace }),
[showActiveOnly, filterRepoIds, hideDefaultBranchWorkspace]
)
const hasFilters = sidebarHasActiveFilters(filterState)
const setShowActiveOnly = useAppStore((s) => s.setShowActiveOnly)
const setHideDefaultBranchWorkspace = useAppStore((s) => s.setHideDefaultBranchWorkspace)
const setFilterRepoIds = useAppStore((s) => s.setFilterRepoIds)
const clearFilters = useCallback(() => {
setShowActiveOnly(false)
setFilterRepoIds([])
}, [setShowActiveOnly, setFilterRepoIds])
const actions = computeClearFilterActions(filterState)
if (actions.resetShowActiveOnly) {
setShowActiveOnly(false)
}
if (actions.resetFilterRepoIds) {
setFilterRepoIds([])
}
if (actions.resetHideDefaultBranchWorkspace) {
setHideDefaultBranchWorkspace(false)
}
}, [setShowActiveOnly, setFilterRepoIds, setHideDefaultBranchWorkspace, filterState])
if (worktrees.length === 0) {
return (

View File

@ -1,6 +1,24 @@
import { describe, expect, it } from 'vitest'
import { computeVisibleWorktreeIds } from './visible-worktrees'
import type { Repo, Worktree } from '../../../../shared/types'
import {
computeClearFilterActions,
computeVisibleWorktreeIds,
isDefaultBranchWorkspace,
sidebarHasActiveFilters
} from './visible-worktrees'
import type { Repo, TerminalTab, Worktree } from '../../../../shared/types'
function makeTab(id: string, worktreeId: string, ptyId: string | null): TerminalTab {
return {
id,
ptyId,
worktreeId,
title: id,
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 0
}
}
function makeWorktree(id: string, repoId = 'repo1'): Worktree {
return {
@ -34,6 +52,16 @@ const repoMap = new Map<string, Repo>([
badgeColor: '#000',
addedAt: 0
}
],
[
'repo2',
{
id: 'repo2',
path: '/repo2',
displayName: 'Repo 2',
badgeColor: '#111',
addedAt: 0
}
]
])
@ -47,6 +75,7 @@ describe('computeVisibleWorktreeIds', () => {
tabsByWorktree: {},
browserTabsByWorktree: { [wt.id]: [{ id: 'browser-1' }] },
activeWorktreeId: null,
hideDefaultBranchWorkspace: false,
repoMap
})
@ -62,9 +91,248 @@ describe('computeVisibleWorktreeIds', () => {
tabsByWorktree: {},
browserTabsByWorktree: {},
activeWorktreeId: wt.id,
hideDefaultBranchWorkspace: false,
repoMap
})
expect(result).toEqual([wt.id])
})
it('hides branch-backed main worktrees when default branch workspaces are hidden', () => {
const main = makeWorktree('main')
const feature = makeWorktree('feature')
main.isMainWorktree = true
const result = computeVisibleWorktreeIds({ repo1: [main, feature] }, [main.id, feature.id], {
filterRepoIds: [],
showActiveOnly: false,
tabsByWorktree: {},
browserTabsByWorktree: {},
activeWorktreeId: main.id,
hideDefaultBranchWorkspace: true,
repoMap
})
expect(result).toEqual([feature.id])
})
it('keeps folder-mode main worktrees visible when default branch workspaces are hidden', () => {
const folder = makeWorktree('folder')
folder.isMainWorktree = true
folder.branch = ''
const result = computeVisibleWorktreeIds({ repo1: [folder] }, [folder.id], {
filterRepoIds: [],
showActiveOnly: false,
tabsByWorktree: {},
browserTabsByWorktree: {},
activeWorktreeId: null,
hideDefaultBranchWorkspace: true,
repoMap
})
expect(result).toEqual([folder.id])
})
it('hides branch-backed mains across every repo in a multi-repo workspace', () => {
const main1 = makeWorktree('main1', 'repo1')
main1.isMainWorktree = true
const feature1 = makeWorktree('feature1', 'repo1')
const main2 = makeWorktree('main2', 'repo2')
main2.isMainWorktree = true
const feature2 = makeWorktree('feature2', 'repo2')
const result = computeVisibleWorktreeIds(
{ repo1: [main1, feature1], repo2: [main2, feature2] },
[main1.id, feature1.id, main2.id, feature2.id],
{
filterRepoIds: [],
showActiveOnly: false,
tabsByWorktree: {},
browserTabsByWorktree: {},
activeWorktreeId: null,
hideDefaultBranchWorkspace: true,
repoMap
}
)
expect(result).toEqual([feature1.id, feature2.id])
})
it('composes with showActiveOnly: the hidden main is dropped even if it is the active worktree', () => {
const main = makeWorktree('main')
main.isMainWorktree = true
const feature = makeWorktree('feature')
// Why: verifies filter ordering — hide runs before showActiveOnly, so
// main doesn't slip back in via the "active worktree is always visible"
// exception that showActiveOnly grants. Feature stays because it has a
// live PTY.
const result = computeVisibleWorktreeIds({ repo1: [main, feature] }, [main.id, feature.id], {
filterRepoIds: [],
showActiveOnly: true,
tabsByWorktree: { [feature.id]: [makeTab('t1', feature.id, 'p1')] },
browserTabsByWorktree: {},
activeWorktreeId: main.id,
hideDefaultBranchWorkspace: true,
repoMap
})
expect(result).toEqual([feature.id])
})
it('composes with filterRepoIds: hides mains only within the selected repos', () => {
const main1 = makeWorktree('main1', 'repo1')
main1.isMainWorktree = true
const feature1 = makeWorktree('feature1', 'repo1')
const main2 = makeWorktree('main2', 'repo2')
main2.isMainWorktree = true
const feature2 = makeWorktree('feature2', 'repo2')
// Why: the filterRepoIds=['repo1'] already drops everything in repo2, so
// to actually prove the hide filter is scoped to the selected repos we
// need to flip the situation — select repo2 instead. Only main2 should be
// dropped by hide; main1 survives because the repo filter has already
// removed it from consideration.
const result = computeVisibleWorktreeIds(
{ repo1: [main1, feature1], repo2: [main2, feature2] },
[main1.id, feature1.id, main2.id, feature2.id],
{
filterRepoIds: ['repo2'],
showActiveOnly: false,
tabsByWorktree: {},
browserTabsByWorktree: {},
activeWorktreeId: null,
hideDefaultBranchWorkspace: true,
repoMap
}
)
expect(result).toEqual([feature2.id])
})
})
describe('isDefaultBranchWorkspace', () => {
it('returns true for a branch-backed main worktree', () => {
const main = makeWorktree('main')
main.isMainWorktree = true
expect(isDefaultBranchWorkspace(main)).toBe(true)
})
it('returns false for folder-mode main worktrees (empty branch)', () => {
const folder = makeWorktree('folder')
folder.isMainWorktree = true
folder.branch = ''
expect(isDefaultBranchWorkspace(folder)).toBe(false)
})
it('returns false for non-main worktrees even on the default branch', () => {
const feature = makeWorktree('feature')
expect(isDefaultBranchWorkspace(feature)).toBe(false)
})
})
describe('sidebarHasActiveFilters', () => {
it('returns false when no filters are active', () => {
expect(
sidebarHasActiveFilters({
showActiveOnly: false,
filterRepoIds: [],
hideDefaultBranchWorkspace: false
})
).toBe(false)
})
it('returns true when only hideDefaultBranchWorkspace is active', () => {
// Why: regression guard for the empty-sidebar escape hatch. If hide is
// omitted from the filter union, a user whose only worktree is the
// default-branch row sees "No worktrees found" with no way back.
expect(
sidebarHasActiveFilters({
showActiveOnly: false,
filterRepoIds: [],
hideDefaultBranchWorkspace: true
})
).toBe(true)
})
it('returns true when only showActiveOnly is active', () => {
expect(
sidebarHasActiveFilters({
showActiveOnly: true,
filterRepoIds: [],
hideDefaultBranchWorkspace: false
})
).toBe(true)
})
it('returns true when only filterRepoIds is non-empty', () => {
expect(
sidebarHasActiveFilters({
showActiveOnly: false,
filterRepoIds: ['repo1'],
hideDefaultBranchWorkspace: false
})
).toBe(true)
})
})
describe('computeClearFilterActions', () => {
it('returns no-op actions when nothing is set', () => {
expect(
computeClearFilterActions({
showActiveOnly: false,
filterRepoIds: [],
hideDefaultBranchWorkspace: false
})
).toEqual({
resetShowActiveOnly: false,
resetFilterRepoIds: false,
resetHideDefaultBranchWorkspace: false
})
})
it('flags only hideDefaultBranchWorkspace for reset when it is the sole filter', () => {
// Why: verifies the empty-sidebar escape hatch actually clears the hide
// flag. A regression here would leave users stuck on "No worktrees found"
// because the only active filter would never clear.
expect(
computeClearFilterActions({
showActiveOnly: false,
filterRepoIds: [],
hideDefaultBranchWorkspace: true
})
).toEqual({
resetShowActiveOnly: false,
resetFilterRepoIds: false,
resetHideDefaultBranchWorkspace: true
})
})
it('does not flag hideDefaultBranchWorkspace when it is already off', () => {
// Why: avoids issuing a pointless IPC write on every Clear Filters click
// in the common case where hide was never on.
const actions = computeClearFilterActions({
showActiveOnly: true,
filterRepoIds: ['repo1'],
hideDefaultBranchWorkspace: false
})
expect(actions.resetHideDefaultBranchWorkspace).toBe(false)
expect(actions.resetShowActiveOnly).toBe(true)
expect(actions.resetFilterRepoIds).toBe(true)
})
it('flags every active filter simultaneously', () => {
expect(
computeClearFilterActions({
showActiveOnly: true,
filterRepoIds: ['repo1', 'repo2'],
hideDefaultBranchWorkspace: true
})
).toEqual({
resetShowActiveOnly: true,
resetFilterRepoIds: true,
resetHideDefaultBranchWorkspace: true
})
})
})

View File

@ -3,6 +3,65 @@ import { buildWorktreeComparator, sortWorktreesSmart } from './smart-sort'
import { useAppStore } from '@/store'
import { getAllWorktreesFromState, getRepoMapFromState } from '@/store/selectors'
/**
* Whether a worktree represents the repo's default-branch row that the
* "Hide Default Branch Workspace" setting targets. Folder-mode projects are
* main worktrees with branch === '' and are intentionally preserved.
*
* Why a shared helper: this predicate gates visibility in both the sidebar
* pipeline (computeVisibleWorktreeIds) and the Cmd+J jump palette. Keeping
* the definition in one place prevents the two surfaces from drifting.
*/
export function isDefaultBranchWorkspace(worktree: Worktree): boolean {
return worktree.isMainWorktree && worktree.branch.trim() !== ''
}
/** Inputs describing every sidebar filter that can leave the list empty. */
export type SidebarFilterState = {
showActiveOnly: boolean
filterRepoIds: readonly string[]
hideDefaultBranchWorkspace: boolean
}
/**
* Whether at least one sidebar filter is active drives the "Clear Filters"
* escape hatch in the empty-state message. Kept pure so it can be unit-tested
* alongside the sorting pipeline.
*
* Why include hideDefaultBranchWorkspace here: without it, a user whose only
* worktree is the default-branch row and who toggles hide-on would see the
* "No worktrees found" message with no in-sidebar recovery path.
*/
export function sidebarHasActiveFilters(state: SidebarFilterState): boolean {
return state.showActiveOnly || state.filterRepoIds.length > 0 || state.hideDefaultBranchWorkspace
}
/** Describes which mutators the Clear Filters button must invoke, separated
* from the mutators themselves so the decision logic is testable. */
export type ClearFilterActions = {
resetShowActiveOnly: boolean
resetFilterRepoIds: boolean
resetHideDefaultBranchWorkspace: boolean
}
/**
* Determines which sidebar filters the Clear Filters button needs to reset.
* Returning an explicit action plan (rather than just calling the setters)
* keeps the pure decision separate from the impure mutations, so tests can
* verify the logic without mounting the component.
*
* Why reset only the ones that are set: keeps Clear Filters from churning
* UI state (and the debounced ui.set write-back) on every click when the
* flag was already off.
*/
export function computeClearFilterActions(state: SidebarFilterState): ClearFilterActions {
return {
resetShowActiveOnly: state.showActiveOnly,
resetFilterRepoIds: state.filterRepoIds.length > 0,
resetHideDefaultBranchWorkspace: state.hideDefaultBranchWorkspace
}
}
/**
* Shared pure utility that computes the ordered list of visible (non-archived,
* non-filtered) worktree IDs. Both the App-level Cmd+19 handler and
@ -22,6 +81,11 @@ export function computeVisibleWorktreeIds(
tabsByWorktree: Record<string, TerminalTab[]> | null
browserTabsByWorktree?: Record<string, { id: string }[]> | null
activeWorktreeId?: string | null
// Why required: every caller (WorktreeList, getVisibleWorktreeIds
// fallback, tests) reads the flag from the UI store. Making the field
// required prevents a future caller from silently dropping the filter by
// forgetting to pass it.
hideDefaultBranchWorkspace: boolean
repoMap: Map<string, Repo>
}
): string[] {
@ -30,6 +94,10 @@ export function computeVisibleWorktreeIds(
// Filter archived
all = all.filter((w) => !w.isArchived)
if (opts.hideDefaultBranchWorkspace) {
all = all.filter((w) => !isDefaultBranchWorkspace(w))
}
// Filter by repo
if (opts.filterRepoIds.length > 0) {
const selectedRepoIds = new Set(opts.filterRepoIds)
@ -141,6 +209,7 @@ export function getVisibleWorktreeIds(): string[] {
tabsByWorktree: state.tabsByWorktree,
browserTabsByWorktree: state.browserTabsByWorktree,
activeWorktreeId: state.activeWorktreeId,
hideDefaultBranchWorkspace: state.hideDefaultBranchWorkspace,
repoMap
})
}

View File

@ -97,6 +97,18 @@ describe('createUISlice hydratePersistedUI', () => {
expect(store.getState().showActiveOnly).toBe(true)
})
it('restores the hide-default-branch filter from persisted UI state', () => {
const store = createUIStore()
store.getState().hydratePersistedUI(
makePersistedUI({
hideDefaultBranchWorkspace: true
})
)
expect(store.getState().hideDefaultBranchWorkspace).toBe(true)
})
})
describe('createUISlice settings navigation', () => {

View File

@ -183,6 +183,8 @@ export type UISlice = {
setSortBy: (s: UISlice['sortBy']) => void
showActiveOnly: boolean
setShowActiveOnly: (v: boolean) => void
hideDefaultBranchWorkspace: boolean
setHideDefaultBranchWorkspace: (v: boolean) => void
filterRepoIds: string[]
setFilterRepoIds: (ids: string[]) => void
collapsedGroups: Set<string>
@ -447,6 +449,9 @@ export const createUISlice: StateCreator<AppState, [], [], UISlice> = (set, get)
showActiveOnly: false,
setShowActiveOnly: (v) => set({ showActiveOnly: v }),
hideDefaultBranchWorkspace: false,
setHideDefaultBranchWorkspace: (v) => set({ hideDefaultBranchWorkspace: v }),
filterRepoIds: [],
setFilterRepoIds: (ids) => set({ filterRepoIds: ids }),
@ -581,6 +586,7 @@ export const createUISlice: StateCreator<AppState, [], [], UISlice> = (set, get)
// transient render detail. Restoring it on launch keeps the filtered
// worktree list stable across restarts instead of silently widening it.
showActiveOnly: ui.showActiveOnly,
hideDefaultBranchWorkspace: ui.hideDefaultBranchWorkspace ?? false,
filterRepoIds: (ui.filterRepoIds ?? []).filter((repoId) => validRepoIds.has(repoId)),
collapsedGroups: new Set(ui.collapsedGroups ?? []),
uiZoomLevel: ui.uiZoomLevel ?? 0,

View File

@ -239,6 +239,7 @@ export function getDefaultUIState(): PersistedUIState {
groupBy: 'none',
sortBy: 'recent',
showActiveOnly: false,
hideDefaultBranchWorkspace: false,
filterRepoIds: [],
collapsedGroups: [],
uiZoomLevel: 0,

View File

@ -1231,6 +1231,12 @@ export type PersistedUIState = {
groupBy: 'none' | 'repo' | 'pr-status'
sortBy: 'name' | 'smart' | 'recent' | 'repo'
showActiveOnly: boolean
/** Hide the repo's original checked-out branch from workspace navigation
* (sidebar and Cmd+J jump palette). Folder-mode repos are unaffected
* the predicate in visible-worktrees.ts excludes worktrees with an empty
* branch. Lives alongside showActiveOnly because both are user-facing
* sidebar filters reached through the same dropdown. */
hideDefaultBranchWorkspace: boolean
filterRepoIds: string[]
collapsedGroups: string[]
uiZoomLevel: number