mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
* feat: [AI-GEN] add Trae CLI as a supported TUI agent Closes #10579. Wire trae-cli into the desktop and mobile agent catalogs following the same integration pattern as other CLI agents (e.g. Ante, Devin): - src/shared/types.ts, tui-agent-config.ts: register 'trae' with detectCmdAliases (traecli/trae-agent) and argv prompt injection, matching trae-cli's `trae-cli [prompt]` contract. The CLI's own third documented alias `ta` is intentionally excluded — too generic a 2-letter name to use as a PATH-existence detection signal without false-positiving on unrelated tools. - src/shared/trae-headless-command.ts: recognize `--print`/`-p` and `--output-format json|stream-json` as one-shot headless invocations (same shape as claude-headless-command.ts) so they aren't mistaken for a live interactive session. - agent-kind.ts, telemetry-events.ts, agent-status-types.ts, agent-type-label.ts, tui-agent-display-names.ts, tui-agent-permissions.ts (YOLO via trae-cli's own --yolo flag), tui-agent-selection.ts: standard per-agent registrations. - agent-catalog.tsx, agent-favicon-assets.ts, mobile/src/tasks/mobile-tui-agents.ts, mobile/src/components/mobile-agent-icon-assets.ts: catalog entries and bundled favicon (fetched from docs.trae.cn, required by mobile's offline-icon invariant test). - i18n: add the "Trae" label to all five locale catalogs (en/es/ja/ko/zh). - Tests: agent-process-recognition, agent-status, tui-agent-startup. Verified with `pnpm typecheck` (desktop + mobile), the relevant vitest suites (869 tests across 12 files, all green), oxlint (clean), and a real end-to-end launch of the actual trae-cli binary through Orca's pty.spawn IPC path (confirmed via the OS process table). * fix: [AI-GEN] point Trae catalog entry at the real CLI quick-start doc docs.trae.cn/cli (what the installed CLI's own --help text prints as its "User manual" link) soft-404s — the docs site restructured and the working page is docs.trae.cn/cli_get-started-with-trae-cli (confirmed by HTTP fetch: real page title "TRAE CLI 快速开始" vs the old path's "404 - 页面不存在"). Addresses CodeRabbit's homepageUrl review comment. * fix: [AI-GEN] detect Trae on traecli, not the ambiguous trae-cli name Per @AmethystLiang's review: the open-source bytedance/trae-agent project (MIT, ~12k stars) registers its own console script as `trae-cli` (pyproject.toml: `trae-cli = "trae_agent.cli:main"`), an entirely unrelated CLI with a different contract (`trae-cli run "task"`, `-p` short for `--provider`). Detecting on bare `trae-cli` would false-positive on that project's installs and break launch for anyone who has it instead of the actual TRAE CN CLI. - tui-agent-config.ts: detectCmd/launchCmd/expectedProcess -> `traecli` (TRAE CN's own installer symlinks this alias too, but the other project does not ship it). Dropped the `trae-agent` alias entirely — it's the colliding project's literal repo name, the highest false-positive string available. - agent-catalog.tsx: cmd -> `traecli` to match; faviconDomain -> `www.trae.cn` (bare `trae.cn` 404s on Google's favicon service; `www.trae.cn` is the product-root domain that actually resolves). - mobile-tui-agents.ts: faviconDomain -> `www.trae.cn` to match. - Tests updated: agent-process-recognition now asserts `trae-cli` and `trae-agent` are NOT recognized as Trae (regression guard against reintroducing the collision); tui-agent-startup updated for the new launch command. promptInjectionMode stays `argv` and the headless-command file stays as-is — both verified against the real TRAE CN CLI's actual --help output (pasted in the PR review thread), not assumptions. * refactor: [AI-GEN] share one print-mode headless matcher across agents trae-headless-command.ts was a rename-only fork of claude-headless-command.ts, and ante-headless-command.ts carried a third copy of optionName. Collapse both print-mode files into print-mode-headless-command.ts, dispatch from a Partial<Record<TuiAgent, ...>> table instead of an if-chain, and compress the Trae comments to the repo's one-line style. * fix: [AI-GEN] terminate Trae flag parsing before the positional prompt `traecli` is a Cobra CLI with subcommands, so an argv prompt starting with `help`, `config`, `-…` was dispatched as a subcommand or flag instead of being run as the task. Add `argvPromptSeparator: '--'` (same reason Grok has it), and stop the shared print-mode headless matcher at `--` so a prompt that reads like `--print` no longer drops the pane out of agent recognition. * docs: [AI-GEN] name both Trae CLIs explicitly in the detect-name comment Co-authored-by: Orca <help@stably.ai> * docs: [AI-GEN] drop the vendor tag from the Trae union comment Co-authored-by: Orca <help@stably.ai> * fix: [AI-GEN] guard the nullable startup plan in the Trae separator test Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: 陈泽榜 <chenzebang@jianzhikeji.com> Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Co-authored-by: Orca <help@stably.ai>
302 lines
12 KiB
TypeScript
302 lines
12 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
isAgentForegroundWrapperProcess,
|
|
isExpectedAgentProcess,
|
|
isRecognizedAgentType,
|
|
recognizeAgentProcess,
|
|
recognizeAgentProcessFromCommandLine
|
|
} from './agent-process-recognition'
|
|
|
|
describe('agent process recognition', () => {
|
|
it('recognizes packaged Codex foreground process names', () => {
|
|
expect(recognizeAgentProcess('codex-aarch64-ap')).toEqual({
|
|
agent: 'codex',
|
|
processName: 'codex-aarch64-ap'
|
|
})
|
|
expect(isRecognizedAgentType('codex-aarch64-ap')).toBe(true)
|
|
})
|
|
|
|
it('recognizes the OpenClaude foreground process', () => {
|
|
expect(recognizeAgentProcess('/usr/local/bin/openclaude')).toEqual({
|
|
agent: 'openclaude',
|
|
processName: 'openclaude'
|
|
})
|
|
expect(isRecognizedAgentType('openclaude')).toBe(true)
|
|
expect(isExpectedAgentProcess('/usr/local/bin/openclaude', 'claude')).toBe(false)
|
|
})
|
|
|
|
it('recognizes the Droid foreground process on Windows', () => {
|
|
expect(recognizeAgentProcess(String.raw`C:\Users\dev\AppData\Roaming\npm\droid.cmd`)).toEqual({
|
|
agent: 'droid',
|
|
processName: 'droid'
|
|
})
|
|
})
|
|
|
|
it('matches expected agents from platform-specific foreground process paths', () => {
|
|
expect(recognizeAgentProcess('claude')).toEqual({
|
|
agent: 'claude',
|
|
processName: 'claude'
|
|
})
|
|
expect(
|
|
isExpectedAgentProcess(String.raw`C:\Users\dev\AppData\Roaming\npm\claude.exe`, 'claude')
|
|
).toBe(true)
|
|
expect(isExpectedAgentProcess('/usr/local/bin/claude', 'claude')).toBe(true)
|
|
expect(isExpectedAgentProcess('powershell.exe', 'claude')).toBe(false)
|
|
})
|
|
|
|
it('does not recognize Claude print-mode hook subprocesses as interactive agents', () => {
|
|
expect(
|
|
recognizeAgentProcessFromCommandLine(
|
|
'claude --print --model haiku "Analyze this conversation and determine: Does the assistant have more autonomous work to do RIGHT NOW?"'
|
|
)
|
|
).toBeNull()
|
|
expect(
|
|
recognizeAgentProcessFromCommandLine(
|
|
String.raw`/home/dev/.local/bin/claude -p "Context: This summary will be shown in a list"`
|
|
)
|
|
).toBeNull()
|
|
expect(
|
|
recognizeAgentProcessFromCommandLine(
|
|
String.raw`C:\Users\dev\AppData\Roaming\npm\claude.exe --output-format=json "hook prompt"`
|
|
)
|
|
).toBeNull()
|
|
expect(recognizeAgentProcessFromCommandLine('claude --resume abc123')).toEqual({
|
|
agent: 'claude',
|
|
processName: 'claude'
|
|
})
|
|
})
|
|
|
|
it('recognizes Command Code without classifying Windows cmd.exe as an agent', () => {
|
|
expect(recognizeAgentProcess('command-code')).toEqual({
|
|
agent: 'command-code',
|
|
processName: 'command-code'
|
|
})
|
|
expect(
|
|
recognizeAgentProcess(String.raw`C:\Users\dev\AppData\Roaming\npm\command-code.cmd`)
|
|
).toEqual({
|
|
agent: 'command-code',
|
|
processName: 'command-code'
|
|
})
|
|
expect(isRecognizedAgentType('command-code')).toBe(true)
|
|
expect(isRecognizedAgentType('cmd.exe')).toBe(false)
|
|
expect(recognizeAgentProcess('cmd.exe')).toBeNull()
|
|
})
|
|
|
|
it('recognizes Ante without classifying ante-prefixed path fragments as the agent', () => {
|
|
expect(recognizeAgentProcess('ante')).toEqual({
|
|
agent: 'ante',
|
|
processName: 'ante'
|
|
})
|
|
expect(recognizeAgentProcess('/Users/dev/.ante/bin/ante')).toEqual({
|
|
agent: 'ante',
|
|
processName: 'ante'
|
|
})
|
|
expect(isExpectedAgentProcess('/Users/dev/.ante/bin/ante', 'ante')).toBe(true)
|
|
expect(isRecognizedAgentType('ante')).toBe(true)
|
|
// Why: 'ante' is a common token in directory and binary names; only the
|
|
// exact normalized basename may classify as the agent.
|
|
expect(recognizeAgentProcess('ante-obsidian')).toBeNull()
|
|
expect(recognizeAgentProcess('antechamber')).toBeNull()
|
|
expect(isExpectedAgentProcess('ante-obsidian', 'ante')).toBe(false)
|
|
})
|
|
|
|
it('does not recognize Ante headless one-shot commands as interactive agents', () => {
|
|
expect(recognizeAgentProcessFromCommandLine('ante -p "summarize this diff"')).toBeNull()
|
|
expect(recognizeAgentProcessFromCommandLine('ante -psummarize')).toBeNull()
|
|
expect(
|
|
recognizeAgentProcessFromCommandLine('ante --prompt "review this for security issues"')
|
|
).toBeNull()
|
|
expect(
|
|
recognizeAgentProcessFromCommandLine('ante --prompt=review --output-format minimal')
|
|
).toBeNull()
|
|
expect(recognizeAgentProcessFromCommandLine('ante --resume ses_123')).toEqual({
|
|
agent: 'ante',
|
|
processName: 'ante'
|
|
})
|
|
})
|
|
|
|
it('does not recognize wrapped Ante headless one-shot commands as interactive agents', () => {
|
|
expect(
|
|
recognizeAgentProcessFromCommandLine('node /Users/dev/.ante/bin/ante --prompt "review"')
|
|
).toBeNull()
|
|
expect(
|
|
recognizeAgentProcessFromCommandLine(
|
|
String.raw`node C:\Users\dev\.ante\bin\ante.cmd -p review`
|
|
)
|
|
).toBeNull()
|
|
expect(recognizeAgentProcessFromCommandLine('node /Users/dev/.ante/bin/ante')).toEqual({
|
|
agent: 'ante',
|
|
processName: 'ante'
|
|
})
|
|
})
|
|
|
|
it('recognizes Trae by its traecli binary, not the ambiguous trae-cli name', () => {
|
|
expect(recognizeAgentProcess('traecli')).toEqual({
|
|
agent: 'trae',
|
|
processName: 'traecli'
|
|
})
|
|
expect(recognizeAgentProcess('/Users/dev/.local/bin/traecli')).toEqual({
|
|
agent: 'trae',
|
|
processName: 'traecli'
|
|
})
|
|
expect(isExpectedAgentProcess('/Users/dev/.local/bin/traecli', 'traecli')).toBe(true)
|
|
expect(isRecognizedAgentType('traecli')).toBe(true)
|
|
// Why: `trae-cli` and `trae-agent` both name the unrelated open-source bytedance/trae-agent.
|
|
expect(recognizeAgentProcess('trae-cli')).toBeNull()
|
|
expect(recognizeAgentProcess('trae-agent')).toBeNull()
|
|
})
|
|
|
|
it('does not recognize Trae headless one-shot commands as interactive agents', () => {
|
|
expect(recognizeAgentProcessFromCommandLine('traecli -p "summarize this diff"')).toBeNull()
|
|
expect(recognizeAgentProcessFromCommandLine('traecli --print "review this"')).toBeNull()
|
|
expect(
|
|
recognizeAgentProcessFromCommandLine('traecli --output-format json "review this"')
|
|
).toBeNull()
|
|
expect(
|
|
recognizeAgentProcessFromCommandLine('traecli --output-format=stream-json review')
|
|
).toBeNull()
|
|
expect(recognizeAgentProcessFromCommandLine('traecli --resume AUTO')).toEqual({
|
|
agent: 'trae',
|
|
processName: 'traecli'
|
|
})
|
|
// Why: past `--` nothing is a flag, so this is the interactive pane Orca itself launches.
|
|
expect(recognizeAgentProcessFromCommandLine('traecli -- "--print the release notes"')).toEqual({
|
|
agent: 'trae',
|
|
processName: 'traecli'
|
|
})
|
|
})
|
|
|
|
it('recognizes Mistral Vibe by its installed executable and legacy alias', () => {
|
|
expect(recognizeAgentProcess('/home/dev/.local/bin/vibe')).toEqual({
|
|
agent: 'mistral-vibe',
|
|
processName: 'vibe'
|
|
})
|
|
expect(recognizeAgentProcess('mistral-vibe')).toEqual({
|
|
agent: 'mistral-vibe',
|
|
processName: 'mistral-vibe'
|
|
})
|
|
expect(isRecognizedAgentType('vibe')).toBe(true)
|
|
})
|
|
|
|
it('recognizes Qwen Code by its installed qwen executable', () => {
|
|
expect(recognizeAgentProcess('/home/dev/.local/bin/qwen')).toEqual({
|
|
agent: 'qwen-code',
|
|
processName: 'qwen'
|
|
})
|
|
expect(recognizeAgentProcess(String.raw`C:\Users\dev\AppData\Roaming\npm\qwen.cmd`)).toEqual({
|
|
agent: 'qwen-code',
|
|
processName: 'qwen'
|
|
})
|
|
expect(isExpectedAgentProcess('/usr/local/bin/qwen', 'qwen')).toBe(true)
|
|
expect(isRecognizedAgentType('qwen')).toBe(true)
|
|
})
|
|
|
|
it('recognizes agent CLIs launched through interpreter wrappers', () => {
|
|
expect(
|
|
recognizeAgentProcessFromCommandLine('node /Users/dev/.nvm/versions/node/bin/codex')
|
|
).toEqual({ agent: 'codex', processName: 'codex' })
|
|
expect(
|
|
recognizeAgentProcessFromCommandLine('node /Users/dev/.nvm/versions/node/bin/gemini')
|
|
).toEqual({ agent: 'gemini', processName: 'gemini' })
|
|
expect(recognizeAgentProcessFromCommandLine('python3 /opt/homebrew/bin/hermes --tui')).toEqual({
|
|
agent: 'hermes',
|
|
processName: 'hermes'
|
|
})
|
|
expect(
|
|
recognizeAgentProcessFromCommandLine('python3.12 /opt/homebrew/bin/hermes --tui')
|
|
).toEqual({
|
|
agent: 'hermes',
|
|
processName: 'hermes'
|
|
})
|
|
expect(recognizeAgentProcessFromCommandLine('python -m aider')).toEqual({
|
|
agent: 'aider',
|
|
processName: 'aider'
|
|
})
|
|
expect(
|
|
recognizeAgentProcessFromCommandLine(
|
|
String.raw`python C:\Users\dev\AppData\Roaming\Python\Python312\Scripts\aider.py`
|
|
)
|
|
).toEqual({ agent: 'aider', processName: 'aider' })
|
|
expect(
|
|
recognizeAgentProcessFromCommandLine(
|
|
String.raw`node C:\Users\dev\AppData\Roaming\npm\codex.cmd`
|
|
)
|
|
).toEqual({ agent: 'codex', processName: 'codex' })
|
|
expect(
|
|
recognizeAgentProcessFromCommandLine(
|
|
String.raw`node C:\Users\dev\AppData\Roaming\npm\node_modules\@openai\codex\bin\codex.js`
|
|
)
|
|
).toEqual({ agent: 'codex', processName: 'codex' })
|
|
expect(
|
|
recognizeAgentProcessFromCommandLine(
|
|
String.raw`node C:\Users\dev\AppData\Roaming\npm\node_modules\@google\gemini-cli\bundle\gemini.mjs`
|
|
)
|
|
).toEqual({ agent: 'gemini', processName: 'gemini' })
|
|
})
|
|
|
|
it('recognizes only the agent subcommand of the generic Orca CLI', () => {
|
|
expect(recognizeAgentProcessFromCommandLine('orca claude-teams')).toEqual({
|
|
agent: 'claude-agent-teams',
|
|
processName: 'orca'
|
|
})
|
|
expect(recognizeAgentProcessFromCommandLine('orca status')).toBeNull()
|
|
expect(recognizeAgentProcessFromCommandLine('orca-dev terminal list')).toBeNull()
|
|
expect(recognizeAgentProcessFromCommandLine('node /usr/local/bin/orca claude-teams')).toEqual({
|
|
agent: 'claude-agent-teams',
|
|
processName: 'orca'
|
|
})
|
|
expect(recognizeAgentProcessFromCommandLine('node /usr/local/bin/orca status')).toBeNull()
|
|
})
|
|
|
|
it('recognizes the versioned Cursor Node wrapper without accepting generic agent processes', () => {
|
|
const cursorEntrypoint = String.raw`C:\Users\dev\AppData\Local\cursor-agent\versions\2026.07.09-a3815c0\index.js`
|
|
|
|
expect(recognizeAgentProcessFromCommandLine(`node.exe ${cursorEntrypoint}`)).toEqual({
|
|
agent: 'cursor',
|
|
processName: 'cursor-agent'
|
|
})
|
|
expect(
|
|
recognizeAgentProcessFromCommandLine(`node.exe ${cursorEntrypoint} worker-server`)
|
|
).toEqual({ agent: 'cursor', processName: 'cursor-agent' })
|
|
expect(
|
|
recognizeAgentProcessFromCommandLine(String.raw`node.exe C:\repo\cursor-agent\index.js`)
|
|
).toBeNull()
|
|
expect(
|
|
recognizeAgentProcessFromCommandLine(String.raw`C:\Users\dev\.grok\bin\agent.exe`)
|
|
).toBeNull()
|
|
})
|
|
|
|
it('does not classify prompt text as a wrapped agent command', () => {
|
|
expect(
|
|
recognizeAgentProcessFromCommandLine(
|
|
'node /tmp/not-an-agent.js "compare opencode vs orca in Gemini CLI"'
|
|
)
|
|
).toBeNull()
|
|
expect(recognizeAgentProcessFromCommandLine(String.raw`node C:\tmp\not-an-agent.js`)).toBeNull()
|
|
expect(
|
|
recognizeAgentProcessFromCommandLine(
|
|
String.raw`node C:\repo\server.js --plugin C:\tmp\codex.js`
|
|
)
|
|
).toBeNull()
|
|
expect(recognizeAgentProcessFromCommandLine(String.raw`node C:\repo\codex.js`)).toBeNull()
|
|
expect(recognizeAgentProcessFromCommandLine(String.raw`node C:\repo\gemini.mjs`)).toBeNull()
|
|
expect(recognizeAgentProcessFromCommandLine(String.raw`python C:\repo\aider.py`)).toBeNull()
|
|
expect(recognizeAgentProcessFromCommandLine('python -m not_aider')).toBeNull()
|
|
})
|
|
|
|
it('identifies only foreground processes that can wrap agent entrypoints', () => {
|
|
expect(isAgentForegroundWrapperProcess('node.exe')).toBe(true)
|
|
expect(isAgentForegroundWrapperProcess('/usr/bin/python3')).toBe(true)
|
|
expect(isAgentForegroundWrapperProcess('python3.12.exe')).toBe(true)
|
|
expect(isAgentForegroundWrapperProcess('bash')).toBe(false)
|
|
expect(isAgentForegroundWrapperProcess('vim.exe')).toBe(false)
|
|
})
|
|
|
|
it('recognizes versioned Grok process names observed from the installed CLI', () => {
|
|
expect(recognizeAgentProcess('grok-0.2.51')).toEqual({
|
|
agent: 'grok',
|
|
processName: 'grok-0.2.51'
|
|
})
|
|
})
|
|
})
|