From c445f26541a0ecb465fc28a7e1a7b5ee5624a0dd Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 22 Jul 2026 22:17:53 -0700 Subject: [PATCH] perf(renderer): share retained TabBar projections (#10094) --- .../tab-bar/TabBar.context-menu.test.ts | 8 + .../src/components/tab-bar/TabBar.tsx | 18 +- .../tab-bar/tab-agent-types-by-tab-id.test.ts | 174 ++++++++++++++- .../tab-bar/tab-agent-types-by-tab-id.ts | 200 ++++++++++++++---- 4 files changed, 341 insertions(+), 59 deletions(-) diff --git a/src/renderer/src/components/tab-bar/TabBar.context-menu.test.ts b/src/renderer/src/components/tab-bar/TabBar.context-menu.test.ts index 026c5d25a..70dabed9b 100644 --- a/src/renderer/src/components/tab-bar/TabBar.context-menu.test.ts +++ b/src/renderer/src/components/tab-bar/TabBar.context-menu.test.ts @@ -330,6 +330,14 @@ describe('TabBar context menu wiring', () => { vi.unstubAllGlobals() }) + it('wires the shared agent projection selector into the production TabBar', async () => { + const { selectTabBarAgentProjections } = await import('./tab-agent-types-by-tab-id') + + await renderTabBar({ tabs: [], editorFiles: [], browserTabs: [], tabBarOrder: [] }) + + expect(useAppStoreMock).toHaveBeenCalledWith(selectTabBarAgentProjections) + }) + it('counts every tab kind for SortableTab.tabCount', async () => { // Why: Close Others used to pass tabCount=tabs.length, where tabs is just the // terminal list. With one terminal + any number of editor/browser tabs, the diff --git a/src/renderer/src/components/tab-bar/TabBar.tsx b/src/renderer/src/components/tab-bar/TabBar.tsx index a1e01b50c..95de615d4 100644 --- a/src/renderer/src/components/tab-bar/TabBar.tsx +++ b/src/renderer/src/components/tab-bar/TabBar.tsx @@ -78,10 +78,7 @@ import { useTabStripDragScrollHandlers } from './tab-strip-drag-scroll' import { shouldShowWindowsShellMenu } from './windows-shell-menu-visibility' import { canToggleNativeChat } from '../native-chat/native-chat-availability' import { isNativeChatTranscriptLocalReadable } from '@/lib/native-chat-transcript-readability' -import { - selectNativeChatTabWideFallbackUnsafeTabsById, - selectTabAgentTypesByTabId -} from './tab-agent-types-by-tab-id' +import { selectTabBarAgentProjections } from './tab-agent-types-by-tab-id' import { resolveCommittedTitleAgentType } from '@/lib/pane-agent-evidence' const isWindows = navigator.userAgent.includes('Windows') @@ -419,16 +416,9 @@ function TabBarInner({ // Why: tab-wide launch/title hints are safe only before split; gate the view-mode toggle to the active leaf's agent. const toggleTabViewMode = useAppStore((s) => s.toggleTabViewMode) - // Why: agentStatusByPaneKey churns on every status flip; project {tabId:agentType} to re-render only on agent identity change. - const tabAgentTypesByTabId = useAppStore( - useShallow((s) => - selectTabAgentTypesByTabId(s.agentStatusByPaneKey ?? {}, s.terminalLayoutsByTabId) - ) - ) - const nativeChatTabWideFallbackUnsafeTabsById = useAppStore( - useShallow((s) => selectNativeChatTabWideFallbackUnsafeTabsById(s.terminalLayoutsByTabId)) - ) - const nativeChatEnabled = useAppStore((s) => s.settings?.experimentalNativeChat === true) + // Why: every retained TabBar observes the same hot maps; one feature-gated selector shares their projections. + const { nativeChatEnabled, tabAgentTypesByTabId, nativeChatTabWideFallbackUnsafeTabsById } = + useAppStore(useShallow(selectTabBarAgentProjections)) const nativeChatTranscriptIsLocalReadable = useAppStore((s) => isNativeChatTranscriptLocalReadable(getConnectionIdFromState(s, worktreeId)) ) diff --git a/src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.test.ts b/src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.test.ts index 727624abb..e460318e6 100644 --- a/src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.test.ts +++ b/src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.test.ts @@ -1,9 +1,11 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { shallow } from 'zustand/shallow' import type { AgentStatusEntry } from '../../../../shared/agent-status-types' import type { TerminalLayoutSnapshot } from '../../../../shared/types' import { findTabAgentEntry } from '../native-chat/native-chat-tab-agent-entry' import { + createTabBarAgentProjectionSelector, + selectTabBarAgentProjections, selectNativeChatTabWideFallbackUnsafeTabsById, selectTabAgentTypesByTabId } from './tab-agent-types-by-tab-id' @@ -205,4 +207,174 @@ describe('selectTabAgentTypesByTabId', () => { it('ignores malformed pane keys with no tab id', () => { expect(selectTabAgentTypesByTabId({ ':leaf-a': entry({ agentType: 'claude' }) })).toEqual({}) }) + + it('shares one pair of global scans across retained TabBar consumers', () => { + const onStatusEntryVisited = vi.fn() + const onAgentTypeLayoutVisited = vi.fn() + const onUnsafeLayoutVisited = vi.fn() + const select = createTabBarAgentProjectionSelector({ + onStatusEntryVisited, + onAgentTypeLayoutVisited, + onUnsafeLayoutVisited + }) + const statuses = { + 'tab-1:leaf-a': entry({ agentType: 'claude' }), + 'tab-2:leaf-a': entry({ agentType: 'codex' }), + 'tab-3:leaf-a': entry({ agentType: 'grok' }) + } + const layouts = { + 'tab-1': splitLayout('leaf-a'), + 'tab-2': splitLayout('leaf-a') + } + + for (let consumer = 0; consumer < 100; consumer++) { + select({ + settings: { experimentalNativeChat: true }, + agentStatusByPaneKey: statuses, + terminalLayoutsByTabId: layouts + }) + } + + expect(onStatusEntryVisited).toHaveBeenCalledTimes(3) + expect(onAgentTypeLayoutVisited).toHaveBeenCalledTimes(2) + expect(onUnsafeLayoutVisited).toHaveBeenCalledTimes(2) + }) + + it('reuses outputs and invalidates only the projection whose input changed', () => { + const onStatusEntryVisited = vi.fn() + const onAgentTypeLayoutVisited = vi.fn() + const onUnsafeLayoutVisited = vi.fn() + const select = createTabBarAgentProjectionSelector({ + onStatusEntryVisited, + onAgentTypeLayoutVisited, + onUnsafeLayoutVisited + }) + const split = { 'tab-1': splitLayout('leaf-a') } + const working = { + 'tab-1:leaf-a': entry({ agentType: 'claude', state: 'working' }), + 'tab-1:leaf-b': entry({ agentType: 'codex', state: 'working' }) + } + + const first = select({ + settings: { experimentalNativeChat: true }, + agentStatusByPaneKey: working, + terminalLayoutsByTabId: split + }) + const done = { + 'tab-1:leaf-a': entry({ agentType: 'claude', state: 'done' }), + 'tab-1:leaf-b': entry({ agentType: 'codex', state: 'done' }) + } + const afterStatus = select({ + settings: { experimentalNativeChat: true }, + agentStatusByPaneKey: done, + terminalLayoutsByTabId: split + }) + + expect(afterStatus).toBe(first) + expect(onStatusEntryVisited).toHaveBeenCalledTimes(4) + expect(onAgentTypeLayoutVisited).toHaveBeenCalledTimes(2) + expect(onUnsafeLayoutVisited).toHaveBeenCalledTimes(1) + + const singleLeaf = { + 'tab-1': { + root: { type: 'leaf' as const, leafId: 'leaf-b' }, + activeLeafId: 'leaf-b', + expandedLeafId: null + } + } + const afterLayout = select({ + settings: { experimentalNativeChat: true }, + agentStatusByPaneKey: done, + terminalLayoutsByTabId: singleLeaf + }) + + expect(afterLayout.tabAgentTypesByTabId).toEqual({ 'tab-1': 'codex' }) + expect(afterLayout.tabAgentTypesByTabId).not.toBe(first.tabAgentTypesByTabId) + expect(afterLayout.nativeChatTabWideFallbackUnsafeTabsById).toEqual({}) + expect(afterLayout.nativeChatTabWideFallbackUnsafeTabsById).not.toBe( + first.nativeChatTabWideFallbackUnsafeTabsById + ) + expect(onStatusEntryVisited).toHaveBeenCalledTimes(6) + expect(onAgentTypeLayoutVisited).toHaveBeenCalledTimes(3) + expect(onUnsafeLayoutVisited).toHaveBeenCalledTimes(2) + }) + + it('normalizes missing maps to shared empty inputs', () => { + const select = createTabBarAgentProjectionSelector() + + const first = select({ settings: { experimentalNativeChat: true } }) + + expect(select({ settings: { experimentalNativeChat: true } })).toBe(first) + }) + + it('releases enabled inputs on disable and rescans them after re-enabling', () => { + const onStatusEntryVisited = vi.fn() + const onAgentTypeLayoutVisited = vi.fn() + const onUnsafeLayoutVisited = vi.fn() + const select = createTabBarAgentProjectionSelector({ + onStatusEntryVisited, + onAgentTypeLayoutVisited, + onUnsafeLayoutVisited + }) + const state = { + settings: { experimentalNativeChat: true }, + agentStatusByPaneKey: { 'tab-1:leaf-a': entry({ agentType: 'claude' }) }, + terminalLayoutsByTabId: { 'tab-1': splitLayout('leaf-a') } + } + + const first = select(state) + select({ ...state, settings: { experimentalNativeChat: false } }) + const afterReenable = select(state) + + expect(afterReenable).not.toBe(first) + expect(onStatusEntryVisited).toHaveBeenCalledTimes(2) + expect(onAgentTypeLayoutVisited).toHaveBeenCalledTimes(2) + expect(onUnsafeLayoutVisited).toHaveBeenCalledTimes(2) + }) + + it('production selector skips all map scans while native chat is disabled', () => { + let statusEnumerations = 0 + let layoutEnumerations = 0 + const statuses = new Proxy( + { 'tab-1:leaf-a': entry({ agentType: 'claude' }) }, + { + ownKeys(target) { + statusEnumerations++ + return Reflect.ownKeys(target) + } + } + ) + const layouts = new Proxy( + { 'tab-1': splitLayout('leaf-a') }, + { + ownKeys(target) { + layoutEnumerations++ + return Reflect.ownKeys(target) + } + } + ) + const disabledState = { + settings: { experimentalNativeChat: false }, + agentStatusByPaneKey: statuses, + terminalLayoutsByTabId: layouts + } + + const disabled = selectTabBarAgentProjections(disabledState) + for (let consumer = 0; consumer < 100; consumer++) { + expect(selectTabBarAgentProjections(disabledState)).toBe(disabled) + } + expect(statusEnumerations).toBe(0) + expect(layoutEnumerations).toBe(0) + + const enabledState = { + ...disabledState, + settings: { experimentalNativeChat: true } + } + const enabled = selectTabBarAgentProjections(enabledState) + for (let consumer = 0; consumer < 100; consumer++) { + expect(selectTabBarAgentProjections(enabledState)).toBe(enabled) + } + expect(statusEnumerations).toBe(1) + expect(layoutEnumerations).toBe(2) + }) }) diff --git a/src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.ts b/src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.ts index 68d78a43a..b320d5e0d 100644 --- a/src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.ts +++ b/src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.ts @@ -5,6 +5,102 @@ import { resolveNativeChatActiveLayoutLeafId } from '../native-chat/native-chat-leaf-routing' +type TabBarAgentProjectionSelectorDependencies = { + onStatusEntryVisited?: (paneKey: string) => void + onAgentTypeLayoutVisited?: (tabId: string) => void + onUnsafeLayoutVisited?: (tabId: string) => void +} + +export type TabBarAgentProjectionState = { + agentStatusByPaneKey?: Record + terminalLayoutsByTabId?: Record + settings?: { experimentalNativeChat?: boolean } | null +} + +export type TabBarAgentProjections = { + nativeChatEnabled: boolean + tabAgentTypesByTabId: Record + nativeChatTabWideFallbackUnsafeTabsById: Record +} + +const EMPTY_AGENT_STATUS_BY_PANE_KEY: Record = Object.freeze({}) +const EMPTY_TERMINAL_LAYOUTS_BY_TAB_ID: Record = Object.freeze({}) +const EMPTY_TAB_AGENT_TYPES_BY_TAB_ID: Record = Object.freeze({}) +const EMPTY_UNSAFE_TABS_BY_ID: Record = Object.freeze({}) +const DISABLED_TAB_BAR_AGENT_PROJECTIONS: TabBarAgentProjections = Object.freeze({ + nativeChatEnabled: false, + tabAgentTypesByTabId: EMPTY_TAB_AGENT_TYPES_BY_TAB_ID, + nativeChatTabWideFallbackUnsafeTabsById: EMPTY_UNSAFE_TABS_BY_ID +}) + +function reuseRecordIfEqual( + previous: Record | undefined, + next: Record +): Record { + if (!previous) { + return next + } + const nextKeys = Object.keys(next) + if (Object.keys(previous).length !== nextKeys.length) { + return next + } + return nextKeys.every((key) => previous[key] === next[key]) ? previous : next +} + +function projectTabAgentTypesByTabId( + agentStatusByPaneKey: Record, + terminalLayoutsByTabId: Record, + dependencies?: TabBarAgentProjectionSelectorDependencies +): Record { + const byTabId: Record = {} + const claimed = new Set() + for (const [tabId, layout] of Object.entries(terminalLayoutsByTabId)) { + dependencies?.onAgentTypeLayoutVisited?.(tabId) + if (!layout.root && !layout.activeLeafId) { + continue + } + claimed.add(tabId) + const activeLeafId = resolveNativeChatActiveLayoutLeafId(layout) + if (!activeLeafId) { + continue + } + const entry = agentStatusByPaneKey[`${tabId}:${activeLeafId}`] + if (entry?.agentType != null) { + byTabId[tabId] = entry.agentType + } + } + for (const [paneKey, entry] of Object.entries(agentStatusByPaneKey)) { + dependencies?.onStatusEntryVisited?.(paneKey) + const colon = paneKey.indexOf(':') + if (colon <= 0) { + continue + } + const tabId = paneKey.slice(0, colon) + if (claimed.has(tabId)) { + continue + } + claimed.add(tabId) + if (entry.agentType != null) { + byTabId[tabId] = entry.agentType + } + } + return byTabId +} + +function projectNativeChatTabWideFallbackUnsafeTabsById( + terminalLayoutsByTabId: Record, + dependencies?: TabBarAgentProjectionSelectorDependencies +): Record { + const unsafeTabs: Record = {} + for (const [tabId, layout] of Object.entries(terminalLayoutsByTabId)) { + dependencies?.onUnsafeLayoutVisited?.(tabId) + if (!isNativeChatTabWideFallbackSafe(layout)) { + unsafeTabs[tabId] = true + } + } + return unsafeTabs +} + /** * Project `agentStatusByPaneKey` down to the stable `{ terminalTabId: agentType }` * the tab strip actually reads (to gate the native-chat view-mode toggle). @@ -26,53 +122,69 @@ export function selectTabAgentTypesByTabId( agentStatusByPaneKey: Record, terminalLayoutsByTabId: Record = {} ): Record { - const byTabId: Record = {} - const claimed = new Set() - // Why: the tab action opens chat on the active split leaf, so that leaf's - // identity must outrank object insertion order from unrelated siblings. - for (const [tabId, layout] of Object.entries(terminalLayoutsByTabId)) { - // A rootless snapshot with no active leaf is hydration absence, not a - // topology decision; preserve the legacy tab lookup until a leaf exists. - if (!layout.root && !layout.activeLeafId) { - continue - } - claimed.add(tabId) - const activeLeafId = resolveNativeChatActiveLayoutLeafId(layout) - if (!activeLeafId) { - continue - } - const entry = agentStatusByPaneKey[`${tabId}:${activeLeafId}`] - if (entry?.agentType != null) { - byTabId[tabId] = entry.agentType - } - } - for (const [paneKey, entry] of Object.entries(agentStatusByPaneKey)) { - const colon = paneKey.indexOf(':') - if (colon <= 0) { - continue - } - const tabId = paneKey.slice(0, colon) - if (claimed.has(tabId)) { - continue - } - claimed.add(tabId) - if (entry.agentType != null) { - byTabId[tabId] = entry.agentType - } - } - return byTabId + return projectTabAgentTypesByTabId(agentStatusByPaneKey, terminalLayoutsByTabId) } export function selectNativeChatTabWideFallbackUnsafeTabsById( terminalLayoutsByTabId: Record = {} ): Record { - // Why: legacy and hydrating store shapes may not expose layout state yet; - // absence carries no unsafe split evidence and must not crash tab rendering. - const unsafeTabs: Record = {} - for (const [tabId, layout] of Object.entries(terminalLayoutsByTabId)) { - if (!isNativeChatTabWideFallbackSafe(layout)) { - unsafeTabs[tabId] = true - } - } - return unsafeTabs + return projectNativeChatTabWideFallbackUnsafeTabsById(terminalLayoutsByTabId) } + +export function createTabBarAgentProjectionSelector( + dependencies?: TabBarAgentProjectionSelectorDependencies +): (state: TabBarAgentProjectionState) => TabBarAgentProjections { + let cachedAgentStatusByPaneKey: Record | null = null + let cachedAgentTypeLayoutsByTabId: Record | null = null + let cachedAgentTypesByTabId = EMPTY_TAB_AGENT_TYPES_BY_TAB_ID + let cachedUnsafeLayoutsByTabId: Record | null = null + let cachedUnsafeTabsById = EMPTY_UNSAFE_TABS_BY_ID + let cachedEnabledResult: TabBarAgentProjections | null = null + + return (state) => { + if (state.settings?.experimentalNativeChat !== true) { + if (cachedEnabledResult) { + cachedAgentStatusByPaneKey = null + cachedAgentTypeLayoutsByTabId = null + cachedAgentTypesByTabId = EMPTY_TAB_AGENT_TYPES_BY_TAB_ID + cachedUnsafeLayoutsByTabId = null + cachedUnsafeTabsById = EMPTY_UNSAFE_TABS_BY_ID + cachedEnabledResult = null + } + return DISABLED_TAB_BAR_AGENT_PROJECTIONS + } + + const statuses = state.agentStatusByPaneKey ?? EMPTY_AGENT_STATUS_BY_PANE_KEY + const layouts = state.terminalLayoutsByTabId ?? EMPTY_TERMINAL_LAYOUTS_BY_TAB_ID + if (statuses !== cachedAgentStatusByPaneKey || layouts !== cachedAgentTypeLayoutsByTabId) { + cachedAgentTypesByTabId = reuseRecordIfEqual( + cachedAgentTypesByTabId, + projectTabAgentTypesByTabId(statuses, layouts, dependencies) + ) + cachedAgentStatusByPaneKey = statuses + cachedAgentTypeLayoutsByTabId = layouts + } + if (layouts !== cachedUnsafeLayoutsByTabId) { + cachedUnsafeTabsById = reuseRecordIfEqual( + cachedUnsafeTabsById, + projectNativeChatTabWideFallbackUnsafeTabsById(layouts, dependencies) + ) + cachedUnsafeLayoutsByTabId = layouts + } + if ( + cachedEnabledResult?.tabAgentTypesByTabId === cachedAgentTypesByTabId && + cachedEnabledResult.nativeChatTabWideFallbackUnsafeTabsById === cachedUnsafeTabsById + ) { + return cachedEnabledResult + } + cachedEnabledResult = { + nativeChatEnabled: true, + tabAgentTypesByTabId: cachedAgentTypesByTabId, + nativeChatTabWideFallbackUnsafeTabsById: cachedUnsafeTabsById + } + return cachedEnabledResult + } +} + +// Why: every retained TabBar requests the same global projection tuple. +export const selectTabBarAgentProjections = createTabBarAgentProjectionSelector()