diff --git a/desktop/src-tauri/src/commands/agent_discovery.rs b/desktop/src-tauri/src/commands/agent_discovery.rs index 35462cffc..fe82aaf42 100644 --- a/desktop/src-tauri/src/commands/agent_discovery.rs +++ b/desktop/src-tauri/src/commands/agent_discovery.rs @@ -20,9 +20,16 @@ fn active_installs() -> &'static std::sync::Mutex Vec { - crate::managed_agents::clear_resolve_cache(); - crate::managed_agents::discover_acp_runtimes() +pub async fn discover_acp_providers() -> Result, String> { + // Discovery spawns login shells to resolve each runtime binary, which can + // take seconds. Run it off the main thread so the webview stays responsive + // (sync Tauri commands block the main thread). + tokio::task::spawn_blocking(|| { + crate::managed_agents::clear_resolve_cache(); + crate::managed_agents::discover_acp_runtimes() + }) + .await + .map_err(|e| format!("runtime discovery task panicked: {e}")) } #[tauri::command] @@ -292,26 +299,32 @@ fn floor_char_boundary(s: &str, mut index: usize) -> usize { } #[tauri::command] -pub fn discover_managed_agent_prereqs( +pub async fn discover_managed_agent_prereqs( input: DiscoverManagedAgentPrereqsRequest, -) -> ManagedAgentPrereqsInfo { - let acp_command = input - .acp_command - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()) - .unwrap_or(DEFAULT_ACP_COMMAND); - let mcp_command = input - .mcp_command - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()) - .unwrap_or(""); +) -> Result { + // Command resolution can spawn a login shell on cache miss — keep it off + // the main thread so the webview stays responsive. + tokio::task::spawn_blocking(move || { + let acp_command = input + .acp_command + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + .unwrap_or(DEFAULT_ACP_COMMAND); + let mcp_command = input + .mcp_command + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + .unwrap_or(""); - ManagedAgentPrereqsInfo { - acp: command_availability(acp_command), - mcp: command_availability(mcp_command), - } + ManagedAgentPrereqsInfo { + acp: command_availability(acp_command), + mcp: command_availability(mcp_command), + } + }) + .await + .map_err(|e| format!("prereq discovery task panicked: {e}")) } #[tauri::command] diff --git a/desktop/src-tauri/src/commands/agent_providers.rs b/desktop/src-tauri/src/commands/agent_providers.rs index a903e4bba..acdd46ac6 100644 --- a/desktop/src-tauri/src/commands/agent_providers.rs +++ b/desktop/src-tauri/src/commands/agent_providers.rs @@ -1,14 +1,20 @@ use crate::managed_agents::{discover_provider_candidates, invoke_provider, BackendProviderInfo}; #[tauri::command] -pub fn discover_backend_providers() -> Vec { - discover_provider_candidates() - .into_iter() - .map(|(id, path)| BackendProviderInfo { - id, - binary_path: path.display().to_string(), - }) - .collect() +pub async fn discover_backend_providers() -> Result, String> { + // PATH scanning hits the filesystem for every directory — run it off the + // main thread so the webview stays responsive. + tokio::task::spawn_blocking(|| { + discover_provider_candidates() + .into_iter() + .map(|(id, path)| BackendProviderInfo { + id, + binary_path: path.display().to_string(), + }) + .collect() + }) + .await + .map_err(|e| format!("provider discovery task panicked: {e}")) } #[tauri::command] diff --git a/desktop/src/features/agents/ui/CreateAgentDialog.tsx b/desktop/src/features/agents/ui/CreateAgentDialog.tsx index 12bc81700..ba8200fb4 100644 --- a/desktop/src/features/agents/ui/CreateAgentDialog.tsx +++ b/desktop/src/features/agents/ui/CreateAgentDialog.tsx @@ -1,4 +1,4 @@ -import { AlertTriangle, ChevronDown } from "lucide-react"; +import { AlertTriangle, ChevronDown, Loader2 } from "lucide-react"; import * as React from "react"; import { @@ -791,6 +791,9 @@ export function CreateAgentDialog({ size="sm" type="button" > + {isDiscoveryPending ? ( + + ) : null} {meshPreparing ? "Connecting to mesh..." : createMutation.isPending