From 106009a90c3f1e84ae89fc69fb28cf44e09d6776 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 10 Jul 2026 20:51:28 -0700 Subject: [PATCH] perf(renderer): index terminal unified tab reads (#8076) --- .../components/terminal-pane/TerminalPane.tsx | 18 +++---- .../terminal-unified-tab-lookup.test.ts | 32 ++++++++---- .../terminal-unified-tab-lookup.ts | 51 +++++++++++-------- 3 files changed, 61 insertions(+), 40 deletions(-) diff --git a/src/renderer/src/components/terminal-pane/TerminalPane.tsx b/src/renderer/src/components/terminal-pane/TerminalPane.tsx index 57b0a6927..2231ba668 100644 --- a/src/renderer/src/components/terminal-pane/TerminalPane.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalPane.tsx @@ -180,7 +180,10 @@ import { subscribeTerminalPaneAttention } from './terminal-pane-attention-subscriptions' import { getCachedTerminalTabForWorktree } from './terminal-tab-lookup' -import { getCachedTerminalGroupIdForWorktree } from './terminal-unified-tab-lookup' +import { + getCachedTerminalGroupIdForWorktree, + getCachedUnifiedTerminalTabForWorktree +} from './terminal-unified-tab-lookup' import { resolveNativeChatLeafTitleAgent } from './native-chat-leaf-title-agent' import { useRepoById } from '@/store/selectors' import { @@ -655,23 +658,18 @@ export default function TerminalPane({ // communicates the presence-lock inside the chat surface instead (U9/R8). const unifiedTabId = useAppStore( (store) => - (store.unifiedTabsByWorktree[worktreeId] ?? []).find( - (t) => t.contentType === 'terminal' && t.entityId === tabId - )?.id + getCachedUnifiedTerminalTabForWorktree(store.unifiedTabsByWorktree, worktreeId, tabId)?.id ) const isChatViewMode = useAppStore( (store) => - (store.unifiedTabsByWorktree[worktreeId] ?? []).find( - (t) => t.contentType === 'terminal' && t.entityId === tabId - )?.viewMode === 'chat' + getCachedUnifiedTerminalTabForWorktree(store.unifiedTabsByWorktree, worktreeId, tabId) + ?.viewMode === 'chat' ) const nativeChatEnabled = useAppStore((store) => store.settings?.experimentalNativeChat === true) const effectiveChatViewMode = nativeChatEnabled && isChatViewMode const unifiedTabLabel = useAppStore( (store) => - (store.unifiedTabsByWorktree[worktreeId] ?? []).find( - (t) => t.contentType === 'terminal' && t.entityId === tabId - )?.label + getCachedUnifiedTerminalTabForWorktree(store.unifiedTabsByWorktree, worktreeId, tabId)?.label ) const runtimePaneTitlesByPaneId = useAppStore( useShallow((store) => store.runtimePaneTitlesByTabId[tabId] ?? {}) diff --git a/src/renderer/src/components/terminal-pane/terminal-unified-tab-lookup.test.ts b/src/renderer/src/components/terminal-pane/terminal-unified-tab-lookup.test.ts index 998f25e11..18872bf04 100644 --- a/src/renderer/src/components/terminal-pane/terminal-unified-tab-lookup.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-unified-tab-lookup.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it, vi } from 'vitest' import type { Tab } from '../../../../shared/types' -import { getCachedTerminalGroupIdForWorktree } from './terminal-unified-tab-lookup' +import { + getCachedTerminalGroupIdForWorktree, + getCachedUnifiedTerminalTabForWorktree +} from './terminal-unified-tab-lookup' function makeTerminalTab(entityId: string, groupId: string): Tab { return { @@ -45,8 +48,8 @@ function iterableTabs(tabs: Tab[]): { } } -describe('getCachedTerminalGroupIdForWorktree', () => { - it('reuses the terminal group lookup while the unified tab array is unchanged', () => { +describe('terminal unified tab lookup', () => { + it('shares one terminal lookup across all pane field and group reads', () => { const tabs = [ makeEditorTab('editor-1'), ...Array.from({ length: 200 }, (_, index) => @@ -56,14 +59,23 @@ describe('getCachedTerminalGroupIdForWorktree', () => { const { value, iterator } = iterableTabs(tabs) const unifiedTabsByWorktree = { 'wt-1': value } - expect(getCachedTerminalGroupIdForWorktree(unifiedTabsByWorktree, 'wt-1', 'terminal-199')).toBe( - 'group-3' - ) + for (let index = 0; index < 200; index += 1) { + const terminalTabId = `terminal-${index}` + expect( + getCachedUnifiedTerminalTabForWorktree(unifiedTabsByWorktree, 'wt-1', terminalTabId) + ).toBe(tabs[index + 1]) + expect( + getCachedUnifiedTerminalTabForWorktree(unifiedTabsByWorktree, 'wt-1', terminalTabId)?.id + ).toBe(terminalTabId) + expect( + getCachedUnifiedTerminalTabForWorktree(unifiedTabsByWorktree, 'wt-1', terminalTabId)?.label + ).toBe(terminalTabId) + } expect(getCachedTerminalGroupIdForWorktree(unifiedTabsByWorktree, 'wt-1', 'terminal-0')).toBe( 'group-0' ) expect( - getCachedTerminalGroupIdForWorktree(unifiedTabsByWorktree, 'wt-1', 'editor-1') + getCachedUnifiedTerminalTabForWorktree(unifiedTabsByWorktree, 'wt-1', 'editor-1') ).toBeNull() expect(iterator).toHaveBeenCalledTimes(1) @@ -73,9 +85,9 @@ describe('getCachedTerminalGroupIdForWorktree', () => { const first = iterableTabs([makeTerminalTab('terminal-1', 'group-a')]) const second = iterableTabs([makeTerminalTab('terminal-1', 'group-b')]) - expect(getCachedTerminalGroupIdForWorktree({ 'wt-1': first.value }, 'wt-1', 'terminal-1')).toBe( - 'group-a' - ) + expect( + getCachedUnifiedTerminalTabForWorktree({ 'wt-1': first.value }, 'wt-1', 'terminal-1')?.groupId + ).toBe('group-a') expect( getCachedTerminalGroupIdForWorktree({ 'wt-1': second.value }, 'wt-1', 'terminal-1') ).toBe('group-b') diff --git a/src/renderer/src/components/terminal-pane/terminal-unified-tab-lookup.ts b/src/renderer/src/components/terminal-pane/terminal-unified-tab-lookup.ts index 837911e96..2113121b2 100644 --- a/src/renderer/src/components/terminal-pane/terminal-unified-tab-lookup.ts +++ b/src/renderer/src/components/terminal-pane/terminal-unified-tab-lookup.ts @@ -1,29 +1,40 @@ import type { Tab } from '../../../../shared/types' -const terminalGroupLookupByUnifiedTabs = new WeakMap>() +const terminalTabLookupByUnifiedTabs = new WeakMap>() + +export function getCachedUnifiedTerminalTabForWorktree( + unifiedTabsByWorktree: Record, + worktreeId: string, + terminalTabId: string +): Tab | null { + const unifiedTabs = unifiedTabsByWorktree[worktreeId] + if (!unifiedTabs) { + return null + } + + let lookup = terminalTabLookupByUnifiedTabs.get(unifiedTabs) + if (!lookup) { + // Why: every retained TerminalPane reads this tab on every store update. + // Share one immutable-array index instead of repeating linear scans. + lookup = new Map() + for (const tab of unifiedTabs) { + if (tab.contentType === 'terminal') { + lookup.set(tab.entityId, tab) + } + } + terminalTabLookupByUnifiedTabs.set(unifiedTabs, lookup) + } + + return lookup.get(terminalTabId) ?? null +} export function getCachedTerminalGroupIdForWorktree( unifiedTabsByWorktree: Record, worktreeId: string, terminalTabId: string ): string | null { - const unifiedTabs = unifiedTabsByWorktree[worktreeId] - if (!unifiedTabs) { - return null - } - - let lookup = terminalGroupLookupByUnifiedTabs.get(unifiedTabs) - if (!lookup) { - // Why: every mounted TerminalPane asks for its owning group on store updates. - // Cache by immutable tab-array ref so 200 panes do not repeat the same scan. - lookup = new Map() - for (const tab of unifiedTabs) { - if (tab.contentType === 'terminal') { - lookup.set(tab.entityId, tab.groupId) - } - } - terminalGroupLookupByUnifiedTabs.set(unifiedTabs, lookup) - } - - return lookup.get(terminalTabId) ?? null + return ( + getCachedUnifiedTerminalTabForWorktree(unifiedTabsByWorktree, worktreeId, terminalTabId) + ?.groupId ?? null + ) }