diff --git a/src/renderer/src/runtime/web-session-close-intent.test.ts b/src/renderer/src/runtime/web-session-close-intent.test.ts index 185342b99da..59a449fbf01 100644 --- a/src/renderer/src/runtime/web-session-close-intent.test.ts +++ b/src/renderer/src/runtime/web-session-close-intent.test.ts @@ -1,5 +1,8 @@ import { afterEach, describe, expect, it } from 'vitest' import { + MAX_WEB_SESSION_CLOSE_INTENT_WORKTREES, + clearWebSessionCloseIntentsForWorktree, + getWebSessionCloseIntentCountsForTests, isWebSessionCloseIntentPending, reconcileWebSessionCloseIntents, recordWebSessionCloseIntent, @@ -31,6 +34,34 @@ describe('web session close intent', () => { expect(isWebSessionCloseIntentPending(WT, 'host-tab-1', 1000 + 11_000)).toBe(false) }) + it('prunes expired worktree intents when recording a newer intent', () => { + recordWebSessionCloseIntent('old-worktree', 'host-tab-old', 1000) + + recordWebSessionCloseIntent(WT, 'host-tab-1', 11_001) + + expect(getWebSessionCloseIntentCountsForTests()).toEqual({ worktrees: 1, tabs: 1 }) + expect(isWebSessionCloseIntentPending('old-worktree', 'host-tab-old', 11_001)).toBe(false) + expect(isWebSessionCloseIntentPending(WT, 'host-tab-1', 11_001)).toBe(true) + }) + + it('bounds worktree intents while retaining recently reused worktrees', () => { + recordWebSessionCloseIntent('keep', 'host-tab-keep', 1000) + for (let i = 0; i < MAX_WEB_SESSION_CLOSE_INTENT_WORKTREES - 1; i += 1) { + recordWebSessionCloseIntent(`worktree-${i}`, 'host-tab', 1000) + } + + expect(isWebSessionCloseIntentPending('keep', 'host-tab-keep', 1000)).toBe(true) + + recordWebSessionCloseIntent('worktree-new', 'host-tab-new', 1000) + + expect(getWebSessionCloseIntentCountsForTests()).toEqual({ + worktrees: MAX_WEB_SESSION_CLOSE_INTENT_WORKTREES, + tabs: MAX_WEB_SESSION_CLOSE_INTENT_WORKTREES + }) + expect(isWebSessionCloseIntentPending('worktree-0', 'host-tab', 1000)).toBe(false) + expect(isWebSessionCloseIntentPending('keep', 'host-tab-keep', 1000)).toBe(true) + }) + it('scopes intents per worktree', () => { recordWebSessionCloseIntent(WT, 'host-tab-1', 1000) expect(isWebSessionCloseIntentPending('other::/wt', 'host-tab-1', 1000)).toBe(false) @@ -40,4 +71,14 @@ describe('web session close intent', () => { recordWebSessionCloseIntent(WT, ' ', 1000) expect(isWebSessionCloseIntentPending(WT, '', 1000)).toBe(false) }) + + it('clears one worktree without touching another', () => { + recordWebSessionCloseIntent(WT, 'host-tab-1', 1000) + recordWebSessionCloseIntent('repo::/other', 'host-tab-2', 1000) + + clearWebSessionCloseIntentsForWorktree(WT) + + expect(isWebSessionCloseIntentPending(WT, 'host-tab-1', 1000)).toBe(false) + expect(isWebSessionCloseIntentPending('repo::/other', 'host-tab-2', 1000)).toBe(true) + }) }) diff --git a/src/renderer/src/runtime/web-session-close-intent.ts b/src/renderer/src/runtime/web-session-close-intent.ts index a8f6549c080..209ca38f32e 100644 --- a/src/renderer/src/runtime/web-session-close-intent.ts +++ b/src/renderer/src/runtime/web-session-close-intent.ts @@ -13,12 +13,45 @@ // permanently hiding a tab that legitimately still exists host-side. const CLOSE_INTENT_TTL_MS = 10_000 +export const MAX_WEB_SESSION_CLOSE_INTENT_WORKTREES = 256 type CloseIntent = { recordedAt: number } // worktreeId -> (hostTabId -> intent) const pendingCloseByWorktree = new Map>() +function deleteEmptyCloseIntentWorktree(worktreeId: string, byTab: Map): void { + if (byTab.size === 0) { + pendingCloseByWorktree.delete(worktreeId) + } +} + +function refreshCloseIntentWorktree(worktreeId: string, byTab: Map): void { + pendingCloseByWorktree.delete(worktreeId) + pendingCloseByWorktree.set(worktreeId, byTab) +} + +function pruneExpiredWebSessionCloseIntents(now: number): void { + for (const [worktreeId, byTab] of pendingCloseByWorktree) { + for (const [hostTabId, intent] of byTab) { + if (now - intent.recordedAt > CLOSE_INTENT_TTL_MS) { + byTab.delete(hostTabId) + } + } + deleteEmptyCloseIntentWorktree(worktreeId, byTab) + } +} + +function trimWebSessionCloseIntentWorktrees(): void { + while (pendingCloseByWorktree.size > MAX_WEB_SESSION_CLOSE_INTENT_WORKTREES) { + const oldestWorktreeId = pendingCloseByWorktree.keys().next().value + if (oldestWorktreeId === undefined) { + break + } + pendingCloseByWorktree.delete(oldestWorktreeId) + } +} + export function recordWebSessionCloseIntent( worktreeId: string, hostTabId: string, @@ -28,12 +61,16 @@ export function recordWebSessionCloseIntent( if (!worktreeId || !trimmed) { return } + // Why: close intents already expire; prune all worktrees on new writes so + // removed worktrees that never reconcile again do not retain stale tab ids. + pruneExpiredWebSessionCloseIntents(now) let byTab = pendingCloseByWorktree.get(worktreeId) if (!byTab) { byTab = new Map() - pendingCloseByWorktree.set(worktreeId, byTab) } + refreshCloseIntentWorktree(worktreeId, byTab) byTab.set(trimmed, { recordedAt: now }) + trimWebSessionCloseIntentWorktrees() } /** @@ -47,17 +84,19 @@ export function isWebSessionCloseIntentPending( now: number ): boolean { const byTab = pendingCloseByWorktree.get(worktreeId) - const intent = byTab?.get(hostTabId) + if (!byTab) { + return false + } + const intent = byTab.get(hostTabId) if (!intent) { return false } if (now - intent.recordedAt > CLOSE_INTENT_TTL_MS) { - byTab!.delete(hostTabId) - if (byTab!.size === 0) { - pendingCloseByWorktree.delete(worktreeId) - } + byTab.delete(hostTabId) + deleteEmptyCloseIntentWorktree(worktreeId, byTab) return false } + refreshCloseIntentWorktree(worktreeId, byTab) return true } @@ -82,11 +121,25 @@ export function reconcileWebSessionCloseIntents( for (const hostTabId of confirmed) { byTab.delete(hostTabId) } - if (byTab.size === 0) { - pendingCloseByWorktree.delete(worktreeId) + if (byTab.size > 0) { + refreshCloseIntentWorktree(worktreeId, byTab) + return } + pendingCloseByWorktree.delete(worktreeId) +} + +export function clearWebSessionCloseIntentsForWorktree(worktreeId: string): void { + pendingCloseByWorktree.delete(worktreeId) } export function resetWebSessionCloseIntentForTests(): void { pendingCloseByWorktree.clear() } + +export function getWebSessionCloseIntentCountsForTests(): { worktrees: number; tabs: number } { + let tabs = 0 + for (const byTab of pendingCloseByWorktree.values()) { + tabs += byTab.size + } + return { worktrees: pendingCloseByWorktree.size, tabs } +} diff --git a/src/renderer/src/runtime/web-session-focus-intent.test.ts b/src/renderer/src/runtime/web-session-focus-intent.test.ts new file mode 100644 index 00000000000..c3044814ee1 --- /dev/null +++ b/src/renderer/src/runtime/web-session-focus-intent.test.ts @@ -0,0 +1,48 @@ +import { afterEach, describe, expect, it } from 'vitest' +import { + MAX_WEB_SESSION_FOCUS_INTENT_WORKTREES, + clearWebSessionFocusIntent, + getWebSessionFocusIntentCountForTests, + peekWebSessionFocusIntent, + recordWebSessionFocusIntent, + resetWebSessionFocusIntentForTests +} from './web-session-focus-intent' + +const WT = 'repo::/wt' + +afterEach(() => resetWebSessionFocusIntentForTests()) + +describe('web session focus intent', () => { + it('expires a never-confirmed focus intent', () => { + recordWebSessionFocusIntent(WT, 'host-tab-1', 1000) + + expect(peekWebSessionFocusIntent(WT, 1000)).toBe('host-tab-1') + expect(peekWebSessionFocusIntent(WT, 61_001)).toBeNull() + expect(getWebSessionFocusIntentCountForTests()).toBe(0) + }) + + it('bounds worktree intents while retaining recently reused worktrees', () => { + recordWebSessionFocusIntent('keep', 'host-tab-keep', 1000) + for (let i = 0; i < MAX_WEB_SESSION_FOCUS_INTENT_WORKTREES - 1; i += 1) { + recordWebSessionFocusIntent(`worktree-${i}`, `host-tab-${i}`, 1000) + } + + expect(peekWebSessionFocusIntent('keep', 1000)).toBe('host-tab-keep') + + recordWebSessionFocusIntent('worktree-new', 'host-tab-new', 1000) + + expect(getWebSessionFocusIntentCountForTests()).toBe(MAX_WEB_SESSION_FOCUS_INTENT_WORKTREES) + expect(peekWebSessionFocusIntent('worktree-0', 1000)).toBeNull() + expect(peekWebSessionFocusIntent('keep', 1000)).toBe('host-tab-keep') + }) + + it('clears one worktree without touching another', () => { + recordWebSessionFocusIntent(WT, 'host-tab-1', 1000) + recordWebSessionFocusIntent('repo::/other', 'host-tab-2', 1000) + + clearWebSessionFocusIntent(WT) + + expect(peekWebSessionFocusIntent(WT, 1000)).toBeNull() + expect(peekWebSessionFocusIntent('repo::/other', 1000)).toBe('host-tab-2') + }) +}) diff --git a/src/renderer/src/runtime/web-session-focus-intent.ts b/src/renderer/src/runtime/web-session-focus-intent.ts index a5c9997cb4f..b6b60faa790 100644 --- a/src/renderer/src/runtime/web-session-focus-intent.ts +++ b/src/renderer/src/runtime/web-session-focus-intent.ts @@ -9,18 +9,60 @@ // The intent persists until a snapshot matches it (surviving racing/duplicate // snapshots, unlike a transient per-snapshot flag). -const pendingFocusByWorktree = new Map() +// Why: a create/activate echo should arrive quickly; if a removed worktree never +// publishes again, the focus intent needs a bounded lifetime. +const FOCUS_INTENT_TTL_MS = 60_000 +export const MAX_WEB_SESSION_FOCUS_INTENT_WORKTREES = 256 -export function recordWebSessionFocusIntent(worktreeId: string, hostTabId: string): void { +type FocusIntent = { hostTabId: string; recordedAt: number } + +const pendingFocusByWorktree = new Map() + +function pruneExpiredWebSessionFocusIntents(now: number): void { + for (const [worktreeId, intent] of pendingFocusByWorktree) { + if (now - intent.recordedAt > FOCUS_INTENT_TTL_MS) { + pendingFocusByWorktree.delete(worktreeId) + } + } +} + +function trimWebSessionFocusIntents(): void { + while (pendingFocusByWorktree.size > MAX_WEB_SESSION_FOCUS_INTENT_WORKTREES) { + const oldestWorktreeId = pendingFocusByWorktree.keys().next().value + if (oldestWorktreeId === undefined) { + break + } + pendingFocusByWorktree.delete(oldestWorktreeId) + } +} + +export function recordWebSessionFocusIntent( + worktreeId: string, + hostTabId: string, + now = Date.now() +): void { const trimmed = hostTabId.trim() if (!worktreeId || !trimmed) { return } - pendingFocusByWorktree.set(worktreeId, trimmed) + pruneExpiredWebSessionFocusIntents(now) + pendingFocusByWorktree.delete(worktreeId) + pendingFocusByWorktree.set(worktreeId, { hostTabId: trimmed, recordedAt: now }) + trimWebSessionFocusIntents() } -export function peekWebSessionFocusIntent(worktreeId: string): string | null { - return pendingFocusByWorktree.get(worktreeId) ?? null +export function peekWebSessionFocusIntent(worktreeId: string, now = Date.now()): string | null { + const intent = pendingFocusByWorktree.get(worktreeId) + if (!intent) { + return null + } + if (now - intent.recordedAt > FOCUS_INTENT_TTL_MS) { + pendingFocusByWorktree.delete(worktreeId) + return null + } + pendingFocusByWorktree.delete(worktreeId) + pendingFocusByWorktree.set(worktreeId, intent) + return intent.hostTabId } export function clearWebSessionFocusIntent(worktreeId: string): void { @@ -30,3 +72,7 @@ export function clearWebSessionFocusIntent(worktreeId: string): void { export function resetWebSessionFocusIntentForTests(): void { pendingFocusByWorktree.clear() } + +export function getWebSessionFocusIntentCountForTests(): number { + return pendingFocusByWorktree.size +} diff --git a/src/renderer/src/runtime/web-session-reorder-intent.test.ts b/src/renderer/src/runtime/web-session-reorder-intent.test.ts new file mode 100644 index 00000000000..a59fd570d76 --- /dev/null +++ b/src/renderer/src/runtime/web-session-reorder-intent.test.ts @@ -0,0 +1,71 @@ +import { afterEach, describe, expect, it } from 'vitest' +import { + MAX_WEB_SESSION_REORDER_INTENT_WORKTREES, + clearWebSessionReorderIntentsForWorktree, + getWebSessionReorderIntentCountsForTests, + recordWebSessionReorderIntent, + resetWebSessionReorderIntentForTests, + resolveWebSessionReorderedOrder +} from './web-session-reorder-intent' + +const WT = 'repo::/wt' + +afterEach(() => resetWebSessionReorderIntentForTests()) + +describe('web session reorder intent', () => { + it('prunes expired worktree intents when recording a newer intent', () => { + recordWebSessionReorderIntent('old-worktree', 'group-1', ['tab-b', 'tab-a'], 1000) + + recordWebSessionReorderIntent(WT, 'group-1', ['tab-c', 'tab-d'], 11_001) + + expect(getWebSessionReorderIntentCountsForTests()).toEqual({ worktrees: 1, groups: 1 }) + expect( + resolveWebSessionReorderedOrder('old-worktree', 'group-1', ['tab-a', 'tab-b'], 11_001) + ).toEqual(['tab-a', 'tab-b']) + expect(resolveWebSessionReorderedOrder(WT, 'group-1', ['tab-d', 'tab-c'], 11_001)).toEqual([ + 'tab-c', + 'tab-d' + ]) + }) + + it('bounds worktree intents while retaining recently reused worktrees', () => { + recordWebSessionReorderIntent('keep', 'group-1', ['tab-b', 'tab-a'], 1000) + for (let i = 0; i < MAX_WEB_SESSION_REORDER_INTENT_WORKTREES - 1; i += 1) { + recordWebSessionReorderIntent(`worktree-${i}`, 'group-1', ['tab-b', 'tab-a'], 1000) + } + + expect(resolveWebSessionReorderedOrder('keep', 'group-1', ['tab-a', 'tab-b'], 1000)).toEqual([ + 'tab-b', + 'tab-a' + ]) + + recordWebSessionReorderIntent('worktree-new', 'group-1', ['tab-b', 'tab-a'], 1000) + + expect(getWebSessionReorderIntentCountsForTests()).toEqual({ + worktrees: MAX_WEB_SESSION_REORDER_INTENT_WORKTREES, + groups: MAX_WEB_SESSION_REORDER_INTENT_WORKTREES + }) + expect( + resolveWebSessionReorderedOrder('worktree-0', 'group-1', ['tab-a', 'tab-b'], 1000) + ).toEqual(['tab-a', 'tab-b']) + expect(resolveWebSessionReorderedOrder('keep', 'group-1', ['tab-a', 'tab-b'], 1000)).toEqual([ + 'tab-b', + 'tab-a' + ]) + }) + + it('clears one worktree without touching another', () => { + recordWebSessionReorderIntent(WT, 'group-1', ['tab-b', 'tab-a'], 1000) + recordWebSessionReorderIntent('repo::/other', 'group-1', ['tab-d', 'tab-c'], 1000) + + clearWebSessionReorderIntentsForWorktree(WT) + + expect(resolveWebSessionReorderedOrder(WT, 'group-1', ['tab-a', 'tab-b'], 1000)).toEqual([ + 'tab-a', + 'tab-b' + ]) + expect( + resolveWebSessionReorderedOrder('repo::/other', 'group-1', ['tab-c', 'tab-d'], 1000) + ).toEqual(['tab-d', 'tab-c']) + }) +}) diff --git a/src/renderer/src/runtime/web-session-reorder-intent.ts b/src/renderer/src/runtime/web-session-reorder-intent.ts index 0aabec8a623..ec30f6afb9d 100644 --- a/src/renderer/src/runtime/web-session-reorder-intent.ts +++ b/src/renderer/src/runtime/web-session-reorder-intent.ts @@ -13,12 +13,51 @@ // rejected RPC) from pinning a stale order forever. const REORDER_INTENT_TTL_MS = 10_000 +export const MAX_WEB_SESSION_REORDER_INTENT_WORKTREES = 256 type ReorderIntent = { order: string[]; recordedAt: number } // worktreeId -> (groupId -> intent) const pendingReorderByWorktree = new Map>() +function deleteEmptyReorderIntentWorktree( + worktreeId: string, + byGroup: Map +): void { + if (byGroup.size === 0) { + pendingReorderByWorktree.delete(worktreeId) + } +} + +function refreshReorderIntentWorktree( + worktreeId: string, + byGroup: Map +): void { + pendingReorderByWorktree.delete(worktreeId) + pendingReorderByWorktree.set(worktreeId, byGroup) +} + +function pruneExpiredWebSessionReorderIntents(now: number): void { + for (const [worktreeId, byGroup] of pendingReorderByWorktree) { + for (const [groupId, intent] of byGroup) { + if (now - intent.recordedAt > REORDER_INTENT_TTL_MS) { + byGroup.delete(groupId) + } + } + deleteEmptyReorderIntentWorktree(worktreeId, byGroup) + } +} + +function trimWebSessionReorderIntentWorktrees(): void { + while (pendingReorderByWorktree.size > MAX_WEB_SESSION_REORDER_INTENT_WORKTREES) { + const oldestWorktreeId = pendingReorderByWorktree.keys().next().value + if (oldestWorktreeId === undefined) { + break + } + pendingReorderByWorktree.delete(oldestWorktreeId) + } +} + function sameMembership(a: readonly string[], b: readonly string[]): boolean { if (a.length !== b.length) { return false @@ -40,12 +79,16 @@ export function recordWebSessionReorderIntent( if (!worktreeId || !groupId || order.length === 0) { return } + // Why: reorder intents already expire; prune all worktrees on new writes so + // removed worktrees that never reconcile again do not retain stale orders. + pruneExpiredWebSessionReorderIntents(now) let byGroup = pendingReorderByWorktree.get(worktreeId) if (!byGroup) { byGroup = new Map() - pendingReorderByWorktree.set(worktreeId, byGroup) } + refreshReorderIntentWorktree(worktreeId, byGroup) byGroup.set(groupId, { order: [...order], recordedAt: now }) + trimWebSessionReorderIntentWorktrees() } /** @@ -62,15 +105,16 @@ export function resolveWebSessionReorderedOrder( now: number ): string[] { const byGroup = pendingReorderByWorktree.get(worktreeId) - const intent = byGroup?.get(groupId) + if (!byGroup) { + return hostOrder + } + const intent = byGroup.get(groupId) if (!intent) { return hostOrder } const clear = (): void => { - byGroup!.delete(groupId) - if (byGroup!.size === 0) { - pendingReorderByWorktree.delete(worktreeId) - } + byGroup.delete(groupId) + deleteEmptyReorderIntentWorktree(worktreeId, byGroup) } if (now - intent.recordedAt > REORDER_INTENT_TTL_MS) { clear() @@ -87,6 +131,7 @@ export function resolveWebSessionReorderedOrder( clear() return hostOrder } + refreshReorderIntentWorktree(worktreeId, byGroup) return [...intent.order] } @@ -97,3 +142,11 @@ export function clearWebSessionReorderIntentsForWorktree(worktreeId: string): vo export function resetWebSessionReorderIntentForTests(): void { pendingReorderByWorktree.clear() } + +export function getWebSessionReorderIntentCountsForTests(): { worktrees: number; groups: number } { + let groups = 0 + for (const byGroup of pendingReorderByWorktree.values()) { + groups += byGroup.size + } + return { worktrees: pendingReorderByWorktree.size, groups } +} diff --git a/src/renderer/src/runtime/web-session-tabs-sync.test.ts b/src/renderer/src/runtime/web-session-tabs-sync.test.ts index 7090ac68bc1..99af226071d 100644 --- a/src/renderer/src/runtime/web-session-tabs-sync.test.ts +++ b/src/renderer/src/runtime/web-session-tabs-sync.test.ts @@ -7,16 +7,19 @@ import { makePaneKey } from '../../../shared/stable-pane-id' import { toWebTerminalSurfaceTabId } from '../../../shared/terminal-surface-id' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants' import { + peekWebSessionFocusIntent, recordWebSessionFocusIntent, resetWebSessionFocusIntentForTests } from './web-session-focus-intent' import { + isWebSessionCloseIntentPending, recordWebSessionCloseIntent, resetWebSessionCloseIntentForTests } from './web-session-close-intent' import { recordWebSessionReorderIntent, - resetWebSessionReorderIntentForTests + resetWebSessionReorderIntentForTests, + resolveWebSessionReorderedOrder } from './web-session-reorder-intent' import type { BrowserPage, BrowserWorkspace, Tab, TerminalTab } from '../../../shared/types' import type { OpenFile } from '../store/slices/editor' @@ -582,6 +585,9 @@ describe('applyWebSessionTabsSnapshot', () => { freshness: 1, hostMappings: 1 }) + recordWebSessionFocusIntent(WT, 'host-browser-page', NOW) + recordWebSessionCloseIntent(WT, 'host-browser-unified', NOW) + recordWebSessionReorderIntent(WT, 'host-group-1', ['tab-b', 'tab-a'], NOW) applyFreshWebSessionTabsSnapshot( afterHostSnapshot, @@ -603,6 +609,11 @@ describe('applyWebSessionTabsSnapshot', () => { freshness: 0, hostMappings: 0 }) + expect(peekWebSessionFocusIntent(WT, NOW + 1)).toBeNull() + expect(isWebSessionCloseIntentPending(WT, 'host-browser-unified', NOW + 1)).toBe(false) + expect( + resolveWebSessionReorderedOrder(WT, 'host-group-1', ['tab-a', 'tab-b'], NOW + 1) + ).toEqual(['tab-a', 'tab-b']) }) it('clears web session tracking maps for one runtime environment on teardown', () => { diff --git a/src/renderer/src/runtime/web-session-tabs-sync.ts b/src/renderer/src/runtime/web-session-tabs-sync.ts index e28072d5357..1766c0f23e0 100644 --- a/src/renderer/src/runtime/web-session-tabs-sync.ts +++ b/src/renderer/src/runtime/web-session-tabs-sync.ts @@ -53,6 +53,7 @@ import { resolveTerminalLayoutRoot } from './remote-terminal-layout-resolution' import { toRuntimeWorktreeSelector } from './runtime-worktree-selector' import { clearWebSessionFocusIntent, peekWebSessionFocusIntent } from './web-session-focus-intent' import { + clearWebSessionCloseIntentsForWorktree, isWebSessionCloseIntentPending, reconcileWebSessionCloseIntents } from './web-session-close-intent' @@ -314,6 +315,8 @@ function clearWebSessionTabsTrackingForWorktree(environmentId: string, worktreeI latestSessionTabsSnapshotByWorktree.delete(key) lastHostTerminalTabCountByWorktree.delete(key) clearWebRuntimeWakeTerminalRespawnForWorktree(worktreeId) + clearWebSessionFocusIntent(worktreeId) + clearWebSessionCloseIntentsForWorktree(worktreeId) clearWebSessionReorderIntentsForWorktree(worktreeId) const keyPrefix = `${environmentId}:${worktreeId}:` for (const key of hostSessionTabIdByLocalKey.keys()) { @@ -1655,7 +1658,7 @@ export function applyWebSessionTabsSnapshot( // An unsolicited server-active (e.g. an agent "thinking" echo) must not steal // focus — that's the #5435 contract. Intent matches by the host active tab id // (terminal session id, or browserPageId for browser tabs); consume it once. - const focusIntentHostTabId = peekWebSessionFocusIntent(worktreeId) + const focusIntentHostTabId = peekWebSessionFocusIntent(worktreeId, now) const honorSnapshotActiveFocus = focusIntentHostTabId !== null && snapshot.activeTabId !== null &&