diff --git a/desktop/public/harness-logos/CREDITS.md b/desktop/public/harness-logos/CREDITS.md index de4e003c3f..716c43e1ae 100644 --- a/desktop/public/harness-logos/CREDITS.md +++ b/desktop/public/harness-logos/CREDITS.md @@ -9,6 +9,7 @@ license permits redistribution. | File | Upstream | Commit | License | Source path | Modifications | |---|---|---|---|---|---| +| `devin.svg` | [Cognition Devin documentation](https://docs.devin.ai/cli) | Retrieved 2026-07-27 | Cognition trademark; nominative use to identify the Devin harness | Official documentation `logo/favicon.svg` | Added the official black mark to a white square canvas so it remains legible in both app themes | | `hermes.png` | [NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent) | `6ad632b` | MIT © 2025 Nous Research | `website/static/img/logo.png` | Cropped the baked-in border frame, padded to square, resized to 64×64, quantised to a 16-colour palette | | `openclaw.svg` | [openclaw/openclaw](https://github.com/openclaw/openclaw) | `b06f40a` | MIT © 2026 OpenClaw Foundation | `ui/public/favicon.svg` | Removed the SMIL animation elements (renders the upstream rest pose statically — verified pixel-identical to the upstream frame at t=0); minified paths | | `omp.svg` | [can1357/oh-my-pi](https://github.com/can1357/oh-my-pi) | `667111575ebba136dadfd6989379e7f67e0d40d9` | MIT © 2025 Mario Zechner; © 2025–2026 Can Bölük | `assets/icon.svg` | None | diff --git a/desktop/public/harness-logos/devin.svg b/desktop/public/harness-logos/devin.svg new file mode 100644 index 0000000000..e760797d68 --- /dev/null +++ b/desktop/public/harness-logos/devin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/desktop/src-tauri/src/managed_agents/discovery.rs b/desktop/src-tauri/src/managed_agents/discovery.rs index eecbf4de3e..7190816fd0 100644 --- a/desktop/src-tauri/src/managed_agents/discovery.rs +++ b/desktop/src-tauri/src/managed_agents/discovery.rs @@ -10,8 +10,11 @@ use crate::managed_agents::{ HarnessSource, }; +mod presets; mod runtime_metadata; +use presets::{preset_catalog_entry, PRESET_HARNESSES}; +pub(crate) use presets::{preset_harness_definitions, preset_harness_ids}; pub(crate) use runtime_metadata::KnownAcpRuntime; const GOOSE_AVATAR_URL: &str = "https://goose-docs.ai/img/logo_dark.png"; @@ -1436,225 +1439,6 @@ pub(crate) fn discover_acp_runtime_availability(runtime_id: &str) -> Option, -} - -/// Build the catalog entry for one preset harness through an injectable -/// resolver — the seam the preset loop consumes and tests bind. -/// -/// Availability consumes only the adapter-missing arm of the builtin -/// predicate: adapter presence alone decides `Available` (exactly today's -/// behavior — an `amp-acp` without `amp` stays selectable), and -/// `underlying_cli` is consulted only when the adapter is absent, to -/// distinguish `AdapterMissing` (vendor CLI present) from `NotInstalled` -/// (neither found). See the `underlying_cli` field doc for why the full -/// `classify_runtime` predicate is deliberately not used here. -fn preset_catalog_entry( - def: &PresetHarness, - resolve: impl Fn(&str) -> Option, -) -> AcpRuntimeCatalogEntry { - let (availability, command, binary_path) = match resolve(def.command) { - Some(path) => ( - AcpAvailabilityStatus::Available, - Some(def.command.to_string()), - Some(path.display().to_string()), - ), - None => { - let underlying_cli_found = def - .underlying_cli - .map(|cli| resolve(cli).is_some()) - .unwrap_or(false); - if underlying_cli_found { - (AcpAvailabilityStatus::AdapterMissing, None, None) - } else { - (AcpAvailabilityStatus::NotInstalled, None, None) - } - } - }; - let underlying_cli_path = def - .underlying_cli - .and_then(resolve) - .map(|p| p.display().to_string()); - - let default_args = normalize_agent_args( - def.command, - def.args.iter().map(|s| s.to_string()).collect(), - ); - - AcpRuntimeCatalogEntry { - id: def.id.to_string(), - label: def.label.to_string(), - // No remote URL — all preset icons are bundled assets. - avatar_url: String::new(), - availability, - command, - binary_path, - default_args, - mcp_command: None, - model_env_var: None, - provider_env_var: None, - thinking_env_var: None, - install_hint: def.install_hint.to_string(), - install_instructions_url: def.install_instructions_url.to_string(), - can_auto_install: false, - // Kept false even for adapter presets: presets carry one flat - // install_hint (the adapter's), so the requiresExternalCli - // "CLI is missing" wording would pair the wrong noun with it. - // The builtin path, with per-availability hints, is the only - // consumer of the true case. - requires_external_cli: false, - underlying_cli_path, - node_required: false, - auth_status: AuthStatus::NotApplicable, - login_hint: None, - source: HarnessSource::Preset, - // Preset entries have static, non-editable env; definition_env is empty. - definition_env: Default::default(), - } -} - -const PRESET_HARNESSES: &[PresetHarness] = &[ - PresetHarness { - id: "cursor", - label: "Cursor", - command: "cursor-agent", - args: &["acp"], - install_instructions_url: "https://cursor.com/downloads", - install_hint: "Buzz talks to Cursor through the cursor-agent CLI's ACP mode.", - underlying_cli: None, - }, - PresetHarness { - id: "omp", - label: "Oh My Pi", - command: "omp", - args: &["acp"], - install_instructions_url: "https://github.com/can1357/oh-my-pi", - install_hint: "Buzz talks to Oh My Pi through its CLI's ACP mode (omp acp).", - underlying_cli: None, - }, - PresetHarness { - id: "grok", - label: "Grok Build", - command: "grok", - args: &["agent", "--always-approve", "stdio"], - install_instructions_url: "https://build.x.ai/docs", - install_hint: "Buzz talks to Grok Build through its CLI's agent stdio mode.", - underlying_cli: None, - }, - PresetHarness { - id: "opencode", - label: "OpenCode", - command: "opencode", - args: &["acp"], - install_instructions_url: "https://opencode.ai/docs", - install_hint: "Buzz talks to OpenCode through its CLI's ACP mode (opencode acp).", - underlying_cli: None, - }, - PresetHarness { - id: "kimi", - label: "Kimi Code", - command: "kimi", - args: &["acp"], - install_instructions_url: "https://kimi.ai/download", - install_hint: "Buzz talks to Kimi Code through its CLI's ACP mode (kimi acp).", - underlying_cli: None, - }, - PresetHarness { - id: "amp", - label: "Amp", - command: "amp-acp", - args: &[], - install_instructions_url: "https://github.com/tao12345666333/amp-acp", - install_hint: "Buzz talks to the Amp CLI through the amp-acp adapter. Follow the setup guide to install the adapter so the amp-acp command is on your PATH.", - underlying_cli: Some("amp"), - }, - PresetHarness { - id: "hermes", - label: "Hermes Agent", - command: "hermes-acp", - args: &[], - install_instructions_url: "https://hermes-agent.nousresearch.com", - install_hint: "Buzz talks to Hermes Agent through its hermes-acp command.", - underlying_cli: None, - }, - PresetHarness { - id: "openclaw", - label: "OpenClaw", - command: "openclaw", - args: &["acp"], - install_instructions_url: "https://docs.openclaw.ai/start/getting-started", - install_hint: "Buzz talks to OpenClaw through its ACP mode (openclaw acp), which relies on the OpenClaw Gateway daemon. Follow the setup guide to install both.\n\n\ - ⚠️ Execution-locus note: `openclaw acp` runs tools inside the \ - OpenClaw Gateway daemon, not in the Desktop process. \ - Desktop-injected BUZZ_* env vars are visible to the `openclaw` \ - harness process itself, but do NOT automatically reach the \ - Gateway's execution environment. If your tools or agent logic \ - needs BUZZ_* credentials at execution time, set them on the \ - Gateway's own environment separately.", - underlying_cli: None, - }, -]; - -/// Return the static preset harness definitions as `HarnessDefinition` values. -/// -/// Used by `warm_harness_registry_from_dir` to seed the loaded-harness registry -/// at startup before the frontend triggers a full discovery run. -pub(crate) fn preset_harness_definitions( -) -> Vec { - PRESET_HARNESSES - .iter() - .map( - |p| crate::managed_agents::custom_harnesses::HarnessDefinition { - id: p.id.to_string(), - label: p.label.to_string(), - command: p.command.to_string(), - args: p.args.iter().map(|s| s.to_string()).collect(), - env: std::collections::BTreeMap::new(), - install_instructions_url: p.install_instructions_url.to_string(), - install_hint: p.install_hint.to_string(), - }, - ) - .collect() -} - -/// Return the static slice of preset harness IDs. -/// -/// Used by `check_id_collision` in `custom_harnesses` to derive the reserved-ID -/// set from the single source of truth (`PRESET_HARNESSES`) rather than a -/// hand-maintained copy. Adding a preset automatically reserves its ID. -pub(crate) fn preset_harness_ids() -> &'static [&'static str] { - // `PRESET_HARNESSES` is `'static`; we project its `id` fields. - // Computed once via OnceLock to avoid repeated allocations on hot paths. - use std::sync::OnceLock; - static IDS: OnceLock> = OnceLock::new(); - IDS.get_or_init(|| PRESET_HARNESSES.iter().map(|p| p.id).collect()) - .as_slice() -} - /// Discover all ACP runtimes, optionally merging user-defined custom harnesses /// from `custom_harnesses_dir`. /// diff --git a/desktop/src-tauri/src/managed_agents/discovery/presets.rs b/desktop/src-tauri/src/managed_agents/discovery/presets.rs new file mode 100644 index 0000000000..72c4657dc7 --- /dev/null +++ b/desktop/src-tauri/src/managed_agents/discovery/presets.rs @@ -0,0 +1,335 @@ +use std::path::PathBuf; +use std::sync::OnceLock; + +use crate::managed_agents::{ + AcpAvailabilityStatus, AcpRuntimeCatalogEntry, AuthStatus, HarnessSource, +}; + +use super::normalize_agent_args; + +/// Static data for a well-known tier-2 ACP harness. +pub(super) struct PresetHarness { + pub(super) id: &'static str, + label: &'static str, + command: &'static str, + args: &'static [&'static str], + install_instructions_url: &'static str, + install_hint: &'static str, + /// Vendor CLI the ACP command wraps, when the preset is an adapter. + /// + /// Consulted only when the adapter is absent, so `AdapterMissing` replaces + /// `NotInstalled` when the CLI is present but the adapter is not. `None` + /// when the command is itself the vendor CLI. + underlying_cli: Option<&'static str>, +} + +/// Build one preset catalog entry through an injectable command resolver. +pub(super) fn preset_catalog_entry( + def: &PresetHarness, + resolve: impl Fn(&str) -> Option, +) -> AcpRuntimeCatalogEntry { + let (availability, command, binary_path) = match resolve(def.command) { + Some(path) => ( + AcpAvailabilityStatus::Available, + Some(def.command.to_string()), + Some(path.display().to_string()), + ), + None => { + let underlying_cli_found = def + .underlying_cli + .map(|cli| resolve(cli).is_some()) + .unwrap_or(false); + if underlying_cli_found { + (AcpAvailabilityStatus::AdapterMissing, None, None) + } else { + (AcpAvailabilityStatus::NotInstalled, None, None) + } + } + }; + let underlying_cli_path = def + .underlying_cli + .and_then(resolve) + .map(|path| path.display().to_string()); + + AcpRuntimeCatalogEntry { + id: def.id.to_string(), + label: def.label.to_string(), + // No remote URL — all preset icons are bundled assets. + avatar_url: String::new(), + availability, + command, + binary_path, + default_args: normalize_agent_args( + def.command, + def.args.iter().map(|arg| arg.to_string()).collect(), + ), + mcp_command: None, + model_env_var: None, + provider_env_var: None, + thinking_env_var: None, + install_hint: def.install_hint.to_string(), + install_instructions_url: def.install_instructions_url.to_string(), + can_auto_install: false, + // Presets carry one flat install hint, so builtin external-CLI copy + // would name the wrong missing component for adapter presets. + requires_external_cli: false, + underlying_cli_path, + node_required: false, + auth_status: AuthStatus::NotApplicable, + login_hint: None, + source: HarnessSource::Preset, + definition_env: Default::default(), + } +} + +pub(super) const PRESET_HARNESSES: &[PresetHarness] = &[ + PresetHarness { + id: "devin", + label: "Devin", + command: "devin", + args: &["acp"], + install_instructions_url: "https://docs.devin.ai/cli", + install_hint: "Buzz talks to Devin through the official Devin CLI's ACP mode (devin acp).", + underlying_cli: None, + }, + PresetHarness { + id: "cursor", + label: "Cursor", + command: "cursor-agent", + args: &["acp"], + install_instructions_url: "https://cursor.com/downloads", + install_hint: "Buzz talks to Cursor through the cursor-agent CLI's ACP mode.", + underlying_cli: None, + }, + PresetHarness { + id: "omp", + label: "Oh My Pi", + command: "omp", + args: &["acp"], + install_instructions_url: "https://github.com/can1357/oh-my-pi", + install_hint: "Buzz talks to Oh My Pi through its CLI's ACP mode (omp acp).", + underlying_cli: None, + }, + PresetHarness { + id: "grok", + label: "Grok Build", + command: "grok", + args: &["agent", "--always-approve", "stdio"], + install_instructions_url: "https://build.x.ai/docs", + install_hint: "Buzz talks to Grok Build through its CLI's agent stdio mode.", + underlying_cli: None, + }, + PresetHarness { + id: "opencode", + label: "OpenCode", + command: "opencode", + args: &["acp"], + install_instructions_url: "https://opencode.ai/docs", + install_hint: "Buzz talks to OpenCode through its CLI's ACP mode (opencode acp).", + underlying_cli: None, + }, + PresetHarness { + id: "kimi", + label: "Kimi Code", + command: "kimi", + args: &["acp"], + install_instructions_url: "https://kimi.ai/download", + install_hint: "Buzz talks to Kimi Code through its CLI's ACP mode (kimi acp).", + underlying_cli: None, + }, + PresetHarness { + id: "amp", + label: "Amp", + command: "amp-acp", + args: &[], + install_instructions_url: "https://github.com/tao12345666333/amp-acp", + install_hint: "Buzz talks to the Amp CLI through the amp-acp adapter. Follow the setup guide to install the adapter so the amp-acp command is on your PATH.", + underlying_cli: Some("amp"), + }, + PresetHarness { + id: "hermes", + label: "Hermes Agent", + command: "hermes-acp", + args: &[], + install_instructions_url: "https://hermes-agent.nousresearch.com", + install_hint: "Buzz talks to Hermes Agent through its hermes-acp command.", + underlying_cli: None, + }, + PresetHarness { + id: "openclaw", + label: "OpenClaw", + command: "openclaw", + args: &["acp"], + install_instructions_url: "https://docs.openclaw.ai/start/getting-started", + install_hint: "Buzz talks to OpenClaw through its ACP mode (openclaw acp), which relies on the OpenClaw Gateway daemon. Follow the setup guide to install both.\n\n\ + ⚠️ Execution-locus note: `openclaw acp` runs tools inside the \ + OpenClaw Gateway daemon, not in the Desktop process. \ + Desktop-injected BUZZ_* env vars are visible to the `openclaw` \ + harness process itself, but do NOT automatically reach the \ + Gateway's execution environment. If your tools or agent logic \ + needs BUZZ_* credentials at execution time, set them on the \ + Gateway's own environment separately.", + underlying_cli: None, + }, +]; + +/// Return preset definitions for the spawn/readiness registry. +pub(crate) fn preset_harness_definitions( +) -> Vec { + PRESET_HARNESSES + .iter() + .map( + |preset| crate::managed_agents::custom_harnesses::HarnessDefinition { + id: preset.id.to_string(), + label: preset.label.to_string(), + command: preset.command.to_string(), + args: preset.args.iter().map(|arg| arg.to_string()).collect(), + env: Default::default(), + install_instructions_url: preset.install_instructions_url.to_string(), + install_hint: preset.install_hint.to_string(), + }, + ) + .collect() +} + +/// Return preset IDs from the catalog's single source of truth. +pub(crate) fn preset_harness_ids() -> &'static [&'static str] { + static IDS: OnceLock> = OnceLock::new(); + IDS.get_or_init(|| PRESET_HARNESSES.iter().map(|preset| preset.id).collect()) + .as_slice() +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use crate::managed_agents::{AcpAvailabilityStatus, AuthStatus, HarnessSource}; + + use super::{preset_catalog_entry, PresetHarness, PRESET_HARNESSES}; + + /// Amp-shaped preset: an ACP adapter wrapping a separately installed CLI. + const ADAPTER_PRESET: PresetHarness = PresetHarness { + id: "amp-test", + label: "Amp Test", + command: "amp-acp", + args: &[], + install_instructions_url: "https://example.com/install", + install_hint: "Install the amp-acp npm adapter.", + underlying_cli: Some("amp"), + }; + + #[test] + fn devin_preset_uses_official_native_acp_invocation() { + let preset = PRESET_HARNESSES + .iter() + .find(|preset| preset.id == "devin") + .expect("Devin preset should be present"); + + assert_eq!(preset.label, "Devin"); + assert_eq!(preset.command, "devin"); + assert_eq!(preset.args, &["acp"]); + assert_eq!(preset.underlying_cli, None); + assert_eq!(preset.install_instructions_url, "https://docs.devin.ai/cli"); + + let entry = preset_catalog_entry(preset, |command| { + (command == "devin").then(|| PathBuf::from("/usr/local/bin/devin")) + }); + assert_eq!(entry.availability, AcpAvailabilityStatus::Available); + assert_eq!(entry.command.as_deref(), Some("devin")); + assert_eq!(entry.default_args, vec!["acp"]); + assert_eq!(entry.binary_path.as_deref(), Some("/usr/local/bin/devin")); + assert_eq!(entry.auth_status, AuthStatus::NotApplicable); + assert_eq!(entry.source, HarnessSource::Preset); + + let missing_entry = preset_catalog_entry(preset, |_| None); + assert_eq!( + missing_entry.availability, + AcpAvailabilityStatus::NotInstalled + ); + assert!(missing_entry.command.is_none()); + assert_eq!(missing_entry.default_args, vec!["acp"]); + } + + #[test] + fn devin_preset_is_exposed_in_the_runtime_catalog() { + use crate::managed_agents::custom_harnesses::registry_test_lock; + + // Discovery touches process-global command-resolution and the loaded + // harness registry. Serialize with the other discovery tests. + let _path_guard = crate::managed_agents::lock_path_mutex(); + let _registry_guard = registry_test_lock(); + + let entry = super::super::discover_acp_runtimes_from(None) + .into_iter() + .find(|entry| entry.id == "devin") + .expect("Devin preset should appear in the runtime catalog"); + + assert_eq!(entry.label, "Devin"); + assert_eq!(entry.default_args, vec!["acp"]); + assert_eq!(entry.install_instructions_url, "https://docs.devin.ai/cli"); + assert_eq!(entry.source, HarnessSource::Preset); + } + + #[test] + fn adapter_missing_when_underlying_cli_present() { + let entry = preset_catalog_entry(&ADAPTER_PRESET, |command| { + (command == "amp").then(|| PathBuf::from("/usr/local/bin/amp")) + }); + assert_eq!(entry.availability, AcpAvailabilityStatus::AdapterMissing); + assert!(entry.command.is_none()); + assert!(entry.binary_path.is_none()); + assert_eq!( + entry.underlying_cli_path.as_deref(), + Some("/usr/local/bin/amp") + ); + assert!(!entry.requires_external_cli); + assert_eq!(entry.install_hint, "Install the amp-acp npm adapter."); + } + + #[test] + fn not_installed_when_adapter_and_cli_are_missing() { + let entry = preset_catalog_entry(&ADAPTER_PRESET, |_| None); + assert_eq!(entry.availability, AcpAvailabilityStatus::NotInstalled); + assert!(entry.underlying_cli_path.is_none()); + assert!(!entry.requires_external_cli); + } + + #[test] + fn available_when_adapter_and_cli_are_present() { + let entry = preset_catalog_entry(&ADAPTER_PRESET, |command| match command { + "amp-acp" => Some(PathBuf::from("/usr/local/bin/amp-acp")), + "amp" => Some(PathBuf::from("/usr/local/bin/amp")), + _ => None, + }); + assert_eq!(entry.availability, AcpAvailabilityStatus::Available); + assert_eq!(entry.command.as_deref(), Some("amp-acp")); + assert_eq!(entry.binary_path.as_deref(), Some("/usr/local/bin/amp-acp")); + assert_eq!( + entry.underlying_cli_path.as_deref(), + Some("/usr/local/bin/amp") + ); + } + + #[test] + fn adapter_presence_is_enough_for_availability() { + let entry = preset_catalog_entry(&ADAPTER_PRESET, |command| { + (command == "amp-acp").then(|| PathBuf::from("/usr/local/bin/amp-acp")) + }); + assert_eq!(entry.availability, AcpAvailabilityStatus::Available); + assert_eq!(entry.command.as_deref(), Some("amp-acp")); + assert_eq!(entry.binary_path.as_deref(), Some("/usr/local/bin/amp-acp")); + assert!(entry.underlying_cli_path.is_none()); + } + + #[test] + fn preset_without_underlying_cli_stays_simple() { + let preset = PresetHarness { + underlying_cli: None, + ..ADAPTER_PRESET + }; + let entry = preset_catalog_entry(&preset, |_| None); + assert_eq!(entry.availability, AcpAvailabilityStatus::NotInstalled); + assert!(!entry.requires_external_cli); + assert!(entry.underlying_cli_path.is_none()); + } +} diff --git a/desktop/src-tauri/src/managed_agents/discovery/tests.rs b/desktop/src-tauri/src/managed_agents/discovery/tests.rs index 1b587dca0e..6fe6a77521 100644 --- a/desktop/src-tauri/src/managed_agents/discovery/tests.rs +++ b/desktop/src-tauri/src/managed_agents/discovery/tests.rs @@ -6,9 +6,9 @@ use super::{ codex_adapter_is_outdated, create_time_agent_command_override, default_agent_command, effective_agent_command, find_nvm_default_bin, find_via_login_shell, is_login_shell_path_uninit, is_safe_nvm_tag, managed_agent_avatar_url, normalize_agent_args, - parse_semver_tag, preset_catalog_entry, probe_codex_acp_version, record_agent_command, - refresh_login_shell_path, try_record_agent_command, PresetHarness, BUZZ_AGENT_AVATAR_URL, - CLAUDE_CODE_AVATAR_URL, CODEX_AVATAR_URL, GOOSE_AVATAR_URL, + parse_semver_tag, probe_codex_acp_version, record_agent_command, refresh_login_shell_path, + try_record_agent_command, BUZZ_AGENT_AVATAR_URL, CLAUDE_CODE_AVATAR_URL, CODEX_AVATAR_URL, + GOOSE_AVATAR_URL, }; use crate::managed_agents::AcpAvailabilityStatus; @@ -187,90 +187,6 @@ fn classifies_cli_missing_when_adapter_found_but_cli_absent() { assert_eq!(path.as_deref(), Some("/opt/homebrew/bin/codex-acp")); } -/// Amp-shaped preset: an ACP adapter (`amp-acp`) wrapping a separately -/// installed vendor CLI (`amp`). -const ADAPTER_PRESET: PresetHarness = PresetHarness { - id: "amp-test", - label: "Amp Test", - command: "amp-acp", - args: &[], - install_instructions_url: "https://example.com/install", - install_hint: "Install the amp-acp npm adapter.", - underlying_cli: Some("amp"), -}; - -#[test] -fn preset_entry_adapter_missing_when_underlying_cli_present() { - // Vendor CLI resolves, adapter does not — the state Tyler's Amp - // hand-test hit. Must NOT degrade to the misleading NotInstalled. - let entry = preset_catalog_entry(&ADAPTER_PRESET, |cmd| { - (cmd == "amp").then(|| PathBuf::from("/usr/local/bin/amp")) - }); - assert_eq!(entry.availability, AcpAvailabilityStatus::AdapterMissing); - assert!(entry.command.is_none()); - assert!(entry.binary_path.is_none()); - assert_eq!( - entry.underlying_cli_path.as_deref(), - Some("/usr/local/bin/amp") - ); - assert!(!entry.requires_external_cli); - assert_eq!(entry.install_hint, "Install the amp-acp npm adapter."); -} - -#[test] -fn preset_entry_not_installed_when_both_missing() { - let entry = preset_catalog_entry(&ADAPTER_PRESET, |_| None); - assert_eq!(entry.availability, AcpAvailabilityStatus::NotInstalled); - assert!(entry.underlying_cli_path.is_none()); - assert!(!entry.requires_external_cli); -} - -#[test] -fn preset_entry_available_when_adapter_and_cli_present() { - let entry = preset_catalog_entry(&ADAPTER_PRESET, |cmd| match cmd { - "amp-acp" => Some(PathBuf::from("/usr/local/bin/amp-acp")), - "amp" => Some(PathBuf::from("/usr/local/bin/amp")), - _ => None, - }); - assert_eq!(entry.availability, AcpAvailabilityStatus::Available); - assert_eq!(entry.command.as_deref(), Some("amp-acp")); - assert_eq!(entry.binary_path.as_deref(), Some("/usr/local/bin/amp-acp")); - assert_eq!( - entry.underlying_cli_path.as_deref(), - Some("/usr/local/bin/amp") - ); -} - -#[test] -fn preset_entry_stays_available_when_adapter_present_but_cli_absent() { - // Wren's regression guard: today an `amp-acp` install without `amp` - // is Available and selectable. Feeding underlying_cli through the - // FULL classify_runtime predicate would flip this to CliMissing - // (unselectable, with backwards install copy) — the adapter-missing - // arm is the only one presets consume. - let entry = preset_catalog_entry(&ADAPTER_PRESET, |cmd| { - (cmd == "amp-acp").then(|| PathBuf::from("/usr/local/bin/amp-acp")) - }); - assert_eq!(entry.availability, AcpAvailabilityStatus::Available); - assert_eq!(entry.command.as_deref(), Some("amp-acp")); - assert_eq!(entry.binary_path.as_deref(), Some("/usr/local/bin/amp-acp")); - assert!(entry.underlying_cli_path.is_none()); -} - -#[test] -fn preset_entry_without_underlying_cli_stays_simple() { - // Most presets: the command IS the vendor CLI. No external-CLI flag, - // absent command means plain NotInstalled. - let preset = PresetHarness { - underlying_cli: None, - ..ADAPTER_PRESET - }; - let entry = preset_catalog_entry(&preset, |_| None); - assert_eq!(entry.availability, AcpAvailabilityStatus::NotInstalled); - assert!(!entry.requires_external_cli); - assert!(entry.underlying_cli_path.is_none()); -} - fn persona_with_runtime(id: &str, runtime: Option<&str>) -> crate::managed_agents::AgentDefinition { crate::managed_agents::AgentDefinition { id: id.to_string(), diff --git a/desktop/src/features/onboarding/ui/RuntimeIcon.tsx b/desktop/src/features/onboarding/ui/RuntimeIcon.tsx index f9c201d115..5b247c31f7 100644 --- a/desktop/src/features/onboarding/ui/RuntimeIcon.tsx +++ b/desktop/src/features/onboarding/ui/RuntimeIcon.tsx @@ -17,6 +17,7 @@ const RUNTIME_LOGOS: Record = { // Public-path logos for bundled presets. Served from /harness-logos/ at runtime. // Keys match the preset `id` values emitted by the backend PRESET_HARNESSES. export const PRESET_LOGOS: Record = { + devin: "/harness-logos/devin.svg", omp: "/harness-logos/omp.svg", grok: "/harness-logos/grok.svg", opencode: "/harness-logos/opencode.svg", diff --git a/desktop/src/features/onboarding/ui/presetLogos.test.mjs b/desktop/src/features/onboarding/ui/presetLogos.test.mjs index 0ff2193155..7a5df620da 100644 --- a/desktop/src/features/onboarding/ui/presetLogos.test.mjs +++ b/desktop/src/features/onboarding/ui/presetLogos.test.mjs @@ -24,15 +24,15 @@ const desktopRoot = path.resolve( "../../../..", ); -const discoveryRs = readFileSync( - path.join(desktopRoot, "src-tauri/src/managed_agents/discovery.rs"), +const presetsRs = readFileSync( + path.join(desktopRoot, "src-tauri/src/managed_agents/discovery/presets.rs"), "utf8", ); -const presetBlock = discoveryRs.match( +const presetBlock = presetsRs.match( /const PRESET_HARNESSES: &\[PresetHarness\] = &\[([\s\S]*?)\n\];/, ); -assert.ok(presetBlock, "could not locate PRESET_HARNESSES in discovery.rs"); +assert.ok(presetBlock, "could not locate PRESET_HARNESSES in presets.rs"); const presetIds = [...presetBlock[1].matchAll(/^\s{8}id: "([^"]+)",$/gm)].map( (match) => match[1],