From 824f4dbdde11b90a216cb13276451bee60e40328 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 30 May 2026 12:19:33 -0700 Subject: [PATCH] Resolve Resource Manager Space scan ready state before commit (#3200) * Resolve resource space scan ready state before commit * Avoid render-time ref writes for space scan ready state Co-authored-by: Orca --------- Co-authored-by: Jinwoo-H Co-authored-by: Orca --- .../status-bar/ResourceUsageStatusSegment.tsx | 61 ++++---- .../resource-usage-space-scan-ready.test.ts | 148 ++++++++++++++++++ .../resource-usage-space-scan-ready.ts | 60 +++++++ 3 files changed, 236 insertions(+), 33 deletions(-) create mode 100644 src/renderer/src/components/status-bar/resource-usage-space-scan-ready.test.ts create mode 100644 src/renderer/src/components/status-bar/resource-usage-space-scan-ready.ts diff --git a/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx b/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx index c76302c9f98..deb95392ffa 100644 --- a/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx +++ b/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx @@ -61,6 +61,10 @@ import { getResourceUsageRuntimePaneTitlesByTabId, getResourceUsageTabsByWorktree } from './resource-usage-open-slices' +import { + resolveResourceUsageSpaceScanReady, + type ResourceUsageSpaceScanSnapshot +} from './resource-usage-space-scan-ready' const POLL_MS = 2_000 const SESSIONS_POLL_MS = 10_000 @@ -670,7 +674,13 @@ export function ResourceUsageStatusSegment({ const [sessionsError, setSessionsError] = useState(false) const [killConfirm, setKillConfirm] = useState(null) const [killing, setKilling] = useState(false) - const [spaceScanReady, setSpaceScanReady] = useState(false) + const [spaceScanSnapshot, setSpaceScanSnapshot] = useState( + () => ({ + ready: false, + previousScanning: workspaceSpaceScanning, + lastSeenScannedAt: workspaceSpaceScannedAt + }) + ) // Why: tab titles can update on terminal keystrokes. The resource popover's // merged tree needs them only while open, so closed status-bar badges should // not subscribe to those high-churn maps. @@ -684,8 +694,6 @@ export function ResourceUsageStatusSegment({ const tabsByWorktree = useAppStore((s) => getResourceUsageTabsByWorktree(s, open, runtimeEnvironmentActive) ) - const previousSpaceScanningRef = useRef(workspaceSpaceScanning) - const lastSeenSpaceScanAtRef = useRef(workspaceSpaceScannedAt) // Why: this segment only understands the local Electron PTY/resource daemon. // While a runtime server is active, hiding local samples avoids showing or // killing sessions from the wrong machine. @@ -754,37 +762,24 @@ export function ResourceUsageStatusSegment({ // Why: Space scans can finish after the user backs out of the full page or // closes this popover; the status-bar trigger becomes the handoff point. - useEffect(() => { - if (runtimeEnvironmentActive) { - setSpaceScanReady(false) - previousSpaceScanningRef.current = false - return - } - const scannedAt = workspaceSpaceScannedAt - const wasScanning = previousSpaceScanningRef.current - const scanCompleted = - wasScanning && - !workspaceSpaceScanning && - scannedAt !== null && - scannedAt !== lastSeenSpaceScanAtRef.current - - if (scanCompleted) { - lastSeenSpaceScanAtRef.current = scannedAt - setSpaceScanReady(!open && activeView !== 'space') - } else if (spaceScanReady && (open || activeView === 'space')) { - setSpaceScanReady(false) - lastSeenSpaceScanAtRef.current = scannedAt - } - - previousSpaceScanningRef.current = workspaceSpaceScanning - }, [ - activeView, - open, + const nextSpaceScanSnapshot = resolveResourceUsageSpaceScanReady({ + snapshot: spaceScanSnapshot, runtimeEnvironmentActive, - spaceScanReady, - workspaceSpaceScannedAt, - workspaceSpaceScanning - ]) + open, + activeView, + scannedAt: workspaceSpaceScannedAt, + scanning: workspaceSpaceScanning + }) + if ( + nextSpaceScanSnapshot.ready !== spaceScanSnapshot.ready || + nextSpaceScanSnapshot.previousScanning !== spaceScanSnapshot.previousScanning || + nextSpaceScanSnapshot.lastSeenScannedAt !== spaceScanSnapshot.lastSeenScannedAt + ) { + // Why: keep the scan transition render-time without mutating refs during + // render; React can safely retry this guarded state update before commit. + setSpaceScanSnapshot(nextSpaceScanSnapshot) + } + const spaceScanReady = nextSpaceScanSnapshot.ready // Poll memory + sessions when popover is open. Sessions also poll in the // background at a slower rate so the badge count stays reasonably fresh diff --git a/src/renderer/src/components/status-bar/resource-usage-space-scan-ready.test.ts b/src/renderer/src/components/status-bar/resource-usage-space-scan-ready.test.ts new file mode 100644 index 00000000000..447985dcc05 --- /dev/null +++ b/src/renderer/src/components/status-bar/resource-usage-space-scan-ready.test.ts @@ -0,0 +1,148 @@ +import { describe, expect, it } from 'vitest' +import { + resolveResourceUsageSpaceScanReady, + type ResourceUsageSpaceScanSnapshot +} from './resource-usage-space-scan-ready' + +const baseSnapshot: ResourceUsageSpaceScanSnapshot = { + ready: false, + previousScanning: false, + lastSeenScannedAt: null +} + +describe('resolveResourceUsageSpaceScanReady', () => { + it('marks Space results ready when a scan finishes away from the resource popover and Space page', () => { + expect( + resolveResourceUsageSpaceScanReady({ + snapshot: { ...baseSnapshot, previousScanning: true }, + runtimeEnvironmentActive: false, + open: false, + activeView: 'terminal', + scannedAt: 100, + scanning: false + }) + ).toEqual({ + ready: true, + previousScanning: false, + lastSeenScannedAt: 100 + }) + }) + + it('does not show the ready handoff when results finish while already visible', () => { + expect( + resolveResourceUsageSpaceScanReady({ + snapshot: { ...baseSnapshot, previousScanning: true }, + runtimeEnvironmentActive: false, + open: true, + activeView: 'terminal', + scannedAt: 100, + scanning: false + }) + ).toEqual({ + ready: false, + previousScanning: false, + lastSeenScannedAt: 100 + }) + + expect( + resolveResourceUsageSpaceScanReady({ + snapshot: { ...baseSnapshot, previousScanning: true }, + runtimeEnvironmentActive: false, + open: false, + activeView: 'space', + scannedAt: 100, + scanning: false + }).ready + ).toBe(false) + }) + + it('clears the handoff once the popover or Space page is opened', () => { + const readySnapshot = { + ready: true, + previousScanning: false, + lastSeenScannedAt: 100 + } + + expect( + resolveResourceUsageSpaceScanReady({ + snapshot: readySnapshot, + runtimeEnvironmentActive: false, + open: true, + activeView: 'terminal', + scannedAt: 100, + scanning: false + }) + ).toEqual({ + ready: false, + previousScanning: false, + lastSeenScannedAt: 100 + }) + + expect( + resolveResourceUsageSpaceScanReady({ + snapshot: readySnapshot, + runtimeEnvironmentActive: false, + open: false, + activeView: 'space', + scannedAt: 100, + scanning: false + }).ready + ).toBe(false) + }) + + it('does not announce the same scan completion twice', () => { + const result = resolveResourceUsageSpaceScanReady({ + snapshot: { + ready: false, + previousScanning: true, + lastSeenScannedAt: 100 + }, + runtimeEnvironmentActive: false, + open: false, + activeView: 'terminal', + scannedAt: 100, + scanning: false + }) + + expect(result.ready).toBe(false) + expect(result.lastSeenScannedAt).toBe(100) + }) + + it('hides local Space scan handoffs while a remote runtime is active', () => { + expect( + resolveResourceUsageSpaceScanReady({ + snapshot: { + ready: true, + previousScanning: true, + lastSeenScannedAt: 100 + }, + runtimeEnvironmentActive: true, + open: false, + activeView: 'terminal', + scannedAt: 200, + scanning: true + }) + ).toEqual({ + ready: false, + previousScanning: false, + lastSeenScannedAt: 100 + }) + }) + + it('tracks scanning starts without changing readiness', () => { + expect( + resolveResourceUsageSpaceScanReady({ + snapshot: baseSnapshot, + runtimeEnvironmentActive: false, + open: false, + activeView: 'terminal', + scannedAt: null, + scanning: true + }) + ).toEqual({ + ready: false, + previousScanning: true, + lastSeenScannedAt: null + }) + }) +}) diff --git a/src/renderer/src/components/status-bar/resource-usage-space-scan-ready.ts b/src/renderer/src/components/status-bar/resource-usage-space-scan-ready.ts new file mode 100644 index 00000000000..dd8a6aaedad --- /dev/null +++ b/src/renderer/src/components/status-bar/resource-usage-space-scan-ready.ts @@ -0,0 +1,60 @@ +export type ResourceUsageSpaceScanSnapshot = { + ready: boolean + previousScanning: boolean + lastSeenScannedAt: number | null +} + +export function resolveResourceUsageSpaceScanReady({ + snapshot, + runtimeEnvironmentActive, + open, + activeView, + scannedAt, + scanning +}: { + snapshot: ResourceUsageSpaceScanSnapshot + runtimeEnvironmentActive: boolean + open: boolean + activeView: string + scannedAt: number | null + scanning: boolean +}): ResourceUsageSpaceScanSnapshot { + if (runtimeEnvironmentActive) { + return { + ready: false, + previousScanning: false, + lastSeenScannedAt: snapshot.lastSeenScannedAt + } + } + + const scanCompleted = + snapshot.previousScanning && + !scanning && + scannedAt !== null && + scannedAt !== snapshot.lastSeenScannedAt + + if (scanCompleted) { + return { + ready: !open && activeView !== 'space', + previousScanning: scanning, + lastSeenScannedAt: scannedAt + } + } + + if (snapshot.ready && (open || activeView === 'space')) { + return { + ready: false, + previousScanning: scanning, + lastSeenScannedAt: scannedAt + } + } + + if (snapshot.previousScanning !== scanning) { + return { + ...snapshot, + previousScanning: scanning + } + } + + return snapshot +}