fix(desktop): unblock UI freeze when entering the create-agent details step

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 <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
Taylor Ho
2026-07-03 02:19:45 -07:00
parent 9c71264b4e
commit 28ca228e19
3 changed files with 52 additions and 30 deletions
@@ -20,9 +20,16 @@ fn active_installs() -> &'static std::sync::Mutex<std::collections::HashSet<Stri
}
#[tauri::command]
pub fn discover_acp_providers() -> Vec<AcpRuntimeCatalogEntry> {
crate::managed_agents::clear_resolve_cache();
crate::managed_agents::discover_acp_runtimes()
pub async fn discover_acp_providers() -> Result<Vec<AcpRuntimeCatalogEntry>, 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<ManagedAgentPrereqsInfo, String> {
// 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]
@@ -1,14 +1,20 @@
use crate::managed_agents::{discover_provider_candidates, invoke_provider, BackendProviderInfo};
#[tauri::command]
pub fn discover_backend_providers() -> Vec<BackendProviderInfo> {
discover_provider_candidates()
.into_iter()
.map(|(id, path)| BackendProviderInfo {
id,
binary_path: path.display().to_string(),
})
.collect()
pub async fn discover_backend_providers() -> Result<Vec<BackendProviderInfo>, 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]
@@ -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 ? (
<Loader2 aria-hidden className="h-4 w-4 animate-spin" />
) : null}
{meshPreparing
? "Connecting to mesh..."
: createMutation.isPending