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 <help@stably.ai>
This commit is contained in:
Neil
2026-08-04 00:35:45 -07:00
co-authored by Orca
parent c498d2d405
commit 4de70b0add
6 changed files with 168 additions and 34 deletions
@@ -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}
/>
<FilterToggleRow
indented
icon={<GitBranch className="size-3.5" />}
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 && (
<FilterToggleRow
indented
icon={<GitBranch className="size-3.5" />}
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}
/>
)}
<FilterToggleRow
icon={<GitBranch className="size-3.5" />}
label={translate(
@@ -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<string, unknown>
}))
vi.mock('@/store', () => ({
useAppStore: (selector: (state: Record<string, unknown>) => unknown) => selector(mocks.state)
}))
import SidebarWorkspaceFilterSection from './SidebarWorkspaceFilterSection'
const EXEMPTION_LABEL = 'Except default branch'
function setState(overrides: Record<string, unknown> = {}): 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(<SidebarWorkspaceFilterSection />)
})
}
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)
})
})
@@ -38,20 +38,24 @@ const SidebarWorkspaceFilterSection = React.memo(function SidebarWorkspaceFilter
checked={!showSleepingWorkspaces}
onChange={(hideSleeping) => setShowSleepingWorkspaces(!hideSleeping)}
/>
<FilterToggleRow
indented
icon={<GitBranch className="size-3.5" />}
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 && (
<FilterToggleRow
indented
icon={<GitBranch className="size-3.5" />}
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}
/>
)}
<FilterToggleRow
icon={<GitBranch className="size-3.5" />}
label={translate(
@@ -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'}`
@@ -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({
@@ -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'
}