Files
orca/mobile/src/tasks/agent-launch-request.ts
T
Kelvin Amoaba 9b2427b958 fix(mobile): say why an agent set to Chat opened as a terminal (#38696)
* fix(mobile): say why a chat launch started a terminal instead

When the host falls back to a terminal for an agent the user set to Chat
(for example Antigravity without its chat runtime installed), show the
host's reason in the launch toast instead of opening a CLI silently.

* fix(mobile): skip the fallback notice for agents that have no chat

* fix(mobile): keep the launch notice toast up long enough to read

* refactor(agent-launch): own the chat-fallback rule in the launch contract
2026-11-05 23:48:01 +00:00

123 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// agent.launch lets the host choose chat or PTY; worktree.create with startupAgent forces an agent-first terminal.
// Build from existing create params and strip reserved agent fields so the legacy fallback stays equivalent.
import {
isAgentLaunchChatFallback,
isAgentLaunchModeReceipt,
withoutReservedAgentCreateFields,
type AgentLaunchPlacement,
type AgentLaunchPrompt,
type AgentLaunchResult
} from '../../../src/shared/agent-launch/contracts/agent-launch-intent'
import {
AGENT_LAUNCH_REPLAY_REQUIRED_RUNTIME_CAPABILITY,
AGENT_LAUNCH_RUNTIME_CAPABILITY
} from '../../../src/shared/agent-launch/contracts/agent-launch-runtime-capability'
import type { TuiAgent } from '../../../src/shared/agent-launch/catalog/tui-agent'
import type { RpcSendParams } from '../transport/rpc-params-contract'
import type { WorkspaceCreateParams } from './workspace-create-params'
// False identifies older hosts without agent.launch.
export type AgentLaunchSupport = {
replay: boolean
}
export function readAgentLaunchSupport(
capabilities: readonly string[] | null | undefined
): AgentLaunchSupport | false {
if (!capabilities?.includes(AGENT_LAUNCH_RUNTIME_CAPABILITY)) {
return false
}
return { replay: capabilities.includes(AGENT_LAUNCH_REPLAY_REQUIRED_RUNTIME_CAPABILITY) }
}
export type WorktreeCreateAgentLaunch = {
agent: TuiAgent
supported: AgentLaunchSupport | false | Promise<AgentLaunchSupport | false>
}
export type AgentLaunchCreateOutcome = {
worktreeId: AgentLaunchResult['worktreeId']
warning?: string
}
export function agentLaunchCreateParams(
agent: TuiAgent,
create: WorkspaceCreateParams,
operationId?: string | null
): RpcSendParams<'agent.launch'> {
return {
agent,
...(operationId ? { operationId } : {}),
target: { kind: 'create-worktree', create: withoutReservedAgentCreateFields(create) }
}
}
// Use the separate replay method: older hosts would strip operationId from agent.launch and run an unprotected duplicate.
export function agentLaunchExistingParams(args: {
agent: TuiAgent
worktreeId: string
operationId: string
prompt?: AgentLaunchPrompt
agentArgs?: string
surface?: 'chat' | 'terminal'
launchSource?: string
paneKey?: string
sessionId?: string
placement?: AgentLaunchPlacement
sessionOptions?: Readonly<Record<string, string>>
chatOptions?: Readonly<Record<string, string>>
}): RpcSendParams<'agent.launchReplay'> {
return {
agent: args.agent,
...(args.agentArgs !== undefined ? { agentArgs: args.agentArgs } : {}),
...(args.surface ? { surface: args.surface } : {}),
operationId: args.operationId,
target: { kind: 'existing', worktree: `id:${args.worktreeId}` },
...(args.prompt ? { prompt: args.prompt } : {}),
...(args.sessionOptions ? { sessionOptions: { ...args.sessionOptions } } : {}),
...(args.chatOptions ? { chatOptions: { ...args.chatOptions } } : {}),
...(args.launchSource ? { launchSource: args.launchSource } : {}),
...(args.paneKey ? { paneKey: args.paneKey } : {}),
...(args.sessionId ? { sessionId: args.sessionId } : {}),
...(args.placement ? { placement: args.placement } : {})
}
}
// The host publishes and activates its chosen surface before replying; navigate to the workspace without racing that snapshot.
export function readAgentLaunchCreateOutcome(result: unknown): AgentLaunchCreateOutcome | null {
if (!result || typeof result !== 'object' || !('worktreeId' in result)) {
return null
}
const worktreeId = result.worktreeId
if (typeof worktreeId !== 'string' || !worktreeId.trim()) {
return null
}
const warning = readAgentLaunchNotice(result)
return { worktreeId, ...(warning ? { warning } : {}) }
}
/** The launch reply's warning plus, when a chat-capable agent fell back to a terminal, the host's reason. */
export function readAgentLaunchNotice(source: unknown): string {
if (!source || typeof source !== 'object') {
return ''
}
const warning =
'warning' in source && typeof source.warning === 'string' ? source.warning.trim() : ''
const receipt =
'receipt' in source && isAgentLaunchModeReceipt(source.receipt) ? source.receipt : null
const fallback = receipt && isAgentLaunchChatFallback(receipt) ? receipt.detail.trim() : ''
return [warning, fallback].filter(Boolean).join(' ')
}
// Capability probing can beat this client’s advertisement; method refusal permits legacy create fallback.
export function isAgentLaunchUnsupportedRefusal(error: {
code?: string
message?: string
}): boolean {
if (error.code === 'method_not_found' || error.code === 'forbidden') {
return true
}
return (error.message ?? '').includes('agent_launch_unsupported')
}