mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
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:
@@ -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',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user