Resolve workspace cleanup active view during render

This commit is contained in:
Neil
2026-05-30 15:12:40 -04:00
committed by GitHub
parent 79e4e64257
commit de1cb48db0
3 changed files with 163 additions and 48 deletions
@@ -38,6 +38,11 @@ import {
type WorkspaceCleanupScanError,
type WorkspaceCleanupTier
} from '../../../../shared/workspace-cleanup'
import {
resolveWorkspaceCleanupActiveView,
type WorkspaceCleanupView,
type WorkspaceCleanupViewCounts
} from './workspace-cleanup-view-selection'
const TIER_LABELS: Record<WorkspaceCleanupTier, string> = {
ready: 'Suggested cleanup',
@@ -45,8 +50,6 @@ const TIER_LABELS: Record<WorkspaceCleanupTier, string> = {
protected: 'Not suggested for cleanup'
}
type CleanupView = WorkspaceCleanupTier | 'hidden'
const BLOCKER_LABELS: Record<WorkspaceCleanupBlocker, string> = {
'main-worktree': 'Main workspace',
'folder-repo': 'Folder project',
@@ -185,7 +188,7 @@ export default function WorkspaceCleanupDialog(): React.JSX.Element {
const open = activeModal === 'workspace-cleanup'
const [selectedIds, setSelectedIds] = useState<Set<string>>(() => new Set())
const [activeView, setActiveView] = useState<CleanupView>('ready')
const [activeView, setActiveView] = useState<WorkspaceCleanupView>('ready')
const [confirming, setConfirming] = useState(false)
const [removing, setRemoving] = useState(false)
const [rowFailures, setRowFailures] = useState<Record<string, string>>({})
@@ -278,6 +281,22 @@ export default function WorkspaceCleanupDialog(): React.JSX.Element {
const hiddenByKeepCount = filteredCandidates.filter((candidate) =>
candidate.blockers.includes('dismissed')
).length
const cleanupViewCounts = useMemo<WorkspaceCleanupViewCounts>(
() => ({
ready: groups.ready.length,
review: groups.review.length,
protected: groups.protected.length,
hidden: hiddenCandidates.length
}),
[groups.protected.length, groups.ready.length, groups.review.length, hiddenCandidates.length]
)
const resolvedActiveView = resolveWorkspaceCleanupActiveView({
requestedView: activeView,
counts: cleanupViewCounts,
open,
loading,
hasScan: scan != null
})
const repoNameById = useMemo(
() => new Map(repos.map((repo) => [repo.id, repo.displayName || repo.path])),
[repos]
@@ -295,7 +314,7 @@ export default function WorkspaceCleanupDialog(): React.JSX.Element {
const inactiveCount = filteredCandidates.length
const hasAnyCandidates = candidates.length > 0
const initialLoading = loading && !scan
const activeRows = activeView === 'hidden' ? hiddenCandidates : groups[activeView]
const activeRows = resolvedActiveView === 'hidden' ? hiddenCandidates : groups[resolvedActiveView]
const activeQueueableRows = useMemo(
() => activeRows.filter(canQueueWorkspaceCleanupCandidate),
[activeRows]
@@ -313,33 +332,6 @@ export default function WorkspaceCleanupDialog(): React.JSX.Element {
? 'mixed'
: 'unchecked'
useEffect(() => {
if (!open || loading || !scan) {
return
}
if (activeRows.length > 0) {
return
}
if (readyCount > 0) {
setActiveView('ready')
} else if (groups.review.length > 0) {
setActiveView('review')
} else if (groups.protected.length > 0) {
setActiveView('protected')
} else if (hiddenCandidates.length > 0) {
setActiveView('hidden')
}
}, [
activeRows.length,
groups.protected.length,
groups.review.length,
hiddenCandidates.length,
loading,
open,
readyCount,
scan
])
const handleOpenChange = useCallback(
(nextOpen: boolean) => {
if (!nextOpen && !removing) {
@@ -567,19 +559,14 @@ export default function WorkspaceCleanupDialog(): React.JSX.Element {
<div className="grid min-h-0 flex-1 grid-cols-1 overflow-hidden md:grid-cols-[185px_minmax(0,1fr)]">
<CleanupViewNav
activeView={activeView}
counts={{
ready: groups.ready.length,
review: groups.review.length,
protected: groups.protected.length,
hidden: hiddenByKeepCount
}}
activeView={resolvedActiveView}
counts={cleanupViewCounts}
onViewChange={setActiveView}
/>
<div className="flex min-h-0 min-w-0 flex-col border-t border-border md:border-l md:border-t-0">
<div className="flex min-h-10 items-center justify-between gap-3 border-b border-border px-3 py-2">
<div className="flex min-w-0 items-center gap-2">
{activeView !== 'hidden' && activeQueueableRows.length > 0 ? (
{resolvedActiveView !== 'hidden' && activeQueueableRows.length > 0 ? (
<button
type="button"
role="checkbox"
@@ -588,8 +575,8 @@ export default function WorkspaceCleanupDialog(): React.JSX.Element {
}
aria-label={
allActiveQueueableSelected
? `Unselect all in ${TIER_LABELS[activeView]}`
: `Select all in ${TIER_LABELS[activeView]}`
? `Unselect all in ${TIER_LABELS[resolvedActiveView]}`
: `Select all in ${TIER_LABELS[resolvedActiveView]}`
}
onClick={toggleActiveSelection}
className="flex size-4 shrink-0 items-center justify-center rounded border border-border bg-background text-primary hover:bg-accent"
@@ -602,12 +589,12 @@ export default function WorkspaceCleanupDialog(): React.JSX.Element {
</button>
) : null}
<div className="min-w-0 truncate text-[11px] font-semibold uppercase tracking-[0.05em] text-muted-foreground">
{activeView === 'hidden'
{resolvedActiveView === 'hidden'
? 'Ignored cleanup suggestions'
: TIER_LABELS[activeView]}
: TIER_LABELS[resolvedActiveView]}
</div>
</div>
{activeView === 'hidden' && hiddenByKeepCount > 0 ? (
{resolvedActiveView === 'hidden' && hiddenByKeepCount > 0 ? (
<Button
variant="link"
size="xs"
@@ -723,11 +710,11 @@ function CleanupViewNav({
counts,
onViewChange
}: {
activeView: CleanupView
counts: Record<CleanupView, number>
onViewChange: (view: CleanupView) => void
activeView: WorkspaceCleanupView
counts: WorkspaceCleanupViewCounts
onViewChange: (view: WorkspaceCleanupView) => void
}): React.JSX.Element {
const items: { view: CleanupView; label: string }[] = [
const items: { view: WorkspaceCleanupView; label: string }[] = [
{ view: 'ready', label: 'Suggested' },
{ view: 'review', label: 'Needs review' },
{ view: 'protected', label: 'Not suggested' },
@@ -0,0 +1,94 @@
import { describe, expect, it } from 'vitest'
import {
resolveWorkspaceCleanupActiveView,
type WorkspaceCleanupViewCounts
} from './workspace-cleanup-view-selection'
const emptyCounts: WorkspaceCleanupViewCounts = {
ready: 0,
review: 0,
protected: 0,
hidden: 0
}
describe('resolveWorkspaceCleanupActiveView', () => {
it('keeps the requested view while it still has rows', () => {
expect(
resolveWorkspaceCleanupActiveView({
requestedView: 'review',
counts: { ...emptyCounts, ready: 2, review: 1 },
open: true,
loading: false,
hasScan: true
})
).toBe('review')
})
it('falls back to the first populated cleanup view when the requested view is empty', () => {
expect(
resolveWorkspaceCleanupActiveView({
requestedView: 'protected',
counts: { ...emptyCounts, review: 3, hidden: 1 },
open: true,
loading: false,
hasScan: true
})
).toBe('review')
})
it('uses hidden suggestions when no visible cleanup views have rows', () => {
expect(
resolveWorkspaceCleanupActiveView({
requestedView: 'ready',
counts: { ...emptyCounts, hidden: 2 },
open: true,
loading: false,
hasScan: true
})
).toBe('hidden')
})
it('leaves the requested view alone before an open completed scan', () => {
expect(
resolveWorkspaceCleanupActiveView({
requestedView: 'protected',
counts: { ...emptyCounts, ready: 4 },
open: false,
loading: false,
hasScan: true
})
).toBe('protected')
expect(
resolveWorkspaceCleanupActiveView({
requestedView: 'protected',
counts: { ...emptyCounts, ready: 4 },
open: true,
loading: true,
hasScan: true
})
).toBe('protected')
expect(
resolveWorkspaceCleanupActiveView({
requestedView: 'protected',
counts: { ...emptyCounts, ready: 4 },
open: true,
loading: false,
hasScan: false
})
).toBe('protected')
})
it('keeps an empty requested view when every view is empty', () => {
expect(
resolveWorkspaceCleanupActiveView({
requestedView: 'hidden',
counts: emptyCounts,
open: true,
loading: false,
hasScan: true
})
).toBe('hidden')
})
})
@@ -0,0 +1,34 @@
export type WorkspaceCleanupView = 'ready' | 'review' | 'protected' | 'hidden'
export type WorkspaceCleanupViewCounts = Record<WorkspaceCleanupView, number>
export function resolveWorkspaceCleanupActiveView({
requestedView,
counts,
open,
loading,
hasScan
}: {
requestedView: WorkspaceCleanupView
counts: WorkspaceCleanupViewCounts
open: boolean
loading: boolean
hasScan: boolean
}): WorkspaceCleanupView {
if (!open || loading || !hasScan || counts[requestedView] > 0) {
return requestedView
}
if (counts.ready > 0) {
return 'ready'
}
if (counts.review > 0) {
return 'review'
}
if (counts.protected > 0) {
return 'protected'
}
if (counts.hidden > 0) {
return 'hidden'
}
return requestedView
}