diff --git a/src/main/runtime/rpc/methods/orchestration-inject-rejection-message.test.ts b/src/main/runtime/rpc/methods/orchestration-inject-rejection-message.test.ts new file mode 100644 index 00000000000..a7ae03b00a8 --- /dev/null +++ b/src/main/runtime/rpc/methods/orchestration-inject-rejection-message.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest' +import { buildInjectRejectionMessage } from './orchestration-inject-rejection-message' +import { TUI_AGENT_CONFIG } from '../../../../shared/tui-agent-config' +import { recognizeAgentProcess } from '../../../../shared/agent-process-recognition' + +describe('buildInjectRejectionMessage', () => { + const message = buildInjectRejectionMessage('term_a') + + it('keeps the substring callers and scripts match on', () => { + expect(message).toContain('Cannot dispatch --inject to terminal term_a') + expect(message).toContain('no recognized agent detected') + }) + + it('names every agent Orca recognizes, including agy', () => { + expect(message).toMatch(/\bagy\b/) + for (const config of Object.values(TUI_AGENT_CONFIG)) { + expect(message).toContain(config.expectedProcess) + } + }) + + it('lists only names detection actually resolves, deduped and sorted', () => { + const listed = (/\(([^)]+)\)/.exec(message)?.[1] ?? '').split(', ') + + expect(listed.length).toBeGreaterThan(0) + expect(new Set(listed).size).toBe(listed.length) + expect([...listed].sort()).toEqual(listed) + for (const name of listed) { + expect(recognizeAgentProcess(name)).not.toBeNull() + } + }) +}) diff --git a/src/main/runtime/rpc/methods/orchestration-inject-rejection-message.ts b/src/main/runtime/rpc/methods/orchestration-inject-rejection-message.ts new file mode 100644 index 00000000000..33d622ca80e --- /dev/null +++ b/src/main/runtime/rpc/methods/orchestration-inject-rejection-message.ts @@ -0,0 +1,16 @@ +import { TUI_AGENT_CONFIG } from '../../../../shared/tui-agent-config' + +// Why: the old five-name example read as an allowlist (#15125); derive from the field detection keys on so it cannot drift. +// Not filtered by `disabledTuiAgents` — that gates Orca's launchers, not detection, so a hand-started disabled agent still injects. +const RECOGNIZED_AGENT_PROCESS_NAMES = [ + ...new Set(Object.values(TUI_AGENT_CONFIG).map((config) => config.expectedProcess)) +].sort() + +export function buildInjectRejectionMessage(terminal: string): string { + return ( + `Cannot dispatch --inject to terminal ${terminal}: no recognized agent detected. ` + + `Orca detects these agent CLIs (${RECOGNIZED_AGENT_PROCESS_NAMES.join(', ')}). ` + + 'Start one in the terminal and let it finish launching, ' + + 'or dispatch without --inject and send the prompt manually.' + ) +} diff --git a/src/main/runtime/rpc/methods/orchestration-tasks-dispatch.test.ts b/src/main/runtime/rpc/methods/orchestration-tasks-dispatch.test.ts index a7b6250595d..8c6c3156c52 100644 --- a/src/main/runtime/rpc/methods/orchestration-tasks-dispatch.test.ts +++ b/src/main/runtime/rpc/methods/orchestration-tasks-dispatch.test.ts @@ -3,6 +3,7 @@ import type { RpcContext } from '../core' import { createOrchestrationRpcHarness } from './orchestration-rpc-test-harness' import type { OrchestrationDb } from '../../orchestration/db' import type { OrcaRuntimeService } from '../../orca-runtime' +import { buildInjectRejectionMessage } from './orchestration-inject-rejection-message' describe('orchestration RPC methods', () => { const h = createOrchestrationRpcHarness() @@ -389,7 +390,7 @@ describe('orchestration RPC methods', () => { to: 'term_a', inject: true }) - ).rejects.toThrow('no recognized agent detected') + ).rejects.toThrow(buildInjectRejectionMessage('term_a')) }) it('rejects dispatch to occupied terminal', async () => { diff --git a/src/main/runtime/rpc/methods/orchestration.ts b/src/main/runtime/rpc/methods/orchestration.ts index 30e0b516129..b986fa25148 100644 --- a/src/main/runtime/rpc/methods/orchestration.ts +++ b/src/main/runtime/rpc/methods/orchestration.ts @@ -27,6 +27,7 @@ import { resolveBareOrchestrationRecipient, type SendRecipientWarning } from './orchestration-recipient-routing' +import { buildInjectRejectionMessage } from './orchestration-inject-rejection-message' import { resolveRunScope } from './orchestration-run-scope' import { ORCHESTRATION_RUN_METHODS } from './orchestration-runs' import { ORCHESTRATION_WORKER_METHODS } from './orchestration-worker-methods' @@ -1643,11 +1644,7 @@ export const ORCHESTRATION_METHODS: RpcMethod[] = [ if (params.inject) { const hasAgent = await runtime.isTerminalRunningAgent(to) if (!hasAgent) { - throw new Error( - `Cannot dispatch --inject to terminal ${to}: no recognized agent detected. ` + - 'Start an agent CLI (e.g. claude, codex, gemini, droid, cursor) in the terminal first, ' + - 'or dispatch without --inject and send the prompt manually.' - ) + throw new Error(buildInjectRejectionMessage(to)) } } diff --git a/src/shared/agent-process-recognition.test.ts b/src/shared/agent-process-recognition.test.ts index 776c7bd5faf..015fb2964a1 100644 --- a/src/shared/agent-process-recognition.test.ts +++ b/src/shared/agent-process-recognition.test.ts @@ -360,6 +360,22 @@ describe('agent process recognition', () => { expect(isAgentForegroundWrapperProcess('vim.exe')).toBe(false) }) + it('recognizes the Antigravity CLI from bare, POSIX and Windows command lines', () => { + const agy = { agent: 'antigravity', processName: 'agy' } + + expect(recognizeAgentProcess('agy')).toEqual(agy) + expect(recognizeAgentProcess('/Users/dev/.local/bin/agy')).toEqual(agy) + expect(recognizeAgentProcess(String.raw`C:\Users\dev\AppData\Local\agy\bin\agy.exe`)).toEqual( + agy + ) + expect( + recognizeAgentProcessFromCommandLine( + String.raw`"C:\Users\dev\AppData\Local\agy\bin\agy.exe" --dangerously-skip-permissions` + ) + ).toEqual(agy) + expect(recognizeAgentProcessFromCommandLine('agy --dangerously-skip-permissions')).toEqual(agy) + }) + it('recognizes versioned Grok process names observed from the installed CLI', () => { expect(recognizeAgentProcess('grok-0.2.51')).toEqual({ agent: 'grok',