diff --git a/src/main/ipc/crash-reporting.ts b/src/main/ipc/crash-reporting.ts index 4cdd4bdbbb4..5a00669176c 100644 --- a/src/main/ipc/crash-reporting.ts +++ b/src/main/ipc/crash-reporting.ts @@ -331,6 +331,7 @@ const COALESCED_RENDERER_BREADCRUMB_NAMES = new Set([ 'renderer_unhandled_rejection', 'terminal_park_verdict_churn', 'terminal_safe_fit_retry_exhausted', + 'terminal_tab_id_owned_by_multiple_worktrees', TERMINAL_WEBGL_DIAGNOSTIC_BREADCRUMB ]) const RENDERER_BREADCRUMB_COALESCE_MS = 30_000 @@ -343,9 +344,14 @@ const RENDERER_BREADCRUMB_COALESCE_MS = 30_000 // mounted pane within ~60ms. Windows crash F0BKR84AHEH lost 26-90% of its // 30-entry ring to two such bursts. `suppressedSinceLast` keeps the pane count // — the only signal these carry — in one slot. +// +// terminal_tab_id_owned_by_multiple_worktrees: its renderer-side guard is +// once-per-tab-id, not once-per-session, so one stale worktree map duplicates +// every tab id at once and emits a crumb per tab. const NAME_ONLY_COALESCED_BREADCRUMB_NAMES = new Set([ 'terminal_park_verdict_churn', - 'terminal_safe_fit_retry_exhausted' + 'terminal_safe_fit_retry_exhausted', + 'terminal_tab_id_owned_by_multiple_worktrees' ]) function rendererBreadcrumbCoalesceKey( diff --git a/src/renderer/src/components/terminal/active-terminal-repair-loop.react185.test.tsx b/src/renderer/src/components/terminal/active-terminal-repair-loop.react185.test.tsx index cedfa68d20a..5ee768436d4 100644 --- a/src/renderer/src/components/terminal/active-terminal-repair-loop.react185.test.tsx +++ b/src/renderer/src/components/terminal/active-terminal-repair-loop.react185.test.tsx @@ -115,6 +115,27 @@ describe('active-terminal repair effect cannot drive a React #185 update loop', expect(useAppStore.getState().activeTabIdByWorktree['wt-active']).toBe('t1') }) + it('does not reallocate activeTabIdByWorktree when the tab is already active', () => { + // Why: that map is a dependency of both the repair effect and the parked + // watcher sync, so a redundant activation must not re-run either. + useAppStore.setState({ + activeWorktreeId: 'wt-active', + activeTabType: 'terminal', + activeTabId: 't1', + activeTabIdByWorktree: {}, + tabsByWorktree: { 'wt-active': [terminalTab('t1', 'wt-active')] }, + unifiedTabsByWorktree: {} + }) + act(() => { + useAppStore.getState().setActiveTab('t1') + }) + const settled = useAppStore.getState().activeTabIdByWorktree + act(() => { + useAppStore.getState().setActiveTab('t1') + }) + expect(useAppStore.getState().activeTabIdByWorktree).toBe(settled) + }) + it('keeps bell attribution off a background worktree tab', () => { useAppStore.setState({ activeWorktreeId: 'wt-active', diff --git a/src/renderer/src/store/slices/active-tab-owner-worktree.ts b/src/renderer/src/store/slices/active-tab-owner-worktree.ts index 9ff0291797e..3d188ef9870 100644 --- a/src/renderer/src/store/slices/active-tab-owner-worktree.ts +++ b/src/renderer/src/store/slices/active-tab-owner-worktree.ts @@ -23,6 +23,9 @@ export function resolveActiveTabOwnerWorktreeId( ): string | null { let firstOwnerId: string | null = null let ownerCount = 0 + // Why tracked in-loop rather than re-read by key: `tabsByWorktree[activeWorktreeId]` + // resolves inherited members for ids like `toString`, and `?.some` would then throw. + let activeWorktreeOwnsTab = false for (const [worktreeId, tabs] of Object.entries(tabsByWorktree)) { if (!tabs.some((tab) => tab.id === tabId)) { continue @@ -31,6 +34,9 @@ export function resolveActiveTabOwnerWorktreeId( if (firstOwnerId === null) { firstOwnerId = worktreeId } + if (worktreeId === activeWorktreeId) { + activeWorktreeOwnsTab = true + } } // Why breadcrumb: no production origin for a duplicated tab id is known yet, @@ -39,17 +45,11 @@ export function resolveActiveTabOwnerWorktreeId( reportedDuplicateTabIds.add(tabId) recordRendererCrashBreadcrumb('terminal_tab_id_owned_by_multiple_worktrees', { ownerCount, - resolvedToActiveWorktree: Boolean( - activeWorktreeId && tabsByWorktree[activeWorktreeId]?.some((tab) => tab.id === tabId) - ) + resolvedToActiveWorktree: activeWorktreeOwnsTab }) } - if ( - ownerCount > 1 && - activeWorktreeId && - tabsByWorktree[activeWorktreeId]?.some((tab) => tab.id === tabId) - ) { + if (ownerCount > 1 && activeWorktreeId && activeWorktreeOwnsTab) { return activeWorktreeId } return firstOwnerId