diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d0637846..5a300946b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,8 @@ jobs: - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - name: Format check run: just fmt-check + - name: Desktop Tauri format check + run: just desktop-tauri-fmt-check - name: Clippy run: just clippy diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs index cbd2cd4d9..530d2b336 100644 --- a/desktop/scripts/check-file-sizes.mjs +++ b/desktop/scripts/check-file-sizes.mjs @@ -30,7 +30,7 @@ const rules = [ // Exceptions should stay rare and temporary. Prefer splitting files instead. const overrides = new Map([ - ["src-tauri/src/managed_agents/persona_card.rs", 760], // PNG/ZIP persona card codec + provider/model fields + 27 unit tests (~350 lines of tests) + ["src-tauri/src/managed_agents/persona_card.rs", 772], // PNG/ZIP persona card codec + provider/model fields + 27 unit tests (~350 lines of tests); rustfmt adds line breaks around long literals/builders ["src/app/AppShell.tsx", 775], ["src/features/channels/hooks.ts", 550], // canvas query + mutation hooks + DM hide mutation ["src/features/channels/ui/ChannelManagementSheet.tsx", 800], @@ -40,7 +40,7 @@ const overrides = new Map([ ["src/features/tokens/ui/TokenSettingsCard.tsx", 800], ["src/shared/api/relayClientSession.ts", 725], // durable websocket session manager with reconnect/replay/recovery state ["src/shared/api/tauri.ts", 1100], // remote agent provider API bindings + canvas API functions - ["src-tauri/src/commands/agents.rs", 820], // remote agent lifecycle routing (local + provider branches) + scope enforcement + ["src-tauri/src/commands/agents.rs", 845], // remote agent lifecycle routing (local + provider branches) + scope enforcement; rustfmt adds line breaks around long tuple/closure blocks ["src-tauri/src/managed_agents/backend.rs", 530], // provider IPC, validation, discovery, binary resolution + tests ["src/features/agents/ui/AgentsView.tsx", 790], // remote agent stop/delete + channel UUID resolution + presence-aware delete guard + persona/team import + provider/model fields ["src/features/agents/ui/CreateAgentDialog.tsx", 685], // provider selector + config form + schema-typed config coercion + required field validation + locked scopes diff --git a/desktop/src-tauri/src/app_state.rs b/desktop/src-tauri/src/app_state.rs index 81ebc01d8..7af53fd05 100644 --- a/desktop/src-tauri/src/app_state.rs +++ b/desktop/src-tauri/src/app_state.rs @@ -55,7 +55,6 @@ pub fn build_app_state() -> AppState { session_token: Mutex::new(None), managed_agents_store_lock: Mutex::new(()), managed_agent_processes: Mutex::new(HashMap::new()), - } } @@ -80,8 +79,7 @@ pub fn resolve_persisted_identity(app: &AppHandle, state: &AppState) -> Result<( .path() .app_data_dir() .map_err(|e| format!("app data dir: {e}"))?; - std::fs::create_dir_all(&data_dir) - .map_err(|e| format!("create app data dir: {e}"))?; + std::fs::create_dir_all(&data_dir).map_err(|e| format!("create app data dir: {e}"))?; let key_path = data_dir.join("identity.key"); // Try to load an existing key. @@ -103,7 +101,9 @@ pub fn resolve_persisted_identity(app: &AppHandle, state: &AppState) -> Result<( .map(|d| d.as_secs()) .unwrap_or(0); let bad_name = format!("identity.key.bad.{ts}"); - eprintln!("sprout-desktop: corrupt identity.key ({error}), quarantining to {bad_name}"); + eprintln!( + "sprout-desktop: corrupt identity.key ({error}), quarantining to {bad_name}" + ); let bad_path = data_dir.join(bad_name); if std::fs::rename(&key_path, &bad_path).is_err() { let _ = std::fs::remove_file(&key_path); @@ -125,8 +125,7 @@ pub fn resolve_persisted_identity(app: &AppHandle, state: &AppState) -> Result<( } fn load_key_file(path: &std::path::Path) -> Result { - let content = - std::fs::read_to_string(path).map_err(|e| format!("read identity.key: {e}"))?; + let content = std::fs::read_to_string(path).map_err(|e| format!("read identity.key: {e}"))?; let trimmed = content.trim(); if trimmed.is_empty() { return Err("empty identity.key".to_string()); diff --git a/desktop/src-tauri/src/commands/agent_discovery.rs b/desktop/src-tauri/src/commands/agent_discovery.rs index e93dadaa2..7821fbf81 100644 --- a/desktop/src-tauri/src/commands/agent_discovery.rs +++ b/desktop/src-tauri/src/commands/agent_discovery.rs @@ -6,7 +6,7 @@ use crate::{ managed_agents::{ command_availability, discover_local_acp_providers, AcpProviderInfo, DiscoverManagedAgentPrereqsRequest, ManagedAgentPrereqsInfo, RelayAgentInfo, - DEFAULT_ADMIN_COMMAND, DEFAULT_ACP_COMMAND, DEFAULT_MCP_COMMAND, + DEFAULT_ACP_COMMAND, DEFAULT_ADMIN_COMMAND, DEFAULT_MCP_COMMAND, }, relay::{build_authed_request, send_json_request}, }; diff --git a/desktop/src-tauri/src/commands/agents.rs b/desktop/src-tauri/src/commands/agents.rs index 4f859d04e..4150fcfd2 100644 --- a/desktop/src-tauri/src/commands/agents.rs +++ b/desktop/src-tauri/src/commands/agents.rs @@ -11,8 +11,8 @@ use crate::{ stop_managed_agent_process, sync_managed_agent_processes, validate_provider_config, BackendKind, BackendProviderInfo, CreateManagedAgentRequest, CreateManagedAgentResponse, ManagedAgentLogResponse, ManagedAgentRecord, ManagedAgentSummary, - MintManagedAgentTokenRequest, MintManagedAgentTokenResponse, DEFAULT_AGENT_ARG, - DEFAULT_ACP_COMMAND, DEFAULT_AGENT_COMMAND, DEFAULT_AGENT_PARALLELISM, + MintManagedAgentTokenRequest, MintManagedAgentTokenResponse, DEFAULT_ACP_COMMAND, + DEFAULT_AGENT_ARG, DEFAULT_AGENT_COMMAND, DEFAULT_AGENT_PARALLELISM, DEFAULT_AGENT_TURN_TIMEOUT_SECONDS, DEFAULT_MCP_COMMAND, }, relay::{relay_ws_url, sync_managed_agent_profile}, @@ -61,18 +61,17 @@ async fn deploy_to_provider( .filter(|p| p.exists()) .map(|p| p.canonicalize().unwrap_or(p)) .filter(|canonical| { - discover_provider_candidates() - .iter() - .any(|(id, cp)| id == provider_id && cp.canonicalize().ok().as_ref() == Some(canonical)) + discover_provider_candidates().iter().any(|(id, cp)| { + id == provider_id && cp.canonicalize().ok().as_ref() == Some(canonical) + }) }) .map_or_else(|| resolve_provider_binary(provider_id), Ok)?; let config_clone = config.clone(); - let deploy_result = tokio::task::spawn_blocking(move || { - provider_deploy(&bin_path, &agent_json, &config_clone) - }) - .await - .map_err(|e| format!("spawn_blocking failed: {e}"))?; + let deploy_result = + tokio::task::spawn_blocking(move || provider_deploy(&bin_path, &agent_json, &config_clone)) + .await + .map_err(|e| format!("spawn_blocking failed: {e}"))?; // Persist result under lock. let _store_guard = state @@ -152,7 +151,16 @@ pub async fn create_managed_agent( // ── Phase 1: generate keys and collect mint parameters (sync lock) ──────── // We do NOT mint here — minting is async and must happen outside the lock. - let (agent_keys, private_key_nsec, pubkey, resolved_relay_url, token_scopes, token_name, mint_token, input) = { + let ( + agent_keys, + private_key_nsec, + pubkey, + resolved_relay_url, + token_scopes, + token_name, + mint_token, + input, + ) = { let _store_guard = state .managed_agents_store_lock .lock() @@ -215,7 +223,16 @@ pub async fn create_managed_agent( .unwrap_or_else(relay_ws_url); let mint_token = input.mint_token; - (keys, private_key_nsec, pubkey, resolved_relay_url, token_scopes, token_name, mint_token, input) + ( + keys, + private_key_nsec, + pubkey, + resolved_relay_url, + token_scopes, + token_name, + mint_token, + input, + ) }; // ── Pre-Phase 2: validate provider config BEFORE any side effects ──────── @@ -231,8 +248,12 @@ pub async fn create_managed_agent( } // Enforce minimum scopes for remote agents. The harness needs users:read // to query its owner (for !shutdown). Without it, the agent is unstoppable. - const REQUIRED_PROVIDER_SCOPES: &[&str] = - &["messages:read", "messages:write", "channels:read", "users:read"]; + const REQUIRED_PROVIDER_SCOPES: &[&str] = &[ + "messages:read", + "messages:write", + "channels:read", + "users:read", + ]; for required in REQUIRED_PROVIDER_SCOPES { if !token_scopes.iter().any(|s| s == required) { return Err(format!( @@ -295,7 +316,9 @@ pub async fn create_managed_agent( // Cache the discovered binary path for deploy_to_provider. let provider_binary_path = if let BackendKind::Provider { ref id, .. } = input.backend { // Use resolve_provider_binary (discovered candidates only). - resolve_provider_binary(id).ok().map(|p| p.display().to_string()) + resolve_provider_binary(id) + .ok() + .map(|p| p.display().to_string()) } else { None }; @@ -429,9 +452,14 @@ pub async fn create_managed_agent( // Read the saved record to build the deploy payload (record has the // canonical field values after Phase 3 normalization). let agent_json = { - let _g = state.managed_agents_store_lock.lock().map_err(|e| e.to_string())?; + let _g = state + .managed_agents_store_lock + .lock() + .map_err(|e| e.to_string())?; let records = load_managed_agents(&app)?; - let rec = records.iter().find(|r| r.pubkey == pubkey) + let rec = records + .iter() + .find(|r| r.pubkey == pubkey) .ok_or_else(|| "agent disappeared".to_string())?; build_deploy_payload(rec) }; @@ -448,10 +476,18 @@ pub async fn create_managed_agent( // Rebuild summary if provider deploy may have updated backend_agent_id. let final_agent = if input.backend != BackendKind::Local && spawn_error.is_none() { - let _store_guard = state.managed_agents_store_lock.lock().map_err(|e| e.to_string())?; + let _store_guard = state + .managed_agents_store_lock + .lock() + .map_err(|e| e.to_string())?; let records = load_managed_agents(&app)?; - let runtimes = state.managed_agent_processes.lock().map_err(|e| e.to_string())?; - let record = records.iter().find(|r| r.pubkey == pubkey) + let runtimes = state + .managed_agent_processes + .lock() + .map_err(|e| e.to_string())?; + let record = records + .iter() + .find(|r| r.pubkey == pubkey) .ok_or_else(|| "agent disappeared".to_string())?; build_managed_agent_summary(&app, record, &runtimes)? } else { @@ -690,7 +726,8 @@ pub async fn mint_managed_agent_token( // Re-minting: do NOT send owner_pubkey. Ownership was established during // the first mint (create flow). Sending it again would be rejected by the // relay if the owner is already set to a different pubkey. - let minted_token = mint_token_via_api(&state, &agent_keys, &relay_url, &token_name, &scopes, None).await?; + let minted_token = + mint_token_via_api(&state, &agent_keys, &relay_url, &token_name, &scopes, None).await?; // ── Phase 3: persist new token to agent record (sync lock) ─────────────── let (agent, api_token) = { @@ -724,7 +761,10 @@ pub async fn mint_managed_agent_token( (agent, minted_token) }; - Ok(MintManagedAgentTokenResponse { agent, token: api_token }) + Ok(MintManagedAgentTokenResponse { + agent, + token: api_token, + }) } #[tauri::command] @@ -739,7 +779,9 @@ pub fn get_managed_agent_log( .lock() .map_err(|error| error.to_string())?; let records = load_managed_agents(&app)?; - let record = records.iter().find(|record| record.pubkey == pubkey) + 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()); diff --git a/desktop/src-tauri/src/commands/dms.rs b/desktop/src-tauri/src/commands/dms.rs index f600ff5e9..2808650d9 100644 --- a/desktop/src-tauri/src/commands/dms.rs +++ b/desktop/src-tauri/src/commands/dms.rs @@ -22,10 +22,7 @@ pub async fn open_dm( } #[tauri::command] -pub async fn hide_dm( - channel_id: String, - state: State<'_, AppState>, -) -> Result<(), String> { +pub async fn hide_dm(channel_id: String, state: State<'_, AppState>) -> Result<(), String> { let path = format!("/api/dms/{channel_id}/hide"); let request = build_authed_request(&state.http_client, Method::POST, &path, &state)?; send_empty_request(request).await diff --git a/desktop/src-tauri/src/commands/messages.rs b/desktop/src-tauri/src/commands/messages.rs index b969a2b37..a50158a28 100644 --- a/desktop/src-tauri/src/commands/messages.rs +++ b/desktop/src-tauri/src/commands/messages.rs @@ -52,12 +52,13 @@ pub async fn get_forum_posts( state: State<'_, AppState>, ) -> Result { let path = format!("/api/channels/{channel_id}/messages"); - let request = build_authed_request(&state.http_client, Method::GET, &path, &state)? - .query(&GetForumPostsQuery { + let request = build_authed_request(&state.http_client, Method::GET, &path, &state)?.query( + &GetForumPostsQuery { limit, before, with_threads: true, - }); + }, + ); send_json_request(request).await } @@ -108,8 +109,8 @@ async fn resolve_thread_ref( parent_event_id: &str, state: &AppState, ) -> Result { - let parent_eid = EventId::from_hex(parent_event_id) - .map_err(|e| format!("invalid parent event ID: {e}"))?; + let parent_eid = + EventId::from_hex(parent_event_id).map_err(|e| format!("invalid parent event ID: {e}"))?; let path = format!("/api/events/{parent_event_id}"); let request = build_authed_request(&state.http_client, Method::GET, &path, state)?; @@ -243,8 +244,7 @@ pub async fn add_reaction( emoji: String, state: State<'_, AppState>, ) -> Result<(), String> { - let target_eid = EventId::from_hex(&event_id) - .map_err(|e| format!("invalid event ID: {e}"))?; + let target_eid = EventId::from_hex(&event_id).map_err(|e| format!("invalid event ID: {e}"))?; let builder = events::build_reaction(target_eid, emoji.trim())?; submit_event(builder, &state).await?; Ok(()) @@ -296,8 +296,7 @@ pub async fn remove_reaction( #[tauri::command] pub async fn delete_message(event_id: String, state: State<'_, AppState>) -> Result<(), String> { - let target_eid = EventId::from_hex(&event_id) - .map_err(|e| format!("invalid event ID: {e}"))?; + let target_eid = EventId::from_hex(&event_id).map_err(|e| format!("invalid event ID: {e}"))?; let builder = events::build_delete_compat(target_eid)?; submit_event(builder, &state).await?; Ok(()) diff --git a/desktop/src-tauri/src/commands/personas.rs b/desktop/src-tauri/src/commands/personas.rs index 0e3a35029..14128b813 100644 --- a/desktop/src-tauri/src/commands/personas.rs +++ b/desktop/src-tauri/src/commands/personas.rs @@ -28,7 +28,10 @@ fn trim_optional(value: Option) -> Option { } #[tauri::command] -pub fn list_personas(app: AppHandle, state: State<'_, AppState>) -> Result, String> { +pub fn list_personas( + app: AppHandle, + state: State<'_, AppState>, +) -> Result, String> { let _store_guard = state .managed_agents_store_lock .lock() diff --git a/desktop/src-tauri/src/commands/profile.rs b/desktop/src-tauri/src/commands/profile.rs index 301937bab..f2ff43bf5 100644 --- a/desktop/src-tauri/src/commands/profile.rs +++ b/desktop/src-tauri/src/commands/profile.rs @@ -94,10 +94,11 @@ pub async fn get_users_batch( state: State<'_, AppState>, ) -> Result { let request = - build_authed_request(&state.http_client, Method::POST, "/api/users/batch", &state)? - .json(&GetUsersBatchBody { + build_authed_request(&state.http_client, Method::POST, "/api/users/batch", &state)?.json( + &GetUsersBatchBody { pubkeys: pubkeys.as_slice(), - }); + }, + ); send_json_request(request).await } @@ -109,13 +110,9 @@ pub async fn search_users( ) -> Result { let limit = limit.unwrap_or(8); let limit_param = limit.to_string(); - let request = build_authed_request( - &state.http_client, - Method::GET, - "/api/users/search", - &state, - )? - .query(&[("q", query.as_str()), ("limit", limit_param.as_str())]); + let request = + build_authed_request(&state.http_client, Method::GET, "/api/users/search", &state)? + .query(&[("q", query.as_str()), ("limit", limit_param.as_str())]); send_json_request(request).await } diff --git a/desktop/src-tauri/src/commands/teams.rs b/desktop/src-tauri/src/commands/teams.rs index 4645747dd..d395c59b6 100644 --- a/desktop/src-tauri/src/commands/teams.rs +++ b/desktop/src-tauri/src/commands/teams.rs @@ -93,11 +93,7 @@ pub fn update_team( } #[tauri::command] -pub fn delete_team( - id: String, - app: AppHandle, - state: State<'_, AppState>, -) -> Result<(), String> { +pub fn delete_team(id: String, app: AppHandle, state: State<'_, AppState>) -> Result<(), String> { let _store_guard = state .managed_agents_store_lock .lock() diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 3544a31a5..31829cafe 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -158,7 +158,7 @@ fn error_response(status: u16, msg: &str) -> http::Response> { #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - let app = tauri::Builder::default() + let builder = tauri::Builder::default() .plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_opener::init()) .plugin( @@ -168,8 +168,16 @@ pub fn run() { ) .plugin(tauri_plugin_websocket::init()) .plugin(tauri_plugin_dialog::init()) - .plugin(tauri_plugin_updater::Builder::new().build()) - .plugin(tauri_plugin_process::init()) + .plugin(tauri_plugin_process::init()); + + // The updater config is only generated for signed release builds. + let builder = if cfg!(debug_assertions) { + builder + } else { + builder.plugin(tauri_plugin_updater::Builder::new().build()) + }; + + let app = builder .register_asynchronous_uri_scheme_protocol("sprout-media", |ctx, request, responder| { let app = ctx.app_handle().clone(); tauri::async_runtime::spawn(async move { @@ -256,7 +264,6 @@ pub fn run() { update_managed_agent, discover_backend_providers, probe_backend_provider, - list_personas, create_persona, update_persona, diff --git a/desktop/src-tauri/src/managed_agents/backend.rs b/desktop/src-tauri/src/managed_agents/backend.rs index 0dce29ed9..88d2bbcf7 100644 --- a/desktop/src-tauri/src/managed_agents/backend.rs +++ b/desktop/src-tauri/src/managed_agents/backend.rs @@ -21,8 +21,10 @@ pub fn invoke_provider( request: &serde_json::Value, timeout: Duration, ) -> Result { - let request_bytes = - format!("{}\n", serde_json::to_string(request).map_err(|e| e.to_string())?); + let request_bytes = format!( + "{}\n", + serde_json::to_string(request).map_err(|e| e.to_string())? + ); let mut child = std::process::Command::new(binary) .stdin(std::process::Stdio::piped()) @@ -241,7 +243,8 @@ fn split_config_key(key: &str) -> Vec { } else if ch.is_uppercase() { // Start a new word on: (a) transition from lowercase to uppercase, or // (b) uppercase followed by lowercase (end of acronym run, e.g. "APIKey" → "API" + "Key"). - let prev_lower = !current.is_empty() && current.chars().last().map_or(false, |c| c.is_lowercase()); + let prev_lower = + !current.is_empty() && current.chars().last().map_or(false, |c| c.is_lowercase()); let acronym_end = !current.is_empty() && current.chars().last().map_or(false, |c| c.is_uppercase()) && chars.get(i + 1).map_or(false, |c| c.is_lowercase()); diff --git a/desktop/src-tauri/src/managed_agents/discovery.rs b/desktop/src-tauri/src/managed_agents/discovery.rs index 1156aead0..467d531d5 100644 --- a/desktop/src-tauri/src/managed_agents/discovery.rs +++ b/desktop/src-tauri/src/managed_agents/discovery.rs @@ -92,7 +92,10 @@ fn normalize_command_identity(command: &str) -> String { let lower = lower.strip_suffix(".exe").unwrap_or(&lower).to_string(); if let Some(suffix) = std::env::consts::EXE_SUFFIX.strip_prefix('.') { - return lower.strip_suffix(&format!(".{suffix}")).unwrap_or(&lower).to_string(); + return lower + .strip_suffix(&format!(".{suffix}")) + .unwrap_or(&lower) + .to_string(); } if !std::env::consts::EXE_SUFFIX.is_empty() { @@ -309,7 +312,11 @@ pub async fn mint_token_via_api( // Build NIP-98 auth header signed by the AGENT's keys (not the desktop user's). let payload_hash = format!("{:x}", Sha256::digest(&body_bytes)); - let forwarded_proto = if url.starts_with("http://") { "http" } else { "https" }; + let forwarded_proto = if url.starts_with("http://") { + "http" + } else { + "https" + }; let tags = vec![ Tag::parse(vec!["u", &url]).map_err(|e| format!("url tag failed: {e}"))?, Tag::parse(vec!["method", "POST"]).map_err(|e| format!("method tag failed: {e}"))?, @@ -343,8 +350,7 @@ mod tests { #[test] fn resolves_known_avatar_for_bare_command() { - let avatar_url = - managed_agent_avatar_url("goose").expect("goose avatar should resolve"); + let avatar_url = managed_agent_avatar_url("goose").expect("goose avatar should resolve"); assert_eq!(avatar_url, GOOSE_AVATAR_URL); } diff --git a/desktop/src-tauri/src/managed_agents/persona_card.rs b/desktop/src-tauri/src/managed_agents/persona_card.rs index 5b6a01b10..07c1a6861 100644 --- a/desktop/src-tauri/src/managed_agents/persona_card.rs +++ b/desktop/src-tauri/src/managed_agents/persona_card.rs @@ -43,7 +43,9 @@ const MAX_ZIP_DECOMPRESSED: usize = 100 * 1024 * 1024; pub fn parse_png_persona(png_bytes: &[u8]) -> Result { let decoder = Decoder::new(Cursor::new(png_bytes)); - let reader = decoder.read_info().map_err(|e| format!("Invalid PNG: {e}"))?; + let reader = decoder + .read_info() + .map_err(|e| format!("Invalid PNG: {e}"))?; let info = reader.info(); let mut sprout_text: Option<&str> = None; @@ -67,7 +69,10 @@ pub fn parse_png_persona(png_bytes: &[u8]) -> Result Result { // --------------------------------------------------------------------------- pub fn parse_json_persona(json_bytes: &[u8]) -> Result { - let v: Value = - serde_json::from_slice(json_bytes).map_err(|e| format!("Invalid JSON: {e}"))?; + let v: Value = serde_json::from_slice(json_bytes).map_err(|e| format!("Invalid JSON: {e}"))?; let fields = extract_sprout_fields(&v)?; Ok(ParsedPersonaPreview { @@ -218,14 +222,8 @@ pub fn encode_persona_json( ) -> Result, String> { let mut map = serde_json::Map::new(); map.insert("version".to_string(), serde_json::json!(1)); - map.insert( - "displayName".to_string(), - serde_json::json!(display_name), - ); - map.insert( - "systemPrompt".to_string(), - serde_json::json!(system_prompt), - ); + map.insert("displayName".to_string(), serde_json::json!(display_name)); + map.insert("systemPrompt".to_string(), serde_json::json!(system_prompt)); if let Some(url) = avatar_url { map.insert("avatarUrl".to_string(), serde_json::json!(url)); } @@ -300,7 +298,9 @@ pub fn parse_zip_personas(zip_bytes: &[u8]) -> Result = (0..51).map(|i| (format!("{i}.png"), png.as_slice())).collect(); + let entries: Vec<(String, &[u8])> = (0..51) + .map(|i| (format!("{i}.png"), png.as_slice())) + .collect(); let refs: Vec<(&str, &[u8])> = entries.iter().map(|(n, d)| (n.as_str(), *d)).collect(); let zip = make_test_zip(&refs); let err = parse_zip_personas(&zip).unwrap_err(); @@ -553,8 +560,10 @@ mod tests { #[test] fn parse_png_duplicate_chunks() { // Two sprout_persona chunks — should use the first and ignore the second. - let payload1 = serde_json::json!({"version": 1, "displayName": "First", "systemPrompt": "Prompt 1"}); - let payload2 = serde_json::json!({"version": 1, "displayName": "Second", "systemPrompt": "Prompt 2"}); + let payload1 = + serde_json::json!({"version": 1, "displayName": "First", "systemPrompt": "Prompt 1"}); + let payload2 = + serde_json::json!({"version": 1, "displayName": "Second", "systemPrompt": "Prompt 2"}); let b64_1 = STANDARD.encode(payload1.to_string().as_bytes()); let b64_2 = STANDARD.encode(payload2.to_string().as_bytes()); @@ -563,8 +572,10 @@ mod tests { let mut enc = Encoder::new(Cursor::new(&mut buf), 1, 1); enc.set_color(ColorType::Rgba); enc.set_depth(BitDepth::Eight); - enc.add_text_chunk("sprout_persona".to_string(), b64_1).unwrap(); - enc.add_text_chunk("sprout_persona".to_string(), b64_2).unwrap(); + enc.add_text_chunk("sprout_persona".to_string(), b64_1) + .unwrap(); + enc.add_text_chunk("sprout_persona".to_string(), b64_2) + .unwrap(); let mut w = enc.write_header().unwrap(); w.write_image_data(&[0, 0, 0, 255]).unwrap(); } @@ -597,9 +608,14 @@ mod tests { #[test] fn parse_json_round_trip() { - let bytes = - encode_persona_json("Ada Lovelace", "You are Ada.", Some("https://example.com/ada.png"), None, None) - .unwrap(); + let bytes = encode_persona_json( + "Ada Lovelace", + "You are Ada.", + Some("https://example.com/ada.png"), + None, + None, + ) + .unwrap(); let result = parse_json_persona(&bytes).unwrap(); assert_eq!(result.display_name, "Ada Lovelace"); assert_eq!(result.system_prompt, "You are Ada."); @@ -622,7 +638,8 @@ mod tests { #[test] fn parse_json_round_trip_data_uri_avatar() { let data_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg=="; - let bytes = encode_persona_json("Carol", "You are Carol.", Some(data_uri), None, None).unwrap(); + let bytes = + encode_persona_json("Carol", "You are Carol.", Some(data_uri), None, None).unwrap(); let result = parse_json_persona(&bytes).unwrap(); assert_eq!(result.display_name, "Carol"); assert_eq!(result.avatar_data_url.as_deref(), Some(data_uri)); @@ -698,8 +715,7 @@ mod tests { "displayName": "X", "systemPrompt": "" }); - let err = - parse_json_persona(&serde_json::to_vec(&json_empty_prompt).unwrap()).unwrap_err(); + let err = parse_json_persona(&serde_json::to_vec(&json_empty_prompt).unwrap()).unwrap_err(); assert!(err.contains("systemPrompt is empty")); } diff --git a/desktop/src-tauri/src/managed_agents/personas.rs b/desktop/src-tauri/src/managed_agents/personas.rs index 0518d7546..39a9a30b9 100644 --- a/desktop/src-tauri/src/managed_agents/personas.rs +++ b/desktop/src-tauri/src/managed_agents/personas.rs @@ -68,7 +68,9 @@ fn built_in_persona_records(now: &str) -> Vec { } fn built_in_order(id: &str) -> Option { - BUILT_IN_PERSONAS.iter().position(|persona| persona.id == id) + BUILT_IN_PERSONAS + .iter() + .position(|persona| persona.id == id) } fn sort_personas(records: &mut [PersonaRecord]) { @@ -78,10 +80,12 @@ fn sort_personas(records: &mut [PersonaRecord]) { left_builtin .cmp(&right_builtin) - .then_with(|| match (built_in_order(&left.id), built_in_order(&right.id)) { - (Some(left_order), Some(right_order)) => left_order.cmp(&right_order), - _ => std::cmp::Ordering::Equal, - }) + .then_with( + || match (built_in_order(&left.id), built_in_order(&right.id)) { + (Some(left_order), Some(right_order)) => left_order.cmp(&right_order), + _ => std::cmp::Ordering::Equal, + }, + ) .then_with(|| { left.display_name .to_lowercase() @@ -179,10 +183,20 @@ mod tests { assert!(changed); assert_eq!(records.len(), BUILT_IN_PERSONAS.len()); assert!(records.iter().all(|record| record.is_builtin)); - let display_names: Vec<&str> = records.iter().map(|record| record.display_name.as_str()).collect(); + let display_names: Vec<&str> = records + .iter() + .map(|record| record.display_name.as_str()) + .collect(); assert_eq!( display_names, - vec!["Orchestrator", "Researcher", "Planner", "Builder", "Refactor", "Reviewer"] + vec![ + "Orchestrator", + "Researcher", + "Planner", + "Builder", + "Refactor", + "Reviewer" + ] ); } @@ -230,12 +244,12 @@ mod tests { let (records, changed) = merge_personas(legacy_builtins, "2026-03-19T00:00:00Z"); assert!(changed); - assert!( - records - .iter() - .any(|record| record.id == "builtin:implementer" && record.display_name == "Builder") - ); - assert!(records.iter().any(|record| record.id == "builtin:orchestrator")); + assert!(records + .iter() + .any(|record| record.id == "builtin:implementer" && record.display_name == "Builder")); + assert!(records + .iter() + .any(|record| record.id == "builtin:orchestrator")); assert!(records.iter().any(|record| record.id == "builtin:refactor")); assert!(records.iter().any(|record| record.id == "builtin:reviewer")); } diff --git a/desktop/src-tauri/src/managed_agents/runtime.rs b/desktop/src-tauri/src/managed_agents/runtime.rs index 21618c5b2..b90028b11 100644 --- a/desktop/src-tauri/src/managed_agents/runtime.rs +++ b/desktop/src-tauri/src/managed_agents/runtime.rs @@ -35,7 +35,9 @@ fn terminate_process(pid: u32) -> Result<(), String> { .status() .map_err(|error| format!("failed to terminate process {pid}: {error}"))?; if !status.success() && process_is_running(pid) { - return Err(format!("failed to terminate process {pid}: signal was rejected")); + return Err(format!( + "failed to terminate process {pid}: signal was rejected" + )); } for _ in 0..10 { diff --git a/desktop/src-tauri/src/managed_agents/teams.rs b/desktop/src-tauri/src/managed_agents/teams.rs index 7ce7e0a5f..a9f2327a5 100644 --- a/desktop/src-tauri/src/managed_agents/teams.rs +++ b/desktop/src-tauri/src/managed_agents/teams.rs @@ -41,8 +41,8 @@ pub fn load_teams(app: &AppHandle) -> Result, String> { return Ok(Vec::new()); } - let content = - fs::read_to_string(&path).map_err(|error| format!("failed to read teams store: {error}"))?; + let content = fs::read_to_string(&path) + .map_err(|error| format!("failed to read teams store: {error}"))?; let mut records: Vec = serde_json::from_str(&content) .map_err(|error| format!("failed to parse teams store: {error}"))?; sort_teams(&mut records); @@ -66,10 +66,7 @@ pub fn save_teams(app: &AppHandle, records: &[TeamRecord]) -> Result<(), String> /// Encode a team as a JSON blob for export. The format includes the team's /// name, description, and the full persona data for each member (so the /// import side can recreate personas that don't exist locally). -pub fn encode_team_json( - team: &TeamRecord, - personas: &[PersonaRecord], -) -> Result, String> { +pub fn encode_team_json(team: &TeamRecord, personas: &[PersonaRecord]) -> Result, String> { let resolved_personas: Vec = team .persona_ids .iter() @@ -185,11 +182,7 @@ mod tests { #[test] fn sort_teams_alphabetical_case_insensitive() { - let mut teams = vec![ - team("3", "Zulu"), - team("1", "alpha"), - team("2", "Bravo"), - ]; + let mut teams = vec![team("3", "Zulu"), team("1", "alpha"), team("2", "Bravo")]; sort_teams(&mut teams); let names: Vec<&str> = teams.iter().map(|t| t.name.as_str()).collect(); @@ -198,10 +191,7 @@ mod tests { #[test] fn sort_teams_breaks_ties_by_id() { - let mut teams = vec![ - team("b", "same"), - team("a", "same"), - ]; + let mut teams = vec![team("b", "same"), team("a", "same")]; sort_teams(&mut teams); let ids: Vec<&str> = teams.iter().map(|t| t.id.as_str()).collect(); diff --git a/desktop/src-tauri/src/relay.rs b/desktop/src-tauri/src/relay.rs index 78fb349f6..580df1de7 100644 --- a/desktop/src-tauri/src/relay.rs +++ b/desktop/src-tauri/src/relay.rs @@ -69,13 +69,7 @@ pub async fn sync_managed_agent_profile( avatar_url: Option<&str>, ) -> Result<(), String> { // Build a kind:0 profile event signed by the agent's keys. - let builder = crate::events::build_profile( - Some(display_name), - None, - avatar_url, - None, - None, - )?; + let builder = crate::events::build_profile(Some(display_name), None, avatar_url, None, None)?; // Sign with the agent's keys (not the desktop user's). let event = builder @@ -268,7 +262,10 @@ pub async fn submit_event( let url = format!("{}/api/events", relay_api_base_url()); let request = if auth_header.starts_with("Bearer ") { - state.http_client.post(&url).header("Authorization", &auth_header) + state + .http_client + .post(&url) + .header("Authorization", &auth_header) } else { let pubkey = auth_header.strip_prefix("X-Pubkey ").unwrap_or(""); state.http_client.post(&url).header("X-Pubkey", pubkey) diff --git a/justfile b/justfile index 0feb09eca..435d599be 100644 --- a/justfile +++ b/justfile @@ -43,7 +43,7 @@ build-release: cargo build --workspace --release # Run repo lint and formatting checks -check: fmt-check clippy desktop-check +check: fmt-check clippy desktop-check desktop-tauri-fmt-check # Format all Rust code fmt: @@ -77,12 +77,20 @@ desktop-typecheck: desktop-build: cd {{desktop_dir}} && pnpm build +# Format desktop Tauri Rust code +desktop-tauri-fmt: + cargo fmt --manifest-path {{desktop_tauri_manifest}} --all + +# Check desktop Tauri Rust formatting +desktop-tauri-fmt-check: + cargo fmt --manifest-path {{desktop_tauri_manifest}} --all -- --check + # Check the desktop Tauri Rust crate compiles desktop-tauri-check: cargo check --manifest-path {{desktop_tauri_manifest}} # Run desktop checks suitable for CI / pre-push -desktop-ci: desktop-check desktop-build desktop-tauri-check +desktop-ci: desktop-check desktop-tauri-fmt-check desktop-build desktop-tauri-check # Seed deterministic channel data for desktop Playwright tests desktop-e2e-seed: