diff --git a/mobile/src/tasks/mobile-agent-catalog.test.ts b/mobile/src/tasks/mobile-agent-catalog.test.ts index d4a97358b05..0607d5941de 100644 --- a/mobile/src/tasks/mobile-agent-catalog.test.ts +++ b/mobile/src/tasks/mobile-agent-catalog.test.ts @@ -1,43 +1,24 @@ import { describe, expect, it } from 'vitest' -import { readFileSync } from 'node:fs' -import { resolve } from 'node:path' +import { TUI_AGENT_CONFIG } from '../../../src/shared/tui-agent-config' +import { TUI_AGENT_DISPLAY_NAMES } from '../../../src/shared/tui-agent-display-names' +import { TUI_AGENT_AUTO_PICK_ORDER } from '../../../src/shared/tui-agent-selection' import { MOBILE_AGENT_CATALOG } from './mobile-agent-catalog' -import { MOBILE_TUI_AGENT_AUTO_PICK_ORDER } from './mobile-tui-agents' - -const currentDir = import.meta.dirname - -function readDesktopSharedFile(relativePath: string): string { - return readFileSync(resolve(currentDir, '../../../src/shared', relativePath), 'utf8') -} - -function parseDesktopAutoPickOrder(): string[] { - const source = readDesktopSharedFile('tui-agent-selection.ts') - const match = source.match(/TUI_AGENT_AUTO_PICK_ORDER = \[([\s\S]*?)\] as const/) - expect(match).not.toBeNull() - return Array.from(match?.[1].matchAll(/'([^']+)'/g) ?? [], (entry) => entry[1]) -} - -function parseDesktopConfiguredAgents(): string[] { - const source = readDesktopSharedFile('tui-agent-config.ts') - const match = source.match(/TUI_AGENT_CONFIG: Record = {([\s\S]*?)^}/m) - expect(match).not.toBeNull() - return Array.from( - match?.[1].matchAll(/^ (?:'([^']+)'|([a-z][a-z0-9-]*)): {/gm) ?? [], - (entry) => entry[1] ?? entry[2] - ) -} describe('mobile agent catalog', () => { - it('stays in the same order as desktop auto-pick and covers every configured TUI agent', () => { - const desktopAutoPickOrder = parseDesktopAutoPickOrder() - expect(MOBILE_TUI_AGENT_AUTO_PICK_ORDER).toEqual(desktopAutoPickOrder) - expect(MOBILE_AGENT_CATALOG.map((agent) => agent.id)).toEqual(desktopAutoPickOrder) + it('follows desktop auto-pick order and covers every configured TUI agent', () => { + expect(MOBILE_AGENT_CATALOG.map((agent) => agent.id)).toEqual([...TUI_AGENT_AUTO_PICK_ORDER]) expect(new Set(MOBILE_AGENT_CATALOG.map((agent) => agent.id))).toEqual( - new Set(parseDesktopConfiguredAgents()) + new Set(Object.keys(TUI_AGENT_CONFIG)) ) }) + it('labels every agent with the desktop display name', () => { + for (const entry of MOBILE_AGENT_CATALOG) { + expect(entry.label).toBe(TUI_AGENT_DISPLAY_NAMES[entry.id]) + } + }) + it('uses the bundled Claude icon path for Claude Agent Teams', () => { expect(MOBILE_AGENT_CATALOG.find((agent) => agent.id === 'claude-agent-teams')).toEqual( expect.not.objectContaining({ faviconDomain: expect.any(String) }) diff --git a/mobile/src/tasks/mobile-tui-agents.ts b/mobile/src/tasks/mobile-tui-agents.ts index 598e762aa77..46b7ebcb0d3 100644 --- a/mobile/src/tasks/mobile-tui-agents.ts +++ b/mobile/src/tasks/mobile-tui-agents.ts @@ -1,85 +1,17 @@ import type { TuiAgent } from '../../../src/shared/tui-agent' +import { isTuiAgent } from '../../../src/shared/tui-agent-config' +import { TUI_AGENT_DISPLAY_NAMES } from '../../../src/shared/tui-agent-display-names' +import { + TUI_AGENT_AUTO_PICK_ORDER, + isTuiAgentEnabled, + normalizeDisabledTuiAgents, + pickTuiAgent +} from '../../../src/shared/tui-agent-selection' -// Why: mobile tests run from the mobile package only, so runtime imports of -// desktop shared modules can break Vitest transforms in CI. Keep this list -// mirrored with src/shared/tui-agent-selection.ts and assert parity in tests. -export const MOBILE_TUI_AGENT_AUTO_PICK_ORDER = [ - 'claude', - 'claude-agent-teams', - 'openclaude', - 'codex', - 'grok', - 'copilot', - 'opencode', - 'mimo-code', - 'ante', - 'trae', - 'pi', - 'omp', - 'prime-agent', - 'gemini', - 'antigravity', - 'aider', - 'goose', - 'amp', - 'kilo', - 'kiro', - 'crush', - 'aug', - 'autohand', - 'cline', - 'codebuff', - 'command-code', - 'continue', - 'cursor', - 'droid', - 'kimi', - 'mistral-vibe', - 'qwen-code', - 'rovo', - 'hermes', - 'devin', - 'openclaw' -] as const satisfies readonly TuiAgent[] - -export const MOBILE_TUI_AGENT_LABELS: Record = { - claude: 'Claude', - 'claude-agent-teams': 'Claude Agent Teams', - openclaude: 'OpenClaude', - codex: 'Codex', - grok: 'Grok', - copilot: 'GitHub Copilot', - opencode: 'OpenCode', - 'mimo-code': 'MiMo Code', - ante: 'Ante', - trae: 'Trae', - pi: 'Pi', - omp: 'OMP', - 'prime-agent': 'Prime Agent', - gemini: 'Gemini', - antigravity: 'Antigravity', - aider: 'Aider', - goose: 'Goose', - amp: 'Amp', - kilo: 'Kilocode', - kiro: 'Kiro', - crush: 'Charm', - aug: 'Auggie', - autohand: 'Autohand Code', - cline: 'Cline', - codebuff: 'Codebuff', - 'command-code': 'Command Code', - continue: 'Continue', - cursor: 'Cursor', - droid: 'Droid', - kimi: 'Kimi', - 'mistral-vibe': 'Mistral Vibe', - 'qwen-code': 'Qwen Code', - rovo: 'Rovo Dev', - hermes: 'Hermes', - devin: 'Devin', - openclaw: 'OpenClaw' -} +// Why: one agent registry. Mobile keeps its own names for the favicon domains only, because +// desktop's live in the renderer catalog next to bundled `?url` icon imports Metro can't load. +export const MOBILE_TUI_AGENT_AUTO_PICK_ORDER = TUI_AGENT_AUTO_PICK_ORDER +export const MOBILE_TUI_AGENT_LABELS: Record = TUI_AGENT_DISPLAY_NAMES export const MOBILE_TUI_AGENT_FAVICON_DOMAINS: Partial> = { openclaude: 'openclaude.gitlawb.com', @@ -115,33 +47,15 @@ export const MOBILE_TUI_AGENT_FAVICON_DOMAINS: Partial> openclaw: 'openclaw.ai' } -export function isMobileTuiAgent(value: unknown): value is TuiAgent { - return MOBILE_TUI_AGENT_AUTO_PICK_ORDER.includes(value as TuiAgent) -} +export const isMobileTuiAgent: (value: unknown) => value is TuiAgent = isTuiAgent -function normalizeDisabledMobileTuiAgents(value: unknown): TuiAgent[] { - if (!Array.isArray(value)) { - return [] - } - const seen = new Set() - for (const item of value) { - if (isMobileTuiAgent(item)) { - seen.add(item) - } - } - return [...seen] +// Why: mobile passes raw persisted settings through; the shared helpers already discard non-arrays. +function asDisabledList(disabled: unknown): Iterable | null { + return Array.isArray(disabled) ? disabled : null } export function isMobileTuiAgentEnabled(agent: TuiAgent, disabled?: unknown): boolean { - return !normalizeDisabledMobileTuiAgents(disabled).includes(agent) -} - -export function filterEnabledMobileTuiAgents( - agents: Iterable, - disabled?: unknown -): T[] { - const disabledSet = new Set(normalizeDisabledMobileTuiAgents(disabled)) - return [...agents].filter((agent) => !disabledSet.has(agent)) + return isTuiAgentEnabled(agent, asDisabledList(disabled)) } export function pickMobileTuiAgent( @@ -149,18 +63,13 @@ export function pickMobileTuiAgent( detected: Iterable, disabled?: unknown ): TuiAgent | null { - if (preferred === 'blank') { - return null - } - const disabledSet = new Set(normalizeDisabledMobileTuiAgents(disabled)) - const detectedSet = detected instanceof Set ? detected : new Set(detected) - if (preferred && detectedSet.has(preferred) && !disabledSet.has(preferred)) { - return preferred - } - for (const agent of MOBILE_TUI_AGENT_AUTO_PICK_ORDER) { - if (detectedSet.has(agent) && !disabledSet.has(agent)) { - return agent - } - } - return null + return pickTuiAgent(preferred, detected, asDisabledList(disabled)) +} + +export function filterEnabledMobileTuiAgents( + agents: Iterable, + disabled?: unknown +): T[] { + const disabledSet = new Set(normalizeDisabledTuiAgents(disabled)) + return [...agents].filter((agent) => !disabledSet.has(agent)) } diff --git a/src/main/ipc/agent-hooks.test.ts b/src/main/ipc/agent-hooks.test.ts index 48440713cd0..e9e7a728e83 100644 --- a/src/main/ipc/agent-hooks.test.ts +++ b/src/main/ipc/agent-hooks.test.ts @@ -208,72 +208,6 @@ describe('agentStatus:getSnapshot IPC', () => { }) }) -describe('agentHooks:antigravityStatus IPC', () => { - it('returns Antigravity hook installation status', async () => { - const { registerAgentHookHandlers } = await import('./agent-hooks') - registerAgentHookHandlers() - - const handler = handleHandlers.get('agentHooks:antigravityStatus') - expect(handler).toBeDefined() - expect(handler!({})).toEqual({ agent: 'antigravity', state: 'absent' }) - }) -}) - -describe('agentHooks:ampStatus IPC', () => { - it('returns Amp hook installation status', async () => { - const { registerAgentHookHandlers } = await import('./agent-hooks') - registerAgentHookHandlers() - - const handler = handleHandlers.get('agentHooks:ampStatus') - expect(handler).toBeDefined() - expect(handler!({})).toEqual({ agent: 'amp', state: 'absent' }) - }) -}) - -describe('agentHooks:openClaudeStatus IPC', () => { - it('returns OpenClaude hook installation status', async () => { - const { registerAgentHookHandlers } = await import('./agent-hooks') - registerAgentHookHandlers() - - const handler = handleHandlers.get('agentHooks:openClaudeStatus') - expect(handler).toBeDefined() - expect(handler!({})).toEqual({ agent: 'openclaude', state: 'absent' }) - }) -}) - -describe('agentHooks:commandCodeStatus IPC', () => { - it('returns Command Code hook installation status', async () => { - const { registerAgentHookHandlers } = await import('./agent-hooks') - registerAgentHookHandlers() - - const handler = handleHandlers.get('agentHooks:commandCodeStatus') - expect(handler).toBeDefined() - expect(handler!({})).toEqual({ agent: 'command-code', state: 'absent' }) - }) -}) - -describe('agentHooks:devinStatus IPC', () => { - it('returns Devin hook installation status', async () => { - const { registerAgentHookHandlers } = await import('./agent-hooks') - registerAgentHookHandlers() - - const handler = handleHandlers.get('agentHooks:devinStatus') - expect(handler).toBeDefined() - expect(handler!({})).toEqual({ agent: 'devin', state: 'absent' }) - }) -}) - -describe('agentHooks:kimiStatus IPC', () => { - it('returns Kimi hook installation status', async () => { - const { registerAgentHookHandlers } = await import('./agent-hooks') - registerAgentHookHandlers() - - const handler = handleHandlers.get('agentHooks:kimiStatus') - expect(handler).toBeDefined() - expect(handler!({})).toEqual({ agent: 'kimi', state: 'absent' }) - }) -}) - describe('agentStatus:inferInterrupt IPC', () => { it('forwards valid inference requests to the hook server', async () => { inferInterrupt.mockReturnValue(true) diff --git a/src/main/ipc/agent-hooks.ts b/src/main/ipc/agent-hooks.ts index 5615fa38208..ee858e3cca8 100644 --- a/src/main/ipc/agent-hooks.ts +++ b/src/main/ipc/agent-hooks.ts @@ -1,5 +1,4 @@ import { ipcMain } from 'electron' -import type { AgentHookInstallStatus } from '../../shared/agent-hook-types' import type { AgentStatusIpcPayload, MigrationUnsupportedPtyEntry @@ -7,21 +6,7 @@ import type { import type { AgentInterruptInferenceRequest } from '../../shared/agent-interrupt-intent' import type { AgentQuestionAnsweredInferenceRequest } from '../../shared/agent-question-answered-intent' import { agentHookServer } from '../agent-hooks/server' -import { ampHookService } from '../amp/hook-service' import { getMigrationUnsupportedPtySnapshot } from '../agent-hooks/migration-unsupported-pty-state' -import { claudeHookService } from '../claude/hook-service' -import { codexHookService } from '../codex/hook-service' -import { geminiHookService } from '../gemini/hook-service' -import { antigravityHookService } from '../antigravity/hook-service' -import { cursorHookService } from '../cursor/hook-service' -import { droidHookService } from '../droid/hook-service' -import { commandCodeHookService } from '../command-code/hook-service' -import { grokHookService } from '../grok/hook-service' -import { copilotHookService } from '../copilot/hook-service' -import { hermesHookService } from '../hermes/hook-service' -import { devinHookService } from '../devin/hook-service' -import { kimiHookService } from '../kimi/hook-service' -import { openClaudeHookService } from '../openclaude/hook-service' import { registerAgentPaneAuthorityIpcHandlers } from './agent-pane-authority-ipc' import { registerAgentStatusRowTeardownIpcHandlers } from './agent-status-row-teardown-ipc' import { createAgentPaneAuthorityOwnership } from './agent-pane-authority-ownership' @@ -48,20 +33,6 @@ export function registerAgentHookHandlers( // recreates the main window). Today the module-level `registered` guard in // register-core-handlers.ts prevents re-entry, but decoupling from that guard // future-proofs this file. - ipcMain.removeHandler('agentHooks:claudeStatus') - ipcMain.removeHandler('agentHooks:openClaudeStatus') - ipcMain.removeHandler('agentHooks:codexStatus') - ipcMain.removeHandler('agentHooks:geminiStatus') - ipcMain.removeHandler('agentHooks:antigravityStatus') - ipcMain.removeHandler('agentHooks:ampStatus') - ipcMain.removeHandler('agentHooks:cursorStatus') - ipcMain.removeHandler('agentHooks:droidStatus') - ipcMain.removeHandler('agentHooks:commandCodeStatus') - ipcMain.removeHandler('agentHooks:grokStatus') - ipcMain.removeHandler('agentHooks:copilotStatus') - ipcMain.removeHandler('agentHooks:hermesStatus') - ipcMain.removeHandler('agentHooks:devinStatus') - ipcMain.removeHandler('agentHooks:kimiStatus') ipcMain.removeHandler('agentStatus:getSnapshot') ipcMain.removeHandler('agentStatus:inferInterrupt') ipcMain.removeHandler('agentStatus:inferQuestionAnswered') @@ -98,192 +69,4 @@ export function registerAgentHookHandlers( 'agentStatus:getMigrationUnsupportedSnapshot', (): MigrationUnsupportedPtyEntry[] => getMigrationUnsupportedPtySnapshot() ) - - // Why: errors from getStatus() (fs permission denied, homedir resolution - // failure, etc.) must be reported inline via state:'error' so the sidebar can - // render a coherent per-agent error row. Letting the exception propagate out - // of the IPC handler surfaces as an unhandled renderer-side rejection, which - // defeats the AgentHookInstallStatus contract the UI relies on. - ipcMain.handle('agentHooks:claudeStatus', (): AgentHookInstallStatus => { - try { - return claudeHookService.getStatus() - } catch (err) { - return { - agent: 'claude', - state: 'error', - configPath: '', - managedHooksPresent: false, - detail: err instanceof Error ? err.message : String(err) - } - } - }) - ipcMain.handle('agentHooks:openClaudeStatus', (): AgentHookInstallStatus => { - try { - return openClaudeHookService.getStatus() - } catch (err) { - return { - agent: 'openclaude', - state: 'error', - configPath: '', - managedHooksPresent: false, - detail: err instanceof Error ? err.message : String(err) - } - } - }) - ipcMain.handle('agentHooks:codexStatus', (): AgentHookInstallStatus => { - try { - return codexHookService.getStatus() - } catch (err) { - return { - agent: 'codex', - state: 'error', - configPath: '', - managedHooksPresent: false, - detail: err instanceof Error ? err.message : String(err) - } - } - }) - ipcMain.handle('agentHooks:geminiStatus', (): AgentHookInstallStatus => { - try { - return geminiHookService.getStatus() - } catch (err) { - return { - agent: 'gemini', - state: 'error', - configPath: '', - managedHooksPresent: false, - detail: err instanceof Error ? err.message : String(err) - } - } - }) - ipcMain.handle('agentHooks:antigravityStatus', (): AgentHookInstallStatus => { - try { - return antigravityHookService.getStatus() - } catch (err) { - return { - agent: 'antigravity', - state: 'error', - configPath: '', - managedHooksPresent: false, - detail: err instanceof Error ? err.message : String(err) - } - } - }) - ipcMain.handle('agentHooks:ampStatus', (): AgentHookInstallStatus => { - try { - return ampHookService.getStatus() - } catch (err) { - return { - agent: 'amp', - state: 'error', - configPath: '', - managedHooksPresent: false, - detail: err instanceof Error ? err.message : String(err) - } - } - }) - ipcMain.handle('agentHooks:cursorStatus', (): AgentHookInstallStatus => { - try { - return cursorHookService.getStatus() - } catch (err) { - return { - agent: 'cursor', - state: 'error', - configPath: '', - managedHooksPresent: false, - detail: err instanceof Error ? err.message : String(err) - } - } - }) - ipcMain.handle('agentHooks:droidStatus', (): AgentHookInstallStatus => { - try { - return droidHookService.getStatus() - } catch (err) { - return { - agent: 'droid', - state: 'error', - configPath: '', - managedHooksPresent: false, - detail: err instanceof Error ? err.message : String(err) - } - } - }) - ipcMain.handle('agentHooks:commandCodeStatus', (): AgentHookInstallStatus => { - try { - return commandCodeHookService.getStatus() - } catch (err) { - return { - agent: 'command-code', - state: 'error', - configPath: '', - managedHooksPresent: false, - detail: err instanceof Error ? err.message : String(err) - } - } - }) - ipcMain.handle('agentHooks:grokStatus', (): AgentHookInstallStatus => { - try { - return grokHookService.getStatus() - } catch (err) { - return { - agent: 'grok', - state: 'error', - configPath: '', - managedHooksPresent: false, - detail: err instanceof Error ? err.message : String(err) - } - } - }) - ipcMain.handle('agentHooks:copilotStatus', (): AgentHookInstallStatus => { - try { - return copilotHookService.getStatus() - } catch (err) { - return { - agent: 'copilot', - state: 'error', - configPath: '', - managedHooksPresent: false, - detail: err instanceof Error ? err.message : String(err) - } - } - }) - ipcMain.handle('agentHooks:hermesStatus', (): AgentHookInstallStatus => { - try { - return hermesHookService.getStatus() - } catch (err) { - return { - agent: 'hermes', - state: 'error', - configPath: '', - managedHooksPresent: false, - detail: err instanceof Error ? err.message : String(err) - } - } - }) - ipcMain.handle('agentHooks:devinStatus', (): AgentHookInstallStatus => { - try { - return devinHookService.getStatus() - } catch (err) { - return { - agent: 'devin', - state: 'error', - configPath: '', - managedHooksPresent: false, - detail: err instanceof Error ? err.message : String(err) - } - } - }) - ipcMain.handle('agentHooks:kimiStatus', (): AgentHookInstallStatus => { - try { - return kimiHookService.getStatus() - } catch (err) { - return { - agent: 'kimi', - state: 'error', - configPath: '', - managedHooksPresent: false, - detail: err instanceof Error ? err.message : String(err) - } - } - }) } diff --git a/src/preload/api-types.ts b/src/preload/api-types.ts index b1d9c7bce55..632e53005ae 100644 --- a/src/preload/api-types.ts +++ b/src/preload/api-types.ts @@ -6,7 +6,7 @@ import type { GrokAccountsApi, MinimaxCredentialsApi } from './api/agent-account-api' -import type { AgentHooksApi, HooksApi } from './api/agent-hook-api' +import type { HooksApi } from './api/agent-hook-api' import type { SkillsApi } from './api/agent-skill-api' import type { AgentAwakeApi, AgentStatusApi, AgentTrustApi } from './api/agent-status-api' import type { @@ -104,7 +104,6 @@ export type PreloadApi = { claudeAccounts: ClaudeAccountsApi cli: CliApi codexConfigSync: CodexConfigSyncApi - agentHooks: AgentHooksApi agentTrust: AgentTrustApi preflight: PreflightApi notifications: NotificationsApi diff --git a/src/preload/api/agent-hook-api.ts b/src/preload/api/agent-hook-api.ts index c4116aa2b6d..c01e8e9d23d 100644 --- a/src/preload/api/agent-hook-api.ts +++ b/src/preload/api/agent-hook-api.ts @@ -2,7 +2,6 @@ import type { OrcaHooks } from '../../shared/orca-yaml-hook-types' import type { WorktreeSetupLaunch } from '../../shared/worktree/launch-types' import type { ExecutionHostId } from '../../shared/execution-host' import type { SetupScriptImportCandidate } from '../../shared/setup-script-imports' -import type { AgentHookInstallStatus } from '../../shared/agent-hook-types' export type HooksApi = { check: (args: { repoId: string; hostId?: ExecutionHostId }) => Promise<{ @@ -34,19 +33,3 @@ export type HooksApi = { hostId?: ExecutionHostId }) => Promise } - -export type AgentHooksApi = { - claudeStatus: () => Promise - openClaudeStatus: () => Promise - codexStatus: () => Promise - geminiStatus: () => Promise - antigravityStatus: () => Promise - ampStatus: () => Promise - cursorStatus: () => Promise - droidStatus: () => Promise - commandCodeStatus: () => Promise - grokStatus: () => Promise - copilotStatus: () => Promise - hermesStatus: () => Promise - devinStatus: () => Promise -} diff --git a/src/preload/index.ts b/src/preload/index.ts index f8134289b74..419c2dcbad6 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -34,7 +34,6 @@ import type { } from '../shared/terminal-preview' import type { AgentSessionPtyWriteRefusal } from '../shared/agent-session-pty-write-admission' import type { CliInstallStatus } from '../shared/cli-install-types' -import type { AgentHookInstallStatus } from '../shared/agent-hook-types' import type { CodexConfigSyncStatus } from '../shared/codex-config-sync-types' import type { TerminalPaneSplitSource } from '../shared/feature-education-telemetry' import type { TerminalTabCreateReply } from '../shared/terminal-reveal-identity' @@ -2314,34 +2313,6 @@ const api = { codexConfigSync: { status: (): Promise => ipcRenderer.invoke('codexConfigSync:status') }, - agentHooks: { - claudeStatus: (): Promise => - ipcRenderer.invoke('agentHooks:claudeStatus'), - openClaudeStatus: (): Promise => - ipcRenderer.invoke('agentHooks:openClaudeStatus'), - codexStatus: (): Promise => - ipcRenderer.invoke('agentHooks:codexStatus'), - geminiStatus: (): Promise => - ipcRenderer.invoke('agentHooks:geminiStatus'), - antigravityStatus: (): Promise => - ipcRenderer.invoke('agentHooks:antigravityStatus'), - ampStatus: (): Promise => ipcRenderer.invoke('agentHooks:ampStatus'), - cursorStatus: (): Promise => - ipcRenderer.invoke('agentHooks:cursorStatus'), - droidStatus: (): Promise => - ipcRenderer.invoke('agentHooks:droidStatus'), - commandCodeStatus: (): Promise => - ipcRenderer.invoke('agentHooks:commandCodeStatus'), - grokStatus: (): Promise => ipcRenderer.invoke('agentHooks:grokStatus'), - devinStatus: (): Promise => - ipcRenderer.invoke('agentHooks:devinStatus'), - copilotStatus: (): Promise => - ipcRenderer.invoke('agentHooks:copilotStatus'), - hermesStatus: (): Promise => - ipcRenderer.invoke('agentHooks:hermesStatus'), - kimiStatus: (): Promise => ipcRenderer.invoke('agentHooks:kimiStatus') - }, - agentTrust: { markTrusted: (args: { preset: 'cursor' | 'copilot' | 'codex' diff --git a/src/renderer/src/web/preload-api/web-agent-hooks-api.ts b/src/renderer/src/web/preload-api/web-agent-hooks-api.ts deleted file mode 100644 index 41ed387d559..00000000000 --- a/src/renderer/src/web/preload-api/web-agent-hooks-api.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { PreloadApi } from '../../../../preload/api-types' - -export function createAgentHooksApi(): NonNullable['agentHooks']> { - const status = ( - agent: - | 'claude' - | 'openclaude' - | 'codex' - | 'gemini' - | 'antigravity' - | 'amp' - | 'cursor' - | 'droid' - | 'command-code' - | 'grok' - | 'copilot' - | 'hermes' - | 'devin' - ) => - Promise.resolve({ - agent, - state: 'not_installed', - configPath: '', - managedHooksPresent: false, - detail: 'Agent hook status is only available on the Orca server.' - } as const) - return { - claudeStatus: () => status('claude'), - openClaudeStatus: () => status('openclaude'), - codexStatus: () => status('codex'), - geminiStatus: () => status('gemini'), - antigravityStatus: () => status('antigravity'), - ampStatus: () => status('amp'), - cursorStatus: () => status('cursor'), - droidStatus: () => status('droid'), - commandCodeStatus: () => status('command-code'), - grokStatus: () => status('grok'), - copilotStatus: () => status('copilot'), - hermesStatus: () => status('hermes'), - devinStatus: () => status('devin') - } -} diff --git a/src/renderer/src/web/web-preload-api-composition.test.ts b/src/renderer/src/web/web-preload-api-composition.test.ts index 85769ffa45c..b1199332d95 100644 --- a/src/renderer/src/web/web-preload-api-composition.test.ts +++ b/src/renderer/src/web/web-preload-api-composition.test.ts @@ -58,7 +58,6 @@ describe('web preload API composition', () => { 'codexAccounts', 'claudeAccounts', 'cli', - 'agentHooks', 'macosTccPrompts', 'codexConfigSync', 'developerPermissions', diff --git a/src/renderer/src/web/web-preload-api.ts b/src/renderer/src/web/web-preload-api.ts index 50a63270d80..a53b5e0ca06 100644 --- a/src/renderer/src/web/web-preload-api.ts +++ b/src/renderer/src/web/web-preload-api.ts @@ -6,7 +6,6 @@ import { createGrokAccountsApi, createMiniMaxCredentialsApi } from './preload-api/web-agent-accounts-api' -import { createAgentHooksApi } from './preload-api/web-agent-hooks-api' import { createWebAgentStatusApi } from './preload-api/web-agent-status-api' import { createWebAiVaultApi } from './preload-api/web-ai-vault-api' import { createWebAppApi } from './preload-api/web-app-api' @@ -111,7 +110,6 @@ function createWebPreloadApi(): Partial { codexAccounts: createAccountsApi(), claudeAccounts: createAccountsApi(), cli: createCliApi(), - agentHooks: createAgentHooksApi(), macosTccPrompts: createMacosTccPromptsApi(), codexConfigSync: { status: () => diff --git a/src/shared/tui-agent-config.test.ts b/src/shared/tui-agent-config.test.ts new file mode 100644 index 00000000000..3ada94778b0 --- /dev/null +++ b/src/shared/tui-agent-config.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest' +import { TUI_AGENT_CONFIG } from './tui-agent-config' +import type { TuiAgent } from './tui-agent' + +describe('TUI_AGENT_CONFIG', () => { + it('resolves launchCmd and expectedProcess for every agent', () => { + for (const [agent, config] of Object.entries(TUI_AGENT_CONFIG)) { + expect(config.launchCmd, agent).toBeTruthy() + expect(config.expectedProcess, agent).toBeTruthy() + } + }) + + it('defaults launchCmd and expectedProcess to detectCmd', () => { + expect(TUI_AGENT_CONFIG.codex).toMatchObject({ + detectCmd: 'codex', + launchCmd: 'codex', + expectedProcess: 'codex' + }) + }) + + it('keeps explicit overrides where the launch line or process differs from the binary', () => { + const overrides: Partial>> = { + 'claude-agent-teams': { launchCmd: 'orca claude-teams', expectedProcess: 'claude' }, + kiro: { launchCmd: 'kiro-cli chat --tui', expectedProcess: 'kiro-cli' }, + 'command-code': { launchCmd: 'command-code --trust' }, + hermes: { launchCmd: 'hermes --tui' } + } + for (const [agent, expected] of Object.entries(overrides)) { + expect(TUI_AGENT_CONFIG[agent as TuiAgent]).toMatchObject(expected) + } + }) +}) diff --git a/src/shared/tui-agent-config.ts b/src/shared/tui-agent-config.ts index 4a89e1cb13b..664c0e39106 100644 --- a/src/shared/tui-agent-config.ts +++ b/src/shared/tui-agent-config.ts @@ -52,11 +52,23 @@ export type TuiAgentConfig = { ctrlEnterEncoding?: 'csi-u' } -export const TUI_AGENT_CONFIG: Record = { +/** Authoring form: `launchCmd` and `expectedProcess` default to `detectCmd` (true for most agents). */ +type TuiAgentConfigSource = Omit & { + launchCmd?: string + expectedProcess?: string +} + +function resolveTuiAgentConfig(source: TuiAgentConfigSource): TuiAgentConfig { + return { + ...source, + launchCmd: source.launchCmd ?? source.detectCmd, + expectedProcess: source.expectedProcess ?? source.detectCmd + } +} + +const TUI_AGENT_CONFIG_SOURCE: Record = { claude: { detectCmd: 'claude', - launchCmd: 'claude', - expectedProcess: 'claude', promptInjectionMode: 'argv', // Why: `claude --prefill ` seeds the input without submitting, avoiding the paste-after-ready race (PR https://github.com/stablyai/orca/pull/926). draftPromptFlag: '--prefill' @@ -79,15 +91,11 @@ export const TUI_AGENT_CONFIG: Record = { }, openclaude: { detectCmd: 'openclaude', - launchCmd: 'openclaude', - expectedProcess: 'openclaude', promptInjectionMode: 'argv', draftPromptFlag: '--prefill' }, codex: { detectCmd: 'codex', - launchCmd: 'codex', - expectedProcess: 'codex', promptInjectionMode: 'argv', windowsInputRecordPasteNewline: 'alt-enter', preflightTrust: 'codex', @@ -97,14 +105,10 @@ export const TUI_AGENT_CONFIG: Record = { }, autohand: { detectCmd: 'autohand', - launchCmd: 'autohand', - expectedProcess: 'autohand', promptInjectionMode: 'stdin-after-start' }, ante: { detectCmd: 'ante', - launchCmd: 'ante', - expectedProcess: 'ante', // Why: `ante --prompt` is headless (runs once and exits), so launch the bare TUI and inject after startup. promptInjectionMode: 'stdin-after-start' }, @@ -112,8 +116,6 @@ export const TUI_AGENT_CONFIG: Record = { // Why: the unrelated open-source bytedance/trae-agent also installs a `trae-cli` // binary, so detect TRAE CN's CLI on `traecli`, an alias only TRAE CN ships. detectCmd: 'traecli', - launchCmd: 'traecli', - expectedProcess: 'traecli', // Why: `traecli [prompt]` takes the task as a positional argv, same as Claude/Codex. promptInjectionMode: 'argv', // Why: separator so prompts starting with `help`/`config`/`-…` aren't parsed as a @@ -122,24 +124,18 @@ export const TUI_AGENT_CONFIG: Record = { }, opencode: { detectCmd: 'opencode', - launchCmd: 'opencode', - expectedProcess: 'opencode', promptInjectionMode: 'flag-prompt', // Why: opencode enables bracketed paste before its composer mounts; wait for the post-\x1b[?2004h show-cursor so paste lands. draftPasteReadySignal: 'render-cursor-after-bracketed-paste' }, 'mimo-code': { detectCmd: 'mimo', - launchCmd: 'mimo', - expectedProcess: 'mimo', promptInjectionMode: 'flag-prompt', // Why: mirrors opencode's cursor-gated signal by parity; mimo's startup stream isn't separately validated. draftPasteReadySignal: 'render-cursor-after-bracketed-paste' }, pi: { detectCmd: 'pi', - launchCmd: 'pi', - expectedProcess: 'pi', promptInjectionMode: 'argv', // Why: pi has no `--prefill` and paste-after-ready races its long startup; the orca-prefill extension seeds this env var instead. draftPromptEnvVar: 'ORCA_PI_PREFILL', @@ -148,8 +144,6 @@ export const TUI_AGENT_CONFIG: Record = { }, omp: { detectCmd: 'omp', - launchCmd: 'omp', - expectedProcess: 'omp', promptInjectionMode: 'argv', draftPromptEnvVar: 'ORCA_OMP_PREFILL', // Why: OMP wraps Pi's TUI, so the bytes land in a Pi reader that decodes CSI-u (see pi above). @@ -157,8 +151,6 @@ export const TUI_AGENT_CONFIG: Record = { }, 'prime-agent': { detectCmd: 'prime-agent', - launchCmd: 'prime-agent', - expectedProcess: 'prime-agent', // Why: `prime-agent [options] [@files...] [message...]` takes the task as positional argv. promptInjectionMode: 'argv', // Why: separator so prompts starting with `help`/`agents`/`-…` aren't parsed as a @@ -169,38 +161,26 @@ export const TUI_AGENT_CONFIG: Record = { }, gemini: { detectCmd: 'gemini', - launchCmd: 'gemini', - expectedProcess: 'gemini', promptInjectionMode: 'flag-prompt-interactive' }, antigravity: { detectCmd: 'agy', - launchCmd: 'agy', - expectedProcess: 'agy', promptInjectionMode: 'flag-prompt-interactive' }, aider: { detectCmd: 'aider', - launchCmd: 'aider', - expectedProcess: 'aider', promptInjectionMode: 'stdin-after-start' }, goose: { detectCmd: 'goose', - launchCmd: 'goose', - expectedProcess: 'goose', promptInjectionMode: 'stdin-after-start' }, amp: { detectCmd: 'amp', - launchCmd: 'amp', - expectedProcess: 'amp', promptInjectionMode: 'stdin-after-start' }, kilo: { detectCmd: 'kilo', - launchCmd: 'kilo', - expectedProcess: 'kilo', promptInjectionMode: 'stdin-after-start' }, kiro: { @@ -208,32 +188,23 @@ export const TUI_AGENT_CONFIG: Record = { detectCmd: 'kiro-cli', // Why: trust flags like --trust-all-tools attach to Kiro's `chat` subcommand, not top-level kiro-cli. launchCmd: 'kiro-cli chat --tui', - expectedProcess: 'kiro-cli', promptInjectionMode: 'stdin-after-start' }, crush: { detectCmd: 'crush', - launchCmd: 'crush', - expectedProcess: 'crush', promptInjectionMode: 'stdin-after-start' }, aug: { // Why: @augmentcode/auggie installs a binary named `auggie`, not `aug`; keep id 'aug' for stored prefs. detectCmd: 'auggie', - launchCmd: 'auggie', - expectedProcess: 'auggie', promptInjectionMode: 'stdin-after-start' }, cline: { detectCmd: 'cline', - launchCmd: 'cline', - expectedProcess: 'cline', promptInjectionMode: 'stdin-after-start' }, codebuff: { detectCmd: 'codebuff', - launchCmd: 'codebuff', - expectedProcess: 'codebuff', promptInjectionMode: 'stdin-after-start' }, 'command-code': { @@ -241,28 +212,21 @@ export const TUI_AGENT_CONFIG: Record = { detectCmd: 'command-code', // Why: `--trust` skips the first-run trust prompt so it doesn't consume the task text. launchCmd: 'command-code --trust', - expectedProcess: 'command-code', promptInjectionMode: 'argv' }, continue: { // Why: Continue's CLI binary is `cn`; `continue` is a bash/zsh builtin and would resolve to the shell keyword. detectCmd: 'cn', - launchCmd: 'cn', - expectedProcess: 'cn', promptInjectionMode: 'stdin-after-start' }, cursor: { detectCmd: 'cursor-agent', - launchCmd: 'cursor-agent', - expectedProcess: 'cursor-agent', promptInjectionMode: 'argv', // Why: first-launch trust menu swallows the bracketed paste; pre-write the .workspace-trusted marker so it skips (agent-trust-presets.ts). preflightTrust: 'cursor' }, droid: { detectCmd: 'droid', - launchCmd: 'droid', - expectedProcess: 'droid', promptInjectionMode: 'argv', // Why: Droid decodes CSI-u on Windows; the legacy Esc+CR fallback reads as Enter and submits instead of newline. windowsShiftEnterEncoding: 'csi-u', @@ -270,49 +234,36 @@ export const TUI_AGENT_CONFIG: Record = { }, kimi: { detectCmd: 'kimi', - launchCmd: 'kimi', - expectedProcess: 'kimi', promptInjectionMode: 'stdin-after-start' }, 'mistral-vibe': { // Why: installer exposes binary `vibe` though the package is mistral-vibe; keep old name as alias for wrapped installs. detectCmd: 'vibe', detectCmdAliases: ['mistral-vibe'], - launchCmd: 'vibe', - expectedProcess: 'vibe', promptInjectionMode: 'stdin-after-start' }, 'qwen-code': { // Why: package is qwen-code but its installed CLI binary on PATH is `qwen`. detectCmd: 'qwen', - launchCmd: 'qwen', - expectedProcess: 'qwen', promptInjectionMode: 'stdin-after-start' }, rovo: { detectCmd: 'rovo', - launchCmd: 'rovo', - expectedProcess: 'rovo', promptInjectionMode: 'stdin-after-start' }, hermes: { detectCmd: 'hermes', // Why: bare `hermes` opens the classic REPL; `--tui` starts the full-screen agent UI Orca hosts. launchCmd: 'hermes --tui', - expectedProcess: 'hermes', // Why: Hermes delivers the prompt via its startup-query contract, submitting only after the composer is ready. promptInjectionMode: 'hermes-query' }, openclaw: { detectCmd: 'openclaw', - launchCmd: 'openclaw', - expectedProcess: 'openclaw', promptInjectionMode: 'stdin-after-start' }, copilot: { detectCmd: 'copilot', - launchCmd: 'copilot', - expectedProcess: 'copilot', // Why: `--prompt` exits on completion (kills the hosted session); `-i/--interactive` keeps it interactive. promptInjectionMode: 'flag-interactive', // Why: first-launch trust menu swallows the bracketed paste; pre-write trust so it skips (see agent-trust-presets.ts). @@ -320,8 +271,6 @@ export const TUI_AGENT_CONFIG: Record = { }, grok: { detectCmd: 'grok', - launchCmd: 'grok', - expectedProcess: 'grok', // Why: argv (grok takes a positional prompt) so multi-line/special-char text isn't mangled as raw PTY keystrokes. promptInjectionMode: 'argv', // Why: separator so prompts like `help`/`--version` aren't parsed as Grok CLI syntax. @@ -334,13 +283,18 @@ export const TUI_AGENT_CONFIG: Record = { }, devin: { detectCmd: 'devin', - launchCmd: 'devin', - expectedProcess: 'devin', // Why: `devin -- ` auto-submits immediately (docs.devin.ai/cli), so start the REPL with no argv prompt. promptInjectionMode: 'stdin-after-start' } } +export const TUI_AGENT_CONFIG: Record = Object.fromEntries( + Object.entries(TUI_AGENT_CONFIG_SOURCE).map(([agent, source]) => [ + agent, + resolveTuiAgentConfig(source) + ]) +) as Record + export function isTuiAgent(value: unknown): value is TuiAgent { return typeof value === 'string' && Object.hasOwn(TUI_AGENT_CONFIG, value) }