fix(mobile): preserve coalesced host activation

This commit is contained in:
Brennan Benson
2026-08-01 02:49:11 -07:00
parent 8d35b014c1
commit 206bbc8351
3 changed files with 310 additions and 163 deletions
@@ -1,7 +1,12 @@
import { describe, expect, it, vi } from 'vitest'
import { getDefaultWorkspaceSession } from '../../shared/constants'
import { makePaneKey } from '../../shared/stable-pane-id'
import type { FolderWorkspace, ProjectGroup, WorkspaceSessionState } from '../../shared/types'
import type {
FolderWorkspace,
ProjectGroup,
RuntimeMobileSessionTabsResult,
WorkspaceSessionState
} from '../../shared/types'
import { OrcaRuntimeService } from './orca-runtime'
const electronMocks = vi.hoisted(() => {
@@ -23,13 +28,15 @@ vi.mock('electron', () => electronMocks)
const TAB_ID = '11111111-1111-4111-8111-111111111111'
const LEAF_ID = '22222222-2222-4222-8222-222222222222'
const PRIOR_TAB_ID = '44444444-4444-4444-8444-444444444444'
const PRIOR_LEAF_ID = '55555555-5555-4555-8555-555555555555'
function makeSession(worktreeId: string, ptyId = `${worktreeId}@@race-pty`): WorkspaceSessionState {
return {
...getDefaultWorkspaceSession(),
activeWorktreeId: worktreeId,
activeTabId: TAB_ID,
activeTabIdByWorktree: { [worktreeId]: TAB_ID },
activeTabId: PRIOR_TAB_ID,
activeTabIdByWorktree: { [worktreeId]: PRIOR_TAB_ID },
tabsByWorktree: {
[worktreeId]: [
{
@@ -41,6 +48,16 @@ function makeSession(worktreeId: string, ptyId = `${worktreeId}@@race-pty`): Wor
color: null,
sortOrder: 0,
createdAt: 1
},
{
id: PRIOR_TAB_ID,
ptyId: 'serve-prior-folder-pty',
worktreeId,
title: 'Prior terminal',
customTitle: null,
color: null,
sortOrder: 1,
createdAt: 1
}
]
},
@@ -50,6 +67,12 @@ function makeSession(worktreeId: string, ptyId = `${worktreeId}@@race-pty`): Wor
activeLeafId: LEAF_ID,
expandedLeafId: null,
ptyIdsByLeafId: { [LEAF_ID]: ptyId }
},
[PRIOR_TAB_ID]: {
root: { type: 'leaf', leafId: PRIOR_LEAF_ID },
activeLeafId: PRIOR_LEAF_ID,
expandedLeafId: null,
ptyIdsByLeafId: { [PRIOR_LEAF_ID]: 'serve-prior-folder-pty' }
}
},
terminalPtyIncarnationsByPaneKey: {
@@ -63,7 +86,7 @@ function createRuntime(
group: ProjectGroup,
ptyId?: string
): OrcaRuntimeService {
const session = makeSession(`folder:${folders[0]!.id}`, ptyId)
let session = makeSession(`folder:${folders[0]!.id}`, ptyId)
return new OrcaRuntimeService({
getRepo: () => undefined,
getRepos: () => [],
@@ -71,6 +94,9 @@ function createRuntime(
getProjectGroups: () => [group],
getWorkspaceSession: () => session,
getWorkspaceSessionHostIds: () => ['local'],
setWorkspaceSession: (next: WorkspaceSessionState) => {
session = next
},
addRepo: () => {},
updateRepo: () => undefined as never,
getAllWorktreeMeta: () => ({}),
@@ -89,74 +115,74 @@ function createRuntime(
}
describe('mobile folder session mutation races', () => {
it('reattaches an inventory-restored serve terminal before folder input is writable', async () => {
const ptyId = 'serve-restarted-folder-pty'
const folder: FolderWorkspace = {
id: 'restored-folder',
projectGroupId: 'restored-folder-group',
name: 'Restored folder',
folderPath: '/workspace/restored-folder',
connectionId: null,
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 1,
createdAt: 1,
updatedAt: 1
}
const group: ProjectGroup = {
id: folder.projectGroupId,
name: folder.projectGroupId,
parentPath: folder.folderPath,
connectionId: null,
parentGroupId: null,
createdFrom: 'manual',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const folderKey = `folder:${folder.id}`
const incarnationId = '33333333-3333-4333-8333-333333333333'
const runtime = createRuntime([folder], group, ptyId)
vi.spyOn(
runtime as unknown as {
resolveTerminalWorkspaceLaunchScope: () => Promise<{
id: string
path: string
connectionId: null
repo: null
folderWorkspace: FolderWorkspace
}>
},
'resolveTerminalWorkspaceLaunchScope'
).mockResolvedValue({
id: folderKey,
path: folder.folderPath,
connectionId: null,
repo: null,
folderWorkspace: folder
})
let attached = false
let releaseAttach!: () => void
const attach = new Promise<void>((resolve) => {
releaseAttach = resolve
})
const spawn = vi.fn(async () => {
await attach
attached = true
return { id: ptyId, incarnationId }
})
runtime.setPtyController({
spawn,
write: () => attached,
kill: () => true,
getForegroundProcess: async () => null,
listProcesses: async () => [
it.each([
['navigation first', 'caller', 'host'],
['host focus first', 'host', 'caller']
] as const)(
'reattaches once and honors host focus with %s',
async (_order, firstNavigation, secondNavigation) => {
const ptyId = 'serve-restarted-folder-pty'
const folder: FolderWorkspace = {
id: 'restored-folder',
projectGroupId: 'restored-folder-group',
name: 'Restored folder',
folderPath: '/workspace/restored-folder',
connectionId: null,
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 1,
createdAt: 1,
updatedAt: 1
}
const group: ProjectGroup = {
id: folder.projectGroupId,
name: folder.projectGroupId,
parentPath: folder.folderPath,
connectionId: null,
parentGroupId: null,
createdFrom: 'manual',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const folderKey = `folder:${folder.id}`
const incarnationId = '33333333-3333-4333-8333-333333333333'
const runtime = createRuntime([folder], group, ptyId)
vi.spyOn(
runtime as unknown as {
resolveTerminalWorkspaceLaunchScope: () => Promise<{
id: string
path: string
connectionId: null
repo: null
folderWorkspace: FolderWorkspace
}>
},
'resolveTerminalWorkspaceLaunchScope'
).mockResolvedValue({
id: folderKey,
path: folder.folderPath,
connectionId: null,
repo: null,
folderWorkspace: folder
})
let attached = false
let releaseAttach!: () => void
const attach = new Promise<void>((resolve) => {
releaseAttach = resolve
})
const spawn = vi.fn(async () => {
await attach
attached = true
return { id: ptyId, incarnationId }
})
const listProcesses = vi.fn(async () => [
{
id: ptyId,
cwd: folder.folderPath,
@@ -165,37 +191,64 @@ describe('mobile folder session mutation races', () => {
terminalHandle: 'term_restarted_folder',
incarnationId
}
]
})
const listed = await runtime.listMobileSessionTabs(`id:${folderKey}`)
const listedTab = listed.tabs[0]
expect(listedTab).toMatchObject({ status: 'ready', terminal: 'term_restarted_folder' })
await expect(
runtime.sendTerminal(listedTab?.type === 'terminal' ? listedTab.terminal! : 'missing', {
text: 'before attach'
])
runtime.setPtyController({
spawn,
write: () => attached,
kill: () => true,
getForegroundProcess: async () => null,
listProcesses
})
).rejects.toThrow('terminal_not_writable')
const activations = Promise.all([
runtime.activateMobileSessionTab(`id:${folderKey}`, TAB_ID),
runtime.activateMobileSessionTab(`id:${folderKey}`, TAB_ID)
])
await vi.waitFor(() => expect(spawn).toHaveBeenCalledOnce())
releaseAttach()
const activated = await activations
const activatedTab = activated[0].tabs[0]
expect(activated[1].tabs[0]).toMatchObject({ status: 'ready' })
await expect(
runtime.sendTerminal(activatedTab?.type === 'terminal' ? activatedTab.terminal! : 'missing', {
text: 'folder input'
})
).resolves.toMatchObject({ accepted: true, bytesWritten: 12 })
expect(spawn).toHaveBeenCalledOnce()
expect(spawn).toHaveBeenCalledWith(
expect.objectContaining({ sessionId: ptyId, worktreeId: folderKey })
)
})
const listed = await runtime.listMobileSessionTabs(`id:${folderKey}`)
const listedTab = listed.tabs[0]
expect(listedTab).toMatchObject({ status: 'ready', terminal: 'term_restarted_folder' })
await expect(
runtime.sendTerminal(listedTab?.type === 'terminal' ? listedTab.terminal! : 'missing', {
text: 'before attach'
})
).rejects.toThrow('terminal_not_writable')
const pendingGet = vi.spyOn(runtime['pendingMobileTerminalMaterializations'], 'get')
const firstActivation = runtime.activateMobileSessionTab(
`id:${folderKey}`,
TAB_ID,
undefined,
{ navigation: firstNavigation }
)
await vi.waitFor(() => expect(spawn).toHaveBeenCalledOnce())
const secondActivation = runtime.activateMobileSessionTab(
`id:${folderKey}`,
TAB_ID,
undefined,
{ navigation: secondNavigation }
)
await vi.waitFor(() => expect(pendingGet).toHaveBeenCalledTimes(2))
releaseAttach()
const activated = await Promise.all([firstActivation, secondActivation])
const activatedTab = activated[0].tabs.find(
(candidate) => candidate.type === 'terminal' && candidate.parentTabId === TAB_ID
)
expect(activated[1].tabs).toContainEqual(
expect.objectContaining({ parentTabId: TAB_ID, status: 'ready' })
)
await expect(
runtime.sendTerminal(
activatedTab?.type === 'terminal' ? activatedTab.terminal! : 'missing',
{
text: 'folder input'
}
)
).resolves.toMatchObject({ accepted: true, bytesWritten: 12 })
expect(spawn).toHaveBeenCalledOnce()
expect(spawn).toHaveBeenCalledWith(
expect.objectContaining({ sessionId: ptyId, worktreeId: folderKey })
)
expect((await runtime.listMobileSessionTabs(`id:${folderKey}`)).activeTabId).toBe(
`${TAB_ID}::${LEAF_ID}`
)
}
)
it('fences shared SSH reattachment when the reconnect generation changes', async () => {
const connectionId = 'reattach-generation'
@@ -231,6 +284,10 @@ describe('mobile folder session mutation races', () => {
}
const folderKey = `folder:${folder.id}`
const runtime = createRuntime([folder], group, ptyId)
const focusTerminal = vi.fn()
const published: RuntimeMobileSessionTabsResult[] = []
runtime.setNotifier({ focusTerminal } as never)
runtime.onMobileSessionTabsChanged((snapshot) => published.push(snapshot))
vi.spyOn(
runtime as unknown as {
resolveTerminalWorkspaceLaunchScope: () => Promise<{
@@ -289,6 +346,10 @@ describe('mobile folder session mutation races', () => {
})
expect(spawn).toHaveBeenCalledOnce()
expect(kill).toHaveBeenCalledWith(ptyId)
expect(focusTerminal).not.toHaveBeenCalled()
expect(published).not.toContainEqual(
expect.objectContaining({ activeTabId: `${TAB_ID}::${LEAF_ID}` })
)
})
it.each(['activate', 'close'] as const)(
@@ -389,6 +450,10 @@ describe('mobile folder session mutation races', () => {
const folderKey = `folder:${folder.id}`
const folders = [folder]
const runtime = createRuntime(folders, group)
const focusTerminal = vi.fn()
const published: RuntimeMobileSessionTabsResult[] = []
runtime.setNotifier({ focusTerminal } as never)
runtime.onMobileSessionTabsChanged((snapshot) => published.push(snapshot))
vi.spyOn(
runtime as unknown as {
resolveTerminalWorkspaceLaunchScope: () => Promise<{
@@ -440,5 +505,9 @@ describe('mobile folder session mutation races', () => {
})
expect(spawn).toHaveBeenCalledOnce()
expect(kill).toHaveBeenCalledWith('stale-materialized-pty')
expect(focusTerminal).not.toHaveBeenCalled()
expect(published).not.toContainEqual(
expect.objectContaining({ activeTabId: `${TAB_ID}::${LEAF_ID}` })
)
})
})
+95 -37
View File
@@ -28261,9 +28261,12 @@ describe('OrcaRuntimeService', () => {
it('reattaches an inventory-restored serve terminal once before accepting input', async () => {
const ptyId = 'serve-restarted-git-pty'
const priorPtyId = 'serve-prior-git-pty'
const incarnationId = '91919191-9191-4919-8919-919191919191'
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(
makeWorkspaceSessionWithHeadlessTerminal({
activeTabId: 'prior-tab',
activeTabIdByWorktree: { [TEST_WORKTREE_ID]: 'prior-tab' },
tabsByWorktree: {
[TEST_WORKTREE_ID]: [
{
@@ -28275,11 +28278,22 @@ describe('OrcaRuntimeService', () => {
color: null,
sortOrder: 0,
createdAt: 1
},
{
id: 'prior-tab',
ptyId: priorPtyId,
worktreeId: TEST_WORKTREE_ID,
title: 'Prior Git Terminal',
customTitle: null,
color: null,
sortOrder: 1,
createdAt: 1
}
]
},
terminalLayoutsByTabId: {
'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: ptyId })
'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: ptyId }),
'prior-tab': makeHeadlessTerminalLayout({ [HEADLESS_SECOND_LEAF_ID]: priorPtyId })
},
terminalPtyIncarnationsByPaneKey: {
[makePaneKey('host-tab', HEADLESS_LEAF_ID)]: incarnationId
@@ -28296,27 +28310,29 @@ describe('OrcaRuntimeService', () => {
attached = true
return { id: ptyId, incarnationId }
})
const listProcesses = vi.fn(async () => [
{
id: ptyId,
cwd: TEST_WORKTREE_PATH,
title: 'Restored Git Terminal',
worktreeId: TEST_WORKTREE_ID,
terminalHandle: 'term_restarted_git',
incarnationId
}
])
const runtime = new OrcaRuntimeService(runtimeStore as never)
runtime.setPtyController({
spawn,
write: () => attached,
kill: () => true,
getForegroundProcess: async () => null,
listProcesses: async () => [
{
id: ptyId,
cwd: TEST_WORKTREE_PATH,
title: 'Restored Git Terminal',
worktreeId: TEST_WORKTREE_ID,
terminalHandle: 'term_restarted_git',
incarnationId
}
]
listProcesses
})
runtime.syncWindowGraph(0, { tabs: [], leaves: [] })
const listed = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)
const listedTab = listed.tabs[0]
const listedTab = listed.tabs.find(
(candidate) => candidate.type === 'terminal' && candidate.parentTabId === 'host-tab'
)
expect(listedTab).toMatchObject({ status: 'ready', terminal: 'term_restarted_git' })
await expect(
runtime.sendTerminal(listedTab?.type === 'terminal' ? listedTab.terminal! : 'missing', {
@@ -28324,15 +28340,27 @@ describe('OrcaRuntimeService', () => {
})
).rejects.toThrow('terminal_not_writable')
const activations = Promise.all([
runtime.activateMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab'),
runtime.activateMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab')
])
const pendingGet = vi.spyOn(runtime['pendingMobileTerminalMaterializations'], 'get')
const navigationActivation = runtime.activateMobileSessionTab(
`id:${TEST_WORKTREE_ID}`,
'host-tab',
undefined,
{ navigation: 'caller' }
)
await vi.waitFor(() => expect(spawn).toHaveBeenCalledOnce())
const hostActivation = runtime.activateMobileSessionTab(
`id:${TEST_WORKTREE_ID}`,
'host-tab',
undefined,
{ navigation: 'host' }
)
await vi.waitFor(() => expect(pendingGet).toHaveBeenCalledTimes(2))
releaseAttach()
const activated = await activations
const activatedTab = activated[0].tabs[0]
expect(activated[1].tabs[0]).toMatchObject({ status: 'ready' })
const activated = await Promise.all([navigationActivation, hostActivation])
const activatedTab = activated[0].tabs.find(
(candidate) => candidate.type === 'terminal' && candidate.parentTabId === 'host-tab'
)
expect(activated[1].activeTabId).toBe(`host-tab::${HEADLESS_LEAF_ID}`)
await expect(
runtime.sendTerminal(activatedTab?.type === 'terminal' ? activatedTab.terminal! : 'missing', {
text: 'after attach'
@@ -28347,12 +28375,18 @@ describe('OrcaRuntimeService', () => {
worktreeId: TEST_WORKTREE_ID
})
)
expect((await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)).activeTabId).toBe(
`host-tab::${HEADLESS_LEAF_ID}`
)
})
it('reattaches hydrated SSH headless terminals with the persisted relay identity', async () => {
const incarnationId = '92929292-9292-4929-8929-929292929292'
const priorPtyId = 'ssh:ssh-1@@prior-relay-pty'
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(
makeWorkspaceSessionWithHeadlessTerminal({
activeTabId: 'prior-tab',
activeTabIdByWorktree: { [TEST_WORKTREE_ID]: 'prior-tab' },
tabsByWorktree: {
[TEST_WORKTREE_ID]: [
{
@@ -28364,13 +28398,24 @@ describe('OrcaRuntimeService', () => {
color: null,
sortOrder: 0,
createdAt: 1
},
{
id: 'prior-tab',
ptyId: priorPtyId,
worktreeId: TEST_WORKTREE_ID,
title: 'Prior Remote Terminal',
customTitle: null,
color: null,
sortOrder: 1,
createdAt: 1
}
]
},
terminalLayoutsByTabId: {
'host-tab': makeHeadlessTerminalLayout({
[HEADLESS_LEAF_ID]: 'ssh:ssh-1@@relay-pty'
})
}),
'prior-tab': makeHeadlessTerminalLayout({ [HEADLESS_SECOND_LEAF_ID]: priorPtyId })
},
terminalPtyIncarnationsByPaneKey: {
[makePaneKey('host-tab', HEADLESS_LEAF_ID)]: incarnationId
@@ -28397,32 +28442,42 @@ describe('OrcaRuntimeService', () => {
await ptyAttach()
return { id: 'ssh:ssh-1@@relay-pty', isReattach: true }
})
const listProcesses = vi.fn(async () => [
{
id: 'ssh:ssh-1@@relay-pty',
cwd: TEST_WORKTREE_PATH,
title: 'Remote Terminal',
worktreeId: TEST_WORKTREE_ID,
terminalHandle: 'term_restarted_ssh',
incarnationId
}
])
const runtime = new OrcaRuntimeService(remoteStore as never)
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null,
listProcesses: async () => [
{
id: 'ssh:ssh-1@@relay-pty',
cwd: TEST_WORKTREE_PATH,
title: 'Remote Terminal',
worktreeId: TEST_WORKTREE_ID,
terminalHandle: 'term_restarted_ssh',
incarnationId
}
]
listProcesses
})
runtime.syncWindowGraph(0, { tabs: [], leaves: [] })
const activations = Promise.all([
runtime.activateMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab'),
runtime.activateMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab')
])
const pendingGet = vi.spyOn(runtime['pendingMobileTerminalMaterializations'], 'get')
const navigationActivation = runtime.activateMobileSessionTab(
`id:${TEST_WORKTREE_ID}`,
'host-tab',
undefined,
{ navigation: 'caller' }
)
await vi.waitFor(() => expect(ptyAttach).toHaveBeenCalledOnce())
const hostActivation = runtime.activateMobileSessionTab(
`id:${TEST_WORKTREE_ID}`,
'host-tab',
undefined,
{ navigation: 'host' }
)
await vi.waitFor(() => expect(pendingGet).toHaveBeenCalledTimes(2))
releaseAttach()
await expect(activations).resolves.toHaveLength(2)
await expect(Promise.all([navigationActivation, hostActivation])).resolves.toHaveLength(2)
expect(spawn).toHaveBeenCalledOnce()
expect(ptyAttach).toHaveBeenCalledOnce()
@@ -28435,6 +28490,9 @@ describe('OrcaRuntimeService', () => {
persistHostSessionBinding: true
})
)
expect((await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)).activeTabId).toBe(
`host-tab::${HEADLESS_LEAF_ID}`
)
})
it('spawns fresh after an expired hydrated SSH headless reattach clears persistence', async () => {
+45 -25
View File
@@ -8149,28 +8149,24 @@ export class OrcaRuntimeService {
return
}
try {
await this.createRuntimeOwnedMobileSessionTerminal(
worktreeId,
targetsHost,
undefined,
{
identity: {
tabId: tab.parentTabId,
leafId: tab.leafId,
sessionId
},
cwd: tab.startupCwd,
command: agentStartup.command,
env: agentStartup.env,
startupCommandDelivery: agentStartup.startupCommandDelivery,
launchConfig: agentStartup.launchConfig,
launchAgent: tab.launchAgent,
targetGroupId,
acceptWorkspaceInventory: () =>
acceptMaterialization() &&
!this.hasLiveRuntimeSessionOwnedPtyBinding(worktreeId, tab)
}
)
// Why: coalescing shares attachment, not a caller's host navigation intent.
await this.createRuntimeOwnedMobileSessionTerminal(worktreeId, false, undefined, {
identity: {
tabId: tab.parentTabId,
leafId: tab.leafId,
sessionId
},
cwd: tab.startupCwd,
command: agentStartup.command,
env: agentStartup.env,
startupCommandDelivery: agentStartup.startupCommandDelivery,
launchConfig: agentStartup.launchConfig,
launchAgent: tab.launchAgent,
targetGroupId,
acceptWorkspaceInventory: () =>
acceptMaterialization() &&
!this.hasLiveRuntimeSessionOwnedPtyBinding(worktreeId, tab)
})
} catch (error) {
if (
error instanceof Error &&
@@ -8208,9 +8204,30 @@ export class OrcaRuntimeService {
if (!acceptMaterialization()) {
throw new Error('tab_not_found')
}
const materializedSnapshot = this.mobileSessionTabsByWorktree.get(worktreeId)
const materializedTab = materializedSnapshot?.tabs.find(
(candidate): candidate is RuntimeMobileSessionTerminalTab =>
candidate.type === 'terminal' && candidate.id === tab.id
)
if (!materializedSnapshot || !materializedTab) {
throw new Error('tab_not_found')
}
if (targetsHost && !materializedTab.isActive) {
this.activateHeadlessMobileSessionTerminalTab(
worktreeId,
materializedSnapshot,
materializedTab,
{
persistWorkspaceSession: this.shouldPersistHeadlessMobileSessionActivation(
materializedSnapshot,
materializedTab
)
}
)
}
return this.applyMobileSessionTabNavigation(
this.getMobileSessionTabsForWorktree(worktreeId),
tab.id,
materializedTab.id,
navigation,
opts.clientNavigationId
)
@@ -8337,7 +8354,8 @@ export class OrcaRuntimeService {
private activateHeadlessMobileSessionTerminalTab(
worktreeId: string,
snapshot: RuntimeMobileSessionTabsSnapshot,
activeTab: RuntimeMobileSessionTerminalTab
activeTab: RuntimeMobileSessionTerminalTab,
options: { persistWorkspaceSession?: boolean } = {}
): void {
const tabs = snapshot.tabs.map((candidate) => ({
...candidate,
@@ -8357,7 +8375,9 @@ export class OrcaRuntimeService {
),
tabs
}
this.persistHeadlessTerminalActiveLeaf(worktreeId, activeTab)
if (options.persistWorkspaceSession !== false) {
this.persistHeadlessTerminalActiveLeaf(worktreeId, activeTab)
}
this.mobileSessionTabsByWorktree.set(worktreeId, nextSnapshot)
this.emitMobileSessionTabsSnapshot(nextSnapshot)
}