Fix Orca Mobile agent tabs opening as a plain shell instead of launching the agent (#7837)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Brennan Benson
2026-07-08 15:34:37 -07:00
committed by GitHub
co-authored by Orca
parent e218bf88f6
commit bd4f2e02db
2 changed files with 174 additions and 0 deletions
+151
View File
@@ -16553,6 +16553,157 @@ describe('OrcaRuntimeService', () => {
})
})
function makePendingAgentTabActivationRuntime(opts: { disabledTuiAgents?: string[] } = {}): {
runtime: OrcaRuntimeService
spawn: ReturnType<typeof vi.fn>
} {
const spawn = vi.fn().mockResolvedValue({ id: 'serve-materialized-pty' })
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(
makeWorkspaceSessionWithHeadlessTerminal({
tabsByWorktree: {
[TEST_WORKTREE_ID]: [
{
id: 'host-tab',
ptyId: 'serve-dead-pty',
worktreeId: TEST_WORKTREE_ID,
title: 'Terminal 1',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1,
launchAgent: 'claude'
}
]
},
terminalLayoutsByTabId: {
'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: 'serve-dead-pty' })
}
})
)
const runtime = new OrcaRuntimeService({
...runtimeStore,
getSettings: () => ({
...store.getSettings(),
disabledTuiAgents: opts.disabledTuiAgents ?? []
})
} as never)
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null,
listProcesses: async () => []
})
runtime.syncWindowGraph(0, { tabs: [], leaves: [] })
return { runtime, spawn }
}
it('launches the pending agent when mobile activation materializes an agent tab', async () => {
const { runtime, spawn } = makePendingAgentTabActivationRuntime()
const listed = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)
expect(listed.tabs[0]).toMatchObject({
type: 'terminal',
launchAgent: 'claude',
status: 'pending-handle'
})
// Why notifyClients false: this mirrors the phone tapping the tab, which is
// the path that materializes pending tabs headlessly (#7587 aftermath).
const activated = await runtime.activateMobileSessionTab(
`id:${TEST_WORKTREE_ID}`,
`host-tab::${HEADLESS_LEAF_ID}`,
undefined,
{ notifyClients: false }
)
expect(spawn).toHaveBeenCalledWith(
expect.objectContaining({
command: expect.stringContaining('claude'),
sessionId: 'serve-dead-pty',
tabId: 'host-tab',
leafId: HEADLESS_LEAF_ID,
worktreeId: TEST_WORKTREE_ID
})
)
expect(activated.tabs[0]).toMatchObject({
type: 'terminal',
launchAgent: 'claude',
status: 'ready'
})
})
it('materializes a plain shell when the pending tab has no launch agent', async () => {
const spawn = vi.fn().mockResolvedValue({ id: 'serve-materialized-pty' })
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(
makeWorkspaceSessionWithHeadlessTerminal({
tabsByWorktree: {
[TEST_WORKTREE_ID]: [
{
id: 'host-tab',
ptyId: 'serve-dead-pty',
worktreeId: TEST_WORKTREE_ID,
title: 'Terminal 1',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1
}
]
},
terminalLayoutsByTabId: {
'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: 'serve-dead-pty' })
}
})
)
const runtime = new OrcaRuntimeService(runtimeStore as never)
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null,
listProcesses: async () => []
})
runtime.syncWindowGraph(0, { tabs: [], leaves: [] })
await runtime.activateMobileSessionTab(
`id:${TEST_WORKTREE_ID}`,
`host-tab::${HEADLESS_LEAF_ID}`,
undefined,
{ notifyClients: false }
)
expect(spawn).toHaveBeenCalledWith(
expect.objectContaining({ sessionId: 'serve-dead-pty', worktreeId: TEST_WORKTREE_ID })
)
expect(spawn.mock.calls[0]![0].command).toBeUndefined()
})
it('falls back to a plain shell when the pending tab agent is disabled', async () => {
const { runtime, spawn } = makePendingAgentTabActivationRuntime({
disabledTuiAgents: ['claude']
})
const activated = await runtime.activateMobileSessionTab(
`id:${TEST_WORKTREE_ID}`,
`host-tab::${HEADLESS_LEAF_ID}`,
undefined,
{ notifyClients: false }
)
expect(spawn).toHaveBeenCalledWith(
expect.objectContaining({ sessionId: 'serve-dead-pty', worktreeId: TEST_WORKTREE_ID })
)
expect(spawn.mock.calls[0]![0].command).toBeUndefined()
// Why: the disabled-agent fallback keeps the tab's agent identity; only the
// startup command is skipped.
expect(activated.tabs[0]).toMatchObject({
type: 'terminal',
status: 'ready',
launchAgent: 'claude'
})
})
it('collapses duplicate mobile terminal entries when renderer and headless leaf ids diverge for the same pty', async () => {
const rendererLeafId = HEADLESS_SECOND_LEAF_ID
const ptyId = 'serve-persisted-pty'
+23
View File
@@ -4001,6 +4001,25 @@ export class OrcaRuntimeService {
const targetGroupId = snapshot?.tabGroups?.find((group) =>
group.tabOrder.includes(tab.parentTabId)
)?.id
// Why: a pending agent tab may exist without its startup command ever
// having been delivered (the create's renderer stalled, #7587), so a
// bare materialize would put a plain shell under the agent icon.
// Re-resolve the launch like the create path; providers skip startup
// commands when attaching to live sessions, so this cannot double-launch.
let agentStartup: Awaited<
ReturnType<OrcaRuntimeService['resolveMobileSessionTerminalCommand']>
> = {}
if (tab.launchAgent) {
try {
const workspace = await this.resolveTerminalWorkspaceLaunchScope(`id:${worktreeId}`)
agentStartup = await this.resolveMobileSessionTerminalCommand(workspace, {
agent: tab.launchAgent
})
} catch {
// Why: a disabled or unresolvable agent must not make the tab
// untappable; fall back to the plain-shell materialize.
}
}
try {
await this.createHeadlessMobileSessionTerminal(worktreeId, true, undefined, {
identity: {
@@ -4009,6 +4028,10 @@ export class OrcaRuntimeService {
sessionId
},
cwd: tab.startupCwd,
command: agentStartup.command,
env: agentStartup.env,
startupCommandDelivery: agentStartup.startupCommandDelivery,
launchConfig: agentStartup.launchConfig,
launchAgent: tab.launchAgent,
targetGroupId
})