From 022172a9ea79bbc0789acf2ed1022401287117c3 Mon Sep 17 00:00:00 2001 From: xintaofei Date: Thu, 23 Apr 2026 15:56:41 +0800 Subject: [PATCH] chore(lint): clean up frontend and Rust lint issues --- src-tauri/src/acp/connection.rs | 218 +++++++------ src-tauri/src/acp/manager.rs | 5 +- src-tauri/src/acp/mod.rs | 4 +- src-tauri/src/acp/opencode_plugins.rs | 48 ++- src-tauri/src/acp/preflight.rs | 94 +++--- src-tauri/src/acp/registry.rs | 5 +- src-tauri/src/acp/terminal_runtime.rs | 7 +- src-tauri/src/chat_channel/backends/lark.rs | 9 +- .../src/chat_channel/backends/telegram.rs | 13 +- src-tauri/src/chat_channel/backends/weixin.rs | 135 ++++---- .../src/chat_channel/command_dispatcher.rs | 16 +- .../src/chat_channel/command_handlers.rs | 22 +- .../src/chat_channel/event_subscriber.rs | 4 +- src-tauri/src/chat_channel/i18n.rs | 97 +++--- src-tauri/src/chat_channel/manager.rs | 60 ++-- src-tauri/src/chat_channel/scheduler.rs | 12 +- src-tauri/src/chat_channel/session_bridge.rs | 12 +- .../src/chat_channel/session_commands.rs | 126 ++++---- .../chat_channel/session_event_subscriber.rs | 42 +-- src-tauri/src/chat_channel/types.rs | 3 +- src-tauri/src/commands/acp.rs | 305 ++++++++++-------- src-tauri/src/commands/chat_channel.rs | 118 ++++--- src-tauri/src/commands/conversations.rs | 22 +- src-tauri/src/commands/experts.rs | 52 ++- src-tauri/src/commands/folders.rs | 11 +- src-tauri/src/commands/mcp.rs | 4 +- src-tauri/src/commands/model_provider.rs | 45 +-- src-tauri/src/commands/notification.rs | 7 +- src-tauri/src/commands/project_boot.rs | 8 +- src-tauri/src/commands/system_settings.rs | 6 +- src-tauri/src/commands/terminal.rs | 18 +- src-tauri/src/commands/version_control.rs | 44 +-- src-tauri/src/commands/windows.rs | 63 ++-- .../m20260330_000001_chat_channel.rs | 24 +- ...406_000001_agent_setting_model_provider.rs | 6 +- .../src/db/service/app_metadata_service.rs | 2 +- .../src/db/service/conversation_service.rs | 3 +- src-tauri/src/db/service/folder_service.rs | 5 +- src-tauri/src/db/service/tab_service.rs | 5 +- src-tauri/src/git_credential.rs | 20 +- src-tauri/src/git_repo.rs | 6 +- src-tauri/src/keyring_store.rs | 3 +- src-tauri/src/lib.rs | 9 +- src-tauri/src/models/mod.rs | 2 +- src-tauri/src/parsers/claude.rs | 32 +- src-tauri/src/parsers/cline.rs | 27 +- src-tauri/src/parsers/codex.rs | 119 +++---- src-tauri/src/parsers/gemini.rs | 4 +- src-tauri/src/parsers/mod.rs | 24 +- src-tauri/src/parsers/openclaw.rs | 66 ++-- src-tauri/src/parsers/opencode.rs | 22 +- src-tauri/src/process.rs | 7 +- src-tauri/src/web/auth.rs | 6 +- src-tauri/src/web/event_bridge.rs | 6 +- src-tauri/src/web/handlers/acp.rs | 23 +- src-tauri/src/web/handlers/chat_channel.rs | 8 +- src-tauri/src/web/handlers/conversations.rs | 12 +- src-tauri/src/web/handlers/files.rs | 26 +- src-tauri/src/web/handlers/folder_commands.rs | 22 +- src-tauri/src/web/handlers/git.rs | 92 ++---- src-tauri/src/web/handlers/mcp.rs | 34 +- src-tauri/src/web/handlers/system_settings.rs | 23 +- src-tauri/src/web/handlers/version_control.rs | 9 +- src-tauri/src/web/mod.rs | 60 ++-- src-tauri/src/web/router.rs | 5 +- src-tauri/src/web/ws.rs | 2 +- src-tauri/src/workspace_state/mod.rs | 28 +- .../chat/conversation-context-bar.tsx | 5 +- src/components/layout/sidebar.tsx | 4 +- 69 files changed, 1138 insertions(+), 1248 deletions(-) diff --git a/src-tauri/src/acp/connection.rs b/src-tauri/src/acp/connection.rs index d8c3b10..691aafd 100644 --- a/src-tauri/src/acp/connection.rs +++ b/src-tauri/src/acp/connection.rs @@ -156,9 +156,7 @@ async fn build_agent( debug_assert_eq!(meta.agent_type, agent_type); match meta.distribution { - AgentDistribution::Npx { - cmd, args, env, .. - } => { + AgentDistribution::Npx { cmd, args, env, .. } => { let merged_env = merge_agent_env(env, runtime_env); let mut parts: Vec = Vec::new(); for (k, v) in &merged_env { @@ -251,10 +249,7 @@ async fn build_agent( )) })?; if cached_version == registry_version { - eprintln!( - "[ACP][{}] Using cached binary {cached_version}", - meta.name - ); + eprintln!("[ACP][{}] Using cached binary {cached_version}", meta.name); } else { eprintln!( "[ACP][{}] Using cached binary {cached_version} (registry recommends {registry_version})", @@ -273,8 +268,7 @@ async fn build_agent( server = server.args(cmd_args); } let merged_env = merge_agent_env(env, runtime_env); - let env_key_list: Vec<&str> = - merged_env.iter().map(|(k, _)| k.as_str()).collect(); + let env_key_list: Vec<&str> = merged_env.iter().map(|(k, _)| k.as_str()).collect(); if !merged_env.is_empty() { let env_vars: Vec = merged_env .iter() @@ -314,34 +308,36 @@ async fn build_agent( .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) .unwrap_or(false); let agent_name = meta.name.to_string(); - Ok(AcpAgent::new(sacp::schema::McpServer::Stdio(server)).with_debug( - move |line, dir| { - let (tag, enabled) = match dir { - sacp_tokio::LineDirection::Stderr => ("stderr", true), - sacp_tokio::LineDirection::Stdout => ("stdout", stdio_debug_enabled), - sacp_tokio::LineDirection::Stdin => ("stdin", stdio_debug_enabled), - }; - if !enabled { - return; - } - const MAX: usize = 256; - if line.len() > MAX { - let head = line - .char_indices() - .take_while(|(i, _)| *i < MAX) - .last() - .map(|(i, c)| i + c.len_utf8()) - .unwrap_or(MAX); - eprintln!( - "[ACP][{agent_name}][{tag}] {}... ", - &line[..head], - line.len() - head - ); - } else { - eprintln!("[ACP][{agent_name}][{tag}] {line}"); - } - }, - )) + Ok( + AcpAgent::new(sacp::schema::McpServer::Stdio(server)).with_debug( + move |line, dir| { + let (tag, enabled) = match dir { + sacp_tokio::LineDirection::Stderr => ("stderr", true), + sacp_tokio::LineDirection::Stdout => ("stdout", stdio_debug_enabled), + sacp_tokio::LineDirection::Stdin => ("stdin", stdio_debug_enabled), + }; + if !enabled { + return; + } + const MAX: usize = 256; + if line.len() > MAX { + let head = line + .char_indices() + .take_while(|(i, _)| *i < MAX) + .last() + .map(|(i, c)| i + c.len_utf8()) + .unwrap_or(MAX); + eprintln!( + "[ACP][{agent_name}][{tag}] {}... ", + &line[..head], + line.len() - head + ); + } else { + eprintln!("[ACP][{agent_name}][{tag}] {line}"); + } + }, + ), + ) } } } @@ -574,37 +570,40 @@ fn ensure_codex_mode_option(options: &mut Vec) { if options.iter().any(|o| o.id == "mode") { return; } - options.insert(0, SessionConfigOptionInfo { - id: "mode".to_string(), - name: "Approval Preset".to_string(), - description: Some( - "Choose an approval and sandboxing preset for your session".to_string(), - ), - category: Some("mode".to_string()), - kind: SessionConfigKindInfo::Select(SessionConfigSelectInfo { - current_value: "auto".to_string(), - options: vec![ - SessionConfigSelectOptionInfo { - value: "read-only".to_string(), - name: "Read Only".to_string(), - description: Some("Codex can only read files".to_string()), - }, - SessionConfigSelectOptionInfo { - value: "auto".to_string(), - name: "Default".to_string(), - description: Some( - "Codex can edit files, but asks before running commands".to_string(), - ), - }, - SessionConfigSelectOptionInfo { - value: "full-access".to_string(), - name: "Full Access".to_string(), - description: Some("Codex runs without asking for approval".to_string()), - }, - ], - groups: vec![], - }), - }); + options.insert( + 0, + SessionConfigOptionInfo { + id: "mode".to_string(), + name: "Approval Preset".to_string(), + description: Some( + "Choose an approval and sandboxing preset for your session".to_string(), + ), + category: Some("mode".to_string()), + kind: SessionConfigKindInfo::Select(SessionConfigSelectInfo { + current_value: "auto".to_string(), + options: vec![ + SessionConfigSelectOptionInfo { + value: "read-only".to_string(), + name: "Read Only".to_string(), + description: Some("Codex can only read files".to_string()), + }, + SessionConfigSelectOptionInfo { + value: "auto".to_string(), + name: "Default".to_string(), + description: Some( + "Codex can edit files, but asks before running commands".to_string(), + ), + }, + SessionConfigSelectOptionInfo { + value: "full-access".to_string(), + name: "Full Access".to_string(), + description: Some("Codex runs without asking for approval".to_string()), + }, + ], + groups: vec![], + }), + }, + ); } fn emit_session_config_options_values( @@ -758,7 +757,15 @@ async fn run_connection( async move |req: RequestPermissionRequest, responder: Responder, _cx: ConnectionTo| { - handle_permission_request(&conn_id, &emitter_inner, &perms, &perm_cwd, req, responder).await; + handle_permission_request( + &conn_id, + &emitter_inner, + &perms, + &perm_cwd, + req, + responder, + ) + .await; Ok(()) } }, @@ -979,7 +986,13 @@ async fn run_connection( notif.update, SessionUpdate::AvailableCommandsUpdate(_) ) { - emit_conversation_update(&cid, &h, agent_type, notif.update, None); + emit_conversation_update( + &cid, + &h, + agent_type, + notif.update, + None, + ); } Ok(()) }) @@ -1004,7 +1017,12 @@ async fn run_connection( }, ); emit_session_modes(&conn_id, &emitter_clone, session.modes()); - emit_session_config_options(&conn_id, &emitter_clone, agent_type, &initial_config_options); + emit_session_config_options( + &conn_id, + &emitter_clone, + agent_type, + &initial_config_options, + ); emit_selectors_ready(&conn_id, &emitter_clone); let loop_result = run_conversation_loop( @@ -1058,9 +1076,7 @@ async fn run_connection( "acp://event", AcpEvent::Error { connection_id: conn_id.clone(), - message: format!( - "Failed to load session, starting new: {e}" - ), + message: format!("Failed to load session, starting new: {e}"), agent_type: agent_type.to_string(), code: None, }, @@ -1072,8 +1088,7 @@ async fn run_connection( .await?; let fallback_sid = new_resp.session_id.0.to_string(); let initial_config_options = new_resp.config_options.clone(); - let mut session = - cx.attach_session(new_resp, Default::default())?; + let mut session = cx.attach_session(new_resp, Default::default())?; crate::web::event_bridge::emit_event( &emitter_clone, "acp://event", @@ -1139,7 +1154,12 @@ async fn run_connection( }, ); emit_session_modes(&conn_id, &emitter_clone, session.modes()); - emit_session_config_options(&conn_id, &emitter_clone, agent_type, &initial_config_options); + emit_session_config_options( + &conn_id, + &emitter_clone, + agent_type, + &initial_config_options, + ); emit_selectors_ready(&conn_id, &emitter_clone); let loop_result = run_conversation_loop( @@ -1229,8 +1249,7 @@ async fn handle_permission_request( obj.insert(key.to_string(), parsed); } } else if text.contains("@@\n") || text.contains("@@\r\n") { - if let Some(resolved) = - crate::parsers::resolve_patch_text(&text, Some(cwd)) + if let Some(resolved) = crate::parsers::resolve_patch_text(&text, Some(cwd)) { obj.insert(key.to_string(), serde_json::Value::String(resolved)); } @@ -1759,7 +1778,14 @@ async fn handle_fork_or_exit( // Recursively handle nested forks Box::pin(handle_fork_or_exit( - loop_result, conn_id, emitter, agent_type, perms, cmd_rx, terminal_runtime, _cwd, + loop_result, + conn_id, + emitter, + agent_type, + perms, + cmd_rx, + terminal_runtime, + _cwd, cwd_string, )) .await @@ -2169,8 +2195,10 @@ async fn run_conversation_loop<'a>( }) => { let cx = session.connection(); let sid = session.session_id().clone(); - if let Err(e) = - set_session_config_option(&cx, &sid, conn_id, emitter, agent_type, config_id, value_id).await + if let Err(e) = set_session_config_option( + &cx, &sid, conn_id, emitter, agent_type, config_id, value_id, + ) + .await { crate::web::event_bridge::emit_event( emitter, @@ -2499,10 +2527,10 @@ fn emit_conversation_update( } SessionUpdate::ToolCall(tc) => { let content = serialize_tool_call_content(&tc.content); - let raw_input = json_value_to_text(&tc.raw_input) - .map(|text| resolve_live_tool_input(&text, cwd)); - let raw_output = json_value_to_text(&tc.raw_output) - .map(|text| structurize_live_output(&text)); + let raw_input = + json_value_to_text(&tc.raw_input).map(|text| resolve_live_tool_input(&text, cwd)); + let raw_output = + json_value_to_text(&tc.raw_output).map(|text| structurize_live_output(&text)); let locations = if tc.locations.is_empty() { None } else { @@ -2581,7 +2609,12 @@ fn emit_conversation_update( ); } SessionUpdate::ConfigOptionUpdate(update) => { - emit_session_config_options_values(connection_id, emitter, agent_type, update.config_options); + emit_session_config_options_values( + connection_id, + emitter, + agent_type, + update.config_options, + ); } SessionUpdate::AvailableCommandsUpdate(update) => { let commands: Vec = update @@ -2661,8 +2694,8 @@ mod tests { ) .unwrap(); - let event = map_claude_sdk_ext_notification("conn-1", &raw) - .expect("valid sdk payload should map"); + let event = + map_claude_sdk_ext_notification("conn-1", &raw).expect("valid sdk payload should map"); match event { AcpEvent::ClaudeSdkMessage { @@ -2700,11 +2733,8 @@ mod tests { .unwrap(); assert!(map_claude_sdk_ext_notification("conn-1", &wrong_method).is_none()); - let missing_fields = UntypedMessage::new( - "_claude/sdkMessage", - serde_json::json!({"sessionId": 1}), - ) - .unwrap(); + let missing_fields = + UntypedMessage::new("_claude/sdkMessage", serde_json::json!({"sessionId": 1})).unwrap(); assert!(map_claude_sdk_ext_notification("conn-1", &missing_fields).is_none()); } diff --git a/src-tauri/src/acp/manager.rs b/src-tauri/src/acp/manager.rs index 7a9b838..f64b9ac 100644 --- a/src-tauri/src/acp/manager.rs +++ b/src-tauri/src/acp/manager.rs @@ -219,10 +219,7 @@ impl ConnectionManager { pub async fn disconnect_all(&self) -> usize { let cmd_txs: Vec<_> = { let mut connections = self.connections.lock().await; - connections - .drain() - .map(|(_, conn)| conn.cmd_tx) - .collect() + connections.drain().map(|(_, conn)| conn.cmd_tx).collect() }; let disconnected = cmd_txs.len(); for cmd_tx in cmd_txs { diff --git a/src-tauri/src/acp/mod.rs b/src-tauri/src/acp/mod.rs index 4f017bd..7be28cf 100644 --- a/src-tauri/src/acp/mod.rs +++ b/src-tauri/src/acp/mod.rs @@ -1,11 +1,11 @@ pub mod binary_cache; pub mod connection; pub mod error; -pub mod fork; pub mod file_system_runtime; +pub mod fork; pub mod manager; +pub mod opencode_plugins; pub mod preflight; pub mod registry; pub mod terminal_runtime; pub mod types; -pub mod opencode_plugins; diff --git a/src-tauri/src/acp/opencode_plugins.rs b/src-tauri/src/acp/opencode_plugins.rs index 409d7a4..6464a51 100644 --- a/src-tauri/src/acp/opencode_plugins.rs +++ b/src-tauri/src/acp/opencode_plugins.rs @@ -88,9 +88,7 @@ fn has_project_opencode_config(project_root: &Path) -> bool { /// Inspect `~/.config/opencode/opencode.json` and `~/.cache/opencode/node_modules/` /// to determine which declared plugins are installed and which are missing. -pub fn check_opencode_plugins( - project_root: Option<&Path>, -) -> Result { +pub fn check_opencode_plugins(project_root: Option<&Path>) -> Result { let config_path = opencode_config_path() .ok_or_else(|| "Cannot determine opencode config directory".to_string())?; let cache_dir = opencode_cache_dir() @@ -257,12 +255,7 @@ fn write_backup_and_prune(path: &Path, content: &str, keep: usize) -> Result<(), let mut backups: Vec<_> = fs::read_dir(parent) .map_err(|e| e.to_string())? .filter_map(|entry| entry.ok()) - .filter(|entry| { - entry - .file_name() - .to_string_lossy() - .starts_with(&prefix) - }) + .filter(|entry| entry.file_name().to_string_lossy().starts_with(&prefix)) .collect(); // Sort by name descending (timestamp in name → newest first) @@ -280,8 +273,8 @@ pub(crate) fn atomic_rewrite_opencode_json( path: &Path, mutator: impl FnOnce(&mut serde_json::Value) -> Result<(), String>, ) -> Result<(), String> { - let raw = fs::read_to_string(path) - .map_err(|e| format!("Failed to read {}: {e}", path.display()))?; + let raw = + fs::read_to_string(path).map_err(|e| format!("Failed to read {}: {e}", path.display()))?; // Try parsing first. If serde_json succeeds the file is valid JSON // and any "//" or "/*" sequences live inside string values — not real @@ -302,14 +295,12 @@ pub(crate) fn atomic_rewrite_opencode_json( mutator(&mut doc)?; - let new_raw = serde_json::to_string_pretty(&doc) - .map_err(|e| format!("Failed to serialize JSON: {e}"))?; + let new_raw = + serde_json::to_string_pretty(&doc).map_err(|e| format!("Failed to serialize JSON: {e}"))?; let tmp_path = path.with_extension("json.tmp"); - fs::write(&tmp_path, &new_raw) - .map_err(|e| format!("Failed to write temp file: {e}"))?; - fs::rename(&tmp_path, path) - .map_err(|e| format!("Failed to rename temp file: {e}"))?; + fs::write(&tmp_path, &new_raw).map_err(|e| format!("Failed to write temp file: {e}"))?; + fs::rename(&tmp_path, path).map_err(|e| format!("Failed to rename temp file: {e}"))?; Ok(()) } @@ -366,12 +357,9 @@ fn pin_latest_specs( for item in arr.iter_mut() { if let Some(spec_str) = item.as_str() { if let Some((parsed_name, _)) = parse_plugin_spec(spec_str) { - if let Some((_, version)) = - pin_map.iter().find(|(n, _)| *n == parsed_name) + if let Some((_, version)) = pin_map.iter().find(|(n, _)| *n == parsed_name) { - *item = serde_json::Value::String(format!( - "{parsed_name}@{version}" - )); + *item = serde_json::Value::String(format!("{parsed_name}@{version}")); pinned += 1; } } @@ -417,9 +405,9 @@ pub async fn install_missing_plugins( task_id: String, emitter: &EventEmitter, ) -> Result<(), String> { - let _guard = PLUGIN_OP_LOCK.try_lock().map_err(|_| { - "Another plugin operation is in progress".to_string() - })?; + let _guard = PLUGIN_OP_LOCK + .try_lock() + .map_err(|_| "Another plugin operation is in progress".to_string())?; emit_plugin_event(emitter, &task_id, PluginInstallEventKind::Started, ""); @@ -597,12 +585,14 @@ pub async fn install_missing_plugins( /// Uninstall a single plugin: remove from opencode.json, then `bun remove` from cache. pub async fn uninstall_plugin(name: String) -> Result { - let _guard = PLUGIN_OP_LOCK.try_lock().map_err(|_| { - "Another plugin operation is in progress".to_string() - })?; + let _guard = PLUGIN_OP_LOCK + .try_lock() + .map_err(|_| "Another plugin operation is in progress".to_string())?; if is_protected_package(&name) { - return Err(format!("Cannot uninstall {name}: it is an internal opencode package")); + return Err(format!( + "Cannot uninstall {name}: it is an internal opencode package" + )); } let config_path = opencode_config_path() diff --git a/src-tauri/src/acp/preflight.rs b/src-tauri/src/acp/preflight.rs index 9ad7591..99672a9 100644 --- a/src-tauri/src/acp/preflight.rs +++ b/src-tauri/src/acp/preflight.rs @@ -102,14 +102,30 @@ async fn check_npm_environment(node_required: Option<&str>) -> Vec { let (node_result, npm_result) = tokio::join!( async { match &node_path { - Some(p) => crate::process::tokio_command(p).arg("--version").output().await, - None => Err(std::io::Error::new(std::io::ErrorKind::NotFound, "node not found in PATH")), + Some(p) => { + crate::process::tokio_command(p) + .arg("--version") + .output() + .await + } + None => Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "node not found in PATH", + )), } }, async { match &npm_path { - Some(p) => crate::process::tokio_command(p).arg("--version").output().await, - None => Err(std::io::Error::new(std::io::ErrorKind::NotFound, "npm not found in PATH")), + Some(p) => { + crate::process::tokio_command(p) + .arg("--version") + .output() + .await + } + None => Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "npm not found in PATH", + )), } }, ); @@ -310,47 +326,44 @@ async fn check_binary_environment( // but still pass — the Settings page's version-badge flow is the // canonical place to surface "upgrade available". if platform_supported { - let cache_check = - match binary_cache::find_best_cached_binary_for_agent(agent_type, cmd) { - Ok(Some((_, cached_version))) => { - let message = if cached_version == version { - "Binary is cached locally".to_string() - } else { - format!( - "Binary {cached_version} is cached locally (recommended: {version})" - ) - }; - CheckItem { - check_id: "binary_cached".into(), - label: "Binary cache".into(), - status: CheckStatus::Pass, - message, - fixes: vec![], - } + let cache_check = match binary_cache::find_best_cached_binary_for_agent(agent_type, cmd) { + Ok(Some((_, cached_version))) => { + let message = if cached_version == version { + "Binary is cached locally".to_string() + } else { + format!("Binary {cached_version} is cached locally (recommended: {version})") + }; + CheckItem { + check_id: "binary_cached".into(), + label: "Binary cache".into(), + status: CheckStatus::Pass, + message, + fixes: vec![], } - Ok(None) => CheckItem { - check_id: "binary_cached".into(), - label: "Binary cache".into(), - status: CheckStatus::Warn, - message: - "Binary is not installed. Download it from Agent Settings before connecting." - .into(), - fixes: vec![], - }, - Err(_) => CheckItem { - check_id: "binary_cached".into(), - label: "Binary cache".into(), - status: CheckStatus::Warn, - message: "Cannot determine binary cache path".into(), - fixes: vec![], - }, - }; + } + Ok(None) => CheckItem { + check_id: "binary_cached".into(), + label: "Binary cache".into(), + status: CheckStatus::Warn, + message: + "Binary is not installed. Download it from Agent Settings before connecting." + .into(), + fixes: vec![], + }, + Err(_) => CheckItem { + check_id: "binary_cached".into(), + label: "Binary cache".into(), + status: CheckStatus::Warn, + message: "Cannot determine binary cache path".into(), + fixes: vec![], + }, + }; checks.push(cache_check); } // OpenCode plugin checks if agent_type == AgentType::OpenCode { - use crate::acp::opencode_plugins::{self, PluginStatus, spec_has_floating_version}; + use crate::acp::opencode_plugins::{self, spec_has_floating_version, PluginStatus}; match opencode_plugins::check_opencode_plugins(None) { Ok(summary) => { let missing: Vec<_> = summary @@ -376,8 +389,7 @@ async fn check_binary_environment( fixes: vec![], }); } else { - let names: Vec<&str> = - missing.iter().map(|p| p.name.as_str()).collect(); + let names: Vec<&str> = missing.iter().map(|p| p.name.as_str()).collect(); checks.push(CheckItem { check_id: "opencode_plugins".into(), label: "OpenCode plugins".into(), diff --git a/src-tauri/src/acp/registry.rs b/src-tauri/src/acp/registry.rs index 6b8a406..6d489d2 100644 --- a/src-tauri/src/acp/registry.rs +++ b/src-tauri/src/acp/registry.rs @@ -38,8 +38,9 @@ pub struct AcpAgentMeta { impl AcpAgentMeta { pub fn registry_version(&self) -> Option<&'static str> { match &self.distribution { - AgentDistribution::Npx { version, .. } - | AgentDistribution::Binary { version, .. } => Some(*version), + AgentDistribution::Npx { version, .. } | AgentDistribution::Binary { version, .. } => { + Some(*version) + } } } } diff --git a/src-tauri/src/acp/terminal_runtime.rs b/src-tauri/src/acp/terminal_runtime.rs index 928f2da..c01fbb3 100644 --- a/src-tauri/src/acp/terminal_runtime.rs +++ b/src-tauri/src/acp/terminal_runtime.rs @@ -3,10 +3,9 @@ use std::process::Stdio; use std::sync::Arc; use sacp::schema::{ - CreateTerminalRequest, CreateTerminalResponse, KillTerminalRequest, - KillTerminalResponse, ReleaseTerminalRequest, ReleaseTerminalResponse, - TerminalExitStatus, TerminalOutputRequest, TerminalOutputResponse, WaitForTerminalExitRequest, - WaitForTerminalExitResponse, + CreateTerminalRequest, CreateTerminalResponse, KillTerminalRequest, KillTerminalResponse, + ReleaseTerminalRequest, ReleaseTerminalResponse, TerminalExitStatus, TerminalOutputRequest, + TerminalOutputResponse, WaitForTerminalExitRequest, WaitForTerminalExitResponse, }; use tokio::io::{AsyncRead, AsyncReadExt}; use tokio::sync::Mutex; diff --git a/src-tauri/src/chat_channel/backends/lark.rs b/src-tauri/src/chat_channel/backends/lark.rs index 310823d..7dd4c41 100644 --- a/src-tauri/src/chat_channel/backends/lark.rs +++ b/src-tauri/src/chat_channel/backends/lark.rs @@ -202,9 +202,7 @@ impl LarkBackend { let token = result .tenant_access_token - .ok_or_else(|| { - ChatChannelError::AuthenticationFailed("No token in response".into()) - })?; + .ok_or_else(|| ChatChannelError::AuthenticationFailed("No token in response".into()))?; let expire_secs = result.expire.unwrap_or(7200); let expires_at = Instant::now() @@ -252,10 +250,7 @@ impl LarkBackend { ))); } - let message_id = result - .data - .and_then(|d| d.message_id) - .unwrap_or_default(); + let message_id = result.data.and_then(|d| d.message_id).unwrap_or_default(); Ok(SentMessageId(message_id)) } diff --git a/src-tauri/src/chat_channel/backends/telegram.rs b/src-tauri/src/chat_channel/backends/telegram.rs index 59ce5f0..2d763ef 100644 --- a/src-tauri/src/chat_channel/backends/telegram.rs +++ b/src-tauri/src/chat_channel/backends/telegram.rs @@ -34,10 +34,7 @@ impl TelegramBackend { } fn api_url(&self, method: &str) -> String { - format!( - "https://api.telegram.org/bot{}/{}", - self.bot_token, method - ) + format!("https://api.telegram.org/bot{}/{}", self.bot_token, method) } async fn send_text( @@ -171,9 +168,8 @@ impl ChatChannelBackend for TelegramBackend { { offset = uid + 1; } - if let Some(text) = update - .pointer("/message/text") - .and_then(|t| t.as_str()) + if let Some(text) = + update.pointer("/message/text").and_then(|t| t.as_str()) { // Group chat filtering: only process if @bot is mentioned let chat_type = update @@ -184,8 +180,7 @@ impl ChatChannelBackend for TelegramBackend { if (chat_type == "group" || chat_type == "supergroup") && !bot_username.is_empty() { - let at_bot = - format!("@{}", bot_username); + let at_bot = format!("@{}", bot_username); if !text.to_lowercase().contains(&at_bot) { eprintln!("[Telegram] skipped group msg without @bot: {text}"); continue; diff --git a/src-tauri/src/chat_channel/backends/weixin.rs b/src-tauri/src/chat_channel/backends/weixin.rs index 97c7b0a..cf68442 100644 --- a/src-tauri/src/chat_channel/backends/weixin.rs +++ b/src-tauri/src/chat_channel/backends/weixin.rs @@ -57,6 +57,18 @@ pub struct WeixinQrcodeStatusPublic { pub status: String, } +struct SendRequest<'a> { + client: &'a reqwest::Client, + base_url: &'a str, + bot_token: &'a str, + wechat_uin: &'a str, + to_user_id: &'a str, + context_token: &'a str, + text: &'a str, + reply_context: &'a Mutex>, + pending_messages: &'a Mutex>, +} + // ── QR code auth functions (called before backend exists) ── pub async fn weixin_get_qrcode() -> Result { @@ -186,16 +198,16 @@ fn generate_qrcode_data_uri(content: &str) -> Result { Ok(format!("data:image/png;base64,{b64}")) } -pub async fn weixin_check_qrcode( - qrcode: &str, -) -> Result { +pub async fn weixin_check_qrcode(qrcode: &str) -> Result { let client = qr_client(); let resp = client .get(format!("{ILINK_BASE_URL}/ilink/bot/get_qrcode_status")) .query(&[("qrcode", qrcode)]) .send() .await - .map_err(|e| ChatChannelError::ConnectionFailed(format!("QR status request failed: {e}")))?; + .map_err(|e| { + ChatChannelError::ConnectionFailed(format!("QR status request failed: {e}")) + })?; let body: serde_json::Value = resp .json() @@ -289,11 +301,7 @@ impl WeixinBackend { } /// Build the JSON body for the iLink sendmessage API. - fn build_send_body( - to_user_id: &str, - context_token: &str, - text: &str, - ) -> serde_json::Value { + fn build_send_body(to_user_id: &str, context_token: &str, text: &str) -> serde_json::Value { serde_json::json!({ "msg": { "from_user_id": "", @@ -313,23 +321,14 @@ impl WeixinBackend { /// Send a message via the iLink API and handle the response. /// Returns `Ok(true)` if sent, `Ok(false)` if buffered due to expired context. - async fn do_send( - client: &reqwest::Client, - base_url: &str, - bot_token: &str, - wechat_uin: &str, - to_user_id: &str, - context_token: &str, - text: &str, - reply_context: &Mutex>, - pending_messages: &Mutex>, - ) -> Result { - let body = Self::build_send_body(to_user_id, context_token, text); - let url = format!("{base_url}/ilink/bot/sendmessage"); + async fn do_send(req: SendRequest<'_>) -> Result { + let body = Self::build_send_body(req.to_user_id, req.context_token, req.text); + let url = format!("{}/ilink/bot/sendmessage", req.base_url); - let resp = client + let resp = req + .client .post(&url) - .headers(Self::build_headers(bot_token, wechat_uin)) + .headers(Self::build_headers(req.bot_token, req.wechat_uin)) .json(&body) .send() .await @@ -356,20 +355,18 @@ impl WeixinBackend { if ret == -2 { // Context token expired — mark stale and buffer - if let Some(ref mut c) = *reply_context.lock().await { + if let Some(ref mut c) = *req.reply_context.lock().await { c.expired = true; } - let mut buf = pending_messages.lock().await; + let mut buf = req.pending_messages.lock().await; if buf.len() < MAX_PENDING_MESSAGES { - buf.push(text.to_string()); + buf.push(req.text.to_string()); } eprintln!("[Weixin] context_token expired (ret=-2), buffered message"); return Ok(false); } - return Err(ChatChannelError::SendFailed(format!( - "ret={ret}: {errmsg}" - ))); + return Err(ChatChannelError::SendFailed(format!("ret={ret}: {errmsg}"))); } } } @@ -377,10 +374,7 @@ impl WeixinBackend { Ok(true) } - async fn send_text( - &self, - text: &str, - ) -> Result { + async fn send_text(&self, text: &str) -> Result { // Extract context data under lock, then release let (to_user_id, context_token, expired) = { let guard = self.reply_context.lock().await; @@ -418,17 +412,17 @@ impl WeixinBackend { text.len() ); - Self::do_send( - &self.client, - &self.base_url, - &self.bot_token, - &self.wechat_uin, - &to_user_id, - &context_token, + Self::do_send(SendRequest { + client: &self.client, + base_url: &self.base_url, + bot_token: &self.bot_token, + wechat_uin: &self.wechat_uin, + to_user_id: &to_user_id, + context_token: &context_token, text, - &self.reply_context, - &self.pending_messages, - ) + reply_context: &self.reply_context, + pending_messages: &self.pending_messages, + }) .await?; Ok(SentMessageId(String::new())) @@ -478,10 +472,8 @@ impl ChatChannelBackend for WeixinBackend { eprintln!("[Weixin] verify response status={status_code}, body={resp_text}"); - let verify_result: serde_json::Value = - serde_json::from_str(&resp_text).map_err(|e| { - ChatChannelError::ConnectionFailed(format!("JSON parse failed: {e}")) - })?; + let verify_result: serde_json::Value = serde_json::from_str(&resp_text) + .map_err(|e| ChatChannelError::ConnectionFailed(format!("JSON parse failed: {e}")))?; // iLink API auth failures come back as `{"errcode":-14,"errmsg":"session timeout"}` // (no `ret` field). Treat any non-zero errcode as authentication failure. @@ -606,8 +598,7 @@ impl ChatChannelBackend for WeixinBackend { for msg in msgs { // Only handle user messages (message_type=1), // skip bot echo (message_type=2) - let msg_type = - msg.get("message_type").and_then(|v| v.as_i64()); + let msg_type = msg.get("message_type").and_then(|v| v.as_i64()); if msg_type != Some(1) { continue; } @@ -654,10 +645,8 @@ impl ChatChannelBackend for WeixinBackend { if !from_user_id.is_empty() && !context_token.is_empty() { let was_expired = { let mut guard = reply_context.lock().await; - let was = guard - .as_ref() - .map(|c| c.expired) - .unwrap_or(false); + let was = + guard.as_ref().map(|c| c.expired).unwrap_or(false); *guard = Some(WeixinReplyContext { to_user_id: from_user_id.to_string(), context_token: context_token.to_string(), @@ -676,23 +665,22 @@ impl ChatChannelBackend for WeixinBackend { buffered.len() ); for pending_text in &buffered { - let ok = WeixinBackend::do_send( - &client, - &base_url, - &bot_token, - &wechat_uin, - from_user_id, + let ok = WeixinBackend::do_send(SendRequest { + client: &client, + base_url: &base_url, + bot_token: &bot_token, + wechat_uin: &wechat_uin, + to_user_id: from_user_id, context_token, - pending_text, - &reply_context, - &pending_messages, - ) + text: pending_text, + reply_context: &reply_context, + pending_messages: &pending_messages, + }) .await; if let Err(e) = ok { eprintln!("[Weixin] resend error: {e}"); // Re-buffer remaining on hard error - let mut buf = - pending_messages.lock().await; + let mut buf = pending_messages.lock().await; if buf.len() < MAX_PENDING_MESSAGES { buf.push(pending_text.clone()); } @@ -724,15 +712,11 @@ impl ChatChannelBackend for WeixinBackend { } Err(e) => { consecutive_errors += 1; - eprintln!( - "[Weixin] polling error ({consecutive_errors}): {e}" - ); + eprintln!("[Weixin] polling error ({consecutive_errors}): {e}"); *status.lock().await = ChannelConnectionStatus::Error; // Exponential backoff: 5s, 10s, 20s, capped at 30s - let delay = std::cmp::min( - 5 * 2u64.saturating_pow(consecutive_errors - 1), - 30, - ); + let delay = + std::cmp::min(5 * 2u64.saturating_pow(consecutive_errors - 1), 30); tokio::time::sleep(Duration::from_secs(delay)).await; } } @@ -791,9 +775,8 @@ impl ChatChannelBackend for WeixinBackend { eprintln!("[Weixin] test_connection: status={status_code}, body={resp_text}"); - let resp_json: serde_json::Value = serde_json::from_str(&resp_text).map_err(|e| { - ChatChannelError::ConnectionFailed(format!("Not valid JSON: {e}")) - })?; + let resp_json: serde_json::Value = serde_json::from_str(&resp_text) + .map_err(|e| ChatChannelError::ConnectionFailed(format!("Not valid JSON: {e}")))?; if !status_code.is_success() { return Err(ChatChannelError::AuthenticationFailed(format!( diff --git a/src-tauri/src/chat_channel/command_dispatcher.rs b/src-tauri/src/chat_channel/command_dispatcher.rs index 5d354bf..a2df03a 100644 --- a/src-tauri/src/chat_channel/command_dispatcher.rs +++ b/src-tauri/src/chat_channel/command_dispatcher.rs @@ -155,9 +155,16 @@ async fn dispatch_command( guard.find_by_sender(channel_id, sender_id).is_some() }; if has_session { - return session_commands::handle_followup( - db, text, channel_id, sender_id, conn_mgr, bridge, lang, prefix, - ) + return session_commands::handle_followup(session_commands::FollowupRequest { + db, + text, + channel_id, + sender_id, + conn_mgr, + bridge, + lang, + prefix, + }) .await; } return command_handlers::handle_help(prefix, lang); @@ -205,8 +212,7 @@ async fn dispatch_command( .await } "cancel" => { - session_commands::handle_cancel(db, channel_id, sender_id, conn_mgr, bridge, lang) - .await + session_commands::handle_cancel(db, channel_id, sender_id, conn_mgr, bridge, lang).await } "approve" => { let always = args.eq_ignore_ascii_case("always"); diff --git a/src-tauri/src/chat_channel/command_handlers.rs b/src-tauri/src/chat_channel/command_handlers.rs index 3740cf9..f0b3521 100644 --- a/src-tauri/src/chat_channel/command_handlers.rs +++ b/src-tauri/src/chat_channel/command_handlers.rs @@ -6,11 +6,7 @@ use super::manager::ChatChannelManager; use super::types::{MessageLevel, RichMessage}; use crate::db::entities::conversation; -pub async fn handle_search( - db: &DatabaseConnection, - keyword: &str, - lang: Lang, -) -> RichMessage { +pub async fn handle_search(db: &DatabaseConnection, keyword: &str, lang: Lang) -> RichMessage { let matched = match conversation::Entity::find() .filter(conversation::Column::DeletedAt.is_null()) .filter(conversation::Column::Title.contains(keyword)) @@ -40,10 +36,7 @@ pub async fn handle_search( let title = conv.title.as_deref().unwrap_or(i18n::untitled(lang)); let agent = &conv.agent_type; let time = conv.created_at.format("%m-%d %H:%M"); - body.push_str(&format!( - "#{} [{}] {} ({})\n", - conv.id, agent, title, time, - )); + body.push_str(&format!("#{} [{}] {} ({})\n", conv.id, agent, title, time,)); } RichMessage::info(body.trim_end()).with_title(i18n::search_results_count_title( @@ -55,11 +48,7 @@ pub async fn handle_search( pub async fn handle_today(db: &DatabaseConnection, lang: Lang) -> RichMessage { let now = Utc::now(); - let today_start = now - .date_naive() - .and_hms_opt(0, 0, 0) - .unwrap() - .and_utc(); + let today_start = now.date_naive().and_hms_opt(0, 0, 0).unwrap().and_utc(); let rows = match conversation::Entity::find() .filter(conversation::Column::DeletedAt.is_null()) @@ -99,10 +88,7 @@ pub async fn handle_today(db: &DatabaseConnection, lang: Lang) -> RichMessage { let mut body = i18n::total_sessions(lang, rows.len() as u32); body.push_str(&format!("\n\n{}", i18n::by_agent_label(lang))); for (agent, count) in &by_agent { - body.push_str(&format!( - "\n {}", - i18n::agent_count(lang, agent, *count) - )); + body.push_str(&format!("\n {}", i18n::agent_count(lang, agent, *count))); } if !titles.is_empty() { diff --git a/src-tauri/src/chat_channel/event_subscriber.rs b/src-tauri/src/chat_channel/event_subscriber.rs index 16b1dc1..5166531 100644 --- a/src-tauri/src/chat_channel/event_subscriber.rs +++ b/src-tauri/src/chat_channel/event_subscriber.rs @@ -9,7 +9,9 @@ use super::i18n::Lang; use super::manager::ChatChannelManager; use super::message_formatter; use super::types::RichMessage; -use crate::db::service::{app_metadata_service, chat_channel_message_log_service, chat_channel_service}; +use crate::db::service::{ + app_metadata_service, chat_channel_message_log_service, chat_channel_service, +}; use crate::web::event_bridge::WebEventBroadcaster; /// Minimum interval between pushes for the same event type per channel (debounce). diff --git a/src-tauri/src/chat_channel/i18n.rs b/src-tauri/src/chat_channel/i18n.rs index 5f26374..482a326 100644 --- a/src-tauri/src/chat_channel/i18n.rs +++ b/src-tauri/src/chat_channel/i18n.rs @@ -340,7 +340,6 @@ pub fn search_results_count_title(lang: Lang, keyword: &str, count: usize) -> St } } - pub fn no_activity_today(lang: Lang) -> &'static str { match lang { Lang::ZhCn => "今日暂无编码活动", @@ -667,36 +666,26 @@ pub fn search_usage(lang: Lang, prefix: &str) -> String { pub fn unknown_command(lang: Lang, prefix: &str, command: &str) -> String { match lang { - Lang::ZhCn => format!( - "未知命令: {prefix}{command}\n输入 {prefix}help 查看可用命令" - ), - Lang::ZhTw => format!( - "未知命令: {prefix}{command}\n輸入 {prefix}help 查看可用命令" - ), - Lang::Ja => format!( - "不明なコマンド: {prefix}{command}\n{prefix}help でヘルプを表示" - ), - Lang::Ko => format!( - "알 수 없는 명령: {prefix}{command}\n{prefix}help 로 도움말 보기" - ), + Lang::ZhCn => format!("未知命令: {prefix}{command}\n输入 {prefix}help 查看可用命令"), + Lang::ZhTw => format!("未知命令: {prefix}{command}\n輸入 {prefix}help 查看可用命令"), + Lang::Ja => format!("不明なコマンド: {prefix}{command}\n{prefix}help でヘルプを表示"), + Lang::Ko => format!("알 수 없는 명령: {prefix}{command}\n{prefix}help 로 도움말 보기"), Lang::Es => format!( "Comando desconocido: {prefix}{command}\nEscriba {prefix}help para ver los comandos" ), - Lang::De => format!( - "Unbekannter Befehl: {prefix}{command}\n{prefix}help für Hilfe eingeben" - ), - Lang::Fr => format!( - "Commande inconnue : {prefix}{command}\nTapez {prefix}help pour l'aide" - ), - Lang::Pt => format!( - "Comando desconhecido: {prefix}{command}\nDigite {prefix}help para ajuda" - ), - Lang::Ar => format!( - "أمر غير معروف: {prefix}{command}\nاكتب {prefix}help لعرض المساعدة" - ), - Lang::En => format!( - "Unknown command: {prefix}{command}\nType {prefix}help for available commands" - ), + Lang::De => { + format!("Unbekannter Befehl: {prefix}{command}\n{prefix}help für Hilfe eingeben") + } + Lang::Fr => { + format!("Commande inconnue : {prefix}{command}\nTapez {prefix}help pour l'aide") + } + Lang::Pt => { + format!("Comando desconhecido: {prefix}{command}\nDigite {prefix}help para ajuda") + } + Lang::Ar => format!("أمر غير معروف: {prefix}{command}\nاكتب {prefix}help لعرض المساعدة"), + Lang::En => { + format!("Unknown command: {prefix}{command}\nType {prefix}help for available commands") + } } } @@ -782,10 +771,14 @@ pub fn folder_index_out_of_range(lang: Lang, prefix: &str) -> String { match lang { Lang::ZhCn => format!("序号超出范围,请使用 {prefix}folder 查看列表。"), Lang::ZhTw => format!("序號超出範圍,請使用 {prefix}folder 查看列表。"), - Lang::Ja => format!("インデックスが範囲外です。{prefix}folder でリストを確認してください。"), + Lang::Ja => { + format!("インデックスが範囲外です。{prefix}folder でリストを確認してください。") + } Lang::Ko => format!("인덱스가 범위를 벗어났습니다. {prefix}folder로 목록을 확인하세요."), Lang::Es => format!("Índice fuera de rango. Usa {prefix}folder para ver la lista."), - Lang::De => format!("Index außerhalb des Bereichs. {prefix}folder verwenden, um aufzulisten."), + Lang::De => { + format!("Index außerhalb des Bereichs. {prefix}folder verwenden, um aufzulisten.") + } Lang::Fr => format!("Index hors limites. Utilisez {prefix}folder pour lister."), Lang::Pt => format!("Índice fora de intervalo. Use {prefix}folder para listar."), Lang::Ar => format!("الفهرس خارج النطاق. استخدم {prefix}folder لعرض القائمة."), @@ -842,7 +835,9 @@ pub fn no_folder_selected(lang: Lang, prefix: &str) -> String { match lang { Lang::ZhCn => format!("未选择工作目录,请先使用 {prefix}folder 选择。"), Lang::ZhTw => format!("未選擇工作目錄,請先使用 {prefix}folder 選擇。"), - Lang::Ja => format!("フォルダが選択されていません。先に {prefix}folder を使用してください。"), + Lang::Ja => { + format!("フォルダが選択されていません。先に {prefix}folder を使用してください。") + } Lang::Ko => format!("폴더가 선택되지 않았습니다. 먼저 {prefix}folder를 사용하세요."), Lang::Es => format!("Ninguna carpeta seleccionada. Usa {prefix}folder primero."), Lang::De => format!("Kein Ordner ausgewählt. Zuerst {prefix}folder verwenden."), @@ -873,12 +868,22 @@ pub fn agent_select_hint(lang: Lang, prefix: &str) -> String { match lang { Lang::ZhCn => format!("回复 {prefix}agent <数字> 或 {prefix}agent <名称> 选择。"), Lang::ZhTw => format!("回覆 {prefix}agent <數字> 或 {prefix}agent <名稱> 選擇。"), - Lang::Ja => format!("{prefix}agent <番号> または {prefix}agent <名前> で選択してください。"), + Lang::Ja => { + format!("{prefix}agent <番号> または {prefix}agent <名前> で選択してください。") + } Lang::Ko => format!("{prefix}agent <번호> 또는 {prefix}agent <이름>으로 선택하세요."), - Lang::Es => format!("Responde {prefix}agent o {prefix}agent para seleccionar."), - Lang::De => format!("Antworte {prefix}agent oder {prefix}agent zur Auswahl."), - Lang::Fr => format!("Répondez {prefix}agent ou {prefix}agent pour sélectionner."), - Lang::Pt => format!("Responda {prefix}agent ou {prefix}agent para selecionar."), + Lang::Es => { + format!("Responde {prefix}agent o {prefix}agent para seleccionar.") + } + Lang::De => { + format!("Antworte {prefix}agent oder {prefix}agent zur Auswahl.") + } + Lang::Fr => { + format!("Répondez {prefix}agent ou {prefix}agent pour sélectionner.") + } + Lang::Pt => { + format!("Responda {prefix}agent ou {prefix}agent para selecionar.") + } Lang::Ar => format!("أجب بـ {prefix}agent <رقم> أو {prefix}agent <اسم> للاختيار."), Lang::En => format!("Reply {prefix}agent or {prefix}agent to select."), } @@ -891,7 +896,9 @@ pub fn agent_index_out_of_range(lang: Lang, prefix: &str) -> String { Lang::Ja => format!("インデックスが範囲外です。{prefix}agent でリストを確認してください。"), Lang::Ko => format!("인덱스가 범위를 벗어났습니다. {prefix}agent로 목록을 확인하세요."), Lang::Es => format!("Índice fuera de rango. Usa {prefix}agent para ver la lista."), - Lang::De => format!("Index außerhalb des Bereichs. {prefix}agent verwenden, um aufzulisten."), + Lang::De => { + format!("Index außerhalb des Bereichs. {prefix}agent verwenden, um aufzulisten.") + } Lang::Fr => format!("Index hors limites. Utilisez {prefix}agent pour lister."), Lang::Pt => format!("Índice fora de intervalo. Use {prefix}agent para listar."), Lang::Ar => format!("الفهرس خارج النطاق. استخدم {prefix}agent لعرض القائمة."), @@ -1299,7 +1306,9 @@ pub fn no_active_session_use_task(lang: Lang, prefix: &str) -> String { match lang { Lang::ZhCn => format!("没有活跃的会话,请使用 {prefix}task 开始新任务。"), Lang::ZhTw => format!("沒有活躍的對話,請使用 {prefix}task 開始新任務。"), - Lang::Ja => format!("アクティブなセッションがありません。{prefix}task で開始してください。"), + Lang::Ja => { + format!("アクティブなセッションがありません。{prefix}task で開始してください。") + } Lang::Ko => format!("활성 세션이 없습니다. {prefix}task로 시작하세요."), Lang::Es => format!("No hay sesión activa. Usa {prefix}task para iniciar una."), Lang::De => format!("Keine aktive Sitzung. {prefix}task zum Starten verwenden."), @@ -1314,11 +1323,17 @@ pub fn session_connection_lost(lang: Lang, prefix: &str) -> String { match lang { Lang::ZhCn => format!("会话连接已断开,请使用 {prefix}task 开始新任务。"), Lang::ZhTw => format!("對話連線已斷開,請使用 {prefix}task 開始新任務。"), - Lang::Ja => format!("セッション接続が切断されました。{prefix}task で新しく開始してください。"), + Lang::Ja => { + format!("セッション接続が切断されました。{prefix}task で新しく開始してください。") + } Lang::Ko => format!("세션 연결이 끊어졌습니다. {prefix}task로 새로 시작하세요."), Lang::Es => format!("Conexión de sesión perdida. Usa {prefix}task para iniciar una nueva."), - Lang::De => format!("Sitzungsverbindung verloren. {prefix}task für neue Sitzung verwenden."), - Lang::Fr => format!("Connexion de session perdue. Utilisez {prefix}task pour en démarrer une nouvelle."), + Lang::De => { + format!("Sitzungsverbindung verloren. {prefix}task für neue Sitzung verwenden.") + } + Lang::Fr => format!( + "Connexion de session perdue. Utilisez {prefix}task pour en démarrer une nouvelle." + ), Lang::Pt => format!("Conexão da sessão perdida. Use {prefix}task para iniciar uma nova."), Lang::Ar => format!("انقطع اتصال الجلسة. استخدم {prefix}task لبدء جلسة جديدة."), Lang::En => format!("Session connection lost. Use {prefix}task to start a new one."), diff --git a/src-tauri/src/chat_channel/manager.rs b/src-tauri/src/chat_channel/manager.rs index 0f1fd2e..44ff2ed 100644 --- a/src-tauri/src/chat_channel/manager.rs +++ b/src-tauri/src/chat_channel/manager.rs @@ -268,28 +268,26 @@ impl ChatChannelManager { } async fn auto_connect_channels(&self, db_conn: &DatabaseConnection) { - let channels = - match crate::db::service::chat_channel_service::list_enabled(db_conn).await { - Ok(c) => c, - Err(e) => { - eprintln!("[ChatChannel] failed to load enabled channels: {e}"); - return; - } - }; + let channels = match crate::db::service::chat_channel_service::list_enabled(db_conn).await { + Ok(c) => c, + Err(e) => { + eprintln!("[ChatChannel] failed to load enabled channels: {e}"); + return; + } + }; for ch in channels { - let channel_type: ChannelType = match serde_json::from_value( - serde_json::Value::String(ch.channel_type.clone()), - ) { - Ok(t) => t, - Err(_) => { - eprintln!( - "[ChatChannel] unknown channel type '{}' for '{}' (id={}), skipping", - ch.channel_type, ch.name, ch.id - ); - continue; - } - }; + let channel_type: ChannelType = + match serde_json::from_value(serde_json::Value::String(ch.channel_type.clone())) { + Ok(t) => t, + Err(_) => { + eprintln!( + "[ChatChannel] unknown channel type '{}' for '{}' (id={}), skipping", + ch.channel_type, ch.name, ch.id + ); + continue; + } + }; let config: serde_json::Value = match serde_json::from_str(&ch.config_json) { Ok(v) => v, @@ -313,17 +311,17 @@ impl ChatChannelManager { } }; - let backend = - match super::backends::create_backend(ch.id, channel_type, &config, token) { - Ok(b) => b, - Err(e) => { - eprintln!( - "[ChatChannel] failed to create backend for '{}' (id={}): {e}", - ch.name, ch.id - ); - continue; - } - }; + let backend = match super::backends::create_backend(ch.id, channel_type, &config, token) + { + Ok(b) => b, + Err(e) => { + eprintln!( + "[ChatChannel] failed to create backend for '{}' (id={}): {e}", + ch.name, ch.id + ); + continue; + } + }; if let Err(e) = self .add_channel(ch.id, ch.name.clone(), channel_type, backend) diff --git a/src-tauri/src/chat_channel/scheduler.rs b/src-tauri/src/chat_channel/scheduler.rs index b5a3aec..94d36a6 100644 --- a/src-tauri/src/chat_channel/scheduler.rs +++ b/src-tauri/src/chat_channel/scheduler.rs @@ -8,7 +8,9 @@ use super::i18n::Lang; use super::manager::ChatChannelManager; use super::message_formatter::{self, DailyReportData}; use crate::db::entities::conversation; -use crate::db::service::{app_metadata_service, chat_channel_message_log_service, chat_channel_service}; +use crate::db::service::{ + app_metadata_service, chat_channel_message_log_service, chat_channel_service, +}; const MESSAGE_LANGUAGE_KEY: &str = "chat_message_language"; /// Days to retain message logs before cleanup. @@ -60,10 +62,7 @@ pub fn spawn_daily_report_scheduler( continue; } - let report_time = ch - .daily_report_time - .as_deref() - .unwrap_or("18:00"); + let report_time = ch.daily_report_time.as_deref().unwrap_or("18:00"); if current_time != report_time { continue; @@ -124,8 +123,7 @@ async fn generate_daily_report(db: &DatabaseConnection) -> DailyReportData { .await .unwrap_or_default(); - let mut by_agent: std::collections::HashMap = - std::collections::HashMap::new(); + let mut by_agent: std::collections::HashMap = std::collections::HashMap::new(); let mut folder_ids: HashSet = HashSet::new(); let mut activities: Vec = Vec::new(); diff --git a/src-tauri/src/chat_channel/session_bridge.rs b/src-tauri/src/chat_channel/session_bridge.rs index c9dfeea..aaba69b 100644 --- a/src-tauri/src/chat_channel/session_bridge.rs +++ b/src-tauri/src/chat_channel/session_bridge.rs @@ -54,9 +54,9 @@ impl SessionBridge { } pub fn find_by_sender(&self, channel_id: i32, sender_id: &str) -> Option<&ActiveSession> { - self.sessions.values().find(|s| { - s.channel_id == channel_id && s.sender_id == sender_id - }) + self.sessions + .values() + .find(|s| s.channel_id == channel_id && s.sender_id == sender_id) } pub fn find_by_sender_mut( @@ -64,9 +64,9 @@ impl SessionBridge { channel_id: i32, sender_id: &str, ) -> Option<&mut ActiveSession> { - self.sessions.values_mut().find(|s| { - s.channel_id == channel_id && s.sender_id == sender_id - }) + self.sessions + .values_mut() + .find(|s| s.channel_id == channel_id && s.sender_id == sender_id) } pub fn all_sessions(&self) -> impl Iterator { diff --git a/src-tauri/src/chat_channel/session_commands.rs b/src-tauri/src/chat_channel/session_commands.rs index 89fd93a..d7984fd 100644 --- a/src-tauri/src/chat_channel/session_commands.rs +++ b/src-tauri/src/chat_channel/session_commands.rs @@ -16,6 +16,17 @@ use crate::db::service::{conversation_service, folder_service, sender_context_se use crate::models::agent::AgentType; use crate::web::event_bridge::EventEmitter; +pub struct FollowupRequest<'a> { + pub db: &'a DatabaseConnection, + pub text: &'a str, + pub channel_id: i32, + pub sender_id: &'a str, + pub conn_mgr: &'a ConnectionManager, + pub bridge: &'a Arc>, + pub lang: Lang, + pub prefix: &'a str, +} + // ── /folder ── pub async fn handle_folder( @@ -70,13 +81,7 @@ async fn list_folders( .map(|id| id == f.id) .unwrap_or(false); let marker = if current { " [*]" } else { "" }; - body.push_str(&format!( - "{}. {}{} ({})\n", - i + 1, - f.name, - marker, - f.path - )); + body.push_str(&format!("{}. {}{} ({})\n", i + 1, f.name, marker, f.path)); } body.push_str(&format!("\n{}", i18n::folder_select_hint(lang, prefix))); @@ -107,8 +112,7 @@ async fn select_folder_by_index( return RichMessage::info(i18n::folder_index_out_of_range(lang, prefix)); }; - let _ = sender_context_service::update_folder(db, channel_id, sender_id, Some(folder.id)) - .await; + let _ = sender_context_service::update_folder(db, channel_id, sender_id, Some(folder.id)).await; RichMessage::info(format!("{} ({})", folder.name, folder.path)) .with_title(i18n::folder_selected_title(lang)) @@ -128,8 +132,7 @@ async fn select_folder_by_path( } }; - let _ = - sender_context_service::update_folder(db, channel_id, sender_id, Some(entry.id)).await; + let _ = sender_context_service::update_folder(db, channel_id, sender_id, Some(entry.id)).await; RichMessage::info(format!("{} ({})", entry.name, entry.path)) .with_title(i18n::folder_selected_title(lang)) @@ -317,10 +320,7 @@ pub async fn handle_task( conversation::ConversationStatus::Cancelled, ) .await; - return RichMessage::error(format!( - "{}{e}", - i18n::failed_to_start_agent_label(lang) - )); + return RichMessage::error(format!("{}{e}", i18n::failed_to_start_agent_label(lang))); } }; @@ -352,11 +352,8 @@ pub async fn handle_task( ) .await; - RichMessage::info(format!( - "[{}] #{} @ {}", - agent_type, conv.id, folder.name, - )) - .with_title(i18n::task_started_title(lang)) + RichMessage::info(format!("[{}] #{} @ {}", agent_type, conv.id, folder.name,)) + .with_title(i18n::task_started_title(lang)) } // ── /sessions ── @@ -401,16 +398,16 @@ pub async fn handle_sessions( { Ok(c) => c, Err(e) => { - return RichMessage::error(format!( - "{}{e}", - i18n::failed_to_list_sessions_label(lang) - )); + return RichMessage::error(format!("{}{e}", i18n::failed_to_list_sessions_label(lang))); } }; if convs.is_empty() { - return RichMessage::info(i18n::no_active_sessions_in_folder(lang)) - .with_title(format!("{} - {}", i18n::sessions_title(lang), folder.name)); + return RichMessage::info(i18n::no_active_sessions_in_folder(lang)).with_title(format!( + "{} - {}", + i18n::sessions_title(lang), + folder.name + )); } let mut body = String::new(); @@ -433,8 +430,11 @@ pub async fn handle_sessions( body.push_str(&format!("\n{}", i18n::sessions_resume_hint(lang, prefix))); - RichMessage::info(body.trim_end()) - .with_title(format!("{} - {}", i18n::sessions_title(lang), folder.name)) + RichMessage::info(body.trim_end()).with_title(format!( + "{} - {}", + i18n::sessions_title(lang), + folder.name + )) } // ── /resume ── @@ -491,10 +491,7 @@ pub async fn handle_resume( { Ok(id) => id, Err(e) => { - return RichMessage::error(format!( - "{}{e}", - i18n::failed_to_start_agent_label(lang) - )); + return RichMessage::error(format!("{}{e}", i18n::failed_to_start_agent_label(lang))); } }; @@ -579,8 +576,7 @@ pub async fn handle_cancel( // Clear session from context let _ = sender_context_service::clear_session(db, channel_id, sender_id).await; - RichMessage::info(i18n::task_cancelled_body(lang)) - .with_title(i18n::task_cancelled_title(lang)) + RichMessage::info(i18n::task_cancelled_body(lang)).with_title(i18n::task_cancelled_title(lang)) } // ── /approve, /deny ── @@ -661,8 +657,7 @@ pub async fn handle_permission_response( // Update auto_approve if requested if always && approve { - let _ = - sender_context_service::update_auto_approve(db, channel_id, sender_id, true).await; + let _ = sender_context_service::update_auto_approve(db, channel_id, sender_id, true).await; } let action = if approve { @@ -680,66 +675,58 @@ pub async fn handle_permission_response( // ── follow-up (non-command text) ── -pub async fn handle_followup( - db: &DatabaseConnection, - text: &str, - channel_id: i32, - sender_id: &str, - conn_mgr: &ConnectionManager, - bridge: &Arc>, - lang: Lang, - prefix: &str, -) -> RichMessage { - let ctx = match sender_context_service::get_or_create(db, channel_id, sender_id).await { - Ok(c) => c, - Err(e) => { - return RichMessage::error(format!("{}{e}", i18n::failed_to_load_context_label(lang))); - } - }; +pub async fn handle_followup(req: FollowupRequest<'_>) -> RichMessage { + let ctx = + match sender_context_service::get_or_create(req.db, req.channel_id, req.sender_id).await { + Ok(c) => c, + Err(e) => { + return RichMessage::error(format!( + "{}{e}", + i18n::failed_to_load_context_label(req.lang) + )); + } + }; let connection_id = match &ctx.current_connection_id { Some(id) => id.clone(), None => { - return RichMessage::info(i18n::no_active_session_use_task(lang, prefix)); + return RichMessage::info(i18n::no_active_session_use_task(req.lang, req.prefix)); } }; // Check connection exists in bridge { - let bridge_guard = bridge.lock().await; + let bridge_guard = req.bridge.lock().await; if bridge_guard.get(&connection_id).is_none() { // Connection lost, clear context drop(bridge_guard); - let _ = sender_context_service::clear_session(db, channel_id, sender_id).await; - return RichMessage::info(i18n::session_connection_lost(lang, prefix)); + let _ = + sender_context_service::clear_session(req.db, req.channel_id, req.sender_id).await; + return RichMessage::info(i18n::session_connection_lost(req.lang, req.prefix)); } } // Send prompt to agent let blocks = vec![PromptInputBlock::Text { - text: text.to_string(), + text: req.text.to_string(), }]; - if let Err(e) = conn_mgr.send_prompt(&connection_id, blocks).await { + if let Err(e) = req.conn_mgr.send_prompt(&connection_id, blocks).await { // Connection may have died - bridge.lock().await.remove(&connection_id); - let _ = sender_context_service::clear_session(db, channel_id, sender_id).await; + req.bridge.lock().await.remove(&connection_id); + let _ = sender_context_service::clear_session(req.db, req.channel_id, req.sender_id).await; return RichMessage::error(format!( "{}{e}", - i18n::failed_to_send_message_label(lang) + i18n::failed_to_send_message_label(req.lang) )); } - RichMessage::info(i18n::message_sent(lang)) + RichMessage::info(i18n::message_sent(req.lang)) } // ── /resume (list recent) ── -async fn list_recent_sessions( - db: &DatabaseConnection, - lang: Lang, - prefix: &str, -) -> RichMessage { +async fn list_recent_sessions(db: &DatabaseConnection, lang: Lang, prefix: &str) -> RichMessage { let recent = match conversation::Entity::find() .filter(conversation::Column::DeletedAt.is_null()) .order_by_desc(conversation::Column::CreatedAt) @@ -768,10 +755,7 @@ async fn list_recent_sessions( let title = conv.title.as_deref().unwrap_or(i18n::untitled(lang)); let agent = &conv.agent_type; let time = conv.created_at.format("%m-%d %H:%M"); - body.push_str(&format!( - "#{} [{}] {} ({})\n", - conv.id, agent, title, time, - )); + body.push_str(&format!("#{} [{}] {} ({})\n", conv.id, agent, title, time,)); } body.push_str(&format!("\n{}", i18n::recent_resume_hint(lang, prefix))); diff --git a/src-tauri/src/chat_channel/session_event_subscriber.rs b/src-tauri/src/chat_channel/session_event_subscriber.rs index 82cc6a1..f841ef6 100644 --- a/src-tauri/src/chat_channel/session_event_subscriber.rs +++ b/src-tauri/src/chat_channel/session_event_subscriber.rs @@ -10,9 +10,7 @@ use super::session_bridge::{PendingPermission, SessionBridge}; use super::types::{MessageLevel, RichMessage}; use crate::acp::manager::ConnectionManager; use crate::acp::types::PromptInputBlock; -use crate::db::service::{ - app_metadata_service, conversation_service, sender_context_service, -}; +use crate::db::service::{app_metadata_service, conversation_service, sender_context_service}; use crate::web::event_bridge::WebEventBroadcaster; use super::manager::ChatChannelManager; @@ -132,10 +130,7 @@ async fn handle_acp_event_payload( } "content_delta" => { - let text = payload - .get("text") - .and_then(|v| v.as_str()) - .unwrap_or(""); + let text = payload.get("text").and_then(|v| v.as_str()).unwrap_or(""); // Collect flush info under the lock, then release before any IO. let flush_info: Option<(i32, String, Option)> = { @@ -245,14 +240,11 @@ async fn handle_acp_event_payload( let channel_id = session.channel_id; let sender_id = session.sender_id.clone(); - let auto_approve = sender_context_service::get_or_create( - db, - channel_id, - &sender_id, - ) - .await - .map(|ctx| ctx.auto_approve) - .unwrap_or(false); + let auto_approve = + sender_context_service::get_or_create(db, channel_id, &sender_id) + .await + .map(|ctx| ctx.auto_approve) + .unwrap_or(false); if auto_approve { let option_id = options @@ -286,8 +278,7 @@ async fn handle_acp_event_payload( serde_json::Value::Null => None, other => Some(other.to_string()), }); - let tool_desc = - format_tool_call_detail(tool_title, raw_input_str.as_deref()); + let tool_desc = format_tool_call_detail(tool_title, raw_input_str.as_deref()); session.permission_pending = Some(PendingPermission { request_id: request_id.to_string(), @@ -407,16 +398,12 @@ async fn handle_acp_event_payload( crate::db::entities::conversation::ConversationStatus::Cancelled, ) .await; - let _ = - sender_context_service::clear_session(db, channel_id, &sender_id).await; + let _ = sender_context_service::clear_session(db, channel_id, &sender_id).await; } } "status_changed" => { - let status = payload - .get("status") - .and_then(|v| v.as_str()) - .unwrap_or(""); + let status = payload.get("status").and_then(|v| v.as_str()).unwrap_or(""); if status == "disconnected" || status == "error" { let mut guard = bridge.lock().await; @@ -425,8 +412,7 @@ async fn handle_acp_event_payload( let sender_id = session.sender_id.clone(); drop(guard); - let _ = - sender_context_service::clear_session(db, channel_id, &sender_id).await; + let _ = sender_context_service::clear_session(db, channel_id, &sender_id).await; } } } @@ -653,7 +639,11 @@ fn format_tool_call_detail(title: &str, raw_input: Option<&str>) -> String { if let Some(pattern) = obj.get("pattern").and_then(|v| v.as_str()) { let path = obj.get("path").and_then(|v| v.as_str()); return if let Some(p) = path { - format!("Grep: \"{}\" in {}", truncate_str(pattern, 40), short_path(p)) + format!( + "Grep: \"{}\" in {}", + truncate_str(pattern, 40), + short_path(p) + ) } else { format!("Grep: \"{}\"", truncate_str(pattern, 60)) }; diff --git a/src-tauri/src/chat_channel/types.rs b/src-tauri/src/chat_channel/types.rs index 32eaee8..7370749 100644 --- a/src-tauri/src/chat_channel/types.rs +++ b/src-tauri/src/chat_channel/types.rs @@ -147,7 +147,8 @@ impl InteractiveMessage { .iter() .map(|b| format!("[{}]", b.label)) .collect(); - msg.body.push_str(&format!("\n\n{}", button_text.join(" "))); + msg.body + .push_str(&format!("\n\n{}", button_text.join(" "))); } msg } diff --git a/src-tauri/src/commands/acp.rs b/src-tauri/src/commands/acp.rs index 04cddf4..549ab04 100644 --- a/src-tauri/src/commands/acp.rs +++ b/src-tauri/src/commands/acp.rs @@ -8,9 +8,9 @@ use tauri::State; use crate::acp::binary_cache; use crate::acp::error::AcpError; -use crate::acp::opencode_plugins::{self, PluginCheckSummary}; #[cfg(feature = "tauri-runtime")] use crate::acp::manager::ConnectionManager; +use crate::acp::opencode_plugins::{self, PluginCheckSummary}; use crate::acp::preflight::{self, PreflightResult}; use crate::acp::registry; use crate::acp::types::{ @@ -154,9 +154,7 @@ pub(crate) fn verify_agent_installed(agent_type: AgentType) -> Result<(), AcpErr } Ok(()) } - registry::AgentDistribution::Binary { - cmd, platforms, .. - } => { + registry::AgentDistribution::Binary { cmd, platforms, .. } => { let platform = registry::current_platform(); if !platforms.iter().any(|p| p.platform == platform) { return Err(AcpError::PlatformNotSupported(format!( @@ -212,7 +210,11 @@ async fn npm_list_version( prefix: Option<&std::path::Path>, ) -> Option { let mut cmd = crate::process::tokio_command(npm_path); - cmd.arg("list").arg("-g").arg(package_name).arg("--json").arg("--depth=0"); + cmd.arg("list") + .arg("-g") + .arg(package_name) + .arg("--json") + .arg("--depth=0"); if let Some(p) = prefix { cmd.arg(format!("--prefix={}", p.display())); } @@ -266,9 +268,9 @@ async fn run_npm_streaming( cmd.stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()); - let mut child = cmd.spawn().map_err(|e| { - AcpError::protocol(format!("failed to spawn npm: {e}")) - })?; + let mut child = cmd + .spawn() + .map_err(|e| AcpError::protocol(format!("failed to spawn npm: {e}")))?; let stdout = child.stdout.take(); let stderr = child.stderr.take(); @@ -283,9 +285,7 @@ async fn run_npm_streaming( let reader = BufReader::new(out); let mut lines = reader.lines(); while let Ok(Some(line)) = lines.next_line().await { - emit_agent_install_event( - &emitter, &task_id, AgentInstallEventKind::Log, &line, - ); + emit_agent_install_event(&emitter, &task_id, AgentInstallEventKind::Log, &line); } } } @@ -300,9 +300,7 @@ async fn run_npm_streaming( let reader = BufReader::new(err); let mut lines = reader.lines(); while let Ok(Some(line)) = lines.next_line().await { - emit_agent_install_event( - &emitter, &task_id, AgentInstallEventKind::Log, &line, - ); + emit_agent_install_event(&emitter, &task_id, AgentInstallEventKind::Log, &line); if !collected.is_empty() { collected.push('\n'); } @@ -316,9 +314,10 @@ async fn run_npm_streaming( let (_, stderr_result) = tokio::join!(stdout_handle, stderr_handle); let collected_stderr = stderr_result.unwrap_or_default(); - let status = child.wait().await.map_err(|e| { - AcpError::protocol(format!("failed to wait for npm process: {e}")) - })?; + let status = child + .wait() + .await + .map_err(|e| AcpError::protocol(format!("failed to wait for npm process: {e}")))?; Ok((status.success(), collected_stderr)) } @@ -331,49 +330,58 @@ async fn install_npm_global_package_streaming( let registry_arg = format!("--registry={NPM_OFFICIAL_REGISTRY}"); emit_agent_install_event( - emitter, task_id, AgentInstallEventKind::Log, + emitter, + task_id, + AgentInstallEventKind::Log, format!("$ npm install -g {package}"), ); - let (success, stderr) = run_npm_streaming( - &["install", "-g", ®istry_arg, package], - task_id, - emitter, - ).await?; + let (success, stderr) = + run_npm_streaming(&["install", "-g", ®istry_arg, package], task_id, emitter).await?; if !success { // EACCES: permission denied — retry with a user-local --prefix so // we don't require root/sudo on macOS / Linux. if stderr.contains("EACCES") { emit_agent_install_event( - emitter, task_id, AgentInstallEventKind::Log, + emitter, + task_id, + AgentInstallEventKind::Log, "Permission denied, retrying with user prefix...", ); - return install_npm_to_user_prefix_streaming( - package, ®istry_arg, task_id, emitter, - ).await; + return install_npm_to_user_prefix_streaming(package, ®istry_arg, task_id, emitter) + .await; } // EEXIST: file conflict — retry with --force to overwrite if stderr.contains("EEXIST") { emit_agent_install_event( - emitter, task_id, AgentInstallEventKind::Log, + emitter, + task_id, + AgentInstallEventKind::Log, "File conflict, retrying with --force...", ); let (retry_success, retry_stderr) = run_npm_streaming( &["install", "-g", "--force", ®istry_arg, package], task_id, emitter, - ).await?; + ) + .await?; if !retry_success { if retry_stderr.contains("EACCES") { emit_agent_install_event( - emitter, task_id, AgentInstallEventKind::Log, + emitter, + task_id, + AgentInstallEventKind::Log, "Permission denied on --force retry, falling back to user prefix...", ); return install_npm_to_user_prefix_streaming( - package, ®istry_arg, task_id, emitter, - ).await; + package, + ®istry_arg, + task_id, + emitter, + ) + .await; } let err = retry_stderr.trim().to_string(); let msg = if err.is_empty() { @@ -424,7 +432,9 @@ async fn install_npm_to_user_prefix_streaming( let prefix_arg = format!("--prefix={}", prefix.display()); emit_agent_install_event( - emitter, task_id, AgentInstallEventKind::Log, + emitter, + task_id, + AgentInstallEventKind::Log, format!("$ npm install -g --prefix={} {package}", prefix.display()), ); @@ -432,21 +442,32 @@ async fn install_npm_to_user_prefix_streaming( &["install", "-g", &prefix_arg, registry_arg, package], task_id, emitter, - ).await?; + ) + .await?; if !success { // EEXIST in the user prefix: retry with --force to overwrite stale files // from a previous installation. if stderr.contains("EEXIST") { emit_agent_install_event( - emitter, task_id, AgentInstallEventKind::Log, + emitter, + task_id, + AgentInstallEventKind::Log, "File conflict in user prefix, retrying with --force...", ); let (force_success, force_stderr) = run_npm_streaming( - &["install", "-g", "--force", &prefix_arg, registry_arg, package], + &[ + "install", + "-g", + "--force", + &prefix_arg, + registry_arg, + package, + ], task_id, emitter, - ).await?; + ) + .await?; if !force_success { let err = force_stderr.trim().to_string(); let msg = if err.is_empty() { @@ -854,10 +875,7 @@ fn persist_cline_local_config(config_patch_json: Option<&str>) -> Result<(), Acp act_model_key.to_string(), serde_json::Value::String(model.clone()), ); - gs_obj.insert( - plan_model_key.to_string(), - serde_json::Value::String(model), - ); + gs_obj.insert(plan_model_key.to_string(), serde_json::Value::String(model)); } None => { gs_obj.remove(act_model_key); @@ -888,9 +906,8 @@ fn persist_cline_local_config(config_patch_json: Option<&str>) -> Result<(), Acp } if let Some(parent) = gs_path.parent() { - fs::create_dir_all(parent).map_err(|e| { - AcpError::protocol(format!("create cline data directory failed: {e}")) - })?; + fs::create_dir_all(parent) + .map_err(|e| AcpError::protocol(format!("create cline data directory failed: {e}")))?; } let serialized_gs = serde_json::to_string_pretty(&gs) .map_err(|e| AcpError::protocol(format!("serialize cline globalState failed: {e}")))?; @@ -917,10 +934,7 @@ fn persist_cline_local_config(config_patch_json: Option<&str>) -> Result<(), Acp let key_field = cline_api_key_field_for_provider(&provider); match trim_non_empty(runtime.api_key) { Some(api_key) => { - secrets_obj.insert( - key_field.to_string(), - serde_json::Value::String(api_key), - ); + secrets_obj.insert(key_field.to_string(), serde_json::Value::String(api_key)); } None => { secrets_obj.remove(key_field); @@ -928,9 +942,8 @@ fn persist_cline_local_config(config_patch_json: Option<&str>) -> Result<(), Acp } if let Some(parent) = secrets_path.parent() { - fs::create_dir_all(parent).map_err(|e| { - AcpError::protocol(format!("create cline data directory failed: {e}")) - })?; + fs::create_dir_all(parent) + .map_err(|e| AcpError::protocol(format!("create cline data directory failed: {e}")))?; } let serialized_secrets = serde_json::to_string_pretty(&secrets) .map_err(|e| AcpError::protocol(format!("serialize cline secrets failed: {e}")))?; @@ -1719,7 +1732,11 @@ fn trim_non_empty(value: Option) -> Option { /// Shared by runtime env resolution, model-provider cascade, and config patching. fn agent_env_keys(agent_type: AgentType) -> (&'static str, &'static str, &'static str) { match agent_type { - AgentType::ClaudeCode => ("ANTHROPIC_BASE_URL", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_MODEL"), + AgentType::ClaudeCode => ( + "ANTHROPIC_BASE_URL", + "ANTHROPIC_AUTH_TOKEN", + "ANTHROPIC_MODEL", + ), AgentType::Gemini => ("GOOGLE_GEMINI_BASE_URL", "GEMINI_API_KEY", "GEMINI_MODEL"), _ => ("OPENAI_BASE_URL", "OPENAI_API_KEY", "OPENAI_MODEL"), } @@ -1814,11 +1831,17 @@ fn cascade_update_agent_config( AgentType::ClaudeCode | AgentType::Gemini => { // Write into config.env (not root-level) let mut env = serde_json::Map::new(); - env.insert(url_key.to_string(), serde_json::Value::String(api_url.to_string())); - env.insert(key_key.to_string(), serde_json::Value::String(api_key.to_string())); + env.insert( + url_key.to_string(), + serde_json::Value::String(api_url.to_string()), + ); + env.insert( + key_key.to_string(), + serde_json::Value::String(api_key.to_string()), + ); let patch = serde_json::json!({ "env": env }); - let patch_str = serde_json::to_string(&patch) - .map_err(|e| AcpError::protocol(e.to_string()))?; + let patch_str = + serde_json::to_string(&patch).map_err(|e| AcpError::protocol(e.to_string()))?; persist_agent_local_config_json(agent_type, Some(&patch_str))?; } AgentType::OpenClaw => { @@ -1855,7 +1878,10 @@ fn cascade_update_agent_config( if api_url.trim().is_empty() { table.remove("api_base_url"); } else { - table.insert("api_base_url".to_string(), toml::Value::String(api_url.to_string())); + table.insert( + "api_base_url".to_string(), + toml::Value::String(api_url.to_string()), + ); } } let toml_str = toml::to_string_pretty(&toml_value) @@ -1882,8 +1908,8 @@ fn cascade_update_agent_config( persist_opencode_auth_json(&auth_str)?; let patch = serde_json::json!({ "apiBaseUrl": api_url }); - let patch_str = serde_json::to_string(&patch) - .map_err(|e| AcpError::protocol(e.to_string()))?; + let patch_str = + serde_json::to_string(&patch).map_err(|e| AcpError::protocol(e.to_string()))?; persist_agent_local_config_json(agent_type, Some(&patch_str))?; } AgentType::Cline => {} @@ -2106,17 +2132,11 @@ pub(crate) async fn acp_get_agent_status_core( true, setting.as_ref().and_then(|m| m.installed_version.clone()), ), - registry::AgentDistribution::Binary { - platforms, cmd, .. - } => { - let detected = - binary_cache::detect_installed_version(agent_type, cmd) - .ok() - .flatten(); - ( - platforms.iter().any(|p| p.platform == platform), - detected, - ) + registry::AgentDistribution::Binary { platforms, cmd, .. } => { + let detected = binary_cache::detect_installed_version(agent_type, cmd) + .ok() + .flatten(); + (platforms.iter().any(|p| p.platform == platform), detected) } }; @@ -2137,9 +2157,7 @@ pub async fn acp_get_agent_status( acp_get_agent_status_core(agent_type, &db).await } -pub(crate) async fn acp_list_agents_core( - db: &AppDatabase, -) -> Result, AcpError> { +pub(crate) async fn acp_list_agents_core(db: &AppDatabase) -> Result, AcpError> { let platform = registry::current_platform(); let agent_types = registry::all_acp_agents(); @@ -2417,9 +2435,17 @@ pub async fn acp_update_agent_preferences( ) -> Result<(), AcpError> { let emitter = EventEmitter::Tauri(app); acp_update_agent_preferences_core( - agent_type, enabled, env, config_json, opencode_auth_json, - codex_auth_json, codex_config_toml, &db, &emitter, - ).await + agent_type, + enabled, + env, + config_json, + opencode_auth_json, + codex_auth_json, + codex_config_toml, + &db, + &emitter, + ) + .await } pub(crate) async fn acp_update_agent_env_core( @@ -2557,7 +2583,12 @@ pub async fn acp_update_agent_config( ) -> Result<(), AcpError> { let emitter = EventEmitter::Tauri(app); acp_update_agent_config_core( - agent_type, config_json, opencode_auth_json, codex_auth_json, codex_config_toml, &emitter, + agent_type, + config_json, + opencode_auth_json, + codex_auth_json, + codex_config_toml, + &emitter, ) .await } @@ -2589,17 +2620,25 @@ pub(crate) async fn acp_download_agent_binary_core( })?; emit_agent_install_event( - emitter, &task_id, AgentInstallEventKind::Log, + emitter, + &task_id, + AgentInstallEventKind::Log, format!("Downloading {} v{version} for {platform}", meta.name), ); let emitter_clone = emitter.clone(); let task_id_clone = task_id.clone(); let _ = binary_cache::ensure_binary_for_agent_with_progress( - agent_type, version, fallback.url, cmd, + agent_type, + version, + fallback.url, + cmd, move |msg| { emit_agent_install_event( - &emitter_clone, &task_id_clone, AgentInstallEventKind::Log, msg, + &emitter_clone, + &task_id_clone, + AgentInstallEventKind::Log, + msg, ); }, ) @@ -2607,21 +2646,26 @@ pub(crate) async fn acp_download_agent_binary_core( emit_acp_agents_updated(emitter, "binary_downloaded", Some(agent_type)); Ok(()) } - registry::AgentDistribution::Npx { .. } => Err( - AcpError::protocol("download is only supported for binary agents"), - ), + registry::AgentDistribution::Npx { .. } => Err(AcpError::protocol( + "download is only supported for binary agents", + )), }; match &result { Ok(()) => { emit_agent_install_event( - emitter, &task_id, AgentInstallEventKind::Completed, + emitter, + &task_id, + AgentInstallEventKind::Completed, format!("{} installed successfully", meta.name), ); } Err(e) => { emit_agent_install_event( - emitter, &task_id, AgentInstallEventKind::Failed, e.to_string(), + emitter, + &task_id, + AgentInstallEventKind::Failed, + e.to_string(), ); } } @@ -2645,12 +2689,9 @@ pub(crate) async fn acp_detect_agent_local_version_core( ) -> Result, AcpError> { let detected = detect_local_version(agent_type).await; if let Some(version) = detected.clone() { - let _ = agent_setting_service::set_installed_version( - conn, - agent_type, - Some(version.clone()), - ) - .await; + let _ = + agent_setting_service::set_installed_version(conn, agent_type, Some(version.clone())) + .await; return Ok(Some(version)); } @@ -2699,13 +2740,17 @@ pub(crate) async fn acp_prepare_npx_agent_core( .and_then(|m| m.installed_version); emit_agent_install_event( - emitter, &task_id, AgentInstallEventKind::Log, + emitter, + &task_id, + AgentInstallEventKind::Log, format!("Installing {} ({package})", meta.name), ); install_npm_global_package_streaming(package, &task_id, emitter).await?; emit_agent_install_event( - emitter, &task_id, AgentInstallEventKind::Log, + emitter, + &task_id, + AgentInstallEventKind::Log, "Detecting installed version...", ); let resolved = detect_local_version(agent_type) @@ -2741,13 +2786,18 @@ pub(crate) async fn acp_prepare_npx_agent_core( match &result { Ok(version) => { emit_agent_install_event( - emitter, &task_id, AgentInstallEventKind::Completed, + emitter, + &task_id, + AgentInstallEventKind::Completed, format!("{} v{version} installed successfully", meta.name), ); } Err(e) => { emit_agent_install_event( - emitter, &task_id, AgentInstallEventKind::Failed, e.to_string(), + emitter, + &task_id, + AgentInstallEventKind::Failed, + e.to_string(), ); } } @@ -2777,7 +2827,9 @@ pub(crate) async fn acp_uninstall_agent_core( let meta = registry::get_agent_meta(agent_type); emit_agent_install_event( - emitter, &task_id, AgentInstallEventKind::Log, + emitter, + &task_id, + AgentInstallEventKind::Log, format!("Uninstalling {}...", meta.name), ); @@ -2802,13 +2854,18 @@ pub(crate) async fn acp_uninstall_agent_core( match &result { Ok(()) => { emit_agent_install_event( - emitter, &task_id, AgentInstallEventKind::Completed, + emitter, + &task_id, + AgentInstallEventKind::Completed, format!("{} uninstalled successfully", meta.name), ); } Err(e) => { emit_agent_install_event( - emitter, &task_id, AgentInstallEventKind::Failed, e.to_string(), + emitter, + &task_id, + AgentInstallEventKind::Failed, + e.to_string(), ); } } @@ -2868,9 +2925,7 @@ pub async fn acp_list_agent_skills( let Some(spec) = skill_storage_spec(agent_type) else { return Ok(AgentSkillsListResult { supported: false, - message: Some(format!( - "{agent_type} 暂不支持在设置页管理 Skills" - )), + message: Some(format!("{agent_type} 暂不支持在设置页管理 Skills")), locations: Vec::new(), skills: Vec::new(), }); @@ -3033,8 +3088,7 @@ pub async fn acp_delete_agent_skill( } pub(crate) async fn opencode_list_plugins_core() -> Result { - opencode_plugins::check_opencode_plugins(None) - .map_err(|e| AcpError::Protocol(e)) + opencode_plugins::check_opencode_plugins(None).map_err(AcpError::Protocol) } #[cfg_attr(feature = "tauri-runtime", tauri::command)] @@ -3049,7 +3103,7 @@ pub(crate) async fn opencode_install_plugins_core( ) -> Result<(), AcpError> { opencode_plugins::install_missing_plugins(names, task_id, emitter) .await - .map_err(|e| AcpError::Protocol(e)) + .map_err(AcpError::Protocol) } #[cfg(feature = "tauri-runtime")] @@ -3068,13 +3122,11 @@ pub(crate) async fn opencode_uninstall_plugin_core( ) -> Result { opencode_plugins::uninstall_plugin(name) .await - .map_err(|e| AcpError::Protocol(e)) + .map_err(AcpError::Protocol) } #[cfg_attr(feature = "tauri-runtime", tauri::command)] -pub async fn opencode_uninstall_plugin( - name: String, -) -> Result { +pub async fn opencode_uninstall_plugin(name: String) -> Result { opencode_uninstall_plugin_core(name).await } @@ -3108,7 +3160,10 @@ struct DeviceCodeUserCodeResp { device_auth_id: String, #[serde(alias = "usercode")] user_code: String, - #[serde(default = "default_interval", deserialize_with = "deserialize_interval")] + #[serde( + default = "default_interval", + deserialize_with = "deserialize_interval" + )] interval: u64, } @@ -3118,11 +3173,8 @@ fn default_interval() -> u64 { fn extract_jwt_account_id(jwt: &str) -> Option { let payload = jwt.split('.').nth(1)?; - let decoded = base64::Engine::decode( - &base64::engine::general_purpose::URL_SAFE_NO_PAD, - payload, - ) - .ok()?; + let decoded = + base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, payload).ok()?; let value: serde_json::Value = serde_json::from_slice(&decoded).ok()?; value .get("https://api.openai.com/auth") @@ -3138,11 +3190,13 @@ where use serde::de; let value = serde_json::Value::deserialize(deserializer)?; match &value { - serde_json::Value::Number(n) => n.as_u64().ok_or_else(|| { - de::Error::custom(format!("invalid interval number: {n}")) - }), + serde_json::Value::Number(n) => n + .as_u64() + .ok_or_else(|| de::Error::custom(format!("invalid interval number: {n}"))), serde_json::Value::String(s) => s.trim().parse::().map_err(de::Error::custom), - _ => Err(de::Error::custom(format!("unexpected interval type: {value}"))), + _ => Err(de::Error::custom(format!( + "unexpected interval type: {value}" + ))), } } @@ -3161,9 +3215,7 @@ struct OAuthTokenResp { refresh_token: String, } -pub(crate) async fn codex_request_device_code_core() - -> Result -{ +pub(crate) async fn codex_request_device_code_core() -> Result { let client = reqwest::Client::new(); let url = format!("{CODEX_OAUTH_ISSUER}/api/accounts/deviceauth/usercode"); let body = serde_json::json!({ "client_id": CODEX_OAUTH_CLIENT_ID }); @@ -3187,8 +3239,11 @@ pub(crate) async fn codex_request_device_code_core() .text() .await .map_err(|e| AcpError::protocol(format!("read device code response failed: {e}")))?; - let uc: DeviceCodeUserCodeResp = serde_json::from_str(&raw_body) - .map_err(|e| AcpError::protocol(format!("parse device code response failed: {e} | body: {raw_body}")))?; + let uc: DeviceCodeUserCodeResp = serde_json::from_str(&raw_body).map_err(|e| { + AcpError::protocol(format!( + "parse device code response failed: {e} | body: {raw_body}" + )) + })?; Ok(CodexDeviceCodeResponse { user_code: uc.user_code, @@ -3199,9 +3254,7 @@ pub(crate) async fn codex_request_device_code_core() } #[cfg_attr(feature = "tauri-runtime", tauri::command)] -pub async fn codex_request_device_code() - -> Result -{ +pub async fn codex_request_device_code() -> Result { codex_request_device_code_core().await } diff --git a/src-tauri/src/commands/chat_channel.rs b/src-tauri/src/commands/chat_channel.rs index bbb4914..4b24efb 100644 --- a/src-tauri/src/commands/chat_channel.rs +++ b/src-tauri/src/commands/chat_channel.rs @@ -98,20 +98,19 @@ pub async fn connect_chat_channel_core( .map_err(AppCommandError::from)? .ok_or_else(|| AppCommandError::not_found(format!("Chat channel {id} not found")))?; - let channel_type: ChannelType = - serde_json::from_value(serde_json::Value::String(model.channel_type.clone())) - .map_err(|_| { - AppCommandError::configuration_invalid(format!( - "Invalid channel type: {}", - model.channel_type - )) - })?; + let channel_type: ChannelType = serde_json::from_value(serde_json::Value::String( + model.channel_type.clone(), + )) + .map_err(|_| { + AppCommandError::configuration_invalid(format!( + "Invalid channel type: {}", + model.channel_type + )) + })?; - let config: serde_json::Value = - serde_json::from_str(&model.config_json).map_err(|e| { - AppCommandError::configuration_invalid("Invalid config JSON") - .with_detail(e.to_string()) - })?; + let config: serde_json::Value = serde_json::from_str(&model.config_json).map_err(|e| { + AppCommandError::configuration_invalid("Invalid config JSON").with_detail(e.to_string()) + })?; let token = crate::keyring_store::get_channel_token(id).ok_or_else(|| { eprintln!("[connect_chat_channel] channel {id}: Token not set in keyring"); @@ -138,29 +137,25 @@ pub async fn connect_chat_channel_core( Ok(()) } -pub async fn test_chat_channel_core( - db: &AppDatabase, - id: i32, -) -> Result<(), AppCommandError> { +pub async fn test_chat_channel_core(db: &AppDatabase, id: i32) -> Result<(), AppCommandError> { let model = chat_channel_service::get_by_id(&db.conn, id) .await .map_err(AppCommandError::from)? .ok_or_else(|| AppCommandError::not_found(format!("Chat channel {id} not found")))?; - let channel_type: ChannelType = - serde_json::from_value(serde_json::Value::String(model.channel_type.clone())) - .map_err(|_| { - AppCommandError::configuration_invalid(format!( - "Invalid channel type: {}", - model.channel_type - )) - })?; + let channel_type: ChannelType = serde_json::from_value(serde_json::Value::String( + model.channel_type.clone(), + )) + .map_err(|_| { + AppCommandError::configuration_invalid(format!( + "Invalid channel type: {}", + model.channel_type + )) + })?; - let config: serde_json::Value = - serde_json::from_str(&model.config_json).map_err(|e| { - AppCommandError::configuration_invalid("Invalid config JSON") - .with_detail(e.to_string()) - })?; + let config: serde_json::Value = serde_json::from_str(&model.config_json).map_err(|e| { + AppCommandError::configuration_invalid("Invalid config JSON").with_detail(e.to_string()) + })?; let token = crate::keyring_store::get_channel_token(id) .ok_or_else(|| AppCommandError::configuration_missing("Token not set"))?; @@ -215,18 +210,20 @@ pub async fn list_chat_channel_messages_core( ) -> Result, AppCommandError> { let limit = limit.unwrap_or(50); let offset = offset.unwrap_or(0); - let rows = chat_channel_message_log_service::list_by_channel(&db.conn, channel_id, limit, offset) - .await - .map_err(AppCommandError::from)?; - Ok(rows.into_iter().map(ChatChannelMessageLogInfo::from).collect()) + let rows = + chat_channel_message_log_service::list_by_channel(&db.conn, channel_id, limit, offset) + .await + .map_err(AppCommandError::from)?; + Ok(rows + .into_iter() + .map(ChatChannelMessageLogInfo::from) + .collect()) } const COMMAND_PREFIX_KEY: &str = "chat_command_prefix"; const DEFAULT_COMMAND_PREFIX: &str = "/"; -pub async fn get_chat_command_prefix_core( - db: &AppDatabase, -) -> Result { +pub async fn get_chat_command_prefix_core(db: &AppDatabase) -> Result { let val = crate::db::service::app_metadata_service::get_value(&db.conn, COMMAND_PREFIX_KEY) .await .map_err(AppCommandError::from)?; @@ -238,10 +235,7 @@ pub async fn set_chat_command_prefix_core( prefix: String, ) -> Result<(), AppCommandError> { let trimmed = prefix.trim(); - if trimmed.is_empty() - || trimmed.len() > 3 - || trimmed.chars().any(|c| c.is_alphanumeric()) - { + if trimmed.is_empty() || trimmed.len() > 3 || trimmed.chars().any(|c| c.is_alphanumeric()) { return Err(AppCommandError::invalid_input( "Prefix must be 1-3 non-alphanumeric characters", )); @@ -254,9 +248,7 @@ pub async fn set_chat_command_prefix_core( const MESSAGE_LANGUAGE_KEY: &str = "chat_message_language"; -pub async fn get_chat_message_language_core( - db: &AppDatabase, -) -> Result { +pub async fn get_chat_message_language_core(db: &AppDatabase) -> Result { let val = crate::db::service::app_metadata_service::get_value(&db.conn, MESSAGE_LANGUAGE_KEY) .await .map_err(AppCommandError::from)?; @@ -360,14 +352,20 @@ pub async fn weixin_check_qrcode_core( if result.status == "confirmed" { eprintln!( "[Weixin] QR confirmed for channel {channel_id}, bot_token={}, base_url={}", - result.bot_token.as_deref().map(|t| if t.len() > 8 { &t[..8] } else { t }).unwrap_or("None"), + result + .bot_token + .as_deref() + .map(|t| if t.len() > 8 { &t[..8] } else { t }) + .unwrap_or("None"), result.base_url.as_deref().unwrap_or("None"), ); if let Some(ref token) = result.bot_token { save_chat_channel_token_core(channel_id, token)?; eprintln!("[Weixin] Token saved for channel {channel_id}"); } else { - eprintln!("[Weixin] WARNING: No bot_token in confirmed response for channel {channel_id}"); + eprintln!( + "[Weixin] WARNING: No bot_token in confirmed response for channel {channel_id}" + ); } if let Some(ref base_url) = result.base_url { let config_json = serde_json::json!({ "base_url": base_url }).to_string(); @@ -415,7 +413,16 @@ pub async fn create_chat_channel( daily_report_enabled: bool, daily_report_time: Option, ) -> Result { - create_chat_channel_core(&db, name, channel_type, config_json, enabled, daily_report_enabled, daily_report_time).await + create_chat_channel_core( + &db, + name, + channel_type, + config_json, + enabled, + daily_report_enabled, + daily_report_time, + ) + .await } #[allow(clippy::too_many_arguments)] @@ -431,7 +438,17 @@ pub async fn update_chat_channel( daily_report_enabled: Option, daily_report_time: Option>, ) -> Result { - update_chat_channel_core(&db, id, name, enabled, config_json, event_filter_json, daily_report_enabled, daily_report_time).await + update_chat_channel_core( + &db, + id, + name, + enabled, + config_json, + event_filter_json, + daily_report_enabled, + daily_report_time, + ) + .await } #[cfg(feature = "tauri-runtime")] @@ -446,7 +463,10 @@ pub async fn delete_chat_channel( #[cfg(feature = "tauri-runtime")] #[tauri::command] -pub async fn save_chat_channel_token(channel_id: i32, token: String) -> Result<(), AppCommandError> { +pub async fn save_chat_channel_token( + channel_id: i32, + token: String, +) -> Result<(), AppCommandError> { save_chat_channel_token_core(channel_id, &token) } diff --git a/src-tauri/src/commands/conversations.rs b/src-tauri/src/commands/conversations.rs index 94eae99..29c1ee8 100644 --- a/src-tauri/src/commands/conversations.rs +++ b/src-tauri/src/commands/conversations.rs @@ -3,9 +3,9 @@ use std::collections::{HashMap, HashSet}; use crate::app_error::AppCommandError; #[cfg(feature = "tauri-runtime")] use crate::db::entities::conversation; -use crate::db::service::{conversation_service, folder_service}; #[cfg(feature = "tauri-runtime")] use crate::db::service::import_service; +use crate::db::service::{conversation_service, folder_service}; #[cfg(feature = "tauri-runtime")] use crate::db::AppDatabase; use crate::models::*; @@ -288,8 +288,7 @@ pub async fn get_folder_conversation_core( .await .map_err(AppCommandError::from)?; - let (turns, session_stats, resolved_ext_id) = if let Some(ref ext_id) = summary.external_id - { + let (turns, session_stats, resolved_ext_id) = if let Some(ref ext_id) = summary.external_id { let at = summary.agent_type; let eid = ext_id.clone(); let db_created_at = summary.created_at; @@ -317,7 +316,10 @@ pub async fn get_folder_conversation_core( // ID after session/new fallback overwrote the original // (Gemini CLI). Fall back to matching by folder_path // and started_at from the parsed conversation list. - if matches!(at, AgentType::OpenClaw | AgentType::Cline | AgentType::Gemini) { + if matches!( + at, + AgentType::OpenClaw | AgentType::Cline | AgentType::Gemini + ) { if let Ok(all) = parser.list_conversations() { // Filter by folder_path first, then find the closest // started_at match within 300 seconds of db_created_at. @@ -333,17 +335,14 @@ pub async fn get_folder_conversation_core( (c.started_at - db_created_at).num_seconds().unsigned_abs() }) .filter(|c| { - let diff = (c.started_at - db_created_at).num_seconds().unsigned_abs(); + let diff = + (c.started_at - db_created_at).num_seconds().unsigned_abs(); diff < 300 }); if let Some(conv) = matched { let new_ext_id = conv.id.clone(); if let Ok(d) = parser.get_conversation(&new_ext_id) { - return Ok(( - d.turns, - d.session_stats, - Some(new_ext_id), - )); + return Ok((d.turns, d.session_stats, Some(new_ext_id))); } } } @@ -367,8 +366,7 @@ pub async fn get_folder_conversation_core( // If we resolved a different external_id (e.g. ACP UUID → parser branch ID), // update the database so future lookups are direct. if let Some(new_ext_id) = resolved_ext_id { - let _ = - conversation_service::update_external_id(conn, conversation_id, new_ext_id).await; + let _ = conversation_service::update_external_id(conn, conversation_id, new_ext_id).await; } let mut summary = summary; diff --git a/src-tauri/src/commands/experts.rs b/src-tauri/src/commands/experts.rs index 50f7457..64960f0 100644 --- a/src-tauri/src/commands/experts.rs +++ b/src-tauri/src/commands/experts.rs @@ -23,11 +23,11 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use tokio::sync::Mutex; +use crate::acp::types::AgentSkillScope; use crate::commands::acp::{ preferred_scope_skill_dir, remove_skill_entry, scoped_skill_dirs, skill_storage_spec, validate_skill_id, }; -use crate::acp::types::AgentSkillScope; use crate::models::agent::AgentType; // ─── Embedded bundle ──────────────────────────────────────────────────── @@ -373,9 +373,7 @@ fn create_link_raw(src: &Path, dst: &Path) -> io::Result { copy_dir_recursive(src, dst).map_err(|copy_err| { io::Error::new( io::ErrorKind::Other, - format!( - "junction failed ({junction_err}); copy fallback failed ({copy_err})" - ), + format!("junction failed ({junction_err}); copy fallback failed ({copy_err})"), ) })?; Ok(true) @@ -518,9 +516,7 @@ fn ensure_central_experts_installed_blocking() -> InstallReport { report.pending_user_review.push(meta.id.clone()); } Err(e) => { - report - .errors - .push(format!("{}: {}", meta.id, e)); + report.errors.push(format!("{}: {}", meta.id, e)); } } } @@ -639,10 +635,10 @@ fn extract_bundle_dir( .path() .to_str() .ok_or_else(|| ExpertsError::Io("non-utf8 path in bundle".into()))?; - let rel_within = - rel.strip_prefix(bundle_prefix) - .and_then(|s| s.strip_prefix('/')) - .unwrap_or(rel); + let rel_within = rel + .strip_prefix(bundle_prefix) + .and_then(|s| s.strip_prefix('/')) + .unwrap_or(rel); let out_path = target.join(rel_within); if let Some(parent) = out_path.parent() { fs::create_dir_all(parent)?; @@ -686,8 +682,7 @@ pub async fn experts_list() -> Result, ExpertsError> { pub async fn experts_list_for_agent( agent_type: AgentType, ) -> Result, ExpertsError> { - let _ = skill_storage_spec(agent_type) - .ok_or(ExpertsError::UnsupportedAgent(agent_type))?; + let _ = skill_storage_spec(agent_type).ok_or(ExpertsError::UnsupportedAgent(agent_type))?; let dirs = scoped_skill_dirs(agent_type, AgentSkillScope::Global, None) .map_err(|_| ExpertsError::UnsupportedAgent(agent_type))?; @@ -727,8 +722,8 @@ pub async fn experts_list_for_agent( pub async fn experts_get_install_status( expert_id: String, ) -> Result, ExpertsError> { - let expert_id = validate_skill_id(&expert_id) - .map_err(|e| ExpertsError::Metadata(e.to_string()))?; + let expert_id = + validate_skill_id(&expert_id).map_err(|e| ExpertsError::Metadata(e.to_string()))?; let _ = find_metadata(&expert_id)?; // ensure it exists in the bundle let expected = expert_central_path(&expert_id); let agents = supported_agents(); @@ -776,8 +771,8 @@ pub async fn experts_link_to_agent( expert_id: String, agent_type: AgentType, ) -> Result { - let expert_id = validate_skill_id(&expert_id) - .map_err(|e| ExpertsError::Metadata(e.to_string()))?; + let expert_id = + validate_skill_id(&expert_id).map_err(|e| ExpertsError::Metadata(e.to_string()))?; let _ = find_metadata(&expert_id)?; let central = expert_central_path(&expert_id); if !central.exists() { @@ -819,9 +814,8 @@ pub async fn experts_link_to_agent( } ExpertLinkState::NotLinked => { // Shouldn't happen after AlreadyExists, but retry once. - create_link_raw(¢ral, &link_path).map_err(|e| ExpertsError::Io(format!( - "retry link failed: {e}" - )))?; + create_link_raw(¢ral, &link_path) + .map_err(|e| ExpertsError::Io(format!("retry link failed: {e}")))?; } } } @@ -846,8 +840,8 @@ pub async fn experts_unlink_from_agent( expert_id: String, agent_type: AgentType, ) -> Result<(), ExpertsError> { - let expert_id = validate_skill_id(&expert_id) - .map_err(|e| ExpertsError::Metadata(e.to_string()))?; + let expert_id = + validate_skill_id(&expert_id).map_err(|e| ExpertsError::Metadata(e.to_string()))?; let _guard = mutation_lock().lock().await; @@ -865,10 +859,14 @@ pub async fn experts_unlink_from_agent( continue; } let state = classify_link(&candidate, ¢ral); - if matches!(state, ExpertLinkState::LinkedToCodeg | ExpertLinkState::Broken) { + if matches!( + state, + ExpertLinkState::LinkedToCodeg | ExpertLinkState::Broken + ) { // Safe to remove a link to our central store or a broken link. - remove_skill_entry(&candidate) - .map_err(|e| ExpertsError::Io(format!("remove link {}: {e}", candidate.display())))?; + remove_skill_entry(&candidate).map_err(|e| { + ExpertsError::Io(format!("remove link {}: {e}", candidate.display())) + })?; removed = true; } else if state == ExpertLinkState::LinkedElsewhere { return Err(ExpertsError::ForeignLink { @@ -893,8 +891,8 @@ pub async fn experts_unlink_from_agent( #[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn experts_read_content(expert_id: String) -> Result { - let expert_id = validate_skill_id(&expert_id) - .map_err(|e| ExpertsError::Metadata(e.to_string()))?; + let expert_id = + validate_skill_id(&expert_id).map_err(|e| ExpertsError::Metadata(e.to_string()))?; let _ = find_metadata(&expert_id)?; let path = expert_central_path(&expert_id).join("SKILL.md"); if !path.exists() { diff --git a/src-tauri/src/commands/folders.rs b/src-tauri/src/commands/folders.rs index 2b1c8a5..48e1ccf 100644 --- a/src-tauri/src/commands/folders.rs +++ b/src-tauri/src/commands/folders.rs @@ -1192,11 +1192,7 @@ pub async fn git_checkout(path: String, branch_name: String) -> Result<(), AppCo } #[cfg_attr(feature = "tauri-runtime", tauri::command)] -pub async fn git_reset( - path: String, - commit: String, - mode: String, -) -> Result<(), AppCommandError> { +pub async fn git_reset(path: String, commit: String, mode: String) -> Result<(), AppCommandError> { let mode = mode.trim().to_lowercase(); let mode_flag = match mode.as_str() { "soft" | "mixed" | "hard" | "keep" => format!("--{mode}"), @@ -2128,10 +2124,7 @@ pub async fn git_delete_branch( .map_err(AppCommandError::io)?; if !output.status.success() { - return Err(git_command_error( - &format!("branch {flag}"), - &output.stderr, - )); + return Err(git_command_error(&format!("branch {flag}"), &output.stderr)); } Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) } diff --git a/src-tauri/src/commands/mcp.rs b/src-tauri/src/commands/mcp.rs index 9d06d7c..3fb1a57 100644 --- a/src-tauri/src/commands/mcp.rs +++ b/src-tauri/src/commands/mcp.rs @@ -1735,9 +1735,7 @@ fn upsert_openclaw_server(id: &str, spec: &Value) -> Result<(), AppCommandError> let mcp = obj .get_mut("mcp") .and_then(Value::as_object_mut) - .ok_or_else(|| { - mcp_configuration_invalid(format!("invalid mcp in {}", path.display())) - })?; + .ok_or_else(|| mcp_configuration_invalid(format!("invalid mcp in {}", path.display())))?; if !mcp.get("servers").map(Value::is_object).unwrap_or(false) { mcp.insert("servers".to_string(), Value::Object(Map::new())); diff --git a/src-tauri/src/commands/model_provider.rs b/src-tauri/src/commands/model_provider.rs index 6831c42..02c65ca 100644 --- a/src-tauri/src/commands/model_provider.rs +++ b/src-tauri/src/commands/model_provider.rs @@ -18,9 +18,7 @@ fn validate_agent_types(agent_types: &[String]) -> Result<(), AppCommandError> { } for at in agent_types { let _: AgentType = serde_json::from_value(serde_json::Value::String(at.clone())) - .map_err(|_| { - AppCommandError::invalid_input(format!("Invalid agent type: {at}")) - })?; + .map_err(|_| AppCommandError::invalid_input(format!("Invalid agent type: {at}")))?; } Ok(()) } @@ -32,20 +30,28 @@ fn validate_fields( ) -> Result<(), AppCommandError> { if let Some(n) = name { if n.len() > 256 { - return Err(AppCommandError::invalid_input("Name must be 256 characters or less")); + return Err(AppCommandError::invalid_input( + "Name must be 256 characters or less", + )); } } if let Some(u) = api_url { if u.len() > 2048 { - return Err(AppCommandError::invalid_input("API URL must be 2048 characters or less")); + return Err(AppCommandError::invalid_input( + "API URL must be 2048 characters or less", + )); } if !u.starts_with("http://") && !u.starts_with("https://") { - return Err(AppCommandError::invalid_input("API URL must start with http:// or https://")); + return Err(AppCommandError::invalid_input( + "API URL must start with http:// or https://", + )); } } if let Some(k) = api_key { if k.len() > 4096 { - return Err(AppCommandError::invalid_input("API Key must be 4096 characters or less")); + return Err(AppCommandError::invalid_input( + "API Key must be 4096 characters or less", + )); } } Ok(()) @@ -72,15 +78,9 @@ pub async fn create_model_provider_core( let agent_types_json = serde_json::to_string(&agent_types) .map_err(|e| AppCommandError::invalid_input(e.to_string()))?; - let model = model_provider_service::create( - &db.conn, - name, - api_url, - api_key, - agent_types_json, - ) - .await - .map_err(AppCommandError::from)?; + let model = model_provider_service::create(&db.conn, name, api_url, api_key, agent_types_json) + .await + .map_err(AppCommandError::from)?; Ok(ModelProviderInfo::from(model)) } @@ -122,8 +122,12 @@ pub async fn update_model_provider_core( .map_err(AppCommandError::from)?; // Cascade credential changes to all dependent agent settings and config files. - let url_changed = api_url.as_deref().is_some_and(|u| u != old_provider.api_url); - let key_changed = api_key.as_deref().is_some_and(|k| k != old_provider.api_key); + let url_changed = api_url + .as_deref() + .is_some_and(|u| u != old_provider.api_url); + let key_changed = api_key + .as_deref() + .is_some_and(|k| k != old_provider.api_key); if url_changed || key_changed { let final_url = api_url.as_deref().unwrap_or(&old_provider.api_url); let final_key = api_key.as_deref().unwrap_or(&old_provider.api_key); @@ -135,10 +139,7 @@ pub async fn update_model_provider_core( Ok(ModelProviderInfo::from(model)) } -pub async fn delete_model_provider_core( - db: &AppDatabase, - id: i32, -) -> Result<(), AppCommandError> { +pub async fn delete_model_provider_core(db: &AppDatabase, id: i32) -> Result<(), AppCommandError> { // Check if any agent settings reference this provider. let dependents = agent_setting_service::find_by_model_provider_id(&db.conn, id) .await diff --git a/src-tauri/src/commands/notification.rs b/src-tauri/src/commands/notification.rs index 7c4ac8e..7b335e3 100644 --- a/src-tauri/src/commands/notification.rs +++ b/src-tauri/src/commands/notification.rs @@ -28,12 +28,7 @@ pub async fn send_notification( #[cfg(not(target_os = "macos"))] { use tauri_plugin_notification::NotificationExt; - let _ = app - .notification() - .builder() - .title(title) - .body(body) - .show(); + let _ = app.notification().builder().title(title).body(body).show(); } Ok(()) diff --git a/src-tauri/src/commands/project_boot.rs b/src-tauri/src/commands/project_boot.rs index d6c62ef..b71cd63 100644 --- a/src-tauri/src/commands/project_boot.rs +++ b/src-tauri/src/commands/project_boot.rs @@ -29,9 +29,7 @@ async fn detect_one(name: &str) -> PackageManagerInfo { match result { Ok(output) if output.status.success() => { - let version = String::from_utf8_lossy(&output.stdout) - .trim() - .to_string(); + let version = String::from_utf8_lossy(&output.stdout).trim().to_string(); PackageManagerInfo { name: name.to_string(), installed: true, @@ -76,7 +74,9 @@ pub async fn create_shadcn_project( return Err(AppCommandError::invalid_input("Template is required")); } if target_dir.is_empty() { - return Err(AppCommandError::invalid_input("Target directory is required")); + return Err(AppCommandError::invalid_input( + "Target directory is required", + )); } let full_path = PathBuf::from(&target_dir).join(&project_name); diff --git a/src-tauri/src/commands/system_settings.rs b/src-tauri/src/commands/system_settings.rs index 3fb0e50..e82a649 100644 --- a/src-tauri/src/commands/system_settings.rs +++ b/src-tauri/src/commands/system_settings.rs @@ -139,7 +139,11 @@ pub async fn update_system_language_settings( .map_err(AppCommandError::from)?; let emitter = crate::web::event_bridge::EventEmitter::Tauri(app); - crate::web::event_bridge::emit_event(&emitter, LANGUAGE_SETTINGS_UPDATED_EVENT, settings.clone()); + crate::web::event_bridge::emit_event( + &emitter, + LANGUAGE_SETTINGS_UPDATED_EVENT, + settings.clone(), + ); Ok(settings) } diff --git a/src-tauri/src/commands/terminal.rs b/src-tauri/src/commands/terminal.rs index 661d1c2..5518a1c 100644 --- a/src-tauri/src/commands/terminal.rs +++ b/src-tauri/src/commands/terminal.rs @@ -32,16 +32,14 @@ pub(crate) fn prepare_credential_env( } }; - let helper_script = match git_credential::create_credential_helper_script( - app_data_dir, - &app_binary, - ) { - Ok(p) => p, - Err(e) => { - eprintln!("[TERM] failed to create credential helper script: {}", e); - return None; - } - }; + let helper_script = + match git_credential::create_credential_helper_script(app_data_dir, &app_binary) { + Ok(p) => p, + Err(e) => { + eprintln!("[TERM] failed to create credential helper script: {}", e); + return None; + } + }; let helper_path_str = helper_script.to_string_lossy().to_string(); diff --git a/src-tauri/src/commands/version_control.rs b/src-tauri/src/commands/version_control.rs index bfbf4e5..6e2ad38 100644 --- a/src-tauri/src/commands/version_control.rs +++ b/src-tauri/src/commands/version_control.rs @@ -24,9 +24,7 @@ async fn run_git_version(git_path: &str) -> Result, -) -> Result { +pub async fn detect_git(db: State<'_, AppDatabase>) -> Result { detect_git_core(&db.conn).await } @@ -145,9 +141,7 @@ async fn load_git_settings( #[cfg(feature = "tauri-runtime")] #[cfg_attr(feature = "tauri-runtime", tauri::command)] -pub async fn get_git_settings( - db: State<'_, AppDatabase>, -) -> Result { +pub async fn get_git_settings(db: State<'_, AppDatabase>) -> Result { load_git_settings(&db.conn).await } @@ -221,27 +215,21 @@ pub async fn update_github_accounts( // --------------------------------------------------------------------------- #[cfg_attr(feature = "tauri-runtime", tauri::command)] -pub async fn save_account_token( - account_id: String, - token: String, -) -> Result<(), AppCommandError> { +pub async fn save_account_token(account_id: String, token: String) -> Result<(), AppCommandError> { crate::keyring_store::set_token(&account_id, &token) .map_err(|e| AppCommandError::io_error("Failed to save token to keyring").with_detail(e)) } #[cfg_attr(feature = "tauri-runtime", tauri::command)] -pub async fn get_account_token( - account_id: String, -) -> Result, AppCommandError> { +pub async fn get_account_token(account_id: String) -> Result, AppCommandError> { Ok(crate::keyring_store::get_token(&account_id)) } #[cfg_attr(feature = "tauri-runtime", tauri::command)] -pub async fn delete_account_token( - account_id: String, -) -> Result<(), AppCommandError> { - crate::keyring_store::delete_token(&account_id) - .map_err(|e| AppCommandError::io_error("Failed to delete token from keyring").with_detail(e)) +pub async fn delete_account_token(account_id: String) -> Result<(), AppCommandError> { + crate::keyring_store::delete_token(&account_id).map_err(|e| { + AppCommandError::io_error("Failed to delete token from keyring").with_detail(e) + }) } // --------------------------------------------------------------------------- @@ -283,7 +271,9 @@ pub async fn validate_github_token( .header("Accept", "application/vnd.github+json") .send() .await - .map_err(|e| AppCommandError::network("Failed to connect to GitHub API").with_detail(e.to_string()))?; + .map_err(|e| { + AppCommandError::network("Failed to connect to GitHub API").with_detail(e.to_string()) + })?; if !response.status().is_success() { let status = response.status().as_u16(); @@ -315,13 +305,9 @@ pub async fn validate_github_token( }) .unwrap_or_default(); - let user = response - .json::() - .await - .map_err(|e| { - AppCommandError::network("Failed to parse GitHub API response") - .with_detail(e.to_string()) - })?; + let user = response.json::().await.map_err(|e| { + AppCommandError::network("Failed to parse GitHub API response").with_detail(e.to_string()) + })?; Ok(GitHubTokenValidation { success: true, diff --git a/src-tauri/src/commands/windows.rs b/src-tauri/src/commands/windows.rs index 065b57a..56e988b 100644 --- a/src-tauri/src/commands/windows.rs +++ b/src-tauri/src/commands/windows.rs @@ -1,15 +1,15 @@ use std::collections::HashMap; -use std::sync::Mutex; -use std::sync::atomic::{AtomicU8, Ordering as AtomicOrdering}; #[cfg(target_os = "macos")] use std::sync::atomic::AtomicU32; +use std::sync::atomic::{AtomicU8, Ordering as AtomicOrdering}; +use std::sync::Mutex; use sea_orm::DatabaseConnection; use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder}; use crate::app_error::AppCommandError; -use crate::db::AppDatabase; use crate::db::service::app_metadata_service; +use crate::db::AppDatabase; use crate::models::FolderDetail; /// Base traffic-light position (logical px) at 100 % zoom. @@ -123,12 +123,15 @@ fn is_system_dark_mode() -> bool { // Output: " AppsUseLightTheme REG_DWORD 0x0" // Extract the last token on the matching line to avoid // substring false-positives (e.g. "0x00000001" contains "0x0"). - stdout.lines().find(|l| l.contains("AppsUseLightTheme")).map(|line| { - line.split_whitespace() - .last() - .map(|val| val == "0x0" || val == "0x00000000") - .unwrap_or(false) - }) + stdout + .lines() + .find(|l| l.contains("AppsUseLightTheme")) + .map(|line| { + line.split_whitespace() + .last() + .map(|val| val == "0x0" || val == "0x00000000") + .unwrap_or(false) + }) }) .unwrap_or(false) }) @@ -263,6 +266,12 @@ impl SettingsWindowState { } } +impl Default for SettingsWindowState { + fn default() -> Self { + Self::new() + } +} + impl CommitWindowState { pub fn new() -> Self { Self { @@ -284,6 +293,12 @@ impl CommitWindowState { } } +impl Default for CommitWindowState { + fn default() -> Self { + Self::new() + } +} + fn resolve_settings_route(section: Option<&str>) -> &'static str { match section { Some("appearance") => "settings/appearance", @@ -508,6 +523,12 @@ impl MergeWindowState { } } +impl Default for MergeWindowState { + fn default() -> Self { + Self::new() + } +} + #[cfg(feature = "tauri-runtime")] #[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn open_merge_window( @@ -738,11 +759,9 @@ pub async fn open_project_boot_window( .inner_size(1400.0, 900.0) .min_inner_size(1100.0, 700.0) .center(); - let window = apply_platform_window_style(builder) - .build() - .map_err(|e| { - AppCommandError::window("Failed to open project boot window", e.to_string()) - })?; + let window = apply_platform_window_style(builder).build().map_err(|e| { + AppCommandError::window("Failed to open project boot window", e.to_string()) + })?; post_window_setup(&window); Ok(()) @@ -764,12 +783,8 @@ pub async fn update_traffic_light_position( CURRENT_ZOOM.store(clamped, AtomicOrdering::Relaxed); // Persist to DB so the next launch reads the correct value. - let _ = app_metadata_service::upsert_value( - &db.conn, - ZOOM_LEVEL_DB_KEY, - &clamped.to_string(), - ) - .await; + let _ = + app_metadata_service::upsert_value(&db.conn, ZOOM_LEVEL_DB_KEY, &clamped.to_string()).await; let _ = app; Ok(()) @@ -786,13 +801,7 @@ pub async fn update_appearance_mode( ) -> Result<(), AppCommandError> { CACHED_APPEARANCE_MODE.store(mode_from_str(&mode), AtomicOrdering::Relaxed); - let _ = app_metadata_service::upsert_value( - &db.conn, - APPEARANCE_MODE_DB_KEY, - &mode, - ) - .await; + let _ = app_metadata_service::upsert_value(&db.conn, APPEARANCE_MODE_DB_KEY, &mode).await; Ok(()) } - diff --git a/src-tauri/src/db/migration/m20260330_000001_chat_channel.rs b/src-tauri/src/db/migration/m20260330_000001_chat_channel.rs index 28f9bce..556345e 100644 --- a/src-tauri/src/db/migration/m20260330_000001_chat_channel.rs +++ b/src-tauri/src/db/migration/m20260330_000001_chat_channel.rs @@ -20,22 +20,14 @@ impl MigrationTrait for Migration { .primary_key(), ) .col(ColumnDef::new(ChatChannel::Name).string().not_null()) - .col( - ColumnDef::new(ChatChannel::ChannelType) - .string() - .not_null(), - ) + .col(ColumnDef::new(ChatChannel::ChannelType).string().not_null()) .col( ColumnDef::new(ChatChannel::Enabled) .boolean() .not_null() .default(true), ) - .col( - ColumnDef::new(ChatChannel::ConfigJson) - .text() - .not_null(), - ) + .col(ColumnDef::new(ChatChannel::ConfigJson).text().not_null()) .col(ColumnDef::new(ChatChannel::EventFilterJson).text().null()) .col( ColumnDef::new(ChatChannel::DailyReportEnabled) @@ -43,11 +35,7 @@ impl MigrationTrait for Migration { .not_null() .default(false), ) - .col( - ColumnDef::new(ChatChannel::DailyReportTime) - .string() - .null(), - ) + .col(ColumnDef::new(ChatChannel::DailyReportTime).string().null()) .col( ColumnDef::new(ChatChannel::CreatedAt) .timestamp_with_time_zone() @@ -138,11 +126,7 @@ impl MigrationTrait for Migration { async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager - .drop_table( - Table::drop() - .table(ChatChannelMessageLog::Table) - .to_owned(), - ) + .drop_table(Table::drop().table(ChatChannelMessageLog::Table).to_owned()) .await?; manager .drop_table(Table::drop().table(ChatChannel::Table).to_owned()) diff --git a/src-tauri/src/db/migration/m20260406_000001_agent_setting_model_provider.rs b/src-tauri/src/db/migration/m20260406_000001_agent_setting_model_provider.rs index 064d814..58e8159 100644 --- a/src-tauri/src/db/migration/m20260406_000001_agent_setting_model_provider.rs +++ b/src-tauri/src/db/migration/m20260406_000001_agent_setting_model_provider.rs @@ -10,7 +10,11 @@ impl MigrationTrait for Migration { .alter_table( Table::alter() .table(AgentSetting::Table) - .add_column(ColumnDef::new(AgentSetting::ModelProviderId).integer().null()) + .add_column( + ColumnDef::new(AgentSetting::ModelProviderId) + .integer() + .null(), + ) .to_owned(), ) .await diff --git a/src-tauri/src/db/service/app_metadata_service.rs b/src-tauri/src/db/service/app_metadata_service.rs index 189fb9b..ec0d6a0 100644 --- a/src-tauri/src/db/service/app_metadata_service.rs +++ b/src-tauri/src/db/service/app_metadata_service.rs @@ -1,7 +1,7 @@ use chrono::Utc; use sea_orm::sea_query::OnConflict; -use sea_orm::{ConnectionTrait, DatabaseConnection}; use sea_orm::{ActiveValue::NotSet, ColumnTrait, EntityTrait, QueryFilter, Set}; +use sea_orm::{ConnectionTrait, DatabaseConnection}; use crate::db::entities::app_metadata; use crate::db::error::DbError; diff --git a/src-tauri/src/db/service/conversation_service.rs b/src-tauri/src/db/service/conversation_service.rs index e34d32e..b031028 100644 --- a/src-tauri/src/db/service/conversation_service.rs +++ b/src-tauri/src/db/service/conversation_service.rs @@ -207,8 +207,7 @@ pub async fn list_all( sort_by: Option, status: Option, ) -> Result, DbError> { - let mut query = conversation::Entity::find() - .filter(conversation::Column::DeletedAt.is_null()); + let mut query = conversation::Entity::find().filter(conversation::Column::DeletedAt.is_null()); match folder_ids { Some(ids) if !ids.is_empty() => { diff --git a/src-tauri/src/db/service/folder_service.rs b/src-tauri/src/db/service/folder_service.rs index 4b2efcb..6d9fa56 100644 --- a/src-tauri/src/db/service/folder_service.rs +++ b/src-tauri/src/db/service/folder_service.rs @@ -181,10 +181,7 @@ pub async fn list_all_folder_details( Ok(rows.into_iter().map(to_detail).collect()) } -pub async fn reorder_folders( - conn: &DatabaseConnection, - ids: Vec, -) -> Result<(), DbError> { +pub async fn reorder_folders(conn: &DatabaseConnection, ids: Vec) -> Result<(), DbError> { if ids.is_empty() { return Ok(()); } diff --git a/src-tauri/src/db/service/tab_service.rs b/src-tauri/src/db/service/tab_service.rs index 54e8ed4..3e70730 100644 --- a/src-tauri/src/db/service/tab_service.rs +++ b/src-tauri/src/db/service/tab_service.rs @@ -87,10 +87,7 @@ pub async fn delete_tabs_for_folder( conn: &DatabaseConnection, folder_id: i32, ) -> Result<(), DbError> { - let sql = format!( - "DELETE FROM opened_tab WHERE folder_id = {}", - folder_id - ); + let sql = format!("DELETE FROM opened_tab WHERE folder_id = {}", folder_id); conn.execute(Statement::from_string(DbBackend::Sqlite, sql)) .await?; Ok(()) diff --git a/src-tauri/src/git_credential.rs b/src-tauri/src/git_credential.rs index e61474b..f4f6340 100644 --- a/src-tauri/src/git_credential.rs +++ b/src-tauri/src/git_credential.rs @@ -219,10 +219,7 @@ pub async fn get_remote_url(repo_path: &str) -> Option { } /// Get the remote URL for a specific named remote. -pub async fn get_remote_url_by_name( - repo_path: &str, - remote_name: &str, -) -> Option { +pub async fn get_remote_url_by_name(repo_path: &str, remote_name: &str) -> Option { let output = crate::process::tokio_command("git") .args(["remote", "get-url", remote_name]) .current_dir(repo_path) @@ -235,7 +232,11 @@ pub async fn get_remote_url_by_name( } let url = String::from_utf8_lossy(&output.stdout).trim().to_string(); - if url.is_empty() { None } else { Some(url) } + if url.is_empty() { + None + } else { + Some(url) + } } /// Extract the hostname from a git remote URL. @@ -302,9 +303,7 @@ pub fn find_matching_account<'a>( } /// Load GitHub accounts from the database. -pub async fn load_github_accounts( - conn: &DatabaseConnection, -) -> Option { +pub async fn load_github_accounts(conn: &DatabaseConnection) -> Option { let raw = app_metadata_service::get_value(conn, GITHUB_ACCOUNTS_KEY) .await .ok()??; @@ -375,7 +374,10 @@ pub async fn try_inject_for_repo_remote( let remote_url = match get_remote_url_by_name(repo_path, target_remote).await { Some(url) => url, None => { - eprintln!("[GIT_CRED] no remote URL found for {} (remote: {})", repo_path, target_remote); + eprintln!( + "[GIT_CRED] no remote URL found for {} (remote: {})", + repo_path, target_remote + ); return false; } }; diff --git a/src-tauri/src/git_repo.rs b/src-tauri/src/git_repo.rs index be0eacf..95253f1 100644 --- a/src-tauri/src/git_repo.rs +++ b/src-tauri/src/git_repo.rs @@ -62,8 +62,10 @@ pub fn ensure_git_repo(path: &str) -> Result<(), AppCommandError> { git_path.display() )) .with_detail(err.to_string())), - _ => Err(AppCommandError::io(err) - .with_detail(format!("Failed to inspect Git metadata: {}", git_path.display()))), + _ => Err(AppCommandError::io(err).with_detail(format!( + "Failed to inspect Git metadata: {}", + git_path.display() + ))), }, } } diff --git a/src-tauri/src/keyring_store.rs b/src-tauri/src/keyring_store.rs index e5f90e4..041d1ee 100644 --- a/src-tauri/src/keyring_store.rs +++ b/src-tauri/src/keyring_store.rs @@ -69,8 +69,7 @@ fn write_tokens(tokens: &std::collections::HashMap) -> Result<() } let json = serde_json::to_string_pretty(tokens) .map_err(|e| format!("failed to serialize tokens: {e}"))?; - std::fs::write(&path, json) - .map_err(|e| format!("failed to write token store: {e}")) + std::fs::write(&path, json).map_err(|e| format!("failed to write token store: {e}")) } #[cfg(not(feature = "tauri-runtime"))] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c2ae83d..3fda95f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -181,14 +181,11 @@ mod tauri_app { }); } - if label == "main" - && matches!(event, tauri::WindowEvent::CloseRequested { .. }) - { + if label == "main" && matches!(event, tauri::WindowEvent::CloseRequested { .. }) { let app = window.app_handle(); if let Some(cm) = app.try_state::() { - let disconnected = tauri::async_runtime::block_on( - cm.disconnect_by_owner_window(&label), - ); + let disconnected = + tauri::async_runtime::block_on(cm.disconnect_by_owner_window(&label)); eprintln!( "[ACP] main window closing disconnected_connections={}", disconnected diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs index 0a89857..05ab57f 100644 --- a/src-tauri/src/models/mod.rs +++ b/src-tauri/src/models/mod.rs @@ -2,8 +2,8 @@ pub mod agent; pub mod chat_channel; pub mod conversation; pub mod folder; -pub mod model_provider; pub mod message; +pub mod model_provider; pub mod system; pub use agent::AgentType; diff --git a/src-tauri/src/parsers/claude.rs b/src-tauri/src/parsers/claude.rs index 7c77f40..b1e0260 100644 --- a/src-tauri/src/parsers/claude.rs +++ b/src-tauri/src/parsers/claude.rs @@ -586,19 +586,16 @@ impl ClaudeParser { if tur.get("agentType").is_some() { let mut stats = extract_agent_execution_stats(tur); // Load tool calls from subagent's own JSONL transcript - if let Some(agent_id) = - tur.get("agentId").and_then(|v| v.as_str()) - { + if let Some(agent_id) = tur.get("agentId").and_then(|v| v.as_str()) { // Reject path traversal: agentId must be alphanumeric if !agent_id.is_empty() && !agent_id.contains('/') && !agent_id.contains('\\') && !agent_id.contains("..") { - let subagent_dir = - path.with_extension("").join("subagents"); - let subagent_path = subagent_dir - .join(format!("agent-{}.jsonl", agent_id)); + let subagent_dir = path.with_extension("").join("subagents"); + let subagent_path = + subagent_dir.join(format!("agent-{}.jsonl", agent_id)); if subagent_path.exists() { stats.tool_calls = parse_subagent_tool_calls(&subagent_path); @@ -775,9 +772,7 @@ impl ClaudeParser { .. } = b { - if tn == tool_name - && !existing_result_ids.contains(id) - { + if tn == tool_name && !existing_result_ids.contains(id) { return Some(id.clone()); } } @@ -980,7 +975,11 @@ fn extract_claude_user_image(item: &serde_json::Value) -> Option { let mime_type = source .and_then(|s| s.get("media_type")) .and_then(|m| m.as_str()) - .or_else(|| source.and_then(|s| s.get("mime_type")).and_then(|m| m.as_str())) + .or_else(|| { + source + .and_then(|s| s.get("mime_type")) + .and_then(|m| m.as_str()) + }) .or_else(|| item.get("media_type").and_then(|m| m.as_str())) .or_else(|| item.get("mime_type").and_then(|m| m.as_str())) .map(str::trim) @@ -1192,9 +1191,7 @@ fn parse_subagent_tool_calls(path: &PathBuf) -> Vec { .and_then(|v| v.as_str()) .unwrap_or("unknown") .to_string(); - let input = item.get("input").map(|v| { - truncate_str(&v.to_string(), 500) - }); + let input = item.get("input").map(|v| truncate_str(&v.to_string(), 500)); if !id.is_empty() { calls.push((id, name, input)); } @@ -1219,8 +1216,7 @@ fn parse_subagent_tool_calls(path: &PathBuf) -> Vec { .get("is_error") .and_then(|v| v.as_bool()) .unwrap_or(false); - let output = extract_tool_result_text(item) - .map(|s| truncate_str(&s, 500)); + let output = extract_tool_result_text(item).map(|s| truncate_str(&s, 500)); if !id.is_empty() { results.insert(id, (output, is_error)); } @@ -1233,9 +1229,7 @@ fn parse_subagent_tool_calls(path: &PathBuf) -> Vec { calls .into_iter() .map(|(id, name, input)| { - let (output, is_error) = results - .remove(&id) - .unwrap_or((None, false)); + let (output, is_error) = results.remove(&id).unwrap_or((None, false)); AgentToolCall { tool_name: name, input_preview: input, diff --git a/src-tauri/src/parsers/cline.rs b/src-tauri/src/parsers/cline.rs index 4711b55..a3b7c67 100644 --- a/src-tauri/src/parsers/cline.rs +++ b/src-tauri/src/parsers/cline.rs @@ -135,20 +135,13 @@ impl AgentParser for ClineParser { fs::read_to_string(meta_path) .ok() .and_then(|raw| serde_json::from_str::(&raw).ok()) - .and_then(|meta| { - meta.model_usage - .first() - .and_then(|u| u.model_id.clone()) - }) + .and_then(|meta| meta.model_usage.first().and_then(|u| u.model_id.clone())) }); let folder_path = entry.cwd_on_task_initialization.clone(); let folder_name = folder_path.as_deref().map(folder_name_from_path); - let title = entry - .task - .as_deref() - .map(|t| truncate_str(t.trim(), 100)); + let title = entry.task.as_deref().map(|t| truncate_str(t.trim(), 100)); // Count messages from api_conversation_history.json let api_path = tasks_dir.join("api_conversation_history.json"); @@ -309,10 +302,7 @@ impl AgentParser for ClineParser { } } - let started_at = turns - .first() - .map(|t| t.timestamp) - .unwrap_or_else(Utc::now); + let started_at = turns.first().map(|t| t.timestamp).unwrap_or_else(Utc::now); let ended_at = turns.last().map(|t| t.timestamp); let session_stats = compute_session_stats(&turns); @@ -441,8 +431,7 @@ fn collect_text_parts(content: &serde_json::Value) -> Vec { /// Check if text looks like a Cline tool result: `[tool_name ...] Result:` fn is_tool_result_text(text: &str) -> bool { let trimmed = text.trim_start(); - trimmed.starts_with('[') - && trimmed.contains("] Result:") + trimmed.starts_with('[') && trimmed.contains("] Result:") } /// Parse `[tool_name for 'arg'] Result:\ncontent` into (tool_name, output, is_error). @@ -451,10 +440,7 @@ fn parse_tool_result_text(text: &str) -> (String, String, bool) { // Extract tool name from [tool_name ...] or [tool_name] prefix let tool_name = trimmed .strip_prefix('[') - .and_then(|s| { - s.find([']', ' ']) - .map(|i| s[..i].to_string()) - }) + .and_then(|s| s.find([']', ' ']).map(|i| s[..i].to_string())) .unwrap_or_default(); let is_error = trimmed.contains("[ERROR]") || trimmed.contains("Error:"); @@ -537,8 +523,7 @@ fn parse_content_blocks(content: &serde_json::Value) -> Vec { .and_then(|v| v.as_str()) .unwrap_or("unknown") .to_string(); - let tool_use_id = - item.get("id").and_then(|v| v.as_str()).map(String::from); + let tool_use_id = item.get("id").and_then(|v| v.as_str()).map(String::from); let input_preview = item.get("input").map(|v| { let s = v.to_string(); truncate_str(&s, 2000) diff --git a/src-tauri/src/parsers/codex.rs b/src-tauri/src/parsers/codex.rs index 38bdb6c..b9ee4cc 100644 --- a/src-tauri/src/parsers/codex.rs +++ b/src-tauri/src/parsers/codex.rs @@ -564,24 +564,14 @@ fn parse_codex_subagent_stats( let input_preview = if tool_name == "exec_command" { parse_codex_json_arg(payload) - .and_then(|a| { - a.get("cmd") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - }) + .and_then(|a| a.get("cmd").and_then(|v| v.as_str()).map(|s| s.to_string())) .or_else(|| { value_to_preview( - payload - .get("arguments") - .or_else(|| payload.get("input")), + payload.get("arguments").or_else(|| payload.get("input")), ) }) } else { - value_to_preview( - payload - .get("arguments") - .or_else(|| payload.get("input")), - ) + value_to_preview(payload.get("arguments").or_else(|| payload.get("input"))) }; let tc = AgentToolCall { @@ -631,7 +621,11 @@ fn parse_codex_subagent_stats( let total_duration_ms = match (first_ts, last_ts) { (Some(f), Some(l)) => { let dur = (l - f).num_milliseconds(); - if dur > 0 { Some(dur as u64) } else { None } + if dur > 0 { + Some(dur as u64) + } else { + None + } } _ => None, }; @@ -955,10 +949,8 @@ impl CodexParser { .and_then(|a| a.get("message")) .and_then(|v| v.as_str()) .unwrap_or(""); - let description = truncate_str( - message.lines().next().unwrap_or(""), - 60, - ); + let description = + truncate_str(message.lines().next().unwrap_or(""), 60); if let Some(ref id) = tool_use_id { spawn_agent_call_ids.insert(id.clone()); @@ -993,47 +985,43 @@ impl CodexParser { "close_agent" => { if let Some(ref id) = tool_use_id { close_agent_call_ids.insert(id.clone()); - let target = parse_codex_json_arg(payload) - .and_then(|a| { + let target = + parse_codex_json_arg(payload).and_then(|a| { a.get("target") .and_then(|v| v.as_str()) .map(|s| s.to_string()) }); if let Some(target) = target { - close_agent_targets - .insert(id.clone(), target); + close_agent_targets.insert(id.clone(), target); } } } _ => { if let Some(ref id) = tool_use_id { - call_id_tool_names.insert( - id.clone(), - raw_tool_name.to_string(), - ); + call_id_tool_names + .insert(id.clone(), raw_tool_name.to_string()); } - let input_preview = - if raw_tool_name == "exec_command" { - parse_codex_json_arg(payload) - .and_then(|a| { - a.get("cmd") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - }) - .or_else(|| { - value_to_preview( - payload.get("arguments").or_else( - || payload.get("input"), - ), - ) - }) - } else { - value_to_preview( - payload - .get("arguments") - .or_else(|| payload.get("input")), - ) - }; + let input_preview = if raw_tool_name == "exec_command" { + parse_codex_json_arg(payload) + .and_then(|a| { + a.get("cmd") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + }) + .or_else(|| { + value_to_preview( + payload + .get("arguments") + .or_else(|| payload.get("input")), + ) + }) + } else { + value_to_preview( + payload + .get("arguments") + .or_else(|| payload.get("input")), + ) + }; messages.push(UnifiedMessage { id: format!("tool-{}", messages.len()), role: MessageRole::Assistant, @@ -1071,15 +1059,11 @@ impl CodexParser { if is_spawn { if let Some(output_obj) = parse_codex_json_output(payload) { if let (Some(agent_id), Some(call_id)) = ( - output_obj - .get("agent_id") - .and_then(|v| v.as_str()), + output_obj.get("agent_id").and_then(|v| v.as_str()), tool_use_id.as_ref(), ) { - agent_id_to_spawn_call_id.insert( - agent_id.to_string(), - call_id.clone(), - ); + agent_id_to_spawn_call_id + .insert(agent_id.to_string(), call_id.clone()); } } messages.push(UnifiedMessage { @@ -1098,14 +1082,12 @@ impl CodexParser { }); } else if is_wait { if let Some(output_obj) = parse_codex_json_output(payload) { - if let Some(status) = output_obj - .get("status") - .and_then(|s| s.as_object()) + if let Some(status) = + output_obj.get("status").and_then(|s| s.as_object()) { for (agent_id, result) in status { - if let Some(text) = result - .get("completed") - .and_then(|v| v.as_str()) + if let Some(text) = + result.get("completed").and_then(|v| v.as_str()) { agent_final_results .entry(agent_id.clone()) @@ -1115,8 +1097,7 @@ impl CodexParser { } } } else if is_close { - active_agent_count = - active_agent_count.saturating_sub(1); + active_agent_count = active_agent_count.saturating_sub(1); if let Some(output_obj) = parse_codex_json_output(payload) { if let Some(agent_id) = tool_use_id .as_ref() @@ -1135,7 +1116,9 @@ impl CodexParser { } } else { let is_exec = tool_use_id.as_ref().is_some_and(|id| { - call_id_tool_names.get(id).is_some_and(|n| n == "exec_command") + call_id_tool_names + .get(id) + .is_some_and(|n| n == "exec_command") }); let output_value = payload.get("output"); let raw_output = value_to_preview(output_value); @@ -1235,9 +1218,7 @@ impl CodexParser { if let Some(dir) = session_dir { let stats = agent_stats_cache .entry(agent_id.to_string()) - .or_insert_with(|| { - parse_codex_subagent_stats(dir, agent_id) - }); + .or_insert_with(|| parse_codex_subagent_stats(dir, agent_id)); if stats.is_some() { *agent_stats = stats.clone(); } @@ -1677,9 +1658,7 @@ fn group_into_turns(messages: Vec) -> Vec { // Only absorb immediately following Tool messages // (stop at the next assistant message to keep turns small for virtualization) - while i < messages.len() - && matches!(messages[i].role, MessageRole::Tool) - { + while i < messages.len() && matches!(messages[i].role, MessageRole::Tool) { blocks.extend(messages[i].content.clone()); if usage.is_none() { usage = messages[i].usage.clone(); diff --git a/src-tauri/src/parsers/gemini.rs b/src-tauri/src/parsers/gemini.rs index a4a349a..9f8b004 100644 --- a/src-tauri/src/parsers/gemini.rs +++ b/src-tauri/src/parsers/gemini.rs @@ -687,9 +687,7 @@ fn group_into_turns(messages: Vec) -> Vec { // Only absorb immediately following Tool messages // (stop at the next assistant message to keep turns small for virtualization) - while i < messages.len() - && matches!(messages[i].role, MessageRole::Tool) - { + while i < messages.len() && matches!(messages[i].role, MessageRole::Tool) { blocks.extend(messages[i].content.clone()); if usage.is_none() { usage = messages[i].usage.clone(); diff --git a/src-tauri/src/parsers/mod.rs b/src-tauri/src/parsers/mod.rs index a8ef4bf..fecd4df 100644 --- a/src-tauri/src/parsers/mod.rs +++ b/src-tauri/src/parsers/mod.rs @@ -471,8 +471,7 @@ pub fn resolve_patch_text(patch: &str, cwd: Option<&str>) -> Option { if let (Some(ref fl), true) = (&file_lines, current_file_path.is_some()) { // Collect context lines from this hunk to find match position let hunk_lines = collect_hunk_lines(&lines, i + 1); - if let Some((old_start, old_count, new_count)) = - find_hunk_position(fl, &hunk_lines) + if let Some((old_start, old_count, new_count)) = find_hunk_position(fl, &hunk_lines) { let new_start = old_start; // same start for context-based patches output.push_str(&format!( @@ -496,7 +495,11 @@ pub fn resolve_patch_text(patch: &str, cwd: Option<&str>) -> Option { i += 1; } - if any_resolved { Some(output) } else { None } + if any_resolved { + Some(output) + } else { + None + } } /// Load file lines from disk, trying both absolute path and cwd-relative. @@ -523,9 +526,7 @@ pub fn load_file_lines(path: &str, cwd: Option<&str>) -> Option> { fn collect_hunk_lines<'a>(lines: &'a [&'a str], start: usize) -> Vec<&'a str> { let mut result = Vec::new(); for &line in &lines[start..] { - if line == "@@" - || line.starts_with("*** ") - { + if line == "@@" || line.starts_with("*** ") { break; } result.push(line); @@ -541,10 +542,7 @@ fn collect_hunk_lines<'a>(lines: &'a [&'a str], start: usize) -> Vec<&'a str> { /// 1. Contiguous match of context+added lines (post-patch file, no further edits) /// 2. Contiguous match of context+deleted lines (pre-patch file) /// 3. Subsequence match of context-only lines (file has been further modified) -fn find_hunk_position( - file_lines: &[String], - hunk_lines: &[&str], -) -> Option<(usize, usize, usize)> { +fn find_hunk_position(file_lines: &[String], hunk_lines: &[&str]) -> Option<(usize, usize, usize)> { let mut old_count = 0usize; let mut new_count = 0usize; for hl in hunk_lines { @@ -601,7 +599,11 @@ fn find_contiguous(file_lines: &[String], view: &[&str]) -> Option { if file_lines[i].as_str() != first { continue; } - if view.iter().enumerate().all(|(j, v)| file_lines[i + j].as_str() == *v) { + if view + .iter() + .enumerate() + .all(|(j, v)| file_lines[i + j].as_str() == *v) + { return Some(i); } } diff --git a/src-tauri/src/parsers/openclaw.rs b/src-tauri/src/parsers/openclaw.rs index b34913b..8fe1ab9 100644 --- a/src-tauri/src/parsers/openclaw.rs +++ b/src-tauri/src/parsers/openclaw.rs @@ -19,9 +19,7 @@ use crate::parsers::{ /// timestamp prefix from OpenClaw user messages. fn sender_block_regex() -> &'static Regex { static RE: OnceLock = OnceLock::new(); - RE.get_or_init(|| { - Regex::new(r"(?s)^Sender \(untrusted metadata\):\s*```[^`]*```\s*").unwrap() - }) + RE.get_or_init(|| Regex::new(r"(?s)^Sender \(untrusted metadata\):\s*```[^`]*```\s*").unwrap()) } fn timestamp_prefix_regex() -> &'static Regex { @@ -354,9 +352,7 @@ impl OpenClawParser { } /// Read sessions.json for a given agent directory. - fn read_session_index( - agent_dir: &Path, - ) -> Result, ParseError> { + fn read_session_index(agent_dir: &Path) -> Result, ParseError> { let index_path = agent_dir.join("sessions").join("sessions.json"); if !index_path.exists() { return Ok(HashMap::new()); @@ -710,18 +706,12 @@ impl OpenClawParser { if jsonl_path.exists() { let meta = Self::read_session_index(&agent_dir) .ok() - .and_then(|index| { - index - .into_values() - .find(|m| m.session_id == session_id) - }); + .and_then(|index| index.into_values().find(|m| m.session_id == session_id)); return Ok((jsonl_path, leaf_id, meta)); } // Try reset files - if let Some((path, meta)) = - Self::find_reset_file(&agent_dir, session_id) - { + if let Some((path, meta)) = Self::find_reset_file(&agent_dir, session_id) { return Ok((path, leaf_id, meta)); } } @@ -747,9 +737,7 @@ impl OpenClawParser { if jsonl_path.exists() { let meta = Self::read_session_index(&agent_dir) .ok() - .and_then(|index| { - index.into_values().find(|m| m.session_id == bare_id) - }); + .and_then(|index| index.into_values().find(|m| m.session_id == bare_id)); return Ok((jsonl_path, None, meta)); } @@ -768,9 +756,8 @@ impl OpenClawParser { // Check if any leaf id matches the bare_id let leaves = tree.leaf_ids(); if leaves.iter().any(|l| l == bare_id) { - let meta = Self::read_session_index(&agent_dir) - .ok() - .and_then(|index| { + let meta = + Self::read_session_index(&agent_dir).ok().and_then(|index| { index.into_values().find(|m| m.session_id == *sid) }); return Ok((path.clone(), Some(bare_id.to_string()), meta)); @@ -905,7 +892,10 @@ fn extract_first_text_content(value: &serde_json::Value) -> Option { let content = value.get("message")?.get("content")?.as_array()?; for item in content { if item.get("type").and_then(|t| t.as_str()) == Some("text") { - return item.get("text").and_then(|t| t.as_str()).map(|s| s.to_string()); + return item + .get("text") + .and_then(|t| t.as_str()) + .map(|s| s.to_string()); } } None @@ -987,8 +977,13 @@ fn extract_assistant_content(value: &serde_json::Value) -> Vec { .to_string(); let is_edit_tool = matches!( tool_name.to_lowercase().as_str(), - "edit" | "write" | "apply_patch" | "patch" | "applypatch" - | "edit_file" | "editfile" + "edit" + | "write" + | "apply_patch" + | "patch" + | "applypatch" + | "edit_file" + | "editfile" ); let max_len = if is_edit_tool { 50000 } else { 500 }; let input_preview = item.get("arguments").map(|a| { @@ -1068,10 +1063,7 @@ fn extract_usage(value: &serde_json::Value) -> Option { .get("cacheWrite") .and_then(|v| v.as_u64()) .unwrap_or(0), - cache_read_input_tokens: usage - .get("cacheRead") - .and_then(|v| v.as_u64()) - .unwrap_or(0), + cache_read_input_tokens: usage.get("cacheRead").and_then(|v| v.as_u64()).unwrap_or(0), }) } @@ -1117,9 +1109,7 @@ fn group_into_turns(messages: Vec) -> Vec { // Only absorb immediately following Tool messages // (stop at the next assistant message to keep turns small for virtualization) - while i < messages.len() - && matches!(messages[i].role, MessageRole::Tool) - { + while i < messages.len() && matches!(messages[i].role, MessageRole::Tool) { blocks.extend(messages[i].content.clone()); if usage.is_none() { usage = messages[i].usage.clone(); @@ -1162,7 +1152,8 @@ mod tests { #[test] fn extracts_working_directory() { - let text = "[Tue 2026-03-17 12:58 GMT+8] [Working directory: ~/forway/agent-workspace]\n\nHello"; + let text = + "[Tue 2026-03-17 12:58 GMT+8] [Working directory: ~/forway/agent-workspace]\n\nHello"; let wd = extract_working_dir(text).unwrap(); let home = dirs::home_dir().unwrap().to_string_lossy().to_string(); assert_eq!(wd, format!("{}/forway/agent-workspace", home)); @@ -1219,9 +1210,13 @@ mod tests { }); let blocks = extract_assistant_content(&value); assert_eq!(blocks.len(), 3); - assert!(matches!(&blocks[0], ContentBlock::Thinking { text } if text == "I should read the file")); + assert!( + matches!(&blocks[0], ContentBlock::Thinking { text } if text == "I should read the file") + ); assert!(matches!(&blocks[1], ContentBlock::Text { text } if text == "Let me check.")); - assert!(matches!(&blocks[2], ContentBlock::ToolUse { tool_name, .. } if tool_name == "read")); + assert!( + matches!(&blocks[2], ContentBlock::ToolUse { tool_name, .. } if tool_name == "read") + ); } #[test] @@ -1272,8 +1267,9 @@ mod tests { json!({"type":"message","id":"a1","parentId":"u1","timestamp":"2026-03-17T04:56:30.466Z","message":{"role":"assistant","content":[{"type":"text","text":"[[reply_to_current]] Hi there!"}],"model":"gpt-5.4","usage":{"input":100,"output":50,"cacheRead":200,"cacheWrite":0,"totalTokens":350},"stopReason":"stop","timestamp":1773723390466_i64}}) ).unwrap(); - let detail = OpenClawParser::parse_conversation_detail(&path, "test/test-session", None, None) - .expect("parse detail"); + let detail = + OpenClawParser::parse_conversation_detail(&path, "test/test-session", None, None) + .expect("parse detail"); fs::remove_file(&path).unwrap(); assert_eq!(detail.turns.len(), 2); diff --git a/src-tauri/src/parsers/opencode.rs b/src-tauri/src/parsers/opencode.rs index aee2196..2ab88a5 100644 --- a/src-tauri/src/parsers/opencode.rs +++ b/src-tauri/src/parsers/opencode.rs @@ -228,9 +228,7 @@ impl OpenCodeParser { // Pre-scan: collect all subagent session IDs from task tool parts so we // can batch-load their tool calls in a single query instead of N queries. - let subagent_session_ids = self - .scan_subagent_session_ids(conn, conversation_id) - .await; + let subagent_session_ids = self.scan_subagent_session_ids(conn, conversation_id).await; let subagent_tools = batch_load_subagent_tool_calls(conn, &subagent_session_ids).await; let mut messages = Vec::with_capacity(rows.len()); @@ -270,8 +268,9 @@ impl OpenCodeParser { None }; - let (content_blocks, usage_from_step_finish) = - self.load_sqlite_parts(conn, &msg_id, &subagent_tools).await?; + let (content_blocks, usage_from_step_finish) = self + .load_sqlite_parts(conn, &msg_id, &subagent_tools) + .await?; let usage = if is_assistant { extract_opencode_usage(&value).or(usage_from_step_finish) @@ -511,8 +510,7 @@ impl OpenCodeParser { agent_stats, }); } else { - let input_preview = state_input - .and_then(|v| value_to_preview(Some(v))); + let input_preview = state_input.and_then(|v| value_to_preview(Some(v))); blocks.push(ContentBlock::ToolUse { tool_use_id: call_id.clone(), @@ -811,9 +809,7 @@ fn group_into_turns(messages: Vec) -> Vec { // Only absorb immediately following Tool messages // (stop at the next assistant message to keep turns small for virtualization) - while i < messages.len() - && matches!(messages[i].role, MessageRole::Tool) - { + while i < messages.len() && matches!(messages[i].role, MessageRole::Tool) { blocks.extend(messages[i].content.clone()); if usage.is_none() { usage = messages[i].usage.clone(); @@ -888,7 +884,11 @@ async fn batch_load_subagent_tool_calls( let values: Vec = session_ids.iter().map(|s| s.as_str().into()).collect(); let rows = match conn - .query_all(Statement::from_sql_and_values(DbBackend::Sqlite, &sql, values)) + .query_all(Statement::from_sql_and_values( + DbBackend::Sqlite, + &sql, + values, + )) .await { Ok(r) => r, diff --git a/src-tauri/src/process.rs b/src-tauri/src/process.rs index 2353723..2f3a720 100644 --- a/src-tauri/src/process.rs +++ b/src-tauri/src/process.rs @@ -280,8 +280,7 @@ fn find_node_bin_dir(home: Option<&std::path::Path>) -> Option { } // All installed versions, newest first. - if let Some(nvm_home) = - resolve_dir(&[("NVM_HOME", &[]), ("APPDATA", &["nvm"])], None, &[]) + if let Some(nvm_home) = resolve_dir(&[("NVM_HOME", &[]), ("APPDATA", &["nvm"])], None, &[]) { if nvm_home.is_dir() { if let Ok(mut entries) = std::fs::read_dir(&nvm_home).map(|rd| { @@ -460,7 +459,9 @@ fn find_node_bin_dir(home: Option<&std::path::Path>) -> Option { } // Return the first candidate that actually contains a `node` binary. - candidates.into_iter().find(|dir| dir.join(node_bin).is_file()) + candidates + .into_iter() + .find(|dir| dir.join(node_bin).is_file()) } /// Prepend a directory to the process `PATH` environment variable. diff --git a/src-tauri/src/web/auth.rs b/src-tauri/src/web/auth.rs index 8999b8c..0db21fb 100644 --- a/src-tauri/src/web/auth.rs +++ b/src-tauri/src/web/auth.rs @@ -5,11 +5,7 @@ use axum::{ response::{IntoResponse, Response}, }; -pub async fn require_token( - request: Request, - next: Next, - token: String, -) -> Response { +pub async fn require_token(request: Request, next: Next, token: String) -> Response { // Allow WebSocket upgrade requests to authenticate via query param. // The token value is URL-encoded by the client, so decode before comparing. if let Some(query) = request.uri().query() { diff --git a/src-tauri/src/web/event_bridge.rs b/src-tauri/src/web/event_bridge.rs index b6473af..9b52cb7 100644 --- a/src-tauri/src/web/event_bridge.rs +++ b/src-tauri/src/web/event_bridge.rs @@ -56,11 +56,7 @@ pub enum EventEmitter { } /// Unified event emission: sends to both Tauri webview and Web clients (if applicable). -pub fn emit_event( - emitter: &EventEmitter, - event: &str, - payload: impl Serialize + Clone, -) { +pub fn emit_event(emitter: &EventEmitter, event: &str, payload: impl Serialize + Clone) { match emitter { #[cfg(feature = "tauri-runtime")] EventEmitter::Tauri(app) => { diff --git a/src-tauri/src/web/handlers/acp.rs b/src-tauri/src/web/handlers/acp.rs index 70cd683..40bab97 100644 --- a/src-tauri/src/web/handlers/acp.rs +++ b/src-tauri/src/web/handlers/acp.rs @@ -188,10 +188,9 @@ pub struct AcpListAgentSkillsParams { pub async fn acp_list_agent_skills( Json(params): Json, ) -> Result, AppCommandError> { - let result = - acp_commands::acp_list_agent_skills(params.agent_type, params.workspace_path) - .await - .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; + let result = acp_commands::acp_list_agent_skills(params.agent_type, params.workspace_path) + .await + .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; Ok(Json(result)) } @@ -482,10 +481,9 @@ pub async fn acp_detect_agent_local_version( Json(params): Json, ) -> Result>, AppCommandError> { let db = &state.db; - let result = - acp_commands::acp_detect_agent_local_version_core(params.agent_type, &db.conn) - .await - .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; + let result = acp_commands::acp_detect_agent_local_version_core(params.agent_type, &db.conn) + .await + .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; Ok(Json(result)) } @@ -610,11 +608,8 @@ pub struct CodexPollDeviceCodeParams { pub async fn codex_poll_device_code( Json(params): Json, ) -> Result, AppCommandError> { - let result = acp_commands::codex_poll_device_code_core( - params.device_auth_id, - params.user_code, - ) - .await - .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; + let result = acp_commands::codex_poll_device_code_core(params.device_auth_id, params.user_code) + .await + .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; Ok(Json(result)) } diff --git a/src-tauri/src/web/handlers/chat_channel.rs b/src-tauri/src/web/handlers/chat_channel.rs index b12803b..b1b92f3 100644 --- a/src-tauri/src/web/handlers/chat_channel.rs +++ b/src-tauri/src/web/handlers/chat_channel.rs @@ -5,8 +5,8 @@ use serde::Deserialize; use crate::app_error::AppCommandError; use crate::app_state::AppState; -use crate::commands::chat_channel as cc_commands; use crate::chat_channel::backends::weixin::{WeixinQrcodeInfo, WeixinQrcodeStatusPublic}; +use crate::commands::chat_channel as cc_commands; use crate::models::chat_channel::{ChannelStatusInfo, ChatChannelInfo, ChatChannelMessageLogInfo}; // --------------------------------------------------------------------------- @@ -167,8 +167,7 @@ pub async fn test_chat_channel( pub async fn get_chat_channel_status( Extension(state): Extension>, ) -> Result>, AppCommandError> { - let result = - cc_commands::get_chat_channel_status_core(&state.chat_channel_manager).await?; + let result = cc_commands::get_chat_channel_status_core(&state.chat_channel_manager).await?; Ok(Json(result)) } @@ -270,7 +269,6 @@ pub async fn weixin_check_qrcode( Json(params): Json, ) -> Result, AppCommandError> { let result = - cc_commands::weixin_check_qrcode_core(&state.db, params.channel_id, ¶ms.qrcode) - .await?; + cc_commands::weixin_check_qrcode_core(&state.db, params.channel_id, ¶ms.qrcode).await?; Ok(Json(result)) } diff --git a/src-tauri/src/web/handlers/conversations.rs b/src-tauri/src/web/handlers/conversations.rs index defee2f..b567471 100644 --- a/src-tauri/src/web/handlers/conversations.rs +++ b/src-tauri/src/web/handlers/conversations.rs @@ -98,8 +98,7 @@ pub struct GetConversationParams { pub async fn get_conversation( Json(params): Json, ) -> Result, AppCommandError> { - let result = - conv_commands::get_conversation(params.agent_type, params.conversation_id).await?; + let result = conv_commands::get_conversation(params.agent_type, params.conversation_id).await?; Ok(Json(result)) } @@ -149,9 +148,10 @@ pub async fn import_local_conversations( .await .map_err(AppCommandError::from)? .ok_or_else(|| AppCommandError::not_found("Folder not found"))?; - let result = import_service::import_local_conversations(&db.conn, params.folder_id, &folder.path) - .await - .map_err(AppCommandError::from)?; + let result = + import_service::import_local_conversations(&db.conn, params.folder_id, &folder.path) + .await + .map_err(AppCommandError::from)?; Ok(Json(result)) } @@ -251,4 +251,4 @@ pub async fn update_conversation_external_id( .await .map_err(AppCommandError::from)?; Ok(Json(())) -} \ No newline at end of file +} diff --git a/src-tauri/src/web/handlers/files.rs b/src-tauri/src/web/handlers/files.rs index 57ee79b..293339d 100644 --- a/src-tauri/src/web/handlers/files.rs +++ b/src-tauri/src/web/handlers/files.rs @@ -77,24 +77,21 @@ pub struct CreateFileTreeEntryParams { pub async fn read_file_preview( Json(params): Json, ) -> Result, AppCommandError> { - let result = - folder_commands::read_file_preview(params.root_path, params.path).await?; + let result = folder_commands::read_file_preview(params.root_path, params.path).await?; Ok(Json(result)) } pub async fn read_file_base64( Json(params): Json, ) -> Result, AppCommandError> { - let result = - folder_commands::read_file_base64(params.path, params.max_bytes).await?; + let result = folder_commands::read_file_base64(params.path, params.max_bytes).await?; Ok(Json(result)) } pub async fn read_file_for_edit( Json(params): Json, ) -> Result, AppCommandError> { - let result = - folder_commands::read_file_for_edit(params.root_path, params.path).await?; + let result = folder_commands::read_file_for_edit(params.root_path, params.path).await?; Ok(Json(result)) } @@ -114,24 +111,17 @@ pub async fn save_file_content( pub async fn save_file_copy( Json(params): Json, ) -> Result, AppCommandError> { - let result = folder_commands::save_file_copy( - params.root_path, - params.path, - params.content, - ) - .await?; + let result = + folder_commands::save_file_copy(params.root_path, params.path, params.content).await?; Ok(Json(result)) } pub async fn rename_file_tree_entry( Json(params): Json, ) -> Result, AppCommandError> { - let result = folder_commands::rename_file_tree_entry( - params.root_path, - params.path, - params.new_name, - ) - .await?; + let result = + folder_commands::rename_file_tree_entry(params.root_path, params.path, params.new_name) + .await?; Ok(Json(result)) } diff --git a/src-tauri/src/web/handlers/folder_commands.rs b/src-tauri/src/web/handlers/folder_commands.rs index 7756bad..1cb6858 100644 --- a/src-tauri/src/web/handlers/folder_commands.rs +++ b/src-tauri/src/web/handlers/folder_commands.rs @@ -68,14 +68,10 @@ pub async fn create_folder_command( Json(params): Json, ) -> Result, AppCommandError> { let db = &state.db; - let result = folder_command_service::create( - &db.conn, - params.folder_id, - ¶ms.name, - ¶ms.command, - ) - .await - .map_err(AppCommandError::from)?; + let result = + folder_command_service::create(&db.conn, params.folder_id, ¶ms.name, ¶ms.command) + .await + .map_err(AppCommandError::from)?; Ok(Json(result)) } @@ -142,10 +138,12 @@ pub async fn bootstrap_folder_commands_from_package_json( crate::commands::folder_commands::load_package_scripts_as_commands(¶ms.folder_path) }) .await - .map_err(|e| AppCommandError::new( - crate::app_error::AppErrorCode::TaskExecutionFailed, - format!("bootstrap task failed: {e}"), - ))?; + .map_err(|e| { + AppCommandError::new( + crate::app_error::AppErrorCode::TaskExecutionFailed, + format!("bootstrap task failed: {e}"), + ) + })?; if commands_to_create.is_empty() { return Ok(Json(existing)); diff --git a/src-tauri/src/web/handlers/git.rs b/src-tauri/src/web/handlers/git.rs index c62d3c9..a7e69c3 100644 --- a/src-tauri/src/web/handlers/git.rs +++ b/src-tauri/src/web/handlers/git.rs @@ -79,8 +79,7 @@ pub struct GitStatusParams { pub async fn git_status( Json(params): Json, ) -> Result>, AppCommandError> { - let result = - folder_commands::git_status(params.path, params.show_all_untracked).await?; + let result = folder_commands::git_status(params.path, params.show_all_untracked).await?; Ok(Json(result)) } @@ -101,8 +100,7 @@ pub struct GitCommitBranchesParams { pub async fn git_commit_branches( Json(params): Json, ) -> Result>, AppCommandError> { - let result = - folder_commands::git_commit_branches(params.path, params.commit).await?; + let result = folder_commands::git_commit_branches(params.path, params.commit).await?; Ok(Json(result)) } @@ -117,9 +115,7 @@ pub struct GitShowFileParams { pub async fn git_show_file( Json(params): Json, ) -> Result, AppCommandError> { - let result = - folder_commands::git_show_file(params.path, params.file, params.ref_name) - .await?; + let result = folder_commands::git_show_file(params.path, params.file, params.ref_name).await?; Ok(Json(result)) } @@ -130,9 +126,7 @@ pub struct GitDiffParams { pub file: Option, } -pub async fn git_diff( - Json(params): Json, -) -> Result, AppCommandError> { +pub async fn git_diff(Json(params): Json) -> Result, AppCommandError> { let result = folder_commands::git_diff(params.path, params.file).await?; Ok(Json(result)) } @@ -160,13 +154,8 @@ pub struct GitLogParams { pub async fn git_log( Json(params): Json, ) -> Result, AppCommandError> { - let result = folder_commands::git_log( - params.path, - params.limit, - params.branch, - params.remote, - ) - .await?; + let result = + folder_commands::git_log(params.path, params.limit, params.branch, params.remote).await?; Ok(Json(result)) } @@ -174,9 +163,7 @@ pub async fn git_log( // New pure git handlers (Pattern A – direct function calls) // --------------------------------------------------------------------------- -pub async fn git_init( - Json(params): Json, -) -> Result, AppCommandError> { +pub async fn git_init(Json(params): Json) -> Result, AppCommandError> { folder_commands::git_init(params.path).await?; Ok(Json(())) } @@ -220,8 +207,7 @@ pub struct GitNewBranchParams { pub async fn git_new_branch( Json(params): Json, ) -> Result, AppCommandError> { - folder_commands::git_new_branch(params.path, params.branch_name, params.start_point) - .await?; + folder_commands::git_new_branch(params.path, params.branch_name, params.start_point).await?; Ok(Json(())) } @@ -236,12 +222,8 @@ pub struct GitWorktreeAddParams { pub async fn git_worktree_add( Json(params): Json, ) -> Result, AppCommandError> { - folder_commands::git_worktree_add( - params.path, - params.branch_name, - params.worktree_path, - ) - .await?; + folder_commands::git_worktree_add(params.path, params.branch_name, params.worktree_path) + .await?; Ok(Json(())) } @@ -260,9 +242,7 @@ pub struct GitResetParams { pub mode: String, } -pub async fn git_reset( - Json(params): Json, -) -> Result, AppCommandError> { +pub async fn git_reset(Json(params): Json) -> Result, AppCommandError> { folder_commands::git_reset(params.path, params.commit, params.mode).await?; Ok(Json(())) } @@ -285,12 +265,8 @@ pub struct GitStashPushParams { pub async fn git_stash_push( Json(params): Json, ) -> Result, AppCommandError> { - let result = folder_commands::git_stash_push( - params.path, - params.message, - params.keep_index, - ) - .await?; + let result = + folder_commands::git_stash_push(params.path, params.message, params.keep_index).await?; Ok(Json(result)) } @@ -304,8 +280,7 @@ pub struct GitStashPopParams { pub async fn git_stash_pop( Json(params): Json, ) -> Result, AppCommandError> { - let result = - folder_commands::git_stash_pop(params.path, params.stash_ref).await?; + let result = folder_commands::git_stash_pop(params.path, params.stash_ref).await?; Ok(Json(result)) } @@ -319,16 +294,14 @@ pub async fn git_stash_list( pub async fn git_stash_apply( Json(params): Json, ) -> Result, AppCommandError> { - let result = - folder_commands::git_stash_apply(params.path, params.stash_ref).await?; + let result = folder_commands::git_stash_apply(params.path, params.stash_ref).await?; Ok(Json(result)) } pub async fn git_stash_drop( Json(params): Json, ) -> Result, AppCommandError> { - let result = - folder_commands::git_stash_drop(params.path, params.stash_ref).await?; + let result = folder_commands::git_stash_drop(params.path, params.stash_ref).await?; Ok(Json(result)) } @@ -342,16 +315,14 @@ pub async fn git_stash_clear( pub async fn git_stash_show( Json(params): Json, ) -> Result>, AppCommandError> { - let result = - folder_commands::git_stash_show(params.path, params.stash_ref).await?; + let result = folder_commands::git_stash_show(params.path, params.stash_ref).await?; Ok(Json(result)) } pub async fn git_is_tracked( Json(params): Json, ) -> Result, AppCommandError> { - let result = - folder_commands::git_is_tracked(params.path, params.file).await?; + let result = folder_commands::git_is_tracked(params.path, params.file).await?; Ok(Json(result)) } @@ -366,12 +337,8 @@ pub struct GitDiffWithBranchParams { pub async fn git_diff_with_branch( Json(params): Json, ) -> Result, AppCommandError> { - let result = folder_commands::git_diff_with_branch( - params.path, - params.branch, - params.file, - ) - .await?; + let result = + folder_commands::git_diff_with_branch(params.path, params.branch, params.file).await?; Ok(Json(result)) } @@ -386,9 +353,7 @@ pub struct GitShowDiffParams { pub async fn git_show_diff( Json(params): Json, ) -> Result, AppCommandError> { - let result = - folder_commands::git_show_diff(params.path, params.commit, params.file) - .await?; + let result = folder_commands::git_show_diff(params.path, params.commit, params.file).await?; Ok(Json(result)) } @@ -437,16 +402,14 @@ pub async fn git_set_remote_url( pub async fn git_merge( Json(params): Json, ) -> Result, AppCommandError> { - let result = - folder_commands::git_merge(params.path, params.branch_name).await?; + let result = folder_commands::git_merge(params.path, params.branch_name).await?; Ok(Json(result)) } pub async fn git_rebase( Json(params): Json, ) -> Result, AppCommandError> { - let result = - folder_commands::git_rebase(params.path, params.branch_name).await?; + let result = folder_commands::git_rebase(params.path, params.branch_name).await?; Ok(Json(result)) } @@ -462,8 +425,7 @@ pub async fn git_delete_branch( Json(params): Json, ) -> Result, AppCommandError> { let result = - folder_commands::git_delete_branch(params.path, params.branch_name, params.force) - .await?; + folder_commands::git_delete_branch(params.path, params.branch_name, params.force).await?; Ok(Json(result)) } @@ -503,16 +465,14 @@ pub async fn git_list_conflicts( pub async fn git_conflict_file_versions( Json(params): Json, ) -> Result, AppCommandError> { - let result = - folder_commands::git_conflict_file_versions(params.path, params.file).await?; + let result = folder_commands::git_conflict_file_versions(params.path, params.file).await?; Ok(Json(result)) } pub async fn git_resolve_conflict( Json(params): Json, ) -> Result, AppCommandError> { - folder_commands::git_resolve_conflict(params.path, params.file, params.content) - .await?; + folder_commands::git_resolve_conflict(params.path, params.file, params.content).await?; Ok(Json(())) } diff --git a/src-tauri/src/web/handlers/mcp.rs b/src-tauri/src/web/handlers/mcp.rs index e43f3ae..7b8c2cb 100644 --- a/src-tauri/src/web/handlers/mcp.rs +++ b/src-tauri/src/web/handlers/mcp.rs @@ -71,8 +71,7 @@ pub async fn mcp_scan_local() -> Result>, AppCommandErr Ok(Json(result)) } -pub async fn mcp_list_marketplaces( -) -> Result>, AppCommandError> { +pub async fn mcp_list_marketplaces() -> Result>, AppCommandError> { let result = mcp_commands::mcp_list_marketplaces().await?; Ok(Json(result)) } @@ -80,23 +79,18 @@ pub async fn mcp_list_marketplaces( pub async fn mcp_search_marketplace( Json(params): Json, ) -> Result>, AppCommandError> { - let result = mcp_commands::mcp_search_marketplace( - params.provider_id, - params.query, - params.limit, - ) - .await?; + let result = + mcp_commands::mcp_search_marketplace(params.provider_id, params.query, params.limit) + .await?; Ok(Json(result)) } pub async fn mcp_get_marketplace_server_detail( Json(params): Json, ) -> Result, AppCommandError> { - let result = mcp_commands::mcp_get_marketplace_server_detail( - params.provider_id, - params.server_id, - ) - .await?; + let result = + mcp_commands::mcp_get_marketplace_server_detail(params.provider_id, params.server_id) + .await?; Ok(Json(result)) } @@ -119,27 +113,21 @@ pub async fn mcp_install_from_marketplace( pub async fn mcp_upsert_local_server( Json(params): Json, ) -> Result, AppCommandError> { - let result = mcp_commands::mcp_upsert_local_server( - params.server_id, - params.spec, - params.apps, - ) - .await?; + let result = + mcp_commands::mcp_upsert_local_server(params.server_id, params.spec, params.apps).await?; Ok(Json(result)) } pub async fn mcp_set_server_apps( Json(params): Json, ) -> Result>, AppCommandError> { - let result = - mcp_commands::mcp_set_server_apps(params.server_id, params.apps).await?; + let result = mcp_commands::mcp_set_server_apps(params.server_id, params.apps).await?; Ok(Json(result)) } pub async fn mcp_remove_server( Json(params): Json, ) -> Result, AppCommandError> { - let result = - mcp_commands::mcp_remove_server(params.server_id, params.apps).await?; + let result = mcp_commands::mcp_remove_server(params.server_id, params.apps).await?; Ok(Json(result)) } diff --git a/src-tauri/src/web/handlers/system_settings.rs b/src-tauri/src/web/handlers/system_settings.rs index 30d357f..b27fd13 100644 --- a/src-tauri/src/web/handlers/system_settings.rs +++ b/src-tauri/src/web/handlers/system_settings.rs @@ -44,8 +44,7 @@ pub async fn get_system_language_settings( Extension(state): Extension>, ) -> Result, AppCommandError> { let db = &state.db; - let settings = - settings_commands::load_system_language_settings(&db.conn).await?; + let settings = settings_commands::load_system_language_settings(&db.conn).await?; Ok(Json(settings)) } @@ -67,13 +66,9 @@ pub async fn update_system_proxy_settings( .with_detail(e.to_string()) })?; - app_metadata_service::upsert_value( - &db.conn, - SYSTEM_PROXY_SETTINGS_KEY, - &serialized, - ) - .await - .map_err(AppCommandError::from)?; + app_metadata_service::upsert_value(&db.conn, SYSTEM_PROXY_SETTINGS_KEY, &serialized) + .await + .map_err(AppCommandError::from)?; proxy::apply_system_proxy_settings(&settings)?; Ok(Json(settings)) @@ -91,13 +86,9 @@ pub async fn update_system_language_settings( .with_detail(e.to_string()) })?; - app_metadata_service::upsert_value( - &db.conn, - SYSTEM_LANGUAGE_SETTINGS_KEY, - &serialized, - ) - .await - .map_err(AppCommandError::from)?; + app_metadata_service::upsert_value(&db.conn, SYSTEM_LANGUAGE_SETTINGS_KEY, &serialized) + .await + .map_err(AppCommandError::from)?; crate::web::event_bridge::emit_event( &state.emitter, diff --git a/src-tauri/src/web/handlers/version_control.rs b/src-tauri/src/web/handlers/version_control.rs index 6608715..6998367 100644 --- a/src-tauri/src/web/handlers/version_control.rs +++ b/src-tauri/src/web/handlers/version_control.rs @@ -125,10 +125,8 @@ pub async fn get_github_accounts( let settings = match raw { Some(raw) => serde_json::from_str::(&raw).map_err(|e| { - AppCommandError::configuration_invalid( - "Failed to parse stored GitHub accounts", - ) - .with_detail(e.to_string()) + AppCommandError::configuration_invalid("Failed to parse stored GitHub accounts") + .with_detail(e.to_string()) })?, None => GitHubAccountsSettings::default(), }; @@ -160,8 +158,7 @@ pub async fn update_github_accounts( pub async fn validate_github_token( Json(params): Json, ) -> Result, AppCommandError> { - let result = - vc_commands::validate_github_token(params.server_url, params.token).await?; + let result = vc_commands::validate_github_token(params.server_url, params.token).await?; Ok(Json(result)) } diff --git a/src-tauri/src/web/mod.rs b/src-tauri/src/web/mod.rs index de44008..c9ef395 100644 --- a/src-tauri/src/web/mod.rs +++ b/src-tauri/src/web/mod.rs @@ -121,11 +121,10 @@ async fn persist_web_service_config( }) .await .map_err(|e: TransactionError| match e { - TransactionError::Connection(db) => AppCommandError::new( - AppErrorCode::DatabaseError, - "Database transaction failed", - ) - .with_detail(db.to_string()), + TransactionError::Connection(db) => { + AppCommandError::new(AppErrorCode::DatabaseError, "Database transaction failed") + .with_detail(db.to_string()) + } TransactionError::Transaction(inner) => inner, }) } @@ -178,12 +177,18 @@ pub(crate) fn find_static_dir_tauri(app: &tauri::AppHandle) -> PathBuf { if let Some(ref dir) = resource { let web = dir.join("web"); if web.join("index.html").exists() { - eprintln!("[WEB] Serving static files from resource/web: {}", web.display()); + eprintln!( + "[WEB] Serving static files from resource/web: {}", + web.display() + ); return web; } // Fallback: files at resource root. if dir.join("index.html").exists() { - eprintln!("[WEB] Serving static files from resource dir: {}", dir.display()); + eprintln!( + "[WEB] Serving static files from resource dir: {}", + dir.display() + ); return dir.clone(); } } @@ -197,7 +202,10 @@ pub(crate) fn find_static_dir_fallback() -> PathBuf { let project_out = manifest_dir.parent().map(|p| p.join("out")); if let Some(ref out) = project_out { if out.join("index.html").exists() { - eprintln!("[WEB] Serving static files from project out/: {}", out.display()); + eprintln!( + "[WEB] Serving static files from project out/: {}", + out.display() + ); return out.clone(); } } @@ -217,7 +225,10 @@ pub fn find_static_dir_standalone(explicit: Option<&str>) -> PathBuf { if let Some(dir) = explicit { let p = PathBuf::from(dir); if p.join("index.html").exists() { - eprintln!("[WEB] Serving static files from CODEG_STATIC_DIR: {}", p.display()); + eprintln!( + "[WEB] Serving static files from CODEG_STATIC_DIR: {}", + p.display() + ); return p; } } @@ -292,12 +303,13 @@ pub(crate) async fn do_start_web_server_with_state( let host = host.unwrap_or_else(|| "0.0.0.0".to_string()); let token = resolve_web_service_token(&app_state.db.conn, token).await?; - let addr: SocketAddr = format!("{}:{}", host, port) - .parse() - .map_err(|e: std::net::AddrParseError| { - AppCommandError::new(AppErrorCode::InvalidInput, ERR_INVALID_ADDRESS) - .with_detail(e.to_string()) - })?; + let addr: SocketAddr = + format!("{}:{}", host, port) + .parse() + .map_err(|e: std::net::AddrParseError| { + AppCommandError::new(AppErrorCode::InvalidInput, ERR_INVALID_ADDRESS) + .with_detail(e.to_string()) + })?; let listener = tokio::net::TcpListener::bind(addr) .await @@ -387,12 +399,13 @@ pub async fn start_web_server( let host_val = host.unwrap_or_else(|| "0.0.0.0".to_string()); let token = resolve_web_service_token(&db.conn, token).await?; - let addr: SocketAddr = format!("{}:{}", host_val, port_val) - .parse() - .map_err(|e: std::net::AddrParseError| { - AppCommandError::new(AppErrorCode::InvalidInput, ERR_INVALID_ADDRESS) - .with_detail(e.to_string()) - })?; + let addr: SocketAddr = + format!("{}:{}", host_val, port_val) + .parse() + .map_err(|e: std::net::AddrParseError| { + AppCommandError::new(AppErrorCode::InvalidInput, ERR_INVALID_ADDRESS) + .with_detail(e.to_string()) + })?; let listener = tokio::net::TcpListener::bind(addr) .await @@ -410,7 +423,10 @@ pub async fn start_web_server( }, connection_manager: (*app.state::()).clone_ref(), terminal_manager: (*app.state::()).clone_ref(), - event_broadcaster: app.state::>().inner().clone(), + event_broadcaster: app + .state::>() + .inner() + .clone(), emitter: crate::web::event_bridge::EventEmitter::Tauri(app.clone()), data_dir: app.path().app_data_dir().unwrap_or_default(), web_server_state: WebServerState::new(), // placeholder; not used by handlers diff --git a/src-tauri/src/web/router.rs b/src-tauri/src/web/router.rs index 1cda392..5cd1841 100644 --- a/src-tauri/src/web/router.rs +++ b/src-tauri/src/web/router.rs @@ -106,10 +106,7 @@ pub fn build_router(state: Arc, token: String, static_dir: std::path:: "/remove_folder_from_workspace", post(handlers::folders::remove_folder_from_workspace), ) - .route( - "/reorder_folders", - post(handlers::folders::reorder_folders), - ) + .route("/reorder_folders", post(handlers::folders::reorder_folders)) .route( "/add_folder_to_history", post(handlers::folders::add_folder_to_history), diff --git a/src-tauri/src/web/ws.rs b/src-tauri/src/web/ws.rs index 8ab487e..073cd3b 100644 --- a/src-tauri/src/web/ws.rs +++ b/src-tauri/src/web/ws.rs @@ -1,10 +1,10 @@ use std::sync::Arc; +use axum::extract::ws::{Message, WebSocket}; use axum::{ extract::{Extension, WebSocketUpgrade}, response::IntoResponse, }; -use axum::extract::ws::{Message, WebSocket}; use crate::app_state::AppState; diff --git a/src-tauri/src/workspace_state/mod.rs b/src-tauri/src/workspace_state/mod.rs index 815cbc2..2e93942 100644 --- a/src-tauri/src/workspace_state/mod.rs +++ b/src-tauri/src/workspace_state/mod.rs @@ -321,7 +321,6 @@ impl WatchEventBatch { "modify".to_string() } } - } fn normalize_slash_path(path: &Path) -> String { @@ -375,7 +374,9 @@ fn git_check_ignored_paths( if let Some(mut stdin) = child.stdin.take() { for path in paths { - stdin.write_all(path.as_bytes()).map_err(AppCommandError::io)?; + stdin + .write_all(path.as_bytes()) + .map_err(AppCommandError::io)?; stdin.write_all(&[0]).map_err(AppCommandError::io)?; } } @@ -899,15 +900,13 @@ pub async fn start_workspace_state_stream_core( let mut watcher = Some( notify::recommended_watcher( move |result: Result| match result { - Ok(event) => { - match event_tx.try_send(event) { - Ok(()) => {} - Err(TrySendError::Full(_)) => { - dropped_events_for_callback.store(true, Ordering::Release); - } - Err(TrySendError::Closed(_)) => {} + Ok(event) => match event_tx.try_send(event) { + Ok(()) => {} + Err(TrySendError::Full(_)) => { + dropped_events_for_callback.store(true, Ordering::Release); } - } + Err(TrySendError::Closed(_)) => {} + }, Err(err) => { eprintln!( "[workspace-state-watch] failed event for {}: {}", @@ -1082,7 +1081,8 @@ mod tests { #[test] fn workspace_state_core_seq_is_monotonic() { - let mut core = WorkspaceStateCore::new("/tmp/repo".to_string(), Vec::new(), Vec::new(), false); + let mut core = + WorkspaceStateCore::new("/tmp/repo".to_string(), Vec::new(), Vec::new(), false); let e1 = core.append_event( "meta".to_string(), @@ -1107,7 +1107,8 @@ mod tests { #[test] fn workspace_state_core_snapshot_incremental_when_since_available() { - let mut core = WorkspaceStateCore::new("/tmp/repo".to_string(), Vec::new(), Vec::new(), false); + let mut core = + WorkspaceStateCore::new("/tmp/repo".to_string(), Vec::new(), Vec::new(), false); let e1 = core.append_event( "meta".to_string(), @@ -1136,7 +1137,8 @@ mod tests { #[test] fn workspace_state_core_snapshot_full_when_since_too_old() { - let mut core = WorkspaceStateCore::new("/tmp/repo".to_string(), Vec::new(), Vec::new(), false); + let mut core = + WorkspaceStateCore::new("/tmp/repo".to_string(), Vec::new(), Vec::new(), false); core.recent_capacity = 1; core.append_event( diff --git a/src/components/chat/conversation-context-bar.tsx b/src/components/chat/conversation-context-bar.tsx index 0f8a583..614dccf 100644 --- a/src/components/chat/conversation-context-bar.tsx +++ b/src/components/chat/conversation-context-bar.tsx @@ -73,10 +73,7 @@ export const ConversationContextBar = memo(function ConversationContextBar({ setTabFolder(ownTab.id, target.id, target.path) toast.success(t("toasts.folderChanged", { name: target.name })) } catch (err) { - console.error( - "[ConversationContextBar] switch folder failed:", - err - ) + console.error("[ConversationContextBar] switch folder failed:", err) toast.error(t("toasts.openFolderFailed")) } }} diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx index d726d72..8484db3 100644 --- a/src/components/layout/sidebar.tsx +++ b/src/components/layout/sidebar.tsx @@ -96,9 +96,7 @@ export function Sidebar() { size="icon" className="h-6 w-6 shrink-0 text-muted-foreground" onClick={handleToggleExpandAll} - title={ - allExpanded ? t("collapseAllGroups") : t("expandAllGroups") - } + title={allExpanded ? t("collapseAllGroups") : t("expandAllGroups")} > {allExpanded ? (