perf(renderer): keep remote snapshot batches linear (#13676)

* perf(renderer): keep remote snapshot batches linear

* test(renderer): cover the prefix-colliding sibling worktree

The sibling-mapping test used a worktree id that shares no prefix with WT,
so it passed against the prefix-scan it was meant to pin. Use an id prefixed
by WT's and drain the new mapping index in the leak counters.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-08-10 20:55:40 -07:00
committed by GitHub
co-authored by Orca
parent 0acf04a985
commit a65e0c5c97
2 changed files with 528 additions and 79 deletions
@@ -643,7 +643,8 @@ describe('applyWebSessionTabsSnapshot', () => {
expect(_getWebSessionTabsTrackingCountsForTest()).toEqual({
freshness: 1,
hostMappings: 1
hostMappings: 1,
hostMappingWorktrees: 1
})
applyFreshWebSessionTabsSnapshot(
@@ -664,7 +665,8 @@ describe('applyWebSessionTabsSnapshot', () => {
expect(_getWebSessionTabsTrackingCountsForTest()).toEqual({
freshness: 0,
hostMappings: 0
hostMappings: 0,
hostMappingWorktrees: 0
})
})
@@ -720,17 +722,89 @@ describe('applyWebSessionTabsSnapshot', () => {
expect(_getWebSessionTabsTrackingCountsForTest()).toEqual({
freshness: 2,
hostMappings: 2
hostMappings: 2,
hostMappingWorktrees: 2
})
clearWebSessionTabsTrackingForEnvironment(ENV)
expect(_getWebSessionTabsTrackingCountsForTest()).toEqual({
freshness: 1,
hostMappings: 1
hostMappings: 1,
hostMappingWorktrees: 1
})
})
it('clears one worktree mapping without dropping a sibling in the same environment', () => {
// Why: POSIX paths may contain ':', so this sibling's worktree id is prefixed by WT's — the case a prefix scan wiped.
const secondWorktree = `${WT}:2`
const terminalSnapshot = makeSnapshot([
{
type: 'terminal',
id: HOST_SURFACE_ID,
title: 'host shell',
parentTabId: 'host-tab-1',
leafId: LEAF_ID,
isActive: true,
status: 'ready',
terminal: 'terminal-1'
}
])
const secondSnapshot = makeSnapshot(
[
{
type: 'terminal',
id: `host-tab-2::${SECOND_LEAF_ID}`,
title: 'second shell',
parentTabId: 'host-tab-2',
leafId: SECOND_LEAF_ID,
isActive: true,
status: 'ready',
terminal: 'terminal-2'
}
],
{ worktree: secondWorktree }
)
applyFreshWebSessionTabsSnapshot(makeState(), terminalSnapshot, ENV, NOW)
applyFreshWebSessionTabsSnapshot(makeState(), secondSnapshot, ENV, NOW)
applyFreshWebSessionTabsSnapshot(
makeState(),
{
...makeSnapshot([], {
publicationEpoch: 'removed-epoch',
snapshotVersion: 0,
activeGroupId: null,
activeTabId: null,
activeTabType: null
}),
removed: true
} as RuntimeMobileSessionTabsResult,
ENV,
NOW + 1
)
expect(_getWebSessionTabsTrackingCountsForTest()).toEqual({
freshness: 1,
hostMappings: 1,
hostMappingWorktrees: 1
})
expect(
resolveHostSessionTabIdForWebSessionTab(makeState(), {
environmentId: ENV,
worktreeId: WT,
tabId: toWebTerminalSurfaceTabId('host-tab-1')
})
).toBeNull()
expect(
resolveHostSessionTabIdForWebSessionTab(makeState(), {
environmentId: ENV,
worktreeId: secondWorktree,
tabId: toWebTerminalSurfaceTabId('host-tab-2')
})
).toBe('host-tab-2')
})
it('keeps a provisional Claude tab when the host Claude surface is unrelated', () => {
const staleLocalAgentTab: TerminalTab = {
id: 'local-agent-tab',
@@ -2273,6 +2347,157 @@ describe('applyWebSessionTabsSnapshot', () => {
)
})
it('keeps an empty snapshot batch as an identity no-op', () => {
const state = makeState()
expect(applyWebSessionTabsSnapshots(state, [], ENV, NOW)).toBe(state)
})
it('matches sequential reconciliation across duplicate-worktree mixed snapshots', () => {
const secondWorktree = 'repo::/other-worktree'
const snapshots: RuntimeMobileSessionTabsResult[] = [
makeSnapshot([
{
type: 'terminal',
id: HOST_SURFACE_ID,
title: 'first agent',
parentTabId: 'host-tab-1',
leafId: LEAF_ID,
isActive: true,
status: 'ready',
terminal: 'terminal-1',
agentStatus: {
state: 'working',
prompt: 'first task',
updatedAt: NOW,
stateStartedAt: NOW,
agentType: 'codex',
paneKey: HOST_SURFACE_ID,
stateHistory: []
}
}
]),
makeSnapshot(
[
{
type: 'browser',
id: 'host-browser-unified',
title: 'Example Domain',
browserWorkspaceId: 'host-browser-workspace',
browserPageId: 'host-browser-page',
url: 'https://example.com/',
loading: false,
canGoBack: false,
canGoForward: false,
isActive: true
}
],
{
worktree: secondWorktree,
activeGroupId: 'host-group-2',
activeTabId: 'host-browser-unified',
activeTabType: 'browser'
}
),
makeSnapshot([], {
snapshotVersion: 2,
activeGroupId: null,
activeTabId: null,
activeTabType: null
}),
makeSnapshot(
[
{
type: 'terminal',
id: `host-tab-3::${THIRD_LEAF_ID}`,
title: 'replacement agent',
parentTabId: 'host-tab-3',
leafId: THIRD_LEAF_ID,
isActive: true,
status: 'ready',
terminal: 'terminal-3',
agentStatus: {
state: 'waiting',
prompt: 'replacement question',
updatedAt: NOW + 1,
stateStartedAt: NOW + 1,
agentType: 'codex',
paneKey: `host-tab-3::${THIRD_LEAF_ID}`,
stateHistory: []
}
}
],
{ snapshotVersion: 3 }
),
makeSnapshot(
[
{
type: 'markdown',
id: 'host-readme-unified',
title: 'README.md',
filePath: '/repo/README.md',
relativePath: 'README.md',
language: 'markdown',
mode: 'edit',
isDirty: false,
isActive: true,
sourceFileId: '/repo/README.md',
sourceFilePath: '/repo/README.md',
sourceRelativePath: 'README.md',
documentVersion: 'file:/repo/README.md'
}
],
{
worktree: secondWorktree,
snapshotVersion: 2,
activeGroupId: 'host-group-2',
activeTabId: 'host-readme-unified',
activeTabType: 'markdown'
}
)
]
const provisionalTab: TerminalTab = {
id: 'host-tab-1',
ptyId: null,
worktreeId: WT,
title: 'Codex',
defaultTitle: 'Codex',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: NOW,
launchAgent: 'codex'
}
const initial = makeState({
activeWorktreeId: null,
tabsByWorktree: { [WT]: [provisionalTab] },
pendingStartupByTabId: {
[provisionalTab.id]: { command: 'codex' }
},
automaticAgentResumeClaimsByTabId: {
[provisionalTab.id]: {
worktreeId: WT,
launchAgent: 'codex',
providerSession: { key: 'session_id', id: 'session-a' }
}
}
})
const initialCopy = structuredClone(initial)
let sequential = initial
for (const snapshot of snapshots) {
const patch = applyWebSessionTabsSnapshot(sequential, snapshot, ENV, NOW)
if (patch !== sequential) {
sequential = { ...sequential, ...patch }
}
}
resetWebSessionTabsSnapshotFreshnessForTests()
const batchPatch = applyWebSessionTabsSnapshots(initial, snapshots, ENV, NOW)
const batched = { ...initial, ...batchPatch }
expect(batched).toEqual(sequential)
expect(initial).toEqual(initialCopy)
})
it('replaces temporary web-created tabs once the host publishes the same PTY', () => {
const localTab: TerminalTab = {
id: 'local-web-tab',
+299 -75
View File
@@ -105,6 +105,10 @@ const latestSessionTabsSnapshotByWorktree = new Map<string, SnapshotFreshness>()
const replayableSessionTabsSnapshotByWorktree = new Map<string, SnapshotFreshness>()
const lastHostTerminalTabCountByWorktree = new Map<string, number>()
const hostSessionTabIdByLocalKey = new Map<string, string>()
const hostSessionTabMappingKeysByEnvironmentAndWorktree = new Map<
string,
Map<string, Set<string>>
>()
type TerminalSurface = RuntimeMobileSessionTerminalClientTab
type ReadyTerminalSurface = RuntimeMobileSessionTerminalClientTab & { status: 'ready' }
@@ -165,6 +169,33 @@ export type WebSessionTabsSyncState = Pick<
> &
Partial<Pick<AppState, 'automaticAgentResumeClaimsByTabId' | 'pendingStartupByTabId'>>
type WebSessionTabsBatchRecordKey =
| 'activeBrowserTabIdByWorktree'
| 'activeFileIdByWorktree'
| 'activeGroupIdByWorktree'
| 'activeTabIdByWorktree'
| 'activeTabTypeByWorktree'
| 'agentStatusByPaneKey'
| 'automaticAgentResumeClaimsByTabId'
| 'browserCertificateFailuresByPageId'
| 'browserPagesByWorkspace'
| 'browserTabsByWorktree'
| 'groupsByWorktree'
| 'layoutByWorktree'
| 'pendingStartupByTabId'
| 'ptyIdsByTabId'
| 'remoteBrowserPageHandlesByPageId'
| 'tabBarOrderByWorktree'
| 'tabsByWorktree'
| 'terminalLayoutsByTabId'
| 'unifiedTabsByWorktree'
| 'unreadTerminalTabs'
type WebSessionTabsBatchContext = {
agentPaneKeysByTabId: Map<string, Set<string>> | null
changedRecords: Set<WebSessionTabsBatchRecordKey>
}
function isSessionTabsListAllResult(value: unknown): value is SessionTabsListAllResult {
return (
Boolean(value) &&
@@ -331,15 +362,23 @@ export function resetWebSessionTabsSnapshotFreshnessForTests(): void {
replayableSessionTabsSnapshotByWorktree.clear()
lastHostTerminalTabCountByWorktree.clear()
hostSessionTabIdByLocalKey.clear()
hostSessionTabMappingKeysByEnvironmentAndWorktree.clear()
}
export function _getWebSessionTabsTrackingCountsForTest(): {
freshness: number
hostMappings: number
hostMappingWorktrees: number
} {
let hostMappingWorktrees = 0
for (const mappingKeysByWorktree of hostSessionTabMappingKeysByEnvironmentAndWorktree.values()) {
hostMappingWorktrees += mappingKeysByWorktree.size
}
return {
freshness: latestSessionTabsSnapshotByWorktree.size,
hostMappings: hostSessionTabIdByLocalKey.size
hostMappings: hostSessionTabIdByLocalKey.size,
// Why: the mapping index is a parallel structure, so leak tests must see it drain alongside the flat map.
hostMappingWorktrees
}
}
@@ -352,12 +391,7 @@ function clearWebSessionTabsTrackingForWorktree(environmentId: string, worktreeI
clearWebSessionReorderIntentsForWorktree({ environmentId }, worktreeId)
clearWebSessionCloseIntentsForWorktree({ environmentId }, worktreeId)
clearWebAgentSessionHandoffsForWorktree(environmentId, worktreeId)
const keyPrefix = `${environmentId}:${worktreeId}:`
for (const key of hostSessionTabIdByLocalKey.keys()) {
if (key.startsWith(keyPrefix)) {
hostSessionTabIdByLocalKey.delete(key)
}
}
clearHostSessionTabIdMappings(environmentId, worktreeId)
}
export function clearWebSessionTabsTrackingForEnvironment(environmentId: string): void {
@@ -381,10 +415,15 @@ export function clearWebSessionTabsTrackingForEnvironment(environmentId: string)
lastHostTerminalTabCountByWorktree.delete(key)
}
}
for (const key of hostSessionTabIdByLocalKey.keys()) {
if (key.startsWith(keyPrefix)) {
hostSessionTabIdByLocalKey.delete(key)
const mappingKeysByWorktree =
hostSessionTabMappingKeysByEnvironmentAndWorktree.get(trimmedEnvironmentId)
if (mappingKeysByWorktree) {
for (const mappingKeys of mappingKeysByWorktree.values()) {
for (const mappingKey of mappingKeys) {
hostSessionTabIdByLocalKey.delete(mappingKey)
}
}
hostSessionTabMappingKeysByEnvironmentAndWorktree.delete(trimmedEnvironmentId)
}
clearWebAgentSessionHandoffsForEnvironment(trimmedEnvironmentId)
clearAllWebRuntimeWakeTerminalRespawn()
@@ -398,6 +437,35 @@ function hostSessionTabMappingKey(args: {
return `${args.environmentId}:${args.worktreeId}:${args.tabId}`
}
function clearHostSessionTabIdMappings(environmentId: string, worktreeId: string): void {
const mappingKeysByWorktree = hostSessionTabMappingKeysByEnvironmentAndWorktree.get(environmentId)
const mappingKeys = mappingKeysByWorktree?.get(worktreeId)
if (!mappingKeys) {
return
}
for (const mappingKey of mappingKeys) {
hostSessionTabIdByLocalKey.delete(mappingKey)
}
mappingKeysByWorktree?.delete(worktreeId)
if (mappingKeysByWorktree?.size === 0) {
hostSessionTabMappingKeysByEnvironmentAndWorktree.delete(environmentId)
}
}
function setHostSessionTabIdMapping(
args: { environmentId: string; worktreeId: string; tabId: string },
hostTabId: string
): void {
const mappingKey = hostSessionTabMappingKey(args)
hostSessionTabIdByLocalKey.set(mappingKey, hostTabId)
const mappingKeysByWorktree =
hostSessionTabMappingKeysByEnvironmentAndWorktree.get(args.environmentId) ?? new Map()
const mappingKeys = mappingKeysByWorktree.get(args.worktreeId) ?? new Set<string>()
mappingKeys.add(mappingKey)
mappingKeysByWorktree.set(args.worktreeId, mappingKeys)
hostSessionTabMappingKeysByEnvironmentAndWorktree.set(args.environmentId, mappingKeysByWorktree)
}
export function resolveHostSessionTabIdForWebSessionTab(
_state: WebSessionTabsSyncState,
args: {
@@ -735,13 +803,60 @@ function isFencedClientAgentStatus(
return isClientOwnedAgentStatus(paneKey, existing) && isAgentStatusFresh(existing, now)
}
function batchAgentPaneKeysForTabs(
state: WebSessionTabsSyncState,
tabIds: ReadonlySet<string>,
batchContext?: WebSessionTabsBatchContext
): string[] {
if (!batchContext) {
return Object.keys(state.agentStatusByPaneKey)
}
if (!batchContext.agentPaneKeysByTabId) {
batchContext.agentPaneKeysByTabId = new Map()
for (const paneKey of Object.keys(state.agentStatusByPaneKey)) {
const tabId = parsePaneKey(paneKey)?.tabId
if (!tabId) {
continue
}
const paneKeys = batchContext.agentPaneKeysByTabId.get(tabId) ?? new Set<string>()
paneKeys.add(paneKey)
batchContext.agentPaneKeysByTabId.set(tabId, paneKeys)
}
}
return [...tabIds].flatMap((tabId) => [...(batchContext.agentPaneKeysByTabId?.get(tabId) ?? [])])
}
function updateBatchAgentPaneKey(
paneKey: string,
present: boolean,
batchContext?: WebSessionTabsBatchContext
): void {
const tabId = parsePaneKey(paneKey)?.tabId
const index = batchContext?.agentPaneKeysByTabId
if (!tabId || !index) {
return
}
if (present) {
const paneKeys = index.get(tabId) ?? new Set<string>()
paneKeys.add(paneKey)
index.set(tabId, paneKeys)
return
}
const paneKeys = index.get(tabId)
paneKeys?.delete(paneKey)
if (paneKeys?.size === 0) {
index.delete(tabId)
}
}
/** Generates a state patch for mirrored agent statuses, merging host entries with client overrides. */
function buildMirroredAgentStatusPatch(
state: WebSessionTabsSyncState,
currentTerminalTabs: readonly TerminalTab[],
terminalSurfaceTabs: readonly TerminalSurface[],
mirroredTerminalTabs: readonly MirroredTerminalTab[],
now: number
now: number,
batchContext?: WebSessionTabsBatchContext
): Pick<WebSessionTabsSyncState, 'agentStatusByPaneKey' | 'agentStatusEpoch' | 'sortEpoch'> | null {
const mirroredTabIds = new Set<string>()
for (const tab of currentTerminalTabs) {
@@ -812,7 +927,7 @@ function buildMirroredAgentStatusPatch(
let aggregateRelevantChange = false
let sortRelevantChange = false
for (const paneKey of Object.keys(state.agentStatusByPaneKey)) {
for (const paneKey of batchAgentPaneKeysForTabs(state, mirroredTabIds, batchContext)) {
if (!isMirroredAgentPaneKeyForTabs(paneKey, mirroredTabIds)) {
continue
}
@@ -830,9 +945,14 @@ function buildMirroredAgentStatusPatch(
continue
}
if (nextAgentStatusByPaneKey === state.agentStatusByPaneKey) {
nextAgentStatusByPaneKey = { ...state.agentStatusByPaneKey }
nextAgentStatusByPaneKey = writableWebSessionTabsRecord(
state,
'agentStatusByPaneKey',
batchContext
)
}
delete nextAgentStatusByPaneKey[paneKey]
updateBatchAgentPaneKey(paneKey, false, batchContext)
changed = true
aggregateRelevantChange = true
sortRelevantChange = true
@@ -844,9 +964,14 @@ function buildMirroredAgentStatusPatch(
continue
}
if (nextAgentStatusByPaneKey === state.agentStatusByPaneKey) {
nextAgentStatusByPaneKey = { ...state.agentStatusByPaneKey }
nextAgentStatusByPaneKey = writableWebSessionTabsRecord(
state,
'agentStatusByPaneKey',
batchContext
)
}
nextAgentStatusByPaneKey[paneKey] = entry
updateBatchAgentPaneKey(paneKey, true, batchContext)
changed = true
const entryAttributionChanged =
existing?.worktreeId !== entry.worktreeId || existing?.tabId !== entry.tabId
@@ -1285,34 +1410,20 @@ function updateHostSessionTabIdMappings(args: {
browserTabs: readonly MirroredBrowserTab[]
editorTabs: readonly MirroredEditorTab[]
}): void {
const keyPrefix = `${args.environmentId}:${args.worktreeId}:`
for (const key of hostSessionTabIdByLocalKey.keys()) {
if (key.startsWith(keyPrefix)) {
hostSessionTabIdByLocalKey.delete(key)
}
}
clearHostSessionTabIdMappings(args.environmentId, args.worktreeId)
const mirroredTerminalIds = new Set(args.terminalTabs.map((tab) => tab.id))
for (const surface of args.terminalSurfaces) {
const localId = toWebTerminalSurfaceTabId(surface.parentTabId)
if (mirroredTerminalIds.has(localId)) {
hostSessionTabIdByLocalKey.set(
hostSessionTabMappingKey({ ...args, tabId: localId }),
surface.parentTabId
)
setHostSessionTabIdMapping({ ...args, tabId: localId }, surface.parentTabId)
}
}
for (const entry of args.browserTabs) {
hostSessionTabIdByLocalKey.set(
hostSessionTabMappingKey({ ...args, tabId: entry.unifiedTab.id }),
entry.hostTabId
)
setHostSessionTabIdMapping({ ...args, tabId: entry.unifiedTab.id }, entry.hostTabId)
}
for (const entry of args.editorTabs) {
hostSessionTabIdByLocalKey.set(
hostSessionTabMappingKey({ ...args, tabId: entry.unifiedTab.id }),
entry.hostTabId
)
setHostSessionTabIdMapping({ ...args, tabId: entry.unifiedTab.id }, entry.hostTabId)
}
}
@@ -1500,20 +1611,47 @@ function pushRecentTabId(recent: string[] | undefined, tabId: string): string[]
return [...base.filter((id) => id !== tabId), tabId]
}
function writableWebSessionTabsRecord<K extends WebSessionTabsBatchRecordKey>(
state: WebSessionTabsSyncState,
recordKey: K,
batchContext?: WebSessionTabsBatchContext
): NonNullable<WebSessionTabsSyncState[K]> {
const record = (state[recordKey] ?? {}) as NonNullable<WebSessionTabsSyncState[K]>
if (!batchContext) {
return { ...record } as NonNullable<WebSessionTabsSyncState[K]>
}
// Why: one batch owns its record copies, so later snapshots can update them without recopying every workspace.
if (batchContext.changedRecords.has(recordKey)) {
return record
}
const next = { ...record } as NonNullable<WebSessionTabsSyncState[K]>
const mutableState = state as unknown as Record<
WebSessionTabsBatchRecordKey,
Record<string, unknown>
>
mutableState[recordKey] = next as Record<string, unknown>
batchContext.changedRecords.add(recordKey)
return next
}
function withWorktreeEntry<T>(
record: Record<string, T>,
state: WebSessionTabsSyncState,
recordKey: WebSessionTabsBatchRecordKey,
key: string,
value: T | null,
equal: (a: T | undefined, b: T | null) => boolean
equal: (a: T | undefined, b: T | null) => boolean,
batchContext?: WebSessionTabsBatchContext,
deleteNull = true
): Record<string, T> {
const record = (state[recordKey] ?? {}) as Record<string, T>
if (equal(record[key], value)) {
return record
}
const next = { ...record }
if (value === null) {
const next = writableWebSessionTabsRecord(state, recordKey, batchContext) as Record<string, T>
if (value === null && deleteNull) {
delete next[key]
} else {
next[key] = value
next[key] = value as T
}
return next
}
@@ -1763,11 +1901,12 @@ function findCurrentVisibleUnifiedTabId(args: {
return null
}
export function applyWebSessionTabsSnapshot(
function applyWebSessionTabsSnapshotWithContext(
state: WebSessionTabsSyncState,
rawSnapshot: RuntimeMobileSessionTabsResult,
environmentId: string,
now = Date.now()
now = Date.now(),
batchContext?: WebSessionTabsBatchContext
): WebSessionTabsSyncState | Partial<WebSessionTabsSyncState> {
const worktreeId = rawSnapshot.worktree
if (worktreeId === FLOATING_TERMINAL_WORKTREE_ID) {
@@ -2262,7 +2401,9 @@ export function applyWebSessionTabsSnapshot(
for (const removedId of removedTerminalIds) {
if (nextPtyIdsByTabId[removedId]) {
nextPtyIdsByTabId =
nextPtyIdsByTabId === state.ptyIdsByTabId ? { ...state.ptyIdsByTabId } : nextPtyIdsByTabId
nextPtyIdsByTabId === state.ptyIdsByTabId
? writableWebSessionTabsRecord(state, 'ptyIdsByTabId', batchContext)
: nextPtyIdsByTabId
delete nextPtyIdsByTabId[removedId]
}
}
@@ -2270,7 +2411,9 @@ export function applyWebSessionTabsSnapshot(
const current = nextPtyIdsByTabId[tab.id] ?? []
if (!sameStringArray(current, ptyIds)) {
nextPtyIdsByTabId =
nextPtyIdsByTabId === state.ptyIdsByTabId ? { ...state.ptyIdsByTabId } : nextPtyIdsByTabId
nextPtyIdsByTabId === state.ptyIdsByTabId
? writableWebSessionTabsRecord(state, 'ptyIdsByTabId', batchContext)
: nextPtyIdsByTabId
nextPtyIdsByTabId[tab.id] = ptyIds
}
}
@@ -2280,7 +2423,7 @@ export function applyWebSessionTabsSnapshot(
if (nextTerminalLayoutsByTabId[removedId]) {
nextTerminalLayoutsByTabId =
nextTerminalLayoutsByTabId === state.terminalLayoutsByTabId
? { ...state.terminalLayoutsByTabId }
? writableWebSessionTabsRecord(state, 'terminalLayoutsByTabId', batchContext)
: nextTerminalLayoutsByTabId
delete nextTerminalLayoutsByTabId[removedId]
}
@@ -2289,7 +2432,7 @@ export function applyWebSessionTabsSnapshot(
if (!terminalLayoutEqual(nextTerminalLayoutsByTabId[tab.id], layout)) {
nextTerminalLayoutsByTabId =
nextTerminalLayoutsByTabId === state.terminalLayoutsByTabId
? { ...state.terminalLayoutsByTabId }
? writableWebSessionTabsRecord(state, 'terminalLayoutsByTabId', batchContext)
: nextTerminalLayoutsByTabId
nextTerminalLayoutsByTabId[tab.id] = layout
}
@@ -2300,7 +2443,7 @@ export function applyWebSessionTabsSnapshot(
if (nextUnreadTerminalTabs[removedId]) {
nextUnreadTerminalTabs =
nextUnreadTerminalTabs === state.unreadTerminalTabs
? { ...state.unreadTerminalTabs }
? writableWebSessionTabsRecord(state, 'unreadTerminalTabs', batchContext)
: nextUnreadTerminalTabs
delete nextUnreadTerminalTabs[removedId]
}
@@ -2314,14 +2457,14 @@ export function applyWebSessionTabsSnapshot(
if (nextPendingStartupByTabId[removedId]) {
nextPendingStartupByTabId =
nextPendingStartupByTabId === pendingStartupByTabId
? { ...pendingStartupByTabId }
? writableWebSessionTabsRecord(state, 'pendingStartupByTabId', batchContext)
: nextPendingStartupByTabId
delete nextPendingStartupByTabId[removedId]
}
if (nextAutomaticAgentResumeClaimsByTabId[removedId]) {
nextAutomaticAgentResumeClaimsByTabId =
nextAutomaticAgentResumeClaimsByTabId === automaticAgentResumeClaimsByTabId
? { ...automaticAgentResumeClaimsByTabId }
? writableWebSessionTabsRecord(state, 'automaticAgentResumeClaimsByTabId', batchContext)
: nextAutomaticAgentResumeClaimsByTabId
delete nextAutomaticAgentResumeClaimsByTabId[removedId]
}
@@ -2335,7 +2478,7 @@ export function applyWebSessionTabsSnapshot(
if (nextBrowserPagesByWorkspace[removedWorkspaceId]) {
nextBrowserPagesByWorkspace =
nextBrowserPagesByWorkspace === state.browserPagesByWorkspace
? { ...state.browserPagesByWorkspace }
? writableWebSessionTabsRecord(state, 'browserPagesByWorkspace', batchContext)
: nextBrowserPagesByWorkspace
delete nextBrowserPagesByWorkspace[removedWorkspaceId]
}
@@ -2343,14 +2486,18 @@ export function applyWebSessionTabsSnapshot(
if (nextBrowserCertificateFailuresByPageId[page.id]) {
nextBrowserCertificateFailuresByPageId =
nextBrowserCertificateFailuresByPageId === state.browserCertificateFailuresByPageId
? { ...state.browserCertificateFailuresByPageId }
? writableWebSessionTabsRecord(
state,
'browserCertificateFailuresByPageId',
batchContext
)
: nextBrowserCertificateFailuresByPageId
delete nextBrowserCertificateFailuresByPageId[page.id]
}
if (nextRemoteBrowserPageHandlesByPageId[page.id]) {
nextRemoteBrowserPageHandlesByPageId =
nextRemoteBrowserPageHandlesByPageId === state.remoteBrowserPageHandlesByPageId
? { ...state.remoteBrowserPageHandlesByPageId }
? writableWebSessionTabsRecord(state, 'remoteBrowserPageHandlesByPageId', batchContext)
: nextRemoteBrowserPageHandlesByPageId
delete nextRemoteBrowserPageHandlesByPageId[page.id]
}
@@ -2361,7 +2508,7 @@ export function applyWebSessionTabsSnapshot(
if (!sameBrowserPages(current, [page])) {
nextBrowserPagesByWorkspace =
nextBrowserPagesByWorkspace === state.browserPagesByWorkspace
? { ...state.browserPagesByWorkspace }
? writableWebSessionTabsRecord(state, 'browserPagesByWorkspace', batchContext)
: nextBrowserPagesByWorkspace
nextBrowserPagesByWorkspace[page.workspaceId] = [page]
}
@@ -2372,7 +2519,7 @@ export function applyWebSessionTabsSnapshot(
) {
nextRemoteBrowserPageHandlesByPageId =
nextRemoteBrowserPageHandlesByPageId === state.remoteBrowserPageHandlesByPageId
? { ...state.remoteBrowserPageHandlesByPageId }
? writableWebSessionTabsRecord(state, 'remoteBrowserPageHandlesByPageId', batchContext)
: nextRemoteBrowserPageHandlesByPageId
nextRemoteBrowserPageHandlesByPageId[page.id] = {
environmentId,
@@ -2387,7 +2534,7 @@ export function applyWebSessionTabsSnapshot(
) {
nextBrowserCertificateFailuresByPageId =
nextBrowserCertificateFailuresByPageId === state.browserCertificateFailuresByPageId
? { ...state.browserCertificateFailuresByPageId }
? writableWebSessionTabsRecord(state, 'browserCertificateFailuresByPageId', batchContext)
: nextBrowserCertificateFailuresByPageId
if (certificateFailure) {
nextBrowserCertificateFailuresByPageId[page.id] = certificateFailure
@@ -2398,28 +2545,36 @@ export function applyWebSessionTabsSnapshot(
}
const nextTabsByWorktree = withWorktreeEntry(
state.tabsByWorktree,
state,
'tabsByWorktree',
worktreeId,
nextTerminalTabs,
sameTerminalTabs
sameTerminalTabs,
batchContext
)
const nextBrowserTabsByWorktree = withWorktreeEntry(
state.browserTabsByWorktree,
state,
'browserTabsByWorktree',
worktreeId,
nextBrowserTabs,
sameBrowserTabs
sameBrowserTabs,
batchContext
)
const nextUnifiedTabsByWorktree = withWorktreeEntry(
state.unifiedTabsByWorktree,
state,
'unifiedTabsByWorktree',
worktreeId,
nextUnifiedTabs,
sameUnifiedTabs
sameUnifiedTabs,
batchContext
)
const nextGroupsByWorktree = withWorktreeEntry(
state.groupsByWorktree,
state,
'groupsByWorktree',
worktreeId,
nextGroups,
sameGroups
sameGroups,
batchContext
)
const nextActiveGroupId =
// Why: status/title snapshots carry the host's last active tab; a client that already switched panes keeps its local group focus.
@@ -2429,7 +2584,14 @@ export function applyWebSessionTabsSnapshot(
null
const nextActiveGroupIdByWorktree =
nextGroups && state.activeGroupIdByWorktree[worktreeId] !== nextActiveGroupId
? { ...state.activeGroupIdByWorktree, [worktreeId]: nextActiveGroupId ?? targetGroupId }
? withWorktreeEntry(
state,
'activeGroupIdByWorktree',
worktreeId,
nextActiveGroupId ?? targetGroupId,
(current, next) => current === next,
batchContext
)
: state.activeGroupIdByWorktree
const nextLayoutByWorktree = (() => {
if (!nextGroups) {
@@ -2467,28 +2629,58 @@ export function applyWebSessionTabsSnapshot(
if (tabGroupLayoutEqual(state.layoutByWorktree[worktreeId], fallbackLayout)) {
return state.layoutByWorktree
}
return {
...state.layoutByWorktree,
[worktreeId]: fallbackLayout
}
return withWorktreeEntry(
state,
'layoutByWorktree',
worktreeId,
fallbackLayout,
(current, next) => current === next,
batchContext
)
})()
const nextTabBarOrderByWorktree = withWorktreeEntry(
state.tabBarOrderByWorktree,
state,
'tabBarOrderByWorktree',
worktreeId,
nextTabBarOrder.length > 0 ? nextTabBarOrder : null,
(a, b) => sameStringArray(a ?? [], b ?? [])
(a, b) => sameStringArray(a ?? [], b ?? []),
batchContext
)
const nextActiveTabIdByWorktree =
(state.activeTabIdByWorktree[worktreeId] ?? null) !== nextActiveTerminalId
? { ...state.activeTabIdByWorktree, [worktreeId]: nextActiveTerminalId }
? withWorktreeEntry(
state,
'activeTabIdByWorktree',
worktreeId,
nextActiveTerminalId,
(current, next) => (current ?? null) === next,
batchContext,
false
)
: state.activeTabIdByWorktree
const nextActiveBrowserTabIdByWorktree =
(state.activeBrowserTabIdByWorktree[worktreeId] ?? null) !== nextActiveBrowserWorkspaceId
? { ...state.activeBrowserTabIdByWorktree, [worktreeId]: nextActiveBrowserWorkspaceId }
? withWorktreeEntry(
state,
'activeBrowserTabIdByWorktree',
worktreeId,
nextActiveBrowserWorkspaceId,
(current, next) => (current ?? null) === next,
batchContext,
false
)
: state.activeBrowserTabIdByWorktree
const nextActiveFileIdByWorktree =
(state.activeFileIdByWorktree[worktreeId] ?? null) !== nextActiveEditorFileId
? { ...state.activeFileIdByWorktree, [worktreeId]: nextActiveEditorFileId }
? withWorktreeEntry(
state,
'activeFileIdByWorktree',
worktreeId,
nextActiveEditorFileId,
(current, next) => (current ?? null) === next,
batchContext,
false
)
: state.activeFileIdByWorktree
const isActiveWorktree = state.activeWorktreeId === worktreeId
const focusIntentVisibleTabType =
@@ -2564,14 +2756,22 @@ export function applyWebSessionTabsSnapshot(
const nextActiveTabType = isActiveWorktree ? nextVisibleTabType : state.activeTabType
const nextActiveTabTypeByWorktree =
state.activeTabTypeByWorktree[worktreeId] !== nextVisibleTabType
? { ...state.activeTabTypeByWorktree, [worktreeId]: nextVisibleTabType }
? withWorktreeEntry(
state,
'activeTabTypeByWorktree',
worktreeId,
nextVisibleTabType,
(current, next) => current === next,
batchContext
)
: state.activeTabTypeByWorktree
const agentStatusPatch = buildMirroredAgentStatusPatch(
state,
currentTerminalTabs,
terminalSurfaceTabs,
mirroredTerminalTabs,
now
now,
batchContext
)
const patch: Partial<WebSessionTabsSyncState> = {
@@ -2641,21 +2841,45 @@ export function applyWebSessionTabsSnapshot(
return Object.keys(patch).length === 0 ? state : patch
}
export function applyWebSessionTabsSnapshot(
state: WebSessionTabsSyncState,
rawSnapshot: RuntimeMobileSessionTabsResult,
environmentId: string,
now = Date.now()
): WebSessionTabsSyncState | Partial<WebSessionTabsSyncState> {
return applyWebSessionTabsSnapshotWithContext(state, rawSnapshot, environmentId, now)
}
export function applyWebSessionTabsSnapshots(
state: WebSessionTabsSyncState,
snapshots: readonly RuntimeMobileSessionTabsResult[],
environmentId: string,
now = Date.now()
): WebSessionTabsSyncState | Partial<WebSessionTabsSyncState> {
let nextState = state
const nextState = { ...state }
const batchContext: WebSessionTabsBatchContext = {
agentPaneKeysByTabId: null,
changedRecords: new Set()
}
let mergedPatch: Partial<WebSessionTabsSyncState> = {}
for (const snapshot of snapshots) {
const patch = applyWebSessionTabsSnapshot(nextState, snapshot, environmentId, now)
const patch = applyWebSessionTabsSnapshotWithContext(
nextState,
snapshot,
environmentId,
now,
batchContext
)
if (patch === nextState) {
continue
}
mergedPatch = { ...mergedPatch, ...patch }
nextState = { ...nextState, ...patch }
Object.assign(nextState, patch)
}
const mutableMergedPatch = mergedPatch as Record<string, unknown>
const mutableNextState = nextState as unknown as Record<string, unknown>
for (const recordKey of batchContext.changedRecords) {
mutableMergedPatch[recordKey] = mutableNextState[recordKey]
}
return Object.keys(mergedPatch).length === 0 ? state : mergedPatch
}