diff --git a/src/renderer/src/lib/agent-startup-delayed-delivery-perf.test.ts b/src/renderer/src/lib/agent-startup-delayed-delivery-perf.test.ts new file mode 100644 index 000000000..3ea4a168b --- /dev/null +++ b/src/renderer/src/lib/agent-startup-delayed-delivery-perf.test.ts @@ -0,0 +1,164 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { useAppStore } from '@/store' +import { + queuePendingAgentStartupDelivery, + resetAgentStartupDelayedDeliveryForTests +} from './agent-startup-delayed-delivery' + +const originalState = useAppStore.getState() +const LEAF_ID = '11111111-1111-4111-8111-111111111111' + +function seedPendingState(agentLaunchConfigByPaneKey: Record = {}): void { + useAppStore.setState({ + tabsByWorktree: { + 'wt-background': [ + { + id: 'tab-background', + ptyId: null, + worktreeId: 'wt-background', + title: 'Agent', + customTitle: null, + color: null, + sortOrder: 0, + createdAt: 1 + } + ] + }, + pendingStartupByTabId: { + 'tab-background': { launchToken: 'target-launch' } + }, + agentLaunchConfigByPaneKey, + ptyIdsByTabId: { 'tab-background': [] }, + terminalLayoutsByTabId: {} + } as never) +} + +function bindPendingPty(): void { + useAppStore.setState({ + agentLaunchConfigByPaneKey: { + [`tab-background:${LEAF_ID}`]: { + identity: { + tabId: 'tab-background', + leafId: LEAF_ID, + launchToken: 'target-launch' + } + } + }, + ptyIdsByTabId: { 'tab-background': ['pty-background'] }, + terminalLayoutsByTabId: { + 'tab-background': { + root: { type: 'leaf', leafId: LEAF_ID }, + activeLeafId: LEAF_ID, + expandedLeafId: null, + ptyIdsByLeafId: { [LEAF_ID]: 'pty-background' } + } + } + } as never) +} + +function countedLaunchConfigs(count: number): { + reads: { value: number } + record: Record +} { + const reads = { value: 0 } + const record: Record = {} + for (let index = 0; index < count; index += 1) { + Object.defineProperty(record, `other-pane-${index}`, { + enumerable: true, + get: () => { + reads.value += 1 + return { + identity: { + tabId: `other-tab-${index}`, + launchToken: `other-launch-${index}` + } + } + } + }) + } + return { reads, record } +} + +afterEach(() => { + resetAgentStartupDelayedDeliveryForTests() + useAppStore.setState(originalState, true) +}) + +describe('delayed agent startup subscription', () => { + it('does not rescan launch metadata for unrelated store updates', () => { + const launchConfigs = countedLaunchConfigs(500) + seedPendingState(launchConfigs.record) + + queuePendingAgentStartupDelivery({ + worktreeId: 'wt-background', + tabId: 'tab-background', + launchToken: 'target-launch', + startup: {} as never, + deliver: vi.fn() + }) + expect(launchConfigs.reads.value).toBe(500) + + launchConfigs.reads.value = 0 + for (let update = 0; update < 100; update += 1) { + useAppStore.setState({ activeView: update % 2 === 0 ? 'terminal' : 'settings' } as never) + } + + expect(launchConfigs.reads.value).toBe(0) + }) + + it('delivers when launch registration, PTY ownership, and layout binding arrive', () => { + seedPendingState() + const deliver = vi.fn().mockResolvedValue(undefined) + const startup = {} as never + queuePendingAgentStartupDelivery({ + worktreeId: 'wt-background', + tabId: 'tab-background', + launchToken: 'target-launch', + startup, + deliver + }) + + bindPendingPty() + + expect(deliver).toHaveBeenCalledWith('tab-background', 'pty-background', startup) + }) + + it('drops a delivery when its tab is removed before PTY binding', () => { + seedPendingState() + const deliver = vi.fn().mockResolvedValue(undefined) + queuePendingAgentStartupDelivery({ + worktreeId: 'wt-background', + tabId: 'tab-background', + launchToken: 'target-launch', + startup: {} as never, + deliver + }) + + useAppStore.setState({ tabsByWorktree: {} }) + seedPendingState() + bindPendingPty() + + expect(deliver).not.toHaveBeenCalled() + }) + + it('drops a delivery when a newer pending launch token replaces it', () => { + seedPendingState() + const deliver = vi.fn().mockResolvedValue(undefined) + queuePendingAgentStartupDelivery({ + worktreeId: 'wt-background', + tabId: 'tab-background', + launchToken: 'target-launch', + startup: {} as never, + deliver + }) + + useAppStore.setState({ + pendingStartupByTabId: { + 'tab-background': { launchToken: 'newer-launch' } + } + } as never) + bindPendingPty() + + expect(deliver).not.toHaveBeenCalled() + }) +}) diff --git a/src/renderer/src/lib/agent-startup-delayed-delivery.ts b/src/renderer/src/lib/agent-startup-delayed-delivery.ts index 5ca13944b..2e38fad41 100644 --- a/src/renderer/src/lib/agent-startup-delayed-delivery.ts +++ b/src/renderer/src/lib/agent-startup-delayed-delivery.ts @@ -90,7 +90,33 @@ function ensurePendingAgentStartupSubscription(): void { if (unsubscribePendingAgentStartupDeliveries) { return } - unsubscribePendingAgentStartupDeliveries = useAppStore.subscribe(() => { + const initial = useAppStore.getState() + // Capture the individual references so the gate stays allocation-free and + // remains correct even if a subscribe adapter reuses its state object. + let previousTabs = initial.tabsByWorktree + let previousPendingStartups = initial.pendingStartupByTabId + let previousLaunchConfigs = initial.agentLaunchConfigByPaneKey + let previousPtyIds = initial.ptyIdsByTabId + let previousLayouts = initial.terminalLayoutsByTabId + unsubscribePendingAgentStartupDeliveries = useAppStore.subscribe((state) => { + // Why: a background workspace can stay unmounted indefinitely. Only these + // five immutable slices can change delivery eligibility; unrelated title, + // status, focus, and usage ticks must not rescan every launch registration. + if ( + state.tabsByWorktree === previousTabs && + state.pendingStartupByTabId === previousPendingStartups && + state.agentLaunchConfigByPaneKey === previousLaunchConfigs && + state.ptyIdsByTabId === previousPtyIds && + state.terminalLayoutsByTabId === previousLayouts + ) { + return + } + // Update before flushing because delivery can synchronously write the store. + previousTabs = state.tabsByWorktree + previousPendingStartups = state.pendingStartupByTabId + previousLaunchConfigs = state.agentLaunchConfigByPaneKey + previousPtyIds = state.ptyIdsByTabId + previousLayouts = state.terminalLayoutsByTabId flushPendingAgentStartupDeliveries() }) }