From 2d232171663a5a1fe24e2d2d7a95d7c6859155de Mon Sep 17 00:00:00 2001 From: zbisure <13783310+zbisure@users.noreply.github.com> Date: Wed, 29 Jul 2026 02:18:49 +0800 Subject: [PATCH] feat: add Trae CLI as a supported TUI agent (#10763) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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> 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 * docs: [AI-GEN] drop the vendor tag from the Trae union comment Co-authored-by: Orca * fix: [AI-GEN] guard the nullable startup plan in the Trae separator test Co-authored-by: Orca --------- Co-authored-by: 陈泽榜 Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Co-authored-by: Orca --- .../components/mobile-agent-icon-assets.ts | 1 + mobile/src/tasks/mobile-tui-agents.ts | 3 ++ src/renderer/src/i18n/locales/en.json | 1 + src/renderer/src/i18n/locales/es.json | 1 + src/renderer/src/i18n/locales/ja.json | 1 + src/renderer/src/i18n/locales/ko.json | 1 + src/renderer/src/i18n/locales/zh.json | 1 + src/renderer/src/lib/agent-catalog.tsx | 9 +++++ src/renderer/src/lib/agent-favicon-assets.ts | 2 + src/renderer/src/lib/agent-status.test.ts | 5 +++ src/renderer/src/lib/agent-status.ts | 3 +- .../src/lib/tui-agent-startup.test.ts | 29 +++++++++++++ src/shared/agent-headless-command.ts | 17 +++++--- src/shared/agent-icons/trae.png | Bin 0 -> 412 bytes src/shared/agent-kind.ts | 3 +- src/shared/agent-process-recognition.test.ts | 36 +++++++++++++++++ src/shared/agent-status-types.ts | 1 + src/shared/agent-type-label.ts | 1 + src/shared/ante-headless-command.ts | 7 +--- src/shared/claude-headless-command.ts | 32 --------------- src/shared/print-mode-headless-command.ts | 38 ++++++++++++++++++ src/shared/telemetry-events.ts | 1 + src/shared/tui-agent-config.ts | 12 ++++++ src/shared/tui-agent-display-names.ts | 1 + src/shared/tui-agent-permissions.ts | 3 +- src/shared/tui-agent-selection.ts | 1 + src/shared/types.ts | 1 + 27 files changed, 166 insertions(+), 45 deletions(-) create mode 100644 src/shared/agent-icons/trae.png delete mode 100644 src/shared/claude-headless-command.ts create mode 100644 src/shared/print-mode-headless-command.ts diff --git a/mobile/src/components/mobile-agent-icon-assets.ts b/mobile/src/components/mobile-agent-icon-assets.ts index 52615e89d..9aa4efde3 100644 --- a/mobile/src/components/mobile-agent-icon-assets.ts +++ b/mobile/src/components/mobile-agent-icon-assets.ts @@ -16,6 +16,7 @@ export const MOBILE_AGENT_ICON_ASSETS: Partial = { opencode: 'OpenCode', 'mimo-code': 'MiMo Code', ante: 'Ante', + trae: 'Trae', pi: 'Pi', omp: 'OMP', gemini: 'Gemini', @@ -84,6 +86,7 @@ export const MOBILE_TUI_AGENT_FAVICON_DOMAINS: Partial> opencode: 'opencode.ai', 'mimo-code': 'mimo.xiaomi.com', ante: 'antigma.ai', + trae: 'www.trae.cn', omp: 'omp.sh', gemini: 'gemini.google.com', antigravity: 'antigravity.google', diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 3dfb21e4b..f3c3aebdd 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -356,6 +356,7 @@ "0708ed89f1": "Claude", "fc80296033": "Devin", "da41abbdd4": "Ante", + "060d152fb5": "Trae", "mimo_code_label": "MiMo Code" }, "skill": { diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 434b2f094..865942987 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -333,6 +333,7 @@ "0708ed89f1": "Claude", "fc80296033": "Devin", "da41abbdd4": "Ante", + "060d152fb5": "Trae", "mimo_code_label": "MiMo Code" }, "skill": { diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 81ed97fcb..a7c46e59d 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -333,6 +333,7 @@ "0708ed89f1": "Claude", "fc80296033": "Devin", "da41abbdd4": "Ante", + "060d152fb5": "Trae", "mimo_code_label": "MiMo Code" }, "skill": { diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 0173fa7d8..e74f4c36b 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -333,6 +333,7 @@ "0708ed89f1": "Claude", "fc80296033": "Devin", "da41abbdd4": "Ante", + "060d152fb5": "Trae", "mimo_code_label": "MiMo Code" }, "skill": { diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 9c2439018..50ce746c9 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -333,6 +333,7 @@ "0708ed89f1": "Claude", "fc80296033": "Devin", "da41abbdd4": "Ante", + "060d152fb5": "Trae", "mimo_code_label": "MiMo Code" }, "skill": { diff --git a/src/renderer/src/lib/agent-catalog.tsx b/src/renderer/src/lib/agent-catalog.tsx index f9536dc1c..2106c0eaa 100644 --- a/src/renderer/src/lib/agent-catalog.tsx +++ b/src/renderer/src/lib/agent-catalog.tsx @@ -104,6 +104,15 @@ export const getAgentCatalog = createLocalizedCatalog((): AgentCatalogEntry[] => faviconDomain: 'antigma.ai', homepageUrl: 'https://github.com/AntigmaLabs/ante-preview' }, + { + id: 'trae', + label: translate('auto.lib.agent.catalog.060d152fb5', 'Trae'), + // Why: matches TUI_AGENT_CONFIG.trae.detectCmd, not the ambiguous `trae-cli` — see the Why there. + cmd: 'traecli', + // Why: bare `trae.cn` 404s on Google's favicon service. + faviconDomain: 'www.trae.cn', + homepageUrl: 'https://docs.trae.cn/cli_get-started-with-trae-cli' + }, { id: 'pi', label: translate('auto.lib.agent.catalog.302934c5d9', 'Pi'), diff --git a/src/renderer/src/lib/agent-favicon-assets.ts b/src/renderer/src/lib/agent-favicon-assets.ts index fcfaa89c8..fa30062e3 100644 --- a/src/renderer/src/lib/agent-favicon-assets.ts +++ b/src/renderer/src/lib/agent-favicon-assets.ts @@ -2,6 +2,7 @@ import type { TuiAgent } from '../../../shared/types' import grokUrl from '../../../shared/agent-icons/grok.png?url' import mimoCodeUrl from '../../../shared/agent-icons/mimo-code.png?url' import anteUrl from '../../../shared/agent-icons/ante.png?url' +import traeUrl from '../../../shared/agent-icons/trae.png?url' import geminiUrl from '../../../shared/agent-icons/gemini.png?url' import antigravityUrl from '../../../shared/agent-icons/antigravity.png?url' import gooseUrl from '../../../shared/agent-icons/goose.png?url' @@ -34,6 +35,7 @@ export const AGENT_FAVICON_ASSETS: Partial> = { grok: grokUrl, 'mimo-code': mimoCodeUrl, ante: anteUrl, + trae: traeUrl, gemini: geminiUrl, antigravity: antigravityUrl, goose: gooseUrl, diff --git a/src/renderer/src/lib/agent-status.test.ts b/src/renderer/src/lib/agent-status.test.ts index 14ac79c4a..ce546f541 100644 --- a/src/renderer/src/lib/agent-status.test.ts +++ b/src/renderer/src/lib/agent-status.test.ts @@ -860,6 +860,10 @@ describe('formatAgentTypeLabel', () => { expect(formatAgentTypeLabel('ante')).toBe('Ante') }) + it("maps 'trae' to 'Trae'", () => { + expect(formatAgentTypeLabel('trae')).toBe('Trae') + }) + it('passes through arbitrary custom agent names as-is', () => { expect(formatAgentTypeLabel('weirdo')).toBe('weirdo') }) @@ -884,6 +888,7 @@ describe('agentTypeToIconAgent', () => { expect(agentTypeToIconAgent('antigravity')).toBe('antigravity') expect(agentTypeToIconAgent('command-code')).toBe('command-code') expect(agentTypeToIconAgent('ante')).toBe('ante') + expect(agentTypeToIconAgent('trae')).toBe('trae') }) it('returns null for arbitrary non-iconable strings', () => { diff --git a/src/renderer/src/lib/agent-status.ts b/src/renderer/src/lib/agent-status.ts index af19128f8..1bfa9fc49 100644 --- a/src/renderer/src/lib/agent-status.ts +++ b/src/renderer/src/lib/agent-status.ts @@ -129,7 +129,8 @@ const ICONABLE_AGENT_TYPES: Record = { copilot: true, grok: true, devin: true, - ante: true + ante: true, + trae: true } // Why: return null (not a 'claude' fallback) for unknown so Codex panes don't flash the Claude icon before the hook fires. diff --git a/src/renderer/src/lib/tui-agent-startup.test.ts b/src/renderer/src/lib/tui-agent-startup.test.ts index 14af5e911..d40bab3f4 100644 --- a/src/renderer/src/lib/tui-agent-startup.test.ts +++ b/src/renderer/src/lib/tui-agent-startup.test.ts @@ -115,6 +115,35 @@ describe('buildAgentStartupPlan', () => { }) }) + it('passes the prompt to Trae as a positional argv behind a `--` separator', () => { + expect( + buildAgentStartupPlan({ + agent: 'trae', + prompt: 'Summarize the failing tests', + cmdOverrides: {}, + platform: 'linux' + }) + ).toEqual({ + agent: 'trae', + launchCommand: "traecli -- 'Summarize the failing tests'", + expectedProcess: 'traecli', + followupPrompt: null, + launchConfig: emptyLaunchConfig('traecli') + }) + }) + + // Why: without the separator these dispatch to Trae's `help`/`config` subcommands instead. + it('keeps subcommand-shaped Trae prompts as the positional prompt', () => { + expect( + buildAgentStartupPlan({ + agent: 'trae', + prompt: 'help me name this config', + cmdOverrides: {}, + platform: 'linux' + })?.launchCommand + ).toBe("traecli -- 'help me name this config'") + }) + it('uses cursor-agent as the actual launch binary', () => { expect( buildAgentStartupPlan({ diff --git a/src/shared/agent-headless-command.ts b/src/shared/agent-headless-command.ts index 5823f727e..3dafdd62d 100644 --- a/src/shared/agent-headless-command.ts +++ b/src/shared/agent-headless-command.ts @@ -1,12 +1,19 @@ import { isAnteHeadlessOneShotCommand } from './ante-headless-command' -import { isClaudeHeadlessOneShotCommand } from './claude-headless-command' +import { isPrintModeHeadlessOneShotCommand } from './print-mode-headless-command' import type { TuiAgent } from './types' +// Why: a table (not an if-chain) so adding an agent is one entry; Claude and Trae share +// the same `--print` one-shot contract, Ante's `--prompt` form needs its own matcher. +const HEADLESS_ONE_SHOT_MATCHERS: Partial< + Record boolean> +> = { + claude: isPrintModeHeadlessOneShotCommand, + trae: isPrintModeHeadlessOneShotCommand, + ante: isAnteHeadlessOneShotCommand +} + export function isHeadlessOneShotAgentCommand(agent: TuiAgent, tokens: readonly string[]): boolean { - if (agent === 'claude') { - return isClaudeHeadlessOneShotCommand(tokens) - } - return agent === 'ante' && isAnteHeadlessOneShotCommand(tokens) + return HEADLESS_ONE_SHOT_MATCHERS[agent]?.(tokens) ?? false } type AgentCommandRecognition = { agent: TuiAgent } | null diff --git a/src/shared/agent-icons/trae.png b/src/shared/agent-icons/trae.png new file mode 100644 index 0000000000000000000000000000000000000000..2fc4e537c5e8cd6a7385b0e035490362d2569d47 GIT binary patch literal 412 zcmV;N0b~A&P)ng9R*`bk7VR9J=W zm5mO9AP|5rRO|{V?Z345f6VZ7HWw;q%bM}I2^-uu4)BD98_>x_g6{bLS-}ia`2Grf z3cnz5-Nc@GP6Al&gF%vB0Y*dUxL+WKh^7$e@D)wC33Mvunx67ix5Pm^39$3THqL^M zo3vsIo`dyVrT~uH#*~i`Kykv#01#us7MS=*kio=j5hhFloK3vxUx4C;2o+Os4azV~ z&6HbM_Hu3tU<{a13dVpt{w6{|@noQQ_Ax@%9HSg_58l2=EqVk{jZ#Y$iTN?LDDFzB zdn)au*3a1jS_(>4Rcr;tf@YPpj|f<0-2(5&;_-Mq^Wg!H=o0jd4|P-k0000 // Why: `satisfies Record` makes the lookup exhaustive at compile diff --git a/src/shared/agent-process-recognition.test.ts b/src/shared/agent-process-recognition.test.ts index b6000f332..f84d81ba8 100644 --- a/src/shared/agent-process-recognition.test.ts +++ b/src/shared/agent-process-recognition.test.ts @@ -130,6 +130,42 @@ describe('agent process recognition', () => { }) }) + 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', diff --git a/src/shared/agent-status-types.ts b/src/shared/agent-status-types.ts index 1968ee089..be22ce6e6 100644 --- a/src/shared/agent-status-types.ts +++ b/src/shared/agent-status-types.ts @@ -37,6 +37,7 @@ export type WellKnownAgentType = | 'hermes' | 'devin' | 'ante' + | 'trae' | 'unknown' export type AgentType = WellKnownAgentType | (string & {}) diff --git a/src/shared/agent-type-label.ts b/src/shared/agent-type-label.ts index b638334f6..05d70d238 100644 --- a/src/shared/agent-type-label.ts +++ b/src/shared/agent-type-label.ts @@ -22,6 +22,7 @@ const WELL_KNOWN_LABELS: Record = { hermes: 'Hermes', devin: 'Devin', ante: 'Ante', + trae: 'Trae', kimi: 'Kimi' } diff --git a/src/shared/ante-headless-command.ts b/src/shared/ante-headless-command.ts index c777b2f80..82fcec5d8 100644 --- a/src/shared/ante-headless-command.ts +++ b/src/shared/ante-headless-command.ts @@ -1,9 +1,6 @@ -const ANTE_HEADLESS_PROMPT_FLAGS = new Set(['--prompt', '-p']) +import { optionName } from './print-mode-headless-command' -function optionName(token: string): string { - const eq = token.indexOf('=') - return eq === -1 ? token : token.slice(0, eq) -} +const ANTE_HEADLESS_PROMPT_FLAGS = new Set(['--prompt', '-p']) function isAnteHeadlessPromptFlag(token: string): boolean { const name = optionName(token) diff --git a/src/shared/claude-headless-command.ts b/src/shared/claude-headless-command.ts deleted file mode 100644 index b6647c37b..000000000 --- a/src/shared/claude-headless-command.ts +++ /dev/null @@ -1,32 +0,0 @@ -const CLAUDE_PRINT_MODE_FLAGS = new Set(['--print', '-p']) -const CLAUDE_HEADLESS_OUTPUT_FORMATS = new Set(['json', 'stream-json']) - -function optionName(token: string): string { - const eq = token.indexOf('=') - return eq === -1 ? token : token.slice(0, eq) -} - -function optionValue(tokens: readonly string[], index: number): string | null { - const token = tokens[index] - const eq = token.indexOf('=') - if (eq !== -1) { - return token.slice(eq + 1) - } - return tokens[index + 1] ?? null -} - -export function isClaudeHeadlessOneShotCommand(tokens: readonly string[]): boolean { - for (let index = 1; index < tokens.length; index += 1) { - const name = optionName(tokens[index]) - if (CLAUDE_PRINT_MODE_FLAGS.has(name)) { - return true - } - if (name === '--output-format') { - const value = optionValue(tokens, index)?.toLowerCase() - if (value && CLAUDE_HEADLESS_OUTPUT_FORMATS.has(value)) { - return true - } - } - } - return false -} diff --git a/src/shared/print-mode-headless-command.ts b/src/shared/print-mode-headless-command.ts new file mode 100644 index 000000000..77cee1459 --- /dev/null +++ b/src/shared/print-mode-headless-command.ts @@ -0,0 +1,38 @@ +const PRINT_MODE_FLAGS = new Set(['--print', '-p']) +const HEADLESS_OUTPUT_FORMATS = new Set(['json', 'stream-json']) + +export function optionName(token: string): string { + const eq = token.indexOf('=') + return eq === -1 ? token : token.slice(0, eq) +} + +function optionValue(tokens: readonly string[], index: number): string | null { + const token = tokens[index] + const eq = token.indexOf('=') + if (eq !== -1) { + return token.slice(eq + 1) + } + return tokens[index + 1] ?? null +} + +// Why: `--print`/`-p` prints one response and exits, and `--output-format json|stream-json` +// is only meaningful there — either means a headless run, not the interactive TUI Orca hosts. +export function isPrintModeHeadlessOneShotCommand(tokens: readonly string[]): boolean { + for (let index = 1; index < tokens.length; index += 1) { + // Why: `--` ends option parsing, so a prompt that reads like `--print` is still a prompt. + if (tokens[index] === '--') { + return false + } + const name = optionName(tokens[index]) + if (PRINT_MODE_FLAGS.has(name)) { + return true + } + if (name === '--output-format') { + const value = optionValue(tokens, index)?.toLowerCase() + if (value && HEADLESS_OUTPUT_FORMATS.has(value)) { + return true + } + } + } + return false +} diff --git a/src/shared/telemetry-events.ts b/src/shared/telemetry-events.ts index 9cbfaac20..7f934856a 100644 --- a/src/shared/telemetry-events.ts +++ b/src/shared/telemetry-events.ts @@ -94,6 +94,7 @@ export const AGENT_KIND_VALUES = [ 'grok', 'devin', 'ante', + 'trae', 'other' ] as const export const agentKindSchema = z.enum(AGENT_KIND_VALUES) diff --git a/src/shared/tui-agent-config.ts b/src/shared/tui-agent-config.ts index 4405d1061..49e07d418 100644 --- a/src/shared/tui-agent-config.ts +++ b/src/shared/tui-agent-config.ts @@ -96,6 +96,18 @@ export const TUI_AGENT_CONFIG: Record = { // Why: `ante --prompt` is headless (runs once and exits), so launch the bare TUI and inject after startup. promptInjectionMode: 'stdin-after-start' }, + trae: { + // 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 + // Trae subcommand or flag — `--` stops both in its Cobra parser. + argvPromptSeparator: '--' + }, opencode: { detectCmd: 'opencode', launchCmd: 'opencode', diff --git a/src/shared/tui-agent-display-names.ts b/src/shared/tui-agent-display-names.ts index 4d62d35f9..f680e16f9 100644 --- a/src/shared/tui-agent-display-names.ts +++ b/src/shared/tui-agent-display-names.ts @@ -12,6 +12,7 @@ export const TUI_AGENT_DISPLAY_NAMES: Record = { codex: 'Codex', devin: 'Devin', ante: 'Ante', + trae: 'Trae', autohand: 'Autohand Code', opencode: 'OpenCode', 'mimo-code': 'MiMo Code', diff --git a/src/shared/tui-agent-permissions.ts b/src/shared/tui-agent-permissions.ts index 9833bfca1..ae0c34391 100644 --- a/src/shared/tui-agent-permissions.ts +++ b/src/shared/tui-agent-permissions.ts @@ -27,7 +27,8 @@ export const YOLO_TUI_AGENT_ARGS: Partial> = { copilot: '--yolo', grok: '--permission-mode bypassPermissions', devin: '--permission-mode bypass', - ante: '--yolo' + ante: '--yolo', + trae: '--yolo' } export const YOLO_TUI_AGENT_ENV: Partial>> = { diff --git a/src/shared/tui-agent-selection.ts b/src/shared/tui-agent-selection.ts index 7ac9d2527..48e10305a 100644 --- a/src/shared/tui-agent-selection.ts +++ b/src/shared/tui-agent-selection.ts @@ -13,6 +13,7 @@ export const TUI_AGENT_AUTO_PICK_ORDER = [ 'opencode', 'mimo-code', 'ante', + 'trae', 'pi', 'omp', 'gemini', diff --git a/src/shared/types.ts b/src/shared/types.ts index ca3f65493..ec27f1267 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -2530,6 +2530,7 @@ export type TuiAgent = | 'grok' // xAI Grok CLI | 'devin' // Devin CLI | 'ante' // Ante (Antigma Labs) + | 'trae' // Trae CLI export type TaskViewPresetId = 'all' | 'issues' | 'review' | 'my-issues' | 'my-prs' | 'prs'