perf(desktop): move blocking commands off the UI thread

Finding [L1] commands/** — several Tauri commands still performed filesystem, process discovery, sqlite/local-storage reads, zip/json parsing, managed-agent store work, and workspace symlink updates synchronously on the command thread. Convert the affected commands to async command handlers with explicit spawn_blocking around the blocking sections so the UI thread is not responsible for those operations.

Reacquire AppState from the owned AppHandle inside blocking closures instead of moving borrowed State<'_, AppState> or non-Send guards across await points. Keep existing store mutex serialization inside the blocking closures and preserve command-specific ordering: identity import persists before swapping in-memory keys, workspace apply validates before mutation and persists the effective repos dir before symlink updates, and repos-dir-error emissions still use the AppHandle.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
This commit is contained in:
npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta
2026-07-08 10:59:03 -04:00
co-authored by Tyler Longwell
parent 4aac3916a2
commit 77bd0e700f
13 changed files with 481 additions and 396 deletions
@@ -165,8 +165,10 @@ fn retag_persona_default(field: &mut Option<NormalizedField>) {
/// Returns `null` when the runtime has no config file or it cannot be parsed. /// Returns `null` when the runtime has no config file or it cannot be parsed.
/// Currently only "goose" is supported; other runtimes return `null`. /// Currently only "goose" is supported; other runtimes return `null`.
#[tauri::command] #[tauri::command]
pub fn get_runtime_file_config(runtime_id: String) -> Option<RuntimeFileConfigSubset> { pub async fn get_runtime_file_config(
match runtime_id.as_str() { runtime_id: String,
) -> Result<Option<RuntimeFileConfigSubset>, String> {
tokio::task::spawn_blocking(move || match runtime_id.as_str() {
"goose" => { "goose" => {
let cfg = read_goose_file_config()?; let cfg = read_goose_file_config()?;
let satisfied_env_keys = cfg let satisfied_env_keys = cfg
@@ -182,7 +184,9 @@ pub fn get_runtime_file_config(runtime_id: String) -> Option<RuntimeFileConfigSu
}) })
} }
_ => None, _ => None,
} })
.await
.map_err(|e| format!("spawn_blocking failed: {e}"))
} }
/// Return the key names of all non-empty baked build env vars. /// Return the key names of all non-empty baked build env vars.
@@ -20,9 +20,13 @@ fn active_installs() -> &'static std::sync::Mutex<std::collections::HashSet<Stri
} }
#[tauri::command] #[tauri::command]
pub fn discover_acp_providers() -> Vec<AcpRuntimeCatalogEntry> { pub async fn discover_acp_providers() -> Result<Vec<AcpRuntimeCatalogEntry>, String> {
tokio::task::spawn_blocking(|| {
crate::managed_agents::clear_resolve_cache(); crate::managed_agents::clear_resolve_cache();
crate::managed_agents::discover_acp_runtimes() crate::managed_agents::discover_acp_runtimes()
})
.await
.map_err(|e| format!("spawn_blocking failed: {e}"))
} }
#[tauri::command] #[tauri::command]
@@ -292,9 +296,10 @@ fn floor_char_boundary(s: &str, mut index: usize) -> usize {
} }
#[tauri::command] #[tauri::command]
pub fn discover_managed_agent_prereqs( pub async fn discover_managed_agent_prereqs(
input: DiscoverManagedAgentPrereqsRequest, input: DiscoverManagedAgentPrereqsRequest,
) -> ManagedAgentPrereqsInfo { ) -> Result<ManagedAgentPrereqsInfo, String> {
tokio::task::spawn_blocking(move || {
let acp_command = input let acp_command = input
.acp_command .acp_command
.as_deref() .as_deref()
@@ -312,6 +317,9 @@ pub fn discover_managed_agent_prereqs(
acp: command_availability(acp_command), acp: command_availability(acp_command),
mcp: command_availability(mcp_command), mcp: command_availability(mcp_command),
} }
})
.await
.map_err(|e| format!("spawn_blocking failed: {e}"))
} }
#[tauri::command] #[tauri::command]
@@ -0,0 +1,40 @@
use tauri::{AppHandle, Manager};
use crate::{
app_state::AppState,
managed_agents::{
load_managed_agents, managed_agent_log_path, read_log_tail, BackendKind,
ManagedAgentLogResponse,
},
};
#[tauri::command]
pub async fn get_managed_agent_log(
pubkey: String,
line_count: Option<u32>,
app: AppHandle,
) -> Result<ManagedAgentLogResponse, String> {
tokio::task::spawn_blocking(move || {
let state = app.state::<AppState>();
let _store_guard = state
.managed_agents_store_lock
.lock()
.map_err(|error| error.to_string())?;
let records = load_managed_agents(&app)?;
let record = records
.iter()
.find(|record| record.pubkey == pubkey)
.ok_or_else(|| format!("agent {pubkey} not found"))?;
if record.backend != BackendKind::Local {
return Err("logs are not available for remote agents".to_string());
}
let log_path = managed_agent_log_path(&app, &pubkey)?;
Ok(ManagedAgentLogResponse {
content: read_log_tail(&log_path, line_count.unwrap_or(120) as usize)?,
log_path: log_path.display().to_string(),
})
})
.await
.map_err(|e| format!("spawn_blocking failed: {e}"))?
}
@@ -1,7 +1,8 @@
use crate::managed_agents::{discover_provider_candidates, invoke_provider, BackendProviderInfo}; use crate::managed_agents::{discover_provider_candidates, invoke_provider, BackendProviderInfo};
#[tauri::command] #[tauri::command]
pub fn discover_backend_providers() -> Vec<BackendProviderInfo> { pub async fn discover_backend_providers() -> Result<Vec<BackendProviderInfo>, String> {
tokio::task::spawn_blocking(|| {
discover_provider_candidates() discover_provider_candidates()
.into_iter() .into_iter()
.map(|(id, path)| BackendProviderInfo { .map(|(id, path)| BackendProviderInfo {
@@ -9,6 +10,9 @@ pub fn discover_backend_providers() -> Vec<BackendProviderInfo> {
binary_path: path.display().to_string(), binary_path: path.display().to_string(),
}) })
.collect() .collect()
})
.await
.map_err(|e| format!("spawn_blocking failed: {e}"))
} }
#[tauri::command] #[tauri::command]
@@ -1,4 +1,4 @@
use tauri::{AppHandle, State}; use tauri::{AppHandle, Manager};
use crate::{ use crate::{
app_state::AppState, app_state::AppState,
@@ -11,12 +11,13 @@ use crate::{
}; };
#[tauri::command] #[tauri::command]
pub fn set_managed_agent_start_on_app_launch( pub async fn set_managed_agent_start_on_app_launch(
pubkey: String, pubkey: String,
start_on_app_launch: bool, start_on_app_launch: bool,
app: AppHandle, app: AppHandle,
state: State<'_, AppState>,
) -> Result<ManagedAgentSummary, String> { ) -> Result<ManagedAgentSummary, String> {
tokio::task::spawn_blocking(move || {
let state = app.state::<AppState>();
let _store_guard = state let _store_guard = state
.managed_agents_store_lock .managed_agents_store_lock
.lock() .lock()
@@ -49,4 +50,7 @@ pub fn set_managed_agent_start_on_app_launch(
.ok_or_else(|| format!("agent {pubkey} not found"))?; .ok_or_else(|| format!("agent {pubkey} not found"))?;
let personas = load_personas(&app).unwrap_or_default(); let personas = load_personas(&app).unwrap_or_default();
build_managed_agent_summary(&app, record, &runtimes, &personas) build_managed_agent_summary(&app, record, &runtimes, &personas)
})
.await
.map_err(|e| format!("spawn_blocking failed: {e}"))?
} }
+6 -34
View File
@@ -6,13 +6,12 @@ use crate::{
managed_agents::{ managed_agents::{
build_managed_agent_summary, current_instance_id, discover_provider_candidates, build_managed_agent_summary, current_instance_id, discover_provider_candidates,
ensure_persona_is_active, find_managed_agent_mut, load_managed_agents, load_personas, ensure_persona_is_active, find_managed_agent_mut, load_managed_agents, load_personas,
managed_agent_avatar_url, managed_agent_log_path, managed_agents_base_dir, managed_agent_avatar_url, managed_agents_base_dir, normalize_agent_args, provider_deploy,
normalize_agent_args, provider_deploy, read_log_tail, resolve_provider_binary, resolve_provider_binary, save_managed_agents, start_managed_agent_process,
save_managed_agents, start_managed_agent_process, stop_managed_agent_process, stop_managed_agent_process, sync_managed_agent_processes, try_regenerate_nest,
sync_managed_agent_processes, try_regenerate_nest, validate_provider_config, BackendKind, validate_provider_config, BackendKind, CreateManagedAgentRequest,
CreateManagedAgentRequest, CreateManagedAgentResponse, ManagedAgentLogResponse, CreateManagedAgentResponse, ManagedAgentRecord, ManagedAgentSummary, RelayMeshConfig,
ManagedAgentRecord, ManagedAgentSummary, RelayMeshConfig, DEFAULT_ACP_COMMAND, DEFAULT_ACP_COMMAND, DEFAULT_AGENT_PARALLELISM, DEFAULT_AGENT_TURN_TIMEOUT_SECONDS,
DEFAULT_AGENT_PARALLELISM, DEFAULT_AGENT_TURN_TIMEOUT_SECONDS,
}, },
relay::{relay_ws_url_with_override, sync_managed_agent_profile}, relay::{relay_ws_url_with_override, sync_managed_agent_profile},
util::now_iso, util::now_iso,
@@ -1250,33 +1249,6 @@ pub async fn delete_managed_agent(
.map_err(|e| format!("spawn_blocking failed: {e}"))? .map_err(|e| format!("spawn_blocking failed: {e}"))?
} }
#[tauri::command]
pub fn get_managed_agent_log(
pubkey: String,
line_count: Option<u32>,
app: AppHandle,
state: State<'_, AppState>,
) -> Result<ManagedAgentLogResponse, String> {
let _store_guard = state
.managed_agents_store_lock
.lock()
.map_err(|error| error.to_string())?;
let records = load_managed_agents(&app)?;
let record = records
.iter()
.find(|record| record.pubkey == pubkey)
.ok_or_else(|| format!("agent {pubkey} not found"))?;
if record.backend != BackendKind::Local {
return Err("logs are not available for remote agents".to_string());
}
let log_path = managed_agent_log_path(&app, &pubkey)?;
Ok(ManagedAgentLogResponse {
content: read_log_tail(&log_path, line_count.unwrap_or(120) as usize)?,
log_path: log_path.display().to_string(),
})
}
// Remote agent shutdown is handled entirely by the frontend: // Remote agent shutdown is handled entirely by the frontend:
// 1. Frontend sends "!shutdown" @mention via WebSocket (signed by user's key) // 1. Frontend sends "!shutdown" @mention via WebSocket (signed by user's key)
// 2. Harness sees it, exits gracefully, sets presence to "offline" // 2. Harness sees it, exits gracefully, sets presence to "offline"
@@ -1,4 +1,4 @@
use tauri::{AppHandle, State}; use tauri::{AppHandle, Manager};
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
@@ -44,23 +44,25 @@ fn validate_visibility(value: &str) -> Result<(), String> {
} }
#[tauri::command] #[tauri::command]
pub fn list_channel_templates( pub async fn list_channel_templates(app: AppHandle) -> Result<Vec<ChannelTemplateRecord>, String> {
app: AppHandle, tokio::task::spawn_blocking(move || {
state: State<'_, AppState>, let state = app.state::<AppState>();
) -> Result<Vec<ChannelTemplateRecord>, String> {
let _store_guard = state let _store_guard = state
.channel_templates_store_lock .channel_templates_store_lock
.lock() .lock()
.map_err(|error| error.to_string())?; .map_err(|error| error.to_string())?;
load_channel_templates(&app) load_channel_templates(&app)
})
.await
.map_err(|e| format!("spawn_blocking failed: {e}"))?
} }
#[tauri::command] #[tauri::command]
pub fn create_channel_template( pub async fn create_channel_template(
input: CreateChannelTemplateRequest, input: CreateChannelTemplateRequest,
app: AppHandle, app: AppHandle,
state: State<'_, AppState>,
) -> Result<ChannelTemplateRecord, String> { ) -> Result<ChannelTemplateRecord, String> {
tokio::task::spawn_blocking(move || {
let name = trim_required(&input.name, "Template name")?; let name = trim_required(&input.name, "Template name")?;
let description = trim_optional(input.description); let description = trim_optional(input.description);
let canvas_template = trim_optional(input.canvas_template); let canvas_template = trim_optional(input.canvas_template);
@@ -70,6 +72,7 @@ pub fn create_channel_template(
validate_visibility(&visibility)?; validate_visibility(&visibility)?;
let now = now_iso(); let now = now_iso();
let state = app.state::<AppState>();
let _store_guard = state let _store_guard = state
.channel_templates_store_lock .channel_templates_store_lock
.lock() .lock()
@@ -92,14 +95,17 @@ pub fn create_channel_template(
templates.push(template.clone()); templates.push(template.clone());
save_channel_templates(&app, &templates)?; save_channel_templates(&app, &templates)?;
Ok(template) Ok(template)
})
.await
.map_err(|e| format!("spawn_blocking failed: {e}"))?
} }
#[tauri::command] #[tauri::command]
pub fn update_channel_template( pub async fn update_channel_template(
input: UpdateChannelTemplateRequest, input: UpdateChannelTemplateRequest,
app: AppHandle, app: AppHandle,
state: State<'_, AppState>,
) -> Result<ChannelTemplateRecord, String> { ) -> Result<ChannelTemplateRecord, String> {
tokio::task::spawn_blocking(move || {
let name = trim_required(&input.name, "Template name")?; let name = trim_required(&input.name, "Template name")?;
let description = trim_optional(input.description); let description = trim_optional(input.description);
let canvas_template = trim_optional(input.canvas_template); let canvas_template = trim_optional(input.canvas_template);
@@ -108,6 +114,7 @@ pub fn update_channel_template(
validate_channel_type(&channel_type)?; validate_channel_type(&channel_type)?;
validate_visibility(&visibility)?; validate_visibility(&visibility)?;
let state = app.state::<AppState>();
let _store_guard = state let _store_guard = state
.channel_templates_store_lock .channel_templates_store_lock
.lock() .lock()
@@ -129,14 +136,15 @@ pub fn update_channel_template(
let updated = template.clone(); let updated = template.clone();
save_channel_templates(&app, &templates)?; save_channel_templates(&app, &templates)?;
Ok(updated) Ok(updated)
})
.await
.map_err(|e| format!("spawn_blocking failed: {e}"))?
} }
#[tauri::command] #[tauri::command]
pub fn delete_channel_template( pub async fn delete_channel_template(id: String, app: AppHandle) -> Result<(), String> {
id: String, tokio::task::spawn_blocking(move || {
app: AppHandle, let state = app.state::<AppState>();
state: State<'_, AppState>,
) -> Result<(), String> {
let _store_guard = state let _store_guard = state
.channel_templates_store_lock .channel_templates_store_lock
.lock() .lock()
@@ -149,16 +157,20 @@ pub fn delete_channel_template(
validate_channel_template_deletion(template)?; validate_channel_template_deletion(template)?;
templates.retain(|record| record.id != id); templates.retain(|record| record.id != id);
save_channel_templates(&app, &templates) save_channel_templates(&app, &templates)
})
.await
.map_err(|e| format!("spawn_blocking failed: {e}"))?
} }
#[tauri::command] #[tauri::command]
pub fn duplicate_channel_template( pub async fn duplicate_channel_template(
id: String, id: String,
app: AppHandle, app: AppHandle,
state: State<'_, AppState>,
) -> Result<ChannelTemplateRecord, String> { ) -> Result<ChannelTemplateRecord, String> {
tokio::task::spawn_blocking(move || {
let now = now_iso(); let now = now_iso();
let state = app.state::<AppState>();
let _store_guard = state let _store_guard = state
.channel_templates_store_lock .channel_templates_store_lock
.lock() .lock()
@@ -182,4 +194,7 @@ pub fn duplicate_channel_template(
templates.push(duplicate.clone()); templates.push(duplicate.clone());
save_channel_templates(&app, &templates)?; save_channel_templates(&app, &templates)?;
Ok(duplicate) Ok(duplicate)
})
.await
.map_err(|e| format!("spawn_blocking failed: {e}"))?
} }
+9 -4
View File
@@ -165,15 +165,16 @@ pub fn get_nsec(state: State<'_, AppState>) -> Result<String, String> {
} }
#[tauri::command] #[tauri::command]
pub fn import_identity( pub async fn import_identity(
nsec: String, nsec: String,
app_handle: tauri::AppHandle, app_handle: tauri::AppHandle,
state: State<'_, AppState>,
) -> Result<IdentityInfo, String> { ) -> Result<IdentityInfo, String> {
tokio::task::spawn_blocking(move || {
let trimmed = nsec.trim(); let trimmed = nsec.trim();
let keys = Keys::parse(trimmed).map_err(|e| format!("Invalid private key: {e}"))?; let keys = Keys::parse(trimmed).map_err(|e| format!("Invalid private key: {e}"))?;
// Persist to identity.key // Persist to identity.key before swapping in-memory state. If the disk
// write fails, the running app keeps the old identity.
let data_dir = app_handle let data_dir = app_handle
.path() .path()
.app_data_dir() .app_data_dir()
@@ -182,7 +183,8 @@ pub fn import_identity(
let key_path = data_dir.join("identity.key"); let key_path = data_dir.join("identity.key");
crate::app_state::save_key_file(&key_path, &keys)?; crate::app_state::save_key_file(&key_path, &keys)?;
// Update in-memory keys // Update in-memory keys only after persistence succeeds.
let state = app_handle.state::<AppState>();
let pubkey = keys.public_key(); let pubkey = keys.public_key();
*state.keys.lock().map_err(|e| e.to_string())? = keys; *state.keys.lock().map_err(|e| e.to_string())? = keys;
@@ -202,6 +204,9 @@ pub fn import_identity(
pubkey: pubkey_hex, pubkey: pubkey_hex,
display_name, display_name,
}) })
})
.await
.map_err(|e| format!("spawn_blocking failed: {e}"))?
} }
#[tauri::command] #[tauri::command]
@@ -171,10 +171,12 @@ fn merge_legacy_workspace_storage(
/// under `~/Library/WebKit/<identifier>/...` on macOS and is not included in the /// under `~/Library/WebKit/<identifier>/...` on macOS and is not included in the
/// app data directory. /// app data directory.
#[tauri::command] #[tauri::command]
pub fn get_legacy_workspace_storage( pub async fn get_legacy_workspace_storage(
app: tauri::AppHandle, app: tauri::AppHandle,
) -> Result<LegacyWorkspaceStorage, String> { ) -> Result<LegacyWorkspaceStorage, String> {
let Some(identifier) = legacy_identifier(&app.config().identifier) else { let identifier = app.config().identifier.clone();
tokio::task::spawn_blocking(move || {
let Some(identifier) = legacy_identifier(&identifier) else {
return Ok(LegacyWorkspaceStorage::default()); return Ok(LegacyWorkspaceStorage::default());
}; };
let Some(root) = legacy_webkit_data_root(&identifier) else { let Some(root) = legacy_webkit_data_root(&identifier) else {
@@ -199,6 +201,9 @@ pub fn get_legacy_workspace_storage(
} }
Ok(result) Ok(result)
})
.await
.map_err(|e| format!("spawn_blocking failed: {e}"))?
} }
#[cfg(test)] #[cfg(test)]
+2
View File
@@ -1,5 +1,6 @@
mod agent_config; mod agent_config;
mod agent_discovery; mod agent_discovery;
mod agent_logs;
mod agent_metric_archive; mod agent_metric_archive;
mod agent_models; mod agent_models;
mod agent_providers; mod agent_providers;
@@ -43,6 +44,7 @@ mod workspace;
pub use agent_config::*; pub use agent_config::*;
pub use agent_discovery::*; pub use agent_discovery::*;
pub use agent_logs::*;
pub use agent_metric_archive::*; pub use agent_metric_archive::*;
pub use agent_models::*; pub use agent_models::*;
pub use agent_providers::*; pub use agent_providers::*;
+17 -4
View File
@@ -1,4 +1,4 @@
use tauri::{AppHandle, Emitter, State}; use tauri::{AppHandle, Emitter, Manager, State};
use uuid::Uuid; use uuid::Uuid;
use super::export_util::save_json_with_dialog; use super::export_util::save_json_with_dialog;
@@ -466,10 +466,18 @@ pub async fn delete_persona(id: String, app: AppHandle) -> Result<(), String> {
/// a pending local edit leaves the local record — and its queued publish — /// a pending local edit leaves the local record — and its queued publish —
/// untouched. /// untouched.
#[tauri::command] #[tauri::command]
pub fn reconcile_inbound_persona_event( pub async fn reconcile_inbound_persona_event(
event_json: String,
app: AppHandle,
) -> Result<(), String> {
tokio::task::spawn_blocking(move || reconcile_inbound_persona_event_blocking(event_json, app))
.await
.map_err(|e| format!("spawn_blocking failed: {e}"))?
}
fn reconcile_inbound_persona_event_blocking(
event_json: String, event_json: String,
app: AppHandle, app: AppHandle,
state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
use crate::managed_agents::{ use crate::managed_agents::{
agent_events::managed_agent_content_from_event, agent_events::managed_agent_content_from_event,
@@ -482,6 +490,7 @@ pub fn reconcile_inbound_persona_event(
use buzz_core_pkg::kind::{KIND_DELETION, KIND_MANAGED_AGENT, KIND_PERSONA, KIND_TEAM}; use buzz_core_pkg::kind::{KIND_DELETION, KIND_MANAGED_AGENT, KIND_PERSONA, KIND_TEAM};
use nostr::JsonUtil; use nostr::JsonUtil;
let state = app.state::<AppState>();
let event = nostr::Event::from_json(&event_json) let event = nostr::Event::from_json(&event_json)
.map_err(|e| format!("failed to parse inbound event: {e}"))?; .map_err(|e| format!("failed to parse inbound event: {e}"))?;
@@ -847,10 +856,11 @@ const ZIP_MAGIC: [u8; 4] = [0x50, 0x4B, 0x03, 0x04];
const JSON_OPEN_BRACE: u8 = 0x7B; const JSON_OPEN_BRACE: u8 = 0x7B;
#[tauri::command] #[tauri::command]
pub fn parse_persona_files( pub async fn parse_persona_files(
file_bytes: Vec<u8>, file_bytes: Vec<u8>,
file_name: String, file_name: String,
) -> Result<ParsePersonaFilesResult, String> { ) -> Result<ParsePersonaFilesResult, String> {
tokio::task::spawn_blocking(move || {
if file_bytes.len() > MAX_ZIP_BYTES { if file_bytes.len() > MAX_ZIP_BYTES {
return Err("File is too large (max 100 MB).".to_string()); return Err("File is too large (max 100 MB).".to_string());
} }
@@ -919,6 +929,9 @@ pub fn parse_persona_files(
"Unsupported file format. Expected .persona.md, .persona.png, .persona.json, or .zip" "Unsupported file format. Expected .persona.md, .persona.png, .persona.json, or .zip"
.to_string(), .to_string(),
) )
})
.await
.map_err(|e| format!("spawn_blocking failed: {e}"))?
} }
#[tauri::command] #[tauri::command]
+5 -1
View File
@@ -336,10 +336,11 @@ pub async fn export_team_to_json(
const MAX_TEAM_ZIP_BYTES: usize = 100 * 1024 * 1024; const MAX_TEAM_ZIP_BYTES: usize = 100 * 1024 * 1024;
#[tauri::command] #[tauri::command]
pub fn parse_team_file( pub async fn parse_team_file(
file_bytes: Vec<u8>, file_bytes: Vec<u8>,
_file_name: String, _file_name: String,
) -> Result<ParsedTeamPreview, String> { ) -> Result<ParsedTeamPreview, String> {
tokio::task::spawn_blocking(move || {
if file_bytes.is_empty() { if file_bytes.is_empty() {
return Err("File is empty.".to_string()); return Err("File is empty.".to_string());
} }
@@ -357,6 +358,9 @@ pub fn parse_team_file(
} }
parse_team_json(&file_bytes) parse_team_json(&file_bytes)
})
.await
.map_err(|e| format!("spawn_blocking failed: {e}"))?
} }
/// Parse a persona pack zip as a team: pack name → team name, personas → members. /// Parse a persona pack zip as a team: pack name → team name, personas → members.
+13 -4
View File
@@ -1,6 +1,6 @@
use nostr::Keys; use nostr::Keys;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter, State}; use tauri::{AppHandle, Emitter, Manager, State};
use crate::app_state::AppState; use crate::app_state::AppState;
use crate::managed_agents::{ use crate::managed_agents::{
@@ -71,13 +71,17 @@ pub fn get_active_workspace(state: State<'_, AppState>) -> Result<ActiveWorkspac
/// "what's a valid repos dir". An empty/whitespace value clears the override /// "what's a valid repos dir". An empty/whitespace value clears the override
/// and is valid. `Err` carries the human-readable reason for inline display. /// and is valid. `Err` carries the human-readable reason for inline display.
#[tauri::command] #[tauri::command]
pub fn validate_repos_dir(dir: String) -> Result<(), String> { pub async fn validate_repos_dir(dir: String) -> Result<(), String> {
tokio::task::spawn_blocking(move || {
let trimmed = dir.trim(); let trimmed = dir.trim();
if trimmed.is_empty() { if trimmed.is_empty() {
return Ok(()); return Ok(());
} }
let nest = nest_dir().ok_or("cannot resolve home directory for nest")?; let nest = nest_dir().ok_or("cannot resolve home directory for nest")?;
crate::managed_agents::validate_repos_dir(&nest, trimmed).map(|_| ()) crate::managed_agents::validate_repos_dir(&nest, trimmed).map(|_| ())
})
.await
.map_err(|e| format!("spawn_blocking failed: {e}"))?
} }
/// Apply a workspace's configuration to the backend session. /// Apply a workspace's configuration to the backend session.
@@ -94,13 +98,15 @@ pub fn validate_repos_dir(dir: String) -> Result<(), String> {
/// already block a bad path at Save (`validate_repos_dir`); this fallback only /// already block a bad path at Save (`validate_repos_dir`); this fallback only
/// catches a value that went bad after save (deleted dir, unmounted volume). /// catches a value that went bad after save (deleted dir, unmounted volume).
#[tauri::command] #[tauri::command]
pub fn apply_workspace( pub async fn apply_workspace(
relay_url: String, relay_url: String,
nsec: Option<String>, nsec: Option<String>,
repos_dir: Option<String>, repos_dir: Option<String>,
app: AppHandle, app: AppHandle,
state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
tokio::task::spawn_blocking(move || {
let state = app.state::<AppState>();
// ── Validate before mutating ────────────────────────────────────────── // ── Validate before mutating ──────────────────────────────────────────
let parsed_keys = match nsec.as_deref().map(str::trim).filter(|s| !s.is_empty()) { let parsed_keys = match nsec.as_deref().map(str::trim).filter(|s| !s.is_empty()) {
Some(nsec_trimmed) => { Some(nsec_trimmed) => {
@@ -163,4 +169,7 @@ pub fn apply_workspace(
try_regenerate_nest(&app); try_regenerate_nest(&app);
Ok(()) Ok(())
})
.await
.map_err(|e| format!("spawn_blocking failed: {e}"))?
} }