diff --git a/src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.stale-while-revalidate.test.tsx b/src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.stale-while-revalidate.test.tsx index a50040f93e0..5e1ef3b9212 100644 --- a/src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.stale-while-revalidate.test.tsx +++ b/src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.stale-while-revalidate.test.tsx @@ -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( + '[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') + }) }) diff --git a/src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.tsx b/src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.tsx index 61fe04b9b1e..ecc72781cab 100644 --- a/src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.tsx +++ b/src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.tsx @@ -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 } } diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-scan-notice.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-scan-notice.ts index eb9748e3758..5b9de759572 100644 --- a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-scan-notice.ts +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-scan-notice.ts @@ -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.') diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-sort-header.test.tsx b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-sort-header.test.tsx index 5f7a732db7e..151df522781 100644 --- a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-sort-header.test.tsx +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-sort-header.test.tsx @@ -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 } diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-sort-header.tsx b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-sort-header.tsx index 9940684861d..3a3e78429c3 100644 --- a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-sort-header.tsx +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-sort-header.tsx @@ -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 (
- - {/* 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 } - )} - + {selectAllLabel}