diff --git a/src/renderer/src/components/sidebar/SidebarFilter.tsx b/src/renderer/src/components/sidebar/SidebarFilter.tsx
index 1efbf6d0016..7c4836e698d 100644
--- a/src/renderer/src/components/sidebar/SidebarFilter.tsx
+++ b/src/renderer/src/components/sidebar/SidebarFilter.tsx
@@ -31,6 +31,7 @@ import { FilterToggleRow } from './FilterToggleRow'
import { useShortcutLabel } from '@/hooks/useShortcutLabel'
import { searchRepos } from '@/lib/repo-search'
import { DEFAULT_SHOW_SLEEPING_WORKSPACES } from '../../../../shared/constants'
+import { isSleepingSweepExemptionNarrowingList } from './visible-worktrees'
import { translate } from '@/i18n/i18n'
type SidebarFilterProps = {
@@ -111,15 +112,19 @@ const SidebarFilter = React.memo(function SidebarFilter({
const selectedCount = selectedRepoIdSet.size
const hasRepoFilter = selectedCount > 0
const hasSleepingFilter = showSleepingWorkspaces !== DEFAULT_SHOW_SLEEPING_WORKSPACES
+ // Why counted: turning the exemption off is the only way that row narrows the
+ // list — but only while its parent row is on, which is also when it renders.
+ const hasSleepingExemptionFilter = isSleepingSweepExemptionNarrowingList(
+ showSleepingWorkspaces,
+ alwaysShowDefaultBranchWorkspace
+ )
const hasAnyFilter =
hasSleepingFilter ||
hideDefaultBranchWorkspace ||
hideAutomationGeneratedWorkspaces ||
hideCliCreatedWorkspaces ||
hideDetachedHeadWorkspaces ||
- // Why counted: turning the exemption off is the only way this row narrows
- // the list, so the badge and Reset filters must both notice it.
- !alwaysShowDefaultBranchWorkspace ||
+ hasSleepingExemptionFilter ||
hasRepoFilter
const activeFilterCount =
(hasSleepingFilter ? 1 : 0) +
@@ -127,7 +132,7 @@ const SidebarFilter = React.memo(function SidebarFilter({
(hideAutomationGeneratedWorkspaces ? 1 : 0) +
(hideCliCreatedWorkspaces ? 1 : 0) +
(hideDetachedHeadWorkspaces ? 1 : 0) +
- (alwaysShowDefaultBranchWorkspace ? 0 : 1) +
+ (hasSleepingExemptionFilter ? 1 : 0) +
selectedCount
const filteredRepos = useMemo(() => searchRepos(repos, query), [repos, query])
@@ -221,20 +226,24 @@ const SidebarFilter = React.memo(function SidebarFilter({
onChange={(hideSleeping) => setShowSleepingWorkspaces(!hideSleeping)}
shortcutLabel={sleepingShortcut === 'Unassigned' ? undefined : sleepingShortcut}
/>
- }
- label={translate(
- 'auto.components.sidebar.SidebarFilter.keepDefaultBranch',
- 'Except default branch'
- )}
- ariaLabel={translate(
- 'auto.components.sidebar.SidebarFilter.keepDefaultBranchAria',
- 'Keep the default branch visible while hiding sleeping workspaces'
- )}
- checked={alwaysShowDefaultBranchWorkspace}
- onChange={setAlwaysShowDefaultBranchWorkspace}
- />
+ {/* Why gated: the exemption only has an effect while sleeping workspaces
+ are being swept, so it stays hidden until its parent row is on. */}
+ {!showSleepingWorkspaces && (
+ }
+ label={translate(
+ 'auto.components.sidebar.SidebarFilter.keepDefaultBranch',
+ 'Except default branch'
+ )}
+ ariaLabel={translate(
+ 'auto.components.sidebar.SidebarFilter.keepDefaultBranchAria',
+ 'Keep the default branch visible while hiding sleeping workspaces'
+ )}
+ checked={alwaysShowDefaultBranchWorkspace}
+ onChange={setAlwaysShowDefaultBranchWorkspace}
+ />
+ )}
}
label={translate(
diff --git a/src/renderer/src/components/sidebar/SidebarWorkspaceFilterSection.test.tsx b/src/renderer/src/components/sidebar/SidebarWorkspaceFilterSection.test.tsx
new file mode 100644
index 00000000000..a98d881b09f
--- /dev/null
+++ b/src/renderer/src/components/sidebar/SidebarWorkspaceFilterSection.test.tsx
@@ -0,0 +1,86 @@
+// @vitest-environment happy-dom
+
+/**
+ * The "Except default branch" exemption only bites during the "Hide sleeping"
+ * sweep, so its row must stay out of the filter list until the parent row is on.
+ */
+
+import { act } from 'react'
+import { createRoot, type Root } from 'react-dom/client'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+
+const mocks = vi.hoisted(() => ({
+ state: {} as Record
+}))
+
+vi.mock('@/store', () => ({
+ useAppStore: (selector: (state: Record) => unknown) => selector(mocks.state)
+}))
+
+import SidebarWorkspaceFilterSection from './SidebarWorkspaceFilterSection'
+
+const EXEMPTION_LABEL = 'Except default branch'
+
+function setState(overrides: Record = {}): void {
+ mocks.state = {
+ showSleepingWorkspaces: true,
+ setShowSleepingWorkspaces: vi.fn(),
+ hideDefaultBranchWorkspace: false,
+ setHideDefaultBranchWorkspace: vi.fn(),
+ hideAutomationGeneratedWorkspaces: false,
+ setHideAutomationGeneratedWorkspaces: vi.fn(),
+ hideCliCreatedWorkspaces: false,
+ setHideCliCreatedWorkspaces: vi.fn(),
+ hideDetachedHeadWorkspaces: false,
+ setHideDetachedHeadWorkspaces: vi.fn(),
+ alwaysShowDefaultBranchWorkspace: true,
+ setAlwaysShowDefaultBranchWorkspace: vi.fn(),
+ ...overrides
+ }
+}
+
+let container: HTMLDivElement
+let root: Root
+
+function render(): void {
+ act(() => {
+ root.render()
+ })
+}
+
+function rowLabels(): string[] {
+ return Array.from(container.querySelectorAll('[role="switch"]')).map(
+ (el) => el.textContent?.trim() ?? ''
+ )
+}
+
+beforeEach(() => {
+ container = document.createElement('div')
+ document.body.appendChild(container)
+ root = createRoot(container)
+})
+
+afterEach(() => {
+ act(() => root.unmount())
+ container.remove()
+})
+
+describe('SidebarWorkspaceFilterSection', () => {
+ it('hides the default-branch exemption row while sleeping workspaces are shown', () => {
+ setState({ showSleepingWorkspaces: true })
+ render()
+ expect(rowLabels()).not.toContain(EXEMPTION_LABEL)
+ })
+
+ it('shows the exemption row once "Hide sleeping" is ticked', () => {
+ setState({ showSleepingWorkspaces: false })
+ render()
+ expect(rowLabels()).toContain(EXEMPTION_LABEL)
+ })
+
+ it('keeps the exemption row hidden even when it is switched off', () => {
+ setState({ showSleepingWorkspaces: true, alwaysShowDefaultBranchWorkspace: false })
+ render()
+ expect(rowLabels()).not.toContain(EXEMPTION_LABEL)
+ })
+})
diff --git a/src/renderer/src/components/sidebar/SidebarWorkspaceFilterSection.tsx b/src/renderer/src/components/sidebar/SidebarWorkspaceFilterSection.tsx
index e06f81420fb..216009ae8b7 100644
--- a/src/renderer/src/components/sidebar/SidebarWorkspaceFilterSection.tsx
+++ b/src/renderer/src/components/sidebar/SidebarWorkspaceFilterSection.tsx
@@ -38,20 +38,24 @@ const SidebarWorkspaceFilterSection = React.memo(function SidebarWorkspaceFilter
checked={!showSleepingWorkspaces}
onChange={(hideSleeping) => setShowSleepingWorkspaces(!hideSleeping)}
/>
- }
- label={translate(
- 'auto.components.sidebar.SidebarWorkspaceFilterSection.keepDefaultBranch',
- 'Except default branch'
- )}
- ariaLabel={translate(
- 'auto.components.sidebar.SidebarWorkspaceFilterSection.keepDefaultBranchAria',
- 'Keep the default branch visible while hiding sleeping workspaces'
- )}
- checked={alwaysShowDefaultBranchWorkspace}
- onChange={setAlwaysShowDefaultBranchWorkspace}
- />
+ {/* Why gated: the exemption only has an effect while sleeping workspaces
+ are being swept, so it stays hidden until its parent row is on. */}
+ {!showSleepingWorkspaces && (
+ }
+ label={translate(
+ 'auto.components.sidebar.SidebarWorkspaceFilterSection.keepDefaultBranch',
+ 'Except default branch'
+ )}
+ ariaLabel={translate(
+ 'auto.components.sidebar.SidebarWorkspaceFilterSection.keepDefaultBranchAria',
+ 'Keep the default branch visible while hiding sleeping workspaces'
+ )}
+ checked={alwaysShowDefaultBranchWorkspace}
+ onChange={setAlwaysShowDefaultBranchWorkspace}
+ />
+ )}
}
label={translate(
diff --git a/src/renderer/src/components/sidebar/SidebarWorkspaceOptionsMenu.tsx b/src/renderer/src/components/sidebar/SidebarWorkspaceOptionsMenu.tsx
index 5f3f65dcea6..9ea6a9841ef 100644
--- a/src/renderer/src/components/sidebar/SidebarWorkspaceOptionsMenu.tsx
+++ b/src/renderer/src/components/sidebar/SidebarWorkspaceOptionsMenu.tsx
@@ -16,6 +16,7 @@ import {
} from '@/components/ui/dropdown-menu'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { DEFAULT_SHOW_SLEEPING_WORKSPACES } from '../../../../shared/constants'
+import { isSleepingSweepExemptionNarrowingList } from './visible-worktrees'
import SidebarRepositoryFilterSection from './SidebarRepositoryFilterSection'
import SidebarWorkspaceFilterSection from './SidebarWorkspaceFilterSection'
import { getSidebarHostVisibilityLabel, shouldShowHostScopeControls } from './sidebar-host-options'
@@ -79,13 +80,19 @@ const SidebarWorkspaceOptionsMenu = React.memo(function SidebarWorkspaceOptionsM
const hasRepoFilter = selectedCount > 0
const hasSleepingFilter = showSleepingWorkspaces !== DEFAULT_SHOW_SLEEPING_WORKSPACES
const hasHostVisibilityFilter = visibleWorkspaceHostIds !== null
+ // Why gated on the parent row: the exemption only narrows the list during the
+ // "Hide sleeping" sweep, which is also the only time its row is rendered.
+ const hasSleepingExemptionFilter = isSleepingSweepExemptionNarrowingList(
+ showSleepingWorkspaces,
+ alwaysShowDefaultBranchWorkspace
+ )
const hasAnyFilter =
hasSleepingFilter ||
hideDefaultBranchWorkspace ||
hideAutomationGeneratedWorkspaces ||
hideCliCreatedWorkspaces ||
hideDetachedHeadWorkspaces ||
- !alwaysShowDefaultBranchWorkspace ||
+ hasSleepingExemptionFilter ||
hasRepoFilter ||
hasHostVisibilityFilter
const activeFilterCount =
@@ -94,7 +101,7 @@ const SidebarWorkspaceOptionsMenu = React.memo(function SidebarWorkspaceOptionsM
(hideAutomationGeneratedWorkspaces ? 1 : 0) +
(hideCliCreatedWorkspaces ? 1 : 0) +
(hideDetachedHeadWorkspaces ? 1 : 0) +
- (alwaysShowDefaultBranchWorkspace ? 0 : 1) +
+ (hasSleepingExemptionFilter ? 1 : 0) +
(hasHostVisibilityFilter ? 1 : 0) +
selectedCount
const activeFilterLabel = `${activeFilterCount} ${activeFilterCount === 1 ? 'filter' : 'filters'}`
diff --git a/src/renderer/src/components/sidebar/sidebar-filter-state.test.ts b/src/renderer/src/components/sidebar/sidebar-filter-state.test.ts
index 317f0e6204e..7fe5d255110 100644
--- a/src/renderer/src/components/sidebar/sidebar-filter-state.test.ts
+++ b/src/renderer/src/components/sidebar/sidebar-filter-state.test.ts
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import {
computeClearFilterActions,
isDefaultBranchWorkspace,
+ isSleepingSweepExemptionNarrowingList,
sidebarHasActiveFilters
} from './visible-worktrees'
import type { Worktree } from '../../../../shared/types'
@@ -114,6 +115,21 @@ describe('sidebarHasActiveFilters', () => {
})
})
+describe('isSleepingSweepExemptionNarrowingList', () => {
+ it('is false while sleeping workspaces are shown, even when opted out', () => {
+ expect(isSleepingSweepExemptionNarrowingList(true, false)).toBe(false)
+ })
+
+ it('is true only when the sweep is on and the exemption is opted out', () => {
+ expect(isSleepingSweepExemptionNarrowingList(false, false)).toBe(true)
+ expect(isSleepingSweepExemptionNarrowingList(false, true)).toBe(false)
+ })
+
+ it('treats a missing flag as the on default', () => {
+ expect(isSleepingSweepExemptionNarrowingList(false, undefined)).toBe(false)
+ })
+})
+
describe('computeClearFilterActions', () => {
it('returns no-op actions when nothing is set', () => {
expect(computeClearFilterActions(filterState())).toEqual({
diff --git a/src/renderer/src/components/sidebar/visible-worktrees.ts b/src/renderer/src/components/sidebar/visible-worktrees.ts
index 10aa72bcf1f..38ffcd8c6d5 100644
--- a/src/renderer/src/components/sidebar/visible-worktrees.ts
+++ b/src/renderer/src/components/sidebar/visible-worktrees.ts
@@ -54,6 +54,18 @@ export function isSleepingSweepExemptWorkspace(
return alwaysShowDefaultBranchWorkspace !== false && worktree.isMainWorktree
}
+/**
+ * Whether turning the exemption off is currently narrowing the list. It only
+ * bites during the "Hide sleeping" sweep, so its row — and the filter badge on
+ * both menu surfaces — ignores it while sleeping workspaces are shown.
+ */
+export function isSleepingSweepExemptionNarrowingList(
+ showSleepingWorkspaces: boolean,
+ alwaysShowDefaultBranchWorkspace: boolean | undefined
+): boolean {
+ return !showSleepingWorkspaces && alwaysShowDefaultBranchWorkspace === false
+}
+
export function isAutomationGeneratedWorkspace(worktree: Worktree): boolean {
return worktree.automationProvenance?.kind === 'created-by-automation'
}