mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 10:27:33 +00:00
* 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
123 lines
4.6 KiB
TypeScript
123 lines
4.6 KiB
TypeScript
// 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')
|
||
}
|