From 1a76ebe364b2caeaccf09f7e86ff2f514331c011 Mon Sep 17 00:00:00 2001 From: Neil Date: Wed, 11 Nov 2026 15:52:05 -0800 Subject: [PATCH] perf(sidebar): share tab ownership across activity projections --- .../worktree-agent-activity-summary.ts | 2 +- .../activity/worktree-agent-row-selectors.ts | 21 +++------------- .../activity/worktree-activity-state.ts | 8 ++----- .../lib/workspace/tabs/tab-worktree-index.ts | 24 +++++++++++++++++++ 4 files changed, 30 insertions(+), 25 deletions(-) create mode 100644 src/renderer/src/lib/workspace/tabs/tab-worktree-index.ts diff --git a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-agent-activity-summary.ts b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-agent-activity-summary.ts index bba52d8c3a1..f945ab011d1 100644 --- a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-agent-activity-summary.ts +++ b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-agent-activity-summary.ts @@ -18,7 +18,7 @@ import { import type { AgentStatusOrchestrationContext } from '../../../../../../shared/agent-status/contracts/agent-status-types' import { agentVerdictDisplayMark } from '../../../../../../shared/agent-status/lifecycle/agent-main-agent-verdict' import { applyAgentPaneActivityFlags } from '../../../../../../shared/worktree-status-rollup' -import { getTabIdToWorktreeId } from './worktree-agent-row-selectors' +import { getTabIdToWorktreeId } from '@/lib/workspace/tabs/tab-worktree-index' export type WorktreeAgentActivitySummary = { hasPermission: boolean diff --git a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-agent-row-selectors.ts b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-agent-row-selectors.ts index a9988231730..958474fd018 100644 --- a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-agent-row-selectors.ts +++ b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-agent-row-selectors.ts @@ -1,6 +1,7 @@ import { getStructuredAgentSessionTabsByPaneKey } from '@/components/native-chat/session/status/structured-agent-session-tabs' import type { RetainedAgentEntry } from '@/store/slices/agent-status/agent-status-contract' import type { AppState } from '@/store/types' +import { getTabIdToWorktreeId } from '@/lib/workspace/tabs/tab-worktree-index' import type { AgentStatusEntry } from '../../../../../../shared/agent-status/contracts/agent-status-types' import type { MigrationUnsupportedPtyEntry } from '../../../../../../shared/agent-status/contracts/agent-status-ipc-payload' import { parsePaneKey } from '../../../../../../shared/workspace-layout/stable-pane-id' @@ -35,10 +36,8 @@ type WorktreeAgentRowsState = Pick< unifiedTabsByWorktree?: AppState['unifiedTabsByWorktree'] } -type TabOwnershipRows = Record - type TabWorktreeIndexCache = { - tabsByWorktree: TabOwnershipRows + tabsByWorktree: WorktreeAgentRowsState['tabsByWorktree'] tabIdToWorktreeId: Map } @@ -57,7 +56,6 @@ type RetainedEntriesByWorktreeCache = { entriesByWorktree: Map } -let tabWorktreeIndexCache: TabWorktreeIndexCache | null = null let liveTabWorktreeIndexCache: LiveTabWorktreeIndexCache | null = null let liveEntriesByWorktreeCache: LiveEntriesByWorktreeCache | null = null let migrationUnsupportedByWorktreeCache: MigrationUnsupportedByWorktreeCache | null = null @@ -76,20 +74,7 @@ export function reuseArrayIfEqual(previous: T[] | undefined, next: T[]): T[] return previous } -// Settings reuses this tab index to avoid rescanning every bucket on each store write. -export function getTabIdToWorktreeId(tabsByWorktree: TabOwnershipRows): Map { - if (tabWorktreeIndexCache?.tabsByWorktree === tabsByWorktree) { - return tabWorktreeIndexCache.tabIdToWorktreeId - } - const tabIdToWorktreeId = new Map() - for (const [worktreeId, tabs] of Object.entries(tabsByWorktree)) { - for (const tab of tabs) { - tabIdToWorktreeId.set(tab.id, worktreeId) - } - } - tabWorktreeIndexCache = { tabsByWorktree, tabIdToWorktreeId } - return tabIdToWorktreeId -} +export { getTabIdToWorktreeId } function getLiveTabIdToWorktreeId( tabsByWorktree: WorktreeAgentRowsState['tabsByWorktree'], diff --git a/src/renderer/src/lib/workspace/activity/worktree-activity-state.ts b/src/renderer/src/lib/workspace/activity/worktree-activity-state.ts index b7f4d8d816f..a2ff6739af1 100644 --- a/src/renderer/src/lib/workspace/activity/worktree-activity-state.ts +++ b/src/renderer/src/lib/workspace/activity/worktree-activity-state.ts @@ -1,4 +1,5 @@ import type { AppState } from '@/store/types' +import { getTabIdToWorktreeId } from '../tabs/tab-worktree-index' import { createAgentStatusEpochSelector } from '../../agent-status/evidence/agent-status-epoch-selector' import { tabHasLivePty } from '../../../../../shared/tab-has-live-pty' import type { TerminalTab } from '../../../../../shared/workspace-layout/terminal-tab-types' @@ -50,12 +51,7 @@ export function getLiveAgentStatusByWorktreeId( if (entries.length === 0) { return new Map() } - const worktreeIdByTabId = new Map() - for (const [worktreeId, tabs] of Object.entries(tabsByWorktree ?? {})) { - for (const tab of tabs) { - worktreeIdByTabId.set(tab.id, worktreeId) - } - } + const worktreeIdByTabId = getTabIdToWorktreeId(tabsByWorktree) const result = new Map() for (const entry of entries) { const worktreeId = resolveAgentStatusWorktreeId(entry, worktreeIdByTabId) diff --git a/src/renderer/src/lib/workspace/tabs/tab-worktree-index.ts b/src/renderer/src/lib/workspace/tabs/tab-worktree-index.ts new file mode 100644 index 00000000000..57ba419cde7 --- /dev/null +++ b/src/renderer/src/lib/workspace/tabs/tab-worktree-index.ts @@ -0,0 +1,24 @@ +export type TabOwnershipRows = Record + +// Keep full and projected catalogs cached without retaining replaced snapshots. +const tabWorktreeIndexes = new WeakMap>() + +export function getTabIdToWorktreeId( + tabsByWorktree: TabOwnershipRows | null | undefined +): Map { + if (!tabsByWorktree) { + return new Map() + } + const cached = tabWorktreeIndexes.get(tabsByWorktree) + if (cached) { + return cached + } + const index = new Map() + for (const [worktreeId, tabs] of Object.entries(tabsByWorktree)) { + for (const tab of tabs) { + index.set(tab.id, worktreeId) + } + } + tabWorktreeIndexes.set(tabsByWorktree, index) + return index +}