diff --git a/desktop/src-tauri/src/commands/agent_config.rs b/desktop/src-tauri/src/commands/agent_config.rs index 05fb179125..f6094ed529 100644 --- a/desktop/src-tauri/src/commands/agent_config.rs +++ b/desktop/src-tauri/src/commands/agent_config.rs @@ -596,8 +596,6 @@ fn parse_models(raw: Option<&serde_json::Value>) -> (Vec, Option< #[cfg(test)] mod tests { - use std::collections::BTreeMap; - use super::*; use crate::managed_agents::{BackendKind, RespondTo}; @@ -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, @@ -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, @@ -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, diff --git a/desktop/src-tauri/src/commands/agent_discovery.rs b/desktop/src-tauri/src/commands/agent_discovery.rs index 40c4333a11..32266f4f70 100644 --- a/desktop/src-tauri/src/commands/agent_discovery.rs +++ b/desktop/src-tauri/src/commands/agent_discovery.rs @@ -12,6 +12,8 @@ use crate::{ relay::query_relay, }; +mod post_install_verification; + fn active_installs() -> &'static std::sync::Mutex> { use std::collections::HashSet; use std::sync::{Mutex, OnceLock}; @@ -229,11 +231,10 @@ fn install_acp_runtime_blocking(runtime_id: &str) -> Result) { + // 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, +) -> Option { + 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"))); + } +} diff --git a/desktop/src-tauri/src/managed_agents/config_bridge/reader_tests.rs b/desktop/src-tauri/src/managed_agents/config_bridge/reader_tests.rs index f5fc7c3899..4c11cd6c49 100644 --- a/desktop/src-tauri/src/managed_agents/config_bridge/reader_tests.rs +++ b/desktop/src-tauri/src/managed_agents/config_bridge/reader_tests.rs @@ -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, @@ -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, diff --git a/desktop/src-tauri/src/managed_agents/discovery.rs b/desktop/src-tauri/src/managed_agents/discovery.rs index f40eed5a13..6a5de43352 100644 --- a/desktop/src-tauri/src/managed_agents/discovery.rs +++ b/desktop/src-tauri/src/managed_agents/discovery.rs @@ -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, @@ -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, @@ -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, @@ -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, @@ -1224,6 +1228,14 @@ pub fn discover_acp_runtimes() -> Vec { } } }; + 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 @@ -1251,7 +1263,7 @@ pub fn discover_acp_runtimes() -> Vec { 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, diff --git a/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs b/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs index 4dfcad4256..1baf851853 100644 --- a/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs +++ b/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs @@ -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. @@ -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")); + } +} diff --git a/desktop/src-tauri/src/managed_agents/discovery/tests.rs b/desktop/src-tauri/src/managed_agents/discovery/tests.rs index 0ed4fe0f6a..364caa452b 100644 --- a/desktop/src-tauri/src/managed_agents/discovery/tests.rs +++ b/desktop/src-tauri/src/managed_agents/discovery/tests.rs @@ -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, @@ -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!( @@ -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:?}" ); } diff --git a/desktop/src-tauri/src/managed_agents/readiness.rs b/desktop/src-tauri/src/managed_agents/readiness.rs index 87ee6241ee..c04dfa88a4 100644 --- a/desktop/src-tauri/src/managed_agents/readiness.rs +++ b/desktop/src-tauri/src/managed_agents/readiness.rs @@ -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, @@ -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, diff --git a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx index 7556a79ba2..0acef9c3bc 100644 --- a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx +++ b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx @@ -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 > Agents to set it up.

) : null; const advancedFieldsTransition = shouldReduceMotion ? { duration: 0 } : ADVANCED_FIELDS_MOTION_TRANSITION; - React.useEffect(() => { if ( !open || @@ -661,7 +662,6 @@ export function AgentDefinitionDialog({ effectiveProvider, runtime, ]); - const selection: RuntimeModelProviderSelection = { provider, model, diff --git a/desktop/src/features/onboarding/ui/SetupStep.tsx b/desktop/src/features/onboarding/ui/SetupStep.tsx index 96e7322d14..d0a85a8609 100644 --- a/desktop/src/features/onboarding/ui/SetupStep.tsx +++ b/desktop/src/features/onboarding/ui/SetupStep.tsx @@ -385,7 +385,10 @@ function runtimeDetailText(runtime: AcpRuntimeCatalogEntry): string { return "ACP adapter detected but outdated — reinstall required."; } if (runtime.availability === "cli_missing") { - return "ACP adapter detected; CLI missing."; + return "CLI not detected; the desktop app alone isn’t enough."; + } + if (runtime.availability === "not_installed") { + return "CLI not detected; the desktop app alone isn’t enough."; } return ""; } @@ -596,8 +599,8 @@ function RuntimeProvidersSection({ Set up your agent harnesses

- Buzz detected the harnesses available on this machine. Install or sign - in to at least one to continue. + Buzz checks for command-line harnesses on this machine. Install the + CLI or sign in to at least one to continue.

@@ -624,8 +627,8 @@ function RuntimeProvidersSection({ className="max-w-[560px] rounded-2xl bg-white/70 px-6 py-6 text-sm text-muted-foreground" data-testid="onboarding-acp-empty" > - No supported agent harnesses were detected yet. Install Claude Code - or Codex, then check again. + No supported command-line harnesses were detected yet. Install a + supported CLI, then check again.

)} diff --git a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx index cee156ece7..cf69309cfd 100644 --- a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx +++ b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx @@ -53,6 +53,13 @@ const RUNTIME_SORT_PRIORITY: Record = { goose: 1, }; +function runtimeInstallGuideLabel(runtime: AcpRuntimeCatalogEntry) { + return runtime.availability === "adapter_missing" || + runtime.availability === "adapter_outdated" + ? "Adapter install guide" + : "CLI install guide"; +} + function RuntimeLogo({ runtime }: { runtime: AcpRuntimeCatalogEntry }) { const avatarUrl = RUNTIME_LOGO_URLS[runtime.id] ?? runtime.avatarUrl; @@ -131,7 +138,7 @@ function RuntimeOverflowMenu({ onSelect={() => void openUrl(runtime.installInstructionsUrl)} > - Instructions + {runtimeInstallGuideLabel(runtime)} ) : null} @@ -142,7 +149,6 @@ function RuntimeOverflowMenu({ function RuntimeActions({ authMethods, connectingMethodId, - installSuccess, isConnecting, isInstalling, onConnect, @@ -151,7 +157,6 @@ function RuntimeActions({ }: { authMethods: AcpAuthMethod[]; connectingMethodId: string | null; - installSuccess: boolean; isConnecting: boolean; isInstalling: boolean; onConnect: (method: AcpAuthMethod) => void; @@ -160,7 +165,6 @@ function RuntimeActions({ }) { const isAvailable = runtime.availability === "available"; const canInstall = runtime.canAutoInstall && !runtime.nodeRequired; - const isOn = isAvailable || installSuccess; const isWorking = isInstalling || isConnecting; return ( @@ -183,10 +187,10 @@ function RuntimeActions({ ) : ( { if (checked) { onInstall(); @@ -206,7 +210,8 @@ function RuntimeStatusChip({ runtime }: { runtime: AcpRuntimeCatalogEntry }) { ? "Adapter needed" : runtime.availability === "adapter_outdated" ? "Update needed" - : runtime.availability === "cli_missing" + : runtime.availability === "cli_missing" || + runtime.availability === "not_installed" ? "CLI needed" : null; @@ -239,7 +244,6 @@ function RuntimeStatusChip({ runtime }: { runtime: AcpRuntimeCatalogEntry }) { function RuntimeHeader({ authMethods, connectingMethodId, - installSuccess, isConnecting, isInstalling, onConnect, @@ -248,7 +252,6 @@ function RuntimeHeader({ }: { authMethods: AcpAuthMethod[]; connectingMethodId: string | null; - installSuccess: boolean; isConnecting: boolean; isInstalling: boolean; onConnect: (method: AcpAuthMethod) => void; @@ -267,7 +270,6 @@ function RuntimeHeader({ void; runtime: AcpRuntimeCatalogEntry; @@ -328,7 +328,6 @@ function RuntimeRow({ { @@ -357,6 +356,25 @@ function RuntimeRow({ runtime={runtime} /> + {runtime.availability !== "available" ? ( +
+

{runtime.installHint}

+ {runtime.installInstructionsUrl.trim().length > 0 ? ( + + ) : null} +
+ ) : null} + {runtime.authStatus.status === "config_invalid" ? (

) : null} - {installSuccess && runtime.availability !== "available" ? ( -

- {runtime.label} installed. Checking for sign-in options... -

- ) : null} {installError ? (

{installError} @@ -492,8 +505,8 @@ export function DoctorSettingsPanel() { ); const isRefreshing = runtimesQuery.isFetching; const installMutation = useInstallAcpRuntimeMutation(); - const [installResults, setInstallResults] = React.useState< - Record + const [installErrors, setInstallErrors] = React.useState< + Record >({}); // Per-runtime installing state: tracks which runtime IDs have an in-flight // install so concurrent installs each show their own spinner correctly. @@ -503,36 +516,31 @@ export function DoctorSettingsPanel() { function handleInstall(runtimeId: string) { // Clear any previous result for this runtime before retrying. - setInstallResults((prev) => ({ + setInstallErrors((prev) => ({ ...prev, - [runtimeId]: { success: false, error: null }, + [runtimeId]: null, })); setInstallingIds((prev) => new Set(prev).add(runtimeId)); installMutation.mutate(runtimeId, { onSuccess: (result) => { if (result.success) { - setInstallResults((prev) => ({ + setInstallErrors((prev) => ({ ...prev, - [runtimeId]: { success: true, error: null }, + [runtimeId]: null, })); } else { - setInstallResults((prev) => ({ + setInstallErrors((prev) => ({ ...prev, - [runtimeId]: { - success: false, - error: getInstallErrorMessage(result.steps), - }, + [runtimeId]: getInstallErrorMessage(result.steps), })); } }, onError: (error) => { - setInstallResults((prev) => ({ + setInstallErrors((prev) => ({ ...prev, - [runtimeId]: { - success: false, - error: error instanceof Error ? error.message : "Install failed.", - }, + [runtimeId]: + error instanceof Error ? error.message : "Install failed.", })); }, onSettled: () => { @@ -558,7 +566,7 @@ export function DoctorSettingsPanel() {