From 79aea811734962d35a0177f5cf734b817543dea2 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:01:42 -0700 Subject: [PATCH] perf(renderer): index unread completion tabs (#17246) --- .../terminal-tab-activity-status.test.ts | 55 +++++++++++++++++++ .../tab-bar/terminal-tab-activity-status.ts | 50 ++++++++++++----- 2 files changed, 90 insertions(+), 15 deletions(-) diff --git a/src/renderer/src/components/tab-bar/terminal-tab-activity-status.test.ts b/src/renderer/src/components/tab-bar/terminal-tab-activity-status.test.ts index d2bed923686..bf367f93667 100644 --- a/src/renderer/src/components/tab-bar/terminal-tab-activity-status.test.ts +++ b/src/renderer/src/components/tab-bar/terminal-tab-activity-status.test.ts @@ -3,6 +3,7 @@ import type { AgentStatusEntry } from '../../../../shared/agent-status-types' import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { hasUnreadAgentCompletionForTerminalTab, + resetUnreadAgentCompletionTabIdsCacheForTest, resetTerminalTabActivityFlagsCacheForTest, resolveTerminalTabActivityStatus, resolveTerminalTabAttentionBadge, @@ -41,6 +42,7 @@ const LIVE_PTY = { [TAB_ID]: ['pty-1'] } beforeEach(() => { resetTerminalTabActivityFlagsCacheForTest() + resetUnreadAgentCompletionTabIdsCacheForTest() vi.useFakeTimers() vi.setSystemTime(NOW) }) @@ -48,6 +50,7 @@ beforeEach(() => { afterEach(() => { vi.useRealTimers() resetTerminalTabActivityFlagsCacheForTest() + resetUnreadAgentCompletionTabIdsCacheForTest() }) describe('resolveTerminalTabActivityStatus', () => { @@ -267,6 +270,7 @@ describe('hasUnreadAgentCompletionForTerminalTab', () => { expect( hasUnreadAgentCompletionForTerminalTab({ [`tab-2:${SECOND_LEAF_ID}`]: true }, TAB_ID) ).toBe(false) + expect(hasUnreadAgentCompletionForTerminalTab(undefined, TAB_ID)).toBe(false) }) // Why: the param accepts boolean maps, so a cleared-to-`false` marker must not read as unread. @@ -275,6 +279,57 @@ describe('hasUnreadAgentCompletionForTerminalTab', () => { hasUnreadAgentCompletionForTerminalTab({ [`${TAB_ID}:${FIRST_LEAF_ID}`]: false }, TAB_ID) ).toBe(false) }) + + it('preserves first-colon ownership for legacy and malformed pane keys', () => { + expect(hasUnreadAgentCompletionForTerminalTab({ [TAB_ID]: true }, TAB_ID)).toBe(true) + expect(hasUnreadAgentCompletionForTerminalTab({ [`${TAB_ID}:3`]: true }, TAB_ID)).toBe(true) + expect( + hasUnreadAgentCompletionForTerminalTab( + { [`${TAB_ID}:${FIRST_LEAF_ID}:suffix`]: true }, + TAB_ID + ) + ).toBe(true) + expect( + hasUnreadAgentCompletionForTerminalTab({ [`${TAB_ID}0:${SECOND_LEAF_ID}`]: true }, TAB_ID) + ).toBe(false) + }) + + it('changes only the owning tab when immutable marker snapshots add and clear unread', () => { + const before = { [`tab-2:${SECOND_LEAF_ID}`]: true } + const added = { ...before, [`${TAB_ID}:${FIRST_LEAF_ID}`]: true } + const cleared = { ...added, [`${TAB_ID}:${FIRST_LEAF_ID}`]: false } + + expect(hasUnreadAgentCompletionForTerminalTab(before, TAB_ID)).toBe(false) + expect(hasUnreadAgentCompletionForTerminalTab(added, TAB_ID)).toBe(true) + expect(hasUnreadAgentCompletionForTerminalTab(cleared, TAB_ID)).toBe(false) + expect(hasUnreadAgentCompletionForTerminalTab(before, 'tab-2')).toBe(true) + expect(hasUnreadAgentCompletionForTerminalTab(added, 'tab-2')).toBe(true) + expect(hasUnreadAgentCompletionForTerminalTab(cleared, 'tab-2')).toBe(true) + }) + + it('indexes one immutable marker snapshot once across all mounted tab lookups', () => { + const ownKeys = vi.fn(Reflect.ownKeys) + let valueReads = 0 + const markers: Record = Object.fromEntries( + Array.from({ length: 1_000 }, (_, index) => [`owner-${index}:leaf`, true] as const) + ) + const unread = new Proxy>(markers, { + ownKeys, + get: (target, property, receiver) => { + valueReads += 1 + return Reflect.get(target, property, receiver) + } + }) + + for (let publication = 0; publication < 5; publication += 1) { + for (let tab = 0; tab < 500; tab += 1) { + expect(hasUnreadAgentCompletionForTerminalTab(unread, `absent-${tab}`)).toBe(false) + } + } + + expect(ownKeys).toHaveBeenCalledTimes(1) + expect(valueReads).toBe(1_000) + }) }) describe('resolveTerminalTabAttentionBadge', () => { diff --git a/src/renderer/src/components/tab-bar/terminal-tab-activity-status.ts b/src/renderer/src/components/tab-bar/terminal-tab-activity-status.ts index 97f511a1d41..a801855a588 100644 --- a/src/renderer/src/components/tab-bar/terminal-tab-activity-status.ts +++ b/src/renderer/src/components/tab-bar/terminal-tab-activity-status.ts @@ -256,29 +256,49 @@ export function terminalTabHasUnreadActivity({ ) } +// Why: production writes replace this map; WeakMap supports retained snapshots without pinning them. +let unreadAgentCompletionTabIdsBySnapshot = new WeakMap< + Record, + ReadonlySet +>() + +function getUnreadAgentCompletionTabIds( + unreadAgentCompletionPanes: Record +): ReadonlySet { + const cached = unreadAgentCompletionTabIdsBySnapshot.get(unreadAgentCompletionPanes) + if (cached) { + return cached + } + + // Why: every mounted tab runs this selector per store write; index each immutable marker snapshot once. + const tabIds = new Set() + for (const paneKey of Object.keys(unreadAgentCompletionPanes)) { + if (!unreadAgentCompletionPanes[paneKey]) { + continue + } + const separatorIndex = paneKey.indexOf(':') + tabIds.add(separatorIndex === -1 ? paneKey : paneKey.slice(0, separatorIndex)) + } + unreadAgentCompletionTabIdsBySnapshot.set(unreadAgentCompletionPanes, tabIds) + return tabIds +} + /** Match pane-level unread completion markers to their owning terminal tab. */ export function hasUnreadAgentCompletionForTerminalTab( unreadAgentCompletionPanes: Record | undefined, tabId: string ): boolean { - for (const [paneKey, unread] of Object.entries(unreadAgentCompletionPanes ?? {})) { - // Why entries, not keys: the widened value type lets a cleared marker linger as `false`. - if (!unread) { - continue - } - // paneKey is `${tabId}:${leafId}` and tab ids never contain ":", so the - // prefix up to the first ":" is the owning tab id (see - // selectFloatingWorkspaceHasUnread). Prefix-match to keep legacy keys. - const separatorIndex = paneKey.indexOf(':') - const owningTabId = separatorIndex === -1 ? paneKey : paneKey.slice(0, separatorIndex) - if (owningTabId === tabId) { - return true - } - } - return false + return unreadAgentCompletionPanes + ? getUnreadAgentCompletionTabIds(unreadAgentCompletionPanes).has(tabId) + : false } /** Test-only: clear the memoized per-tab flag cache between cases. */ export function resetTerminalTabActivityFlagsCacheForTest(): void { flagsCache = null } + +/** Test-only: clear the unread marker snapshot index between cases. */ +export function resetUnreadAgentCompletionTabIdsCacheForTest(): void { + unreadAgentCompletionTabIdsBySnapshot = new WeakMap() +}