From bf4ed350ae8a77f8e809e48689625bd9723dc435 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Fri, 26 Jun 2026 20:16:52 -0700 Subject: [PATCH] Materialize mobile-only pending session terminals (#6465) Co-authored-by: Orca --- src/main/runtime/orca-runtime.test.ts | 315 ++++++++++++++++++++++++++ src/main/runtime/orca-runtime.ts | 12 +- 2 files changed, 325 insertions(+), 2 deletions(-) diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index c5b0cbba940..3aa0b1f5688 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -11811,6 +11811,7 @@ describe('OrcaRuntimeService', () => { id: 'tab-1::pane:1', parentTabId: 'tab-1', leafId: 'pane:1', + ptyId: 'pty-pane-1', title: 'left', isActive: false }, @@ -11819,6 +11820,7 @@ describe('OrcaRuntimeService', () => { id: 'tab-1::pane:2', parentTabId: 'tab-1', leafId: 'pane:2', + ptyId: 'pty-pane-2', title: 'right', isActive: true } @@ -11826,6 +11828,8 @@ describe('OrcaRuntimeService', () => { } ] }) + runtime.registerPty('pty-pane-1', TEST_WORKTREE_ID) + runtime.registerPty('pty-pane-2', TEST_WORKTREE_ID) const activated = await runtime.activateMobileSessionTab( `id:${TEST_WORKTREE_ID}`, @@ -11846,6 +11850,317 @@ describe('OrcaRuntimeService', () => { ]) }) + it('materializes pending mobile session terminals without focusing desktop clients', async () => { + const persistedPtyId = `${TEST_WORKTREE_ID}@@mobile-only-pty` + const spawn = vi.fn().mockResolvedValue({ id: persistedPtyId }) + const focusTerminal = vi.fn() + const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( + makeWorkspaceSessionWithHeadlessTerminal({ + tabsByWorktree: { + [TEST_WORKTREE_ID]: [ + { + id: 'host-tab', + ptyId: persistedPtyId, + worktreeId: TEST_WORKTREE_ID, + title: 'Persisted Terminal', + customTitle: null, + color: null, + sortOrder: 0, + createdAt: 1 + } + ] + }, + terminalLayoutsByTabId: { + 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: persistedPtyId }) + } + }) + ) + const runtime = new OrcaRuntimeService(runtimeStore as never) + runtime.setNotifier({ + worktreesChanged: vi.fn(), + reposChanged: vi.fn(), + activateWorktree: vi.fn(), + createTerminal: vi.fn(), + revealTerminalSession: vi.fn(), + splitTerminal: vi.fn(), + renameTerminal: vi.fn(), + focusTerminal, + closeTerminal: vi.fn(), + sleepWorktree: vi.fn(), + terminalFitOverrideChanged: vi.fn(), + terminalDriverChanged: vi.fn() + }) + runtime.setPtyController({ + spawn, + write: () => true, + kill: () => true, + getForegroundProcess: async () => null + }) + + const activated = await runtime.activateMobileSessionTab( + `id:${TEST_WORKTREE_ID}`, + 'host-tab', + HEADLESS_LEAF_ID, + { notifyClients: false } + ) + + expect(focusTerminal).not.toHaveBeenCalled() + expect(spawn).toHaveBeenCalledWith( + expect.objectContaining({ + worktreeId: TEST_WORKTREE_ID, + tabId: 'host-tab', + leafId: HEADLESS_LEAF_ID, + sessionId: persistedPtyId, + persistHostSessionBinding: true + }) + ) + expect(activated.tabs).toEqual([ + expect.objectContaining({ + id: `host-tab::${HEADLESS_LEAF_ID}`, + isActive: true, + status: 'ready', + terminal: expect.any(String) + }) + ]) + }) + + it('materializes phone-local pending terminal tabs without stored PTY bindings', async () => { + const spawn = vi.fn().mockResolvedValue({ id: 'fresh-mobile-pty' }) + const focusTerminal = vi.fn() + const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( + makeWorkspaceSessionWithHeadlessTerminal({ + tabsByWorktree: { + [TEST_WORKTREE_ID]: [ + { + id: 'host-tab', + ptyId: null, + worktreeId: TEST_WORKTREE_ID, + title: 'Persisted Terminal', + customTitle: null, + color: null, + sortOrder: 0, + createdAt: 1 + } + ] + }, + terminalLayoutsByTabId: { + 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: undefined }) + } + }) + ) + const runtime = new OrcaRuntimeService(runtimeStore as never) + runtime.setNotifier({ + worktreesChanged: vi.fn(), + reposChanged: vi.fn(), + activateWorktree: vi.fn(), + createTerminal: vi.fn(), + revealTerminalSession: vi.fn(), + splitTerminal: vi.fn(), + renameTerminal: vi.fn(), + focusTerminal, + closeTerminal: vi.fn(), + sleepWorktree: vi.fn(), + terminalFitOverrideChanged: vi.fn(), + terminalDriverChanged: vi.fn() + }) + runtime.setPtyController({ + spawn, + write: () => true, + kill: () => true, + getForegroundProcess: async () => null + }) + + const activated = await runtime.activateMobileSessionTab( + `id:${TEST_WORKTREE_ID}`, + 'host-tab', + HEADLESS_LEAF_ID, + { notifyClients: false } + ) + + expect(focusTerminal).not.toHaveBeenCalled() + expect(spawn).toHaveBeenCalledWith( + expect.objectContaining({ + worktreeId: TEST_WORKTREE_ID, + tabId: 'host-tab', + leafId: HEADLESS_LEAF_ID, + sessionId: expect.stringMatching(/^serve-/), + persistHostSessionBinding: true + }) + ) + expect(activated.tabs).toEqual([ + expect.objectContaining({ + id: `host-tab::${HEADLESS_LEAF_ID}`, + status: 'ready', + terminal: expect.any(String) + }) + ]) + }) + + it('keeps the target group active when phone-local activation materializes a tab', async () => { + const spawn = vi.fn().mockResolvedValue({ id: 'group-target-pty' }) + const focusTerminal = vi.fn() + const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( + makeWorkspaceSessionWithHeadlessTerminal({ + activeTabIdByWorktree: { [TEST_WORKTREE_ID]: 'host-tab' }, + tabsByWorktree: { + [TEST_WORKTREE_ID]: [ + { + id: 'host-tab', + ptyId: null, + worktreeId: TEST_WORKTREE_ID, + title: 'Left', + customTitle: null, + color: null, + sortOrder: 0, + createdAt: 1 + }, + { + id: 'host-tab-2', + ptyId: null, + worktreeId: TEST_WORKTREE_ID, + title: 'Right', + customTitle: null, + color: null, + sortOrder: 1, + createdAt: 2 + } + ] + }, + terminalLayoutsByTabId: { + 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: undefined }), + 'host-tab-2': makeHeadlessTerminalLayout({ [HEADLESS_SECOND_LEAF_ID]: undefined }) + }, + tabGroups: { + [TEST_WORKTREE_ID]: [ + { + id: 'group-left', + worktreeId: TEST_WORKTREE_ID, + activeTabId: 'host-tab', + tabOrder: ['host-tab'] + }, + { + id: 'group-right', + worktreeId: TEST_WORKTREE_ID, + activeTabId: 'host-tab-2', + tabOrder: ['host-tab-2'] + } + ] + } + }) + ) + const runtime = new OrcaRuntimeService(runtimeStore as never) + runtime.setNotifier({ + worktreesChanged: vi.fn(), + reposChanged: vi.fn(), + activateWorktree: vi.fn(), + createTerminal: vi.fn(), + revealTerminalSession: vi.fn(), + splitTerminal: vi.fn(), + renameTerminal: vi.fn(), + focusTerminal, + closeTerminal: vi.fn(), + sleepWorktree: vi.fn(), + terminalFitOverrideChanged: vi.fn(), + terminalDriverChanged: vi.fn() + }) + runtime.setPtyController({ + spawn, + write: () => true, + kill: () => true, + getForegroundProcess: async () => null + }) + + const activated = await runtime.activateMobileSessionTab( + `id:${TEST_WORKTREE_ID}`, + 'host-tab-2', + HEADLESS_SECOND_LEAF_ID, + { notifyClients: false } + ) + + expect(focusTerminal).not.toHaveBeenCalled() + expect(activated.activeGroupId).toBe('group-right') + expect(activated.tabGroups).toEqual([ + expect.objectContaining({ id: 'group-left', activeTabId: 'host-tab' }), + expect.objectContaining({ id: 'group-right', activeTabId: 'host-tab-2' }) + ]) + expect(activated.activeTabId).toBe(`host-tab-2::${HEADLESS_SECOND_LEAF_ID}`) + }) + + it('refreshes stale daemon liveness before phone-local terminal materialization', async () => { + const stalePtyId = `${TEST_WORKTREE_ID}@@stale-mobile-pty` + const spawn = vi.fn().mockResolvedValue({ id: stalePtyId }) + const listProcesses = vi.fn(async () => []) + const focusTerminal = vi.fn() + const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( + makeWorkspaceSessionWithHeadlessTerminal({ + tabsByWorktree: { + [TEST_WORKTREE_ID]: [ + { + id: 'host-tab', + ptyId: stalePtyId, + worktreeId: TEST_WORKTREE_ID, + title: 'Persisted Terminal', + customTitle: null, + color: null, + sortOrder: 0, + createdAt: 1 + } + ] + }, + terminalLayoutsByTabId: { + 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: stalePtyId }) + } + }) + ) + const runtime = new OrcaRuntimeService(runtimeStore as never) + runtime.registerPty(stalePtyId, TEST_WORKTREE_ID) + runtime.setNotifier({ + worktreesChanged: vi.fn(), + reposChanged: vi.fn(), + activateWorktree: vi.fn(), + createTerminal: vi.fn(), + revealTerminalSession: vi.fn(), + splitTerminal: vi.fn(), + renameTerminal: vi.fn(), + focusTerminal, + closeTerminal: vi.fn(), + sleepWorktree: vi.fn(), + terminalFitOverrideChanged: vi.fn(), + terminalDriverChanged: vi.fn() + }) + runtime.setPtyController({ + spawn, + write: () => true, + kill: () => true, + getForegroundProcess: async () => null, + listProcesses + }) + + const activated = await runtime.activateMobileSessionTab( + `id:${TEST_WORKTREE_ID}`, + 'host-tab', + HEADLESS_LEAF_ID, + { notifyClients: false } + ) + + expect(listProcesses).toHaveBeenCalled() + expect(focusTerminal).not.toHaveBeenCalled() + expect(spawn).toHaveBeenCalledWith( + expect.objectContaining({ + sessionId: stalePtyId, + tabId: 'host-tab', + leafId: HEADLESS_LEAF_ID + }) + ) + expect(activated.tabs).toEqual([ + expect.objectContaining({ + id: `host-tab::${HEADLESS_LEAF_ID}`, + status: 'ready', + terminal: expect.any(String) + }) + ]) + }) + it('closes browser mobile session tabs when addressed by browser workspace id', async () => { const closeSessionTab = vi.fn() const runtime = new OrcaRuntimeService(store) diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index dd9acb092b0..4c421345834 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -3665,6 +3665,7 @@ export class OrcaRuntimeService { const worktreeId = explicitWorktreeId ?? (await this.resolveWorktreeSelector(worktreeSelector)).id this.hydrateHeadlessMobileSessionTabsFromWorkspaceSession(worktreeId) + await this.refreshMobileSessionPtyRecords() const snapshot = this.mobileSessionTabsByWorktree.get(worktreeId) const directTab = snapshot?.tabs.find((candidate) => candidate.id === tabId) const tab = leafId @@ -3692,13 +3693,19 @@ export class OrcaRuntimeService { ) // Why: serve-created tabs can be visible before any renderer has adopted // their tab id, so focusing the renderer would silently no-op. + // Phone-local activation also needs this path for inactive restored tabs: + // desktop focus is intentionally suppressed, but the PTY still must exist. const shouldMaterializePendingTerminal = publicTab?.type === 'terminal' && publicTab.status !== 'ready' && - (!this.notifier?.focusTerminal || + (opts.notifyClients === false || + !this.notifier?.focusTerminal || this.shouldMaterializeHeadlessMobileSessionTab(snapshot!, tab)) if (shouldMaterializePendingTerminal) { const sessionId = tab.ptyId ?? tab.parentLayout?.ptyIdsByLeafId?.[tab.leafId] ?? undefined + const targetGroupId = snapshot?.tabGroups?.find((group) => + group.tabOrder.includes(tab.parentTabId) + )?.id try { await this.createHeadlessMobileSessionTerminal( worktreeId, @@ -3712,7 +3719,8 @@ export class OrcaRuntimeService { leafId: tab.leafId, sessionId }, - tab.launchAgent + tab.launchAgent, + targetGroupId ) } catch (err) { if (sessionId && parseAppSshPtyId(sessionId)) {