Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions desktop/src-tauri/src/commands/agent_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,8 +596,6 @@ fn parse_models(raw: Option<&serde_json::Value>) -> (Vec<AcpModelEntry>, Option<

#[cfg(test)]
mod tests {
use std::collections::BTreeMap;

use super::*;
use crate::managed_agents::{BackendKind, RespondTo};

Expand All @@ -614,7 +612,8 @@ mod tests {
cli_install_commands: &[],
cli_install_commands_windows: &[],
adapter_install_commands: &[],
install_instructions_url: "",
cli_install_instructions_url: "",
adapter_install_instructions_url: "",
cli_install_hint: "",
adapter_install_hint: "",
skill_dir: None,
Expand Down Expand Up @@ -654,7 +653,7 @@ mod tests {
parallelism: 1,
system_prompt: None,
model: None,
env_vars: BTreeMap::new(),
env_vars: Default::default(),
start_on_app_launch: false,
auto_restart_on_config_change: true,
runtime_pid: None,
Expand Down Expand Up @@ -705,7 +704,7 @@ mod tests {
is_active: true,
source_team: None,
source_team_persona_slug: None,
env_vars: BTreeMap::new(),
env_vars: Default::default(),
respond_to: None,
respond_to_allowlist: Vec::new(),
parallelism: None,
Expand Down
18 changes: 4 additions & 14 deletions desktop/src-tauri/src/commands/agent_discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ use crate::{
relay::query_relay,
};

mod post_install_verification;

fn active_installs() -> &'static std::sync::Mutex<std::collections::HashSet<String>> {
use std::collections::HashSet;
use std::sync::{Mutex, OnceLock};
Expand Down Expand Up @@ -229,11 +231,10 @@ fn install_acp_runtime_blocking(runtime_id: &str) -> Result<InstallRuntimeResult
}
}

// Clear the resolve cache so the next discovery picks up new binaries.
crate::managed_agents::clear_resolve_cache();
post_install_verification::run(runtime_id, &mut steps);

Ok(InstallRuntimeResult {
success: true,
success: steps.iter().all(|step| step.success),
steps,
restarted_count: 0,
failed_restart_count: 0,
Expand Down Expand Up @@ -1365,17 +1366,6 @@ mod tests {
);
}

/// Goose install commands are the same on all platforms (script is Windows-aware).
#[test]
fn test_goose_install_commands_same_on_all_platforms() {
let goose = crate::managed_agents::known_acp_runtime_exact("goose").unwrap();
assert_eq!(
goose.cli_install_commands_for_os(),
goose.cli_install_commands,
"goose install commands must be identical across platforms"
);
}

/// buzz-agent has no install commands on any platform.
#[test]
fn test_buzz_agent_has_no_install_commands() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use crate::managed_agents::{AcpAvailabilityStatus, InstallStepResult};

pub(super) fn run(runtime_id: &str, steps: &mut Vec<InstallStepResult>) {
// Observe PATH changes and binaries added after Buzz launched.
crate::managed_agents::refresh_login_shell_path();
crate::managed_agents::clear_resolve_cache();

let availability = crate::managed_agents::discover_acp_runtimes()
.into_iter()
.find(|entry| entry.id == runtime_id)
.map(|entry| entry.availability);
if let Some(failure) = failure(runtime_id, availability) {
steps.push(failure);
}
}

fn failure(
runtime_id: &str,
availability: Option<AcpAvailabilityStatus>,
) -> Option<InstallStepResult> {
if availability == Some(AcpAvailabilityStatus::Available) {
return None;
}

let observed = availability
.map(|status| format!("{status:?}"))
.unwrap_or_else(|| "missing from the runtime catalog".to_string());
Some(InstallStepResult {
step: "verify".to_string(),
command: format!("discover {runtime_id}"),
success: false,
stdout: String::new(),
stderr: format!(
"The installer finished, but Buzz still could not use {runtime_id} (observed: {observed})."
),
exit_code: None,
hint: Some(
"Buzz requires the vendor CLI executable, not only its desktop app. If the CLI was installed while Buzz was open, restart Buzz and check again."
.to_string(),
),
})
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn accepts_available_runtime() {
assert!(failure("goose", Some(AcpAvailabilityStatus::Available)).is_none());
}

#[test]
fn rejects_unresolved_runtime() {
let failure = failure("goose", Some(AcpAvailabilityStatus::NotInstalled))
.expect("not-installed runtime must fail verification");

assert_eq!(failure.step, "verify");
assert!(!failure.success);
assert!(failure.stderr.contains("NotInstalled"));
assert!(failure
.hint
.as_deref()
.is_some_and(|hint| hint.contains("desktop app")));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ fn test_runtime() -> &'static KnownAcpRuntime {
cli_install_commands: &[],
cli_install_commands_windows: &[],
adapter_install_commands: &[],
install_instructions_url: "",
cli_install_instructions_url: "",
adapter_install_instructions_url: "",
cli_install_hint: "",
adapter_install_hint: "",
skill_dir: None,
Expand Down Expand Up @@ -615,7 +616,8 @@ fn buzz_agent_runtime() -> &'static KnownAcpRuntime {
cli_install_commands: &[],
cli_install_commands_windows: &[],
adapter_install_commands: &[],
install_instructions_url: "",
cli_install_instructions_url: "",
adapter_install_instructions_url: "",
cli_install_hint: "",
adapter_install_hint: "",
skill_dir: None,
Expand Down
30 changes: 21 additions & 9 deletions desktop/src-tauri/src/managed_agents/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,11 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
mcp_hooks: false,
underlying_cli: Some("goose"),
cli_install_commands: &["curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash"],
cli_install_commands_windows: &[], // goose install script is already Windows-aware
cli_install_commands_windows: &["powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"$env:CONFIGURE='false'; irm https://raw.githubusercontent.com/aaif-goose/goose/main/download_cli.ps1 | iex\""],
adapter_install_commands: &[],
install_instructions_url: "https://block.github.io/goose/",
cli_install_hint: "Install Goose via the official install script.",
cli_install_instructions_url: "https://goose-docs.ai/docs/getting-started/installation/",
adapter_install_instructions_url: "",
cli_install_hint: "Buzz requires the Goose CLI; the desktop app alone is not enough.",
adapter_install_hint: "",
skill_dir: Some(".goose/skills"),
supports_acp_model_switching: false,
Expand Down Expand Up @@ -106,8 +107,9 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
cli_install_commands: &["curl -fsSL https://claude.ai/install.sh | bash"],
cli_install_commands_windows: &["powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"irm https://claude.ai/install.ps1 | iex\""],
adapter_install_commands: &["npm install -g @agentclientprotocol/claude-agent-acp"],
install_instructions_url: "https://github.com/agentclientprotocol/claude-agent-acp",
cli_install_hint: "Install the Claude Code CLI via the official install script.",
cli_install_instructions_url: "https://code.claude.com/docs/en/getting-started",
adapter_install_instructions_url: "https://github.com/agentclientprotocol/claude-agent-acp",
cli_install_hint: "Buzz requires the Claude Code CLI; the desktop app alone is not enough.",
adapter_install_hint: "Install the Claude Code ACP adapter via npm.",
skill_dir: Some(".claude/skills"),
supports_acp_model_switching: false,
Expand Down Expand Up @@ -137,8 +139,9 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
cli_install_commands: &["curl -fsSL https://chatgpt.com/codex/install.sh | sh"],
cli_install_commands_windows: &["powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"irm https://chatgpt.com/codex/install.ps1 | iex\""],
adapter_install_commands: &["npm install -g @agentclientprotocol/codex-acp"],
install_instructions_url: "https://github.com/agentclientprotocol/codex-acp",
cli_install_hint: "Install the Codex CLI via the official install script.",
cli_install_instructions_url: "https://developers.openai.com/codex/cli/",
adapter_install_instructions_url: "https://github.com/agentclientprotocol/codex-acp",
cli_install_hint: "Buzz requires the Codex CLI; the desktop app alone is not enough.",
adapter_install_hint: "Install the Codex ACP adapter via npm.",
skill_dir: Some(".codex/skills"),
supports_acp_model_switching: false,
Expand Down Expand Up @@ -169,7 +172,8 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
cli_install_commands: &[],
cli_install_commands_windows: &[],
adapter_install_commands: &[],
install_instructions_url: "https://github.com/block/buzz",
cli_install_instructions_url: "https://github.com/block/buzz",
adapter_install_instructions_url: "https://github.com/block/buzz",
cli_install_hint: "Ships with the Buzz desktop app.",
adapter_install_hint: "",
skill_dir: None,
Expand Down Expand Up @@ -1224,6 +1228,14 @@ pub fn discover_acp_runtimes() -> Vec<AcpRuntimeCatalogEntry> {
}
}
};
let install_instructions_url = match availability {
AcpAvailabilityStatus::AdapterMissing | AcpAvailabilityStatus::AdapterOutdated => {
runtime.adapter_install_instructions_url
}
AcpAvailabilityStatus::Available
| AcpAvailabilityStatus::CliMissing
| AcpAvailabilityStatus::NotInstalled => runtime.cli_install_instructions_url,
};

// node_required now means Buzz cannot provide npm for this platform.
// On supported desktop platforms, Buzz downloads a private Node/npm
Expand Down Expand Up @@ -1251,7 +1263,7 @@ pub fn discover_acp_runtimes() -> Vec<AcpRuntimeCatalogEntry> {
provider_env_var: runtime.provider_env_var.map(str::to_string),
thinking_env_var: runtime.thinking_env_var.map(str::to_string),
install_hint,
install_instructions_url: runtime.install_instructions_url.to_string(),
install_instructions_url: install_instructions_url.to_string(),
can_auto_install,
underlying_cli_path,
node_required,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ pub(crate) struct KnownAcpRuntime {
pub cli_install_commands_windows: &'static [&'static str],
/// Shell commands to install the ACP adapter (run sequentially, after CLI).
pub adapter_install_commands: &'static [&'static str],
/// Link to docs/repo for manual instructions.
pub install_instructions_url: &'static str,
/// Official CLI installation documentation.
pub cli_install_instructions_url: &'static str,
/// ACP adapter installation documentation.
pub adapter_install_instructions_url: &'static str,
/// Human-readable hint about installing the CLI binary.
pub cli_install_hint: &'static str,
/// Human-readable hint about installing the ACP adapter.
Expand Down Expand Up @@ -78,3 +80,45 @@ impl KnownAcpRuntime {
self.cli_install_commands
}
}

#[cfg(test)]
mod tests {
use super::super::known_acp_runtime_exact;

#[test]
fn vendor_metadata_distinguishes_cli_and_adapter_guidance() {
let goose = known_acp_runtime_exact("goose").unwrap();
assert_eq!(
goose.cli_install_instructions_url,
"https://goose-docs.ai/docs/getting-started/installation/"
);
assert!(goose.adapter_install_instructions_url.is_empty());
assert!(goose.cli_install_hint.contains("desktop app alone"));
assert!(goose
.cli_install_commands_windows
.iter()
.any(|command| command.contains("raw.githubusercontent.com/aaif-goose/goose/main")));
assert!(goose
.cli_install_commands_windows
.iter()
.any(|command| command.contains("CONFIGURE='false'")));

let claude = known_acp_runtime_exact("claude").unwrap();
assert_eq!(
claude.cli_install_instructions_url,
"https://code.claude.com/docs/en/getting-started"
);
assert!(claude
.adapter_install_instructions_url
.contains("claude-agent-acp"));
assert!(claude.cli_install_hint.contains("desktop app alone"));

let codex = known_acp_runtime_exact("codex").unwrap();
assert_eq!(
codex.cli_install_instructions_url,
"https://developers.openai.com/codex/cli/"
);
assert!(codex.adapter_install_instructions_url.contains("codex-acp"));
assert!(codex.cli_install_hint.contains("desktop app alone"));
}
}
15 changes: 7 additions & 8 deletions desktop/src-tauri/src/managed_agents/discovery/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1104,16 +1104,18 @@ fn test_cli_install_commands_for_os_non_empty_for_claude_codex() {
);
}

/// On Windows, Claude and Codex select the PowerShell install commands.
/// On Windows, every vendor CLI selects its official PowerShell installer.
#[cfg(windows)]
#[test]
fn test_cli_install_commands_for_os_selects_powershell_on_windows() {
let claude = super::known_acp_runtime_exact("claude").unwrap();
let codex = super::known_acp_runtime_exact("codex").unwrap();
let goose = super::known_acp_runtime_exact("goose").unwrap();

// Windows must select the PowerShell commands, not the curl|bash ones.
let claude_cmds = claude.cli_install_commands_for_os();
let codex_cmds = codex.cli_install_commands_for_os();
let goose_cmds = goose.cli_install_commands_for_os();

assert_ne!(
claude_cmds, claude.cli_install_commands,
Expand All @@ -1123,6 +1125,7 @@ fn test_cli_install_commands_for_os_selects_powershell_on_windows() {
codex_cmds, codex.cli_install_commands,
"Windows must NOT use the default curl|bash commands for codex"
);
assert_eq!(goose_cmds, goose.cli_install_commands_windows);

// Verify they are the PowerShell installers.
assert!(
Expand All @@ -1133,13 +1136,9 @@ fn test_cli_install_commands_for_os_selects_powershell_on_windows() {
codex_cmds.iter().any(|c| c.contains("powershell")),
"codex Windows install must use powershell; got: {codex_cmds:?}"
);

// Goose and buzz-agent must NOT use Windows-specific commands.
let goose = super::known_acp_runtime_exact("goose").unwrap();
assert_eq!(
goose.cli_install_commands_for_os(),
goose.cli_install_commands,
"goose must use the same commands on all platforms"
assert!(
goose_cmds.iter().any(|c| c.contains("download_cli.ps1")),
"Goose Windows install must use download_cli.ps1; got: {goose_cmds:?}"
);
}

Expand Down
6 changes: 4 additions & 2 deletions desktop/src-tauri/src/managed_agents/readiness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -869,7 +869,8 @@ mod tests {
cli_install_commands: &[],
cli_install_commands_windows: &[],
adapter_install_commands: &[],
install_instructions_url: "",
cli_install_instructions_url: "",
adapter_install_instructions_url: "",
cli_install_hint: "",
adapter_install_hint: "",
skill_dir: None,
Expand Down Expand Up @@ -1063,7 +1064,8 @@ mod tests {
cli_install_commands: &[],
cli_install_commands_windows: &[],
adapter_install_commands: &[],
install_instructions_url: "",
cli_install_instructions_url: "",
adapter_install_instructions_url: "",
cli_install_hint: "",
adapter_install_hint: "",
skill_dir: None,
Expand Down
8 changes: 4 additions & 4 deletions desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -628,15 +628,16 @@ export function AgentDefinitionDialog({
: selectedRuntime.availability === "adapter_outdated"
? `${selectedRuntime.label} ACP adapter is outdated — reinstall to continue.`
: selectedRuntime.availability === "cli_missing"
? `${selectedRuntime.label} ACP adapter is installed but the CLI is missing.`
: `${selectedRuntime.label} is not installed.`}{" "}
? `${selectedRuntime.label} CLI is missing. ${selectedRuntime.installHint}`
: selectedRuntime.id === "buzz-agent"
? `${selectedRuntime.label} is not installed.`
: `${selectedRuntime.label} CLI is missing. ${selectedRuntime.installHint}`}{" "}
Visit Settings &gt; Agents to set it up.
</p>
) : null;
const advancedFieldsTransition = shouldReduceMotion
? { duration: 0 }
: ADVANCED_FIELDS_MOTION_TRANSITION;

React.useEffect(() => {
if (
!open ||
Expand All @@ -661,7 +662,6 @@ export function AgentDefinitionDialog({
effectiveProvider,
runtime,
]);

const selection: RuntimeModelProviderSelection = {
provider,
model,
Expand Down
Loading
Loading