diff --git a/src/main/runtime/mobile-folder-session-mutation-race.test.ts b/src/main/runtime/mobile-folder-session-mutation-race.test.ts index 1d719e4828d..f61b407f7bc 100644 --- a/src/main/runtime/mobile-folder-session-mutation-race.test.ts +++ b/src/main/runtime/mobile-folder-session-mutation-race.test.ts @@ -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((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((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}` }) + ) }) }) diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index a76d144a569..eff1c5ed196 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -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 () => { diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 6e455e3867a..af146e56ded 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -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) }