diff --git a/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx b/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx index 36308d163..5575a4f85 100644 --- a/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx +++ b/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx @@ -32,7 +32,7 @@ import { cn } from '@/lib/utils' import { activateAndRevealWorktree } from '@/lib/worktree-activation' import { activateTabAndFocusPane } from '@/lib/activate-tab-and-focus-pane' import { useAppStore } from '../../store' -import { useAllWorktrees, useWorktreeMap } from '../../store/selectors' +import { useWorktreeMap } from '../../store/selectors' import { runWorktreeDelete } from '../sidebar/delete-worktree-flow' import { runSleepWorktree } from '../sidebar/sleep-worktree-flow' import { useDaemonActions, DaemonActionDialog } from '../shared/useDaemonActions' @@ -55,6 +55,12 @@ import { isResourceSessionActivationKey, navigateResourceSessionToTab } from './resource-session-navigation' +import { + getResourceUsageAllWorktrees, + getResourceUsageRepos, + getResourceUsageRuntimePaneTitlesByTabId, + getResourceUsageTabsByWorktree +} from './resource-usage-open-slices' const POLL_MS = 2_000 const SESSIONS_POLL_MS = 10_000 @@ -659,16 +665,12 @@ export function ResourceUsageStatusSegment({ const fetchSnapshot = useAppStore((s) => s.fetchMemorySnapshot) const workspaceSessionReady = useAppStore((s) => s.workspaceSessionReady) const ptyIdsByTabId = useAppStore((s) => s.ptyIdsByTabId) - const tabsByWorktree = useAppStore((s) => s.tabsByWorktree) - const runtimePaneTitlesByTabId = useAppStore((s) => s.runtimePaneTitlesByTabId) const setActiveView = useAppStore((s) => s.setActiveView) const openModal = useAppStore((s) => s.openModal) const openSpacePage = useAppStore((s) => s.openSpacePage) const activeView = useAppStore((s) => s.activeView) const workspaceSpaceScannedAt = useAppStore((s) => s.workspaceSpaceAnalysis?.scannedAt ?? null) const workspaceSpaceScanning = useAppStore((s) => s.workspaceSpaceScanning) - const repos = useAppStore((s) => s.repos) - const allWorktrees = useAllWorktrees() const activeRuntimeEnvironmentId = useAppStore( (s) => s.settings?.activeRuntimeEnvironmentId ?? null ) @@ -684,6 +686,19 @@ export function ResourceUsageStatusSegment({ const [killConfirm, setKillConfirm] = useState(null) const [killing, setKilling] = useState(false) const [spaceScanReady, setSpaceScanReady] = useState(false) + // 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. + const runtimePaneTitlesByTabId = useAppStore((s) => + getResourceUsageRuntimePaneTitlesByTabId(s, open, runtimeEnvironmentActive) + ) + const repos = useAppStore((s) => getResourceUsageRepos(s, open, runtimeEnvironmentActive)) + const allWorktrees = useAppStore((s) => + getResourceUsageAllWorktrees(s, open, runtimeEnvironmentActive) + ) + 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. diff --git a/src/renderer/src/components/status-bar/resource-usage-open-slices.test.ts b/src/renderer/src/components/status-bar/resource-usage-open-slices.test.ts new file mode 100644 index 000000000..76374b1cf --- /dev/null +++ b/src/renderer/src/components/status-bar/resource-usage-open-slices.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, it } from 'vitest' +import { + getResourceUsageAllWorktrees, + getResourceUsageRepos, + getResourceUsageRuntimePaneTitlesByTabId, + getResourceUsageTabsByWorktree +} from './resource-usage-open-slices' +import type { AppState } from '../../store' + +const terminalTab = (id: string): AppState['tabsByWorktree'][string][number] => ({ + id, + ptyId: null, + worktreeId: 'wt-1', + title: id, + customTitle: null, + color: null, + sortOrder: 0, + createdAt: 0 +}) + +const worktree = (): AppState['worktreesByRepo'][string][number] => ({ + id: 'wt-1', + repoId: 'repo-1', + path: '/repo/wt-1', + displayName: 'wt-1', + comment: '', + branch: 'main', + head: 'abc123', + isBare: false, + isMainWorktree: false, + linkedIssue: null, + linkedPR: null, + linkedLinearIssue: null, + isArchived: false, + isUnread: false, + isPinned: false, + sortOrder: 0, + lastActivityAt: 0 +}) + +describe('resource usage open slices', () => { + it('returns stable empty slices while the popover is closed', () => { + const tabsByWorktree = { 'wt-1': [terminalTab('tab-1')] } + const runtimePaneTitlesByTabId = { + 'tab-1': { 'tab-1:0': 'Working' } + } as AppState['runtimePaneTitlesByTabId'] + + const closedTabs = getResourceUsageTabsByWorktree({ tabsByWorktree }, false) + const closedTitles = getResourceUsageRuntimePaneTitlesByTabId( + { runtimePaneTitlesByTabId }, + false + ) + + expect(closedTabs).toBe(getResourceUsageTabsByWorktree({ tabsByWorktree: {} }, false)) + expect(closedTitles).toBe( + getResourceUsageRuntimePaneTitlesByTabId({ runtimePaneTitlesByTabId: {} }, false) + ) + expect(closedTabs).toEqual({}) + expect(closedTitles).toEqual({}) + }) + + it('returns live slices while the popover is open', () => { + const tabsByWorktree = { 'wt-1': [terminalTab('tab-1')] } + const runtimePaneTitlesByTabId = { + 'tab-1': { 'tab-1:0': 'Working' } + } as AppState['runtimePaneTitlesByTabId'] + + expect(getResourceUsageTabsByWorktree({ tabsByWorktree }, true)).toBe(tabsByWorktree) + expect(getResourceUsageRuntimePaneTitlesByTabId({ runtimePaneTitlesByTabId }, true)).toBe( + runtimePaneTitlesByTabId + ) + }) + + it('gates repo and worktree slices while closed or runtime-backed', () => { + const repos = [{ id: 'repo-1', path: '/repo', kind: 'git' }] as AppState['repos'] + const row = worktree() + const worktreesByRepo = { + 'repo-1': [row] + } + + expect(getResourceUsageRepos({ repos }, false, false)).toBe( + getResourceUsageRepos({ repos: [] }, false, false) + ) + expect(getResourceUsageAllWorktrees({ worktreesByRepo }, false, false)).toBe( + getResourceUsageAllWorktrees({ worktreesByRepo: {} }, false, false) + ) + expect(getResourceUsageRepos({ repos }, true, true)).toEqual([]) + expect(getResourceUsageAllWorktrees({ worktreesByRepo }, true, true)).toEqual([]) + expect(getResourceUsageRepos({ repos }, true, false)).toBe(repos) + expect(getResourceUsageAllWorktrees({ worktreesByRepo }, true, false)).toEqual([row]) + }) +}) diff --git a/src/renderer/src/components/status-bar/resource-usage-open-slices.ts b/src/renderer/src/components/status-bar/resource-usage-open-slices.ts new file mode 100644 index 000000000..1a2b8cefc --- /dev/null +++ b/src/renderer/src/components/status-bar/resource-usage-open-slices.ts @@ -0,0 +1,49 @@ +import type { AppState } from '../../store' +import { getAllWorktreesFromState } from '../../store/selectors' + +const EMPTY_TABS_BY_WORKTREE: AppState['tabsByWorktree'] = {} +const EMPTY_RUNTIME_PANE_TITLES_BY_TAB_ID: AppState['runtimePaneTitlesByTabId'] = {} +const EMPTY_REPOS: AppState['repos'] = [] +const EMPTY_WORKTREES: ReturnType = [] + +function shouldReadPopoverSlices(open: boolean, runtimeEnvironmentActive: boolean): boolean { + return open && !runtimeEnvironmentActive +} + +export function getResourceUsageTabsByWorktree( + state: Pick, + open: boolean, + runtimeEnvironmentActive = false +): AppState['tabsByWorktree'] { + return shouldReadPopoverSlices(open, runtimeEnvironmentActive) + ? state.tabsByWorktree + : EMPTY_TABS_BY_WORKTREE +} + +export function getResourceUsageRuntimePaneTitlesByTabId( + state: Pick, + open: boolean, + runtimeEnvironmentActive = false +): AppState['runtimePaneTitlesByTabId'] { + return shouldReadPopoverSlices(open, runtimeEnvironmentActive) + ? state.runtimePaneTitlesByTabId + : EMPTY_RUNTIME_PANE_TITLES_BY_TAB_ID +} + +export function getResourceUsageRepos( + state: Pick, + open: boolean, + runtimeEnvironmentActive: boolean +): AppState['repos'] { + return shouldReadPopoverSlices(open, runtimeEnvironmentActive) ? state.repos : EMPTY_REPOS +} + +export function getResourceUsageAllWorktrees( + state: Pick, + open: boolean, + runtimeEnvironmentActive: boolean +): ReturnType { + return shouldReadPopoverSlices(open, runtimeEnvironmentActive) + ? getAllWorktreesFromState(state) + : EMPTY_WORKTREES +}