diff --git a/README.md b/README.md
index 0261dbf7..ba470780 100644
--- a/README.md
+++ b/README.md
@@ -50,7 +50,7 @@ Native builds for macOS, Windows, and Linux on [**Releases**](https://github.com
| | |
|---|---|
-| **Agent-aware** | per-pane detection (22 CLIs) · status dot · notifications · branch + diff · tray icon when input is needed · resume after reboot · tab sidebar grouped by repository |
+| **Agent-aware** | per-pane detection (23 CLIs) · status dot · notifications · branch + diff · tray icon when input is needed · resume after reboot · tab sidebar grouped by repository |
| **CLI + Skills** | bundled `tty7` CLI · [agent skill](skills/tty7/SKILL.md) · `run` streams a command and exits with its code · `split` · `send` · `wait --until free` · `capture` |
| **Editor-grade input** | ghost suggestions from history · explained tab completion · syntax highlighting · multi-line editing · click places the caret · ⌃ R fuzzy history |
| **Window** | tabs & splits · ⌘ P palette · ⌘ F scrollback search · ⌘ J panel with process tree and listening ports · 13 themes, your own YAML, iTerm2 import · IME |
@@ -67,7 +67,7 @@ after a reboot. **Fork** needs both — the agent's own fork command, and the ho
that tells tty7 which session to fork.
-The full support matrix, all twenty-two
+The full support matrix, all twenty-three
| Agent | Detected | Status · resume | Fork |
|---|:-:|:-:|:-:|
@@ -81,6 +81,7 @@ that tells tty7 which session to fork.
| **Qwen Code** | ✓ | ✓ | ✓ |
| **Goose** | ✓ | ✓ | ✓ |
| **Qoder CLI** | ✓ | ✓ | ✓ |
+| **CodeBuddy** | ✓ | ✓ | ✓ |
| **Gemini** | ✓ | ✓ | |
| **Copilot** | ✓ | ✓ | |
| **Kimi Code** | ✓ | ✓ | |
diff --git a/assets/icons/agents/codebuddy.svg b/assets/icons/agents/codebuddy.svg
new file mode 100644
index 00000000..c07cb1be
--- /dev/null
+++ b/assets/icons/agents/codebuddy.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/crates/tty7-core/src/core/agent_hooks.rs b/crates/tty7-core/src/core/agent_hooks.rs
index 4720232c..fd77c5d8 100644
--- a/crates/tty7-core/src/core/agent_hooks.rs
+++ b/crates/tty7-core/src/core/agent_hooks.rs
@@ -43,9 +43,9 @@ fn effective_agent(agent: &str, ran_by_grok: bool) -> &str {
}
fn effective_event<'a>(agent: &str, event: &'a str, stdin_json: &str) -> Option<&'a str> {
- // Qoder also emits SessionStart after compacting the active turn.
- // Preserve its status until a real turn or session boundary arrives.
- if agent == "qodercli"
+ // Qoder and CodeBuddy also emit SessionStart after compacting the active
+ // turn. Preserve its status until a real turn or session boundary arrives.
+ if matches!(agent, "qodercli" | "codebuddy")
&& event == "session-start"
&& let Ok(payload) = serde_json::from_str::(stdin_json)
&& payload.get("source").and_then(|value| value.as_str()) == Some("compact")
@@ -268,10 +268,11 @@ pub enum HookAgent {
Kimi,
QoderCLI,
Crush,
+ CodeBuddy,
}
impl HookAgent {
- pub const ALL: [HookAgent; 15] = [
+ pub const ALL: [HookAgent; 16] = [
HookAgent::Claude,
HookAgent::Codex,
HookAgent::TraeCode,
@@ -287,6 +288,7 @@ impl HookAgent {
HookAgent::Kimi,
HookAgent::QoderCLI,
HookAgent::Crush,
+ HookAgent::CodeBuddy,
];
/// The hooks behind a detected agent process, if it has any.
@@ -311,6 +313,7 @@ impl HookAgent {
CLIAgent::Kimi => Some(HookAgent::Kimi),
CLIAgent::QoderCLI => Some(HookAgent::QoderCLI),
CLIAgent::Crush => Some(HookAgent::Crush),
+ CLIAgent::CodeBuddy => Some(HookAgent::CodeBuddy),
CLIAgent::Aider
| CLIAgent::Amp
| CLIAgent::Cursor
@@ -334,6 +337,7 @@ impl HookAgent {
HookAgent::Qwen => Some(QWEN_HOOK_EVENTS),
HookAgent::QoderCLI => Some(QODER_HOOK_EVENTS),
HookAgent::Crush => Some(CRUSH_HOOK_EVENTS),
+ HookAgent::CodeBuddy => Some(CODEBUDDY_HOOK_EVENTS),
HookAgent::Copilot
| HookAgent::OpenCode
| HookAgent::Pi
@@ -379,6 +383,7 @@ impl HookAgent {
HookAgent::Kimi => "kimi",
HookAgent::QoderCLI => "qodercli",
HookAgent::Crush => "crush",
+ HookAgent::CodeBuddy => "codebuddy",
}
}
@@ -399,6 +404,7 @@ impl HookAgent {
HookAgent::Kimi => "Kimi Code",
HookAgent::QoderCLI => "Qoder CLI",
HookAgent::Crush => "Crush",
+ HookAgent::CodeBuddy => "CodeBuddy",
}
}
@@ -433,6 +439,7 @@ impl HookAgent {
HookAgent::Kimi => target.kimi_config_path(),
HookAgent::QoderCLI => target.qoder_settings_path(),
HookAgent::Crush => target.crush_settings_path(),
+ HookAgent::CodeBuddy => target.codebuddy_settings_path(),
}
}
@@ -548,6 +555,19 @@ impl<'a> HookTarget<'a> {
self.under(&self.xdg_config_dir(), &["crush", "crush.json"])
}
+ /// CodeBuddy moves its whole home, `settings.json` included, to
+ /// `CODEBUDDY_CONFIG_DIR` when that is set and not blank. Local-only, like
+ /// the other overrides.
+ fn codebuddy_settings_path(&self) -> PathBuf {
+ if self.is_local()
+ && let Some(dir) = std::env::var_os("CODEBUDDY_CONFIG_DIR")
+ .filter(|d| !d.to_string_lossy().trim().is_empty())
+ {
+ return PathBuf::from(dir).join("settings.json");
+ }
+ self.under_home(&[".codebuddy", "settings.json"])
+ }
+
fn traecli_hooks_path(&self) -> PathBuf {
if self.is_local() {
if let Some(dir) = std::env::var_os("TRAECLI_HOME").filter(|d| !d.is_empty()) {
@@ -860,6 +880,23 @@ const QODER_HOOK_EVENTS: &[(&str, &str)] = &[
("SessionEnd", "session-end"),
];
+/// CodeBuddy's hooks are Claude Code's, file layout and event names alike, but
+/// like Qoder it has a first-class `PermissionRequest`, so it takes Qoder's
+/// table rather than Claude's `Notification` sniffing. A turn that dies on an
+/// API error reports `StopFailure` instead of `Stop`; without it the pane
+/// would stay on working.
+const CODEBUDDY_HOOK_EVENTS: &[(&str, &str)] = &[
+ ("SessionStart", "session-start"),
+ ("UserPromptSubmit", "prompt-submit"),
+ ("PermissionRequest", "permission-request"),
+ // An authorized MCP tool can still pause for user input mid-call.
+ ("Elicitation", "question-asked"),
+ ("PostToolUse", "tool-complete"),
+ ("Stop", "stop"),
+ ("StopFailure", "stop"),
+ ("SessionEnd", "session-end"),
+];
+
/// Crush currently fires exactly one hook, `PreToolUse`, before every
/// top-level tool call and before its permission check. Its payload still
/// carries `session_id` and `cwd`, which is what resume needs.
@@ -1235,6 +1272,7 @@ fn owned_file_content(target: &HookTarget, agent: HookAgent) -> Option {
| HookAgent::Qwen
| HookAgent::QoderCLI
| HookAgent::Crush
+ | HookAgent::CodeBuddy
| HookAgent::Kimi => None,
}
}
@@ -1692,6 +1730,7 @@ mod tests {
.chain(GOOSE_HOOK_EVENTS)
.chain(KIMI_HOOK_EVENTS)
.chain(CRUSH_HOOK_EVENTS)
+ .chain(CODEBUDDY_HOOK_EVENTS)
.map(|(_, e)| *e)
.chain(GROK_HOOK_EVENTS.iter().map(|(_, e, _)| *e))
.collect();
@@ -1728,6 +1767,7 @@ mod tests {
(HookAgent::Kimi, "/home/me/.kimi-code/config.toml"),
(HookAgent::QoderCLI, "/home/me/.qoder/settings.json"),
(HookAgent::Crush, "/home/me/.config/crush/crush.json"),
+ (HookAgent::CodeBuddy, "/home/me/.codebuddy/settings.json"),
] {
assert_eq!(
agent.target_path(&t),
@@ -1750,6 +1790,7 @@ mod tests {
HookAgent::Kimi,
HookAgent::QoderCLI,
HookAgent::Crush,
+ HookAgent::CodeBuddy,
] {
assert_eq!(hooks_state(&real, agent), HooksState::NotInstalled);
install_hooks(&real, agent).unwrap_or_else(|e| panic!("{}: {e}", agent.slug()));
@@ -1879,6 +1920,71 @@ mod tests {
assert_eq!(state.status, AgentStatus::Done);
}
+ /// CodeBuddy reuses Claude Code's hook shape and payload, so a turn walks
+ /// the same way — but it fires `SessionStart` with `source: "compact"`
+ /// in the middle of a turn, which must not reset the pane to idle.
+ #[test]
+ fn codebuddy_turns_survive_compaction_and_record_the_session() {
+ use crate::core::cli_agent::{AgentSessionState, AgentStatus, CLIAgent};
+
+ let apply_hook = |state: &mut AgentSessionState, hook: &str, input: &str| {
+ let event = HookAgent::CodeBuddy
+ .hook_map_events()
+ .unwrap()
+ .iter()
+ .find_map(|(name, event)| (*name == hook).then_some(*event))
+ .unwrap_or_else(|| panic!("CodeBuddy installs no {hook} hook"));
+ if let Some(event) = effective_event("codebuddy", event, input) {
+ let ev = round_trip("codebuddy", event, input);
+ assert_eq!(ev.agent, Some(CLIAgent::CodeBuddy));
+ state.apply_event(&ev);
+ }
+ };
+ let mut state = AgentSessionState::default();
+ apply_hook(
+ &mut state,
+ "SessionStart",
+ r#"{"session_id":"cb-1","cwd":"/repo","source":"startup"}"#,
+ );
+ assert_eq!(state.status, AgentStatus::Idle);
+ apply_hook(
+ &mut state,
+ "UserPromptSubmit",
+ r#"{"session_id":"cb-1","cwd":"/repo","prompt":"Fix the build"}"#,
+ );
+ assert_eq!(state.status, AgentStatus::Working);
+
+ let before = state.clone();
+ apply_hook(
+ &mut state,
+ "SessionStart",
+ r#"{"session_id":"cb-1","cwd":"/repo","source":"compact"}"#,
+ );
+ assert_eq!(state, before, "compaction must preserve the active turn");
+
+ apply_hook(
+ &mut state,
+ "PermissionRequest",
+ r#"{"session_id":"cb-1","tool_name":"Bash"}"#,
+ );
+ assert_eq!(state.status, AgentStatus::Waiting);
+ apply_hook(&mut state, "PostToolUse", r#"{"session_id":"cb-1"}"#);
+ assert_eq!(state.status, AgentStatus::Working);
+ apply_hook(&mut state, "StopFailure", r#"{"session_id":"cb-1"}"#);
+ assert_eq!(state.status, AgentStatus::Done, "a failed turn still ends");
+ assert_eq!(state.session_id.as_deref(), Some("cb-1"));
+ assert_eq!(state.cwd.as_deref(), Some(Path::new("/repo")));
+
+ assert!(
+ !HookAgent::CodeBuddy
+ .hook_map_events()
+ .unwrap()
+ .iter()
+ .any(|(hook, _)| *hook == "Notification"),
+ "PermissionRequest reports the block; Notification would only muddy it"
+ );
+ }
+
/// Crush's lone `PreToolUse` has no `Stop` to close a turn behind it, so it
/// must not open one: the pane would read as busy until Crush exits.
#[test]
@@ -2170,6 +2276,7 @@ mod tests {
(HookAgent::Kimi, "/home/me/.kimi-code/config.toml"),
(HookAgent::QoderCLI, "/home/me/.qoder/settings.json"),
(HookAgent::Crush, "/home/me/.config/crush/crush.json"),
+ (HookAgent::CodeBuddy, "/home/me/.codebuddy/settings.json"),
] {
assert_eq!(
agent.target_path(&target),
@@ -2499,6 +2606,105 @@ mod tests {
}
}
+ #[test]
+ fn codebuddy_config_dir_controls_local_hook_lifecycle() {
+ const CASE_ENV: &str = "TTY7_TEST_CODEBUDDY_CONFIG_CASE";
+ const ROOT_ENV: &str = "TTY7_TEST_CODEBUDDY_CONFIG_ROOT";
+ let Ok(case) = std::env::var(CASE_ENV) else {
+ // Each case gets its own environment, without changing the one
+ // shared by the other tests or touching the user's settings.
+ for case in ["override", "blank", "unset"] {
+ let sandbox = tempfile::tempdir().unwrap();
+ let mut child = std::process::Command::new(std::env::current_exe().unwrap());
+ child
+ .args([
+ "--exact",
+ "core::agent_hooks::tests::codebuddy_config_dir_controls_local_hook_lifecycle",
+ "--nocapture",
+ ])
+ .env(CASE_ENV, case)
+ .env(ROOT_ENV, sandbox.path());
+ match case {
+ "override" => {
+ child.env("CODEBUDDY_CONFIG_DIR", sandbox.path().join("custom config"))
+ }
+ // CodeBuddy trims the value, so whitespace is as unset as empty.
+ "blank" => child.env("CODEBUDDY_CONFIG_DIR", " "),
+ _ => child.env_remove("CODEBUDDY_CONFIG_DIR"),
+ };
+ let output = crate::core::proc::output_within(
+ crate::core::proc::hide_console(&mut child),
+ std::time::Duration::from_secs(30),
+ )
+ .expect("run the isolated CodeBuddy hook test");
+ assert!(
+ output.status.success(),
+ "{case}:\n{}\n{}",
+ String::from_utf8_lossy(&output.stdout),
+ String::from_utf8_lossy(&output.stderr),
+ );
+ }
+ return;
+ };
+
+ let root = PathBuf::from(std::env::var_os(ROOT_ENV).unwrap());
+ let host = local_host();
+ let target = HookTarget {
+ host: &*host,
+ home: root.join("home"),
+ exe: std::env::current_exe().unwrap(),
+ };
+ let default_settings = target.home.join(".codebuddy").join("settings.json");
+ let custom_settings = root.join("custom config").join("settings.json");
+ let (settings, untouched) = if case == "override" {
+ (&custom_settings, &default_settings)
+ } else {
+ (&default_settings, &custom_settings)
+ };
+ let user_config = serde_json::json!({
+ "model": "codebuddy-test",
+ "hooks": {
+ "Stop": [{ "hooks": [{ "type": "command", "command": "echo user-hook" }] }]
+ }
+ });
+ for path in [settings, untouched] {
+ std::fs::create_dir_all(path.parent().unwrap()).unwrap();
+ std::fs::write(path, user_config.to_string()).unwrap();
+ }
+
+ let agent = HookAgent::CodeBuddy;
+ assert_eq!(agent.target_path(&target), *settings);
+ let remote_host = FakeRemote::shared();
+ let remote = HookTarget::remote(&*remote_host, PathBuf::from("/home/me"));
+ assert_eq!(
+ agent.target_path(&remote),
+ PathBuf::from("/home/me/.codebuddy/settings.json"),
+ "a local override must not redirect remote hooks"
+ );
+
+ assert_eq!(hooks_state(&target, agent), HooksState::NotInstalled);
+ assert_eq!(
+ install_hooks(&target, agent).unwrap(),
+ HookOutcome::Installed
+ );
+ assert_eq!(hooks_state(&target, agent), HooksState::Installed);
+ assert!(
+ std::fs::read_to_string(settings)
+ .unwrap()
+ .contains("agent-hook codebuddy")
+ );
+ assert_eq!(
+ uninstall_hooks(&target, agent).unwrap(),
+ HookOutcome::Removed
+ );
+ assert_eq!(hooks_state(&target, agent), HooksState::NotInstalled);
+ for path in [settings, untouched] {
+ let actual: serde_json::Value =
+ serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
+ assert_eq!(actual, user_config, "{}", path.display());
+ }
+ }
+
/// Crush's hook map is one list under `hooks.PreToolUse` like Claude's, but
/// each entry carries `command` and `matcher` directly instead of wrapping
/// them in a nested `hooks` array. The merge has to write the flat shape and
diff --git a/crates/tty7-core/src/core/cli_agent.rs b/crates/tty7-core/src/core/cli_agent.rs
index c4340f67..eb7d5865 100644
--- a/crates/tty7-core/src/core/cli_agent.rs
+++ b/crates/tty7-core/src/core/cli_agent.rs
@@ -28,10 +28,11 @@ pub enum CLIAgent {
TraeCode,
QoderCLI,
Crush,
+ CodeBuddy,
}
impl CLIAgent {
- pub const ALL: [CLIAgent; 22] = [
+ pub const ALL: [CLIAgent; 23] = [
CLIAgent::Claude,
CLIAgent::Codex,
CLIAgent::TraeCode,
@@ -54,6 +55,7 @@ impl CLIAgent {
CLIAgent::Kimi,
CLIAgent::QoderCLI,
CLIAgent::Crush,
+ CLIAgent::CodeBuddy,
];
fn aliases(self) -> &'static [&'static str] {
@@ -99,6 +101,11 @@ impl CLIAgent {
// Charm's terminal agent. One binary, and the name on `PATH` is
// the one it starts as.
CLIAgent::Crush => &["crush"],
+ // Tencent's terminal agent. The npm package installs `codebuddy`,
+ // `codebuddy-code` and the short `cbc`, all pointing at one script.
+ // `cbc` also names the COIN-OR solver; a solver run wearing the
+ // avatar until it exits is the cost of catching the short name.
+ CLIAgent::CodeBuddy => &["codebuddy", "codebuddy-code", "cbc"],
}
}
@@ -126,6 +133,7 @@ impl CLIAgent {
CLIAgent::Kimi => "kimi",
CLIAgent::QoderCLI => "qodercli",
CLIAgent::Crush => "crush",
+ CLIAgent::CodeBuddy => "codebuddy",
}
}
@@ -158,6 +166,7 @@ impl CLIAgent {
CLIAgent::Kimi => "Kimi Code",
CLIAgent::QoderCLI => "Qoder CLI",
CLIAgent::Crush => "Crush",
+ CLIAgent::CodeBuddy => "CodeBuddy",
}
}
@@ -194,6 +203,7 @@ impl CLIAgent {
CLIAgent::OhMyPi => Some(format!("omp{flags} --resume {session_id}")),
CLIAgent::Kimi => Some(format!("kimi{flags} --session {session_id}")),
CLIAgent::Crush => Some(format!("crush{flags} --session {session_id}")),
+ CLIAgent::CodeBuddy => Some(format!("codebuddy{flags} --resume {session_id}")),
_ => None,
}
}
@@ -209,7 +219,7 @@ impl CLIAgent {
CLIAgent::Qwen => &["--no-chat-recording"],
// Print mode still emits a session id in hooks when persistence
// is disabled, but there is no saved conversation to reopen.
- CLIAgent::QoderCLI => &["--no-session-persistence"],
+ CLIAgent::QoderCLI | CLIAgent::CodeBuddy => &["--no-session-persistence"],
_ => &[],
};
argv.iter().any(|t| ephemeral.contains(&t.as_str()))
@@ -230,6 +240,9 @@ impl CLIAgent {
CLIAgent::QoderCLI => Some(format!(
"qodercli{flags} --resume {session_id} --fork-session"
)),
+ CLIAgent::CodeBuddy => Some(format!(
+ "codebuddy{flags} --resume {session_id} --fork-session"
+ )),
CLIAgent::OpenCode => Some(format!("opencode{flags} --session {session_id} --fork")),
CLIAgent::OhMyPi => Some(format!("omp{flags} --fork {session_id}")),
// Droid forks with a standalone flag rather than resume-plus-a-switch.
@@ -258,7 +271,8 @@ impl CLIAgent {
| CLIAgent::Amp
| CLIAgent::Qwen
| CLIAgent::Goose
- | CLIAgent::QoderCLI => Some("Fork Session"),
+ | CLIAgent::QoderCLI
+ | CLIAgent::CodeBuddy => Some("Fork Session"),
_ => None,
}
}
@@ -442,6 +456,22 @@ impl CLIAgent {
// `--session {id}` this command appends. `-c` is *not* in that
// group here — Crush spells `--cwd` with it, and it must survive.
CLIAgent::Crush => &["--session", "-s", "--continue", "-C"],
+ // CodeBuddy keeps Claude Code's session flags. `--resume-session-at`
+ // cuts a resumed conversation short and only makes sense next to
+ // the resume it came with. Unlike Qoder, `-w` *is* `--worktree`
+ // here, and replaying either would branch off a fresh tree.
+ CLIAgent::CodeBuddy => &[
+ "--resume",
+ "-r",
+ "--continue",
+ "-c",
+ "--session-id",
+ "--fork-session",
+ "--resume-session-at",
+ "--worktree",
+ "-w",
+ "--worktree-branch",
+ ],
_ => &[],
};
let mut i = 0;
@@ -509,6 +539,8 @@ impl CLIAgent {
CLIAgent::QoderCLI => 0xFFFFFF,
// The blue-violet field Charm ships the Crush heart on.
CLIAgent::Crush => 0x6B50FF,
+ // The near-black field CodeBuddy's own app icon sits on.
+ CLIAgent::CodeBuddy => 0x1F1F1F,
}
}
@@ -551,6 +583,7 @@ impl CLIAgent {
CLIAgent::Kimi => "icons/agents/kimi.svg",
CLIAgent::QoderCLI => "icons/agents/qodercli.svg",
CLIAgent::Crush => "icons/agents/crush.svg",
+ CLIAgent::CodeBuddy => "icons/agents/codebuddy.svg",
CLIAgent::Aider
| CLIAgent::Auggie
| CLIAgent::Hermes
@@ -938,6 +971,35 @@ mod tests {
}
}
+ /// All three of CodeBuddy's npm bins are node scripts pointing at one file,
+ /// so the pty sees node plus whichever shim name was typed.
+ #[test]
+ fn codebuddy_is_detected_through_each_of_its_binaries() {
+ for launcher in [
+ "codebuddy",
+ "codebuddy-code",
+ "cbc",
+ "/opt/homebrew/bin/codebuddy",
+ "/usr/local/bin/cbc",
+ ] {
+ assert_eq!(
+ CLIAgent::detect_from_argv(&argv(&["node", launcher])),
+ Some(CLIAgent::CodeBuddy),
+ "on {launcher}"
+ );
+ assert_eq!(
+ CLIAgent::detect_from_argv(&argv(&[launcher])),
+ Some(CLIAgent::CodeBuddy),
+ "on {launcher}"
+ );
+ }
+ assert_eq!(
+ CLIAgent::from_slug("codebuddy"),
+ Some(CLIAgent::CodeBuddy),
+ "hook events name the agent by this slug"
+ );
+ }
+
#[test]
fn detects_npx_package_form() {
assert_eq!(
@@ -1750,6 +1812,62 @@ mod tests {
);
}
+ #[test]
+ fn codebuddy_resume_and_fork_drop_stale_session_and_worktree_flags() {
+ assert_eq!(
+ CLIAgent::CodeBuddy.resume_command("cb-1", None).as_deref(),
+ Some("codebuddy --resume cb-1")
+ );
+ assert_eq!(
+ CLIAgent::CodeBuddy.fork_command("cb-1", None).as_deref(),
+ Some("codebuddy --resume cb-1 --fork-session")
+ );
+ for stale in [
+ vec!["--resume", "cb-0"],
+ vec!["-r", "cb-0"],
+ vec!["--continue"],
+ vec!["-c"],
+ vec!["--session-id", "cb-0"],
+ vec!["--resume", "cb-0", "--resume-session-at", "m-9"],
+ vec!["--worktree"],
+ vec!["--worktree", "old-tree", "--worktree-branch", "main"],
+ vec!["-w", "old-tree"],
+ vec!["--worktree=old-tree"],
+ vec!["--resume", "cb-0", "--fork-session"],
+ ] {
+ let mut launch = argv(&["codebuddy", "--model", "m-1"]);
+ launch.extend(argv(&stale));
+ launch.extend(argv(&["--permission-mode", "acceptEdits"]));
+ assert_eq!(
+ CLIAgent::CodeBuddy
+ .resume_command("cb-2", Some(&launch))
+ .as_deref(),
+ Some("codebuddy --model m-1 --permission-mode acceptEdits --resume cb-2"),
+ "launch argv: {launch:?}"
+ );
+ assert_eq!(
+ CLIAgent::CodeBuddy
+ .fork_command("cb-2", Some(&launch))
+ .as_deref(),
+ Some(
+ "codebuddy --model m-1 --permission-mode acceptEdits --resume cb-2 --fork-session"
+ ),
+ "launch argv: {launch:?}"
+ );
+ }
+
+ let ephemeral = argv(&["codebuddy", "-p", "--no-session-persistence"]);
+ assert_eq!(
+ CLIAgent::CodeBuddy.resume_command("cb-1", Some(&ephemeral)),
+ None,
+ "nothing was saved, so there is nothing to reopen"
+ );
+ assert_eq!(
+ CLIAgent::CodeBuddy.fork_command("cb-1", Some(&ephemeral)),
+ None
+ );
+ }
+
#[test]
fn oh_my_pi_resume_and_fork_use_its_own_flags() {
let argv = |parts: &[&str]| parts.iter().map(|s| s.to_string()).collect::>();
diff --git a/docs/agents/overview.mdx b/docs/agents/overview.mdx
index 94fd0c10..affe5b2d 100644
--- a/docs/agents/overview.mdx
+++ b/docs/agents/overview.mdx
@@ -1,6 +1,6 @@
---
title: "Coding agents"
-description: "What tty7 does around Claude Code, Codex, TraeCode, and 19 others — without ever wrapping them."
+description: "What tty7 does around Claude Code, Codex, TraeCode, and 20 others — without ever wrapping them."
---
tty7 recognises coding agents running in a pane and builds around them. It does
@@ -15,7 +15,7 @@ need, and what changed.
## Which agents
-Twenty-two CLIs are recognised on sight, by the command running in the pane:
+Twenty-three CLIs are recognised on sight, by the command running in the pane:
| Agent | Command |
|---|---|
@@ -35,6 +35,7 @@ Twenty-two CLIs are recognised on sight, by the command running in the pane:
| Kimi Code | `kimi`, `kimi-code` |
| Qoder CLI | `qoder`, `qodercli` |
| Crush | `crush` |
+| CodeBuddy | `codebuddy`, `codebuddy-code`, `cbc` |
| Auggie | `auggie` |
| Hermes | `hermes` |
| Vibe | `vibe`, `vibe-acp` |
@@ -63,7 +64,7 @@ If you launch agents through a wrapper script, map its name to an agent in
The key is your command's name; the value is one of the slugs above (`claude`,
`codex`, `traecli`, `gemini`, `aider`, `amp`, `opencode`, `copilot`, `cursor`, `goose`,
`droid`, `pi`, `auggie`, `hermes`, `vibe`, `antigravity`, `grok`, `qwen`,
-`omp`, `kimi`, `qodercli`, `crush`).
+`omp`, `kimi`, `qodercli`, `crush`, `codebuddy`).
## What you get for free
diff --git a/docs/agents/status.mdx b/docs/agents/status.mdx
index 57ae6438..3ac83361 100644
--- a/docs/agents/status.mdx
+++ b/docs/agents/status.mdx
@@ -14,7 +14,7 @@ the agent say which one it is.
| Agent | |
|---|---|
-| Claude Code · Codex · TraeCode · Copilot CLI · OpenCode · Pi · Grok Build · Oh My Pi · Gemini · Droid · Qwen Code · Goose · Kimi Code · Qoder CLI · Crush | Hooks available |
+| Claude Code · Codex · TraeCode · Copilot CLI · OpenCode · Pi · Grok Build · Oh My Pi · Gemini · Droid · Qwen Code · Goose · Kimi Code · Qoder CLI · Crush · CodeBuddy | Hooks available |
| Aider · Amp · Cursor · Auggie · Hermes · Vibe · Antigravity | Detected and labelled, but no status channel yet |
Installing writes into that agent's own configuration directory. Once installed
diff --git a/docs/getting-started/first-launch.mdx b/docs/getting-started/first-launch.mdx
index 20e2e571..3785e32a 100644
--- a/docs/getting-started/first-launch.mdx
+++ b/docs/getting-started/first-launch.mdx
@@ -54,7 +54,7 @@ leave it off if you type accented characters.
## 4. If you use coding agents, install the hooks
-**Settings → Integrations.** tty7 detects 22 coding CLIs by process name on its own —
+**Settings → Integrations.** tty7 detects 23 coding CLIs by process name on its own —
you get brand avatars and tab labels for free. The *status dots*, the "needs
your permission" notifications, and `tty7 wait` all need one more thing: a small
hook the agent calls to report what it is doing.
diff --git a/docs/index.mdx b/docs/index.mdx
index 07e72400..511b0b6f 100644
--- a/docs/index.mdx
+++ b/docs/index.mdx
@@ -31,7 +31,7 @@ something floods the screen.
syntax highlighting, click-to-place-caret, real multi-line editing.
- 22 coding CLIs are recognised on sight. Per-pane status dots, notifications
+ 23 coding CLIs are recognised on sight. Per-pane status dots, notifications
when one needs you, git context, and session resume after a reboot.
diff --git a/src/ui/assets.rs b/src/ui/assets.rs
index 882a7113..2bf9e9ab 100644
--- a/src/ui/assets.rs
+++ b/src/ui/assets.rs
@@ -64,6 +64,9 @@ fn agent_icon(path: &str) -> Option<&'static [u8]> {
"icons/agents/kimi.svg" => include_bytes!("../../assets/icons/agents/kimi.svg"),
"icons/agents/qodercli.svg" => include_bytes!("../../assets/icons/agents/qodercli.svg"),
"icons/agents/crush.svg" => include_bytes!("../../assets/icons/agents/crush.svg"),
+ "icons/agents/codebuddy.svg" => {
+ include_bytes!("../../assets/icons/agents/codebuddy.svg")
+ }
_ => return None,
};
Some(bytes)
diff --git a/src/ui/i18n/en.rs b/src/ui/i18n/en.rs
index c6de1a62..056926da 100644
--- a/src/ui/i18n/en.rs
+++ b/src/ui/i18n/en.rs
@@ -795,6 +795,7 @@ pub fn translate_en(key: L10nKey) -> &'static str {
L10nKey::SettingsAgentKimiCode => "Kimi Code",
L10nKey::SettingsAgentQoderCLI => "Qoder CLI",
L10nKey::SettingsAgentCrush => "Crush",
+ L10nKey::SettingsAgentCodeBuddy => "CodeBuddy",
L10nKey::SettingsSearchAboutKeywords => "version license credits build update check github",
L10nKey::SettingsSearchAppHttpProxyKeywords => {
"proxy http https socks socks5 clash v2ray network download update"
@@ -892,6 +893,9 @@ pub fn translate_en(key: L10nKey) -> &'static str {
}
L10nKey::SettingsSearchQoderCLIKeywords => "agent integration hooks install qoder qodercli",
L10nKey::SettingsSearchCrushKeywords => "agent integration hooks install crush",
+ L10nKey::SettingsSearchCodeBuddyKeywords => {
+ "agent integration hooks install codebuddy codebuddy-code cbc tencent"
+ }
L10nKey::SettingsSearchPiKeywords => "agent integration extension install pi",
L10nKey::SettingsSearchPortForwardingKeywords => {
"ssh tunnel local remote dynamic socks forward rule"
diff --git a/src/ui/i18n/ja.rs b/src/ui/i18n/ja.rs
index 7b7eb124..f89a0b09 100644
--- a/src/ui/i18n/ja.rs
+++ b/src/ui/i18n/ja.rs
@@ -805,6 +805,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
L10nKey::SettingsAgentKimiCode => "Kimi Code",
L10nKey::SettingsAgentQoderCLI => "Qoder CLI",
L10nKey::SettingsAgentCrush => "Crush",
+ L10nKey::SettingsAgentCodeBuddy => "CodeBuddy",
L10nKey::SettingsSearchAboutKeywords => {
"バージョン ライセンス クレジット ビルド 更新 確認 github about version license credits update check"
}
@@ -948,6 +949,9 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
L10nKey::SettingsSearchCrushKeywords => {
"エージェント 統合 フック インストール crush agent integration hooks install"
}
+ L10nKey::SettingsSearchCodeBuddyKeywords => {
+ "エージェント 統合 フック インストール codebuddy cbc tencent agent integration hooks install"
+ }
L10nKey::SettingsSearchPiKeywords => {
"エージェント 統合 拡張 インストール pi agent integration extension install"
}
diff --git a/src/ui/i18n/mod.rs b/src/ui/i18n/mod.rs
index f3d6b4c7..dd78acd6 100644
--- a/src/ui/i18n/mod.rs
+++ b/src/ui/i18n/mod.rs
@@ -614,6 +614,7 @@ l10n_keys! {
SettingsAgentKimiCode,
SettingsAgentQoderCLI,
SettingsAgentCrush,
+ SettingsAgentCodeBuddy,
SettingsSearchAppHttpProxyKeywords,
SettingsSearchAboutKeywords,
SettingsSearchAutoDownloadKeywords,
@@ -670,6 +671,7 @@ l10n_keys! {
SettingsSearchQwenCodeKeywords,
SettingsSearchQoderCLIKeywords,
SettingsSearchCrushKeywords,
+ SettingsSearchCodeBuddyKeywords,
SettingsSearchRememberWindowSizeKeywords,
SettingsSearchReportMouseToAppsKeywords,
SettingsSearchRestoreLastLayoutKeywords,
@@ -1607,6 +1609,7 @@ mod tests {
L10nKey::SettingsAgentQwenCode,
L10nKey::SettingsAgentQoderCLI,
L10nKey::SettingsAgentCrush,
+ L10nKey::SettingsAgentCodeBuddy,
// Windows names its backdrop materials, and Japanese Windows keeps
// those names in Latin script — so does this list. Chinese does
// translate them (云母 / 亚克力), which is what Microsoft's own
diff --git a/src/ui/i18n/zh.rs b/src/ui/i18n/zh.rs
index fe153ff8..a1d7af2d 100644
--- a/src/ui/i18n/zh.rs
+++ b/src/ui/i18n/zh.rs
@@ -702,6 +702,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
L10nKey::SettingsAgentKimiCode => "Kimi Code",
L10nKey::SettingsAgentQoderCLI => "Qoder CLI",
L10nKey::SettingsAgentCrush => "Crush",
+ L10nKey::SettingsAgentCodeBuddy => "CodeBuddy",
L10nKey::SettingsSearchAboutKeywords => {
"关于 版本 许可证 致谢 构建 更新 检查 github about version license credits update"
}
@@ -839,6 +840,9 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
}
L10nKey::SettingsSearchQoderCLIKeywords => "Qoder CLI agent 集成 钩子 安装 qoder qodercli",
L10nKey::SettingsSearchCrushKeywords => "Crush agent 集成 钩子 安装 crush",
+ L10nKey::SettingsSearchCodeBuddyKeywords => {
+ "CodeBuddy 腾讯云代码助手 agent 集成 钩子 安装 codebuddy cbc tencent"
+ }
L10nKey::SettingsSearchPiKeywords => {
"Pi agent 集成 扩展 安装 pi agent integration extension install"
}
diff --git a/src/ui/settings.rs b/src/ui/settings.rs
index 59bd1bf2..976de1f4 100644
--- a/src/ui/settings.rs
+++ b/src/ui/settings.rs
@@ -770,6 +770,11 @@ fn settings_search_entries() -> &'static [SearchEntry] {
title: SettingsAgentCrush,
keywords: SettingsSearchCrushKeywords,
},
+ SearchEntry {
+ section: Agents,
+ title: SettingsAgentCodeBuddy,
+ keywords: SettingsSearchCodeBuddyKeywords,
+ },
SearchEntry {
section: General,
title: SettingsStartupWindow,