mirror of
https://github.com/stablyai/orca.git
synced 2026-10-01 00:02:10 +00:00
fix(mobile): a re-derived frame changes membership only for the registering surface
The replay re-ran the whole accepted frame, so a surface the host retired after accept (a phone close whose remote PTY is still exiting, or a closed chat tab) came back. Every other surface now keeps the host's current decision; the removal repair is extracted from the terminal retirement helper so non-terminal tabs are removed the same way.
This commit is contained in:
@@ -216,15 +216,34 @@ export function retireTerminalSurfacesFromSnapshot(args: {
|
||||
if (retiredTabs.length === 0) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
snapshot: removeMobileSessionSnapshotTabs(args.snapshot, retiredTabs, args.retirementProofs),
|
||||
retired: retiredTabs.map((tab) => ({
|
||||
worktreeId: args.snapshot.worktree,
|
||||
parentTabId: tab.parentTabId,
|
||||
leafId: tab.leafId,
|
||||
ptyId: args.ptyId
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/** The snapshot without `removedTabs`, repairing split layouts, tab groups and the active surface. */
|
||||
export function removeMobileSessionSnapshotTabs(
|
||||
snapshot: RuntimeMobileSessionTabsSnapshot,
|
||||
removedTabs: readonly RuntimeMobileSessionSnapshotTab[],
|
||||
retirementProofs?: readonly RuntimeMobileSessionRetiredTerminalSurface[]
|
||||
): RuntimeMobileSessionTabsSnapshot {
|
||||
const removedTerminals = removedTabs.filter(
|
||||
(tab): tab is RuntimeMobileSessionTerminalTab => tab.type === 'terminal'
|
||||
)
|
||||
const retiredLeafIdsByParent = new Map<string, Set<string>>()
|
||||
for (const tab of retiredTabs) {
|
||||
for (const tab of removedTerminals) {
|
||||
const leafIds = retiredLeafIdsByParent.get(tab.parentTabId) ?? new Set<string>()
|
||||
leafIds.add(tab.leafId)
|
||||
retiredLeafIdsByParent.set(tab.parentTabId, leafIds)
|
||||
}
|
||||
const retiredIds = new Set(retiredTabs.map((tab) => tab.id))
|
||||
let tabs = args.snapshot.tabs.filter((tab) => !retiredIds.has(tab.id))
|
||||
const removedIds = new Set(removedTabs.map((tab) => tab.id))
|
||||
let tabs = snapshot.tabs.filter((tab) => !removedIds.has(tab.id))
|
||||
tabs = tabs.map((tab) => {
|
||||
if (tab.type !== 'terminal') {
|
||||
return tab
|
||||
@@ -235,7 +254,7 @@ export function retireTerminalSurfacesFromSnapshot(args: {
|
||||
}
|
||||
const sourceLayout =
|
||||
tab.parentLayout ??
|
||||
retiredTabs.find((retired) => retired.parentTabId === tab.parentTabId)?.parentLayout
|
||||
removedTerminals.find((retired) => retired.parentTabId === tab.parentTabId)?.parentLayout
|
||||
const parentLayout = sourceLayout
|
||||
? retireLeavesFromTerminalLayout(sourceLayout, retiredLeafIds)
|
||||
: undefined
|
||||
@@ -244,21 +263,18 @@ export function retireTerminalSurfacesFromSnapshot(args: {
|
||||
...(parentLayout ? { parentLayout } : {}),
|
||||
isActive:
|
||||
tab.isActive ||
|
||||
retiredTabs.some((retired) => retired.parentTabId === tab.parentTabId && retired.isActive)
|
||||
removedTerminals.some(
|
||||
(retired) => retired.parentTabId === tab.parentTabId && retired.isActive
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const validTopLevelIds = new Set(tabs.map(topLevelTabId))
|
||||
const tabGroups = repairMobileSessionTabGroupsAfterRetirement(
|
||||
args.snapshot.tabGroups,
|
||||
snapshot.tabGroups,
|
||||
validTopLevelIds
|
||||
)
|
||||
const active = chooseActiveSurface(
|
||||
tabs,
|
||||
args.snapshot.activeTabId,
|
||||
tabGroups,
|
||||
args.snapshot.activeGroupId
|
||||
)
|
||||
const active = chooseActiveSurface(tabs, snapshot.activeTabId, tabGroups, snapshot.activeGroupId)
|
||||
tabs = tabs.map((tab) => ({ ...tab, isActive: tab.id === active?.id }))
|
||||
const activeTopLevelId = active ? topLevelTabId(active) : null
|
||||
const activeGroupId =
|
||||
@@ -269,38 +285,29 @@ export function retireTerminalSurfacesFromSnapshot(args: {
|
||||
null
|
||||
const retainedGroupIds = new Set(tabGroups?.map((group) => group.id) ?? [])
|
||||
|
||||
const retired = retiredTabs.map((tab) => ({
|
||||
worktreeId: args.snapshot.worktree,
|
||||
parentTabId: tab.parentTabId,
|
||||
leafId: tab.leafId,
|
||||
ptyId: args.ptyId
|
||||
}))
|
||||
return {
|
||||
snapshot: {
|
||||
...args.snapshot,
|
||||
snapshotVersion: args.snapshot.snapshotVersion + 1,
|
||||
activeGroupId,
|
||||
activeTabId: active?.id ?? null,
|
||||
activeTabType: active?.type ?? null,
|
||||
...(tabGroups ? { tabGroups } : { tabGroups: undefined }),
|
||||
...(args.snapshot.tabGroupLayout
|
||||
? {
|
||||
tabGroupLayout: pruneTabGroupLayoutAfterRetirement(
|
||||
args.snapshot.tabGroupLayout,
|
||||
retainedGroupIds
|
||||
)
|
||||
}
|
||||
: {}),
|
||||
...(args.retirementProofs && args.retirementProofs.length > 0
|
||||
? {
|
||||
retiredTerminalSurfaces: appendRetiredTerminalSurfaceProofs(
|
||||
args.snapshot.retiredTerminalSurfaces,
|
||||
args.retirementProofs
|
||||
)
|
||||
}
|
||||
: {}),
|
||||
tabs
|
||||
},
|
||||
retired
|
||||
...snapshot,
|
||||
snapshotVersion: snapshot.snapshotVersion + 1,
|
||||
activeGroupId,
|
||||
activeTabId: active?.id ?? null,
|
||||
activeTabType: active?.type ?? null,
|
||||
...(tabGroups ? { tabGroups } : { tabGroups: undefined }),
|
||||
...(snapshot.tabGroupLayout
|
||||
? {
|
||||
tabGroupLayout: pruneTabGroupLayoutAfterRetirement(
|
||||
snapshot.tabGroupLayout,
|
||||
retainedGroupIds
|
||||
)
|
||||
}
|
||||
: {}),
|
||||
...(retirementProofs && retirementProofs.length > 0
|
||||
? {
|
||||
retiredTerminalSurfaces: appendRetiredTerminalSurfaceProofs(
|
||||
snapshot.retiredTerminalSurfaces,
|
||||
retirementProofs
|
||||
)
|
||||
}
|
||||
: {}),
|
||||
tabs
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { OrcaRuntimeWithWriteOrchestrationPointerPty } from './orca-runtime-writ
|
||||
import type { RuntimeMobileSessionTabsSnapshot } from '../../shared/runtime-types'
|
||||
import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types'
|
||||
import { getMobileSessionSnapshotTabIdentityKeys } from './mobile-session-tab-merge'
|
||||
import { removeMobileSessionSnapshotTabs } from './mobile-session-terminal-retirement'
|
||||
|
||||
export class OrcaRuntimeWithSyncMobileSessionTabs extends OrcaRuntimeWithWriteOrchestrationPointerPty {
|
||||
// Returns the worktrees whose stored snapshot object changed during this
|
||||
@@ -217,17 +218,39 @@ export class OrcaRuntimeWithSyncMobileSessionTabs extends OrcaRuntimeWithWriteOr
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.mergeRendererMobileSnapshot(accepted.frame)
|
||||
this.mergeRendererMobileSnapshot(accepted.frame, { parentTabId: tabId, leafId })
|
||||
this.notifyMobileSessionTabsChanged(worktreeId)
|
||||
}
|
||||
|
||||
protected mergeRendererMobileSnapshot(snapshot: RuntimeMobileSessionTabsSnapshot): void {
|
||||
protected mergeRendererMobileSnapshot(
|
||||
snapshot: RuntimeMobileSessionTabsSnapshot,
|
||||
rederivedSurface?: { parentTabId: string; leafId: string }
|
||||
): void {
|
||||
const existing = this.mobileSessionTabsByWorktree.get(snapshot.worktree)
|
||||
this.nativeChatDraftResolutions.reconcile(snapshot)
|
||||
const launchDraftFencedSnapshot = this.nativeChatDraftResolutions.applyFence(snapshot)
|
||||
const fencedSnapshot = this.applyMobileSessionRetirementFences(launchDraftFencedSnapshot)
|
||||
this.releaseRuntimeSessionOwnershipForRendererRetiredTabs(fencedSnapshot, existing)
|
||||
const nextSnapshot = this.mergePreservedHeadlessMobileSessionTabs(fencedSnapshot, existing)
|
||||
// Why: a replay re-derives only the registering surface; every other surface keeps the host's
|
||||
// current decision, so one the host retired after accept (its PTY may still be exiting) stays out.
|
||||
const heldKeys = new Set(
|
||||
existing?.tabs.flatMap((tab) => getMobileSessionSnapshotTabIdentityKeys(tab)) ?? []
|
||||
)
|
||||
const unheldSurfaces = rederivedSurface
|
||||
? fencedSnapshot.tabs.filter(
|
||||
(tab) =>
|
||||
!(
|
||||
tab.type === 'terminal' &&
|
||||
tab.parentTabId === rederivedSurface.parentTabId &&
|
||||
tab.leafId === rederivedSurface.leafId
|
||||
) && !getMobileSessionSnapshotTabIdentityKeys(tab).some((key) => heldKeys.has(key))
|
||||
)
|
||||
: []
|
||||
const mergedSnapshot =
|
||||
unheldSurfaces.length > 0
|
||||
? removeMobileSessionSnapshotTabs(fencedSnapshot, unheldSurfaces)
|
||||
: fencedSnapshot
|
||||
this.releaseRuntimeSessionOwnershipForRendererRetiredTabs(mergedSnapshot, existing)
|
||||
const nextSnapshot = this.mergePreservedHeadlessMobileSessionTabs(mergedSnapshot, existing)
|
||||
// Why: clients drop same-epoch frames whose version isn't strictly newer,
|
||||
// and main-local touches may already have emitted a higher version than
|
||||
// the renderer's counter — keep the stored version strictly monotonic so
|
||||
|
||||
Reference in New Issue
Block a user