diff --git a/README.md b/README.md
index 0f337f9e677..93e09614609 100644
--- a/README.md
+++ b/README.md
@@ -195,6 +195,7 @@ Works with **any CLI agent** — if it runs in a terminal, it runs in Orca.
Autohand Code
Charm
Cline
+
CodeBuddy
Codebuff
Freebuff
Command Code
diff --git a/config/tsconfig.cli.json b/config/tsconfig.cli.json
index 030a90632b0..85f3529b2dc 100644
--- a/config/tsconfig.cli.json
+++ b/config/tsconfig.cli.json
@@ -9,6 +9,7 @@
"../src/main/claude/claude-hook-event-versions.ts",
"../src/main/claude/claude-managed-hook-events.ts",
"../src/main/qoder/hook-service.ts",
+ "../src/main/codebuddy/hook-service.ts",
"../src/main/agent-hooks/hook-stdin-contract.ts",
"../src/main/agent-hooks/hook-post-command.ts",
"../src/main/agent-hooks/hook-config-write-path.ts",
diff --git a/docs/reference/codebuddy-harness.md b/docs/reference/codebuddy-harness.md
new file mode 100644
index 00000000000..e59eaec855e
--- /dev/null
+++ b/docs/reference/codebuddy-harness.md
@@ -0,0 +1,44 @@
+# CodeBuddy terminal harness
+
+CodeBuddy uses its own identity in the desktop and mobile agent catalogs, process
+detection, telemetry, hooks, resume records and AI Vault. `codebuddy` and `cbc`
+identify the interactive CLI; print, server, ACP and detached background modes do
+not identify an interactive pane. Model and effort selections use `--model` and
+`--effort`; the CLI resolves its stable model aliases for the current account.
+
+Managed hooks use the existing Claude-compatible installer and transport, with
+CodeBuddy's own `.codebuddy/settings.json` and hook source. Installation preserves
+user hooks and statusline configuration. Local and SSH installers share the same
+plan; Windows explicitly selects CodeBuddy's supported PowerShell hook shell.
+Status flows through the execution host's canonical hook store.
+
+## Observed lifecycle
+
+Verified with the authenticated CodeBuddy 2.159.0 CLI on macOS:
+
+- `UserPromptSubmit` starts work. `SessionStart` can arrive afterward and must not
+ settle that work.
+- An unanswered `AskUserQuestion` emits a `Notification` with
+ `notification_type: permission_prompt` and the message
+ `needs your permission to use AskUserQuestion`.
+- In this version, `PreToolUse(AskUserQuestion)` arrives **after** the user answers;
+ it resumes working, followed by `PostToolUse` and `Stop`.
+- `Stop` supplies the final assistant message and the provider session identity
+ supplies the resume command.
+
+The sanitized real event sequence is
+`src/shared/__fixtures__/codebuddy-question-hooks.jsonl`. The regression test
+replays it through the shared hook listener. Qoder keeps its existing lifecycle
+semantics while sharing the common event projection.
+
+AI Vault reads CodeBuddy's `type: message` JSONL records with top-level roles and
+`input_text` / `output_text` content blocks. Local, WSL and SSH discovery use the
+provider's `.codebuddy/projects` tree and the same incremental parser.
+
+## Validation scope
+
+Live macOS checks exercised launch through Orca's agent menu, model switching,
+question waiting, answer submission, working and completion indicators, history
+discovery and a resumed session recalling its earlier answer. Hidden-renderer CDP
+screenshots record the working, question and completed states. Windows, Linux,
+WSL and SSH runtime execution have not been exercised live on this machine.
diff --git a/docs/site/content/docs/agents/supported.mdx b/docs/site/content/docs/agents/supported.mdx
index 0658ce6fd8c..ecc8cfd1305 100644
--- a/docs/site/content/docs/agents/supported.mdx
+++ b/docs/site/content/docs/agents/supported.mdx
@@ -45,6 +45,7 @@ To restore prompts for one agent only, edit that agent's default arguments or en
| Auggie | Auto-setup | [Augment](https://docs.augmentcode.com/cli/overview) |
| Autohand | Auto-setup | [Autohand](https://github.com/autohandai/code-cli) |
| Cline | Auto-setup | [Cline](https://docs.cline.bot/cline-cli/overview) |
+| CodeBuddy | `npm install -g @tencent-ai/codebuddy-code` | [CodeBuddy](https://www.codebuddy.ai/cli) |
| Codebuff | Auto-setup | [Codebuff](https://www.codebuff.com/docs/help/quick-start) |
| Freebuff | Auto-setup | [Freebuff](https://freebuff.com/cli) |
| Command Code | Auto-setup, status | [Command Code](https://commandcode.ai/docs/quickstart) |
diff --git a/mobile/src/components/mobile-agent-icon-assets.ts b/mobile/src/components/mobile-agent-icon-assets.ts
index 8d0a6d474f7..53745ead77e 100644
--- a/mobile/src/components/mobile-agent-icon-assets.ts
+++ b/mobile/src/components/mobile-agent-icon-assets.ts
@@ -18,6 +18,7 @@ export const MOBILE_AGENT_ICON_ASSETS: Partial = TUI_AGENT_DISPL
export const MOBILE_TUI_AGENT_FAVICON_DOMAINS: Partial> = {
openclaude: 'openclaude.gitlawb.com',
+ codebuddy: 'codebuddy.ai',
grok: 'x.ai',
copilot: 'github.com',
opencode: 'opencode.ai',
diff --git a/src/cli/search-command-arguments.test.ts b/src/cli/search-command-arguments.test.ts
index be9195a1fab..27b06286f52 100644
--- a/src/cli/search-command-arguments.test.ts
+++ b/src/cli/search-command-arguments.test.ts
@@ -126,7 +126,7 @@ describe('orca search argument parsing', () => {
it('rejects an unknown --agent and names the known ones', () => {
expect(() => parseSearch(['search', 'q', '--agent', 'claude', '--agent', 'bogus'])).toThrow(
- /Unknown --agent "bogus"\. Known agents: claude, codex, /
+ /Unknown --agent "bogus"\. Known agents: claude, codebuddy, codex, /
)
})
diff --git a/src/main/agent-hooks/managed-agent-hook-registry.ts b/src/main/agent-hooks/managed-agent-hook-registry.ts
index 31513658035..11b642ee090 100644
--- a/src/main/agent-hooks/managed-agent-hook-registry.ts
+++ b/src/main/agent-hooks/managed-agent-hook-registry.ts
@@ -1,3 +1,4 @@
+import { codebuddyHookService } from '../codebuddy/hook-service'
import { qoderHookService } from '../qoder/hook-service'
import type { AgentHookInstallStatus } from '../../shared/agent-hook-types'
import type { HookInstallAgent } from '../../shared/telemetry-events'
@@ -46,6 +47,7 @@ export const MANAGED_AGENT_HOOK_INSTALLERS: readonly ManagedAgentHookInstaller[]
['codex', () => codexHookService.install()],
['gemini', () => geminiHookService.install()],
['qoder', () => qoderHookService.install()],
+ ['codebuddy', () => codebuddyHookService.install()],
['antigravity', () => antigravityHookService.install()],
['amp', () => ampHookService.install()],
['cursor', () => cursorHookService.install()],
@@ -73,6 +75,7 @@ export const MANAGED_AGENT_HOOK_SCRIPT_REFRESHERS: readonly ManagedAgentHookScri
['codex', () => codexHookService.refreshManagedScripts()],
['gemini', () => geminiHookService.refreshManagedScripts()],
['qoder', () => qoderHookService.refreshManagedScripts()],
+ ['codebuddy', () => codebuddyHookService.refreshManagedScripts()],
['antigravity', () => antigravityHookService.refreshManagedScripts()],
['cursor', () => cursorHookService.refreshManagedScripts()],
['droid', () => droidHookService.refreshManagedScripts()],
@@ -92,6 +95,7 @@ export const MANAGED_AGENT_HOOK_REMOVERS: readonly ManagedAgentHookRemover[] = [
['codex', () => codexHookService.remove()],
['gemini', () => geminiHookService.remove()],
['qoder', () => qoderHookService.remove()],
+ ['codebuddy', () => codebuddyHookService.remove()],
['antigravity', () => antigravityHookService.remove()],
['amp', () => ampHookService.remove()],
['cursor', () => cursorHookService.remove()],
@@ -117,6 +121,7 @@ export const MANAGED_AGENT_HOOK_STATUS_READERS: readonly ManagedAgentHookStatusR
['codex', () => codexHookService.getStatus()],
['gemini', () => geminiHookService.getStatus()],
['qoder', () => qoderHookService.getStatus()],
+ ['codebuddy', () => codebuddyHookService.getStatus()],
['antigravity', () => antigravityHookService.getStatus()],
['amp', () => ampHookService.getStatus()],
['cursor', () => cursorHookService.getStatus()],
diff --git a/src/main/agent-hooks/managed-hook-command-contract.test.ts b/src/main/agent-hooks/managed-hook-command-contract.test.ts
index b3166a6c90e..ebb1412ec58 100644
--- a/src/main/agent-hooks/managed-hook-command-contract.test.ts
+++ b/src/main/agent-hooks/managed-hook-command-contract.test.ts
@@ -96,6 +96,20 @@ const buildersByAgent = new Map([
remote: (path) => [getClaudeRemoteCommand(path)]
}
],
+ [
+ 'codebuddy',
+ {
+ local: (path) => [
+ getManagedLifecycleHook(path, {
+ configDirName: '.codebuddy',
+ scriptBaseName: 'codebuddy-hook',
+ usesWindowsCompatLauncher: true,
+ windowsHookShell: 'powershell'
+ }).command
+ ],
+ remote: (path) => [getClaudeRemoteCommand(path)]
+ }
+ ],
[
'codex',
{
@@ -224,7 +238,8 @@ describe('managed hook command contract', () => {
const scannedCommand =
platform === 'win32' &&
((agent === 'codex' && command.startsWith('if (Test-Path')) ||
- (agent === 'qoder' && command.startsWith('$scriptPath = Join-Path')))
+ ((agent === 'qoder' || agent === 'codebuddy') &&
+ command.startsWith('$scriptPath = Join-Path')))
? command
.replaceAll('$LASTEXITCODE', '')
.replaceAll('$env:', '')
diff --git a/src/main/agent-hooks/remote-hook-service-registry-coverage.test.ts b/src/main/agent-hooks/remote-hook-service-registry-coverage.test.ts
index f9ff9c232e0..dea6c4a4663 100644
--- a/src/main/agent-hooks/remote-hook-service-registry-coverage.test.ts
+++ b/src/main/agent-hooks/remote-hook-service-registry-coverage.test.ts
@@ -1,3 +1,4 @@
+import { codebuddyHookService } from '../codebuddy/hook-service'
import { describe, expect, it, vi } from 'vitest'
vi.mock('electron', () => ({
@@ -53,6 +54,7 @@ describe('remote hook service registry coverage', () => {
['kimi', kimiHookService],
['muse', museHookService],
['qoder', qoderHookService],
+ ['codebuddy', codebuddyHookService],
['zcode', zcodeHookService],
['dsh', dshHookService]
])
diff --git a/src/main/agent-hooks/remote-managed-hook-installers.ts b/src/main/agent-hooks/remote-managed-hook-installers.ts
index 272e330be11..b012af91ff3 100644
--- a/src/main/agent-hooks/remote-managed-hook-installers.ts
+++ b/src/main/agent-hooks/remote-managed-hook-installers.ts
@@ -1,3 +1,4 @@
+import { codebuddyHookService } from '../codebuddy/hook-service'
import { qoderHookService } from '../qoder/hook-service'
import type { SFTPWrapper } from 'ssh2'
import type { AgentHookInstallStatus, AgentHookTarget } from '../../shared/agent-hook-types'
@@ -64,6 +65,7 @@ const REMOTE_MANAGED_HOOK_INSTALLERS: readonly RemoteManagedHookInstaller[] = [
],
['gemini', (sftp, remoteHome) => geminiHookService.installRemote(sftp, remoteHome)],
['qoder', (sftp, remoteHome) => qoderHookService.installRemote(sftp, remoteHome)],
+ ['codebuddy', (sftp, remoteHome) => codebuddyHookService.installRemote(sftp, remoteHome)],
['antigravity', (sftp, remoteHome) => antigravityHookService.installRemote(sftp, remoteHome)],
['amp', (sftp, remoteHome) => ampHookService.installRemote(sftp, remoteHome)],
['cursor', (sftp, remoteHome) => cursorHookService.installRemote(sftp, remoteHome)],
diff --git a/src/main/agent-hooks/server-retired-pane-new-turn.test.ts b/src/main/agent-hooks/server-retired-pane-new-turn.test.ts
index a405199a6e7..23dbb56c695 100644
--- a/src/main/agent-hooks/server-retired-pane-new-turn.test.ts
+++ b/src/main/agent-hooks/server-retired-pane-new-turn.test.ts
@@ -25,6 +25,7 @@ const NEW_TURN_EVENT: Record = {
qoder: 'SessionStart',
claude: 'SessionStart',
kimi: 'UserPromptSubmit',
+ codebuddy: 'UserPromptSubmit',
codex: 'SessionStart',
gemini: 'BeforeAgent',
antigravity: 'PreInvocation',
diff --git a/src/main/ai-vault/remote-session-scanner-sources.ts b/src/main/ai-vault/remote-session-scanner-sources.ts
index ed34e8b52a5..dfd012415d5 100644
--- a/src/main/ai-vault/remote-session-scanner-sources.ts
+++ b/src/main/ai-vault/remote-session-scanner-sources.ts
@@ -8,6 +8,7 @@ import { isAntigravityTranscriptPath } from './session-scanner-antigravity-paths
import { parseCodexSessionContent } from './session-scanner-codex-parser'
import { parseDroidSessionContent } from './session-scanner-droid-parser'
import { parseClaudeSessionContent } from './session-scanner-primary-parsers'
+import { parseCodebuddySessionContent } from './session-scanner-codebuddy-parser'
import { parseGeminiSessionContent } from './session-scanner-gemini-parsers'
import { parseCopilotSessionContent } from './session-scanner-copilot-parser'
import { parseCursorSessionContent } from './session-scanner-cursor-parser'
@@ -65,6 +66,17 @@ export function remoteSessionSources(
// top-level sessions carrying the parent's sessionId.
partitionSubagentTranscripts: partitionSubagentTranscriptPaths
},
+ {
+ // The execution host owns CodeBuddy's transcripts, including subagent pruning.
+ ...jsonlSource(
+ 'codebuddy',
+ remoteHome,
+ hostPlatform,
+ ['.codebuddy', 'projects'],
+ parseCodebuddySessionContent
+ ),
+ partitionSubagentTranscripts: partitionSubagentTranscriptPaths
+ },
remoteAntigravitySource(remoteHome, hostPlatform),
source(
'gemini',
diff --git a/src/main/ai-vault/session-cwd-bucket-layouts.test.ts b/src/main/ai-vault/session-cwd-bucket-layouts.test.ts
index 38c2f0b68e2..7cbbbfbcc6d 100644
--- a/src/main/ai-vault/session-cwd-bucket-layouts.test.ts
+++ b/src/main/ai-vault/session-cwd-bucket-layouts.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
-import { PI_CWD_BUCKET_LAYOUT } from './session-cwd-bucket-layouts'
+import { PI_CWD_BUCKET_LAYOUT, CODEBUDDY_CWD_BUCKET_LAYOUT } from './session-cwd-bucket-layouts'
function piDirInScope(dirName: string, scopePath: string): boolean {
return PI_CWD_BUCKET_LAYOUT.isDirInScope(
@@ -31,3 +31,19 @@ describe('PI_CWD_BUCKET_LAYOUT', () => {
expect(piDirInScope('--C--Users-ada-repo--', 'C:\\Users\\ada\\repo')).toBe(true)
})
})
+
+describe('CODEBUDDY_CWD_BUCKET_LAYOUT', () => {
+ it.each([
+ ['C:\\Users\\ada\\repo', 'C-Users-ada-repo-src'],
+ ['/home/ada/repo', 'home-ada-repo-src'],
+ ['/', 'home-ada-repo'],
+ [`/${'界'.repeat(100)}`, `${'界'.repeat(60)}-hash`]
+ ])('keeps candidate buckets for %s', (scope, bucket) => {
+ expect(
+ CODEBUDDY_CWD_BUCKET_LAYOUT.isDirInScope(
+ bucket,
+ new Set(CODEBUDDY_CWD_BUCKET_LAYOUT.encodeScopePrefixes(scope))
+ )
+ ).toBe(true)
+ })
+})
diff --git a/src/main/ai-vault/session-cwd-bucket-layouts.ts b/src/main/ai-vault/session-cwd-bucket-layouts.ts
index d8b4a565949..3f0195cdcd4 100644
--- a/src/main/ai-vault/session-cwd-bucket-layouts.ts
+++ b/src/main/ai-vault/session-cwd-bucket-layouts.ts
@@ -3,7 +3,7 @@ import { encodeClaudeProjectPaths, isClaudeProjectDirInScope } from './claude-pr
/** An agent that keeps one directory per cwd, named by a lossy encoding of that cwd. */
export type CwdBucketLayout = {
- agent: Extract
+ agent: Extract
encodeScopePrefixes: (scopePath: string) => string[]
isDirInScope: (dirName: string, prefixes: ReadonlySet) => boolean
}
@@ -41,3 +41,28 @@ export const PI_CWD_BUCKET_LAYOUT: CwdBucketLayout = {
return false
}
}
+
+// CodeBuddy 2.159.0 collapses separators and retains 180 UTF-8 bytes before long-path hashes.
+function codebuddyScopePrefix(path: string): string {
+ const encoded = path
+ .replace(/[/\\:]+/g, '-')
+ .replace(/-+/g, '-')
+ .replace(/^-|-$/g, '')
+ let prefix = ''
+ for (const character of encoded) {
+ if (Buffer.byteLength(prefix + character, 'utf8') > 180) {
+ break
+ }
+ prefix += character
+ }
+ return prefix
+}
+
+export const CODEBUDDY_CWD_BUCKET_LAYOUT: CwdBucketLayout = {
+ agent: 'codebuddy',
+ encodeScopePrefixes: (path) => [
+ ...new Set([path, path.normalize('NFC')].map(codebuddyScopePrefix))
+ ],
+ // This is only a prefilter: each transcript's cwd decides membership, including lossy collisions.
+ isDirInScope: (name, prefixes) => [...prefixes].some((prefix) => name.startsWith(prefix))
+}
diff --git a/src/main/ai-vault/session-scanner-agent-parser.ts b/src/main/ai-vault/session-scanner-agent-parser.ts
index 80a56bf2519..95910d208b7 100644
--- a/src/main/ai-vault/session-scanner-agent-parser.ts
+++ b/src/main/ai-vault/session-scanner-agent-parser.ts
@@ -1,3 +1,4 @@
+import { parseCodebuddySessionFile } from './session-scanner-codebuddy-parser'
import type { AiVaultSession } from '../../shared/ai-vault-types'
import { throwIfSignalAborted } from '../../shared/abort-signal-reason'
import { parseDevinSessionFile } from './session-scanner-devin-parser'
@@ -87,6 +88,8 @@ export async function parseAgentSessionFile(
signal?: AbortSignal
): Promise {
switch (candidate.agent) {
+ case 'codebuddy':
+ return parseCodebuddySessionFile(candidate.file, platform, messages)
case 'claude':
return parseClaudeSessionFile(candidate.file, platform, messages)
case 'codex':
diff --git a/src/main/ai-vault/session-scanner-agent-sources.ts b/src/main/ai-vault/session-scanner-agent-sources.ts
index ee10422031b..66d069b68cc 100644
--- a/src/main/ai-vault/session-scanner-agent-sources.ts
+++ b/src/main/ai-vault/session-scanner-agent-sources.ts
@@ -34,6 +34,7 @@ const COPILOT_SESSIONS_DIR = join(
'session-state'
)
const CURSOR_PROJECTS_DIR = join(homedir(), '.cursor', 'projects')
+const CODEBUDDY_PROJECTS_DIR = join(homedir(), '.codebuddy', 'projects')
const HERMES_SESSIONS_DIR = join(homedir(), '.hermes', 'sessions')
const ROVO_SESSIONS_DIR = join(homedir(), '.rovodev', 'sessions')
const OPENCLAW_STATE_DIR = resolveAbsoluteDirOverride(
@@ -114,6 +115,15 @@ export const AI_VAULT_AGENT_SOURCES: AiVaultAgentSourceTable = {
// their parent instead.
directoryPredicate: (name) => name !== SUBAGENT_DIR_NAME
},
+ codebuddy: {
+ rootDirs: (options, wslHomeDirs) => [
+ options.codebuddyProjectsDir ?? CODEBUDDY_PROJECTS_DIR,
+ ...wslHomeDirs.map((homeDir) => join(homeDir, '.codebuddy', 'projects'))
+ ],
+ extensions: ['.jsonl'],
+ // Nested subagent transcripts are not independent top-level conversations.
+ directoryPredicate: (name) => name !== SUBAGENT_DIR_NAME
+ },
codex: {
rootDirs: (options, wslHomeDirs) =>
uniqueCodexSessionsDirs([
diff --git a/src/main/ai-vault/session-scanner-codebuddy-parser.ts b/src/main/ai-vault/session-scanner-codebuddy-parser.ts
new file mode 100644
index 00000000000..94fc52d1eab
--- /dev/null
+++ b/src/main/ai-vault/session-scanner-codebuddy-parser.ts
@@ -0,0 +1,230 @@
+import { createInterface } from 'node:readline'
+import {
+ remoteSessionContentLines,
+ type RemoteSessionContent
+} from './remote-session-content-lines'
+import { openTranscriptReadStream } from '../native-chat/wsl-transcript-fs-access'
+import type { AiVaultSession } from '../../shared/ai-vault-types'
+import type { ExecutionHostId } from '../../shared/execution-host'
+import { isKnownHarnessInjectedUserTurnText } from '../../shared/harness-injected-user-turns'
+import type { FileWithMtime, ResumableSessionParseState } from './session-scanner-types'
+import type { TranscriptMessageSink } from './session-transcript-consumers'
+import {
+ addPreviewMessage,
+ accumulatorSessionIdentity,
+ createAccumulator,
+ finalizeSession,
+ sessionIdFromFileName,
+ updateLatestLocation,
+ updateTimeline
+} from './session-scanner-accumulator'
+import {
+ asRecord,
+ extractString,
+ normalizeTitleText,
+ parseJsonObject
+} from './session-scanner-values'
+
+type ParserSessionOptions = {
+ executionHostId?: ExecutionHostId
+ executionHostPlatform?: NodeJS.Platform | null
+}
+
+// CodeBuddy stores top-level message roles and input_text/output_text blocks, not Claude records.
+export type CodebuddySessionParseState = {
+ accumulator: ReturnType
+ aiTitle: string | null
+ firstUserTitle: string | null
+}
+
+export function createCodebuddySessionParseState(
+ file: FileWithMtime,
+ messages?: TranscriptMessageSink
+): CodebuddySessionParseState {
+ return {
+ accumulator: createAccumulator({
+ agent: 'codebuddy',
+ file,
+ sessionId: sessionIdFromFileName(file.path),
+ messages
+ }),
+ aiTitle: null,
+ firstUserTitle: null
+ }
+}
+
+export function cloneCodebuddySessionParseState(
+ state: CodebuddySessionParseState
+): CodebuddySessionParseState {
+ return {
+ accumulator: {
+ ...state.accumulator,
+ previewMessages: [...state.accumulator.previewMessages]
+ },
+ aiTitle: state.aiTitle,
+ firstUserTitle: state.firstUserTitle
+ }
+}
+
+// CodeBuddy conversation content is an array of typed text blocks; join the
+// text ones. Non-text blocks (tool calls, diffs) carry no preview copy.
+function codebuddyContentText(content: unknown): string | null {
+ if (typeof content === 'string') {
+ return content
+ }
+ if (!Array.isArray(content)) {
+ return null
+ }
+ const parts = content
+ .map((block) => {
+ const record = asRecord(block)
+ const type = extractString(record?.type)
+ return type === 'input_text' || type === 'output_text' || type === 'text'
+ ? extractString(record?.text)
+ : null
+ })
+ .filter((text): text is string => typeof text === 'string' && text.length > 0)
+ return parts.length > 0 ? parts.join('\n\n') : null
+}
+
+export function consumeCodebuddySessionLine(state: CodebuddySessionParseState, line: string): void {
+ const { accumulator } = state
+ const record = parseJsonObject(line)
+ if (!record) {
+ return
+ }
+
+ if (typeof record.sessionId === 'string' && record.sessionId.trim()) {
+ accumulator.sessionId = record.sessionId.trim()
+ }
+ updateTimeline(accumulator, record.timestamp)
+ updateLatestLocation(accumulator, record)
+
+ // The CLI's own session title; later summaries revise it, like Claude's
+ // custom-title.
+ if (record.type === 'summary') {
+ const summary = normalizeTitleText(extractString(record.summary) ?? '')
+ if (summary) {
+ accumulator.title = summary
+ }
+ return
+ }
+
+ if (record.type === 'ai-title') {
+ const title = normalizeTitleText(extractString(record.aiTitle) ?? '')
+ if (title) {
+ // CodeBuddy can revise generated names; AI Vault mirrors the current one.
+ state.aiTitle = title
+ }
+ return
+ }
+
+ if (record.type !== 'message') {
+ return
+ }
+
+ const role = extractString(record.role)
+ const text = codebuddyContentText(record.content)
+ accumulator.messageCount++
+ if (role === 'user') {
+ // Meta prompts (injected context) only seed the last-resort title.
+ // `providerData.skipRun` marks machinery-injected turns that never reached
+ // the model as a user prompt.
+ const isMetaUserTurn =
+ asRecord(record.providerData)?.skipRun === true ||
+ (text != null && isKnownHarnessInjectedUserTurnText(text))
+ addPreviewMessage(accumulator, {
+ role: 'user',
+ text,
+ timestamp: record.timestamp,
+ seedFirstUserPrompt: !isMetaUserTurn
+ })
+ if (text && !isMetaUserTurn) {
+ state.firstUserTitle ??= text
+ }
+ return
+ }
+ if (role === 'assistant') {
+ addPreviewMessage(accumulator, { role: 'assistant', text, timestamp: record.timestamp })
+ const model = extractString(asRecord(record.providerData)?.model)
+ if (model) {
+ accumulator.model = model
+ }
+ }
+}
+
+export function finalizeCodebuddySessionParseState(
+ state: CodebuddySessionParseState,
+ platform: NodeJS.Platform,
+ options: ParserSessionOptions = {}
+): AiVaultSession | null {
+ // Finalize a snapshot: the live state (and its preview array) may keep
+ // accumulating appended lines after this session object is handed out.
+ const snapshot = cloneCodebuddySessionParseState(state)
+ // Why: the CLI's summary title wins via accumulator.title; the generated
+ // ai-title should outrank the raw first prompt when present.
+ snapshot.accumulator.fallbackTitle = snapshot.aiTitle ?? snapshot.firstUserTitle
+ return finalizeSession(snapshot.accumulator, platform, options)
+}
+
+function codebuddyResumeStateFromParseState(
+ state: CodebuddySessionParseState
+): ResumableSessionParseState {
+ return {
+ consumeLine: (line) => consumeCodebuddySessionLine(state, line),
+ identity: () => accumulatorSessionIdentity(state.accumulator),
+ clone: () => codebuddyResumeStateFromParseState(cloneCodebuddySessionParseState(state)),
+ touchFile: (file) => {
+ state.accumulator.modifiedAt = file.modifiedAt
+ },
+ finalize: (platform, options) => finalizeCodebuddySessionParseState(state, platform, options)
+ }
+}
+
+export function createCodebuddySessionResumeState(
+ file: FileWithMtime,
+ messages?: TranscriptMessageSink
+): ResumableSessionParseState {
+ return codebuddyResumeStateFromParseState(createCodebuddySessionParseState(file, messages))
+}
+
+export async function parseCodebuddySessionFile(
+ file: FileWithMtime,
+ platform: NodeJS.Platform = process.platform,
+ messages?: TranscriptMessageSink
+): Promise {
+ const lines = createInterface({
+ input: openTranscriptReadStream(file.path, { encoding: 'utf-8' }, 'scan'),
+ crlfDelay: Infinity
+ })
+ return parseCodebuddySessionLines({ file, lines, platform, messages })
+}
+
+export async function parseCodebuddySessionContent(
+ file: FileWithMtime,
+ content: RemoteSessionContent,
+ platform: NodeJS.Platform = process.platform,
+ options: ParserSessionOptions = {},
+ signal?: AbortSignal
+): Promise {
+ return parseCodebuddySessionLines({
+ file,
+ lines: remoteSessionContentLines(content, signal),
+ platform,
+ options
+ })
+}
+
+async function parseCodebuddySessionLines(args: {
+ file: FileWithMtime
+ lines: AsyncIterable | Iterable
+ platform: NodeJS.Platform
+ options?: ParserSessionOptions
+ messages?: TranscriptMessageSink
+}): Promise {
+ const state = createCodebuddySessionParseState(args.file, args.messages)
+ for await (const line of args.lines) {
+ consumeCodebuddySessionLine(state, line)
+ }
+ return finalizeCodebuddySessionParseState(state, args.platform, args.options)
+}
diff --git a/src/main/ai-vault/session-scanner-codebuddy-scope.test.ts b/src/main/ai-vault/session-scanner-codebuddy-scope.test.ts
new file mode 100644
index 00000000000..9086ab1e811
--- /dev/null
+++ b/src/main/ai-vault/session-scanner-codebuddy-scope.test.ts
@@ -0,0 +1,135 @@
+import { mkdtemp, mkdir, rm, utimes, writeFile } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { afterEach, describe, expect, it } from 'vitest'
+import { scanAiVaultSessions } from './session-scanner'
+import { resetProjectDirCwdCacheForTests } from './session-scanner-scope-discovery'
+import { isolatedScanRoots, jsonLines } from './session-scanner-test-fixtures'
+
+let tempRoots: string[] = []
+
+afterEach(async () => {
+ await Promise.all(tempRoots.map((root) => rm(root, { recursive: true, force: true })))
+ tempRoots = []
+ resetProjectDirCwdCacheForTests()
+})
+
+function codebuddySessionDirName(cwd: string): string {
+ return cwd
+ .replace(/[/\\:]+/g, '-')
+ .replace(/-+/g, '-')
+ .replace(/^-|-$/g, '')
+}
+
+async function writeCodeBuddySession(args: {
+ sessionsDir: string
+ cwd: string
+ id: string
+ timestamp: string
+}): Promise {
+ const dir = join(args.sessionsDir, codebuddySessionDirName(args.cwd))
+ await mkdir(dir, { recursive: true })
+ const file = join(dir, `${args.id}.jsonl`)
+ await writeFile(
+ file,
+ jsonLines([
+ {
+ type: 'message',
+ timestamp: args.timestamp,
+ sessionId: args.id,
+ cwd: args.cwd,
+ role: 'user',
+ content: [{ type: 'input_text', text: `prompt ${args.id}` }]
+ }
+ ])
+ )
+ const time = new Date(args.timestamp)
+ await utimes(file, time, time)
+}
+
+describe('scanAiVaultSessions — CodeBuddy scope discovery', () => {
+ it('lists an older in-scope CodeBuddy session the recency cap would drop', async () => {
+ const root = await mkdtemp(join(tmpdir(), 'orca-ai-vault-codebuddy-scope-'))
+ tempRoots.push(root)
+ const roots = isolatedScanRoots(root)
+ const workspace = '/home/ada/orca/workspaces/orca/feature'
+
+ await writeCodeBuddySession({
+ sessionsDir: roots.codebuddyProjectsDir,
+ cwd: `${workspace}/packages/app`,
+ id: 'old-in-scope',
+ timestamp: '2026-05-01T10:00:00.000Z'
+ })
+ // Newer, out of scope, and a sibling whose encoding shares the workspace prefix.
+ await writeCodeBuddySession({
+ sessionsDir: roots.codebuddyProjectsDir,
+ cwd: '/home/ada/other',
+ id: 'recent-elsewhere',
+ timestamp: '2026-06-01T10:00:00.000Z'
+ })
+ await writeCodeBuddySession({
+ sessionsDir: roots.codebuddyProjectsDir,
+ cwd: `${workspace}-sibling`,
+ id: 'sibling',
+ timestamp: '2026-04-01T10:00:00.000Z'
+ })
+
+ const result = await scanAiVaultSessions({
+ ...roots,
+ platform: 'linux',
+ limit: 1,
+ scopePaths: [workspace]
+ })
+ const ids = result.sessions.map((session) => session.sessionId)
+
+ expect(ids).toContain('old-in-scope')
+ expect(ids).toContain('recent-elsewhere')
+ expect(ids).not.toContain('sibling')
+ expect(result.sessions.find((session) => session.sessionId === 'old-in-scope')?.agent).toBe(
+ 'codebuddy'
+ )
+ })
+
+ it('finds an older session when another cwd shares its encoded bucket', async () => {
+ const root = await mkdtemp(join(tmpdir(), 'orca-codebuddy-collision-'))
+ tempRoots.push(root)
+ const roots = isolatedScanRoots(root)
+ for (const [cwd, id, timestamp] of [
+ ['/home/ada/repo/app', 'in-scope', '2026-04-01T10:00:00.000Z'],
+ ['/home/ada/repo-app', 'collision', '2026-05-01T10:00:00.000Z'],
+ ['/home/ada/elsewhere', 'recent', '2026-06-01T10:00:00.000Z']
+ ]) {
+ await writeCodeBuddySession({ sessionsDir: roots.codebuddyProjectsDir, cwd, id, timestamp })
+ }
+ const result = await scanAiVaultSessions({
+ ...roots,
+ platform: 'linux',
+ limit: 1,
+ scopePaths: ['/home/ada/repo/app']
+ })
+ expect(result.sessions.map((session) => session.sessionId)).toContain('in-scope')
+ expect(result.sessions.map((session) => session.sessionId)).not.toContain('collision')
+ })
+
+ it('adds nothing when no scope is requested', async () => {
+ const root = await mkdtemp(join(tmpdir(), 'orca-ai-vault-codebuddy-noscope-'))
+ tempRoots.push(root)
+ const roots = isolatedScanRoots(root)
+ await writeCodeBuddySession({
+ sessionsDir: roots.codebuddyProjectsDir,
+ cwd: '/home/ada/old',
+ id: 'old',
+ timestamp: '2026-05-01T10:00:00.000Z'
+ })
+ await writeCodeBuddySession({
+ sessionsDir: roots.codebuddyProjectsDir,
+ cwd: '/home/ada/new',
+ id: 'new',
+ timestamp: '2026-06-01T10:00:00.000Z'
+ })
+
+ const result = await scanAiVaultSessions({ ...roots, platform: 'linux', limit: 1 })
+
+ expect(result.sessions.map((session) => session.sessionId)).toEqual(['new'])
+ })
+})
diff --git a/src/main/ai-vault/session-scanner-codebuddy-source.test.ts b/src/main/ai-vault/session-scanner-codebuddy-source.test.ts
new file mode 100644
index 00000000000..a171ac6d671
--- /dev/null
+++ b/src/main/ai-vault/session-scanner-codebuddy-source.test.ts
@@ -0,0 +1,80 @@
+import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { afterEach, describe, expect, it } from 'vitest'
+import { scanAiVaultSessions } from './session-scanner'
+import { isolatedScanRoots, jsonLines } from './session-scanner-test-fixtures'
+
+let tempRoots: string[] = []
+
+afterEach(async () => {
+ await Promise.all(tempRoots.map((root) => rm(root, { recursive: true, force: true })))
+ tempRoots = []
+})
+
+describe('scanAiVaultSessions — CodeBuddy transcripts', () => {
+ it('discovers ~/.codebuddy/projects transcripts and attributes them to codebuddy', async () => {
+ const root = await mkdtemp(join(tmpdir(), 'orca-ai-vault-codebuddy-'))
+ tempRoots.push(root)
+ const roots = isolatedScanRoots(root)
+ await mkdir(join(roots.codebuddyProjectsDir, 'project'), { recursive: true })
+
+ await writeFile(
+ join(roots.codebuddyProjectsDir, 'project', 'codebuddy-session.jsonl'),
+ jsonLines([
+ {
+ id: 'cb-msg-1',
+ timestamp: 1_777_634_400_000,
+ type: 'message',
+ role: 'user',
+ content: [{ type: 'input_text', text: 'fix the flaky test' }],
+ sessionId: 'codebuddy-session',
+ cwd: '/repo/app'
+ },
+ {
+ id: 'cb-msg-2',
+ timestamp: 1_777_634_405_000,
+ type: 'message',
+ role: 'assistant',
+ status: 'completed',
+ content: [{ type: 'output_text', text: 'On it' }],
+ providerData: { model: 'glm-5.3-flash' },
+ sessionId: 'codebuddy-session',
+ cwd: '/repo/app'
+ },
+ {
+ id: 'cb-title-1',
+ timestamp: 1_777_634_406_000,
+ type: 'ai-title',
+ aiTitle: 'Fixing the flaky test',
+ sessionId: 'codebuddy-session',
+ cwd: '/repo/app'
+ },
+ {
+ id: 'cb-summary-1',
+ timestamp: 1_777_634_407_000,
+ type: 'summary',
+ summary: 'Fix the flaky test',
+ providerData: { source: 'initial-user-message' }
+ }
+ ])
+ )
+
+ const result = await scanAiVaultSessions({ ...roots, platform: 'darwin' })
+
+ const codebuddy = result.sessions.find((session) => session.agent === 'codebuddy')
+ expect(codebuddy).toMatchObject({
+ sessionId: 'codebuddy-session',
+ cwd: '/repo/app',
+ title: 'Fix the flaky test',
+ model: 'glm-5.3-flash',
+ resumeCommand: "cd '/repo/app' && codebuddy --resume 'codebuddy-session'",
+ filePath: join(roots.codebuddyProjectsDir, 'project', 'codebuddy-session.jsonl')
+ })
+ expect(codebuddy?.messageCount).toBe(2)
+ expect(codebuddy?.previewMessages.map((message) => message.text)).toEqual([
+ 'fix the flaky test',
+ 'On it'
+ ])
+ })
+})
diff --git a/src/main/ai-vault/session-scanner-log-agent-fixtures.ts b/src/main/ai-vault/session-scanner-log-agent-fixtures.ts
index ed2cf8d2eee..be080271269 100644
--- a/src/main/ai-vault/session-scanner-log-agent-fixtures.ts
+++ b/src/main/ai-vault/session-scanner-log-agent-fixtures.ts
@@ -32,6 +32,23 @@ export async function writeLogAgentFixtures(
])
)
+ // CodeBuddy writes its own message-record shape under its own root.
+ await mkdir(join(roots.codebuddyProjectsDir, 'project'), { recursive: true })
+ await writeFile(
+ join(roots.codebuddyProjectsDir, 'project', 'codebuddy-session.jsonl'),
+ jsonlBody([
+ {
+ id: 'codebuddy-msg-1',
+ timestamp: 1_777_634_430_000,
+ type: 'message',
+ role: 'user',
+ content: [{ type: 'input_text', text: 'CodeBuddy title' }],
+ sessionId: 'codebuddy-session',
+ cwd: '/tmp/codebuddy'
+ }
+ ])
+ )
+
await mkdir(join(roots.codexSessionsDir, '2026', '05', '01'), { recursive: true })
await writeFile(
join(roots.codexSessionsDir, '2026', '05', '01', 'rollout-2026-codex-session.jsonl'),
diff --git a/src/main/ai-vault/session-scanner-parse-cache.ts b/src/main/ai-vault/session-scanner-parse-cache.ts
index 4610c198cb3..a00f4edd2cf 100644
--- a/src/main/ai-vault/session-scanner-parse-cache.ts
+++ b/src/main/ai-vault/session-scanner-parse-cache.ts
@@ -5,6 +5,7 @@ import { createCodexSessionResumeState } from './session-scanner-codex-parser'
import { createDroidSessionResumeState } from './session-scanner-droid-parser'
import { createMessageGraphSessionResumeState } from './session-scanner-graph-parsers'
import { createClaudeSessionResumeState } from './session-scanner-primary-parsers'
+import { createCodebuddySessionResumeState } from './session-scanner-codebuddy-parser'
import { createGeminiJsonlSessionResumeState } from './session-scanner-gemini-parsers'
import { createCopilotSessionResumeState } from './session-scanner-copilot-parser'
import { createCursorSessionResumeState } from './session-scanner-cursor-parser'
@@ -51,6 +52,8 @@ function resumableStateFactoryFor(
switch (candidate.agent) {
case 'claude':
return (messages) => createClaudeSessionResumeState(candidate.file, messages)
+ case 'codebuddy':
+ return (messages) => createCodebuddySessionResumeState(candidate.file, messages)
case 'codex':
return (messages) =>
createCodexSessionResumeState(candidate.file, candidate.codexHome, messages)
diff --git a/src/main/ai-vault/session-scanner-scope-discovery.ts b/src/main/ai-vault/session-scanner-scope-discovery.ts
index d9ef112f772..52795830e66 100644
--- a/src/main/ai-vault/session-scanner-scope-discovery.ts
+++ b/src/main/ai-vault/session-scanner-scope-discovery.ts
@@ -20,7 +20,7 @@ const REPRESENTATIVE_CWD_LINE_LIMIT = 200
const REPRESENTATIVE_FILE_LIMIT = 3
const TRANSCRIPT_EXTENSIONS = new Set(['.jsonl'])
-// Both encodings are lossy; retain Claude’s existing cache and verify each Pi transcript.
+// Bucket encodings are lossy; retain Claude’s cache and verify other transcripts individually.
const PROJECT_DIR_CWD_CACHE_MAX = 2048
const projectDirCwdCache = new Map()
@@ -75,7 +75,7 @@ type InScopeDiscoveryArgs = {
}
/**
- * Fully include the transcripts of cwd-bucket directories (Claude, Pi) whose cwd
+ * Fully include the transcripts of cwd-bucket directories (Claude, Pi, CodeBuddy) whose cwd
* falls inside the active workspace/project paths.
*
* Why: these agents keep one directory per cwd, e.g. `~/.claude/projects//`.
@@ -108,7 +108,7 @@ export async function discoverInScopeCwdBucketFiles(
}
await collectBucketFiles({
agent: layout.agent,
- scopePaths: layout.agent === 'pi' ? args.scopePaths : undefined,
+ scopePaths: layout.agent !== 'claude' ? args.scopePaths : undefined,
projectDir,
issues: args.issues,
collected,
diff --git a/src/main/ai-vault/session-scanner-test-fixtures.ts b/src/main/ai-vault/session-scanner-test-fixtures.ts
index 43d03d340dd..0d9b8cef5ab 100644
--- a/src/main/ai-vault/session-scanner-test-fixtures.ts
+++ b/src/main/ai-vault/session-scanner-test-fixtures.ts
@@ -54,6 +54,7 @@ export async function writeOpenCode2SqliteFixture(root: string): Promise
export function isolatedScanRoots(root: string) {
return {
claudeProjectsDir: join(root, 'claude-projects'),
+ codebuddyProjectsDir: join(root, 'codebuddy-projects'),
codexSessionsDir: join(root, 'codex-sessions'),
geminiSessionsDir: join(root, 'gemini-sessions'),
antigravityBrainDir: join(root, 'antigravity-brain'),
diff --git a/src/main/ai-vault/session-scanner-types.ts b/src/main/ai-vault/session-scanner-types.ts
index 099b3593431..be97a4b3187 100644
--- a/src/main/ai-vault/session-scanner-types.ts
+++ b/src/main/ai-vault/session-scanner-types.ts
@@ -14,6 +14,7 @@ import type { OpenCodeWslRuntime } from './session-scanner-opencode-wsl-runtime'
export type AiVaultScanOptions = {
claudeProjectsDir?: string
+ codebuddyProjectsDir?: string
codexSessionsDir?: string
additionalCodexSessionsDirs?: readonly string[]
// Why: tests inject a sandbox "real ~/.codex" so real-home attribution
diff --git a/src/main/ai-vault/session-scanner.ts b/src/main/ai-vault/session-scanner.ts
index 2f5629f94c3..f56f17af796 100644
--- a/src/main/ai-vault/session-scanner.ts
+++ b/src/main/ai-vault/session-scanner.ts
@@ -27,7 +27,11 @@ import { getSessionParseCacheEntry } from './session-parse-cache-store'
import { describeSkippedTranscriptRecords } from './session-transcript-record-budget'
import { canStopParsingSessions } from './session-scan-cutoff'
import { discoverInScopeCwdBucketFiles } from './session-scanner-scope-discovery'
-import { CLAUDE_CWD_BUCKET_LAYOUT, PI_CWD_BUCKET_LAYOUT } from './session-cwd-bucket-layouts'
+import {
+ CLAUDE_CWD_BUCKET_LAYOUT,
+ PI_CWD_BUCKET_LAYOUT,
+ CODEBUDDY_CWD_BUCKET_LAYOUT
+} from './session-cwd-bucket-layouts'
import { discoverAiVaultSessionSources } from './session-scanner-source-discovery'
import { cursorChatMetaRefusals, withCursorChatMetaScan } from './session-scanner-cursor-chat-meta'
import type {
@@ -166,7 +170,11 @@ function mergeSessions(
// Agents whose on-disk layout names a directory per cwd, so a scope's older
// sessions can be found without reading every transcript's header.
-const CWD_BUCKET_LAYOUTS = [CLAUDE_CWD_BUCKET_LAYOUT, PI_CWD_BUCKET_LAYOUT]
+const CWD_BUCKET_LAYOUTS = [
+ CLAUDE_CWD_BUCKET_LAYOUT,
+ PI_CWD_BUCKET_LAYOUT,
+ CODEBUDDY_CWD_BUCKET_LAYOUT
+]
async function scanInScopeSessions(args: {
discoveries: SessionFileDiscovery[]
diff --git a/src/main/claude/hook-settings.ts b/src/main/claude/hook-settings.ts
index 6ce4ea4869c..381e4d26b6f 100644
--- a/src/main/claude/hook-settings.ts
+++ b/src/main/claude/hook-settings.ts
@@ -19,8 +19,8 @@ import { isGitBashAvailable } from '../git-bash'
import type { ClaudeManagedHookPlan } from './claude-managed-hook-events'
export type ClaudeCompatibleHookSettings = {
- configDirName: '.claude' | '.openclaude' | '.qoder'
- scriptBaseName: 'claude-hook' | 'openclaude-hook' | 'qoder-hook'
+ configDirName: '.claude' | '.openclaude' | '.qoder' | '.codebuddy'
+ scriptBaseName: 'claude-hook' | 'openclaude-hook' | 'qoder-hook' | 'codebuddy-hook'
usesWindowsCompatLauncher: boolean
windowsHookShell?: 'powershell'
}
diff --git a/src/main/codebuddy/hook-service.test.ts b/src/main/codebuddy/hook-service.test.ts
new file mode 100644
index 00000000000..e20ae46e39c
--- /dev/null
+++ b/src/main/codebuddy/hook-service.test.ts
@@ -0,0 +1,47 @@
+import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import type * as OsModule from 'node:os'
+import { join } from 'node:path'
+import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
+const sandbox = vi.hoisted(() => ({ home: '' }))
+vi.mock('node:os', async (importOriginal) => ({
+ ...(await importOriginal()),
+ homedir: () => sandbox.home
+}))
+vi.mock('electron', () => ({ app: { getPath: () => sandbox.home } }))
+import { codebuddyHookService, CODEBUDDY_HOOK_EVENTS } from './hook-service'
+
+beforeAll(() => {
+ sandbox.home = mkdtempSync(join(tmpdir(), 'orca-codebuddy-test-'))
+ mkdirSync(join(sandbox.home, '.codebuddy'))
+})
+afterAll(() => rmSync(sandbox.home, { recursive: true, force: true }))
+
+describe('CodeBuddy managed hooks', () => {
+ it('preserves user configuration through repeated install and removal', () => {
+ const path = join(sandbox.home, '.codebuddy', 'settings.json')
+ const userHook = { hooks: [{ type: 'command', command: 'echo user-hook' }] }
+ const settings = {
+ model: 'custom',
+ hooks: { Stop: [userHook] },
+ statusLine: { command: 'user-status' }
+ }
+ writeFileSync(path, JSON.stringify(settings))
+ expect(codebuddyHookService.install().state).toBe('installed')
+ expect(codebuddyHookService.install().state).toBe('installed')
+ const installed = JSON.parse(readFileSync(path, 'utf8'))
+ for (const event of CODEBUDDY_HOOK_EVENTS) {
+ expect(installed.hooks[event]).toHaveLength(event === 'Stop' ? 2 : 1)
+ }
+ expect(installed.statusLine).toEqual(settings.statusLine)
+ expect(codebuddyHookService.remove().state).toBe('not_installed')
+ expect(JSON.parse(readFileSync(path, 'utf8'))).toMatchObject(settings)
+ })
+
+ it('leaves malformed user settings untouched', () => {
+ const path = join(sandbox.home, '.codebuddy', 'settings.json')
+ writeFileSync(path, '{broken')
+ expect(codebuddyHookService.install().state).toBe('error')
+ expect(readFileSync(path, 'utf8')).toBe('{broken')
+ })
+})
diff --git a/src/main/codebuddy/hook-service.ts b/src/main/codebuddy/hook-service.ts
new file mode 100644
index 00000000000..1a7c9619a34
--- /dev/null
+++ b/src/main/codebuddy/hook-service.ts
@@ -0,0 +1,34 @@
+import type { ClaudeManagedHookPlan } from '../claude/claude-managed-hook-events'
+import { ClaudeHookService } from '../claude/hook-service'
+
+export const CODEBUDDY_HOOK_EVENTS = [
+ 'SessionStart',
+ 'SessionEnd',
+ 'UserPromptSubmit',
+ 'PreToolUse',
+ 'PostToolUse',
+ 'PostToolUseFailure',
+ 'PermissionRequest',
+ 'Stop',
+ 'StopFailure',
+ 'Notification'
+] as const
+
+export const CODEBUDDY_MANAGED_HOOK_PLAN: ClaudeManagedHookPlan = {
+ install: CODEBUDDY_HOOK_EVENTS.map((eventName) => ({ eventName, definition: {} })),
+ retire: [],
+ statusLine: 'leave'
+}
+
+export const codebuddyHookService = new ClaudeHookService({
+ agent: 'codebuddy',
+ source: 'codebuddy',
+ displayName: 'CodeBuddy',
+ settings: {
+ configDirName: '.codebuddy',
+ scriptBaseName: 'codebuddy-hook',
+ usesWindowsCompatLauncher: true,
+ windowsHookShell: 'powershell'
+ },
+ hookPlan: CODEBUDDY_MANAGED_HOOK_PLAN
+})
diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json
index caed5202f08..cbbba795d59 100644
--- a/src/renderer/src/i18n/locales/en.json
+++ b/src/renderer/src/i18n/locales/en.json
@@ -616,7 +616,8 @@
"zcode_label": "ZCode",
"d443a47995": "Prime Agent",
"mimo_code_label": "MiMo Code",
- "opencode2_label": "OpenCode 2"
+ "opencode2_label": "OpenCode 2",
+ "codebuddy_label": "CodeBuddy"
},
"skill": {
"cli": {
diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json
index 05d26267342..3e2132d6011 100644
--- a/src/renderer/src/i18n/locales/es.json
+++ b/src/renderer/src/i18n/locales/es.json
@@ -385,7 +385,8 @@
"d443a47995": "Prime Agent",
"mimo_code_label": "MiMo Code",
"muse_label": "Muse",
- "zcode_label": "ZCode"
+ "zcode_label": "ZCode",
+ "codebuddy_label": "CodeBuddy"
},
"skill": {
"cli": {
diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json
index 4f591ca8bf2..53ceaf15c39 100644
--- a/src/renderer/src/i18n/locales/fr.json
+++ b/src/renderer/src/i18n/locales/fr.json
@@ -595,6 +595,7 @@
"mimo_code_label": "MiMo Code",
"muse_label": "Muse",
"opencode2_label": "OpenCode 2",
+ "codebuddy_label": "CodeBuddy",
"zcode_label": "ZCode"
},
"skill": {
diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json
index 3e4a5cd42ee..1b55d87552a 100644
--- a/src/renderer/src/i18n/locales/ja.json
+++ b/src/renderer/src/i18n/locales/ja.json
@@ -496,6 +496,7 @@
"mimo_code_label": "MiMo Code",
"muse_label": "Muse",
"opencode2_label": "OpenCode 2",
+ "codebuddy_label": "CodeBuddy",
"zcode_label": "ZCode"
},
"skill": {
diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json
index 75f96c732ab..a5aca8c17b6 100644
--- a/src/renderer/src/i18n/locales/ko.json
+++ b/src/renderer/src/i18n/locales/ko.json
@@ -496,6 +496,7 @@
"mimo_code_label": "MiMo Code",
"muse_label": "Muse",
"opencode2_label": "OpenCode 2",
+ "codebuddy_label": "CodeBuddy",
"zcode_label": "ZCode"
},
"skill": {
diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json
index 5d3eadd54d0..47fefd2f0e3 100644
--- a/src/renderer/src/i18n/locales/zh.json
+++ b/src/renderer/src/i18n/locales/zh.json
@@ -496,6 +496,7 @@
"mimo_code_label": "MiMo Code",
"muse_label": "Muse",
"opencode2_label": "OpenCode 2",
+ "codebuddy_label": "CodeBuddy",
"zcode_label": "ZCode"
},
"skill": {
diff --git a/src/renderer/src/lib/agent-catalog-platform.ts b/src/renderer/src/lib/agent-catalog-platform.ts
new file mode 100644
index 00000000000..9a9eac351f0
--- /dev/null
+++ b/src/renderer/src/lib/agent-catalog-platform.ts
@@ -0,0 +1,13 @@
+export function getCatalogPlatform(): NodeJS.Platform {
+ const userAgent = typeof navigator === 'undefined' ? '' : navigator.userAgent
+ if (userAgent.includes('Windows')) {
+ return 'win32'
+ }
+ if (userAgent.includes('Mac')) {
+ return 'darwin'
+ }
+ if (userAgent) {
+ return 'linux'
+ }
+ return typeof process === 'undefined' ? 'linux' : process.platform
+}
diff --git a/src/renderer/src/lib/agent-catalog.tsx b/src/renderer/src/lib/agent-catalog.tsx
index 56cd2180c72..325d838552d 100644
--- a/src/renderer/src/lib/agent-catalog.tsx
+++ b/src/renderer/src/lib/agent-catalog.tsx
@@ -1,3 +1,4 @@
+import { getCatalogPlatform } from './agent-catalog-platform'
import type React from 'react'
import { ClaudeIcon, DroidIcon, OpenAIIcon } from '@/components/status-bar/icons'
import openClaudeLogoUrl from '../../../../resources/openclaude-logo.png?url'
@@ -30,20 +31,6 @@ export type AgentCatalogEntry = {
homepageUrl: string
}
-function getCatalogPlatform(): NodeJS.Platform {
- const userAgent = typeof navigator === 'undefined' ? '' : navigator.userAgent
- if (userAgent.includes('Windows')) {
- return 'win32'
- }
- if (userAgent.includes('Mac')) {
- return 'darwin'
- }
- if (userAgent) {
- return 'linux'
- }
- return typeof process === 'undefined' ? 'linux' : process.platform
-}
-
export const getAgentCatalog = createLocalizedCatalog((): AgentCatalogEntry[] => [
{
id: 'claude',
@@ -349,6 +336,13 @@ export const getAgentCatalog = createLocalizedCatalog((): AgentCatalogEntry[] =>
cmd: 'openclaw',
faviconDomain: 'openclaw.ai',
homepageUrl: 'https://github.com/openclaw/openclaw'
+ },
+ {
+ id: 'codebuddy',
+ label: translate('auto.lib.agent.catalog.codebuddy_label', 'CodeBuddy'),
+ cmd: 'codebuddy',
+ faviconDomain: 'codebuddy.ai',
+ homepageUrl: 'https://www.codebuddy.ai/cli'
}
])
diff --git a/src/renderer/src/lib/agent-status.ts b/src/renderer/src/lib/agent-status.ts
index 41ccc2cc7d7..04ecd728541 100644
--- a/src/renderer/src/lib/agent-status.ts
+++ b/src/renderer/src/lib/agent-status.ts
@@ -100,6 +100,7 @@ export { formatAgentTypeLabel } from '../../../shared/agent-type-label'
const ICONABLE_AGENT_TYPES: Record = {
claude: true,
'claude-agent-teams': true,
+ codebuddy: true,
openclaude: true,
codex: true,
autohand: true,
diff --git a/src/renderer/src/runtime/agent-resume-host-authority-capability.test.ts b/src/renderer/src/runtime/agent-resume-host-authority-capability.test.ts
index 24393546a81..e5b6d8327ae 100644
--- a/src/renderer/src/runtime/agent-resume-host-authority-capability.test.ts
+++ b/src/renderer/src/runtime/agent-resume-host-authority-capability.test.ts
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import { RESUMABLE_TUI_AGENTS } from '../../../shared/agent-session-resume'
import {
AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY,
+ AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY,
AGENT_SESSION_DSH_RESUME_RUNTIME_CAPABILITY,
AGENT_SESSION_KIMI_RESUME_RUNTIME_CAPABILITY,
AGENT_SESSION_MUSE_RESUME_RUNTIME_CAPABILITY,
@@ -56,6 +57,7 @@ describe('agentResumeHostAuthorityCapability', () => {
)
).toEqual({
claude: undefined,
+ codebuddy: AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY,
codex: undefined,
qoder: AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY,
gemini: undefined,
diff --git a/src/renderer/src/runtime/agent-resume-host-authority-capability.ts b/src/renderer/src/runtime/agent-resume-host-authority-capability.ts
index efec3fe38b1..a5cefe6a74c 100644
--- a/src/renderer/src/runtime/agent-resume-host-authority-capability.ts
+++ b/src/renderer/src/runtime/agent-resume-host-authority-capability.ts
@@ -3,6 +3,7 @@ import type { TuiAgent } from '../../../shared/tui-agent'
import {
AGENT_SESSION_KIMI_RESUME_RUNTIME_CAPABILITY,
AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY,
+ AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY,
AGENT_SESSION_DSH_RESUME_RUNTIME_CAPABILITY,
AGENT_SESSION_MUSE_RESUME_RUNTIME_CAPABILITY,
AGENT_SESSION_ZCODE_RESUME_RUNTIME_CAPABILITY,
@@ -20,6 +21,7 @@ import {
const RESUME_HOST_AUTHORITY_CAPABILITY_BY_AGENT = {
// These shipped inside agent-session.host-authority.v1's enum, so the generic probe covers them.
claude: undefined,
+ codebuddy: AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY,
codex: undefined,
qoder: AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY,
gemini: undefined,
diff --git a/src/renderer/src/runtime/remote-agent-session-launch.test.ts b/src/renderer/src/runtime/remote-agent-session-launch.test.ts
index d8cf0c542cb..dda22ff12fd 100644
--- a/src/renderer/src/runtime/remote-agent-session-launch.test.ts
+++ b/src/renderer/src/runtime/remote-agent-session-launch.test.ts
@@ -45,7 +45,7 @@ describe('remote agent-session launch routing', () => {
expect(legacy).not.toHaveBeenCalled()
})
- it.each(['kimi', 'muse'] as const)(
+ it.each(['kimi', 'muse', 'codebuddy'] as const)(
'falls back to legacy when an older host lacks the %s resume capability',
async (agent) => {
const hostAuthority = vi.fn().mockResolvedValue('structured')
@@ -70,23 +70,29 @@ describe('remote agent-session launch routing', () => {
}
)
- it('uses host authority when the host supports Muse resume', async () => {
- const hostAuthority = vi.fn().mockResolvedValue('host')
- const legacy = vi.fn()
- mocks.supportsCapability.mockResolvedValue(true)
+ it.each(['muse', 'codebuddy'] as const)(
+ 'uses host authority when the host supports %s resume',
+ async (agent) => {
+ const hostAuthority = vi.fn().mockResolvedValue('host')
+ const legacy = vi.fn()
+ mocks.supportsCapability.mockResolvedValue(true)
- await expect(
- runRemoteAgentSessionLaunch({
- environmentId: 'env-1',
- hostAuthority,
- hostAuthorityCapability: agentResumeHostAuthorityCapability('muse'),
- legacy
- })
- ).resolves.toBe('host')
- expect(mocks.supportsCapability).toHaveBeenCalledWith('env-1', 'agent-session.muse-resume.v1')
- expect(hostAuthority).toHaveBeenCalledOnce()
- expect(legacy).not.toHaveBeenCalled()
- })
+ await expect(
+ runRemoteAgentSessionLaunch({
+ environmentId: 'env-1',
+ hostAuthority,
+ hostAuthorityCapability: agentResumeHostAuthorityCapability(agent),
+ legacy
+ })
+ ).resolves.toBe('host')
+ expect(mocks.supportsCapability).toHaveBeenCalledWith(
+ 'env-1',
+ `agent-session.${agent}-resume.v1`
+ )
+ expect(hostAuthority).toHaveBeenCalledOnce()
+ expect(legacy).not.toHaveBeenCalled()
+ }
+ )
it('preserves the exact legacy path when the capability is absent', async () => {
const hostAuthority = vi.fn().mockResolvedValue('structured')
diff --git a/src/shared/__fixtures__/codebuddy-question-hooks.jsonl b/src/shared/__fixtures__/codebuddy-question-hooks.jsonl
new file mode 100644
index 00000000000..81a69a899b6
--- /dev/null
+++ b/src/shared/__fixtures__/codebuddy-question-hooks.jsonl
@@ -0,0 +1,6 @@
+{"hook_event_name": "UserPromptSubmit", "prompt": "Use AskUserQuestion to ask me to choose red or blue. After I answer, reply with my color and stop.", "permission_mode": "default", "version": "2.159.0", "model": "fast-model", "session_id": "codebuddy-verification", "transcript_path": "/sessions/codebuddy-verification.jsonl"}
+{"hook_event_name": "SessionStart", "source": "startup", "permission_mode": "default", "version": "2.159.0", "model": "fast-model", "session_id": "codebuddy-verification", "transcript_path": "/sessions/codebuddy-verification.jsonl"}
+{"hook_event_name": "Notification", "notification_type": "permission_prompt", "message": "needs your permission to use AskUserQuestion", "permission_mode": "default", "version": "2.159.0", "model": "fast-model", "session_id": "codebuddy-verification", "transcript_path": "/sessions/codebuddy-verification.jsonl"}
+{"hook_event_name": "PreToolUse", "tool_name": "AskUserQuestion", "tool_input": {"questions": [{"header": "Color", "multiSelect": false, "options": [{"description": "Choose red", "label": "Red"}, {"description": "Choose blue", "label": "Blue"}], "question": "Which color do you choose?"}]}, "permission_mode": "default", "version": "2.159.0", "model": "fast-model", "session_id": "codebuddy-verification", "transcript_path": "/sessions/codebuddy-verification.jsonl"}
+{"hook_event_name": "PostToolUse", "tool_name": "AskUserQuestion", "tool_input": {"questions": [{"header": "Color", "multiSelect": false, "options": [{"description": "Choose red", "label": "Red"}, {"description": "Choose blue", "label": "Blue"}], "question": "Which color do you choose?"}]}, "tool_response": " \u00b7 Which color do you choose? \u2192 Red", "permission_mode": "default", "version": "2.159.0", "model": "fast-model", "session_id": "codebuddy-verification", "transcript_path": "/sessions/codebuddy-verification.jsonl"}
+{"hook_event_name": "Stop", "last_assistant_message": "Red", "permission_mode": "default", "version": "2.159.0", "model": "fast-model", "session_id": "codebuddy-verification", "transcript_path": "/sessions/codebuddy-verification.jsonl"}
diff --git a/src/shared/__fixtures__/codebuddy-question-hooks.meta.json b/src/shared/__fixtures__/codebuddy-question-hooks.meta.json
new file mode 100644
index 00000000000..28ac3a2d76b
--- /dev/null
+++ b/src/shared/__fixtures__/codebuddy-question-hooks.meta.json
@@ -0,0 +1,6 @@
+{
+ "version": "2.159.0",
+ "platform": "darwin",
+ "model": "fast-model",
+ "description": "Live CLI: question notification precedes PreToolUse; PreToolUse follows the answer; SessionStart follows UserPromptSubmit. Host paths and session identifiers replaced."
+}
diff --git a/src/shared/agent-headless-command.ts b/src/shared/agent-headless-command.ts
index f3f67535963..9f742a5355d 100644
--- a/src/shared/agent-headless-command.ts
+++ b/src/shared/agent-headless-command.ts
@@ -1,3 +1,4 @@
+import { isCodebuddyNonInteractiveCommand } from './codebuddy-headless-command'
import { isQoderHeadlessCommand } from './qoder-headless-command'
import { isAnteHeadlessOneShotCommand } from './ante-headless-command'
import { isDshNonInteractiveCommand } from './dsh-launch-command'
@@ -18,6 +19,7 @@ const HEADLESS_ONE_SHOT_MATCHERS: Partial<
> = {
qoder: isQoderHeadlessCommand,
claude: isPrintModeHeadlessOneShotCommand,
+ codebuddy: isCodebuddyNonInteractiveCommand,
trae: isPrintModeHeadlessOneShotCommand,
'prime-agent': isPrimeAgentHeadlessOneShotCommand,
ante: isAnteHeadlessOneShotCommand,
diff --git a/src/shared/agent-hook-listener/provider-dispatch.ts b/src/shared/agent-hook-listener/provider-dispatch.ts
index 067e4552212..ab7e847c4ca 100644
--- a/src/shared/agent-hook-listener/provider-dispatch.ts
+++ b/src/shared/agent-hook-listener/provider-dispatch.ts
@@ -1,3 +1,4 @@
+import { normalizeCompatibleLifecycleEvent } from './providers/compatible-lifecycle-events'
import { normalizeQoderEvent } from './providers/qoder-events'
import type { ParsedAgentStatusPayload } from '../agent-status-types'
import type { AgentHookSource } from '../agent-hook-relay'
@@ -53,6 +54,16 @@ export function normalizeProviderEvent(input: {
let payload: ParsedAgentStatusPayload | null
switch (source) {
+ case 'codebuddy':
+ payload = normalizeCompatibleLifecycleEvent(
+ source,
+ state,
+ eventName,
+ promptText,
+ paneKey,
+ hookPayload
+ )
+ break
case 'claude':
payload = normalizeClaudeEvent(state, eventName, promptText, paneKey, hookPayload)
break
diff --git a/src/shared/agent-hook-listener/provider-event-routing.ts b/src/shared/agent-hook-listener/provider-event-routing.ts
index 40e59a1d820..80cd0fdc91c 100644
--- a/src/shared/agent-hook-listener/provider-event-routing.ts
+++ b/src/shared/agent-hook-listener/provider-event-routing.ts
@@ -30,6 +30,7 @@ export function isNewTurnEvent(source: AgentHookSource, eventName: unknown): boo
// Why: SessionStart lands an idle row (STA-3386) and must also drop stale
// tool/prompt caches left by the pane's previous session.
return eventName === 'SessionStart' || eventName === 'UserPromptSubmit'
+ case 'codebuddy':
case 'kimi':
// Why: Kimi Code emits Claude-compatible hook events, so UserPromptSubmit is its new-turn boundary too.
return eventName === 'UserPromptSubmit'
@@ -143,6 +144,7 @@ export function extractToolFields(
case 'claude':
// Why: Kimi Code uses Claude's tool_name/tool_input payload fields verbatim.
// falls through
+ case 'codebuddy':
case 'kimi':
// Muse uses Claude-compatible tool fields.
// falls through
diff --git a/src/shared/agent-hook-listener/providers/compatible-lifecycle-events.ts b/src/shared/agent-hook-listener/providers/compatible-lifecycle-events.ts
new file mode 100644
index 00000000000..2678dfc5e1e
--- /dev/null
+++ b/src/shared/agent-hook-listener/providers/compatible-lifecycle-events.ts
@@ -0,0 +1,104 @@
+import { isAskUserQuestionTool } from '../../agent-question-answered-intent'
+import {
+ normalizeAgentStatusPayload,
+ type ParsedAgentStatusPayload
+} from '../../agent-status-types'
+import type { HookListenerState } from '../listener-state'
+import {
+ resolvePrompt,
+ resolveToolState,
+ shouldIgnoreCompactContinuationUserPromptSubmit
+} from '../prompt-fields'
+import { extractToolFields, isNewTurnEvent } from '../provider-event-routing'
+import { readString } from '../tool-input-preview'
+
+function readCompatibleState(
+ eventName: unknown,
+ payload: Record
+): 'working' | 'waiting' | 'done' | null {
+ switch (eventName) {
+ case 'SessionStart':
+ // Compaction restarts the session during an active turn.
+ return ['startup', 'resume', 'clear', 'new'].includes(String(payload.source)) ? 'done' : null
+ case 'UserPromptSubmit':
+ case 'PostToolUse':
+ case 'PostToolUseFailure':
+ return 'working'
+ case 'PreToolUse':
+ return isAskUserQuestionTool(readString(payload, 'tool_name')) ? 'waiting' : 'working'
+ case 'PermissionRequest':
+ return 'waiting'
+ case 'Stop':
+ case 'StopFailure':
+ case 'SessionEnd':
+ return 'done'
+ case 'PostCompact':
+ return payload.trigger === 'manual' ? 'done' : null
+ case 'Notification':
+ if (
+ payload.notification_type === 'permission_prompt' ||
+ payload.notification_type === 'elicitation_dialog'
+ ) {
+ return 'waiting'
+ }
+ return payload.notification_type === 'idle_prompt' ? 'done' : null
+ default:
+ return null
+ }
+}
+
+export function normalizeCompatibleLifecycleEvent(
+ source: 'qoder' | 'codebuddy',
+ state: HookListenerState,
+ eventName: unknown,
+ promptText: string,
+ paneKey: string,
+ hookPayload: Record
+): ParsedAgentStatusPayload | null {
+ if (shouldIgnoreCompactContinuationUserPromptSubmit(eventName, promptText)) {
+ return null
+ }
+ // CodeBuddy emits SessionStart after UserPromptSubmit, so it cannot settle a turn.
+ if (source === 'codebuddy' && eventName === 'SessionStart') {
+ return null
+ }
+ const stateName =
+ source === 'codebuddy' && eventName === 'PreToolUse'
+ ? 'working'
+ : readCompatibleState(eventName, hookPayload)
+ if (!stateName) {
+ return null
+ }
+ const resetOnNewTurn = isNewTurnEvent(source, eventName)
+ const toolFields = extractToolFields(source, eventName, hookPayload)
+ if (
+ source === 'codebuddy' &&
+ eventName === 'Notification' &&
+ hookPayload.notification_type === 'permission_prompt' &&
+ hookPayload.message === 'needs your permission to use AskUserQuestion'
+ ) {
+ toolFields.toolName = 'AskUserQuestion'
+ }
+ const snapshot = resolveToolState(state, paneKey, toolFields, { resetOnNewTurn })
+ return normalizeAgentStatusPayload({
+ state: stateName,
+ agentType: source,
+ prompt: resolvePrompt(
+ state,
+ paneKey,
+ source === 'codebuddy' && eventName === 'Notification' ? '' : promptText,
+ { resetOnNewTurn }
+ ),
+ toolName: snapshot.toolName,
+ toolInput: snapshot.toolInput,
+ interactivePrompt: snapshot.interactivePrompt,
+ lastAssistantMessage: snapshot.lastAssistantMessage,
+ lastAssistantMessageIsToolOutput: snapshot.lastAssistantMessageIsToolOutput,
+ sessionBoundary: eventName === 'SessionStart' || eventName === 'PostCompact' ? true : undefined,
+ interrupted: eventName === 'Stop' && hookPayload.is_interrupt === true ? true : undefined,
+ mainAgent:
+ eventName === 'StopFailure'
+ ? { state: 'done', outcome: 'failure', stateStartedAt: Date.now() }
+ : undefined
+ })
+}
diff --git a/src/shared/agent-hook-listener/providers/qoder-events.ts b/src/shared/agent-hook-listener/providers/qoder-events.ts
index bdf2a622dbe..c83923d58f7 100644
--- a/src/shared/agent-hook-listener/providers/qoder-events.ts
+++ b/src/shared/agent-hook-listener/providers/qoder-events.ts
@@ -1,51 +1,5 @@
-import { isAskUserQuestionTool } from '../../agent-question-answered-intent'
-import {
- normalizeAgentStatusPayload,
- type ParsedAgentStatusPayload
-} from '../../agent-status-types'
+import { normalizeCompatibleLifecycleEvent } from './compatible-lifecycle-events'
import type { HookListenerState } from '../listener-state'
-import {
- resolvePrompt,
- resolveToolState,
- shouldIgnoreCompactContinuationUserPromptSubmit
-} from '../prompt-fields'
-import { extractToolFields, isNewTurnEvent } from '../provider-event-routing'
-import { readString } from '../tool-input-preview'
-
-function readQoderState(
- eventName: unknown,
- payload: Record
-): 'working' | 'waiting' | 'done' | null {
- switch (eventName) {
- case 'SessionStart':
- // Compaction restarts the session during an active turn.
- return ['startup', 'resume', 'clear', 'new'].includes(String(payload.source)) ? 'done' : null
- case 'UserPromptSubmit':
- case 'PostToolUse':
- case 'PostToolUseFailure':
- return 'working'
- case 'PreToolUse':
- return isAskUserQuestionTool(readString(payload, 'tool_name')) ? 'waiting' : 'working'
- case 'PermissionRequest':
- return 'waiting'
- case 'Stop':
- case 'StopFailure':
- case 'SessionEnd':
- return 'done'
- case 'PostCompact':
- return payload.trigger === 'manual' ? 'done' : null
- case 'Notification':
- if (
- payload.notification_type === 'permission_prompt' ||
- payload.notification_type === 'elicitation_dialog'
- ) {
- return 'waiting'
- }
- return payload.notification_type === 'idle_prompt' ? 'done' : null
- default:
- return null
- }
-}
export function normalizeQoderEvent(
state: HookListenerState,
@@ -53,35 +7,13 @@ export function normalizeQoderEvent(
promptText: string,
paneKey: string,
hookPayload: Record
-): ParsedAgentStatusPayload | null {
- if (shouldIgnoreCompactContinuationUserPromptSubmit(eventName, promptText)) {
- return null
- }
- const stateName = readQoderState(eventName, hookPayload)
- if (!stateName) {
- return null
- }
- const resetOnNewTurn = isNewTurnEvent('qoder', eventName)
- const snapshot = resolveToolState(
+) {
+ return normalizeCompatibleLifecycleEvent(
+ 'qoder',
state,
+ eventName,
+ promptText,
paneKey,
- extractToolFields('qoder', eventName, hookPayload),
- { resetOnNewTurn }
+ hookPayload
)
- return normalizeAgentStatusPayload({
- state: stateName,
- agentType: 'qoder',
- prompt: resolvePrompt(state, paneKey, promptText, { resetOnNewTurn }),
- toolName: snapshot.toolName,
- toolInput: snapshot.toolInput,
- interactivePrompt: snapshot.interactivePrompt,
- lastAssistantMessage: snapshot.lastAssistantMessage,
- lastAssistantMessageIsToolOutput: snapshot.lastAssistantMessageIsToolOutput,
- sessionBoundary: eventName === 'SessionStart' || eventName === 'PostCompact' ? true : undefined,
- interrupted: eventName === 'Stop' && hookPayload.is_interrupt === true ? true : undefined,
- mainAgent:
- eventName === 'StopFailure'
- ? { state: 'done', outcome: 'failure', stateStartedAt: Date.now() }
- : undefined
- })
}
diff --git a/src/shared/agent-hook-listener/source-routing.ts b/src/shared/agent-hook-listener/source-routing.ts
index 2b7f0147e60..2940685665c 100644
--- a/src/shared/agent-hook-listener/source-routing.ts
+++ b/src/shared/agent-hook-listener/source-routing.ts
@@ -6,6 +6,7 @@ export const HOOK_SOURCE_BY_PATHNAME: Readonly>
'/hook/claude': 'claude',
'/hook/codex': 'codex',
'/hook/qoder': 'qoder',
+ '/hook/codebuddy': 'codebuddy',
'/hook/gemini': 'gemini',
'/hook/antigravity': 'antigravity',
'/hook/amp': 'amp',
diff --git a/src/shared/agent-hook-relay.ts b/src/shared/agent-hook-relay.ts
index 7e0d452a66b..bb2dca99f72 100644
--- a/src/shared/agent-hook-relay.ts
+++ b/src/shared/agent-hook-relay.ts
@@ -38,6 +38,7 @@ const AGENT_HOOK_SOURCES = [
'claude',
'codex',
'qoder',
+ 'codebuddy',
'gemini',
'antigravity',
'amp',
diff --git a/src/shared/agent-hook-types.ts b/src/shared/agent-hook-types.ts
index cc83527fc09..0bc5b67bc28 100644
--- a/src/shared/agent-hook-types.ts
+++ b/src/shared/agent-hook-types.ts
@@ -8,6 +8,7 @@ export const AGENT_HOOK_TARGETS = [
'openclaude',
'codex',
'qoder',
+ 'codebuddy',
'gemini',
'antigravity',
'amp',
diff --git a/src/shared/agent-icons/codebuddy.png b/src/shared/agent-icons/codebuddy.png
new file mode 100644
index 00000000000..ecaab1216f4
Binary files /dev/null and b/src/shared/agent-icons/codebuddy.png differ
diff --git a/src/shared/agent-kind.ts b/src/shared/agent-kind.ts
index 375bac1bd2e..3fb112e6114 100644
--- a/src/shared/agent-kind.ts
+++ b/src/shared/agent-kind.ts
@@ -16,6 +16,7 @@ type ConcreteAgentKind = Exclude
const TUI_AGENT_KIND_BY_AGENT = {
claude: 'claude-code',
'claude-agent-teams': 'claude-agent-teams',
+ codebuddy: 'codebuddy',
openclaude: 'openclaude',
codex: 'codex',
autohand: 'autohand',
diff --git a/src/shared/agent-node-package-entrypoints.ts b/src/shared/agent-node-package-entrypoints.ts
index 905d41f95f0..7f8f9abd7cb 100644
--- a/src/shared/agent-node-package-entrypoints.ts
+++ b/src/shared/agent-node-package-entrypoints.ts
@@ -6,6 +6,7 @@
// new agent's install path is a data edit, as in agent-node-entrypoint-identities.ts.
export const NODE_PACKAGE_SCRIPT_ENTRYPOINTS: Record = {
codex: ['node_modules/@openai/codex/'],
+ codebuddy: ['node_modules/@tencent-ai/codebuddy-code/'],
gemini: ['node_modules/@google/gemini-cli/'],
// Why: ZCode's npm bin is `dist/zcode.cjs`, so a package install runs as `node …zcode.cjs`
// and never shows `zcode` as the foreground name (a SEA build still matches by name).
diff --git a/src/shared/agent-session-option-catalog-codebuddy.ts b/src/shared/agent-session-option-catalog-codebuddy.ts
new file mode 100644
index 00000000000..d0a8afa04c5
--- /dev/null
+++ b/src/shared/agent-session-option-catalog-codebuddy.ts
@@ -0,0 +1,44 @@
+import { hasFlag } from './agent-cli-flag-detection'
+import { removeAgentArgOption } from './agent-session-option-agent-args'
+import type { AgentSessionOptionCatalog, CatalogOption } from './agent-session-option-catalog-types'
+
+const CODEBUDDY_EFFORT: CatalogOption = {
+ id: 'effort',
+ label: 'Reasoning effort',
+ category: 'thought_level',
+ kind: {
+ type: 'select',
+ choices: [
+ { value: 'minimal', label: 'Minimal' },
+ { value: 'low', label: 'Low' },
+ { value: 'medium', label: 'Medium' },
+ { value: 'high', label: 'High' },
+ { value: 'xhigh', label: 'Extra high' },
+ { value: 'max', label: 'Max' }
+ ],
+ defaultValue: 'medium'
+ },
+ apply: {
+ launchArgs: (value) => ['--effort', String(value)],
+ agentArgsOverride: (tokens) => hasFlag(tokens, ['--effort']),
+ removeAgentArgs: (tokens) => removeAgentArgOption(tokens, ['--effort'])
+ }
+}
+
+export const CODEBUDDY_SESSION_OPTION_CATALOG: AgentSessionOptionCatalog = {
+ supportsWorkerLaunchPreferences: true,
+ // Stable CLI aliases let CodeBuddy resolve the account's current model choices.
+ models: [
+ { id: 'default-model', label: 'Default' },
+ { id: 'fast-model', label: 'Fast' },
+ { id: 'balanced-model', label: 'Balanced' },
+ { id: 'primary-model', label: 'Primary' },
+ { id: 'deep-model', label: 'Deep' }
+ ].map((model) => ({ ...model, options: [CODEBUDDY_EFFORT] })),
+ modelApply: {
+ launchArgs: (value) => ['--model', String(value)],
+ agentArgsOverride: (tokens) => hasFlag(tokens, ['--model']),
+ removeAgentArgs: (tokens) => removeAgentArgOption(tokens, ['--model'])
+ },
+ unknownModelOptions: [CODEBUDDY_EFFORT]
+}
diff --git a/src/shared/agent-session-option-catalog.ts b/src/shared/agent-session-option-catalog.ts
index a8e26f5a0ed..74a7f384801 100644
--- a/src/shared/agent-session-option-catalog.ts
+++ b/src/shared/agent-session-option-catalog.ts
@@ -1,3 +1,4 @@
+import { CODEBUDDY_SESSION_OPTION_CATALOG } from './agent-session-option-catalog-codebuddy'
import type { AgentType } from './agent-status-types'
import { ANTIGRAVITY_SESSION_OPTION_CATALOG } from './agent-session-option-catalog-antigravity'
import {
@@ -34,6 +35,7 @@ export { createClaudeCatalogOptions }
const CATALOGS: AgentSessionOptionCatalogMap = {
antigravity: ANTIGRAVITY_SESSION_OPTION_CATALOG,
claude: CLAUDE_SESSION_OPTION_CATALOG,
+ codebuddy: CODEBUDDY_SESSION_OPTION_CATALOG,
codex: CODEX_SESSION_OPTION_CATALOG,
gemini: GEMINI_SESSION_OPTION_CATALOG,
cursor: CURSOR_SESSION_OPTION_CATALOG,
diff --git a/src/shared/agent-session-resume.test.ts b/src/shared/agent-session-resume.test.ts
index 577492ddefc..397a1a9451f 100644
--- a/src/shared/agent-session-resume.test.ts
+++ b/src/shared/agent-session-resume.test.ts
@@ -28,6 +28,10 @@ describe('agent session resume metadata', () => {
expect(isResumableTuiAgent('kimi')).toBe(true)
})
+ it('treats CodeBuddy as a resumable TUI agent', () => {
+ expect(isResumableTuiAgent('codebuddy')).toBe(true)
+ })
+
it.each([
['claude', { session_id: 'claude-session' }, { key: 'session_id', id: 'claude-session' }],
['codex', { session_id: 'codex-session' }, { key: 'session_id', id: 'codex-session' }],
@@ -70,6 +74,7 @@ describe('agent session resume metadata', () => {
it.each([
['claude', { key: 'session_id', id: 's1' }, ['claude', '--resume', 's1']],
+ ['codebuddy', { key: 'session_id', id: 's1' }, ['codebuddy', '--resume', 's1']],
['codex', { key: 'session_id', id: 's1' }, ['codex', 'resume', 's1']],
['gemini', { key: 'session_id', id: 's1' }, ['gemini', '--resume', 's1']],
['antigravity', { key: 'conversation_id', id: 's1' }, ['agy', '--conversation', 's1']],
diff --git a/src/shared/agent-session-resume.ts b/src/shared/agent-session-resume.ts
index 4cbc240c764..40a3699af3c 100644
--- a/src/shared/agent-session-resume.ts
+++ b/src/shared/agent-session-resume.ts
@@ -5,6 +5,7 @@ import type { TuiAgent } from './tui-agent'
export const RESUMABLE_TUI_AGENTS = [
'claude',
+ 'codebuddy',
'codex',
'qoder',
'gemini',
@@ -196,6 +197,7 @@ export function extractAgentProviderSession(
// since recent Claude Code names the transcript file with a UUID that differs
// from the hook session_id (so the id-based glob no longer finds it).
case 'qoder':
+ case 'codebuddy':
case 'claude':
case 'codex': {
const id = readSessionId(payload, ['session_id'])
@@ -277,6 +279,8 @@ export function getAgentResumeArgv(
): string[] | null {
const id = providerSession.id
switch (agent) {
+ case 'codebuddy':
+ return providerSession.key === 'session_id' ? ['codebuddy', '--resume', id] : null
case 'claude':
return providerSession.key === 'session_id' ? ['claude', '--resume', id] : null
case 'codex':
diff --git a/src/shared/agent-type-label.ts b/src/shared/agent-type-label.ts
index a4d70a76037..72d1b1b6d63 100644
--- a/src/shared/agent-type-label.ts
+++ b/src/shared/agent-type-label.ts
@@ -4,6 +4,7 @@ import type { AgentType } from './agent-status-types'
// (e.g. native chat's empty state on both surfaces) from one source of truth.
const WELL_KNOWN_LABELS: Record = {
claude: 'Claude',
+ codebuddy: 'CodeBuddy',
openclaude: 'OpenClaude',
codex: 'Codex',
qoder: 'Qoder CLI',
diff --git a/src/shared/ai-vault-resume-command.test.ts b/src/shared/ai-vault-resume-command.test.ts
index da8ced39e0e..db89cb41263 100644
--- a/src/shared/ai-vault-resume-command.test.ts
+++ b/src/shared/ai-vault-resume-command.test.ts
@@ -28,6 +28,17 @@ describe('buildAiVaultResumeCommand', () => {
).toBe('cmd /d /s /c "cd /d ""C:\\Users\\Ada Lovelace\\repo"" && codex resume ""session-1"""')
})
+ it('resumes CodeBuddy sessions with the Claude-compatible --resume flag', () => {
+ expect(
+ buildAiVaultResumeCommand({
+ agent: 'codebuddy',
+ sessionId: 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee',
+ cwd: '/repo/app',
+ platform: 'darwin'
+ })
+ ).toBe("cd '/repo/app' && codebuddy --resume 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee'")
+ })
+
it('builds a direct queued command for a live cmd shell', () => {
expect(
buildAiVaultResumeCommand({
diff --git a/src/shared/ai-vault-resume-command.ts b/src/shared/ai-vault-resume-command.ts
index cfff419c95c..efb2ddbe5d3 100644
--- a/src/shared/ai-vault-resume-command.ts
+++ b/src/shared/ai-vault-resume-command.ts
@@ -218,6 +218,7 @@ function buildAgentResumeInvocation(
return `${baseCommand} resume ${sessionArg}`
case 'cline':
return `${baseCommand} --id ${sessionArg}`
+ case 'codebuddy':
case 'claude':
case 'zcode':
case 'cursor':
diff --git a/src/shared/ai-vault-session-deletion.ts b/src/shared/ai-vault-session-deletion.ts
index d3c2211ab84..4875ab6e889 100644
--- a/src/shared/ai-vault-session-deletion.ts
+++ b/src/shared/ai-vault-session-deletion.ts
@@ -40,6 +40,7 @@ export const AI_VAULT_DELETABLE_AGENTS = [
'pi',
'omp',
'claude',
+ 'codebuddy',
'rovo',
'grok',
'cline'
diff --git a/src/shared/ai-vault-types.ts b/src/shared/ai-vault-types.ts
index 28f93dcfbe1..e337f98d7a0 100644
--- a/src/shared/ai-vault-types.ts
+++ b/src/shared/ai-vault-types.ts
@@ -3,6 +3,7 @@ import type { ExecutionHostId, ExecutionHostScope } from './execution-host'
export const AI_VAULT_AGENTS = [
'claude',
+ 'codebuddy',
'codex',
'hermes',
'pi',
@@ -51,6 +52,7 @@ export type AiVaultGroup = 'project' | 'folder' | 'agent'
export const AI_VAULT_AGENT_LABELS = {
claude: 'Claude',
+ codebuddy: 'CodeBuddy',
codex: 'Codex',
hermes: 'Hermes',
pi: 'Pi',
diff --git a/src/shared/codebuddy-agent.test.ts b/src/shared/codebuddy-agent.test.ts
new file mode 100644
index 00000000000..6915cf8d573
--- /dev/null
+++ b/src/shared/codebuddy-agent.test.ts
@@ -0,0 +1,67 @@
+import { readFileSync } from 'node:fs'
+import { join } from 'node:path'
+import { describe, expect, it } from 'vitest'
+import { createHookListenerState } from './agent-hook-listener/listener-state'
+import { normalizeAndAccept } from './agent-hook-listener-test-harness'
+import { recognizeAgentProcessFromCommandLine } from './agent-process-recognition'
+import { getAgentSessionOptionCatalog } from './agent-session-option-catalog'
+import { getAgentResumeArgv } from './agent-session-resume'
+
+describe('CodeBuddy harness', () => {
+ it('replays the live question lifecycle without settling at late SessionStart', () => {
+ const state = createHookListenerState()
+ const events = readFileSync(
+ join(__dirname, '__fixtures__/codebuddy-question-hooks.jsonl'),
+ 'utf8'
+ )
+ .trim()
+ .split('\n')
+ .map((line) => JSON.parse(line))
+ const rows = events.map((event) => normalizeAndAccept(state, 'codebuddy', event))
+ expect(rows.map((row) => row?.payload.state ?? null)).toEqual([
+ 'working',
+ null,
+ 'waiting',
+ 'working',
+ 'working',
+ 'done'
+ ])
+ expect(rows[2]?.payload).toMatchObject({ agentType: 'codebuddy', toolName: 'AskUserQuestion' })
+ expect(rows[5]?.payload).toMatchObject({ agentType: 'codebuddy', lastAssistantMessage: 'Red' })
+ expect(rows[5]?.providerSession?.id).toBe('codebuddy-verification')
+ })
+
+ it.each([
+ 'codebuddy',
+ 'cbc',
+ 'C:\\Tools\\codebuddy.cmd',
+ 'node /usr/lib/node_modules/@tencent-ai/codebuddy-code/dist/codebuddy.js'
+ ])('recognizes interactive %s', (command) => {
+ expect(recognizeAgentProcessFromCommandLine(command)?.agent).toBe('codebuddy')
+ })
+
+ it('excludes print runs and resumes the original provider session', () => {
+ for (const flag of ['--print', '--serve', '--acp', '--bg', '--background']) {
+ expect(recognizeAgentProcessFromCommandLine(`codebuddy ${flag}`)).toBeNull()
+ }
+ expect(recognizeAgentProcessFromCommandLine('codebuddy -- --serve')?.agent).toBe('codebuddy')
+ expect(getAgentResumeArgv('codebuddy', { key: 'session_id', id: 'session-1' })).toEqual([
+ 'codebuddy',
+ '--resume',
+ 'session-1'
+ ])
+ })
+
+ it('passes explicit model and effort preferences through the CLI flags', () => {
+ const catalog = getAgentSessionOptionCatalog('codebuddy')
+ expect(catalog?.modelApply.launchArgs?.('fast-model')).toEqual(['--model', 'fast-model'])
+ expect(catalog?.unknownModelOptions?.[0].apply.launchArgs?.('high')).toEqual([
+ '--effort',
+ 'high'
+ ])
+ expect(catalog?.modelApply.removeAgentArgs?.(['--model', 'old', '--', 'prompt'])).toEqual([
+ '--',
+ 'prompt'
+ ])
+ })
+})
diff --git a/src/shared/codebuddy-headless-command.ts b/src/shared/codebuddy-headless-command.ts
new file mode 100644
index 00000000000..5c632b27b3b
--- /dev/null
+++ b/src/shared/codebuddy-headless-command.ts
@@ -0,0 +1,13 @@
+import { agentArgOptionTokens } from './agent-session-option-agent-args'
+import { isPrintModeHeadlessOneShotCommand, optionName } from './print-mode-headless-command'
+
+const NON_INTERACTIVE_FLAGS = new Set(['--serve', '--acp', '--bg', '--background'])
+
+export function isCodebuddyNonInteractiveCommand(tokens: readonly string[]): boolean {
+ return (
+ isPrintModeHeadlessOneShotCommand(tokens) ||
+ agentArgOptionTokens(tokens)
+ .slice(1)
+ .some((token) => NON_INTERACTIVE_FLAGS.has(optionName(token)))
+ )
+}
diff --git a/src/shared/protocol-version.ts b/src/shared/protocol-version.ts
index bd8b3f62a31..3a877a80900 100644
--- a/src/shared/protocol-version.ts
+++ b/src/shared/protocol-version.ts
@@ -236,6 +236,8 @@ export const AGENT_SESSION_OPENCODE2_RESUME_RUNTIME_CAPABILITY =
'agent-session.opencode2-resume.v1' as const
export const AGENT_SESSION_MUSE_RESUME_RUNTIME_CAPABILITY = 'agent-session.muse-resume.v1' as const
export const AGENT_SESSION_DSH_RESUME_RUNTIME_CAPABILITY = 'agent-session.dsh-resume.v1' as const
+export const AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY =
+ 'agent-session.codebuddy-resume.v1' as const
export const AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY =
'agent-session.qoder-resume.v1' as const
export const AGENT_SESSION_ZCODE_RESUME_RUNTIME_CAPABILITY =
@@ -402,6 +404,7 @@ export const RUNTIME_CAPABILITIES = [
AGENT_SESSION_MUSE_RESUME_RUNTIME_CAPABILITY,
AGENT_SESSION_DSH_RESUME_RUNTIME_CAPABILITY,
AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY,
+ AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY,
AGENT_SESSION_ZCODE_RESUME_RUNTIME_CAPABILITY,
FILE_MUTATION_OWNERSHIP_RUNTIME_CAPABILITY,
GITHUB_MARK_PR_READY_RUNTIME_CAPABILITY,
diff --git a/src/shared/skills-cli-agent-keys.ts b/src/shared/skills-cli-agent-keys.ts
index f8d1a913172..1955bfa6d58 100644
--- a/src/shared/skills-cli-agent-keys.ts
+++ b/src/shared/skills-cli-agent-keys.ts
@@ -14,6 +14,7 @@ import type { TuiAgent } from './tui-agent'
export const SKILLS_CLI_AGENT_KEY_BY_TUI_AGENT = {
claude: 'claude-code',
'claude-agent-teams': 'claude-code',
+ codebuddy: 'codebuddy',
// Why: Orca states OpenClaude reads Claude-owned roots (native-chat-agent-profiles).
openclaude: 'claude-code',
codex: 'codex',
diff --git a/src/shared/telemetry-property-schemas.ts b/src/shared/telemetry-property-schemas.ts
index cee76b677d6..89f62138407 100644
--- a/src/shared/telemetry-property-schemas.ts
+++ b/src/shared/telemetry-property-schemas.ts
@@ -10,6 +10,7 @@ import type { GlobalSettings } from './global-settings-types'
export const AGENT_KIND_VALUES = [
'claude-code',
'claude-agent-teams',
+ 'codebuddy',
'openclaude',
'codex',
'autohand',
diff --git a/src/shared/tui-agent-config.ts b/src/shared/tui-agent-config.ts
index 15318913968..27cda1d1979 100644
--- a/src/shared/tui-agent-config.ts
+++ b/src/shared/tui-agent-config.ts
@@ -97,6 +97,11 @@ const TUI_AGENT_CONFIG_SOURCE: Record = {
promptInjectionMode: 'stdin-after-start',
pasteNeedsTypedRequest: true
},
+ codebuddy: {
+ detectCmd: 'codebuddy',
+ detectCmdAliases: ['cbc'],
+ promptInjectionMode: 'argv'
+ },
openclaude: {
detectCmd: 'openclaude',
promptInjectionMode: 'argv',
@@ -148,7 +153,6 @@ const TUI_AGENT_CONFIG_SOURCE: Record = {
detectCmd: 'opencode2',
// The private server inherits this pane's hook endpoint and identity.
launchCmd: 'opencode2 --standalone',
- expectedProcess: 'opencode2',
promptInjectionMode: 'flag-prompt',
draftPasteReadySignal: 'render-cursor-after-bracketed-paste',
draftPasteReadyTimeoutMs: 20_000
diff --git a/src/shared/tui-agent-display-names.ts b/src/shared/tui-agent-display-names.ts
index a90daa3242f..31b6035a953 100644
--- a/src/shared/tui-agent-display-names.ts
+++ b/src/shared/tui-agent-display-names.ts
@@ -8,6 +8,7 @@ import type { TuiAgent } from './tui-agent'
export const TUI_AGENT_DISPLAY_NAMES: Record = {
claude: 'Claude',
'claude-agent-teams': 'Claude Agent Teams',
+ codebuddy: 'CodeBuddy',
openclaude: 'OpenClaude',
codex: 'Codex',
devin: 'Devin',
diff --git a/src/shared/tui-agent-permissions.ts b/src/shared/tui-agent-permissions.ts
index 3a9912bf066..9fc8eb0a691 100644
--- a/src/shared/tui-agent-permissions.ts
+++ b/src/shared/tui-agent-permissions.ts
@@ -5,6 +5,7 @@ export type AgentPermissionMode = 'yolo' | 'manual' | 'mixed'
export const YOLO_TUI_AGENT_ARGS: Partial> = {
claude: '--dangerously-skip-permissions',
+ codebuddy: '--dangerously-skip-permissions',
'claude-agent-teams': '--dangerously-skip-permissions',
openclaude: '--dangerously-skip-permissions',
codex: '--dangerously-bypass-approvals-and-sandbox',
diff --git a/src/shared/tui-agent-rest-signal.test.ts b/src/shared/tui-agent-rest-signal.test.ts
index f7d11032875..6e573acc0b0 100644
--- a/src/shared/tui-agent-rest-signal.test.ts
+++ b/src/shared/tui-agent-rest-signal.test.ts
@@ -29,6 +29,7 @@ const EXPECTED_REST_SIGNALS: Record = {
grok: 'title',
muse: 'ready-body',
qoder: 'ready-body',
+ codebuddy: 'none',
autohand: 'none',
ante: 'none',
trae: 'none',
diff --git a/src/shared/tui-agent-selection.ts b/src/shared/tui-agent-selection.ts
index 85567dcb334..acf305dad59 100644
--- a/src/shared/tui-agent-selection.ts
+++ b/src/shared/tui-agent-selection.ts
@@ -45,7 +45,8 @@ export const TUI_AGENT_AUTO_PICK_ORDER = [
'rovo',
'hermes',
'devin',
- 'openclaw'
+ 'openclaw',
+ 'codebuddy'
] as const satisfies readonly TuiAgent[]
// Why: fresh installs should expose Claude Agent Teams in agent pickers; the
diff --git a/src/shared/tui-agent.ts b/src/shared/tui-agent.ts
index 19722220888..a98c45101fd 100644
--- a/src/shared/tui-agent.ts
+++ b/src/shared/tui-agent.ts
@@ -3,6 +3,7 @@
export type TuiAgent =
| 'claude' // Claude Code
| 'claude-agent-teams' // Claude Code Agent Teams via Orca native panes
+ | 'codebuddy' // CodeBuddy Code
| 'openclaude' // OpenClaude
| 'codex' // OpenAI Codex
| 'autohand' // Autohand Code CLI