From bf08ad9ee81fe67e656617804b2a17967508d18a Mon Sep 17 00:00:00 2001
From: Atish Patel
Date: Thu, 23 Jul 2026 21:45:18 -0700
Subject: [PATCH 1/9] fix(desktop): clarify CLI runtime setup
Co-authored-by: Atish Patel
Signed-off-by: Atish Patel
Co-authored-by: Codex
---
.../src-tauri/src/commands/agent_config.rs | 9 +-
.../src-tauri/src/commands/agent_discovery.rs | 18 +--
.../post_install_verification.rs | 66 +++++++++
.../config_bridge/reader_tests.rs | 6 +-
.../src-tauri/src/managed_agents/discovery.rs | 30 ++--
.../discovery/runtime_metadata.rs | 40 ++++-
.../src/managed_agents/discovery/tests.rs | 15 +-
.../src-tauri/src/managed_agents/readiness.rs | 6 +-
.../agents/ui/AgentDefinitionDialog.tsx | 4 +-
.../src/features/onboarding/ui/SetupStep.tsx | 13 +-
desktop/tests/e2e/doctor-states.spec.ts | 137 ++++++++++++++----
.../e2e/onboarding-agent-defaults.spec.ts | 34 +++++
12 files changed, 298 insertions(+), 80 deletions(-)
create mode 100644 desktop/src-tauri/src/commands/agent_discovery/post_install_verification.rs
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 f080272914..e33ea5581f 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..65b35525b3 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 \"irm https://github.com/aaif-goose/goose/releases/download/stable/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..a1fd160c4e 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,37 @@ 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"));
+
+ 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..d2056e211a 100644
--- a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
+++ b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
@@ -628,8 +628,8 @@ 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.installHint || "CLI not installed."}{" "}
Visit Settings > Agents to set it up.
) : null;
diff --git a/desktop/src/features/onboarding/ui/SetupStep.tsx b/desktop/src/features/onboarding/ui/SetupStep.tsx
index aa02486706..c336b6cdaa 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 "";
}
@@ -605,8 +608,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.
@@ -629,8 +632,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/tests/e2e/doctor-states.spec.ts b/desktop/tests/e2e/doctor-states.spec.ts
index dd2cf83286..7bf4bce7ca 100644
--- a/desktop/tests/e2e/doctor-states.spec.ts
+++ b/desktop/tests/e2e/doctor-states.spec.ts
@@ -22,7 +22,8 @@ const GOOSE_AVAILABLE = {
default_args: ["acp"],
mcp_command: null,
install_hint: "",
- install_instructions_url: "https://block.github.io/goose/",
+ install_instructions_url:
+ "https://goose-docs.ai/docs/getting-started/installation/",
can_auto_install: false,
underlying_cli_path: null,
node_required: false,
@@ -82,8 +83,9 @@ const CODEX_NOT_INSTALLED = {
binary_path: null,
default_args: [],
mcp_command: null,
- install_hint: "Install the Codex CLI, then install the ACP adapter via npm.",
- install_instructions_url: "https://github.com/zed-industries/codex-acp",
+ install_hint:
+ "Buzz requires the Codex CLI; the desktop app alone is not enough.",
+ install_instructions_url: "https://developers.openai.com/codex/cli/",
can_auto_install: true,
underlying_cli_path: null,
node_required: false,
@@ -160,7 +162,7 @@ test.describe("Doctor panel state screenshots", () => {
),
),
);
- expect(new Set(rowHeights).size).toBe(1);
+ expect(rowHeights[2]).toBeGreaterThan(rowHeights[0]);
const [gooseColors, codexColors] = await Promise.all(
["goose", "codex"].map((runtimeId) =>
page.getByTestId(`doctor-runtime-${runtimeId}`).evaluate((element) => {
@@ -194,11 +196,11 @@ test.describe("Doctor panel state screenshots", () => {
await expect(toggle.locator("span")).toHaveClass(/shadow-none/);
}
await expect(
- page.getByRole("menuitem", { name: "Instructions" }),
+ page.getByRole("menuitem", { name: "CLI install guide" }),
).toHaveCount(0);
await page.getByTestId("doctor-runtime-menu-codex").click();
await expect(
- page.getByRole("menuitem", { name: "Instructions" }),
+ page.getByRole("menuitem", { name: "CLI install guide" }),
).toBeVisible();
await waitForAnimations(page);
await page.screenshot({
@@ -212,6 +214,17 @@ test.describe("Doctor panel state screenshots", () => {
await expect(page.getByTestId("doctor-runtime-codex")).not.toContainText(
"Not installed",
);
+ await expect(page.getByTestId("doctor-runtime-status-codex")).toHaveText(
+ "CLI needed",
+ );
+ await expect(
+ page.getByTestId("doctor-runtime-guidance-codex"),
+ ).toContainText("desktop app alone is not enough");
+ await expect(
+ page
+ .getByTestId("doctor-runtime-guidance-codex")
+ .getByRole("button", { name: "CLI install guide" }),
+ ).toBeVisible();
await runtimeList.scrollIntoViewIfNeeded();
await waitForAnimations(page);
@@ -287,7 +300,7 @@ test.describe("Doctor panel state screenshots", () => {
);
await page.getByTestId("doctor-runtime-menu-codex").click();
await expect(
- page.getByRole("menuitem", { name: "Instructions" }),
+ page.getByRole("menuitem", { name: "CLI install guide" }),
).toBeVisible();
await page.keyboard.press("Escape");
@@ -331,7 +344,7 @@ test.describe("Doctor panel state screenshots", () => {
);
await page.getByTestId("doctor-runtime-menu-claude").click();
await expect(
- page.getByRole("menuitem", { name: "Instructions" }),
+ page.getByRole("menuitem", { name: "CLI install guide" }),
).toBeVisible();
await page.keyboard.press("Escape");
@@ -354,8 +367,9 @@ test.describe("Doctor panel state screenshots", () => {
availability: "adapter_missing",
underlying_cli_path: "/usr/local/bin/codex",
node_required: true,
- install_hint:
- "Install the Codex ACP adapter: npm install -g @zed-industries/codex-acp",
+ install_hint: "Install the Codex ACP adapter via npm.",
+ install_instructions_url:
+ "https://github.com/agentclientprotocol/codex-acp",
},
BUZZ_AGENT_AVAILABLE,
],
@@ -373,16 +387,20 @@ test.describe("Doctor panel state screenshots", () => {
"Adapter needed",
);
await expect(row).not.toContainText("Node.js is required");
- await expect(row).toHaveCSS(
- "height",
+ expect(
+ await row.evaluate((element) => element.getBoundingClientRect().height),
+ ).toBeGreaterThan(
await page
.getByTestId("doctor-runtime-goose")
- .evaluate((element) => getComputedStyle(element).height),
+ .evaluate((element) => element.getBoundingClientRect().height),
);
await page.getByTestId("doctor-runtime-menu-codex").click();
await expect(
page.getByRole("menuitem", { name: "Install Node.js" }),
).toBeVisible();
+ await expect(
+ page.getByRole("menuitem", { name: "Adapter install guide" }),
+ ).toBeVisible();
await page.keyboard.press("Escape");
await row.scrollIntoViewIfNeeded();
@@ -395,9 +413,10 @@ test.describe("Doctor panel state screenshots", () => {
*
* The mock is configured with a two-call sequence:
* call 1 → failure (E404)
- * call 2 → success
+ * call 2 → installer exit 0, but post-install discovery still cannot find
+ * the runtime
* This exercises the full retry path: fail state → toggle on again →
- * success banner.
+ * verified failure without a false installed state.
*/
test("05-retry-after-failure", async ({ page }) => {
await installMockBridge(page, {
@@ -427,16 +446,26 @@ test.describe("Doctor panel state screenshots", () => {
],
},
{
- success: true,
+ success: false,
steps: [
{
- step: "adapter",
- command: "npm install -g @zed-industries/codex-acp",
+ step: "cli",
+ command: "powershell.exe install codex",
success: true,
- stdout: "added 1 package",
+ stdout: "installed",
stderr: "",
exit_code: 0,
},
+ {
+ step: "verify",
+ command: "discover codex",
+ success: false,
+ stdout: "",
+ stderr:
+ "The installer finished, but Buzz still could not use codex (observed: NotInstalled).",
+ exit_code: null,
+ hint: "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.",
+ },
],
},
],
@@ -470,25 +499,71 @@ test.describe("Doctor panel state screenshots", () => {
await waitForAnimations(page);
await row.screenshot({ path: `${SHOTS}/05-retry-after-failure.png` });
- // Toggle on again — the mock returns success on the second call.
+ // Toggle on again — the install command exits 0, but verification fails.
await toggle.click();
await expect(loading).toBeVisible();
await expect(toggle).toHaveCount(0);
- // The error disappears, then the success banner and on state render.
+ // The runtime remains retryable and never renders a false success state.
await expect(loading).toHaveCount(0, { timeout: 5_000 });
- await expect(row).not.toContainText("failed", { timeout: 5_000 });
- await expect(
- row.getByText("Codex installed. Checking for sign-in options..."),
- ).toBeVisible({
- timeout: 10_000,
- });
- await expect(toggle).toBeChecked();
- await expect(toggle).toBeDisabled();
+ await expect(row).toContainText("desktop app", { timeout: 5_000 });
+ await expect(row).toContainText('Step "verify" failed');
+ await expect(row.getByText(/installed\. Checking/)).toHaveCount(0);
+ await expect(toggle).not.toBeChecked();
+ await expect(toggle).toBeEnabled();
await row.scrollIntoViewIfNeeded();
await waitForAnimations(page);
- await row.screenshot({ path: `${SHOTS}/05-retry-success.png` });
+ await row.screenshot({ path: `${SHOTS}/05-verification-failure.png` });
+ });
+
+ test("05b-verified-install-enables-runtime", async ({ page }) => {
+ await installMockBridge(page, {
+ acpRuntimesCatalog: [
+ GOOSE_AVAILABLE,
+ CLAUDE_AVAILABLE_LOGGED_IN,
+ CODEX_NOT_INSTALLED,
+ BUZZ_AGENT_AVAILABLE,
+ ],
+ acpRuntimesCatalogAfterInstall: [
+ GOOSE_AVAILABLE,
+ CLAUDE_AVAILABLE_LOGGED_IN,
+ {
+ ...CODEX_NOT_INSTALLED,
+ availability: "available",
+ command: "codex-acp",
+ binary_path: "/usr/local/bin/codex-acp",
+ underlying_cli_path: "/usr/local/bin/codex",
+ auth_status: { status: "logged_in" },
+ },
+ BUZZ_AGENT_AVAILABLE,
+ ],
+ installAcpRuntimeResult: {
+ success: true,
+ steps: [
+ {
+ step: "adapter",
+ command: "npm install -g @agentclientprotocol/codex-acp",
+ success: true,
+ stdout: "added 1 package",
+ stderr: "",
+ exit_code: 0,
+ },
+ ],
+ },
+ });
+
+ await page.goto("/", { waitUntil: "domcontentloaded" });
+ await openSettings(page, "agents");
+
+ const toggle = page.getByTestId("doctor-runtime-toggle-codex");
+ await expect(toggle).toBeEnabled();
+ await toggle.click();
+ await expect(toggle).toBeChecked({ timeout: 5_000 });
+ await expect(toggle).toBeDisabled();
+ await expect(page.getByTestId("doctor-runtime-guidance-codex")).toHaveCount(
+ 0,
+ );
});
/**
@@ -590,7 +665,7 @@ test.describe("Doctor panel state screenshots", () => {
);
await page.getByTestId("doctor-runtime-menu-claude").click();
await expect(
- page.getByRole("menuitem", { name: "Instructions" }),
+ page.getByRole("menuitem", { name: "CLI install guide" }),
).toBeVisible();
await expect(
page.getByRole("menuitem", { name: "Sign in with ChatGPT" }),
diff --git a/desktop/tests/e2e/onboarding-agent-defaults.spec.ts b/desktop/tests/e2e/onboarding-agent-defaults.spec.ts
index e1d8b11b6f..d007bd6118 100644
--- a/desktop/tests/e2e/onboarding-agent-defaults.spec.ts
+++ b/desktop/tests/e2e/onboarding-agent-defaults.spec.ts
@@ -84,6 +84,40 @@ test("setup shows only Claude Code and Codex as detected harnesses", async ({
await expect(page.getByRole("checkbox")).toHaveCount(0);
});
+test("setup distinguishes a missing CLI from an installed desktop app", async ({
+ page,
+}) => {
+ await installMockBridge(
+ page,
+ {
+ acpRuntimesCatalog: [
+ runtime(
+ "codex",
+ "not_installed",
+ { status: "unknown" },
+ {
+ install_hint:
+ "Buzz requires the Codex CLI; the desktop app alone is not enough.",
+ install_instructions_url:
+ "https://developers.openai.com/codex/cli/",
+ },
+ ),
+ ],
+ },
+ { skipCommunitySeed: true, skipOnboardingSeed: true },
+ );
+ await page.goto("/");
+ await navigateToSetupPage(page);
+
+ const card = page.getByTestId("onboarding-runtime-codex");
+ await expect(card).toContainText(
+ "CLI not detected; the desktop app alone isn’t enough.",
+ );
+ await expect(card.getByTestId("onboarding-runtime-install-codex")).toHaveText(
+ "INSTALL",
+ );
+});
+
test("ready state is detected and enables Next without persisting a default", async ({
page,
}) => {
From da410ef7570736e819a82e8bf7ae37cd91b62c7b Mon Sep 17 00:00:00 2001
From: Atish Patel
Date: Thu, 23 Jul 2026 22:41:14 -0700
Subject: [PATCH 2/9] fix(desktop): preserve Buzz Agent guidance
Co-authored-by: Atish Patel
Signed-off-by: Atish Patel
Co-authored-by: Codex
---
desktop/src/features/agents/ui/AgentDefinitionDialog.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
index d2056e211a..0acef9c3bc 100644
--- a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
+++ b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
@@ -629,14 +629,15 @@ export function AgentDefinitionDialog({
? `${selectedRuntime.label} ACP adapter is outdated — reinstall to continue.`
: selectedRuntime.availability === "cli_missing"
? `${selectedRuntime.label} CLI is missing. ${selectedRuntime.installHint}`
- : selectedRuntime.installHint || "CLI not installed."}{" "}
+ : 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,
From 6980307e79a2f1ae85990f6c816dbce6ee09c958 Mon Sep 17 00:00:00 2001
From: Atish Patel
Date: Thu, 23 Jul 2026 22:42:06 -0700
Subject: [PATCH 3/9] fix(desktop): use published Goose installer
Co-authored-by: Atish Patel
Signed-off-by: Atish Patel
Co-authored-by: Codex
---
desktop/src-tauri/src/managed_agents/discovery.rs | 2 +-
.../src/managed_agents/discovery/runtime_metadata.rs | 8 ++++++++
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/desktop/src-tauri/src/managed_agents/discovery.rs b/desktop/src-tauri/src/managed_agents/discovery.rs
index 65b35525b3..6a5de43352 100644
--- a/desktop/src-tauri/src/managed_agents/discovery.rs
+++ b/desktop/src-tauri/src/managed_agents/discovery.rs
@@ -73,7 +73,7 @@ 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: &["powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"irm https://github.com/aaif-goose/goose/releases/download/stable/download_cli.ps1 | iex\""],
+ 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: &[],
cli_install_instructions_url: "https://goose-docs.ai/docs/getting-started/installation/",
adapter_install_instructions_url: "",
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 a1fd160c4e..1baf851853 100644
--- a/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs
+++ b/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs
@@ -94,6 +94,14 @@ mod tests {
);
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!(
From c6625332640b4f66c85a35215789dadec2e7e23e Mon Sep 17 00:00:00 2001
From: Atish Patel
Date: Fri, 24 Jul 2026 07:06:05 -0700
Subject: [PATCH 4/9] fix(desktop): preserve Goose installer environment
Co-authored-by: Atish Patel
Signed-off-by: Atish Patel
Co-authored-by: Codex
---
desktop/src-tauri/src/managed_agents/discovery.rs | 2 +-
.../src-tauri/src/managed_agents/discovery/runtime_metadata.rs | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/desktop/src-tauri/src/managed_agents/discovery.rs b/desktop/src-tauri/src/managed_agents/discovery.rs
index 6a5de43352..cfb65f3a88 100644
--- a/desktop/src-tauri/src/managed_agents/discovery.rs
+++ b/desktop/src-tauri/src/managed_agents/discovery.rs
@@ -73,7 +73,7 @@ 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: &["powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"$env:CONFIGURE='false'; irm https://raw.githubusercontent.com/aaif-goose/goose/main/download_cli.ps1 | iex\""],
+ 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: &[],
cli_install_instructions_url: "https://goose-docs.ai/docs/getting-started/installation/",
adapter_install_instructions_url: "",
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 1baf851853..0e850336ea 100644
--- a/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs
+++ b/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs
@@ -101,7 +101,7 @@ mod tests {
assert!(goose
.cli_install_commands_windows
.iter()
- .any(|command| command.contains("CONFIGURE='false'")));
+ .any(|command| command.contains("\\$env:CONFIGURE='false'")));
let claude = known_acp_runtime_exact("claude").unwrap();
assert_eq!(
From 04c073cd0c0767c3b6d3514fd563636d01b73d6f Mon Sep 17 00:00:00 2001
From: Atish Patel
Date: Fri, 24 Jul 2026 10:10:27 -0700
Subject: [PATCH 5/9] fix(desktop): address runtime onboarding review
Move external CLI guidance into runtime catalog metadata and make post-install rediscovery targeted and auth-probe-free. Clarify setup labels and the Goose Windows installer constraint.
Co-authored-by: Atish Patel
Signed-off-by: Atish Patel
Co-authored-by: Goose
---
.../post_install_verification.rs | 20 +-
.../src-tauri/src/managed_agents/discovery.rs | 227 ++++++++++--------
desktop/src-tauri/src/managed_agents/types.rs | 2 +
desktop/src/features/agents/AGENTS.md | 4 +-
.../agents/ui/AgentDefinitionDialog.tsx | 8 +-
.../src/features/onboarding/ui/SetupStep.tsx | 8 +-
.../settings/ui/DoctorSettingsPanel.tsx | 9 +-
desktop/src/shared/api/tauri.ts | 3 +
desktop/src/shared/api/types.ts | 2 +
desktop/src/testing/e2eBridge.ts | 4 +
10 files changed, 165 insertions(+), 122 deletions(-)
diff --git a/desktop/src-tauri/src/commands/agent_discovery/post_install_verification.rs b/desktop/src-tauri/src/commands/agent_discovery/post_install_verification.rs
index 4c7d206bdc..3155104b56 100644
--- a/desktop/src-tauri/src/commands/agent_discovery/post_install_verification.rs
+++ b/desktop/src-tauri/src/commands/agent_discovery/post_install_verification.rs
@@ -5,10 +5,7 @@ pub(super) fn run(runtime_id: &str, steps: &mut Vec) {
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);
+ let availability = crate::managed_agents::discover_acp_runtime_availability(runtime_id);
if let Some(failure) = failure(runtime_id, availability) {
steps.push(failure);
}
@@ -23,7 +20,7 @@ fn failure(
}
let observed = availability
- .map(|status| format!("{status:?}"))
+ .map(availability_label)
.unwrap_or_else(|| "missing from the runtime catalog".to_string());
Some(InstallStepResult {
step: "verify".to_string(),
@@ -41,6 +38,17 @@ fn failure(
})
}
+fn availability_label(status: AcpAvailabilityStatus) -> String {
+ match status {
+ AcpAvailabilityStatus::Available => "available",
+ AcpAvailabilityStatus::AdapterMissing => "ACP adapter missing",
+ AcpAvailabilityStatus::AdapterOutdated => "ACP adapter outdated",
+ AcpAvailabilityStatus::CliMissing => "CLI missing",
+ AcpAvailabilityStatus::NotInstalled => "not installed",
+ }
+ .to_string()
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -57,7 +65,7 @@ mod tests {
assert_eq!(failure.step, "verify");
assert!(!failure.success);
- assert!(failure.stderr.contains("NotInstalled"));
+ assert!(failure.stderr.contains("observed: not installed"));
assert!(failure
.hint
.as_deref()
diff --git a/desktop/src-tauri/src/managed_agents/discovery.rs b/desktop/src-tauri/src/managed_agents/discovery.rs
index cfb65f3a88..cfa1ea3e8e 100644
--- a/desktop/src-tauri/src/managed_agents/discovery.rs
+++ b/desktop/src-tauri/src/managed_agents/discovery.rs
@@ -73,6 +73,8 @@ 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"],
+ // Goose's stable release currently publishes only the Unix installer;
+ // its official Windows instructions intentionally point at this main-branch script.
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: &[],
cli_install_instructions_url: "https://goose-docs.ai/docs/getting-started/installation/",
@@ -1162,117 +1164,130 @@ struct PartialEntry {
entry: AcpRuntimeCatalogEntry,
}
-pub fn discover_acp_runtimes() -> Vec {
- // Phase 1: build all entries (fast — no probes yet).
- let mut partials: Vec = KNOWN_ACP_RUNTIMES
+fn discover_acp_runtime_phase1(runtime: &'static KnownAcpRuntime) -> PartialEntry {
+ let adapter_result = runtime
+ .commands
.iter()
- .map(|runtime| {
- let adapter_result = runtime
- .commands
- .iter()
- .find_map(|command| find_command(command).map(|path| (*command, path)));
-
- let underlying_cli_found = runtime
- .underlying_cli
- .map(|cli| find_command(cli).is_some())
- .unwrap_or(false);
- let (mut availability, command, binary_path) =
- classify_runtime(adapter_result, runtime.underlying_cli, underlying_cli_found);
-
- // For codex-acp: when the adapter resolves as Available, probe the
- // version. An adapter with major version < 1 is treated as outdated —
- // the CODEX_CONFIG spawn contract requires 1.x.
- if runtime.id == "codex"
- && availability == AcpAvailabilityStatus::Available
- && command.as_deref() == Some("codex-acp")
- {
- if let Some(path_str) = &binary_path {
- availability = codex_adapter_availability(&PathBuf::from(path_str));
- }
- }
+ .find_map(|command| find_command(command).map(|path| (*command, path)));
+
+ let underlying_cli_found = runtime
+ .underlying_cli
+ .map(|cli| find_command(cli).is_some())
+ .unwrap_or(false);
+ let (mut availability, command, binary_path) =
+ classify_runtime(adapter_result, runtime.underlying_cli, underlying_cli_found);
+
+ // For codex-acp: when the adapter resolves as Available, probe the
+ // version. An adapter with major version < 1 is treated as outdated —
+ // the CODEX_CONFIG spawn contract requires 1.x.
+ if runtime.id == "codex"
+ && availability == AcpAvailabilityStatus::Available
+ && command.as_deref() == Some("codex-acp")
+ {
+ if let Some(path_str) = &binary_path {
+ availability = codex_adapter_availability(&PathBuf::from(path_str));
+ }
+ }
- // Warm the adapter-availability cache for the badge fallback.
- // The cache is scoped to the codex runtime; other runtimes leave it
- // unchanged. Invalidated by `clear_resolve_cache`.
- if runtime.id == "codex" {
- cache_adapter_availability(availability.clone());
- }
+ // Warm the adapter-availability cache for the badge fallback.
+ // The cache is scoped to the codex runtime; other runtimes leave it
+ // unchanged. Invalidated by `clear_resolve_cache`.
+ if runtime.id == "codex" {
+ cache_adapter_availability(availability.clone());
+ }
- let underlying_cli_path = runtime
- .underlying_cli
- .and_then(find_command)
- .map(|p| p.display().to_string());
-
- let default_args = command
- .as_deref()
- .map(|cmd| normalize_agent_args(cmd, Vec::new()))
- .unwrap_or_default();
-
- let can_auto_install = !runtime.cli_install_commands_for_os().is_empty()
- || !runtime.adapter_install_commands.is_empty();
-
- let cli_hint = runtime.cli_install_hint;
- let adapter_hint = runtime.adapter_install_hint;
- let install_hint = match availability {
- AcpAvailabilityStatus::Available => cli_hint.to_string(),
- AcpAvailabilityStatus::CliMissing => cli_hint.to_string(),
- AcpAvailabilityStatus::AdapterMissing => adapter_hint.to_string(),
- AcpAvailabilityStatus::AdapterOutdated => adapter_hint.to_string(),
- AcpAvailabilityStatus::NotInstalled => {
- if !cli_hint.is_empty() && !adapter_hint.is_empty() {
- format!("{cli_hint} {adapter_hint}")
- } else if !cli_hint.is_empty() {
- cli_hint.to_string()
- } else {
- adapter_hint.to_string()
- }
- }
- };
- 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,
- };
+ let underlying_cli_path = runtime
+ .underlying_cli
+ .and_then(find_command)
+ .map(|p| p.display().to_string());
- // node_required now means Buzz cannot provide npm for this platform.
- // On supported desktop platforms, Buzz downloads a private Node/npm
- // runtime into app data before running npm-backed adapter installs.
- let node_required = matches!(
- availability,
- AcpAvailabilityStatus::AdapterMissing | AcpAvailabilityStatus::NotInstalled
- ) && runtime_needs_npm(runtime)
- && buzz_managed_node_bin_dir().is_none()
- && resolve_command("npm").is_none()
- && resolve_command("node").is_none();
-
- PartialEntry {
- runtime,
- entry: AcpRuntimeCatalogEntry {
- id: runtime.id.to_string(),
- label: runtime.label.to_string(),
- avatar_url: runtime.avatar_url.to_string(),
- availability,
- command,
- binary_path,
- default_args,
- mcp_command: runtime.mcp_command.map(str::to_string),
- model_env_var: runtime.model_env_var.map(str::to_string),
- 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: install_instructions_url.to_string(),
- can_auto_install,
- underlying_cli_path,
- node_required,
- // Filled in by the probe phase below.
- auth_status: AuthStatus::Unknown,
- login_hint: None,
- },
+ let default_args = command
+ .as_deref()
+ .map(|cmd| normalize_agent_args(cmd, Vec::new()))
+ .unwrap_or_default();
+
+ let can_auto_install = !runtime.cli_install_commands_for_os().is_empty()
+ || !runtime.adapter_install_commands.is_empty();
+
+ let cli_hint = runtime.cli_install_hint;
+ let adapter_hint = runtime.adapter_install_hint;
+ let install_hint = match availability {
+ AcpAvailabilityStatus::Available => cli_hint.to_string(),
+ AcpAvailabilityStatus::CliMissing => cli_hint.to_string(),
+ AcpAvailabilityStatus::AdapterMissing => adapter_hint.to_string(),
+ AcpAvailabilityStatus::AdapterOutdated => adapter_hint.to_string(),
+ AcpAvailabilityStatus::NotInstalled => {
+ if !cli_hint.is_empty() && !adapter_hint.is_empty() {
+ format!("{cli_hint} {adapter_hint}")
+ } else if !cli_hint.is_empty() {
+ cli_hint.to_string()
+ } else {
+ adapter_hint.to_string()
}
- })
+ }
+ };
+ 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
+ // runtime into app data before running npm-backed adapter installs.
+ let node_required = matches!(
+ availability,
+ AcpAvailabilityStatus::AdapterMissing | AcpAvailabilityStatus::NotInstalled
+ ) && runtime_needs_npm(runtime)
+ && buzz_managed_node_bin_dir().is_none()
+ && resolve_command("npm").is_none()
+ && resolve_command("node").is_none();
+
+ PartialEntry {
+ runtime,
+ entry: AcpRuntimeCatalogEntry {
+ id: runtime.id.to_string(),
+ label: runtime.label.to_string(),
+ avatar_url: runtime.avatar_url.to_string(),
+ availability,
+ command,
+ binary_path,
+ default_args,
+ mcp_command: runtime.mcp_command.map(str::to_string),
+ model_env_var: runtime.model_env_var.map(str::to_string),
+ 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: install_instructions_url.to_string(),
+ can_auto_install,
+ requires_external_cli: runtime.underlying_cli.is_some(),
+ underlying_cli_path,
+ node_required,
+ // Filled in by the auth-probe phase in full catalog discovery.
+ auth_status: AuthStatus::Unknown,
+ login_hint: None,
+ },
+ }
+}
+
+/// Discover one runtime's filesystem availability without running any auth probes.
+///
+/// Post-install verification only needs to know whether the requested runtime
+/// resolves, so it should not pay the cost of authenticating every catalog entry.
+pub(crate) fn discover_acp_runtime_availability(runtime_id: &str) -> Option {
+ known_acp_runtime_exact(runtime_id)
+ .map(discover_acp_runtime_phase1)
+ .map(|partial| partial.entry.availability)
+}
+
+pub fn discover_acp_runtimes() -> Vec {
+ // Phase 1: build all entries (fast — no probes yet).
+ let mut partials: Vec = KNOWN_ACP_RUNTIMES
+ .iter()
+ .map(discover_acp_runtime_phase1)
.collect();
// Phase 2: run auth probes in parallel for entries that need them.
diff --git a/desktop/src-tauri/src/managed_agents/types.rs b/desktop/src-tauri/src/managed_agents/types.rs
index 90da759af0..af4908911b 100644
--- a/desktop/src-tauri/src/managed_agents/types.rs
+++ b/desktop/src-tauri/src/managed_agents/types.rs
@@ -585,6 +585,8 @@ pub struct AcpRuntimeCatalogEntry {
pub install_instructions_url: String,
/// true when at least one automated install step is available
pub can_auto_install: bool,
+ /// true when this runtime depends on a separately installed vendor CLI.
+ pub requires_external_cli: bool,
pub underlying_cli_path: Option,
/// true when an npm adapter step is pending but Node.js / npm is absent.
/// The UI hides the Install button and shows a Node.js install callout.
diff --git a/desktop/src/features/agents/AGENTS.md b/desktop/src/features/agents/AGENTS.md
index dc61984487..2af9ddb98c 100644
--- a/desktop/src/features/agents/AGENTS.md
+++ b/desktop/src/features/agents/AGENTS.md
@@ -15,7 +15,9 @@ Plan of record: `Buzz/Harness-Provider-Model.md` in Morgan's Obsidian vault
declares each harness's model/provider/effort env keys and capabilities. Spawn
applies them; `AcpRuntimeCatalogEntry` exposes them over IPC; and
`lib/agentConfigCore.ts` projects them into field descriptors. The frontend
-never maintains a rival copy of this table.
+never maintains a rival copy of this table. Setup guidance follows the same
+rule: `requires_external_cli` is derived from `KnownAcpRuntime` and projected
+to the UI rather than inferred from a runtime ID in a component.
If you need a new capability fact (a new env key, a native option, a "supports
X" flag): add it to `KnownAcpRuntime` first, expose it on
diff --git a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
index 0acef9c3bc..5263920854 100644
--- a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
+++ b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
@@ -627,17 +627,16 @@ export function AgentDefinitionDialog({
? `${selectedRuntime.label} CLI is installed but the ACP adapter is missing.`
: selectedRuntime.availability === "adapter_outdated"
? `${selectedRuntime.label} ACP adapter is outdated — reinstall to continue.`
- : selectedRuntime.availability === "cli_missing"
+ : selectedRuntime.requiresExternalCli
? `${selectedRuntime.label} CLI is missing. ${selectedRuntime.installHint}`
- : selectedRuntime.id === "buzz-agent"
- ? `${selectedRuntime.label} is not installed.`
- : `${selectedRuntime.label} CLI is missing. ${selectedRuntime.installHint}`}{" "}
+ : `${selectedRuntime.label} is not installed.`}{" "}
Visit Settings > Agents to set it up.
) : null;
const advancedFieldsTransition = shouldReduceMotion
? { duration: 0 }
: ADVANCED_FIELDS_MOTION_TRANSITION;
+
React.useEffect(() => {
if (
!open ||
@@ -662,6 +661,7 @@ 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 c336b6cdaa..288ca30259 100644
--- a/desktop/src/features/onboarding/ui/SetupStep.tsx
+++ b/desktop/src/features/onboarding/ui/SetupStep.tsx
@@ -384,10 +384,10 @@ function runtimeDetailText(runtime: AcpRuntimeCatalogEntry): string {
if (runtime.availability === "adapter_outdated") {
return "ACP adapter detected but outdated — reinstall required.";
}
- if (runtime.availability === "cli_missing") {
- return "CLI not detected; the desktop app alone isn’t enough.";
- }
- if (runtime.availability === "not_installed") {
+ if (
+ runtime.availability === "cli_missing" ||
+ runtime.availability === "not_installed"
+ ) {
return "CLI not detected; the desktop app alone isn’t enough.";
}
return "";
diff --git a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx
index 7a0186a7ba..7333fc8c6c 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 setup 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}
diff --git a/desktop/src/shared/api/tauri.ts b/desktop/src/shared/api/tauri.ts
index d2848626ae..5406cf820d 100644
--- a/desktop/src/shared/api/tauri.ts
+++ b/desktop/src/shared/api/tauri.ts
@@ -184,6 +184,8 @@ export type RawAcpRuntimeCatalogEntry = {
install_hint: string;
install_instructions_url: string;
can_auto_install: boolean;
+ /** Optional only for older E2E fixtures; the Rust catalog always supplies it. */
+ requires_external_cli?: boolean;
underlying_cli_path: string | null;
node_required: boolean;
/** Tagged union with snake_case status values — same shape as `AuthStatus`. */
@@ -743,6 +745,7 @@ function fromRawAcpRuntimeCatalogEntry(
installHint: entry.install_hint,
installInstructionsUrl: entry.install_instructions_url,
canAutoInstall: entry.can_auto_install,
+ requiresExternalCli: entry.requires_external_cli ?? false,
underlyingCliPath: entry.underlying_cli_path,
nodeRequired: entry.node_required,
authStatus: entry.auth_status,
diff --git a/desktop/src/shared/api/types.ts b/desktop/src/shared/api/types.ts
index d28f2d0cf1..a82f766b63 100644
--- a/desktop/src/shared/api/types.ts
+++ b/desktop/src/shared/api/types.ts
@@ -548,6 +548,8 @@ export type AcpRuntimeCatalogEntry = {
installHint: string;
installInstructionsUrl: string;
canAutoInstall: boolean;
+ /** True when the runtime depends on a separately installed vendor CLI. */
+ requiresExternalCli: boolean;
underlyingCliPath: string | null;
/** True when an npm adapter step is pending but Node.js / npm is absent. */
nodeRequired: boolean;
diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts
index a7d58e70cc..5169c329a0 100644
--- a/desktop/src/testing/e2eBridge.ts
+++ b/desktop/src/testing/e2eBridge.ts
@@ -6972,6 +6972,7 @@ async function handleDiscoverAcpRuntimes(
install_hint: "Install Goose via the official install script.",
install_instructions_url: "https://block.github.io/goose/",
can_auto_install: true,
+ requires_external_cli: true,
underlying_cli_path: null,
node_required: false,
auth_status: { status: "not_applicable" },
@@ -6990,6 +6991,7 @@ async function handleDiscoverAcpRuntimes(
install_instructions_url:
"https://www.npmjs.com/package/@anthropic-ai/claude-agent-acp",
can_auto_install: true,
+ requires_external_cli: true,
underlying_cli_path: "/usr/local/bin/claude",
node_required: false,
auth_status: { status: "unknown" },
@@ -7008,6 +7010,7 @@ async function handleDiscoverAcpRuntimes(
"The codex-acp adapter must be built from source. See the GitHub repo.",
install_instructions_url: "https://github.com/openai/codex",
can_auto_install: false,
+ requires_external_cli: true,
underlying_cli_path: null,
node_required: false,
auth_status: { status: "unknown" },
@@ -7025,6 +7028,7 @@ async function handleDiscoverAcpRuntimes(
install_hint: "Ships with the Buzz desktop app.",
install_instructions_url: "https://github.com/block/buzz",
can_auto_install: false,
+ requires_external_cli: false,
underlying_cli_path: null,
node_required: false,
auth_status: { status: "not_applicable" },
From 0ecfc59c94a18fb004e172e4c590c91f3f7512de Mon Sep 17 00:00:00 2001
From: Atish Patel
Date: Fri, 24 Jul 2026 11:37:06 -0700
Subject: [PATCH 6/9] fix(desktop): remove stale install success binding
Keep main's per-runtime mutation state without retaining the optimistic-success binding removed by post-install verification.
Co-authored-by: Atish Patel
Signed-off-by: Atish Patel
Co-authored-by: Codex
---
desktop/src/features/settings/ui/DoctorSettingsPanel.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx
index 7333fc8c6c..5926893743 100644
--- a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx
+++ b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx
@@ -312,7 +312,6 @@ function RuntimeRow({
}, [resetEpoch]);
const isInstalling = installMutation.isPending;
const installError = installResult?.error ?? null;
- const installSuccess = installResult?.success ?? false;
function handleInstall() {
setInstallResult(null);
From fc15d79b070204e54e0c256198dbe326179c4062 Mon Sep 17 00:00:00 2001
From: Atish Patel
Date: Fri, 24 Jul 2026 11:50:52 -0700
Subject: [PATCH 7/9] test(desktop): update runtime setup guide expectations
Align Doctor E2E assertions with the review-requested CLI setup guide label.
Co-authored-by: Atish Patel
Signed-off-by: Atish Patel
Co-authored-by: Codex
---
desktop/tests/e2e/doctor-states.spec.ts | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/desktop/tests/e2e/doctor-states.spec.ts b/desktop/tests/e2e/doctor-states.spec.ts
index 7bf4bce7ca..0f356fd9ba 100644
--- a/desktop/tests/e2e/doctor-states.spec.ts
+++ b/desktop/tests/e2e/doctor-states.spec.ts
@@ -196,11 +196,11 @@ test.describe("Doctor panel state screenshots", () => {
await expect(toggle.locator("span")).toHaveClass(/shadow-none/);
}
await expect(
- page.getByRole("menuitem", { name: "CLI install guide" }),
+ page.getByRole("menuitem", { name: "CLI setup guide" }),
).toHaveCount(0);
await page.getByTestId("doctor-runtime-menu-codex").click();
await expect(
- page.getByRole("menuitem", { name: "CLI install guide" }),
+ page.getByRole("menuitem", { name: "CLI setup guide" }),
).toBeVisible();
await waitForAnimations(page);
await page.screenshot({
@@ -223,7 +223,7 @@ test.describe("Doctor panel state screenshots", () => {
await expect(
page
.getByTestId("doctor-runtime-guidance-codex")
- .getByRole("button", { name: "CLI install guide" }),
+ .getByRole("button", { name: "CLI setup guide" }),
).toBeVisible();
await runtimeList.scrollIntoViewIfNeeded();
@@ -300,7 +300,7 @@ test.describe("Doctor panel state screenshots", () => {
);
await page.getByTestId("doctor-runtime-menu-codex").click();
await expect(
- page.getByRole("menuitem", { name: "CLI install guide" }),
+ page.getByRole("menuitem", { name: "CLI setup guide" }),
).toBeVisible();
await page.keyboard.press("Escape");
@@ -344,7 +344,7 @@ test.describe("Doctor panel state screenshots", () => {
);
await page.getByTestId("doctor-runtime-menu-claude").click();
await expect(
- page.getByRole("menuitem", { name: "CLI install guide" }),
+ page.getByRole("menuitem", { name: "CLI setup guide" }),
).toBeVisible();
await page.keyboard.press("Escape");
@@ -665,7 +665,7 @@ test.describe("Doctor panel state screenshots", () => {
);
await page.getByTestId("doctor-runtime-menu-claude").click();
await expect(
- page.getByRole("menuitem", { name: "CLI install guide" }),
+ page.getByRole("menuitem", { name: "CLI setup guide" }),
).toBeVisible();
await expect(
page.getByRole("menuitem", { name: "Sign in with ChatGPT" }),
From 680a9bd5881188879af137e63350c84432ab619a Mon Sep 17 00:00:00 2001
From: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
Date: Fri, 24 Jul 2026 18:39:34 -0400
Subject: [PATCH 8/9] fix(discovery): drop bash-escape from Goose Windows
installer command
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Post-#2750, powershell.exe install commands spawn natively via
install_powershell_command() — the Git Bash -c layer is gone. The Goose
Windows catalog entry carried a \$ escape that was written to survive
the old bash layer; with native spawn the body is passed verbatim, so
PowerShell received literal \:CONFIGURE='false' — a malformed
statement.
Drop the backslash so the runtime body is bare $env:CONFIGURE='false',
which PowerShell evaluates correctly as an environment-variable
assignment.
Also restore the installSuccess derived variable in DoctorSettingsPanel
RuntimeRow that was dropped during conflict resolution, and update the
runtime_metadata test assertion that was guarding the old escaped form.
Add test_powershell_command_goose_catalog_dequoted (Windows-only) to
pin that the catalog command routes through install_powershell_command
and produces the unescaped body as a single argv element. Bump the
agent_discovery.rs file-size override from 1810 to 1826 to account for
the new test.
Co-authored-by: Will Pfleger
Signed-off-by: Will Pfleger
---
desktop/scripts/check-file-sizes.mjs | 4 ++-
.../src-tauri/src/commands/agent_discovery.rs | 26 +++++++++++++++++++
.../src-tauri/src/managed_agents/discovery.rs | 2 +-
.../discovery/runtime_metadata.rs | 2 +-
.../settings/ui/DoctorSettingsPanel.tsx | 1 +
5 files changed, 32 insertions(+), 3 deletions(-)
diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs
index 2651318bc3..96f97126e0 100644
--- a/desktop/scripts/check-file-sizes.mjs
+++ b/desktop/scripts/check-file-sizes.mjs
@@ -498,7 +498,9 @@ const overrides = new Map([
// route PowerShell CLI installs natively on Windows (bypasses Git Bash PATH
// poisoning that resolved GNU tar instead of bsdtar → Codex install failure).
// Includes unit tests for detection, routing, and -Command body preservation.
- ["src-tauri/src/commands/agent_discovery.rs", 1810],
+ // +16: test_powershell_command_goose_catalog_dequoted proves the \$→$ escape
+ // fix for the Goose Windows installer (PR #2680 interaction with #2750).
+ ["src-tauri/src/commands/agent_discovery.rs", 1826],
// draft-persistence predicate: submit-time `loadDraft` check + inline comment
// + deps-array entry in submitMessage closes the never-persisted-boundary
// defect (Thufir Pass-3 finding). Load-bearing correctness fix; queued to
diff --git a/desktop/src-tauri/src/commands/agent_discovery.rs b/desktop/src-tauri/src/commands/agent_discovery.rs
index e33ea5581f..7b1979d654 100644
--- a/desktop/src-tauri/src/commands/agent_discovery.rs
+++ b/desktop/src-tauri/src/commands/agent_discovery.rs
@@ -1665,6 +1665,32 @@ mod tests {
);
}
+ /// Goose Windows catalog command (discovery.rs:78) must dequote to a bare pipeline
+ /// with a literal `$env:` prefix — no backslash before the dollar sign.
+ /// This proves the `\$` → `$` escape fix: post-#2750 the spawn is native and
+ /// PowerShell receives the body verbatim, so a residual `\` would produce
+ /// `\$env:CONFIGURE='false'` which is a malformed statement.
+ #[cfg(windows)]
+ #[test]
+ fn test_powershell_command_goose_catalog_dequoted() {
+ let cmd = super::install_powershell_command(
+ r#"powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "$env:CONFIGURE='false'; irm https://raw.githubusercontent.com/aaif-goose/goose/main/download_cli.ps1 | iex""#,
+ );
+ assert_eq!(
+ cmd.get_args()
+ .map(|a| a.to_string_lossy().into_owned())
+ .collect::>(),
+ vec![
+ "-NoProfile",
+ "-ExecutionPolicy",
+ "Bypass",
+ "-Command",
+ "$env:CONFIGURE='false'; irm https://raw.githubusercontent.com/aaif-goose/goose/main/download_cli.ps1 | iex",
+ ],
+ "Goose catalog command must dequote with bare $env: (no backslash before $)"
+ );
+ }
+
// ── install retry ─────────────────────────────────────────────────────────
/// Build an `InstallStepResult` with just the fields the retry loop reads.
diff --git a/desktop/src-tauri/src/managed_agents/discovery.rs b/desktop/src-tauri/src/managed_agents/discovery.rs
index cfa1ea3e8e..ab76965259 100644
--- a/desktop/src-tauri/src/managed_agents/discovery.rs
+++ b/desktop/src-tauri/src/managed_agents/discovery.rs
@@ -75,7 +75,7 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
cli_install_commands: &["curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash"],
// Goose's stable release currently publishes only the Unix installer;
// its official Windows instructions intentionally point at this main-branch script.
- 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\""],
+ 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: &[],
cli_install_instructions_url: "https://goose-docs.ai/docs/getting-started/installation/",
adapter_install_instructions_url: "",
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 0e850336ea..2fb6a471d4 100644
--- a/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs
+++ b/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs
@@ -101,7 +101,7 @@ mod tests {
assert!(goose
.cli_install_commands_windows
.iter()
- .any(|command| command.contains("\\$env:CONFIGURE='false'")));
+ .any(|command| command.contains("$env:CONFIGURE='false'")));
let claude = known_acp_runtime_exact("claude").unwrap();
assert_eq!(
diff --git a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx
index 5926893743..7333fc8c6c 100644
--- a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx
+++ b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx
@@ -312,6 +312,7 @@ function RuntimeRow({
}, [resetEpoch]);
const isInstalling = installMutation.isPending;
const installError = installResult?.error ?? null;
+ const installSuccess = installResult?.success ?? false;
function handleInstall() {
setInstallResult(null);
From 6fb23ac4fa0cbec1ded3df188bf9d9ca47f727e1 Mon Sep 17 00:00:00 2001
From: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
Date: Fri, 24 Jul 2026 19:30:10 -0400
Subject: [PATCH 9/9] fix(desktop): restore guidance div, not_installed chip,
and remove installSuccess plumbing
Rebase conflict resolution in the prior commit incorrectly preserved main's
#2658 per-card mutation architecture while dropping three Atish-authored hunks:
- guidance div (doctor-runtime-guidance-*) missing from RuntimeRow
- not_installed availability not included in CLI-needed chip branch
- installSuccess binding restored instead of deleted; installSuccess banner kept
This commit replaces the panel with Atish's final state from ca34471bf:
- guidance div reinserted between RuntimeHeader and config_invalid paragraph
- not_installed added to CLI-needed chip (alongside cli_missing)
- installSuccess variable, prop threading through Row/Header/Actions, isOn
computed var, disabled expression, and green banner all removed
- data-testid on install-error paragraph preserved (added by fc15d79b0)
- per-card resetEpoch architecture from #2658 kept (Atish's final intent)
Co-authored-by: Will Pfleger
Signed-off-by: Will Pfleger
---
.../settings/ui/DoctorSettingsPanel.tsx | 39 +++++++++++--------
1 file changed, 23 insertions(+), 16 deletions(-)
diff --git a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx
index 7333fc8c6c..894d88bd22 100644
--- a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx
+++ b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx
@@ -149,7 +149,6 @@ function RuntimeOverflowMenu({
function RuntimeActions({
authMethods,
connectingMethodId,
- installSuccess,
isConnecting,
isInstalling,
onConnect,
@@ -158,7 +157,6 @@ function RuntimeActions({
}: {
authMethods: AcpAuthMethod[];
connectingMethodId: string | null;
- installSuccess: boolean;
isConnecting: boolean;
isInstalling: boolean;
onConnect: (method: AcpAuthMethod) => void;
@@ -167,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 (
@@ -190,10 +187,10 @@ function RuntimeActions({
) : (
{
if (checked) {
onInstall();
@@ -213,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;
@@ -246,7 +244,6 @@ function RuntimeStatusChip({ runtime }: { runtime: AcpRuntimeCatalogEntry }) {
function RuntimeHeader({
authMethods,
connectingMethodId,
- installSuccess,
isConnecting,
isInstalling,
onConnect,
@@ -255,7 +252,6 @@ function RuntimeHeader({
}: {
authMethods: AcpAuthMethod[];
connectingMethodId: string | null;
- installSuccess: boolean;
isConnecting: boolean;
isInstalling: boolean;
onConnect: (method: AcpAuthMethod) => void;
@@ -274,7 +270,6 @@ function RuntimeHeader({
{
@@ -398,6 +391,25 @@ function RuntimeRow({
runtime={runtime}
/>
+ {runtime.availability !== "available" ? (
+