diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs index 8033b68669..45336587a7 100644 --- a/desktop/scripts/check-file-sizes.mjs +++ b/desktop/scripts/check-file-sizes.mjs @@ -50,7 +50,7 @@ const overrides = new Map([ ["src-tauri/src/commands/messages.rs", 510], // feed multi-query + NIP-50 search + forum thread resolution + thread ref + reactions via REQ ["src-tauri/src/nostr_convert.rs", 1150], // 12 Nostr event→model converters (channels, profiles, members, notes, search, agents, relay members) + rank_user_search_results helper for NIP-50 user search + 33 unit tests ["src-tauri/src/managed_agents/runtime.rs", 1110], // ... + respond-to gate env (SPROUT_ACP_RESPOND_TO[_ALLOWLIST]) + per-mode env builder + tests + persona/agent env_vars spawn merge (helper + tests now in env_vars.rs) - ["src-tauri/src/managed_agents/discovery.rs", 610], // KNOWN_ACP_PROVIDERS catalog + resolve_command cache + login_shell_path + classify_provider + discover_acp_providers (three-state: Available/AdapterMissing/NotInstalled) + known_acp_provider/known_acp_provider_exact + normalize_agent_args + 14 unit tests + ["src-tauri/src/managed_agents/discovery.rs", 645], // KNOWN_ACP_PROVIDERS catalog + resolve_command cache + login_shell_path + classify_provider + discover_acp_providers (three-state with dynamic install_hint) + known_acp_provider/known_acp_provider_exact + normalize_agent_args + 14 unit tests ["src-tauri/src/managed_agents/types.rs", 745], // ManagedAgentRecord/Summary + Create/Update request structs + AcpProviderCatalogEntry + InstallRuntimeResult + RespondTo enum + validate_respond_to_allowlist + tests + persona/agent env_vars field ["src-tauri/src/managed_agents/backend.rs", 700], // provider IPC, validation, discovery, binary resolution + tests + redact_secrets_with for user env values + env_secrets_from_request + redact_env_values_in (shared with model discovery) ["src/features/huddle/HuddleContext.tsx", 650], // huddle lifecycle context + joinHuddle + connectAndSetupMedia shared helper + activeSpeakers/isReconnecting state + PTT (reusable AudioContext) + TTS subscription + mic level analyser (10fps throttle) + agent pubkey refresh diff --git a/desktop/src-tauri/src/commands/agent_discovery.rs b/desktop/src-tauri/src/commands/agent_discovery.rs index c4762a5b4b..f8319c039e 100644 --- a/desktop/src-tauri/src/commands/agent_discovery.rs +++ b/desktop/src-tauri/src/commands/agent_discovery.rs @@ -1,5 +1,4 @@ use std::io::Read; -use std::sync::atomic::Ordering::{Acquire, Relaxed}; use tauri::{AppHandle, State}; use crate::{ @@ -13,11 +12,16 @@ use crate::{ relay::query_relay, }; -static INSTALL_IN_PROGRESS: std::sync::atomic::AtomicBool = - std::sync::atomic::AtomicBool::new(false); +fn active_installs() -> &'static std::sync::Mutex> { + use std::collections::HashSet; + use std::sync::{Mutex, OnceLock}; + static ACTIVE: OnceLock>> = OnceLock::new(); + ACTIVE.get_or_init(|| Mutex::new(HashSet::new())) +} #[tauri::command] pub fn discover_acp_providers() -> Vec { + crate::managed_agents::clear_resolve_cache(); crate::managed_agents::discover_acp_providers() } @@ -31,18 +35,25 @@ pub async fn install_acp_runtime(provider_id: String) -> Result Result { - // Prevent concurrent installs. - INSTALL_IN_PROGRESS - .compare_exchange(false, true, Acquire, Relaxed) - .map_err(|_| "an install is already in progress".to_string())?; - - struct Guard; - impl Drop for Guard { - fn drop(&mut self) { - INSTALL_IN_PROGRESS.store(false, std::sync::atomic::Ordering::Release); + // Prevent concurrent installs for the same provider. + { + let mut set = active_installs() + .lock() + .map_err(|_| "install lock poisoned".to_string())?; + if !set.insert(provider_id.to_string()) { + return Err(format!("an install is already in progress for {provider_id}")); } } - let _guard = Guard; + + struct Guard(String); + impl Drop for Guard { + fn drop(&mut self) { + if let Ok(mut set) = active_installs().lock() { + set.remove(&self.0); + } + } + } + let _guard = Guard(provider_id.to_string()); let provider = crate::managed_agents::known_acp_provider_exact(provider_id) .ok_or_else(|| format!("unknown provider: {provider_id}"))?; diff --git a/desktop/src-tauri/src/managed_agents/discovery.rs b/desktop/src-tauri/src/managed_agents/discovery.rs index bd3c2ba6c1..29d1fd7cfe 100644 --- a/desktop/src-tauri/src/managed_agents/discovery.rs +++ b/desktop/src-tauri/src/managed_agents/discovery.rs @@ -95,7 +95,7 @@ const KNOWN_ACP_PROVIDERS: &[KnownAcpProvider] = &[ mcp_command: None, mcp_hooks: false, underlying_cli: Some("codex"), - cli_install_commands: &[], + cli_install_commands: &["npm install -g @openai/codex"], adapter_install_commands: &["npm install -g @zed-industries/codex-acp"], install_instructions_url: "https://github.com/zed-industries/codex-acp", install_hint: "Install the Codex ACP adapter via npm.", @@ -446,6 +446,40 @@ pub fn discover_acp_providers() -> Vec { let can_auto_install = !provider.cli_install_commands.is_empty() || !provider.adapter_install_commands.is_empty(); + let install_hint = if availability == AcpAvailabilityStatus::Available { + provider.install_hint.to_string() + } else if provider.underlying_cli.is_some() { + let has_cli_cmds = !provider.cli_install_commands.is_empty(); + let has_adapter_cmds = !provider.adapter_install_commands.is_empty(); + match availability { + AcpAvailabilityStatus::AdapterMissing => { + if has_adapter_cmds { + format!("Install the {} ACP adapter via the Install button, or follow the manual instructions.", provider.label) + } else { + provider.install_hint.to_string() + } + } + AcpAvailabilityStatus::NotInstalled => match (has_cli_cmds, has_adapter_cmds) { + (true, true) => format!( + "Install will set up the {} CLI and its ACP adapter.", + provider.label + ), + (true, false) => format!( + "Install will set up the {} CLI. Install the ACP adapter manually.", + provider.label + ), + (false, true) => format!( + "Install will set up the ACP adapter. Install the {} CLI manually.", + provider.label + ), + (false, false) => provider.install_hint.to_string(), + }, + AcpAvailabilityStatus::Available => unreachable!(), + } + } else { + provider.install_hint.to_string() + }; + AcpProviderCatalogEntry { id: provider.id.to_string(), label: provider.label.to_string(), @@ -455,7 +489,7 @@ pub fn discover_acp_providers() -> Vec { binary_path, default_args, mcp_command: provider.mcp_command.map(str::to_string), - install_hint: provider.install_hint.to_string(), + install_hint, install_instructions_url: provider.install_instructions_url.to_string(), can_auto_install, underlying_cli_path,