From 28ca228e19c6cc151ac9a4256c3594e8f2b291bb Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Fri, 3 Jul 2026 02:19:45 -0700 Subject: [PATCH] fix(desktop): unblock UI freeze when entering the create-agent details step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking "Blank agent" or "Use template" froze the entire webview until runtime discovery finished. The details dialog opens immediately, but on mount it fires three Tauri commands that were synchronous — and Tauri 2 runs sync commands on the main thread, so the UI blocked while login shells resolved each agent binary. - desktop/src-tauri/src/commands/agent_discovery.rs: make discover_acp_providers and discover_managed_agent_prereqs async and move their shell-spawning work into tokio::task::spawn_blocking so the main thread never blocks; both now return Result to surface a panicked task instead of unwrapping - desktop/src-tauri/src/commands/agent_providers.rs: same treatment for discover_backend_providers, which scans every PATH directory on disk for buzz-backend-* binaries - desktop/src/features/agents/ui/CreateAgentDialog.tsx: show a spinner in the submit button while discovery is pending, so the disabled button reads as "loading" rather than broken No frontend API changes needed — invokeTauri handles the Result return transparently. Also fixes the same freeze anywhere these queries fire (Edit Agent, Add Channel Bot, Settings > Doctor). Co-authored-by: Taylor Ho Signed-off-by: Taylor Ho --- .../src-tauri/src/commands/agent_discovery.rs | 55 ++++++++++++------- .../src-tauri/src/commands/agent_providers.rs | 22 +++++--- .../features/agents/ui/CreateAgentDialog.tsx | 5 +- 3 files changed, 52 insertions(+), 30 deletions(-) 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