From 6cccf993630382f63edbdf19175a06dd33bc91ae Mon Sep 17 00:00:00 2001 From: Austin Thesing Date: Wed, 22 Jul 2026 15:28:26 -0500 Subject: [PATCH 1/2] feat(acp): add OpenCode and Cursor runtimes Signed-off-by: Austin Thesing --- crates/buzz-acp/README.md | 32 ++++++++- crates/buzz-acp/src/config.rs | 15 +++- .../src-tauri/src/managed_agents/discovery.rs | 68 ++++++++++++++++++- .../src/managed_agents/discovery/tests.rs | 18 +++++ 4 files changed, 129 insertions(+), 4 deletions(-) diff --git a/crates/buzz-acp/README.md b/crates/buzz-acp/README.md index b9c8406242..96d334e8d2 100644 --- a/crates/buzz-acp/README.md +++ b/crates/buzz-acp/README.md @@ -9,7 +9,7 @@ Buzz Relay ──WS──→ buzz-acp ──stdio──→ Your Agent (send_message, etc.) ``` -Supports any agent that speaks [ACP](https://agentclientprotocol.com/) over stdio: **goose**, **codex** (via [codex-acp](https://github.com/agentclientprotocol/codex-acp)), and **claude code** (via [claude-agent-acp](https://github.com/agentclientprotocol/claude-agent-acp)). +Supports any agent that speaks [ACP](https://agentclientprotocol.com/) over stdio: **Goose**, **Codex** (via [codex-acp](https://github.com/agentclientprotocol/codex-acp)), **Claude Code** (via [claude-agent-acp](https://github.com/agentclientprotocol/claude-agent-acp)), **OpenCode**, and **Cursor Agent**. ## Prerequisites @@ -89,6 +89,36 @@ buzz-acp Older installs that still expose `claude-code-acp` are also supported. `buzz-acp` treats both Claude ACP command names as the same zero-arg runtime. +## Running with OpenCode + +[OpenCode](https://opencode.ai/docs/acp/) includes a native ACP server. + +```bash +# Install OpenCode if needed +curl -fsSL https://opencode.ai/install | bash + +# buzz-acp recognizes OpenCode and supplies the required `acp` argument. +export BUZZ_ACP_AGENT_COMMAND="opencode" +buzz-acp +``` + +## Running with Cursor Agent + +[Cursor Agent](https://cursor.com/docs/cli/acp) also includes a native ACP server. + +```bash +# Install Cursor Agent if needed +curl https://cursor.com/install -fsS | bash + +# `agent` is Cursor's ACP command. `cursor-agent` is accepted as an alias. +export BUZZ_ACP_AGENT_COMMAND="agent" +buzz-acp +``` + +For both runtimes, an empty `BUZZ_ACP_AGENT_ARGS` is normalized to `acp`. If +you set it for runtime-specific arguments, keep `acp` first (for example, +`BUZZ_ACP_AGENT_ARGS="acp,--pure"`). + ## Configuration All configuration is via environment variables (or CLI flags — every env var has a matching flag). diff --git a/crates/buzz-acp/src/config.rs b/crates/buzz-acp/src/config.rs index d78c4fa319..07834d7e80 100644 --- a/crates/buzz-acp/src/config.rs +++ b/crates/buzz-acp/src/config.rs @@ -610,7 +610,9 @@ pub(crate) fn normalize_agent_command_identity(command: &str) -> String { fn default_agent_args(command: &str) -> Option> { match normalize_agent_command_identity(command).as_str() { - "goose" => Some(vec!["acp".to_string()]), + // These runtimes expose their ACP servers behind an explicit `acp` + // subcommand. Keep this list in sync with desktop discovery. + "goose" | "opencode" | "agent" | "cursor-agent" => Some(vec!["acp".to_string()]), "codex" | "codex-acp" | "claude-agent-acp" | "claude-code-acp" | "claude-code" | "claudecode" | "buzz-agent" => Some(Vec::new()), _ => None, @@ -1427,9 +1429,18 @@ mod tests { } #[test] - fn normalizes_goose_args_to_acp() { + fn normalizes_acp_subcommand_runtimes_to_acp() { assert_eq!(normalize_agent_args("goose", Vec::new()), vec!["acp"]); assert_eq!(normalize_agent_args("goose", vec!["".into()]), vec!["acp"]); + assert_eq!(normalize_agent_args("opencode", Vec::new()), vec!["acp"]); + assert_eq!( + normalize_agent_args("/Users/me/.local/bin/agent", vec!["".into()]), + vec!["acp"] + ); + assert_eq!( + normalize_agent_args("cursor-agent", Vec::new()), + vec!["acp"] + ); } #[test] diff --git a/desktop/src-tauri/src/managed_agents/discovery.rs b/desktop/src-tauri/src/managed_agents/discovery.rs index 50206567ad..9a105d0526 100644 --- a/desktop/src-tauri/src/managed_agents/discovery.rs +++ b/desktop/src-tauri/src/managed_agents/discovery.rs @@ -16,6 +16,8 @@ pub(crate) use runtime_metadata::KnownAcpRuntime; const GOOSE_AVATAR_URL: &str = "https://goose-docs.ai/img/logo_dark.png"; const CLAUDE_CODE_AVATAR_URL: &str = "https://anthropic.gallerycdn.vsassets.io/extensions/anthropic/claude-code/2.1.77/1773707456892/Microsoft.VisualStudio.Services.Icons.Default"; const CODEX_AVATAR_URL: &str = "https://openai.gallerycdn.vsassets.io/extensions/openai/chatgpt/26.5313.41514/1773706730621/Microsoft.VisualStudio.Services.Icons.Default"; +const OPENCODE_AVATAR_URL: &str = "https://opencode.ai/favicon.svg"; +const CURSOR_AVATAR_URL: &str = "https://cursor.com/favicon.svg"; const BUZZ_AGENT_AVATAR_URL: &str = "https://raw.githubusercontent.com/block/buzz/refs/heads/main/crates/buzz-agent/buzz-agent.png"; @@ -157,6 +159,68 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[ // Verified: `codex login status` exits 0 when logged in, non-zero otherwise. auth_probe_args: Some(&["codex", "login", "status"]), }, + KnownAcpRuntime { + id: "opencode", + label: "OpenCode", + commands: &["opencode"], + aliases: &[], + avatar_url: OPENCODE_AVATAR_URL, + mcp_command: None, + mcp_hooks: false, + underlying_cli: None, + cli_install_commands: &["curl -fsSL https://opencode.ai/install | bash"], + cli_install_commands_windows: &[], + adapter_install_commands: &[], + install_instructions_url: "https://opencode.ai/docs/acp/", + cli_install_hint: "Install OpenCode via the official install script.", + adapter_install_hint: "", + skill_dir: None, + supports_acp_model_switching: false, + model_env_var: None, + provider_env_var: None, + provider_locked: false, + default_env: &[], + config_file_path: None, + config_file_format: None, + supports_acp_native_config: false, + thinking_env_var: None, + max_tokens_env_var: None, + context_limit_env_var: None, + required_normalized_fields: &[], + login_hint: Some("Run `opencode auth login` to configure a provider."), + auth_probe_args: None, + }, + KnownAcpRuntime { + id: "cursor", + label: "Cursor Agent", + commands: &["agent", "cursor-agent"], + aliases: &[], + avatar_url: CURSOR_AVATAR_URL, + mcp_command: None, + mcp_hooks: false, + underlying_cli: None, + cli_install_commands: &["curl https://cursor.com/install -fsS | bash"], + cli_install_commands_windows: &[], + adapter_install_commands: &[], + install_instructions_url: "https://cursor.com/docs/cli/acp", + cli_install_hint: "Install Cursor Agent via the official install script.", + adapter_install_hint: "", + skill_dir: None, + supports_acp_model_switching: false, + model_env_var: None, + provider_env_var: None, + provider_locked: true, + default_env: &[], + config_file_path: None, + config_file_format: None, + supports_acp_native_config: false, + thinking_env_var: None, + max_tokens_env_var: None, + context_limit_env_var: None, + required_normalized_fields: &[], + login_hint: Some("Run `agent login` to authenticate with Cursor."), + auth_probe_args: Some(&["agent", "status"]), + }, KnownAcpRuntime { id: "buzz-agent", label: "Buzz Agent", @@ -342,7 +406,9 @@ pub use overrides::{apply_agent_command_update, create_time_agent_command_overri fn default_agent_args(command: &str) -> Option> { match normalize_command_identity(command).as_str() { - "goose" => Some(vec!["acp".to_string()]), + // These runtimes expose their ACP servers behind an explicit `acp` + // subcommand. Keep this list in sync with buzz-acp configuration. + "goose" | "opencode" | "agent" | "cursor-agent" => Some(vec!["acp".to_string()]), "codex" | "codex-acp" | "claude-agent-acp" | "claude-code-acp" | "claude-code" | "claudecode" | "buzz-agent" => Some(Vec::new()), _ => None, diff --git a/desktop/src-tauri/src/managed_agents/discovery/tests.rs b/desktop/src-tauri/src/managed_agents/discovery/tests.rs index 0ed4fe0f6a..929f33c47b 100644 --- a/desktop/src-tauri/src/managed_agents/discovery/tests.rs +++ b/desktop/src-tauri/src/managed_agents/discovery/tests.rs @@ -72,6 +72,24 @@ fn normalizes_claude_and_codex_args_to_empty() { ); } +#[test] +fn normalizes_opencode_and_cursor_args_to_acp() { + assert_eq!(normalize_agent_args("opencode", Vec::new()), vec!["acp"]); + assert_eq!(normalize_agent_args("agent", vec!["".into()]), vec!["acp"]); + assert_eq!(normalize_agent_args("cursor-agent", Vec::new()), vec!["acp"]); +} + +#[test] +fn opencode_and_cursor_are_known_native_acp_runtimes() { + let opencode = super::known_acp_runtime_exact("opencode").expect("OpenCode runtime"); + assert_eq!(opencode.commands, ["opencode"]); + assert_eq!(normalize_agent_args(opencode.commands[0], Vec::new()), vec!["acp"]); + + let cursor = super::known_acp_runtime_exact("cursor").expect("Cursor runtime"); + assert_eq!(cursor.commands, ["agent", "cursor-agent"]); + assert_eq!(normalize_agent_args(cursor.commands[0], Vec::new()), vec!["acp"]); +} + #[test] fn resolves_buzz_agent_avatar() { assert_eq!( From 206c547acc01975999cce12313588b625e109493 Mon Sep 17 00:00:00 2001 From: Austin Thesing Date: Wed, 22 Jul 2026 15:35:49 -0500 Subject: [PATCH 2/2] fix(acp): install native runtimes correctly Signed-off-by: Austin Thesing --- desktop/src-tauri/src/managed_agents/discovery.rs | 10 +++++++--- .../src-tauri/src/managed_agents/discovery/tests.rs | 5 +++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/desktop/src-tauri/src/managed_agents/discovery.rs b/desktop/src-tauri/src/managed_agents/discovery.rs index 9a105d0526..4261949608 100644 --- a/desktop/src-tauri/src/managed_agents/discovery.rs +++ b/desktop/src-tauri/src/managed_agents/discovery.rs @@ -167,7 +167,9 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[ avatar_url: OPENCODE_AVATAR_URL, mcp_command: None, mcp_hooks: false, - underlying_cli: None, + // OpenCode's CLI is its native ACP server; retain the CLI identity so + // the installer executes Phase 1 when the command is absent. + underlying_cli: Some("opencode"), cli_install_commands: &["curl -fsSL https://opencode.ai/install | bash"], cli_install_commands_windows: &[], adapter_install_commands: &[], @@ -198,7 +200,9 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[ avatar_url: CURSOR_AVATAR_URL, mcp_command: None, mcp_hooks: false, - underlying_cli: None, + // Cursor Agent is likewise the native ACP server. This makes its + // install command reachable through the CLI-install phase. + underlying_cli: Some("agent"), cli_install_commands: &["curl https://cursor.com/install -fsS | bash"], cli_install_commands_windows: &[], adapter_install_commands: &[], @@ -209,7 +213,7 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[ supports_acp_model_switching: false, model_env_var: None, provider_env_var: None, - provider_locked: true, + provider_locked: false, default_env: &[], config_file_path: None, config_file_format: None, diff --git a/desktop/src-tauri/src/managed_agents/discovery/tests.rs b/desktop/src-tauri/src/managed_agents/discovery/tests.rs index 929f33c47b..7f7f31c95d 100644 --- a/desktop/src-tauri/src/managed_agents/discovery/tests.rs +++ b/desktop/src-tauri/src/managed_agents/discovery/tests.rs @@ -83,10 +83,15 @@ fn normalizes_opencode_and_cursor_args_to_acp() { fn opencode_and_cursor_are_known_native_acp_runtimes() { let opencode = super::known_acp_runtime_exact("opencode").expect("OpenCode runtime"); assert_eq!(opencode.commands, ["opencode"]); + assert_eq!(opencode.underlying_cli, Some("opencode")); + assert!(!opencode.cli_install_commands_for_os().is_empty()); assert_eq!(normalize_agent_args(opencode.commands[0], Vec::new()), vec!["acp"]); let cursor = super::known_acp_runtime_exact("cursor").expect("Cursor runtime"); assert_eq!(cursor.commands, ["agent", "cursor-agent"]); + assert_eq!(cursor.underlying_cli, Some("agent")); + assert!(!cursor.cli_install_commands_for_os().is_empty()); + assert!(!cursor.provider_locked); assert_eq!(normalize_agent_args(cursor.commands[0], Vec::new()), vec!["acp"]); }