fix(desktop): clear resolve cache on re-run, per-provider concurrency, dynamic install hints

Re-run button now clears the binary resolve cache so it actually
re-probes the filesystem instead of returning stale cached results.

Replace global AtomicBool install guard with per-provider
Mutex<HashSet<String>> so concurrent installs for different providers
(e.g. claude + codex) can proceed in parallel.

Add codex CLI install command (npm install -g @openai/codex) so
clicking Install when both CLI and adapter are missing installs both.

Compute install_hint dynamically based on availability state so the
UI accurately describes what the Install button will do.
This commit is contained in:
Will Pfleger
2026-05-22 19:26:15 -04:00
parent 4696cc4cd6
commit d4b9ac16d8
3 changed files with 61 additions and 16 deletions

View File

@@ -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

View File

@@ -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<std::collections::HashSet<String>> {
use std::collections::HashSet;
use std::sync::{Mutex, OnceLock};
static ACTIVE: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
ACTIVE.get_or_init(|| Mutex::new(HashSet::new()))
}
#[tauri::command]
pub fn discover_acp_providers() -> Vec<AcpProviderCatalogEntry> {
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<InstallRuntimeRe
/// Err(_) = infrastructure failure (panic, concurrency guard).
/// Ok({success: false}) = an install step failed (stderr captured in steps).
fn install_acp_runtime_blocking(provider_id: &str) -> Result<InstallRuntimeResult, String> {
// 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}"))?;

View File

@@ -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<AcpProviderCatalogEntry> {
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<AcpProviderCatalogEntry> {
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,