fix(workspace-cleanup): preserve explicit selections

This commit is contained in:
Brennan Benson
2026-08-23 16:28:07 -07:00
parent cfbed6b794
commit 4ef734e730
8 changed files with 108 additions and 28 deletions
@@ -7,6 +7,7 @@ import type {
WorkspaceCleanupScanProgress,
WorkspaceCleanupScanResult
} from '../../../../shared/workspace-cleanup'
import type { WorkspaceCleanupBrowseState } from '../../../../shared/workspace-cleanup-browse-state'
import { createTestStore, seedStore } from '../../store/slices/store-test-helpers'
import { resetWorkspaceCleanupBrowsePersistTimer } from '../../store/slices/workspace-cleanup-browse'
import { NOW, makeCandidate } from '../../store/slices/workspace-cleanup-slice-test-harness'
@@ -349,6 +350,35 @@ describe('WorkspaceCleanupDialog stale-while-revalidate', () => {
expect(rowCheckbox('alpha')?.getAttribute('aria-checked')).toBe('true')
})
it('reports selections removed by facet filters', async () => {
installApi(cachedFleet())
await renderDialog()
await openDialog()
await act(async () => rowCheckbox('alpha')?.click())
const browse = holders.store.getState().workspaceCleanupBrowse as WorkspaceCleanupBrowseState
await act(async () => {
holders.store.setState({
workspaceCleanupBrowse: {
...browse,
filters: {
...browse.filters,
safety: { ...browse.filters.safety, tiers: ['review'] }
}
}
})
})
await flush()
expect(holders.infoToasts).toContain(
'1 selected workspace is hidden by the current filters and was deselected.'
)
await act(async () => holders.store.setState({ workspaceCleanupBrowse: browse }))
await flush()
expect(rowCheckbox('alpha')?.getAttribute('aria-checked')).toBe('false')
})
it('adopts the in-flight scan on reopen instead of starting a duplicate', async () => {
const rig = installApi(cachedFleet())
await renderDialog()
@@ -433,4 +463,37 @@ describe('WorkspaceCleanupDialog stale-while-revalidate', () => {
expect(rowNames()).not.toContain('beta')
expect(holders.infoToasts).toContain('1 selected workspace no longer exists.')
})
it('keeps selected review rows outside the select-all scope', async () => {
installApi({
scannedAt: CACHED_AT,
candidates: [
makeCandidate({ worktreeId: 'repo1::/tmp/ready', displayName: 'ready' }),
makeCandidate({
worktreeId: 'repo1::/tmp/review',
displayName: 'review',
tier: 'review',
selectedByDefault: false,
git: { clean: null, upstreamAhead: 0, upstreamBehind: 0, checkedAt: null }
})
],
errors: []
})
await renderDialog()
await openDialog()
await act(async () => rowCheckbox('review')?.click())
const selectAll = container?.querySelector<HTMLElement>(
'[aria-label="Select 1 safety-checked workspace"]'
)
expect(selectAll?.getAttribute('aria-checked')).toBe('false')
await act(async () => selectAll?.click())
expect(rowCheckbox('ready')?.getAttribute('aria-checked')).toBe('true')
expect(rowCheckbox('review')?.getAttribute('aria-checked')).toBe('true')
await act(async () => selectAll?.click())
expect(rowCheckbox('ready')?.getAttribute('aria-checked')).toBe('false')
expect(rowCheckbox('review')?.getAttribute('aria-checked')).toBe('true')
})
})
@@ -286,10 +286,9 @@ function WorkspaceCleanupDialogContent({
)
// Header state is scoped to the same rows the header action controls.
const selectedSelectableCount = useMemo(() => {
const selectable = new Set(selectableIdentities)
let count = 0
for (const id of selectedIds) {
if (selectable.has(id)) {
for (const identity of selectableIdentities) {
if (selectedIds.has(identity)) {
count += 1
}
}
@@ -64,10 +64,7 @@ export function formatWorkspaceCleanupScanProgress(
)
}
/**
* Why no suggestion count: "suggestion" is Orca's verdict about the user's own
* work. The dialog stopped acting on it; this toast must stop announcing it.
*/
/** Reports scan size without classifying the user's work. */
export function formatWorkspaceCleanupReadyToast(workspaceCount: number): string {
if (workspaceCount === 0) {
return translate('components.workspace.cleanup.scan.noWorkspaces', 'No workspaces found.')
@@ -95,16 +95,17 @@ describe('WorkspaceCleanupSortHeader', () => {
it('states how many workspaces select-all would take', () => {
render({ field: 'name', direction: 'asc' }, { selectableCount: 12 })
// Why: select-all takes the deletable subset, not every matched row; an
// unqualified label would overstate a destructive action's scope.
expect(container?.textContent).toContain('Select all 12 deletable workspaces')
expect(container?.textContent).toContain('Select all 12 safety-checked workspaces')
})
it('uses singular copy for one safety-checked workspace', () => {
render({ field: 'name', direction: 'asc' }, { selectableCount: 1 })
expect(container?.textContent).toContain('Select 1 safety-checked workspace')
})
it('does not report all-selected when the selected rows are not the selectable ones', () => {
const onToggleSelectAll = vi.fn()
// Why: `selectedCount` used to be canQueue-scoped while `selectableCount`
// is canSelect-scoped, so hand-picking review rows flipped the header to
// fully-checked and the next click cleared the whole selection.
render(
{ field: 'name', direction: 'asc' },
{ selectableCount: 3, selectedCount: 0, onToggleSelectAll }
@@ -31,6 +31,17 @@ export function WorkspaceCleanupSortHeader({
}): React.JSX.Element {
const allSelected = selectableCount > 0 && selectedCount >= selectableCount
const someSelected = selectableCount > 0 && selectedCount > 0 && !allSelected
const selectAllLabel =
selectableCount === 1
? translate(
'components.workspace.cleanup.browse.selectAllCountOne',
'Select 1 safety-checked workspace'
)
: translate(
'components.workspace.cleanup.browse.selectAllCount',
'Select all {{value0}} safety-checked workspaces',
{ value0: selectableCount }
)
return (
<div className="flex items-center gap-1 border-b border-border bg-muted/25 px-3 py-1.5">
<button
@@ -38,10 +49,7 @@ export function WorkspaceCleanupSortHeader({
role="checkbox"
aria-checked={someSelected ? 'mixed' : allSelected}
disabled={selectableCount === 0}
aria-label={translate(
'components.workspace.cleanup.browse.selectAll',
'Select all matching workspaces'
)}
aria-label={selectAllLabel}
onClick={() => onToggleSelectAll(!allSelected)}
className="flex size-4 shrink-0 items-center justify-center rounded border border-border bg-background text-primary hover:bg-accent disabled:opacity-40 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
@@ -51,16 +59,7 @@ export function WorkspaceCleanupSortHeader({
<Minus className="size-3" strokeWidth={3} />
) : null}
</button>
<span className="min-w-0 flex-1 text-xs text-muted-foreground">
{/* Why the count: select-all takes the deletable subset of the matched
rows, not every matched row — an unqualified label would overstate
the scope of a destructive action. */}
{translate(
'components.workspace.cleanup.browse.selectAllCount',
'Select all {{value0}} deletable workspaces',
{ value0: selectableCount }
)}
</span>
<span className="min-w-0 flex-1 text-xs text-muted-foreground">{selectAllLabel}</span>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button
+2 -1
View File
@@ -16229,7 +16229,8 @@
"selectedCount": "{{value0}} selected",
"selectionWithheldOne": "1 selected workspace is hidden by the current filters and was deselected.",
"selectionWithheld": "{{value0}} selected workspaces are hidden by the current filters and were deselected.",
"selectAllCount": "Select all {{value0}} deletable workspaces"
"selectAllCountOne": "Select 1 safety-checked workspace",
"selectAllCount": "Select all {{value0}} safety-checked workspaces"
},
"scan": {
"progress": "Scanning workspaces ({{value0}}/{{value1}})",
+10
View File
@@ -14546,6 +14546,16 @@
},
"workspace": {
"cleanup": {
"browse": {
"selectionWithheldOne": "선택한 워크스페이스 1개가 현재 필터에 의해 숨겨져 선택이 해제되었습니다.",
"selectionWithheld": "선택한 워크스페이스 {{value0}}개가 현재 필터에 의해 숨겨져 선택이 해제되었습니다.",
"selectAllCountOne": "안전 검사를 통과한 워크스페이스 1개 선택",
"selectAllCount": "안전 검사를 통과한 워크스페이스 {{value0}}개 모두 선택"
},
"scan": {
"readyOne": "워크스페이스 1개를 찾았습니다.",
"readyMany": "워크스페이스 {{value0}}개를 찾았습니다."
},
"presentationFixtures": {
"reviewAlphaCleanup": "알파 정리 검토"
},
+10
View File
@@ -14537,6 +14537,16 @@
},
"workspace": {
"cleanup": {
"browse": {
"selectionWithheldOne": "当前筛选条件隐藏了 1 个已选工作区,已取消选择。",
"selectionWithheld": "当前筛选条件隐藏了 {{value0}} 个已选工作区,已取消选择。",
"selectAllCountOne": "选择 1 个通过安全检查的工作区",
"selectAllCount": "选择全部 {{value0}} 个通过安全检查的工作区"
},
"scan": {
"readyOne": "找到 1 个工作区。",
"readyMany": "找到 {{value0}} 个工作区。"
},
"presentationFixtures": {
"reviewAlphaCleanup": "评审 Alpha 清理"
},