mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
refactor(agents): remove dead hook IPC and derive shared agent defaults (#16089)
* refactor(agent-hooks): drop the unused per-agent hook status IPC surface No renderer, CLI, or mobile caller invoked window.api.agentHooks.*Status; main already reads install status through MANAGED_AGENT_HOOK_STATUS_READERS. The 14 handlers had also drifted (kimiStatus existed in main/preload but not in AgentHooksApi or the web stub). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(tui-agent-config): default launchCmd and expectedProcess to detectCmd 32 of 36 entries repeated the binary name three times. Entries are now authored in a source form where both default to detectCmd and resolved once at module load, so TUI_AGENT_CONFIG keeps its exact shape for consumers (verified equal to the previous table). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(mobile): derive the agent order, labels, and picker from src/shared The mobile mirror (and its regex-over-desktop-source parity test) predates mobile importing runtime values from src/shared, which it now does in a dozen modules. Only the favicon-domain map stays mobile-local because desktop's lives in the renderer catalog next to bundled ?url imports. The parity test now imports the real registries and also checks label parity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(web): align preload surface after hook IPC removal --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude Fable 5
Brennan Benson
parent
fe82569b97
commit
a651e81843
@@ -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<TuiAgent, TuiAgentConfig> = {([\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) })
|
||||
|
||||
@@ -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<TuiAgent, string> = {
|
||||
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<TuiAgent, string> = TUI_AGENT_DISPLAY_NAMES
|
||||
|
||||
export const MOBILE_TUI_AGENT_FAVICON_DOMAINS: Partial<Record<TuiAgent, string>> = {
|
||||
openclaude: 'openclaude.gitlawb.com',
|
||||
@@ -115,33 +47,15 @@ export const MOBILE_TUI_AGENT_FAVICON_DOMAINS: Partial<Record<TuiAgent, string>>
|
||||
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<TuiAgent>()
|
||||
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<unknown> | null {
|
||||
return Array.isArray(disabled) ? disabled : null
|
||||
}
|
||||
|
||||
export function isMobileTuiAgentEnabled(agent: TuiAgent, disabled?: unknown): boolean {
|
||||
return !normalizeDisabledMobileTuiAgents(disabled).includes(agent)
|
||||
}
|
||||
|
||||
export function filterEnabledMobileTuiAgents<T extends TuiAgent>(
|
||||
agents: Iterable<T>,
|
||||
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<TuiAgent>,
|
||||
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<T extends TuiAgent>(
|
||||
agents: Iterable<T>,
|
||||
disabled?: unknown
|
||||
): T[] {
|
||||
const disabledSet = new Set(normalizeDisabledTuiAgents(disabled))
|
||||
return [...agents].filter((agent) => !disabledSet.has(agent))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<void>
|
||||
}
|
||||
|
||||
export type AgentHooksApi = {
|
||||
claudeStatus: () => Promise<AgentHookInstallStatus>
|
||||
openClaudeStatus: () => Promise<AgentHookInstallStatus>
|
||||
codexStatus: () => Promise<AgentHookInstallStatus>
|
||||
geminiStatus: () => Promise<AgentHookInstallStatus>
|
||||
antigravityStatus: () => Promise<AgentHookInstallStatus>
|
||||
ampStatus: () => Promise<AgentHookInstallStatus>
|
||||
cursorStatus: () => Promise<AgentHookInstallStatus>
|
||||
droidStatus: () => Promise<AgentHookInstallStatus>
|
||||
commandCodeStatus: () => Promise<AgentHookInstallStatus>
|
||||
grokStatus: () => Promise<AgentHookInstallStatus>
|
||||
copilotStatus: () => Promise<AgentHookInstallStatus>
|
||||
hermesStatus: () => Promise<AgentHookInstallStatus>
|
||||
devinStatus: () => Promise<AgentHookInstallStatus>
|
||||
}
|
||||
|
||||
@@ -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<CodexConfigSyncStatus> => ipcRenderer.invoke('codexConfigSync:status')
|
||||
},
|
||||
agentHooks: {
|
||||
claudeStatus: (): Promise<AgentHookInstallStatus> =>
|
||||
ipcRenderer.invoke('agentHooks:claudeStatus'),
|
||||
openClaudeStatus: (): Promise<AgentHookInstallStatus> =>
|
||||
ipcRenderer.invoke('agentHooks:openClaudeStatus'),
|
||||
codexStatus: (): Promise<AgentHookInstallStatus> =>
|
||||
ipcRenderer.invoke('agentHooks:codexStatus'),
|
||||
geminiStatus: (): Promise<AgentHookInstallStatus> =>
|
||||
ipcRenderer.invoke('agentHooks:geminiStatus'),
|
||||
antigravityStatus: (): Promise<AgentHookInstallStatus> =>
|
||||
ipcRenderer.invoke('agentHooks:antigravityStatus'),
|
||||
ampStatus: (): Promise<AgentHookInstallStatus> => ipcRenderer.invoke('agentHooks:ampStatus'),
|
||||
cursorStatus: (): Promise<AgentHookInstallStatus> =>
|
||||
ipcRenderer.invoke('agentHooks:cursorStatus'),
|
||||
droidStatus: (): Promise<AgentHookInstallStatus> =>
|
||||
ipcRenderer.invoke('agentHooks:droidStatus'),
|
||||
commandCodeStatus: (): Promise<AgentHookInstallStatus> =>
|
||||
ipcRenderer.invoke('agentHooks:commandCodeStatus'),
|
||||
grokStatus: (): Promise<AgentHookInstallStatus> => ipcRenderer.invoke('agentHooks:grokStatus'),
|
||||
devinStatus: (): Promise<AgentHookInstallStatus> =>
|
||||
ipcRenderer.invoke('agentHooks:devinStatus'),
|
||||
copilotStatus: (): Promise<AgentHookInstallStatus> =>
|
||||
ipcRenderer.invoke('agentHooks:copilotStatus'),
|
||||
hermesStatus: (): Promise<AgentHookInstallStatus> =>
|
||||
ipcRenderer.invoke('agentHooks:hermesStatus'),
|
||||
kimiStatus: (): Promise<AgentHookInstallStatus> => ipcRenderer.invoke('agentHooks:kimiStatus')
|
||||
},
|
||||
|
||||
agentTrust: {
|
||||
markTrusted: (args: {
|
||||
preset: 'cursor' | 'copilot' | 'codex'
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import type { PreloadApi } from '../../../../preload/api-types'
|
||||
|
||||
export function createAgentHooksApi(): NonNullable<Partial<PreloadApi>['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')
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,6 @@ describe('web preload API composition', () => {
|
||||
'codexAccounts',
|
||||
'claudeAccounts',
|
||||
'cli',
|
||||
'agentHooks',
|
||||
'macosTccPrompts',
|
||||
'codexConfigSync',
|
||||
'developerPermissions',
|
||||
|
||||
@@ -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<PreloadApi> {
|
||||
codexAccounts: createAccountsApi(),
|
||||
claudeAccounts: createAccountsApi(),
|
||||
cli: createCliApi(),
|
||||
agentHooks: createAgentHooksApi(),
|
||||
macosTccPrompts: createMacosTccPromptsApi(),
|
||||
codexConfigSync: {
|
||||
status: () =>
|
||||
|
||||
@@ -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<Record<TuiAgent, Partial<(typeof TUI_AGENT_CONFIG)[TuiAgent]>>> = {
|
||||
'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)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -52,11 +52,23 @@ export type TuiAgentConfig = {
|
||||
ctrlEnterEncoding?: 'csi-u'
|
||||
}
|
||||
|
||||
export const TUI_AGENT_CONFIG: Record<TuiAgent, TuiAgentConfig> = {
|
||||
/** Authoring form: `launchCmd` and `expectedProcess` default to `detectCmd` (true for most agents). */
|
||||
type TuiAgentConfigSource = Omit<TuiAgentConfig, 'launchCmd' | 'expectedProcess'> & {
|
||||
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<TuiAgent, TuiAgentConfigSource> = {
|
||||
claude: {
|
||||
detectCmd: 'claude',
|
||||
launchCmd: 'claude',
|
||||
expectedProcess: 'claude',
|
||||
promptInjectionMode: 'argv',
|
||||
// Why: `claude --prefill <text>` 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<TuiAgent, TuiAgentConfig> = {
|
||||
},
|
||||
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<TuiAgent, TuiAgentConfig> = {
|
||||
},
|
||||
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<TuiAgent, TuiAgentConfig> = {
|
||||
// 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<TuiAgent, TuiAgentConfig> = {
|
||||
},
|
||||
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<TuiAgent, TuiAgentConfig> = {
|
||||
},
|
||||
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<TuiAgent, TuiAgentConfig> = {
|
||||
},
|
||||
'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<TuiAgent, TuiAgentConfig> = {
|
||||
},
|
||||
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<TuiAgent, TuiAgentConfig> = {
|
||||
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<TuiAgent, TuiAgentConfig> = {
|
||||
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<TuiAgent, TuiAgentConfig> = {
|
||||
},
|
||||
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<TuiAgent, TuiAgentConfig> = {
|
||||
},
|
||||
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<TuiAgent, TuiAgentConfig> = {
|
||||
},
|
||||
devin: {
|
||||
detectCmd: 'devin',
|
||||
launchCmd: 'devin',
|
||||
expectedProcess: 'devin',
|
||||
// Why: `devin -- <prompt>` 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<TuiAgent, TuiAgentConfig> = Object.fromEntries(
|
||||
Object.entries(TUI_AGENT_CONFIG_SOURCE).map(([agent, source]) => [
|
||||
agent,
|
||||
resolveTuiAgentConfig(source)
|
||||
])
|
||||
) as Record<TuiAgent, TuiAgentConfig>
|
||||
|
||||
export function isTuiAgent(value: unknown): value is TuiAgent {
|
||||
return typeof value === 'string' && Object.hasOwn(TUI_AGENT_CONFIG, value)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user