From 4de70b0add18d27f14cc69155ab250c99bbf8548 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 4 Aug 2026 00:35:45 -0700 Subject: [PATCH] Show the "Except default branch" filter only when "Hide sleeping" is on The exemption only takes effect during the hide-sleeping sweep, so its row now renders (and counts toward the filter badge) only while its parent toggle is ticked. Co-authored-by: Orca --- .../src/components/sidebar/SidebarFilter.tsx | 45 ++++++---- .../SidebarWorkspaceFilterSection.test.tsx | 86 +++++++++++++++++++ .../sidebar/SidebarWorkspaceFilterSection.tsx | 32 ++++--- .../sidebar/SidebarWorkspaceOptionsMenu.tsx | 11 ++- .../sidebar/sidebar-filter-state.test.ts | 16 ++++ .../components/sidebar/visible-worktrees.ts | 12 +++ 6 files changed, 168 insertions(+), 34 deletions(-) create mode 100644 src/renderer/src/components/sidebar/SidebarWorkspaceFilterSection.test.tsx 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' }