mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
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:
@@ -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'
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user