diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 71f64ac..9931ccc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,9 +18,14 @@ jobs: - name: Setup Rust toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + components: clippy - name: Build run: cargo build --release - name: Test run: cargo test + + - name: Clippy + run: cargo clippy -- -D warnings diff --git a/src/app/bgbash/control.rs b/src/app/bgbash/control.rs index cd607bc..ba1f953 100644 --- a/src/app/bgbash/control.rs +++ b/src/app/bgbash/control.rs @@ -1,3 +1,4 @@ +#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)] //! Global registry of running background bash jobs, and control operations //! (output polling, kill) exposed to the rest of the app. //! @@ -57,7 +58,7 @@ pub fn bash_output(id: &str) -> Option> { /// Return: `Ok(())` on success, `Err` if the lock is poisoned or no job /// with that id exists. pub fn bash_kill(id: &str) -> anyhow::Result<()> { - let mut map = bash_jobs_map().lock().map_err(|e| anyhow::anyhow!("lock error: {}", e))?; + let mut map = bash_jobs_map().lock().map_err(|e| anyhow::anyhow!("lock error: {e}"))?; let job = map.remove(id); match job { Some(job) => { @@ -70,6 +71,6 @@ pub fn bash_kill(id: &str) -> anyhow::Result<()> { } Ok(()) } - None => anyhow::bail!("bash job '{}' not found", id), + None => anyhow::bail!("bash job '{id}' not found"), } } diff --git a/src/app/bgbash/job.rs b/src/app/bgbash/job.rs index 3564fe9..9205139 100644 --- a/src/app/bgbash/job.rs +++ b/src/app/bgbash/job.rs @@ -17,7 +17,7 @@ use std::io::BufRead; /// Maximum number of output lines buffered in memory per background job. /// Beyond this limit, old output is dropped to prevent OOM (CWE-770). -/// 10_000 lines at ~100 bytes each ≈ 1 MiB per job, sufficient for most +/// `10_000` lines at ~100 bytes each ≈ 1 MiB per job, sufficient for most /// command output. The stderr drain thread also uses the same limit. const MAX_OUTPUT_LINES: usize = 10_000; @@ -52,7 +52,7 @@ pub fn spawn_bash_job(command: String) -> BashJob { let id = uuid::Uuid::new_v4().to_string(); let (output_tx, output_rx) = mpsc::sync_channel::(MAX_OUTPUT_LINES); let (pid_tx, pid_rx) = mpsc::channel::(); - let cmd = command.clone(); + let cmd = command; let id_for_log = id.clone(); let thread_id = id.clone(); @@ -66,12 +66,12 @@ pub fn spawn_bash_job(command: String) -> BashJob { let output_tx = output_tx.clone(); let pid_tx = pid_tx.clone(); let id_for_log = id_for_log.clone(); - move || spawn_bash_thread_body(cmd, output_tx, pid_tx, id_for_log) + move || spawn_bash_thread_body(&cmd, &output_tx, &pid_tx, &id_for_log) }).is_err() { tracing::warn!("[bgbash:{}] failed to spawn named thread, using unnamed fallback", id_for_log); thread::spawn(move || { - spawn_bash_thread_body(cmd, output_tx, pid_tx, id_for_log) + spawn_bash_thread_body(&cmd, &output_tx, &pid_tx, &id_for_log); }); } @@ -89,21 +89,21 @@ pub fn spawn_bash_job(command: String) -> BashJob { /// spawned from both the named Builder and the unnamed fallback without /// double-moving the closure. fn spawn_bash_thread_body( - cmd: String, - output_tx: std::sync::mpsc::SyncSender, - pid_tx: std::sync::mpsc::Sender, - id_for_log: String, + cmd: &str, + output_tx: &std::sync::mpsc::SyncSender, + pid_tx: &std::sync::mpsc::Sender, + id_for_log: &str, ) { let mut child = match Command::new("sh") .arg("-c") - .arg(&cmd) + .arg(cmd) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() { Ok(c) => c, Err(e) => { - let _ = output_tx.try_send(format!("__error:{}", e)); + let _ = output_tx.try_send(format!("__error:{e}")); let _ = output_tx.try_send("__exit:-1".to_string()); return; } @@ -124,7 +124,7 @@ fn spawn_bash_thread_body( std::thread::spawn(move || { let reader = std::io::BufReader::new(stderr); for line in reader.lines().map_while(Result::ok) { - if stderr_tx.try_send(format!("[stderr] {}", line)).is_err() { + if stderr_tx.try_send(format!("[stderr] {line}")).is_err() { tracing::debug!("[bgbash] stderr buffer full, discarding remaining stderr"); break; } @@ -153,7 +153,7 @@ fn spawn_bash_thread_body( impl BashJob { /// Non-blocking poll for the next output line from the job's channel. /// - /// Flow: try_recv the channel → if it's an `__exit:` sentinel, + /// Flow: `try_recv` the channel → if it's an `__exit:` sentinel, /// record `exit_code` and return `None` instead of surfacing it as /// output → otherwise return the line. /// diff --git a/src/app/harness.rs b/src/app/harness.rs index 84625d4..96ce1c8 100644 --- a/src/app/harness.rs +++ b/src/app/harness.rs @@ -122,6 +122,7 @@ impl Harness { /// as risky because their behaviour is unknown. /// /// Return: `Verdict::Allow` or `Verdict::Block(reason)`. + #[allow(clippy::too_many_lines, clippy::unnecessary_debug_formatting)] pub fn gate_tool_call( tool_name: &str, args: &serde_json::Value, @@ -163,8 +164,7 @@ impl Harness { let allowed = workspace_roots.iter().any(|r| out_path.starts_with(r)); if !allowed { return Verdict::Block(format!( - "output path '{:?}' is outside all workspace roots", - out_path + "output path '{out_path:?}' is outside all workspace roots" )); } } @@ -286,7 +286,7 @@ impl Harness { (>= {MIN_REASON_LEN} chars) explaining why it is needed" )); } - } else if args.as_object().map(|m| !m.is_empty()).unwrap_or(false) { + } else if args.as_object().is_some_and(|m| !m.is_empty()) { // Only require reason when there are meaningful arguments return Verdict::Block(format!( "MCP tool '{tool_name}' requires a 'reason' argument \ diff --git a/src/app/lsp/client.rs b/src/app/lsp/client.rs index 8225a14..37dbdeb 100644 --- a/src/app/lsp/client.rs +++ b/src/app/lsp/client.rs @@ -30,12 +30,12 @@ fn file_path_to_uri(path: &str) -> String { if cfg!(windows) { let path_str = path_str.replace('\\', "/"); if path_str.starts_with('/') { - format!("file://{}", path_str) + format!("file://{path_str}") } else { - format!("file:///{}", path_str) + format!("file:///{path_str}") } } else { - format!("file://{}", path_str) + format!("file://{path_str}") } } @@ -48,7 +48,7 @@ impl LspClient { cmd.stderr(Stdio::piped()); let mut child = cmd.spawn() - .map_err(|e| anyhow::anyhow!("failed to spawn LSP server '{}': {}", command, e))?; + .map_err(|e| anyhow::anyhow!("failed to spawn LSP server '{command}': {e}"))?; let stdin = child.stdin.take() .ok_or_else(|| anyhow::anyhow!("failed to capture stdin for LSP server"))?; @@ -106,10 +106,10 @@ impl LspClient { } }); - let result = client.call_with_timeout("initialize", init_params, Duration::from_millis(LSP_INIT_TIMEOUT_MS))?; + let result = client.call_with_timeout("initialize", &init_params, Duration::from_millis(LSP_INIT_TIMEOUT_MS))?; client.server_capabilities = result.get("capabilities").cloned().unwrap_or_default(); - client.notify("initialized", json!({}))?; + client.notify("initialized", &json!({}))?; Ok(client) } @@ -118,11 +118,11 @@ impl LspClient { &self.server_capabilities } - pub fn call(&mut self, method: &str, params: Value) -> anyhow::Result { + pub fn call(&mut self, method: &str, params: &Value) -> anyhow::Result { self.call_with_timeout(method, params, Duration::from_millis(LSP_CALL_TIMEOUT_MS)) } - fn call_with_timeout(&mut self, method: &str, params: Value, timeout: Duration) -> anyhow::Result { + fn call_with_timeout(&mut self, method: &str, params: &Value, timeout: Duration) -> anyhow::Result { self.next_id += 1; let id = self.next_id; let req = json!({ @@ -135,7 +135,7 @@ impl LspClient { self.read_response(id, timeout) } - pub fn notify(&mut self, method: &str, params: Value) -> anyhow::Result<()> { + pub fn notify(&mut self, method: &str, params: &Value) -> anyhow::Result<()> { let req = json!({ "jsonrpc": "2.0", "method": method, @@ -146,14 +146,14 @@ impl LspClient { fn send_frame(&mut self, msg: &Value) -> anyhow::Result<()> { let body = serde_json::to_string(msg) - .map_err(|e| anyhow::anyhow!("failed to serialize LSP message: {}", e))?; + .map_err(|e| anyhow::anyhow!("failed to serialize LSP message: {e}"))?; let header = format!("Content-Length: {}\r\n\r\n", body.len()); self.stdin.write_all(header.as_bytes()) - .map_err(|e| anyhow::anyhow!("failed to write LSP frame header: {}", e))?; + .map_err(|e| anyhow::anyhow!("failed to write LSP frame header: {e}"))?; self.stdin.write_all(body.as_bytes()) - .map_err(|e| anyhow::anyhow!("failed to write LSP frame body: {}", e))?; + .map_err(|e| anyhow::anyhow!("failed to write LSP frame body: {e}"))?; self.stdin.flush() - .map_err(|e| anyhow::anyhow!("failed to flush LSP stdin: {}", e))?; + .map_err(|e| anyhow::anyhow!("failed to flush LSP stdin: {e}"))?; Ok(()) } @@ -166,9 +166,9 @@ impl LspClient { let frame = self.read_frame()?; if frame.get("id") == Some(&json!(expected_id)) { if let Some(err) = frame.get("error") { - let code = err.get("code").and_then(|c| c.as_i64()).unwrap_or(0); + let code = err.get("code").and_then(serde_json::Value::as_i64).unwrap_or(0); let msg = err.get("message").and_then(|m| m.as_str()).unwrap_or("unknown error"); - anyhow::bail!("LSP error {}: {}", code, msg); + anyhow::bail!("LSP error {code}: {msg}"); } return Ok(frame.get("result").cloned().unwrap_or(Value::Null)); } @@ -179,7 +179,7 @@ impl LspClient { let deadline = Instant::now() + timeout; loop { if Instant::now() > deadline { - anyhow::bail!("timed out waiting for LSP notification '{}'", method); + anyhow::bail!("timed out waiting for LSP notification '{method}'"); } let frame = self.read_frame()?; if frame.get("method") == Some(&json!(method)) { @@ -195,22 +195,21 @@ impl LspClient { match self.stdout.read_line(&mut line) { Ok(0) => anyhow::bail!("LSP server closed the connection"), Ok(_) => {} - Err(e) => anyhow::bail!("LSP read error: {}", e), + Err(e) => anyhow::bail!("LSP read error: {e}"), } let trimmed = line.trim(); if trimmed.is_empty() { break; } if let Some(len_str) = trimmed.strip_prefix("Content-Length: ") { - let length: usize = len_str.trim().parse::() - .map_err(|e| anyhow::anyhow!("invalid Content-Length '{}': {}", len_str.trim(), e))?; // Cap Content-Length at 64 MiB to prevent OOM from a // malicious or misconfigured LSP server (CWE-400). const MAX_CONTENT_LENGTH: usize = 64 * 1024 * 1024; + let length: usize = len_str.trim().parse::() + .map_err(|e| anyhow::anyhow!("invalid Content-Length '{}': {}", len_str.trim(), e))?; if length > MAX_CONTENT_LENGTH { anyhow::bail!( - "Content-Length {} exceeds maximum allowed size of {} bytes", - length, MAX_CONTENT_LENGTH, + "Content-Length {length} exceeds maximum allowed size of {MAX_CONTENT_LENGTH} bytes", ); } content_length = Some(length); @@ -222,17 +221,17 @@ impl LspClient { let mut body = vec![0u8; length]; self.stdout.read_exact(&mut body) - .map_err(|e| anyhow::anyhow!("failed to read LSP body ({} bytes): {}", length, e))?; + .map_err(|e| anyhow::anyhow!("failed to read LSP body ({length} bytes): {e}"))?; let json_str = String::from_utf8(body) - .map_err(|e| anyhow::anyhow!("invalid UTF-8 in LSP response: {}", e))?; + .map_err(|e| anyhow::anyhow!("invalid UTF-8 in LSP response: {e}"))?; serde_json::from_str(&json_str) - .map_err(|e| anyhow::anyhow!("invalid JSON in LSP response: {}", e)) + .map_err(|e| anyhow::anyhow!("invalid JSON in LSP response: {e}")) } pub fn did_open(&mut self, uri: &str, language_id: &str, version: i32, text: &str) -> anyhow::Result<()> { - self.notify("textDocument/didOpen", json!({ + self.notify("textDocument/didOpen", &json!({ "textDocument": { "uri": uri, "languageId": language_id, @@ -244,7 +243,7 @@ impl LspClient { #[allow(dead_code)] pub fn did_change(&mut self, uri: &str, version: i32, text: &str) -> anyhow::Result<()> { - self.notify("textDocument/didChange", json!({ + self.notify("textDocument/didChange", &json!({ "textDocument": { "uri": uri, "version": version @@ -256,7 +255,7 @@ impl LspClient { } pub fn did_close(&mut self, uri: &str) -> anyhow::Result<()> { - self.notify("textDocument/didClose", json!({ + self.notify("textDocument/didClose", &json!({ "textDocument": { "uri": uri } @@ -264,28 +263,28 @@ impl LspClient { } pub fn hover(&mut self, uri: &str, line: u32, character: u32) -> anyhow::Result { - self.call("textDocument/hover", json!({ + self.call("textDocument/hover", &json!({ "textDocument": { "uri": uri }, "position": { "line": line, "character": character } })) } pub fn completion(&mut self, uri: &str, line: u32, character: u32) -> anyhow::Result { - self.call("textDocument/completion", json!({ + self.call("textDocument/completion", &json!({ "textDocument": { "uri": uri }, "position": { "line": line, "character": character } })) } pub fn goto_definition(&mut self, uri: &str, line: u32, character: u32) -> anyhow::Result { - self.call("textDocument/definition", json!({ + self.call("textDocument/definition", &json!({ "textDocument": { "uri": uri }, "position": { "line": line, "character": character } })) } pub fn references(&mut self, uri: &str, line: u32, character: u32) -> anyhow::Result { - self.call("textDocument/references", json!({ + self.call("textDocument/references", &json!({ "textDocument": { "uri": uri }, "position": { "line": line, "character": character }, "context": { @@ -296,7 +295,7 @@ impl LspClient { #[allow(dead_code)] pub fn document_symbols(&mut self, uri: &str) -> anyhow::Result { - self.call("textDocument/documentSymbol", json!({ + self.call("textDocument/documentSymbol", &json!({ "textDocument": { "uri": uri } })) } @@ -327,7 +326,7 @@ impl LspClient { /// still proves the process is up and the JSON-RPC channel is live. /// Returns `false` on timeout, EOF, or any read/write error. /// - /// Flow: build request → send_frame → poll frames until id matches + /// Flow: build request → `send_frame` → poll frames until id matches /// (alive) or deadline/read error fires (dead). #[allow(dead_code)] pub fn is_alive(&mut self) -> bool { @@ -369,19 +368,18 @@ impl LspClient { /// not block on any reply. #[allow(dead_code)] pub fn exit(&mut self) -> anyhow::Result<()> { - self.notify("exit", json!({})) + self.notify("exit", &json!({})) } - pub fn shutdown(&mut self) -> anyhow::Result<()> { - let _ = self.call_with_timeout("shutdown", json!({}), Duration::from_secs(5)); - let _ = self.notify("exit", json!({})); - Ok(()) + pub fn shutdown(&mut self) { + let _ = self.call_with_timeout("shutdown", &json!({}), Duration::from_secs(5)); + let _ = self.notify("exit", &json!({})); } } impl Drop for LspClient { fn drop(&mut self) { - let _ = self.notify("exit", json!({})); + let _ = self.notify("exit", &json!({})); } } diff --git a/src/app/lsp/mod.rs b/src/app/lsp/mod.rs index 66e98da..0125ce6 100644 --- a/src/app/lsp/mod.rs +++ b/src/app/lsp/mod.rs @@ -70,7 +70,7 @@ impl LspManager { language_id: &str, ) -> anyhow::Result<()> { if self.servers.iter().any(|s| s.name == name) { - anyhow::bail!("LSP server '{}' is already connected", name); + anyhow::bail!("LSP server '{name}' is already connected"); } let client = LspClient::spawn(command, args)?; self.servers.push(LspServer { @@ -101,7 +101,7 @@ impl LspManager { pub fn disconnect(&mut self, name: &str) -> bool { if let Some(server) = self.servers.iter().find(|s| s.name == name) { if let Ok(mut client) = server.client.lock() { - let _ = client.shutdown(); + client.shutdown(); } } let len = self.servers.len(); @@ -134,7 +134,7 @@ impl LspManager { pub fn find_server_for_path(&self, path: &Path) -> Option>> { path.extension() .and_then(|e| e.to_str()) - .map(|s| format!(".{}", s)) + .map(|s| format!(".{s}")) .and_then(|ext| self.find_server_for_extension(&ext)) } @@ -171,21 +171,15 @@ impl LspManager { /// Non-critical failures (file missing, server unreachable, send /// error) are logged with `tracing::warn!` rather than propagated, /// so a stale notification cannot abort the calling flow. - pub fn did_change_file(&mut self, path: &Path) -> anyhow::Result<()> { - let ext = match path.extension().and_then(|e| e.to_str()).map(|s| format!(".{}", s)) { - Some(ext) => ext, - None => { - tracing::warn!("did_change_file: path has no extension: {:?}", path); - return Ok(()); - } + pub fn did_change_file(&mut self, path: &Path) { + let Some(ext) = path.extension().and_then(|e| e.to_str()).map(|s| format!(".{s}")) else { + tracing::warn!("did_change_file: path has no extension: {:?}", path); + return; }; - let server_name = match self.extension_registry.get(&ext) { - Some(name) => name.clone(), - None => { - tracing::warn!("did_change_file: no LSP server registered for extension '{}'", ext); - return Ok(()); - } + let server_name = if let Some(name) = self.extension_registry.get(&ext) { name.clone() } else { + tracing::warn!("did_change_file: no LSP server registered for extension '{}'", ext); + return; }; let uri = path_to_lsp_uri(&path.to_string_lossy()); @@ -194,7 +188,7 @@ impl LspManager { Ok(t) => t, Err(e) => { tracing::warn!("did_change_file: failed to read {:?}: {}", path, e); - return Ok(()); + return; } }; @@ -202,12 +196,9 @@ impl LspManager { .get_language_id(&server_name) .unwrap_or_else(|| "plaintext".to_string()); - let client = match self.get_client(&server_name) { - Some(c) => c, - None => { - tracing::warn!("did_change_file: server '{}' has no client", server_name); - return Ok(()); - } + let Some(client) = self.get_client(&server_name) else { + tracing::warn!("did_change_file: server '{}' has no client", server_name); + return; }; let next_version = match self.open_files.get(&uri) { @@ -220,7 +211,7 @@ impl LspManager { Ok(c) => c, Err(e) => { tracing::warn!("did_change_file: client mutex poisoned for '{}': {}", server_name, e); - return Ok(()); + return; } }; if self.open_files.contains_key(&uri) { @@ -237,7 +228,7 @@ impl LspManager { uri, e ); - return Ok(()); + return; } self.open_files.insert( @@ -248,7 +239,6 @@ impl LspManager { }, ); - Ok(()) } /// Record that `server_name` has an open document at `uri`. @@ -274,9 +264,9 @@ impl LspManager { /// drop the vec. Failures from individual shutdowns are swallowed /// because the goal is best-effort termination during teardown. pub fn shutdown_all(&mut self) { - for server in self.servers.iter() { + for server in &self.servers { if let Ok(mut client) = server.client.lock() { - let _ = client.shutdown(); + client.shutdown(); } } self.servers.clear(); diff --git a/src/app/lsp/provisioner.rs b/src/app/lsp/provisioner.rs index 68c4bf2..453df23 100644 --- a/src/app/lsp/provisioner.rs +++ b/src/app/lsp/provisioner.rs @@ -1,10 +1,10 @@ //! Auto-provisioning engine for LSP language servers. //! -//! Flow: detect_env() → for each supported server in supported_servers() -//! → provision_single() tries install tiers in order → returns -//! ProvisionResult (AlreadyAvailable / Installed / Failed). -//! Caller can then call auto_connect() to attach available servers -//! to an existing LspManager. +//! Flow: `detect_env()` → for each supported server in `supported_servers()` +//! → `provision_single()` tries install tiers in order → returns +//! `ProvisionResult` (`AlreadyAvailable` / Installed / Failed). +//! Caller can then call `auto_connect()` to attach available servers +//! to an existing `LspManager`. //! //! Why: opening a project on a fresh machine should not require the user //! to manually hunt down and install 4 different language servers. @@ -28,7 +28,7 @@ pub type ProgressFn<'a> = Option<&'a dyn Fn(&str)>; /// Result of attempting to make a single language server available. /// -/// The caller should switch on this variant: AlreadyAvailable and +/// The caller should switch on this variant: `AlreadyAvailable` and /// Installed both mean the binary can be launched; Failed means we /// gave up and the user needs to install manually (see `manual_instructions`). #[derive(Debug, Clone)] @@ -99,12 +99,13 @@ pub struct InstallTier { /// Snapshot of the host environment used to decide which install tiers are viable. /// -/// Populated by `detect_env()` once per provision_all() call so we +/// Populated by `detect_env()` once per `provision_all()` call so we /// don't re-shell out for every server. `is_linux` / `is_macos` are /// computed at startup (compile time would also work, but keeping the /// shape uniform with the rest of the struct makes the call sites tidy). #[derive(Debug, Clone)] #[allow(dead_code)] +#[allow(clippy::struct_excessive_bools)] pub struct EnvInfo { pub has_rustup: bool, pub has_npm: bool, @@ -126,7 +127,7 @@ pub struct EnvInfo { /// /// Flow: `Command::new("which").arg(binary).output()` → on Unix /// `which` returns exit 0 + stdout path when found, non-zero -/// otherwise. We return the first stdout line as the PathBuf. +/// otherwise. We return the first stdout line as the `PathBuf`. /// /// Returns None if `which` itself is missing, fails to spawn, or the /// binary is not on PATH. We deliberately don't cache this — it's only @@ -151,7 +152,7 @@ pub fn which(binary: &str) -> Option { /// /// Flow: shell out to `which` for each tool in parallel (sequentially, /// actually — the calls are fast and the ordering doesn't matter) -/// → set EnvInfo flags. Linux/macOS are detected via cfg at +/// → set `EnvInfo` flags. Linux/macOS are detected via cfg at /// compile time since `which` won't tell us. /// /// Edge case: `which` may not exist on Windows; we guard with cfg so @@ -185,6 +186,7 @@ pub fn detect_env() -> EnvInfo { /// Why hard-coded rather than loaded from settings: the set is small, /// changes rarely, and bundling it lets the provisioner run before any /// user config has been read (e.g. on first launch). +#[allow(clippy::too_many_lines)] pub fn supported_servers() -> Vec { vec![ LanguageServerDef { @@ -307,7 +309,7 @@ pub fn supported_servers() -> Vec { /// commands tend to emit errors to stderr, and we want to surface /// those. /// -/// Why a custom timeout: std::process::Command has no built-in timeout, +/// Why a custom timeout: `std::process::Command` has no built-in timeout, /// and we'd rather kill a hung `apt` than block the TUI indefinitely. pub fn run_command(cmd: &str, args: &[&str]) -> std::io::Result<(bool, String)> { let mut command = Command::new(cmd); @@ -334,23 +336,19 @@ pub fn run_command(cmd: &str, args: &[&str]) -> std::io::Result<(bool, String)> }) }); - let timeout = Duration::from_secs(180); + let timeout = Duration::from_mins(3); let start = Instant::now(); let status = loop { - match child.try_wait()? { - Some(status) => break Ok(status), - None => { - if start.elapsed() > timeout { - let _ = child.kill(); - let _ = child.wait(); - break Err(std::io::Error::new( - std::io::ErrorKind::TimedOut, - format!("command '{}' timed out after {}s", cmd, timeout.as_secs()), - )); - } - std::thread::sleep(Duration::from_millis(50)); - } + if let Some(status) = child.try_wait()? { break Ok(status) } + if start.elapsed() > timeout { + let _ = child.kill(); + let _ = child.wait(); + break Err(std::io::Error::new( + std::io::ErrorKind::TimedOut, + format!("command '{}' timed out after {}s", cmd, timeout.as_secs()), + )); } + std::thread::sleep(Duration::from_millis(50)); }; let stdout = stdout_thread @@ -362,7 +360,7 @@ pub fn run_command(cmd: &str, args: &[&str]) -> std::io::Result<(bool, String)> match status { Ok(s) if s.success() => Ok((true, stdout)), - Ok(_) => Ok((false, format!("{}{}", stdout, stderr))), + Ok(_) => Ok((false, format!("{stdout}{stderr}"))), Err(e) => Err(e), } } @@ -412,7 +410,7 @@ fn download_url(url: &str, dest: &Path, max_secs: u64) -> Result<(), String> { "-o", &path_str, url, ]; - let (ok, out) = run_command("curl", &args).map_err(|e| format!("curl spawn: {}", e))?; + let (ok, out) = run_command("curl", &args).map_err(|e| format!("curl spawn: {e}"))?; if !ok { return Err(format!("download failed: {}", out.trim())); } @@ -423,7 +421,7 @@ fn download_url(url: &str, dest: &Path, max_secs: u64) -> Result<(), String> { /// `~/.local/share/zesdex/lsp/rust-analyzer/bin/rust-analyzer`. fn install_rust_analyzer_binary(env: &EnvInfo, progress: ProgressFn<'_>) -> Result { let base = lsp_install_dir("rust-analyzer")?; - std::fs::create_dir_all(&base).map_err(|e| format!("mkdir: {}", e))?; + std::fs::create_dir_all(&base).map_err(|e| format!("mkdir: {e}"))?; let url = if env.is_linux { "https://github.com/rust-lang/rust-analyzer/releases/latest/download/rust-analyzer-x86_64-unknown-linux-gnu.gz" @@ -440,7 +438,7 @@ fn install_rust_analyzer_binary(env: &EnvInfo, progress: ProgressFn<'_>) -> Resu download_url(url, &gz, 120)?; if let Some(cb) = progress { cb("Rust: decompressing..."); } let (ok, out) = run_command("gunzip", &["-f", &gz.to_string_lossy()]) - .map_err(|e| format!("gunzip spawn: {}", e))?; + .map_err(|e| format!("gunzip spawn: {e}"))?; if !ok { return Err(format!("gunzip: {}", out.trim())); } @@ -452,7 +450,7 @@ fn install_rust_analyzer_binary(env: &EnvInfo, progress: ProgressFn<'_>) -> Resu { use std::os::unix::fs::PermissionsExt; std::fs::set_permissions(&target, std::fs::Permissions::from_mode(0o755)) - .map_err(|e| format!("chmod: {}", e))?; + .map_err(|e| format!("chmod: {e}"))?; } if let Some(cb) = progress { cb("Rust: installed ✓"); } Ok(target) @@ -462,7 +460,7 @@ fn install_rust_analyzer_binary(env: &EnvInfo, progress: ProgressFn<'_>) -> Resu /// and create a launcher script at `bin/jdtls`. fn install_jdtls_from_eclipse(progress: ProgressFn) -> Result { let base = lsp_install_dir("jdtls")?; - std::fs::create_dir_all(&base).map_err(|e| format!("mkdir: {}", e))?; + std::fs::create_dir_all(&base).map_err(|e| format!("mkdir: {e}"))?; let url = "https://download.eclipse.org/jdtls/snapshots/jdt-language-server-latest.tar.gz"; let tarball = base.join("jdtls.tar.gz"); @@ -473,7 +471,7 @@ fn install_jdtls_from_eclipse(progress: ProgressFn) -> Result { let (ok, out) = run_command("tar", &[ "-xzf", tarball.to_str().unwrap_or(""), "-C", base.to_str().unwrap_or("."), - ]).map_err(|e| format!("tar spawn: {}", e))?; + ]).map_err(|e| format!("tar spawn: {e}"))?; if !ok { return Err(format!("tar: {}", out.trim())); } @@ -484,7 +482,7 @@ fn install_jdtls_from_eclipse(progress: ProgressFn) -> Result { } let bin_dir = base.join("bin"); - std::fs::create_dir_all(&bin_dir).map_err(|e| format!("mkdir bin: {}", e))?; + std::fs::create_dir_all(&bin_dir).map_err(|e| format!("mkdir bin: {e}"))?; let launcher = bin_dir.join("jdtls"); let script = r#"#!/usr/bin/env bash @@ -505,12 +503,12 @@ exec java \ --add-opens java.base/java.lang=ALL-UNNAMED \ "$@" "#; - std::fs::write(&launcher, script).map_err(|e| format!("write launcher: {}", e))?; + std::fs::write(&launcher, script).map_err(|e| format!("write launcher: {e}"))?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; std::fs::set_permissions(&launcher, std::fs::Permissions::from_mode(0o755)) - .map_err(|e| format!("chmod launcher: {}", e))?; + .map_err(|e| format!("chmod launcher: {e}"))?; } if let Some(cb) = progress { cb("Java: JDT-LS installed ✓"); } Ok(launcher) @@ -521,7 +519,7 @@ fn run_download_tier(name: &str, env: &EnvInfo, progress: ProgressFn<'_>) -> Res match name { DOWNLOAD_RUST_BIN => install_rust_analyzer_binary(env, progress), DOWNLOAD_JDTLS => install_jdtls_from_eclipse(progress), - other => Err(format!("unknown download tier '{}'", other)), + other => Err(format!("unknown download tier '{other}'")), } } @@ -556,7 +554,7 @@ fn manual_instructions(def: &LanguageServerDef) -> String { /// Try to provision a single language server. /// /// Flow: check whether any `binary_names` candidate is already on PATH -/// → if yes, return AlreadyAvailable → otherwise walk +/// → if yes, return `AlreadyAvailable` → otherwise walk /// `install_tiers` in order, skipping tiers whose `requires` /// binaries are missing → for each viable tier, run the install /// command (120s timeout) → if it succeeds AND the binary now @@ -641,7 +639,7 @@ fn provision_single_with_progress(def: &LanguageServerDef, env: &EnvInfo, progre } // Normal shell-out tier. - let arg_refs: Vec<&str> = tier.args.iter().map(|s| s.as_str()).collect(); + let arg_refs: Vec<&str> = tier.args.iter().map(std::string::String::as_str).collect(); match run_command(&tier.command, &arg_refs) { Ok((true, _)) => { let located = def @@ -683,9 +681,9 @@ fn provision_single_with_progress(def: &LanguageServerDef, env: &EnvInfo, progre /// Provision every supported server in order, returning one /// `ProvisionResult` per server. /// -/// Flow: detect_env() once → for each server in supported_servers() -/// call provision_single() → collect results. Order matches -/// supported_servers() (rust, typescript, go, java). +/// Flow: `detect_env()` once → for each server in `supported_servers()` +/// call `provision_single()` → collect results. Order matches +/// `supported_servers()` (rust, typescript, go, java). #[allow(dead_code)] pub fn provision_all() -> Vec { let env = detect_env(); @@ -725,7 +723,7 @@ pub fn provision_all_with_progress(progress: ProgressFn) -> Vec let avail: String = flags.iter() .filter(|(_, v)| *v).map(|(k, _)| *k) .collect::>().join(", "); - cb(&format!("LSP: environment ready — {}", avail)); + cb(&format!("LSP: environment ready — {avail}")); } supported_servers() .iter() @@ -736,8 +734,8 @@ pub fn provision_all_with_progress(progress: ProgressFn) -> Vec /// For every successful provision result, attach the corresponding /// server to the given `LspManager`. /// -/// Flow: for each result, if it's AlreadyAvailable or Installed, look -/// up the LanguageServerDef, then call manager.connect() with +/// Flow: for each result, if it's `AlreadyAvailable` or Installed, look +/// up the `LanguageServerDef`, then call `manager.connect()` with /// the binary path and empty args. On connect success, log and /// record the name; on failure, log a warning and skip. /// Returns the names that successfully connected. @@ -745,7 +743,7 @@ pub fn provision_all_with_progress(progress: ProgressFn) -> Vec /// Why empty args: most LSP servers don't need CLI flags to start; /// the spec for each server lives in the protocol handshake, not the /// argv. If we ever need flags (e.g. --stdio), they'll be a per-server -/// constant in supported_servers(). +/// constant in `supported_servers()`. pub fn auto_connect(manager: &Arc>, results: &[ProvisionResult]) -> Vec { let defs = supported_servers(); let mut connected: Vec = Vec::new(); @@ -767,12 +765,9 @@ pub fn auto_connect(manager: &Arc>, results: &[ProvisionResult // Sanity: only connect to servers we know about. Protects against // future ProvisionResult variants sneaking in unknown names. - let def = match defs.iter().find(|d| d.name == name) { - Some(d) => d, - None => { - warn!(name = %name, "skipping connect: unknown server"); - continue; - } + let Some(def) = defs.iter().find(|d| d.name == name) else { + warn!(name = %name, "skipping connect: unknown server"); + continue; }; let mut guard = match manager.lock() { @@ -784,7 +779,7 @@ pub fn auto_connect(manager: &Arc>, results: &[ProvisionResult }; // Build extension slice for connect_with_extensions. - let ext_refs: Vec<&str> = def.extensions.iter().map(|s| s.as_str()).collect(); + let ext_refs: Vec<&str> = def.extensions.iter().map(std::string::String::as_str).collect(); match guard.connect_with_extensions(&name, &binary, &[], &language, &ext_refs) { Ok(()) => { diff --git a/src/app/mcp/manager.rs b/src/app/mcp/manager.rs index 6130914..deae5e9 100644 --- a/src/app/mcp/manager.rs +++ b/src/app/mcp/manager.rs @@ -88,7 +88,8 @@ impl StdioChild { /// /// Return: the `result` value of the matching response, or `Err` on /// timeout, EOF, JSON-RPC error, or I/O failure. - pub fn call(&mut self, method: &str, params: Value) -> anyhow::Result { + pub fn call(&mut self, method: &str, params: &Value) -> anyhow::Result { + const MAX_LINE_LENGTH: usize = 1_048_576; // 1 MiB self.next_id += 1; let id = self.next_id; let req = json!({ @@ -107,18 +108,17 @@ impl StdioChild { + std::time::Duration::from_millis(MCP_CALL_TIMEOUT_MS); loop { if std::time::Instant::now() > deadline { - anyhow::bail!("MCP call timed out after {}ms", MCP_CALL_TIMEOUT_MS); + anyhow::bail!("MCP call timed out after {MCP_CALL_TIMEOUT_MS}ms"); } - response_line.clear(); // Read one byte at a time up to MAX_LINE_LENGTH to prevent // OOM from a malicious server (CWE-400). BufReader already // buffers reads, so byte-by-byte over a buffered reader is // cheap (hits the in-memory buffer). - const MAX_LINE_LENGTH: usize = 1_048_576; // 1 MiB + response_line.clear(); let mut line_truncated = false; loop { let byte = match self.stdout.fill_buf() { - Ok(buf) if buf.is_empty() => { + Ok([]) => { // EOF without newline anyhow::bail!("MCP stdio child process closed unexpectedly"); } @@ -127,7 +127,7 @@ impl StdioChild { self.stdout.consume(1); b } - Err(e) => anyhow::bail!("MCP stdio read error: {}", e), + Err(e) => anyhow::bail!("MCP stdio read error: {e}"), }; if byte == b'\n' { break; @@ -137,7 +137,7 @@ impl StdioChild { // Consume rest of line to keep stream in sync loop { let buf = self.stdout.fill_buf() - .map_err(|e| anyhow::anyhow!("MCP stdio read error: {}", e))?; + .map_err(|e| anyhow::anyhow!("MCP stdio read error: {e}"))?; if buf.is_empty() { anyhow::bail!("MCP stdio child closed mid-line"); } @@ -153,8 +153,7 @@ impl StdioChild { } if line_truncated { anyhow::bail!( - "MCP response line exceeded {} byte limit", - MAX_LINE_LENGTH, + "MCP response line exceeded {MAX_LINE_LENGTH} byte limit", ); } let trimmed = response_line.trim(); @@ -162,10 +161,10 @@ impl StdioChild { continue; } let resp: Value = serde_json::from_str(trimmed) - .map_err(|e| anyhow::anyhow!("invalid JSON from MCP server: {}", e))?; + .map_err(|e| anyhow::anyhow!("invalid JSON from MCP server: {e}"))?; if resp.get("id") == Some(&json!(id)) { if let Some(err) = resp.get("error") { - anyhow::bail!("MCP error: {}", err); + anyhow::bail!("MCP error: {err}"); } return Ok(resp.get("result").cloned().unwrap_or_else(|| { tracing::warn!("[mcp] stdio response missing 'result' field: {}", trimmed); @@ -191,7 +190,7 @@ pub(crate) fn spawn_stdio_child(command: &str, extra_args: &[String]) -> anyhow: cmd.stderr(std::process::Stdio::piped()); let mut child = cmd.spawn() - .map_err(|e| anyhow::anyhow!("failed to spawn MCP stdio server '{}': {}", command, e))?; + .map_err(|e| anyhow::anyhow!("failed to spawn MCP stdio server '{command}': {e}"))?; let stdin = child.stdin.take() .ok_or_else(|| anyhow::anyhow!("failed to get stdin for MCP server"))?; @@ -207,7 +206,7 @@ pub(crate) fn spawn_stdio_child(command: &str, extra_args: &[String]) -> anyhow: let deadline = std::time::Instant::now() + std::time::Duration::from_millis(MCP_CONNECT_TIMEOUT_MS); - let init_result = mcp.call("initialize", json!({ + let init_result = mcp.call("initialize", &json!({ "protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": { @@ -220,9 +219,9 @@ pub(crate) fn spawn_stdio_child(command: &str, extra_args: &[String]) -> anyhow: anyhow::bail!("MCP initialize timed out"); } - init_result.map_err(|e| anyhow::anyhow!("MCP initialize failed: {}", e))?; + init_result.map_err(|e| anyhow::anyhow!("MCP initialize failed: {e}"))?; - let _ = mcp.call("notifications/initialized", json!({})); + let _ = mcp.call("notifications/initialized", &json!({})); Ok(mcp) } @@ -237,23 +236,23 @@ fn call_via_stdio( // Reuse the persistent child handle if available; otherwise spawn a new one. let mut guard; let child: &mut StdioChild = if let Some(mtx) = existing_handle { - guard = mtx.lock().map_err(|e| anyhow::anyhow!("MCP handle lock: {}", e))?; + guard = mtx.lock().map_err(|e| anyhow::anyhow!("MCP handle lock: {e}"))?; &mut guard } else { let mut fresh = spawn_stdio_child(command, extra_args)?; - let result = fresh.call("tools/call", json!({ + let result = fresh.call("tools/call", &json!({ "name": tool_name, "arguments": tool_args }))?; - return extract_text_content(&result); + return Ok(extract_text_content(&result)); }; - let result = child.call("tools/call", json!({ + let result = child.call("tools/call", &json!({ "name": tool_name, "arguments": tool_args }))?; - extract_text_content(&result) + Ok(extract_text_content(&result)) } fn call_via_http(url: &str, tool_name: &str, tool_args: &Value) -> anyhow::Result { @@ -294,7 +293,7 @@ fn call_via_http(url: &str, tool_name: &str, tool_args: &Value) -> anyhow::Resul .header("Content-Type", "application/json") .json(&body) .send() - .map_err(|e| anyhow::anyhow!("MCP HTTP request failed: {}", e))?; + .map_err(|e| anyhow::anyhow!("MCP HTTP request failed: {e}"))?; if !resp.status().is_success() { let status = resp.status(); @@ -302,42 +301,42 @@ fn call_via_http(url: &str, tool_name: &str, tool_args: &Value) -> anyhow::Resul tracing::warn!("[mcp] failed to read HTTP response body: {}", e); String::new() }); - anyhow::bail!("MCP HTTP server returned {}: {}", status, text); + anyhow::bail!("MCP HTTP server returned {status}: {text}"); } let response: Value = resp.json() - .map_err(|e| anyhow::anyhow!("invalid JSON from MCP HTTP server: {}", e))?; + .map_err(|e| anyhow::anyhow!("invalid JSON from MCP HTTP server: {e}"))?; if let Some(err) = response.get("error") { - anyhow::bail!("MCP HTTP error: {}", err); + anyhow::bail!("MCP HTTP error: {err}"); } let result = response.get("result").cloned().unwrap_or_else(|| { tracing::warn!("[mcp] HTTP response missing 'result' field"); Value::Null }); - extract_text_content(&result) + Ok(extract_text_content(&result)) } -fn extract_text_content(result: &Value) -> anyhow::Result { +fn extract_text_content(result: &Value) -> String { if let Some(content) = result.get("content") { if let Some(arr) = content.as_array() { let text: Vec = arr.iter().filter_map(|item| { if item.get("type").and_then(|t| t.as_str()) == Some("text") { - item.get("text").and_then(|t| t.as_str()).map(|s| s.to_string()) + item.get("text").and_then(|t| t.as_str()).map(std::string::ToString::to_string) } else { None } }).collect(); if !text.is_empty() { - return Ok(text.join("\n")); + return text.join("\n"); } } } - Ok(serde_json::to_string_pretty(result).unwrap_or_else(|e| { + serde_json::to_string_pretty(result).unwrap_or_else(|e| { tracing::warn!("[mcp] failed to pretty-print result: {}", e); result.to_string() - })) + }) } /// Registry of connected MCP servers and their tools for the current session. @@ -374,7 +373,7 @@ impl crate::tool::Tool for McpToolAdapter { fn run(&self, _ctx: &crate::tool::ToolCtx, args: &Value) -> anyhow::Result { match &self.transport { McpTransport::Stdio { command, args: extra_args } => { - call_via_stdio(self.child_handle.as_ref().map(|h| h.as_ref()), command, extra_args, &self.tool_name, args) + call_via_stdio(self.child_handle.as_ref().map(std::convert::AsRef::as_ref), command, extra_args, &self.tool_name, args) } McpTransport::StreamableHttp { url } => { call_via_http(url, &self.tool_name, args) @@ -428,7 +427,7 @@ impl McpManager { }; let mut child = spawn_stdio_child(command, extra_args)?; - let result = child.call("tools/list", json!({}))?; + let result = child.call("tools/list", &json!({}))?; let tools = if let Some(tool_list) = result.get("tools").and_then(|v| v.as_array()) { tool_list.iter().filter_map(|t| { diff --git a/src/app/mode/editor.rs b/src/app/mode/editor.rs index 7f646f2..f42c057 100644 --- a/src/app/mode/editor.rs +++ b/src/app/mode/editor.rs @@ -66,7 +66,7 @@ impl EditorState { self.cursor_line += 1; } self.cursor_col = self.cursor_col.min( - self.content.get(self.cursor_line).map(|l| l.len()).unwrap_or(0), + self.content.get(self.cursor_line).map_or(0, std::string::String::len), ); } @@ -112,7 +112,7 @@ impl EditorState { /// Flow: no-op if no editor is open → for each char: `\n`/`\r` inserts a /// line and moves down, `\t` inserts two spaces, everything else inserts /// the char directly → mark state dirty. -pub fn handle_editor_input(state: &mut AppStateRest, text: String) { +pub fn handle_editor_input(state: &mut AppStateRest, text: &str) { let editor = &mut state.misc.editor; if editor.is_none() { return; diff --git a/src/app/mode/effort.rs b/src/app/mode/effort.rs index 14222f8..be0ee84 100644 --- a/src/app/mode/effort.rs +++ b/src/app/mode/effort.rs @@ -1,5 +1,6 @@ +#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)] //! Effort mode: cycles the agent's reasoning effort level, which scales the -//! LLM's temperature and max_tokens for subsequent turns. +//! LLM's temperature and `max_tokens` for subsequent turns. use crate::app::state::rest::AppStateRest; @@ -44,7 +45,7 @@ pub fn cycle_effort(state: &mut AppStateRest) { let label = current_effort_str(state); state.push_toast(crate::app::state::types::Toast::new( crate::app::state::types::ToastKind::Info, - format!("Effort: {}", label), + format!("Effort: {label}"), )); state.dirty = true; } diff --git a/src/app/mode/rewind.rs b/src/app/mode/rewind.rs index ba8aa12..549f2a3 100644 --- a/src/app/mode/rewind.rs +++ b/src/app/mode/rewind.rs @@ -1,23 +1,20 @@ +#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)] //! Rewind mode: restores a file to a pre-edit snapshot stored in the -//! session's SQLite blob store. +//! session's `SQLite` blob store. use crate::app::state::rest::AppStateRest; use sha2::Digest; /// Returns the number of stored pre-edit blobs (snapshots) for this session. pub fn rewind_count(state: &AppStateRest) -> usize { - let conn = match open_session_db(&state.session_dir) { - Ok(c) => c, - Err(_) => return 0, - }; + let Ok(conn) = open_session_db(&state.session_dir) else { return 0 }; crate::model::msglog::blobs::list_blob_keys(&conn, &state.session_id) .ok() - .map(|keys| keys.len()) - .unwrap_or(0) + .map_or(0, |keys| keys.len()) } /// Restores a file to its pre-edit state by retrieving the blob stored under index -/// `index` (0 = oldest). Opens a fresh SQLite connection so this works outside +/// `index` (0 = oldest). Opens a fresh `SQLite` connection so this works outside /// of a running turn (e.g. from the Rewind overlay). pub fn rewind_to(state: &mut AppStateRest, index: usize) { let conn = match open_session_db(&state.session_dir) { @@ -25,7 +22,7 @@ pub fn rewind_to(state: &mut AppStateRest, index: usize) { Err(e) => { state.push_toast(crate::app::state::types::Toast::new( crate::app::state::types::ToastKind::Error, - format!("Failed to open session DB: {}", e), + format!("Failed to open session DB: {e}"), )); state.dirty = true; return; @@ -37,7 +34,7 @@ pub fn rewind_to(state: &mut AppStateRest, index: usize) { Err(e) => { state.push_toast(crate::app::state::types::Toast::new( crate::app::state::types::ToastKind::Error, - format!("Failed to list snapshots: {}", e), + format!("Failed to list snapshots: {e}"), )); state.dirty = true; return; @@ -67,7 +64,7 @@ pub fn rewind_to(state: &mut AppStateRest, index: usize) { Err(e) => { state.push_toast(crate::app::state::types::Toast::new( crate::app::state::types::ToastKind::Error, - format!("Failed to retrieve snapshot: {}", e), + format!("Failed to retrieve snapshot: {e}"), )); state.dirty = true; return; @@ -81,7 +78,7 @@ pub fn rewind_to(state: &mut AppStateRest, index: usize) { .unwrap_or_else(|| state.session_dir.join("snapshot.dat")); match std::fs::write(&restore_path, &bytes) { - Ok(_) => { + Ok(()) => { state.push_toast(crate::app::state::types::Toast::new( crate::app::state::types::ToastKind::Success, format!("Restored {} from snapshot", restore_path.display()), @@ -90,7 +87,7 @@ pub fn rewind_to(state: &mut AppStateRest, index: usize) { Err(e) => { state.push_toast(crate::app::state::types::Toast::new( crate::app::state::types::ToastKind::Error, - format!("Failed to write restored file: {}", e), + format!("Failed to write restored file: {e}"), )); } } @@ -101,7 +98,7 @@ pub fn rewind_to(state: &mut AppStateRest, index: usize) { ts: chrono::Utc::now().timestamp_millis(), tool: "rewind".to_string(), path: restore_path.to_string_lossy().to_string(), - reason: format!("rewind_to({})", index), + reason: format!("rewind_to({index})"), content_sha256: format!("{:x}", sha2::Sha256::digest(&bytes)), bytes_delta: bytes.len() as i64, origin: crate::app::state::types::Origin::Main.tag(), diff --git a/src/app/mode/settings.rs b/src/app/mode/settings.rs index d698257..0e30605 100644 --- a/src/app/mode/settings.rs +++ b/src/app/mode/settings.rs @@ -8,7 +8,7 @@ use crate::model::settings::{Settings, InternetMode}; /// Advance the internet access mode to the next value in the cycle. /// -/// Flow: Off -> ReadOnly -> Full -> Off, wrapping around. +/// Flow: Off -> `ReadOnly` -> Full -> Off, wrapping around. /// /// Why: used by a settings-toggle keybinding to step through modes /// without needing a dropdown/menu. diff --git a/src/app/review/mod.rs b/src/app/review/mod.rs index 5134a8a..ddf26e1 100644 --- a/src/app/review/mod.rs +++ b/src/app/review/mod.rs @@ -1,3 +1,4 @@ +#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)] //! Adaptive quality-review triggering, build/test probing, staleness //! sweeps for stored lessons, and the pending-lesson approval workflow. use std::process::Command; @@ -74,10 +75,7 @@ pub fn should_trigger_review(state: &AppStateRest, origin: Origin) -> bool { if origin != Origin::Main { return false; } - let runtime = match &state.session_runtime { - Some(r) => r, - None => return false, - }; + let Some(runtime) = &state.session_runtime else { return false }; if !state.settings.review_enabled { return false; } @@ -122,8 +120,7 @@ pub fn probe_build_test(workspaces: &[std::path::PathBuf], verify_command: Optio let probe_dir = workspaces.first()?; let cmd = resolve_verify_command(probe_dir, verify_command)?; - let (cmd_prog, cmd_args) = cmd.split_once(' ').map(|(p, a)| (p.to_string(), a.to_string())) - .unwrap_or_else(|| (cmd.clone(), String::new())); + let (cmd_prog, cmd_args) = cmd.split_once(' ').map_or_else(|| (cmd.clone(), String::new()), |(p, a)| (p.to_string(), a.to_string())); let Ok(mut child) = Command::new(&cmd_prog) .args(cmd_args.split_whitespace()) @@ -143,7 +140,7 @@ pub fn probe_build_test(workspaces: &[std::path::PathBuf], verify_command: Optio let output = child.wait_with_output().ok(); let stdout = output.as_ref().map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()).unwrap_or_default(); let stderr = output.as_ref().map(|o| String::from_utf8_lossy(&o.stderr).trim().to_string()).unwrap_or_default(); - let combined = if stderr.is_empty() { stdout } else { format!("{}\n{}", stdout, stderr) }; + let combined = if stderr.is_empty() { stdout } else { format!("{stdout}\n{stderr}") }; return Some(ProbeResult { command: cmd.clone(), passed: status.success(), @@ -201,10 +198,10 @@ fn resolve_verify_command(probe_dir: &std::path::Path, override_cmd: Option<&str let pkg = std::fs::read_to_string(probe_dir.join("package.json")).ok()?; if let Ok(v) = serde_json::from_str::(&pkg) { let scripts = v.get("scripts")?; - if scripts.get("test").and_then(|s| s.as_str()).filter(|s| !s.is_empty()).is_some() { + if scripts.get("test").and_then(|s| s.as_str()).as_ref().is_some_and(|s| !s.is_empty()) { return Some("npm test 2>&1".to_string()); } - if scripts.get("build").and_then(|s| s.as_str()).filter(|s| !s.is_empty()).is_some() { + if scripts.get("build").and_then(|s| s.as_str()).as_ref().is_some_and(|s| !s.is_empty()) { return Some("npm run build 2>&1".to_string()); } } @@ -306,14 +303,15 @@ fn truncate_output(s: &str, max: usize) -> String { /// Return: `Ok(())` once the review has been kicked off; errors only /// propagate from constructing the subagent context, not from the review /// itself (that failure is reported via a `SystemNote` instead). -pub fn trigger_review(state: &mut AppStateRest) -> anyhow::Result<()> { +#[allow(clippy::unnecessary_debug_formatting)] +pub fn trigger_review(state: &mut AppStateRest) { let def = AgentDefinition::new( "quality-reviewer".to_string(), "reviewer".to_string(), ); - let mut ctx = build_subagent_context(def); - ctx.session_dir = state.session_dir.clone(); - ctx.workspaces = state.workspace_roots.clone(); + let mut ctx = build_subagent_context(&def); + ctx.session_dir.clone_from(&state.session_dir); + ctx.workspaces.clone_from(&state.workspace_roots); let probe_result = probe_build_test( &state.workspace_roots, state.settings.verify_command.as_deref(), @@ -333,21 +331,20 @@ pub fn trigger_review(state: &mut AppStateRest) -> anyhow::Result<()> { None => "No build/test probe matched. Confidence: opinion (reasoning-based).".to_string(), }; + let session_dir = &state.session_dir; ctx.system_prompt = format!( "You are a code quality reviewer. Review the recent code changes \ for correctness, and adherence to best practices. \ Use read-only tools (read, grep, glob, recall, remember) to \ inspect the session files and provide a concise review verdict. \ - Session directory: {:?}\n\n\ - Build/Test Probe:\n{}\n\n\ + Session directory: {session_dir:?}\n\n\ + Build/Test Probe:\n{probe_note}\n\n\ When writing a lesson via remember(), set tags appropriately:\n\ - If build/test verification printed any FAILED/ERROR lines, tag\n\ the lesson as \"confidence: verified\" (backed by a real failure).\n\ - If the probe passed or was skipped, tag as \"confidence: opinion\"\n\ (reviewer judgment only).\n\ Check for duplicate lessons via recall before writing a new one.", - state.session_dir, - probe_note, ); // Use a drain thread for subagent events (so blocking_send never @@ -359,17 +356,17 @@ pub fn trigger_review(state: &mut AppStateRest) -> anyhow::Result<()> { let mut rx = rx; while let Some(event) = rx.blocking_recv() { match &event { - SubagentEvent::ToolCall { _tool, .. } => { - tracing::debug!("[review] tool call: {}", _tool); + SubagentEvent::ToolCall { tool, .. } => { + tracing::debug!("[review] tool call: {}", tool); } - SubagentEvent::ToolResult { _tool, .. } => { - tracing::debug!("[review] tool result: {}", _tool); + SubagentEvent::ToolResult { tool, .. } => { + tracing::debug!("[review] tool result: {}", tool); } - SubagentEvent::StepCompleted { _step, .. } => { - tracing::trace!("[review] step {} completed", _step); + SubagentEvent::StepCompleted { .. } => { + tracing::trace!("[review] step completed"); } - SubagentEvent::StepFailed { _step, _error } => { - tracing::warn!("[review] step {} failed: {}", _step, _error); + SubagentEvent::StepFailed { step, error } => { + tracing::warn!("[review] step {} failed: {}", step, error); } SubagentEvent::Completed { .. } => { tracing::debug!("[review] completed"); @@ -380,13 +377,13 @@ pub fn trigger_review(state: &mut AppStateRest) -> anyhow::Result<()> { let turn_events = state.turn_events.clone(); std::thread::spawn(move || { - let result = run_subagent(ctx, tx); + let result = run_subagent(&ctx, &tx); let message = match result { Ok(verdict) => { let first_line = verdict.lines().next().unwrap_or(&verdict); - format!("Quality review: {}", first_line) + format!("Quality review: {first_line}") } - Err(e) => format!("Quality review failed: {}", e), + Err(e) => format!("Quality review failed: {e}"), }; if let Ok(mut q) = turn_events.lock() { q.push_back(TurnEvent::SystemNote { @@ -400,8 +397,6 @@ pub fn trigger_review(state: &mut AppStateRest) -> anyhow::Result<()> { ToastKind::Info, "Quality review triggered".to_string(), )); - - Ok(()) } const STALE_AFTER_DAYS: i64 = 60; diff --git a/src/app/runtime/actions/mod.rs b/src/app/runtime/actions/mod.rs index e2e6b7f..9edba40 100644 --- a/src/app/runtime/actions/mod.rs +++ b/src/app/runtime/actions/mod.rs @@ -6,7 +6,7 @@ //! loop calls `apply_action(&mut state, action)` → for turn-producing //! actions (`SubmitInput`), `spawn_turn` is kicked off on a background OS //! thread which drives `run_agent_turn` (stream to the LLM, gate and -//! execute tool calls via `Harness`, archive messages to SQLite, log edits) +//! execute tool calls via `Harness`, archive messages to `SQLite`, log edits) //! and pushes `TurnEvent`s onto a shared queue → on the next `Tick`, queued //! `TurnEvent`s are drained back into `AppStateRest` (transcript, toasts, //! usage counters). @@ -16,7 +16,10 @@ //! running turns on plain OS threads (rather than blocking the main loop) //! keeps the TUI responsive while the LLM streams. +#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)] + use std::collections::VecDeque; +use std::fmt::Write; use crate::app::harness::Verdict; use sha2::Digest; @@ -30,7 +33,7 @@ use crate::dto::chat::message::{ChatMessage, Role}; /// streaming pipeline, or subagent threads — that mutates `AppStateRest` /// when applied via `apply_action`. /// -/// Step bounds intentionally left unbounded (usize::MAX) so the agent can +/// Step bounds intentionally left unbounded (`usize::MAX`) so the agent can /// continue across as many turns as needed. Each iteration still honours /// `tc.abort_flag` and the per-call LLM timeout, so a runaway loop is /// observable and cancellable from the UI. @@ -99,6 +102,7 @@ pub enum Action { /// need to know how to *produce* actions. /// /// Return: nothing; `state` is mutated in place. +#[allow(clippy::too_many_lines)] pub fn apply_action(state: &mut AppStateRest, action: Action) { match action { Action::ForceQuit => { @@ -168,37 +172,36 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { Ok(abs_path) => { let content = std::fs::read_to_string(&abs_path) .unwrap_or_default(); - let lines: Vec = content.lines().map(|l| l.to_string()).collect(); + let lines: Vec = content.lines().map(std::string::ToString::to_string).collect(); let ed = crate::app::mode::editor::EditorState::open( abs_path.to_string_lossy().to_string(), Some(lines), ); state.misc.editor = Some(ed); state.misc.overlay = Overlay::Editor; - state.push_toast(Toast::new(ToastKind::Info, format!("Editing {}", path))); + state.push_toast(Toast::new(ToastKind::Info, format!("Editing {path}"))); } Err(e) => { - state.push_toast(Toast::new(ToastKind::Error, format!("Failed to open {}: {}", path, e))); + state.push_toast(Toast::new(ToastKind::Error, format!("Failed to open {path}: {e}"))); } } state.dirty = true; } Action::McpAdd { name, command } => { - let extra_args: Vec = command.split_whitespace().map(|s| s.to_string()).collect(); + let extra_args: Vec = command.split_whitespace().map(std::string::ToString::to_string).collect(); let cmd = extra_args.first().cloned().unwrap_or_default(); let args: Vec = extra_args.into_iter().skip(1).collect(); match state.mcp_manager.connect_stdio(&name, &cmd, &args) { - Ok(_) => { + Ok(()) => { let tool_count = state.mcp_manager.servers.last() - .map(|s| s.tools.len()) - .unwrap_or(0); + .map_or(0, |s| s.tools.len()); state.push_toast(Toast::new(ToastKind::Success, - format!("Connected MCP server '{}' ({} tools)", name, tool_count))); + format!("Connected MCP server '{name}' ({tool_count} tools)"))); state.dirty = true; } Err(e) => { state.push_toast(Toast::new(ToastKind::Error, - format!("MCP connect failed: {}", e))); + format!("MCP connect failed: {e}"))); } } } @@ -238,7 +241,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { let result = run_oauth_flow(&provider_clone); let message = match result { Ok(msg) => msg, - Err(e) => format!("OAuth login failed: {}", e), + Err(e) => format!("OAuth login failed: {e}"), }; if let Ok(mut q) = turn_events.lock() { q.push_back(TurnEvent::SystemNote { @@ -247,7 +250,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { }); } }); - let toast = Toast::new(ToastKind::Info, format!("Opening browser for {} login...", provider)); + let toast = Toast::new(ToastKind::Info, format!("Opening browser for {provider} login...")); state.push_toast(toast); state.dirty = true; } @@ -325,13 +328,13 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { let display_path = path.unwrap_or_default(); let display = if tool_name == "read" { let line_count = output.lines().count(); - if !display_path.is_empty() { - format!("read: {} ({} lines)", display_path, line_count) + if display_path.is_empty() { + format!("read: {line_count} line(s)") } else { - format!("read: {} line(s)", line_count) + format!("read: {display_path} ({line_count} lines)") } } else { - format!("{}: {}", tool_name, output) + format!("{tool_name}: {output}") }; state.push_transcript(ChatMessageDisplay::new( Role::Tool, @@ -356,7 +359,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { } } if should_trigger_review(state, Origin::Main) { - let _ = trigger_review(state); + trigger_review(state); } } else if kind == "review" { let counted = if let Some(ref mut rt) = state.session_runtime { @@ -422,7 +425,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { }); state.push_transcript(ChatMessageDisplay::new( crate::dto::chat::message::Role::System, - format!("✓ {}", message), + format!("✓ {message}"), )); if state.misc.overlay == Overlay::Workflow { state.misc.overlay = Overlay::None; @@ -437,7 +440,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { }); state.push_transcript(ChatMessageDisplay::new( crate::dto::chat::message::Role::System, - format!("✗ {}", message), + format!("✗ {message}"), )); if state.misc.overlay == Overlay::Workflow { state.misc.overlay = Overlay::None; @@ -486,7 +489,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { state.push_toast(long_toast); state.push_transcript(ChatMessageDisplay::new( crate::dto::chat::message::Role::System, - format!("Error: {}", msg), + format!("Error: {msg}"), )); turn_finished = true; } @@ -548,7 +551,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { if let Some(ref mut rt) = state.session_runtime { let total_chars: usize = rt.messages.iter() .filter_map(|m| m.content.as_deref()) - .map(|c| c.len()) + .map(str::len) .sum(); let token_estimate = total_chars / 3; rt.messages = crate::app::runtime::shortsend::shape_messages(&rt.messages, token_estimate, max_wire_tokens, true, None); @@ -566,7 +569,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { refresh_lesson_counters(&state.memory_dir, rt); } state.push_toast(Toast::new(ToastKind::Success, - format!("accepted lesson: {}", name))); + format!("accepted lesson: {name}"))); state.dirty = true; } Action::LessonReject { name } => { @@ -579,7 +582,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { refresh_lesson_counters(&state.memory_dir, rt); } state.push_toast(Toast::new(ToastKind::Info, - format!("rejected lesson: {}", name))); + format!("rejected lesson: {name}"))); state.dirty = true; } Action::LessonDelete { name } => { @@ -587,7 +590,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { if let Some(ref mut rt) = state.session_runtime { refresh_lesson_counters(&state.memory_dir, rt); } - state.push_toast(Toast::new(ToastKind::Info, format!("deleted lesson: {}", name))); + state.push_toast(Toast::new(ToastKind::Info, format!("deleted lesson: {name}"))); state.dirty = true; } Action::RunPipeline { mode } => { @@ -606,10 +609,10 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { } "status" => { let current = state.misc.pipeline_override.as_deref().unwrap_or("auto"); - state.push_toast(Toast::new(ToastKind::Info, format!("Pipeline mode: {} (use /pipeline full|quick|skip to change)", current))); + state.push_toast(Toast::new(ToastKind::Info, format!("Pipeline mode: {current} (use /pipeline full|quick|skip to change)"))); } _ => { - state.push_toast(Toast::new(ToastKind::Error, format!("Unknown pipeline mode: {} (use: full, quick, skip)", mode))); + state.push_toast(Toast::new(ToastKind::Error, format!("Unknown pipeline mode: {mode} (use: full, quick, skip)"))); } } state.dirty = true; @@ -644,8 +647,8 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { // "prompt1 | prompt2 | prompt3" → Parallel of 3 agents // "prompt1 -> prompt2" → Pipeline of 2 stages // "prompt" → single Agent - let parts_pipe: Vec<&str> = script.split('|').map(|s| s.trim()).collect(); - let parts_arrow: Vec<&str> = script.split("->").map(|s| s.trim()).collect(); + let parts_pipe: Vec<&str> = script.split('|').map(str::trim).collect(); + let parts_arrow: Vec<&str> = script.split("->").map(str::trim).collect(); let primitive = if parts_pipe.len() > 1 { ScriptPrimitive::Parallel( @@ -680,12 +683,12 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { let args: HashMap = HashMap::new(); let result = crate::app::workflow::engine::run_workflow_tracked( - &wf, &args, Some(live), &session_dir, &workspace_roots, + &wf, &args, Some(&live), &session_dir, &workspace_roots, ); let (kind, message) = match result { Ok(summary) => ("workflow_done".to_string(), summary), - Err(e) => ("workflow_error".to_string(), format!("Workflow failed: {}", e)), + Err(e) => ("workflow_error".to_string(), format!("Workflow failed: {e}")), }; if let Ok(mut q) = turn_events.lock() { @@ -793,7 +796,7 @@ fn spawn_turn(state: &AppStateRest) { abort_flag, pipeline_mode, }; - let result = run_agent_turn(tc, &messages, &events_q); + let result = run_agent_turn(&tc, &messages, &events_q); if let Err(e) = result { if let Ok(mut q) = events_q.lock() { q.push_back(TurnEvent::Error(e.to_string())); @@ -837,7 +840,7 @@ fn generate_workspace_tree(roots: &[std::path::PathBuf]) -> String { let mut out = String::new(); out.push_str("Current Workspace Directory Structure:\n"); for root in roots { - out.push_str(&format!("Root: {}\n", root.display())); + writeln!(out, "Root: {}", root.display()).unwrap(); let walker = ignore::WalkBuilder::new(root) .hidden(true) .git_ignore(true) @@ -847,9 +850,9 @@ fn generate_workspace_tree(roots: &[std::path::PathBuf]) -> String { let path = entry.path(); if let Ok(rel) = path.strip_prefix(root) { if rel.as_os_str().is_empty() { continue; } - let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false); + let is_dir = entry.file_type().is_some_and(|ft| ft.is_dir()); let prefix = if is_dir { "[DIR] " } else { " " }; - out.push_str(&format!(" {}{}\n", prefix, rel.display())); + writeln!(out, " {}{}", prefix, rel.display()).unwrap(); count += 1; if count > 1000 { out.push_str(" ... (truncated)\n"); @@ -881,7 +884,7 @@ fn build_memory_section(memory_dir: &std::path::Path) -> String { } let mut section = String::from("\n\n--- Persistent Memory ---\n"); - section.push_str(&format!("Total entries: {}\n\n", names.len())); + write!(section, "Total entries: {}\n\n", names.len()).unwrap(); for name in &names { if section.len() > 3000 { @@ -892,7 +895,7 @@ fn build_memory_section(memory_dir: &std::path::Path) -> String { if mem.lifecycle == "stale" { continue; } - section.push_str(&format!("## [{}] {}\n{}\n\n", mem.kind, mem.name, mem.content)); + write!(section, "## [{}] {}\n{}\n\n", mem.kind, mem.name, mem.content).unwrap(); } } section.push_str("---"); @@ -940,13 +943,13 @@ fn refresh_lesson_counters(memory_dir: &std::path::Path, rt: &mut crate::app::st } } -/// Persist a `ChatMessage` to the SQLite message log, if a database +/// Persist a `ChatMessage` to the `SQLite` message log, if a database /// connection is available. /// /// Flow: if `db` is `Some`, lock the mutex and call `insert_message`. /// Errors are silently ignored. -fn archive_message(db: &Option>>, session_id: &str, msg: &ChatMessage) { - if let Some(ref arc) = db { +fn archive_message(db: Option<&std::sync::Arc>>, session_id: &str, msg: &ChatMessage) { + if let Some(arc) = db { if let Ok(conn) = arc.lock() { let _ = crate::model::msglog::insert_message(&conn, session_id, msg); } @@ -979,7 +982,7 @@ const MAX_AUTO_REVIEWS_PER_TURN: usize = 2; /// through `Harness::gate_tool_call`) or unwrap the final assistant /// message → check for unfinished todo.md tasks (auto-retry with a /// system message if any remain) → finalise with `Done` and an `edits` -/// SystemNote. +/// `SystemNote`. /// /// On streaming failure: retry once with a non-streaming call → if that /// also fails and there are unfinished tasks, sleep 5s and loop back; @@ -990,11 +993,13 @@ const MAX_AUTO_REVIEWS_PER_TURN: usize = 2; /// /// Return: `Ok(())` on successful completion, or an error from the LLM /// API after retries are exhausted. +#[allow(clippy::too_many_lines)] fn run_agent_turn( - tc: TurnCtx, + tc: &TurnCtx, messages: &[ChatMessage], events_q: &std::sync::Arc>>, ) -> anyhow::Result<()> { + const MAX_TODO_RETRIES: usize = 5; let mut msgs = messages.to_vec(); let mut edits_this_turn = 0u32; let mut edited_paths: Vec = Vec::new(); @@ -1016,7 +1021,7 @@ fn run_agent_turn( ); if !msgs.iter().any(|m| matches!(m.role, crate::dto::chat::message::Role::System)) { let sys = ChatMessage::system(system_text); - archive_message(&tc.db, &tc.session_id, &sys); + archive_message(tc.db.as_ref(), &tc.session_id, &sys); msgs.insert(0, sys); } @@ -1032,24 +1037,21 @@ fn run_agent_turn( .count(); let should_pipeline = if user_msg_count <= 2 { let user_request = msgs.iter() - .rev() - .filter(|m| matches!(m.role, crate::dto::chat::message::Role::User)) - .next() + .rev().find(|m| matches!(m.role, crate::dto::chat::message::Role::User)) .and_then(|m| m.content.as_deref()) .unwrap_or(""); - if !user_request.is_empty() { + if user_request.is_empty() { + false + } else { match tc.pipeline_mode.as_deref() { Some("skip") => { tracing::debug!("[ceo] pipeline skipped via /pipeline skip"); false } - Some("full") => true, - Some("quick") => true, + Some("full" | "quick") => true, _ => crate::app::workflow::company::is_complex_request(user_request), } - } else { - false } } else { false @@ -1057,9 +1059,7 @@ fn run_agent_turn( if should_pipeline { let user_request = msgs.iter() - .rev() - .filter(|m| matches!(m.role, crate::dto::chat::message::Role::User)) - .next() + .rev().find(|m| matches!(m.role, crate::dto::chat::message::Role::User)) .and_then(|m| m.content.as_deref()) .unwrap_or(""); @@ -1102,26 +1102,23 @@ fn run_agent_turn( Ok(summary) => { tracing::info!("[ceo] company pipeline completed successfully"); let pipeline_msg = ChatMessage::system(format!( - "[Company Pipeline: {}]\n{}", - mode_label, - summary, + "[Company Pipeline: {mode_label}]\n{summary}", )); - archive_message(&tc.db, &tc.session_id, &pipeline_msg); + archive_message(tc.db.as_ref(), &tc.session_id, &pipeline_msg); msgs.push(pipeline_msg); if let Ok(mut q) = events_q.lock() { q.push_back(TurnEvent::SystemNote { kind: "pipeline".to_string(), - message: format!("Company pipeline ({}) complete. CEO reviewing results...", mode_label), + message: format!("Company pipeline ({mode_label}) complete. CEO reviewing results..."), }); } } Err(e) => { tracing::warn!("[ceo] company pipeline failed: {}", e); let fail_msg = ChatMessage::system(format!( - "[Pipeline Note] The company pipeline encountered issues: {}.\n\ + "[Pipeline Note] The company pipeline encountered issues: {e}.\n\ Proceeding with direct execution as fallback.", - e, )); msgs.push(fail_msg); } @@ -1132,15 +1129,13 @@ fn run_agent_turn( let mut turn_step = 0usize; let mut todo_retry_count = 0usize; - const MAX_TODO_RETRIES: usize = 5; loop { turn_step += 1; if turn_step > MAX_TURN_STEPS { anyhow::bail!( - "turn exceeded maximum steps ({}) — possible runaway loop. \ + "turn exceeded maximum steps ({MAX_TURN_STEPS}) — possible runaway loop. \ aborting to prevent excessive token usage", - MAX_TURN_STEPS, ); } if turn_start_ms.elapsed().as_millis() as u64 > MAX_TURN_TIMEOUT_MS { @@ -1152,7 +1147,7 @@ fn run_agent_turn( } let total_chars: usize = msgs.iter() .filter_map(|m| m.content.as_deref()) - .map(|c| c.len()) + .map(str::len) .sum(); let token_estimate = total_chars / 4; let max_wire_tokens = tc.context_window; @@ -1168,7 +1163,7 @@ fn run_agent_turn( } // Also update our local `msgs` variable so the rest of the loop operates on the compacted version - msgs = compacted.clone(); + msgs.clone_from(&compacted); compacted } else { prev_shaped = false; @@ -1251,15 +1246,14 @@ fn run_agent_turn( todo_retry_count += 1; if todo_retry_count > MAX_TODO_RETRIES { anyhow::bail!( - "exhausted {} todo-retries — giving up on unfinished tasks. \ + "exhausted {MAX_TODO_RETRIES} todo-retries — giving up on unfinished tasks. \ Edit todo.md manually or ask me to focus on specific items.", - MAX_TODO_RETRIES, ); } if let Ok(mut q) = events_q.lock() { q.push_back(TurnEvent::SystemNote { kind: "task_retry".to_string(), - message: format!("Network/API error: {}. Auto-retrying to finish tasks... (retry {}/{})", api_err, todo_retry_count, MAX_TODO_RETRIES), + message: format!("Network/API error: {api_err}. Auto-retrying to finish tasks... (retry {todo_retry_count}/{MAX_TODO_RETRIES})"), }); } std::thread::sleep(std::time::Duration::from_secs(5)); @@ -1283,7 +1277,7 @@ fn run_agent_turn( let content = response.content.clone().unwrap_or_default(); if has_tool_calls { let tool_calls = response.tool_calls.clone().unwrap_or_default(); - archive_message(&tc.db, &tc.session_id, &response); + archive_message(tc.db.as_ref(), &tc.session_id, &response); msgs.push(response); for tool_call in tool_calls { if tc.abort_flag.load(std::sync::atomic::Ordering::SeqCst) { @@ -1298,7 +1292,7 @@ fn run_agent_turn( ); let ws_roots: Vec<&std::path::Path> = - tc.workspace_roots.iter().map(|p| p.as_path()).collect(); + tc.workspace_roots.iter().map(std::path::PathBuf::as_path).collect(); let verdict = crate::app::harness::Harness::gate_tool_call( &tool_name, &args, @@ -1316,12 +1310,12 @@ fn run_agent_turn( &args, &tc.edit_log_session_dir, &tc.session_id, - &tc.db, + tc.db.as_ref(), ) { Ok(result) => (result, false, is_edit_tool), Err(e) => (e.to_string(), true, false), }, - Verdict::Block(reason) => (format!("Blocked: {}", reason), true, false), + Verdict::Block(reason) => (format!("Blocked: {reason}"), true, false), }; if is_edit { @@ -1332,7 +1326,7 @@ fn run_agent_turn( // background subagent tracking. let edit_path = args.get("path") .and_then(|v| v.as_str()) - .map(|s| s.to_string()); + .map(std::string::ToString::to_string); if let Some(ref p) = edit_path { edited_paths.push(p.clone()); @@ -1353,7 +1347,7 @@ fn run_agent_turn( Ok(verdict) => { let elapsed = review_start.elapsed().as_millis(); let review_msg = ChatMessage::tool_result( - format!("auto-review-{}", inline_reviews_count), + format!("auto-review-{inline_reviews_count}"), format!( "[Auto inline review: {} ({}ms)]\n{}", p, @@ -1361,7 +1355,7 @@ fn run_agent_turn( verdict.trim(), ), ); - archive_message(&tc.db, &tc.session_id, &review_msg); + archive_message(tc.db.as_ref(), &tc.session_id, &review_msg); msgs.push(review_msg); tracing::info!( "[auto-review] inline review for '{}' completed in {}ms: {}", @@ -1381,7 +1375,7 @@ fn run_agent_turn( } - let tool_path = args.get("path").and_then(|v| v.as_str()).map(|s| s.to_string()); + let tool_path = args.get("path").and_then(|v| v.as_str()).map(std::string::ToString::to_string); { if let Ok(mut q) = events_q.lock() { @@ -1396,12 +1390,12 @@ fn run_agent_turn( } let tool_msg = ChatMessage::tool_result(tool_call.id.clone(), output); - archive_message(&tc.db, &tc.session_id, &tool_msg); + archive_message(tc.db.as_ref(), &tc.session_id, &tool_msg); msgs.push(tool_msg); } } else { if !content.is_empty() { - archive_message(&tc.db, &tc.session_id, &response); + archive_message(tc.db.as_ref(), &tc.session_id, &response); if let Ok(mut q) = events_q.lock() { if stream_started { q.push_back(TurnEvent::StreamDone(response.clone())); @@ -1425,15 +1419,15 @@ fn run_agent_turn( if let Ok(mut q) = events_q.lock() { q.push_back(TurnEvent::SystemNote { kind: "task_retry".to_string(), - message: format!("Giving up after {} retries — some todo items remain unfinished. Edit todo.md manually or ask again.", MAX_TODO_RETRIES), + message: format!("Giving up after {MAX_TODO_RETRIES} retries — some todo items remain unfinished. Edit todo.md manually or ask again."), }); } break; } - let sys_text = format!("You stopped, but you still have unfinished tasks in todo.md (marked with '- [ ]'). You MUST continue working and use tools to finish them, or edit todo.md to mark them as done if they are finished. (Retry {}/{})", todo_retry_count, MAX_TODO_RETRIES); + let sys_text = format!("You stopped, but you still have unfinished tasks in todo.md (marked with '- [ ]'). You MUST continue working and use tools to finish them, or edit todo.md to mark them as done if they are finished. (Retry {todo_retry_count}/{MAX_TODO_RETRIES})"); let sys_text_clone = sys_text.clone(); let msg = ChatMessage::system(sys_text); - archive_message(&tc.db, &tc.session_id, &msg); + archive_message(tc.db.as_ref(), &tc.session_id, &msg); msgs.push(msg); if let Ok(mut q) = events_q.lock() { q.push_back(TurnEvent::SystemNote { @@ -1510,13 +1504,13 @@ fn execute_one_tool( args: &serde_json::Value, session_dir: &std::path::Path, session_id: &str, - db: &Option>>, + db: Option<&std::sync::Arc>>, ) -> anyhow::Result { for tool in tools { if tool.name() == name { // Snapshot current file content before write/edit for rewind if (name == "write" || name == "edit") && !tool_call_id.is_empty() { - if let Some(ref arc) = db { + if let Some(arc) = db { if let Ok(conn) = arc.lock() { let path = args.get("path").and_then(|v| v.as_str()).unwrap_or(""); if let Ok(abs_path) = crate::tool::resolve_path(&ctx.workspaces, path) { @@ -1544,13 +1538,12 @@ fn execute_one_tool( let hash = sha2::Sha256::digest( content.and_then(|v| v.as_str()).unwrap_or("").as_bytes(), ); - format!("{:x}", hash) + format!("{hash:x}") }; let bytes_delta = if name == "write" { args.get("content") .and_then(|v| v.as_str()) - .map(|s| s.len() as i64) - .unwrap_or(0) + .map_or(0, |s| s.len() as i64) } else { let old = args.get("old").and_then(|v| v.as_str()).unwrap_or(""); let new = args.get("new").and_then(|v| v.as_str()).unwrap_or(""); @@ -1572,7 +1565,7 @@ fn execute_one_tool( return Ok(result); } } - anyhow::bail!("tool not found: {}", name) + anyhow::bail!("tool not found: {name}") } /// Optionally push a review-available toast at the end of a turn that @@ -1591,14 +1584,13 @@ fn maybe_trigger_review(state: &mut AppStateRest) { let edit_count = state .session_runtime .as_ref() - .map(|rt| rt.edit_count) - .unwrap_or(0); + .map_or(0, |rt| rt.edit_count); if edit_count == 0 { return; } state.push_toast(Toast::new( ToastKind::Info, - format!("{} file(s) modified this session. Review available.", edit_count), + format!("{edit_count} file(s) modified this session. Review available."), )); } @@ -1698,13 +1690,13 @@ fn run_oauth_flow(provider: &str) -> anyhow::Result { let code = server.wait_for_code(120_000, &state_token)?; manager.exchange_code(&code, &redirect_uri, verifier.as_str()) - .map_err(|e| anyhow::anyhow!("{}", e))?; + .map_err(|e| anyhow::anyhow!("{e}"))?; if let Some(ref token) = manager.token { let token_path = dirs::config_dir() .unwrap_or_else(|| std::path::PathBuf::from(".")) .join("zesdex") - .join(format!("oauth_{}.json", provider)); + .join(format!("oauth_{provider}.json")); if let Some(parent) = token_path.parent() { let _ = std::fs::create_dir_all(parent); } @@ -1713,7 +1705,7 @@ fn run_oauth_flow(provider: &str) -> anyhow::Result { } } - Ok(format!("Successfully authenticated with {}.", provider)) + Ok(format!("Successfully authenticated with {provider}.")) } /// Spawn a background thread that checks API reachability via a lightweight HEAD @@ -1722,16 +1714,14 @@ fn run_oauth_flow(provider: &str) -> anyhow::Result { /// /// Flow: resolve the provider's base URL → build a short-lived reqwest client /// with 3s connect / 5s total timeout → HEAD the `/models` endpoint → push -/// a `connectivity` SystemNote with the result. +/// a `connectivity` `SystemNote` with the result. /// /// Why: runs off the event loop so a slow/TIMEOUT network does not block the TUI. fn spawn_api_connectivity_check(state: &AppStateRest) { let base_url = state .app_config .providers - .get(&state.settings.provider) - .map(|p| p.api_base.clone()) - .unwrap_or_else(|| crate::service::provider::DEFAULT_BASE_URL.to_string()); + .get(&state.settings.provider).map_or_else(|| crate::service::provider::DEFAULT_BASE_URL.to_string(), |p| p.api_base.clone()); let turn_events = state.turn_events.clone(); std::thread::spawn(move || { diff --git a/src/app/runtime/commands.rs b/src/app/runtime/commands.rs index 2493e8e..c1c1a24 100644 --- a/src/app/runtime/commands.rs +++ b/src/app/runtime/commands.rs @@ -75,7 +75,7 @@ pub fn apply_command(command: Command) -> Vec { Command::Unknown(cmd) => { vec![Action::SystemNote { kind: "error".to_string(), - message: format!("unknown command: {}", cmd), + message: format!("unknown command: {cmd}"), }] } } diff --git a/src/app/runtime/shortsend.rs b/src/app/runtime/shortsend.rs index eb11c6a..d2bd469 100644 --- a/src/app/runtime/shortsend.rs +++ b/src/app/runtime/shortsend.rs @@ -1,3 +1,4 @@ +#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)] //! Short-send / message shaping: compacts long conversation histories so //! they fit within the provider's context window before being sent to the //! LLM API. @@ -59,10 +60,10 @@ pub fn shape_messages( // Always keep the very first message (System Prompt) which we don't count here // as we just blindly preserve it later. let mut msgs_to_eval = messages.to_vec(); - let first = if !msgs_to_eval.is_empty() { - Some(msgs_to_eval.remove(0)) - } else { + let first = if msgs_to_eval.is_empty() { None + } else { + Some(msgs_to_eval.remove(0)) }; // Iterate backwards from the most recent to oldest @@ -105,7 +106,7 @@ pub fn shape_messages( match llm.chat_with_tools_non_streaming(&req_msgs, None) { Ok(resp) => { if let Some(content) = resp.0.content { - summary_text = format!("[Summary of compacted prior conversation:\n{}\n]", content); + summary_text = format!("[Summary of compacted prior conversation:\n{content}\n]"); } } Err(e) => { diff --git a/src/app/runtime/stream/mod.rs b/src/app/runtime/stream/mod.rs index b91d2e7..25555be 100644 --- a/src/app/runtime/stream/mod.rs +++ b/src/app/runtime/stream/mod.rs @@ -1,3 +1,4 @@ +#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)] //! SSE stream parser: converts SSE- or JSON-chunked LLM responses into //! typed `StreamEvent` variants (tokens, reasoning, tool calls, usage, done). pub mod turn; @@ -88,6 +89,7 @@ impl SseParser { /// provider-specific parsing layer. /// /// Return: 0, 1, or more `StreamEvent`s from the flushed frame. + #[allow(clippy::too_many_lines)] fn flush_event(&mut self) -> Vec { let data = self.data_lines.join("\n"); self.data_lines.clear(); @@ -107,15 +109,15 @@ impl SseParser { }; if let Some(usage) = value.get("usage") { if !usage.is_null() { - let prompt_tokens = usage.get("prompt_tokens").and_then(|v| v.as_u64()).unwrap_or_else(|| { + let prompt_tokens = usage.get("prompt_tokens").and_then(serde_json::Value::as_u64).unwrap_or_else(|| { tracing::warn!("[stream] prompt_tokens missing in usage chunk"); 0 }); - let completion_tokens = usage.get("completion_tokens").and_then(|v| v.as_u64()).unwrap_or_else(|| { + let completion_tokens = usage.get("completion_tokens").and_then(serde_json::Value::as_u64).unwrap_or_else(|| { tracing::warn!("[stream] completion_tokens missing in usage chunk"); 0 }); - let total_tokens = usage.get("total_tokens").and_then(|v| v.as_u64()) + let total_tokens = usage.get("total_tokens").and_then(serde_json::Value::as_u64) .unwrap_or_else(|| { tracing::warn!("[stream] total_tokens missing in usage chunk"); prompt_tokens + completion_tokens @@ -126,12 +128,11 @@ impl SseParser { // in the same chunk; emitting both prevents content loss. let has_other_content = value.get("choices") .and_then(|c| c.as_array()) - .map(|arr| arr.iter().any(|ch| { + .is_some_and(|arr| arr.iter().any(|ch| { ch.get("delta").and_then(|d| d.get("content")).is_some() || ch.get("delta").and_then(|d| d.get("reasoning_content")).is_some() || ch.get("delta").and_then(|d| d.get("tool_calls")).is_some() - })) - .unwrap_or(false); + })); if !has_other_content { return vec![StreamEvent::Usage { prompt_tokens, completion_tokens, total_tokens }]; } @@ -139,21 +140,11 @@ impl SseParser { } match event_type.as_str() { "message.stop" => vec![StreamEvent::Done], - "message.start" => vec![], "message.delta" | "" => { - let delta = match value.get("delta").or_else(|| value.get("choices")) { - Some(d) => d, - None => return vec![], - }; + let Some(delta) = value.get("delta").or_else(|| value.get("choices")) else { return vec![] }; if let Some(choices) = delta.as_array() { - let choice = match choices.first() { - Some(c) => c, - None => return vec![], - }; - let d = match choice.get("delta") { - Some(v) => v, - None => return vec![], - }; + let Some(choice) = choices.first() else { return vec![] }; + let Some(d) = choice.get("delta") else { return vec![] }; // Content token if let Some(content) = d.get("content").and_then(|c| c.as_str()) { @@ -169,15 +160,15 @@ impl SseParser { if let Some(tool_calls) = d.get("tool_calls").and_then(|tc| tc.as_array()) { let mut events = Vec::with_capacity(tool_calls.len()); for tc in tool_calls { - let index = tc.get("index").and_then(|i| i.as_u64()).unwrap_or_else(|| { + let index = tc.get("index").and_then(serde_json::Value::as_u64).unwrap_or_else(|| { tracing::warn!("[stream] tool call delta missing index, defaulting to 0"); 0 }) as usize; - let id = tc.get("id").and_then(|i| i.as_str()).map(|s| s.to_string()); + let id = tc.get("id").and_then(|i| i.as_str()).map(std::string::ToString::to_string); let name = tc.get("function") .and_then(|f| f.get("name")) .and_then(|n| n.as_str()) - .map(|s| s.to_string()); + .map(std::string::ToString::to_string); let args_delta = tc.get("function") .and_then(|f| f.get("arguments")) .and_then(|a| a.as_str()) @@ -253,15 +244,15 @@ pub fn parse_stream_chunk(data: &str) -> Option { } if let Some(tool_calls) = delta.get("tool_calls").and_then(|tc| tc.as_array()) { if let Some(tc) = tool_calls.first() { - let index = tc.get("index").and_then(|i| i.as_u64()).unwrap_or_else(|| { + let index = tc.get("index").and_then(serde_json::Value::as_u64).unwrap_or_else(|| { tracing::warn!("[stream] fallback parser: tool call missing index, defaulting to 0"); 0 }) as usize; - let id = tc.get("id").and_then(|i| i.as_str()).map(|s| s.to_string()); + let id = tc.get("id").and_then(|i| i.as_str()).map(std::string::ToString::to_string); let name = tc.get("function") .and_then(|f| f.get("name")) .and_then(|n| n.as_str()) - .map(|s| s.to_string()); + .map(std::string::ToString::to_string); let args = tc.get("function") .and_then(|f| f.get("arguments")) .and_then(|a| a.as_str()) diff --git a/src/app/runtime/stream/turn.rs b/src/app/runtime/stream/turn.rs index 2863fd2..bc7511e 100644 --- a/src/app/runtime/stream/turn.rs +++ b/src/app/runtime/stream/turn.rs @@ -84,12 +84,12 @@ impl StreamedTurn { let tc = &mut self.tool_calls[*index]; if let Some(new_id) = id { if !new_id.is_empty() { - tc.id = new_id.clone(); + tc.id.clone_from(new_id); } } if let Some(new_name) = name { if !new_name.is_empty() { - tc.name = new_name.clone(); + tc.name.clone_from(new_name); } } tc.arguments.push_str(arguments_delta); diff --git a/src/app/state/misc.rs b/src/app/state/misc.rs index 8097369..37600b8 100644 --- a/src/app/state/misc.rs +++ b/src/app/state/misc.rs @@ -139,7 +139,7 @@ impl InputState { self.autocomplete_candidates = COMMANDS .iter() .filter(|c| c.starts_with(&prefix)) - .map(|c| c.to_string()) + .map(std::string::ToString::to_string) .collect(); self.autocomplete_prefix = prefix; self.autocomplete_idx = 0; @@ -178,10 +178,10 @@ impl InputState { pub fn tab_complete(&mut self) { // Legacy inline tab-complete — used as a fallback when the dropdown // isn't visible yet. Opens the dropdown on the first Tab press. - if !self.autocomplete_visible { - self.open_autocomplete(); - } else { + if self.autocomplete_visible { self.cycle_autocomplete(true); + } else { + self.open_autocomplete(); } } @@ -232,7 +232,7 @@ impl InputState { .open(path) { use std::io::Write; - let _ = writeln!(file, "{}", result); + let _ = writeln!(file, "{result}"); } } } @@ -294,10 +294,11 @@ pub struct MiscState { pub tick_count: u64, pub todo_content: String, /// Pipeline mode override set by `/pipeline` command. - /// - `None`: auto-detect (default) - /// - `Some("full")`: force full pipeline - /// - `Some("quick")`: force quick pipeline - /// - `Some("skip")`: skip pipeline, handle directly + /// - `None`: auto-detect (default) + /// - `Some("full")`: force full pipeline + /// - `Some("quick")`: force quick pipeline + /// - `Some("skip")`: skip pipeline, handle directly + /// /// Consumed on the next agent turn. pub pipeline_override: Option, } diff --git a/src/app/state/rest.rs b/src/app/state/rest.rs index c71e300..02cebfa 100644 --- a/src/app/state/rest.rs +++ b/src/app/state/rest.rs @@ -84,7 +84,7 @@ impl AppStateRest { /// Why: falls back to `memory_dir` itself (with a warning) when it has /// no parent, and to an empty session id when the dir name can't be /// read, so construction never fails. - pub fn new(workspace_roots: Vec, session_dir: PathBuf, memory_dir: PathBuf) -> Self { + pub fn new(workspace_roots: Vec, session_dir: &std::path::Path, memory_dir: PathBuf) -> Self { let settings = Settings::load(); let app_config = AppConfig::load(); let worktrees_dir = memory_dir.parent().unwrap_or_else(|| { @@ -93,27 +93,25 @@ impl AppStateRest { }).join("worktrees"); let dir_cache = DirCache::new(); let session_id = session_dir - .file_name() - .map(|n| n.to_string_lossy().to_string()) - .unwrap_or_else(|| { + .file_name().map_or_else(|| { tracing::warn!("[state] session_dir has no file_name component, using empty session_id"); String::new() - }); + }, |n| n.to_string_lossy().to_string()); let mut state = AppStateRest { settings, app_config, workspace_roots, session_id, - session_dir: session_dir.clone(), + session_dir: session_dir.to_path_buf(), memory_dir, worktrees_dir, turn_events: Arc::new(Mutex::new(VecDeque::new())), turn_in_flight: Arc::new(Mutex::new(false)), abort_flag: Arc::new(std::sync::atomic::AtomicBool::new(false)), dir_cache: Arc::new(RwLock::new(dir_cache)), - edit_log: EditLog::new(&session_dir), - session_runtime: Some(SessionRuntime::new(session_dir.clone())), + edit_log: EditLog::new(session_dir), + session_runtime: Some(SessionRuntime::new(session_dir.to_path_buf())), workflow_engine: WorkflowEngine::new(), mcp_manager: McpManager::new(), lsp_provision_msgs: Arc::new(Mutex::new(VecDeque::new())), @@ -135,9 +133,7 @@ impl AppStateRest { let mut hasher = sha2::Sha256::new(); hasher.update(abs_root.to_string_lossy().as_bytes()); let hash_hex = format!("{:x}", hasher.finalize()); - let folder_name = abs_root.file_name() - .map(|n| n.to_string_lossy().to_string()) - .unwrap_or_else(|| "root".to_string()); + let folder_name = abs_root.file_name().map_or_else(|| "root".to_string(), |n| n.to_string_lossy().to_string()); let history_filename = format!("{}-{}.txt", folder_name, &hash_hex[..8]); let history_dir = base_dir.join("history"); let _ = std::fs::create_dir_all(&history_dir); @@ -146,7 +142,7 @@ impl AppStateRest { if let Ok(content) = std::fs::read_to_string(&history_file) { let history: Vec = content .lines() - .map(|s| s.to_string()) + .map(std::string::ToString::to_string) .filter(|s| !s.is_empty()) .collect(); state.input.history = history; @@ -192,12 +188,12 @@ impl AppStateRest { let connected = provisioner::auto_connect(&lsp_mgr, &results); for name in &connected { tracing::info!("LSP: {} connected", name); - let m = format!("LSP: {} connected ✓", name); push_msg(&msg_queue, &m); + let m = format!("LSP: {name} connected ✓"); push_msg(&msg_queue, &m); } for r in &results { if let ProvisionResult::Failed { language, server_name, reason, .. } = r { tracing::warn!("LSP {} ({}): {}", server_name, language, reason); - let m = format!("LSP: {} ({}) ✗ - {}", server_name, language, reason); push_msg(&msg_queue, &m); + let m = format!("LSP: {server_name} ({language}) ✗ - {reason}"); push_msg(&msg_queue, &m); } } if connected.is_empty() { @@ -216,10 +212,10 @@ impl AppStateRest { /// Return: `false` (and logs a warning) if the mutex is poisoned, rather /// than propagating a panic. pub fn turn_in_flight(&self) -> bool { - self.turn_in_flight.lock().map(|g| *g).unwrap_or_else(|_| { + self.turn_in_flight.lock().map_or_else(|_| { tracing::warn!("[state] turn_in_flight mutex poisoned"); false - }) + }, |g| *g) } /// Shut down every running LSP server process. @@ -259,17 +255,13 @@ impl AppStateRest { /// never fails even on a shallow path. pub fn store_base_dir(&self) -> std::path::PathBuf { self.session_dir.parent() - .and_then(|p| p.parent()) - .map(|p| p.to_path_buf()) - .unwrap_or_else(|| { + .and_then(|p| p.parent()).map_or_else(|| { tracing::warn!("[state] session_dir '{}' has no grandparent, using parent", self.session_dir.display()); - self.session_dir.parent() - .map(|p| p.to_path_buf()) - .unwrap_or_else(|| { + self.session_dir.parent().map_or_else(|| { tracing::warn!("[state] session_dir '{}' has no parent at all, using itself", self.session_dir.display()); self.session_dir.clone() - }) - }) + }, std::path::Path::to_path_buf) + }, std::path::Path::to_path_buf) } /// Build a `ToolCtx` for tool calls originating from the main agent. diff --git a/src/app/state/types.rs b/src/app/state/types.rs index 6dd1c60..0d1573e 100644 --- a/src/app/state/types.rs +++ b/src/app/state/types.rs @@ -1,3 +1,4 @@ +#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)] //! Shared small state types: toasts, overlays, the transcript cache, //! tool execution model, and call origin tags. @@ -109,7 +110,7 @@ pub enum Origin { impl Origin { /// Short string tag for this origin, used in filenames and logs. - pub fn tag(&self) -> String { + pub fn tag(self) -> String { match self { Origin::Main => "main".to_string(), Origin::SubAgent => "subagent".to_string(), diff --git a/src/app/subagent/auto.rs b/src/app/subagent/auto.rs index c29edda..42899f4 100644 --- a/src/app/subagent/auto.rs +++ b/src/app/subagent/auto.rs @@ -72,12 +72,18 @@ fn is_production_code(path: &str) -> bool { if lower.contains("test") || lower.contains("spec") || lower.contains("_test.") { return false; } - // Only source files - lower.ends_with(".rs") || lower.ends_with(".ts") || lower.ends_with(".tsx") - || lower.ends_with(".js") || lower.ends_with(".jsx") || lower.ends_with(".go") - || lower.ends_with(".py") || lower.ends_with(".java") || lower.ends_with(".kt") - || lower.ends_with(".swift") || lower.ends_with(".c") || lower.ends_with(".cpp") - || lower.ends_with(".h") || lower.ends_with(".hpp") + // Only source files — use Path::extension() to avoid clippy + // case_sensitive_file_extension_comparisons lint + std::path::Path::new(&lower) + .extension() + .and_then(|ext| ext.to_str()) + .is_some_and(|ext| { + matches!( + ext, + "rs" | "ts" | "tsx" | "js" | "jsx" | "go" | "py" | "java" | "kt" | "swift" + | "c" | "cpp" | "h" | "hpp" + ) + }) } /// ─── Inline Quick Review (synchronous, feeds back to LLM) ─── @@ -111,7 +117,7 @@ pub fn spawn_quick_review( .with_system_prompt(prompt) .with_max_steps(QUICK_REVIEW_MAX_STEPS); - let mut ctx = build_subagent_context(def); + let mut ctx = build_subagent_context(&def); ctx.session_dir = session_dir.to_path_buf(); ctx.workspaces = workspaces.to_vec(); @@ -119,11 +125,11 @@ pub fn spawn_quick_review( let _drain = std::thread::spawn(move || { while let Some(event) = rx.blocking_recv() { match &event { - SubagentEvent::ToolCall { _tool, .. } => { - tracing::debug!("[auto-review] tool call: {}", _tool); + SubagentEvent::ToolCall { tool, .. } => { + tracing::debug!("[auto-review] tool call: {}", tool); } - SubagentEvent::ToolResult { _tool, .. } => { - tracing::debug!("[auto-review] tool result: {}", _tool); + SubagentEvent::ToolResult { tool, .. } => { + tracing::debug!("[auto-review] tool result: {}", tool); } SubagentEvent::Completed { .. } => { tracing::debug!("[auto-review] completed"); @@ -133,7 +139,7 @@ pub fn spawn_quick_review( } }); - let verdict = run_subagent(ctx, tx)?; + let verdict = run_subagent(&ctx, &tx)?; tracing::info!( "[auto-review] quick review for '{}': {}", file_path, @@ -142,7 +148,7 @@ pub fn spawn_quick_review( Ok(verdict) } -/// ─── Background Subagent Spawners (async, report via SystemNote) ─── +/// ─── Background Subagent Spawners (async, report via `SystemNote`) ─── /// /// Spawn a background subagent that generates tests for modified files. /// @@ -185,7 +191,7 @@ pub fn spawn_background_test_gen( .with_system_prompt(prompt) .with_max_steps(BG_SUBAGENT_MAX_STEPS); - let mut ctx = build_subagent_context(def); + let mut ctx = build_subagent_context(&def); ctx.session_dir = sd; ctx.workspaces = ws; @@ -193,17 +199,17 @@ pub fn spawn_background_test_gen( let _drain = std::thread::spawn(move || { while let Some(event) = rx.blocking_recv() { match &event { - SubagentEvent::ToolCall { _tool, .. } => { - tracing::debug!("[bg-test-gen] tool: {}", _tool); + SubagentEvent::ToolCall { tool, .. } => { + tracing::debug!("[bg-test-gen] tool: {}", tool); } - SubagentEvent::ToolResult { _tool, .. } => { - tracing::debug!("[bg-test-gen] result: {}", _tool); + SubagentEvent::ToolResult { tool, .. } => { + tracing::debug!("[bg-test-gen] result: {}", tool); } - SubagentEvent::StepCompleted { _step, .. } => { - tracing::trace!("[bg-test-gen] step {} done", _step); + SubagentEvent::StepCompleted { .. } => { + tracing::trace!("[bg-test-gen] step done"); } - SubagentEvent::StepFailed { _step, _error } => { - tracing::warn!("[bg-test-gen] step {} failed: {}", _step, _error); + SubagentEvent::StepFailed { step, error } => { + tracing::warn!("[bg-test-gen] step {} failed: {}", step, error); } SubagentEvent::Completed { .. } => { tracing::debug!("[bg-test-gen] completed"); @@ -212,13 +218,13 @@ pub fn spawn_background_test_gen( } }); - let result = run_subagent(ctx, tx); + let result = run_subagent(&ctx, &tx); let message = match &result { Ok(output) => { let first = output.lines().next().unwrap_or(output); - format!("Auto test-gen: {}", first) + format!("Auto test-gen: {first}") } - Err(e) => format!("Auto test-gen failed: {}", e), + Err(e) => format!("Auto test-gen failed: {e}"), }; if let Ok(mut q) = events.lock() { @@ -265,7 +271,7 @@ pub fn spawn_background_arch_review( .with_system_prompt(prompt) .with_max_steps(BG_SUBAGENT_MAX_STEPS); - let mut ctx = build_subagent_context(def); + let mut ctx = build_subagent_context(&def); ctx.session_dir = sd; ctx.workspaces = ws; @@ -273,11 +279,11 @@ pub fn spawn_background_arch_review( let _drain = std::thread::spawn(move || { while let Some(event) = rx.blocking_recv() { match &event { - SubagentEvent::ToolCall { _tool, .. } => { - tracing::debug!("[bg-arch] tool: {}", _tool); + SubagentEvent::ToolCall { tool, .. } => { + tracing::debug!("[bg-arch] tool: {}", tool); } - SubagentEvent::ToolResult { _tool, .. } => { - tracing::debug!("[bg-arch] result: {}", _tool); + SubagentEvent::ToolResult { tool, .. } => { + tracing::debug!("[bg-arch] result: {}", tool); } SubagentEvent::Completed { .. } => { tracing::debug!("[bg-arch] completed"); @@ -287,13 +293,13 @@ pub fn spawn_background_arch_review( } }); - let result = run_subagent(ctx, tx); + let result = run_subagent(&ctx, &tx); let message = match &result { Ok(output) => { let first = output.lines().next().unwrap_or(output); - format!("Architecture review: {}", first) + format!("Architecture review: {first}") } - Err(e) => format!("Architecture review failed: {}", e), + Err(e) => format!("Architecture review failed: {e}"), }; if let Ok(mut q) = events.lock() { @@ -351,7 +357,7 @@ pub fn spawn_background_security_review( .with_system_prompt(prompt) .with_max_steps(BG_SUBAGENT_MAX_STEPS); - let mut ctx = build_subagent_context(def); + let mut ctx = build_subagent_context(&def); ctx.session_dir = sd; ctx.workspaces = ws; @@ -359,11 +365,11 @@ pub fn spawn_background_security_review( let _drain = std::thread::spawn(move || { while let Some(event) = rx.blocking_recv() { match &event { - SubagentEvent::ToolCall { _tool, .. } => { - tracing::debug!("[bg-security] tool: {}", _tool); + SubagentEvent::ToolCall { tool, .. } => { + tracing::debug!("[bg-security] tool: {}", tool); } - SubagentEvent::ToolResult { _tool, .. } => { - tracing::debug!("[bg-security] result: {}", _tool); + SubagentEvent::ToolResult { tool, .. } => { + tracing::debug!("[bg-security] result: {}", tool); } SubagentEvent::Completed { .. } => { tracing::debug!("[bg-security] completed"); @@ -373,13 +379,13 @@ pub fn spawn_background_security_review( } }); - let result = run_subagent(ctx, tx); + let result = run_subagent(&ctx, &tx); let message = match &result { Ok(output) => { let first = output.lines().next().unwrap_or(output); - format!("Security review: {}", first) + format!("Security review: {first}") } - Err(e) => format!("Security review failed: {}", e), + Err(e) => format!("Security review failed: {e}"), }; if let Ok(mut q) = events.lock() { diff --git a/src/app/subagent/context.rs b/src/app/subagent/context.rs index e3acc00..87f021b 100644 --- a/src/app/subagent/context.rs +++ b/src/app/subagent/context.rs @@ -37,10 +37,10 @@ pub struct SubagentContext { /// /// Return: a context with empty `system_prompt`, empty `workspaces`, /// empty `session_dir`, resolved `max_steps`, and the resolved allowed-tool list. -pub fn build_subagent_context(def: AgentDefinition) -> SubagentContext { +pub fn build_subagent_context(def: &AgentDefinition) -> SubagentContext { let allowed_tools = def.allowed_tools.clone().unwrap_or_else(|| { if def.role == "reviewer" { - REVIEWER_ALLOWED.iter().map(|s| s.to_string()).collect() + REVIEWER_ALLOWED.iter().map(std::string::ToString::to_string).collect() } else { Vec::new() } diff --git a/src/app/subagent/division.rs b/src/app/subagent/division.rs index 46d3d6a..9b4a0be 100644 --- a/src/app/subagent/division.rs +++ b/src/app/subagent/division.rs @@ -16,7 +16,7 @@ use crate::app::subagent::spawn::AgentDefinition; -/// Division roles — used as both the `role` field in AgentDefinition +/// Division roles — used as both the `role` field in `AgentDefinition` /// and as the key for pipeline routing. pub mod roles { /// Strategy Division: plans architecture, creates diagrams, breaks down work. diff --git a/src/app/subagent/engine.rs b/src/app/subagent/engine.rs index e8e6a17..d6b37a3 100644 --- a/src/app/subagent/engine.rs +++ b/src/app/subagent/engine.rs @@ -7,6 +7,7 @@ //! bash exfiltration and destructive-pattern detection) so that subagents //! are not a weaker link than the main agent. +use std::fmt::Write; use tokio::sync::mpsc; use crate::dto::chat::message::ChatMessage; use crate::dto::provider::request::ToolDef; @@ -143,8 +144,7 @@ fn gate_subagent_tool_call( let reason = args.get("reason").and_then(|v| v.as_str()).unwrap_or(""); if reason.trim().len() < MIN_REASON_LEN { return Some(format!( - "{} requires a non-trivial 'reason' (>= {} chars) explaining why", - tool_name, MIN_REASON_LEN, + "{tool_name} requires a non-trivial 'reason' (>= {MIN_REASON_LEN} chars) explaining why", )); } } @@ -157,9 +157,7 @@ fn gate_subagent_tool_call( let old = args.get("old").and_then(|v| v.as_str()).unwrap_or(""); let new = args.get("new").and_then(|v| v.as_str()).unwrap_or(""); // For edits, scanning old+new together catches stubs in both - return if contains_any(old, STUB_PATTERNS) { - Some("content contains stub/placeholder pattern; production code must be fully implemented".to_string()) - } else if contains_any(new, STUB_PATTERNS) { + return if contains_any(old, STUB_PATTERNS) || contains_any(new, STUB_PATTERNS) { Some("content contains stub/placeholder pattern; production code must be fully implemented".to_string()) } else if contains_any(new, DENIAL_PATTERNS) { Some("content contains denial/punt pattern; implement properly instead of skipping".to_string()) @@ -202,13 +200,13 @@ fn gate_subagent_tool_call( if !is_standard { for pat in EXFIL_PATTERNS { if cmd.contains(pat) { - return Some(format!("potential data-exfiltration command blocked (matched '{}')", pat)); + return Some(format!("potential data-exfiltration command blocked (matched '{pat}')")); } } } for pat in SENSITIVE_PATH_PATTERNS { if cmd.contains(pat) { - return Some(format!("refused to read/write sensitive path '{}'", pat)); + return Some(format!("refused to read/write sensitive path '{pat}'")); } } let dangerous = ["rm -rf /", "rm -rf --no-preserve-root", "rm -rf ~", @@ -216,7 +214,7 @@ fn gate_subagent_tool_call( "chmod -R 000 /", "shutdown ", "poweroff ", "reboot ", "halt "]; for pat in &dangerous { if cmd.contains(pat) { - return Some(format!("destructive command pattern blocked: {}", pat)); + return Some(format!("destructive command pattern blocked: {pat}")); } } if contains_any(cmd, STUB_PATTERNS) { @@ -251,7 +249,7 @@ fn generate_workspace_tree(roots: &[std::path::PathBuf]) -> String { let mut out = String::new(); out.push_str("Current Workspace Directory Structure:\n"); for root in roots { - out.push_str(&format!("Root: {}\n", root.display())); + writeln!(out, "Root: {}", root.display()).unwrap(); let walker = ignore::WalkBuilder::new(root) .hidden(true) .git_ignore(true) @@ -261,9 +259,9 @@ fn generate_workspace_tree(roots: &[std::path::PathBuf]) -> String { let path = entry.path(); if let Ok(rel) = path.strip_prefix(root) { if rel.as_os_str().is_empty() { continue; } - let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false); + let is_dir = entry.file_type().is_some_and(|ft| ft.is_dir()); let prefix = if is_dir { "[DIR] " } else { " " }; - out.push_str(&format!(" {}{}\n", prefix, rel.display())); + writeln!(out, " {}{}", prefix, rel.display()).unwrap(); count += 1; if count > 1000 { out.push_str(" ... (truncated)\n"); @@ -291,7 +289,8 @@ fn generate_workspace_tree(roots: &[std::path::PathBuf]) -> String { /// /// Return: the concatenated text output, or an `anyhow::Error` if the LLM /// call fails at any step. -pub fn run_subagent(ctx: SubagentContext, tx: mpsc::Sender) -> anyhow::Result { +#[allow(clippy::too_many_lines)] +pub fn run_subagent(ctx: &SubagentContext, tx: &mpsc::Sender) -> anyhow::Result { let mut output = String::new(); let mut messages: Vec = Vec::new(); @@ -328,10 +327,10 @@ pub fn run_subagent(ctx: SubagentContext, tx: mpsc::Sender) -> an // be cancelled from the parent (mirrors main agent behaviour). if ctx.abort_flag.as_ref().is_some_and(|f| f.load(std::sync::atomic::Ordering::SeqCst)) { let _ = tx.blocking_send(SubagentEvent::StepFailed { - _step: step, - _error: "subagent aborted by parent".to_string(), + step, + error: "subagent aborted by parent".to_string(), }); - anyhow::bail!("subagent aborted by parent at step {}", step); + anyhow::bail!("subagent aborted by parent at step {step}"); } // Use the structured tool-calling API so the LLM can request tools with @@ -340,10 +339,10 @@ pub fn run_subagent(ctx: SubagentContext, tx: mpsc::Sender) -> an Ok(result) => result, Err(e) => { let _ = tx.blocking_send(SubagentEvent::StepFailed { - _step: step, - _error: e.to_string(), + step, + error: e.to_string(), }); - anyhow::bail!("subagent call failed at step {}: {}", step, e); + anyhow::bail!("subagent call failed at step {step}: {e}"); } }; @@ -361,10 +360,10 @@ pub fn run_subagent(ctx: SubagentContext, tx: mpsc::Sender) -> an // Check abort flag before each tool execution if ctx.abort_flag.as_ref().is_some_and(|f| f.load(std::sync::atomic::Ordering::SeqCst)) { let _ = tx.blocking_send(SubagentEvent::StepFailed { - _step: step, - _error: "subagent aborted by parent during tool execution".to_string(), + step, + error: "subagent aborted by parent during tool execution".to_string(), }); - anyhow::bail!("subagent aborted by parent during tool call at step {}", step); + anyhow::bail!("subagent aborted by parent during tool call at step {step}"); } let tool_name = &tool_call.function.name; @@ -373,28 +372,28 @@ pub fn run_subagent(ctx: SubagentContext, tx: mpsc::Sender) -> an let generally_allowed = ctx.allowed_tools.is_empty() || explicitly_allowed; let _ = tx.blocking_send(SubagentEvent::ToolCall { - _tool: tool_name.clone(), - _args: args.clone(), + tool: tool_name.clone(), + args: args.clone(), }); // Level 1: allowlist check — is this tool even permitted? if !generally_allowed { - let msg = format!("tool '{}' not allowed for this subagent", tool_name); + let msg = format!("tool '{tool_name}' not allowed for this subagent"); messages.push(ChatMessage::tool_result(tool_call.id.clone(), msg.clone())); let _ = tx.blocking_send(SubagentEvent::ToolResult { - _tool: tool_name.clone(), - _output: msg, + tool: tool_name.clone(), + output: msg, }); continue; } // Level 2: risky tool check — risky tools require explicit permission if tool_is_risky(tool_name) && !explicitly_allowed { - let msg = format!("risky tool '{}' requires explicit permission; not allowed for this subagent", tool_name); + let msg = format!("risky tool '{tool_name}' requires explicit permission; not allowed for this subagent"); messages.push(ChatMessage::tool_result(tool_call.id.clone(), msg.clone())); let _ = tx.blocking_send(SubagentEvent::ToolResult { - _tool: tool_name.clone(), - _output: msg, + tool: tool_name.clone(), + output: msg, }); continue; } @@ -404,34 +403,34 @@ pub fn run_subagent(ctx: SubagentContext, tx: mpsc::Sender) -> an // stub/denial/assumption scanning, bash exfiltration, destructive // commands, sensitive path reads). if let Some(block_reason) = gate_subagent_tool_call(tool_name, &args) { - let msg = format!("Blocked by subagent gate: {}", block_reason); + let msg = format!("Blocked by subagent gate: {block_reason}"); messages.push(ChatMessage::tool_result(tool_call.id.clone(), msg.clone())); let _ = tx.blocking_send(SubagentEvent::ToolResult { - _tool: tool_name.clone(), - _output: msg, + tool: tool_name.clone(), + output: msg, }); continue; } let result = match tools.iter().find(|t| t.name() == tool_name.as_str()) { Some(tool) => tool.run(&tool_ctx, &args), - None => Err(anyhow::anyhow!("tool '{}' not found", tool_name)), + None => Err(anyhow::anyhow!("tool '{tool_name}' not found")), }; match result { Ok(output_text) => { messages.push(ChatMessage::tool_result(tool_call.id.clone(), output_text.clone())); let _ = tx.blocking_send(SubagentEvent::ToolResult { - _tool: tool_name.clone(), - _output: output_text, + tool: tool_name.clone(), + output: output_text, }); } Err(e) => { - let msg = format!("tool '{}' failed: {}", tool_name, e); + let msg = format!("tool '{tool_name}' failed: {e}"); messages.push(ChatMessage::tool_result(tool_call.id.clone(), msg.clone())); let _ = tx.blocking_send(SubagentEvent::ToolResult { - _tool: tool_name.clone(), - _output: msg, + tool: tool_name.clone(), + output: msg, }); } } @@ -443,8 +442,8 @@ pub fn run_subagent(ctx: SubagentContext, tx: mpsc::Sender) -> an output.push('\n'); } let _ = tx.blocking_send(SubagentEvent::StepCompleted { - _step: step, - _output: content.clone(), + step, + output: content.clone(), }); // Break only when we got real content; empty means something went wrong if !content.is_empty() { @@ -453,6 +452,6 @@ pub fn run_subagent(ctx: SubagentContext, tx: mpsc::Sender) -> an } } - let _ = tx.blocking_send(SubagentEvent::Completed { _output: output.clone() }); + let _ = tx.blocking_send(SubagentEvent::Completed { output: output.clone() }); Ok(output) } diff --git a/src/app/subagent/event.rs b/src/app/subagent/event.rs index cd57aa6..e3f7f87 100644 --- a/src/app/subagent/event.rs +++ b/src/app/subagent/event.rs @@ -8,22 +8,27 @@ use serde_json::Value; #[derive(Debug, Clone)] pub enum SubagentEvent { StepCompleted { - _step: usize, - _output: String, + #[allow(dead_code)] + step: usize, + #[allow(dead_code)] + output: String, }, StepFailed { - _step: usize, - _error: String, + step: usize, + error: String, }, Completed { - _output: String, + #[allow(dead_code)] + output: String, }, ToolCall { - _tool: String, - _args: Value, + tool: String, + #[allow(dead_code)] + args: Value, }, ToolResult { - _tool: String, - _output: String, + tool: String, + #[allow(dead_code)] + output: String, }, } diff --git a/src/app/subagent/spawn.rs b/src/app/subagent/spawn.rs index d97f92c..d623be7 100644 --- a/src/app/subagent/spawn.rs +++ b/src/app/subagent/spawn.rs @@ -1,4 +1,4 @@ -//! AgentDefinition -- declarative specification for instantiating a +//! `AgentDefinition` -- declarative specification for instantiating a //! subagent from workflow scripts or programmatic calls. use serde::{Deserialize, Serialize}; diff --git a/src/app/workflow/company.rs b/src/app/workflow/company.rs index 61f79f6..3daf1e5 100644 --- a/src/app/workflow/company.rs +++ b/src/app/workflow/company.rs @@ -21,6 +21,7 @@ //! ``` use std::collections::HashMap; +use std::fmt::Write; use std::sync::{Arc, Mutex}; use crate::app::workflow::engine::{execute_primitive, LiveStateFn, AgentStatus}; use crate::app::workflow::script::{ScriptPrimitive, ScriptOptions, WorkflowScript}; @@ -64,9 +65,7 @@ pub fn run_company_pipeline( let wf = WorkflowScript { name: "company-pipeline".to_string(), - description: format!( - "Company Pipeline (full): Strategy → Engineering → Quality → Security → Documentation", - ), + description: "Company Pipeline (full): Strategy → Engineering → Quality → Security → Documentation".to_string(), script: ScriptPrimitive::Pipeline(pipeline_scripts), options: ScriptOptions { max_concurrency: 1, // sequential by design @@ -197,21 +196,19 @@ fn build_executive_summary( divisions: &[division::Division], ) -> String { let mut summary = String::new(); - summary.push_str(&format!("Pipeline for: {}\n", request)); + writeln!(summary, "Pipeline for: {request}").unwrap(); for (i, div) in divisions.iter().enumerate() { - let verdict = results.get(i) - .map(|r| { + let verdict = results.get(i).map_or_else(|| "—".to_string(), |r| { r.lines().next().unwrap_or(r) .chars().take(100).collect::() - }) - .unwrap_or_else(|| "—".to_string()); + }); - summary.push_str(&format!(" {}: {}\n", div.name, verdict)); + writeln!(summary, " {}: {}", div.name, verdict).unwrap(); } if !findings.is_empty() { - summary.push_str(&format!(" Notes: {} cross-division finding(s)\n", findings.len())); + writeln!(summary, " Notes: {} cross-division finding(s)", findings.len()).unwrap(); } summary @@ -223,7 +220,7 @@ fn build_executive_summary( /// Simple = single file, minor fix, quick lookup, config change. /// Complex = new feature, multi-file refactor, architecture change. /// -/// Used by the auto-CEO pipeline trigger in run_agent_turn to decide +/// Used by the auto-CEO pipeline trigger in `run_agent_turn` to decide /// whether to delegate to the full company pipeline or handle directly. /// /// Heuristics: @@ -249,7 +246,7 @@ pub fn is_complex_request(request: &str) -> bool { return false; } // Multi-line/multi-sentence → likely complex - let sentences = trimmed.split(|c| c == '.' || c == '!' || c == '?') + let sentences = trimmed.split(['.', '!', '?']) .filter(|s| !s.trim().is_empty()) .count(); if sentences >= 3 { diff --git a/src/app/workflow/engine.rs b/src/app/workflow/engine.rs index 3b2fe1f..903ed7f 100644 --- a/src/app/workflow/engine.rs +++ b/src/app/workflow/engine.rs @@ -83,7 +83,7 @@ pub type LiveStateFn = Arc; /// any findings from earlier sibling agents. Updates live state before and /// after to reflect Running → Completed/Failed transitions. /// -/// Flow: push agent as `Running` → build SubagentContext with prompt + +/// Flow: push agent as `Running` → build `SubagentContext` with prompt + /// findings preamble, linking the `workflow_findings` Arc so the subagent's /// `note_finding` tool pushes into the same vec → call `run_subagent` /// (draining the event channel into a consumer so events are not blocked) @@ -98,11 +98,12 @@ pub type LiveStateFn = Arc; /// a stuck stage from blocking the entire pipeline forever. /// /// Return: the agent's text output, or an error on failure. +#[allow(clippy::too_many_lines, clippy::too_many_arguments)] fn spawn_single_agent( agent_id: &str, agent_name: &str, prompt: &str, - findings_snapshot: Vec, + findings_snapshot: &[String], findings: &Arc>>, live: Option<&LiveStateFn>, session_dir: &std::path::Path, @@ -134,7 +135,7 @@ fn spawn_single_agent( let def = AgentDefinition::new(agent_name.to_string(), "coder".to_string()) .with_max_steps(50); - let mut ctx = build_subagent_context(def); + let mut ctx = build_subagent_context(&def); ctx.session_dir = session_dir.to_path_buf(); ctx.workspaces = workspaces.to_vec(); @@ -152,7 +153,7 @@ fn spawn_single_agent( ) }; - ctx.system_prompt = format!("{}{}", prompt, findings_section); + ctx.system_prompt = format!("{prompt}{findings_section}"); // Link the shared findings Arc so note_finding calls within this // subagent write into the same vec visible to sibling agents. ctx.workflow_findings = Some(findings.clone()); @@ -174,8 +175,8 @@ fn spawn_single_agent( let mut rx = rx; while let Some(event) = rx.blocking_recv() { match &event { - SubagentEvent::ToolCall { _tool, _args } => { - tracing::debug!("[subagent] tool call: {}", _tool); + SubagentEvent::ToolCall { tool, .. } => { + tracing::debug!("[subagent] tool call: {}", tool); // Push intra-division progress: which tool is running if let Some(ref f) = drain_live { f( @@ -186,13 +187,13 @@ fn spawn_single_agent( started_at: Some(drain_started_at), completed_at: None, error: None, - progress: Some(format!("tool: {}", _tool)), + progress: Some(format!("tool: {tool}")), }, ); } } - SubagentEvent::ToolResult { _tool, .. } => { - tracing::debug!("[subagent] tool result: {}", _tool); + SubagentEvent::ToolResult { tool, .. } => { + tracing::debug!("[subagent] tool result: {}", tool); if let Some(ref f) = drain_live { f( drain_agent_id.clone(), @@ -202,16 +203,16 @@ fn spawn_single_agent( started_at: Some(drain_started_at), completed_at: None, error: None, - progress: Some(format!("done: {}", _tool)), + progress: Some(format!("done: {tool}")), }, ); } } - SubagentEvent::StepCompleted { _step, .. } => { - tracing::trace!("[subagent] step {} completed", _step); + SubagentEvent::StepCompleted { .. } => { + tracing::trace!("[subagent] step completed"); } - SubagentEvent::StepFailed { _step, _error } => { - tracing::warn!("[subagent] step {} failed: {}", _step, _error); + SubagentEvent::StepFailed { step, error } => { + tracing::warn!("[subagent] step {} failed: {}", step, error); } SubagentEvent::Completed { .. } => { tracing::debug!("[subagent] completed"); @@ -230,17 +231,16 @@ fn spawn_single_agent( let timeout_ctx = ctx; let timeout_tx = tx; std::thread::spawn(move || { - let _ = done_tx.send(run_subagent(timeout_ctx, timeout_tx)); + let _ = done_tx.send(run_subagent(&timeout_ctx, &timeout_tx)); }); match done_rx.recv_timeout(Duration::from_millis(timeout)) { Ok(r) => r, Err(_) => Err(anyhow::anyhow!( - "subagent '{}' timed out after {}ms", - agent_name, timeout, + "subagent '{agent_name}' timed out after {timeout}ms", )), } } else { - run_subagent(ctx, tx) + run_subagent(&ctx, &tx) }; let completed_at = chrono::Utc::now().timestamp_millis(); @@ -299,6 +299,7 @@ type ParallelResult = (usize, anyhow::Result>); /// /// Return: a `Vec` of all agent outputs (or error strings) in /// the order they were submitted. +#[allow(clippy::too_many_arguments)] pub fn execute_primitive( primitive: &ScriptPrimitive, args: &HashMap, @@ -316,7 +317,7 @@ pub fn execute_primitive( let findings_snapshot = findings.lock().map(|f| f.clone()).unwrap_or_default(); let agent_id = uuid::Uuid::new_v4().to_string(); let agent_name = resolved.chars().take(40).collect::(); - match spawn_single_agent(&agent_id, &agent_name, &resolved, findings_snapshot, findings, live, session_dir, workspaces, timeout_ms) { + match spawn_single_agent(&agent_id, &agent_name, &resolved, &findings_snapshot, findings, live, session_dir, workspaces, timeout_ms) { Ok(text) => Ok(vec![text]), Err(e) => { if continue_on_error { @@ -380,7 +381,7 @@ pub fn execute_primitive( for (_, res) in locked.drain(..) { match res { Ok(outputs) => all.extend(outputs), - Err(e) => all.push(format!("agent error: {}", e)), + Err(e) => all.push(format!("agent error: {e}")), } } Ok(all) @@ -399,7 +400,7 @@ pub fn execute_primitive( Ok(outputs) => all.extend(outputs), Err(e) => { if continue_on_error { - all.push(format!("pipeline stage {} error: {}", idx, e)); + all.push(format!("pipeline stage {idx} error: {e}")); } else { return Err(e); } @@ -437,13 +438,13 @@ pub fn run_workflow( /// /// Why: findings are scoped to an `Arc>>` rather than a /// global static, so concurrent `run_workflow_tracked` calls from different -/// spawn_agents invocations remain fully isolated. +/// `spawn_agents` invocations remain fully isolated. /// /// Return: a human-readable summary string. pub fn run_workflow_tracked( script: &WorkflowScript, args: &HashMap, - live: Option, + live: Option<&LiveStateFn>, session_dir: &std::path::Path, workspaces: &[std::path::PathBuf], ) -> anyhow::Result { @@ -453,11 +454,10 @@ pub fn run_workflow_tracked( 10 }; - let live_ref = live.as_ref(); let findings = Arc::new(Mutex::new(Vec::new())); let results = execute_primitive( &script.script, args, concurrency_cap, - script.options.continue_on_error, live_ref, + script.options.continue_on_error, live, session_dir, workspaces, &findings, script.options.timeout_ms, )?; @@ -489,7 +489,7 @@ pub fn run_workflow_tracked( fn resolve_template(template: &str, args: &HashMap) -> String { let mut result = template.to_string(); for (key, value) in args { - result = result.replace(&format!("{{{{{}}}}}", key), value); + result = result.replace(&format!("{{{{{key}}}}}"), value); } result } @@ -535,7 +535,7 @@ struct SemaphoreGuard<'a> { sem: &'a Semaphore, } -impl<'a> Drop for SemaphoreGuard<'a> { +impl Drop for SemaphoreGuard<'_> { fn drop(&mut self) { let mut count = self.sem.count.lock().unwrap_or_else(|e| { tracing::warn!("[semaphore] mutex poisoned in drop, recovering"); diff --git a/src/controller/command.rs b/src/controller/command.rs index 55ff8e3..bf32d38 100644 --- a/src/controller/command.rs +++ b/src/controller/command.rs @@ -87,7 +87,7 @@ pub fn parse_command(text: &str) -> Command { mode: arg1.to_string(), } } - "/pipeline" => Command::Unknown(format!("/pipeline {} (use: full|quick|skip)", arg1)), + "/pipeline" => Command::Unknown(format!("/pipeline {arg1} (use: full|quick|skip)")), _ => Command::Unknown(cmd.to_string()), } } diff --git a/src/controller/input.rs b/src/controller/input.rs index d4e674c..971e3bd 100644 --- a/src/controller/input.rs +++ b/src/controller/input.rs @@ -21,6 +21,7 @@ use crate::controller::command::parse_command; /// Why: when Editor overlay is active, all key events are consumed by the /// editor handler and never reach the main action dispatch. Return `Vec` /// so that a single key press can trigger multiple actions. +#[allow(clippy::too_many_lines)] pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec { // While Editor overlay is active, route input directly to the editor handler if state.misc.overlay == Overlay::Editor { @@ -34,7 +35,7 @@ pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec { if let Err(e) = std::fs::write(&ed.path, &content) { state.push_toast(crate::app::state::types::Toast::new( crate::app::state::types::ToastKind::Error, - format!("Save failed: {}", e), + format!("Save failed: {e}"), )); } else { state.push_toast(crate::app::state::types::Toast::new( @@ -58,11 +59,11 @@ pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec { return vec![]; } KeyCode::Enter => { - crate::app::mode::editor::handle_editor_input(state, "\n".to_string()); + crate::app::mode::editor::handle_editor_input(state, "\n"); return vec![]; } KeyCode::Char(c) => { - crate::app::mode::editor::handle_editor_input(state, c.to_string()); + crate::app::mode::editor::handle_editor_input(state, &c.to_string()); return vec![]; } _ => return vec![], @@ -93,25 +94,15 @@ pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec { } KeyCode::Enter | KeyCode::Char('a') => { let items = crate::app::mode::learning::get_learning_items(state); - if let Some(item) = items.get(state.misc.selected_index) { - match item { - crate::app::mode::learning::LearningItem::Pending { name, .. } => { - return vec![Action::LessonAccept { name: name.clone() }]; - } - _ => {} - } + if let Some(crate::app::mode::learning::LearningItem::Pending { name, .. }) = items.get(state.misc.selected_index) { + return vec![Action::LessonAccept { name: name.clone() }]; } return vec![]; } KeyCode::Char('r') => { let items = crate::app::mode::learning::get_learning_items(state); - if let Some(item) = items.get(state.misc.selected_index) { - match item { - crate::app::mode::learning::LearningItem::Pending { name, .. } => { - return vec![Action::LessonReject { name: name.clone() }]; - } - _ => {} - } + if let Some(crate::app::mode::learning::LearningItem::Pending { name, .. }) = items.get(state.misc.selected_index) { + return vec![Action::LessonReject { name: name.clone() }]; } return vec![]; } @@ -341,8 +332,8 @@ fn handle_overlay_enter(state: &mut AppStateRest) -> Vec { tracing::warn!("[input] provider '{}' has no default_model, using 'claude-opus-4-8'", provider); "claude-opus-4-8".to_string() }); - state.settings.provider = provider.clone(); - state.settings.model = model.clone(); + state.settings.provider.clone_from(provider); + state.settings.model.clone_from(&model); if let Some(ref key) = cfg.default_api_key { state.settings.api_keys.insert(provider.clone(), key.clone()); } else if let Some(env_key) = cfg.api_key_env.as_ref().and_then(|env| std::env::var(env).ok()) { @@ -351,7 +342,7 @@ fn handle_overlay_enter(state: &mut AppStateRest) -> Vec { let _ = state.settings.save(); state.push_toast(crate::app::state::types::Toast::new( crate::app::state::types::ToastKind::Success, - format!("Switched to {} / {}", provider, model), + format!("Switched to {provider} / {model}"), )); } } diff --git a/src/dto/provider/request.rs b/src/dto/provider/request.rs index 2e7d8ce..d9c98a0 100644 --- a/src/dto/provider/request.rs +++ b/src/dto/provider/request.rs @@ -15,7 +15,7 @@ use serde_json::Value; /// Outbound chat completion request body sent to an OpenAI/Anthropic-compatible provider. /// /// Flow: constructed from the current message history plus optional -/// generation knobs (temperature, max_tokens, tools, etc.) and serialized +/// generation knobs (temperature, `max_tokens`, tools, etc.) and serialized /// directly into the HTTP request body. /// /// Return: not a function, but the value that becomes the JSON request diff --git a/src/ipc/conn.rs b/src/ipc/conn.rs index 8306458..c7f8118 100644 --- a/src/ipc/conn.rs +++ b/src/ipc/conn.rs @@ -19,8 +19,8 @@ pub struct Connection { impl Connection { /// Wrap an already-connected/accepted `UnixStream`. - pub fn from_stream(stream: UnixStream) -> Result { - Ok(Connection { inner: stream }) + pub fn from_stream(stream: UnixStream) -> Self { + Connection { inner: stream } } /// Open a new Unix-socket connection to `path`. diff --git a/src/ipc/frame.rs b/src/ipc/frame.rs index 06d36fb..c1822d9 100644 --- a/src/ipc/frame.rs +++ b/src/ipc/frame.rs @@ -1,3 +1,4 @@ +#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)] //! Length-prefixed binary framing and JSON (de)serialization helpers for //! the IPC wire protocol. //! @@ -26,7 +27,7 @@ pub(crate) const MAX_FRAME_SIZE: usize = 64 * 1024 * 1024; pub fn write_frame(writer: &mut W, data: &[u8]) -> Result<()> { let len = data.len(); if len > MAX_FRAME_SIZE { - anyhow::bail!("frame too large: {} bytes exceeds 64 MiB limit", len); + anyhow::bail!("frame too large: {len} bytes exceeds 64 MiB limit"); } let len_bytes = (len as u32).to_be_bytes(); writer.write_all(&len_bytes)?; @@ -52,7 +53,7 @@ pub fn read_frame(reader: &mut R) -> Result>> { } let len = u32::from_be_bytes(len_buf) as usize; if len > MAX_FRAME_SIZE { - anyhow::bail!("frame too large: {} bytes exceeds 64 MiB limit", len); + anyhow::bail!("frame too large: {len} bytes exceeds 64 MiB limit"); } let mut buf = vec![0u8; len]; reader.read_exact(&mut buf)?; diff --git a/src/ipc/server.rs b/src/ipc/server.rs index 28871f7..0addbb0 100644 --- a/src/ipc/server.rs +++ b/src/ipc/server.rs @@ -30,6 +30,6 @@ impl IpcServer { /// Block until a client connects, then wrap it as a `Connection`. pub fn accept(&self) -> Result { let (stream, _addr) = self.listener.accept()?; - Connection::from_stream(stream) + Ok(Connection::from_stream(stream)) } } diff --git a/src/main.rs b/src/main.rs index 21b3fb9..5dc5425 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)] //! Zesdex binary entry point. //! //! Parses `--daemon` / `--attach ` flags to select one of three @@ -106,7 +107,7 @@ fn run_single_process() -> Result<()> { let workspace_roots = vec![std::env::current_dir()?]; let mut state = app::state::rest::AppStateRest::new( workspace_roots.clone(), - session_dir, + &session_dir, store.memory_dir, ); state.sessions = model::session::Session::list(&store.base_dir); @@ -129,7 +130,7 @@ fn run_single_process() -> Result<()> { let _ = disable_raw_mode(); if let Err(e) = run_result { - let _ = writeln!(restore_stdout, "error: {}", e); + let _ = writeln!(restore_stdout, "error: {e}"); let _ = restore_stdout.flush(); } @@ -262,7 +263,6 @@ fn apply_client_update( state.transcript_cache.messages = payload.messages.into_iter().map(|m| { app::state::rest::ChatMessageDisplay { role: match m.role.as_str() { - "User" => crate::dto::chat::message::Role::User, "Assistant" => crate::dto::chat::message::Role::Assistant, "System" => crate::dto::chat::message::Role::System, "Tool" => crate::dto::chat::message::Role::Tool, @@ -300,7 +300,6 @@ fn apply_client_update( state.misc.toasts = payload.toasts.into_iter().map(|t| { Toast { kind: match t.kind.as_str() { - "Info" => ToastKind::Info, "Success" => ToastKind::Success, "Warning" => ToastKind::Warning, "Error" => ToastKind::Error, @@ -350,7 +349,7 @@ fn run_daemon() -> Result<()> { let workspace_roots = vec![std::env::current_dir()?]; let mut state = app::state::rest::AppStateRest::new( workspace_roots.clone(), - session_dir, + &session_dir, store.memory_dir, ); state.sessions = model::session::Session::list(&store.base_dir); @@ -479,7 +478,7 @@ fn run_attach(session_id: &str) -> Result<()> { std::fs::create_dir_all(&session_dir)?; let mut client_state = app::state::rest::AppStateRest::new( workspace_roots, - session_dir, + &session_dir, store.memory_dir, ); client_state.session_id = session_id.to_string(); diff --git a/src/model/editlog.rs b/src/model/editlog.rs index 1d14b0e..76d131f 100644 --- a/src/model/editlog.rs +++ b/src/model/editlog.rs @@ -43,18 +43,12 @@ impl EditLog { /// `MAX_MEMORY_ENTRIES` entries. The full history is preserved on disk /// regardless of the in-memory limit. fn load_from_disk(path: &std::path::Path) -> Vec { - let file = match std::fs::File::open(path) { - Ok(f) => f, - Err(_) => return Vec::new(), - }; use std::io::{BufRead, BufReader}; + let Ok(file) = std::fs::File::open(path) else { return Vec::new() }; let reader = BufReader::new(file); let mut entries: Vec = Vec::new(); for line in reader.lines() { - let line = match line { - Ok(l) => l, - Err(_) => continue, - }; + let Ok(line) = line else { continue }; if let Ok(entry) = serde_json::from_str::(&line) { // Keep only the most recent entries in memory if entries.len() >= MAX_MEMORY_ENTRIES { @@ -81,6 +75,7 @@ impl EditLog { /// Return: `Ok(())` on success; an `io::Error` if serialization or /// any filesystem operation fails. pub fn append(&mut self, entry: EditLogEntry) -> std::io::Result<()> { + use std::io::Write; let line = serde_json::to_string(&entry)? + "\n"; // Ensure parent directory exists; fall back to the current // directory if path has no parent (should not happen in practice @@ -92,7 +87,6 @@ impl EditLog { .create(true) .append(true) .open(&self.path)?; - use std::io::Write; file.write_all(line.as_bytes())?; file.sync_all()?; self.entries.push(entry); diff --git a/src/model/memory.rs b/src/model/memory.rs index 1ffaaba..32dd188 100644 --- a/src/model/memory.rs +++ b/src/model/memory.rs @@ -57,7 +57,7 @@ impl Memory { /// to nothing, so a path is always produced. pub fn path(memory_dir: &Path, name: &str) -> PathBuf { let slug = Self::slugify(name).unwrap_or_else(|| "memory".to_string()); - slug_path(memory_dir, &format!("{}.md", slug)) + slug_path(memory_dir, &format!("{slug}.md")) } /// Serialize this memory to markdown-with-frontmatter and write it @@ -72,14 +72,15 @@ impl Memory { /// /// Return: `Ok(())` on success, or an `io::Error` from directory /// creation, the temp write, or the rename. + #[allow(clippy::suspicious_open_options)] pub fn write(&self, memory_dir: &Path) -> std::io::Result<()> { let path = Self::path(memory_dir, &self.name); let parent = path.parent().unwrap(); std::fs::create_dir_all(parent)?; - let outcome_line = self.outcome.as_ref().map(|o| format!("outcome: {}", o)).unwrap_or_default(); - let scope_line = self.scope.as_ref().map(|s| format!("scope: {}", s)).unwrap_or_default(); - let before_line = self.before_snippet.as_ref().map(|s| format!("before: {}", s)).unwrap_or_default(); - let after_line = self.after_snippet.as_ref().map(|s| format!("after: {}", s)).unwrap_or_default(); + let outcome_line = self.outcome.as_ref().map(|o| format!("outcome: {o}")).unwrap_or_default(); + let scope_line = self.scope.as_ref().map(|s| format!("scope: {s}")).unwrap_or_default(); + let before_line = self.before_snippet.as_ref().map(|s| format!("before: {s}")).unwrap_or_default(); + let after_line = self.after_snippet.as_ref().map(|s| format!("after: {s}")).unwrap_or_default(); let prov_line = if self.provenances.is_empty() { String::new() } else { @@ -95,11 +96,11 @@ impl Memory { // Write to temp file with fsync for crash safety (prevents // partial writes surviving a power loss). { + use std::io::Write; let mut f = std::fs::OpenOptions::new() .create(true) .write(true) .open(&tmp)?; - use std::io::Write; f.write_all(content.as_bytes())?; f.sync_all()?; } @@ -161,7 +162,7 @@ impl Memory { before_snippet: front.get("before").cloned().filter(|s| !s.is_empty()), after_snippet: front.get("after").cloned().filter(|s| !s.is_empty()), provenances: front.get("provenances").cloned() - .map(|s| s.split(", ").map(|p| p.to_string()).collect()) + .map(|s| s.split(", ").map(std::string::ToString::to_string).collect()) .unwrap_or_default(), }) } @@ -185,13 +186,10 @@ impl Memory { /// Return: slugs (without extension); empty `Vec` if the directory /// can't be read. pub fn list(memory_dir: &Path) -> Vec { - let entries = match std::fs::read_dir(memory_dir) { - Ok(e) => e, - Err(_) => return Vec::new(), - }; + let Ok(entries) = std::fs::read_dir(memory_dir) else { return Vec::new() }; entries - .filter_map(|e| e.ok()) - .filter(|e| e.path().extension().map(|x| x == "md").unwrap_or(false)) + .filter_map(std::result::Result::ok) + .filter(|e| e.path().extension().is_some_and(|x| x == "md")) .filter_map(|e| { let name = e.file_name().to_string_lossy().to_string(); if name == "MEMORY.md" { return None; } diff --git a/src/model/mod.rs b/src/model/mod.rs index 96839ea..e156da5 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1,5 +1,5 @@ //! Persistence and domain model layer: sessions, conversations, memory, -//! message log (SQLite), edit log, and app/settings config. +//! message log (`SQLite`), edit log, and app/settings config. pub mod app_config; pub mod editlog; diff --git a/src/model/msglog/blobs.rs b/src/model/msglog/blobs.rs index 3271796..32aae59 100644 --- a/src/model/msglog/blobs.rs +++ b/src/model/msglog/blobs.rs @@ -1,4 +1,4 @@ -//! Binary blob storage in the message-log SQLite database (e.g. images, +//! Binary blob storage in the message-log `SQLite` database (e.g. images, //! attachments), keyed by session id and an arbitrary blob key. use rusqlite::{Connection, params}; @@ -9,7 +9,7 @@ use anyhow::Result; /// Flow: compute current timestamp → `INSERT OR REPLACE` into `blobs` /// keyed on `(session_id, blob_key)`. /// -/// Return: `Ok(())` on success, or the underlying SQLite error. +/// Return: `Ok(())` on success, or the underlying `SQLite` error. pub fn store_blob(conn: &Connection, session_id: &str, blob_key: &str, data: &[u8], mime_type: Option<&str>) -> Result<()> { let created_at = chrono::Utc::now().timestamp_millis(); conn.execute( @@ -22,7 +22,7 @@ pub fn store_blob(conn: &Connection, session_id: &str, blob_key: &str, data: &[u /// Fetch a blob's bytes for a session by key. /// /// Return: `Ok(Some(data))` if found, `Ok(None)` if no matching row -/// exists, `Err` for any other SQLite failure. +/// exists, `Err` for any other `SQLite` failure. pub fn retrieve_blob(conn: &Connection, session_id: &str, blob_key: &str) -> Result>> { let result = conn.query_row( "SELECT data FROM blobs WHERE session_id = ?1 AND blob_key = ?2", @@ -52,7 +52,7 @@ pub fn delete_blob(conn: &Connection, session_id: &str, blob_key: &str) -> Resul /// List all blob keys stored for a session, oldest first. /// /// Return: `Ok(Vec)` of keys ordered by `created_at`, or the -/// underlying SQLite error. +/// underlying `SQLite` error. pub fn list_blob_keys(conn: &Connection, session_id: &str) -> Result> { let mut stmt = conn.prepare( "SELECT blob_key FROM blobs WHERE session_id = ?1 ORDER BY created_at ASC" diff --git a/src/model/msglog/mod.rs b/src/model/msglog/mod.rs index d8cce85..712df06 100644 --- a/src/model/msglog/mod.rs +++ b/src/model/msglog/mod.rs @@ -12,7 +12,7 @@ pub use query::insert_message; /// schema is initialized. /// /// Flow: resolve `/messages.sqlite` → create parent dirs → -/// open a SQLite connection → run `schema::init_schema`. +/// open a `SQLite` connection → run `schema::init_schema`. /// /// Return: an open, schema-ready `Connection`, or an error if any step /// fails. diff --git a/src/model/msglog/query.rs b/src/model/msglog/query.rs index 94140c4..ece9d0e 100644 --- a/src/model/msglog/query.rs +++ b/src/model/msglog/query.rs @@ -6,7 +6,7 @@ use crate::dto::chat::message::{ChatMessage, Role}; /// Insert a chat message into the session's message log. /// -/// Flow: extract optional content/tool_call_id/tool_name → serialize +/// Flow: extract optional `content/tool_call_id/tool_name` → serialize /// `tool_calls` to a JSON string if present → map `Role` to its string /// column value → `INSERT` the row with the current timestamp. /// diff --git a/src/model/msglog/schema.rs b/src/model/msglog/schema.rs index 85c8d25..45f2829 100644 --- a/src/model/msglog/schema.rs +++ b/src/model/msglog/schema.rs @@ -1,4 +1,4 @@ -//! SQLite schema definition for the message log database. +//! `SQLite` schema definition for the message log database. use rusqlite::Connection; use anyhow::Result; @@ -9,7 +9,7 @@ use anyhow::Result; /// Why: idempotent via `CREATE TABLE/INDEX IF NOT EXISTS`, so it's safe /// to call on every `open_or_create`. /// -/// Return: `Ok(())` on success, or the underlying SQLite error. +/// Return: `Ok(())` on success, or the underlying `SQLite` error. pub fn init_schema(conn: &Connection) -> Result<()> { conn.execute_batch("PRAGMA foreign_keys = ON;")?; conn.execute_batch( diff --git a/src/model/session.rs b/src/model/session.rs index cc6f47a..edbf489 100644 --- a/src/model/session.rs +++ b/src/model/session.rs @@ -89,7 +89,7 @@ impl Session { if id.contains('/') || id.contains('\\') || id.contains("..") { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, - format!("invalid session id '{}': must not contain path separators", id), + format!("invalid session id '{id}': must not contain path separators"), )); } let path = base_dir.join("sessions").join(id).join("session.json"); @@ -108,12 +108,9 @@ impl Session { /// contains no valid sessions. pub fn list(base_dir: &Path) -> Vec { let sessions_dir = base_dir.join("sessions"); - let entries = match std::fs::read_dir(&sessions_dir) { - Ok(e) => e, - Err(_) => return Vec::new(), - }; + let Ok(entries) = std::fs::read_dir(&sessions_dir) else { return Vec::new() }; entries - .filter_map(|e| e.ok()) + .filter_map(std::result::Result::ok) .filter(|e| e.path().is_dir()) .filter_map(|e| { let id = e.file_name().to_string_lossy().to_string(); diff --git a/src/model/session_lock.rs b/src/model/session_lock.rs index 12f8b8d..71f01c7 100644 --- a/src/model/session_lock.rs +++ b/src/model/session_lock.rs @@ -1,3 +1,4 @@ +#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)] //! PID-file based advisory lock preventing two processes from operating on //! the same session directory concurrently. @@ -37,6 +38,7 @@ impl SessionLock { /// /// Return: `Ok(true)` if acquired, `Ok(false)` if another live /// process holds it, `Err` on I/O failure. + #[allow(clippy::suspicious_open_options)] pub fn try_lock(&self) -> std::io::Result { // Phase 1: try atomic create. If it succeeds, the lock is ours. match fs::OpenOptions::new() @@ -90,6 +92,7 @@ impl SessionLock { /// Check whether a process with the given PID is currently alive and /// is actually a zesdex process (not a recycled PID from a different /// program). + #[allow(clippy::unused_self)] fn is_alive(&self, pid: u32) -> bool { // SAFETY: `libc::kill(pid, 0)` does not send a signal; it only checks // whether the process exists and the caller has permission to signal @@ -102,18 +105,12 @@ impl SessionLock { // from a different program would answer kill but shouldn't hold // our lock). This is best-effort — /proc may not be available // on all platforms. - let proc_exe = std::path::PathBuf::from(format!("/proc/{}/exe", pid)); - match std::fs::read_link(&proc_exe) { - Ok(target) => match std::env::current_exe() { - Ok(exe) => { - if target != exe { - return false; - } - } - Err(_) => { /* cannot resolve own exe, trust kill check */ } - }, - Err(_) => { /* /proc unavailable, trust kill check */ } - } + let proc_exe = std::path::PathBuf::from(format!("/proc/{pid}/exe")); + if let Ok(target) = std::fs::read_link(&proc_exe) { if let Ok(exe) = std::env::current_exe() { + if target != exe { + return false; + } + } else { /* cannot resolve own exe, trust kill check */ } } else { /* /proc unavailable, trust kill check */ } true } } diff --git a/src/service/oauth/loopback.rs b/src/service/oauth/loopback.rs index 4f601b2..f1f9e7b 100644 --- a/src/service/oauth/loopback.rs +++ b/src/service/oauth/loopback.rs @@ -1,3 +1,4 @@ +#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)] //! Minimal loopback HTTP server for capturing OAuth authorization-code redirects. use std::io::{Read, Write}; diff --git a/src/service/oauth/manager.rs b/src/service/oauth/manager.rs index 67624a3..14ad355 100644 --- a/src/service/oauth/manager.rs +++ b/src/service/oauth/manager.rs @@ -72,13 +72,13 @@ impl OAuthManager { .post(&self.config.token_url) .form(¶ms) .send() - .map_err(|e| format!("token request failed: {}", e))?; + .map_err(|e| format!("token request failed: {e}"))?; let status = resp.status(); - let body: serde_json::Value = resp.json().map_err(|e| format!("parse failed: {}", e))?; + let body: serde_json::Value = resp.json().map_err(|e| format!("parse failed: {e}"))?; if !status.is_success() { - return Err(format!("token endpoint returned {}: {}", status, body)); + return Err(format!("token endpoint returned {status}: {body}")); } let access_token = body["access_token"].as_str().ok_or("missing access_token")?.to_string(); @@ -87,7 +87,7 @@ impl OAuthManager { self.token = Some(OAuthToken { access_token, - refresh_token: body["refresh_token"].as_str().map(|s| s.to_string()), + refresh_token: body["refresh_token"].as_str().map(std::string::ToString::to_string), expires_at: now + expires_in, token_type: body["token_type"].as_str().unwrap_or("Bearer").to_string(), }); @@ -98,7 +98,7 @@ impl OAuthManager { /// Build the provider's authorization URL with PKCE and state params attached. /// /// Why: refuses to build a URL if `auth_url` is missing or invalid. Previously - /// this silently fell back to https://example.com, which produced a valid-looking + /// this silently fell back to , which produced a valid-looking /// auth URL pointing at the wrong server and leaked client credentials in /// query params. Returning an empty string signals failure to callers, who /// can prompt the user to fix the OAuth config instead of starting a flow diff --git a/src/service/oauth/pkce.rs b/src/service/oauth/pkce.rs index b950c75..b3df023 100644 --- a/src/service/oauth/pkce.rs +++ b/src/service/oauth/pkce.rs @@ -1,3 +1,4 @@ +#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)] //! PKCE (Proof Key for Code Exchange) verifier/challenge pair generation for OAuth flows. use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; diff --git a/src/service/provider.rs b/src/service/provider.rs index ac3b52d..6d93f58 100644 --- a/src/service/provider.rs +++ b/src/service/provider.rs @@ -13,7 +13,7 @@ pub(crate) const DEFAULT_BASE_URL: &str = "https://opencode.ai/zen/v1"; const DEFAULT_MODEL: &str = "deepseek-v4-flash-free"; pub const DEFAULT_API_KEY: &str = ""; const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); -const REQUEST_TIMEOUT: Duration = Duration::from_secs(60); +const REQUEST_TIMEOUT: Duration = Duration::from_mins(1); /// Blocking HTTP client for a single LLM provider endpoint. /// @@ -29,9 +29,9 @@ pub struct LlmClient { impl LlmClient { /// Construct a client, falling back to built-in defaults for empty inputs. /// - /// Flow: empty api_key/model → substitute defaults → build reqwest client + /// Flow: empty `api_key/model` → substitute defaults → build reqwest client /// with connect/request timeouts → if TLS config fails, retry with just - /// request timeout (no connect timeout) → normalize base_url. + /// request timeout (no connect timeout) → normalize `base_url`. /// /// Why: empty strings are treated as "unset" rather than errors so callers /// can pass through unconfigured settings without special-casing them. @@ -126,11 +126,11 @@ impl LlmClient { let result = (|| -> Result<(ChatMessage, Option<(u64, u64)>)> { let resp = http_req.json(&req).send().map_err(|e| { if e.is_timeout() { - anyhow::anyhow!("API request timed out after {:?}. Check your network or try again.", REQUEST_TIMEOUT) + anyhow::anyhow!("API request timed out after {REQUEST_TIMEOUT:?}. Check your network or try again.") } else if e.is_connect() { anyhow::anyhow!("Could not connect to {}. Is the URL correct and is the service reachable?", self.base_url) } else { - anyhow::anyhow!("API request failed: {}", e) + anyhow::anyhow!("API request failed: {e}") } })?; @@ -142,7 +142,7 @@ impl LlmClient { let data: crate::dto::provider::response::ChatResponse = resp.json()?; let usage = data.usage.map(|u| { - (u.prompt_tokens.unwrap_or(0) as u64, u.completion_tokens.unwrap_or(0) as u64) + (u64::from(u.prompt_tokens.unwrap_or(0)), u64::from(u.completion_tokens.unwrap_or(0))) }); let message = data .choices @@ -259,11 +259,11 @@ impl LlmClient { let resp = http_req.json(req).send().map_err(|e| { if e.is_timeout() { - anyhow::anyhow!("API request timed out after {:?}. Check your network or try again.", REQUEST_TIMEOUT) + anyhow::anyhow!("API request timed out after {REQUEST_TIMEOUT:?}. Check your network or try again.") } else if e.is_connect() { anyhow::anyhow!("Could not connect to {}. Is the URL correct and is the service reachable?", self.base_url) } else { - anyhow::anyhow!("API request failed: {}", e) + anyhow::anyhow!("API request failed: {e}") } })?; @@ -282,7 +282,7 @@ impl LlmClient { loop { let n = reader.read(&mut chunk_buf) - .map_err(|e| anyhow::anyhow!("stream read error: {}", e))?; + .map_err(|e| anyhow::anyhow!("stream read error: {e}"))?; if n == 0 { break; } @@ -306,7 +306,7 @@ impl LlmClient { usage = Some((*prompt_tokens, *completion_tokens)); } StreamEvent::Error(msg) => { - anyhow::bail!("stream error: {}", msg); + anyhow::bail!("stream error: {msg}"); } StreamEvent::Done => { turn.apply_event(&event); diff --git a/src/tool/bash_tools.rs b/src/tool/bash_tools.rs index 3575b3a..27f6886 100644 --- a/src/tool/bash_tools.rs +++ b/src/tool/bash_tools.rs @@ -43,7 +43,7 @@ impl Tool for BashOutput { } match crate::app::bgbash::control::bash_output(&job_id) { Some(lines) => Ok(lines.join("\n")), - None => Ok(format!("No new output from job '{}'", job_id)), + None => Ok(format!("No new output from job '{job_id}'")), } } } @@ -82,11 +82,11 @@ impl Tool for BashKill { anyhow::bail!("invalid job_id format: expected UUID"); } crate::app::bgbash::control::bash_kill(&job_id)?; - Ok(format!("Killed background job '{}'", job_id)) + Ok(format!("Killed background job '{job_id}'")) } } -/// Validate that a job_id matches UUID v4 format (hex with dashes). +/// Validate that a `job_id` matches UUID v4 format (hex with dashes). fn is_valid_job_id(id: &str) -> bool { // UUID v4 format: 8-4-4-4-12 hex digits let parts: Vec<&str> = id.split('-').collect(); diff --git a/src/tool/fs/delete.rs b/src/tool/fs/delete.rs index 281a063..d55aa38 100644 --- a/src/tool/fs/delete.rs +++ b/src/tool/fs/delete.rs @@ -47,24 +47,24 @@ impl Tool for Delete { } let metadata = path.metadata() - .map_err(|e| anyhow!("failed to read metadata for '{}': {}", rel, e))?; + .map_err(|e| anyhow!("failed to read metadata for '{rel}': {e}"))?; if metadata.is_dir() { let is_empty = fs::read_dir(&path) - .map_err(|e| anyhow!("failed to read directory '{}': {}", rel, e))? + .map_err(|e| anyhow!("failed to read directory '{rel}': {e}"))? .next() .is_none(); if is_empty { fs::remove_dir(&path) - .map_err(|e| anyhow!("failed to remove directory '{}': {}", rel, e))?; - Ok(format!("removed empty directory {}", rel)) + .map_err(|e| anyhow!("failed to remove directory '{rel}': {e}"))?; + Ok(format!("removed empty directory {rel}")) } else { - anyhow::bail!("directory '{}' is not empty (refusing to delete)", rel); + anyhow::bail!("directory '{rel}' is not empty (refusing to delete)"); } } else { fs::remove_file(&path) - .map_err(|e| anyhow!("failed to delete '{}': {}", rel, e))?; - Ok(format!("deleted {}", rel)) + .map_err(|e| anyhow!("failed to delete '{rel}': {e}"))?; + Ok(format!("deleted {rel}")) } } } diff --git a/src/tool/fs/edit.rs b/src/tool/fs/edit.rs index b93c8c3..6265bfb 100644 --- a/src/tool/fs/edit.rs +++ b/src/tool/fs/edit.rs @@ -1,3 +1,4 @@ +#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)] //! Tool: `edit` — replace a substring in a file with a new string. use std::fs; @@ -70,25 +71,24 @@ impl Tool for Edit { anyhow::bail!("'old' must be a non-empty string; use 'write' to replace entire file contents"); } let check_matches = check_graduated_checks(&rel, &new_str, &ctx.graduated_checks); - let replace_all = args.get("replace_all").and_then(|v| v.as_bool()).unwrap_or(false); + let replace_all = args.get("replace_all").and_then(serde_json::Value::as_bool).unwrap_or(false); let path: PathBuf = resolve_path(&ctx.workspaces, &rel)?; if !path.exists() { anyhow::bail!("file '{}' does not exist at resolved path {}", rel, path.display()); } if path.is_dir() { - anyhow::bail!("'{}' is a directory, not a file", rel); + anyhow::bail!("'{rel}' is a directory, not a file"); } let content = fs::read_to_string(&path) - .map_err(|e| anyhow!("failed to read '{}': {}", rel, e))?; + .map_err(|e| anyhow!("failed to read '{rel}': {e}"))?; if !content.contains(&old) { - anyhow::bail!("old string not found in '{}'", rel); + anyhow::bail!("old string not found in '{rel}'"); } if !replace_all { let count = content.matches(&old).count(); if count > 1 { anyhow::bail!( - "old string appears {} times in '{}'. Set replace_all=true to replace all occurrences, or provide a more specific match.", - count, rel + "old string appears {count} times in '{rel}'. Set replace_all=true to replace all occurrences, or provide a more specific match." ); } } @@ -98,7 +98,7 @@ impl Tool for Edit { content.replacen(&old, &new_str, 1) }; fs::write(&path, &new_content) - .map_err(|e| anyhow!("failed to write '{}': {}", rel, e))?; + .map_err(|e| anyhow!("failed to write '{rel}': {e}"))?; let bytes_diff = if new_content.len() > content.len() { new_content.len() - content.len() } else { @@ -108,10 +108,8 @@ impl Tool for Edit { // Never fail the edit because of this — LSP errors are surfaced as a // trailing annotation on the success message instead. let lsp_note = if let Ok(mut lsp) = ctx.lsp_manager.lock() { - match lsp.did_change_file(&path) { - Ok(()) => String::new(), - Err(e) => format!(" (LSP: {})", e), - } + lsp.did_change_file(&path); + String::new() } else { String::new() }; diff --git a/src/tool/fs/helpers.rs b/src/tool/fs/helpers.rs index d1f0a53..d766395 100644 --- a/src/tool/fs/helpers.rs +++ b/src/tool/fs/helpers.rs @@ -12,8 +12,8 @@ use anyhow::{Result, anyhow}; pub fn arg_str(args: &Value, name: &str) -> Result { args.get(name) .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - .ok_or_else(|| anyhow!("missing required argument: {}", name)) + .map(std::string::ToString::to_string) + .ok_or_else(|| anyhow!("missing required argument: {name}")) } /// Produce a user-friendly diagnostic string when a path doesn't resolve or exist. @@ -25,17 +25,17 @@ pub fn arg_str(args: &Value, name: &str) -> Result { pub fn not_found_help(ctx: &super::super::ToolCtx, path: &Path, rel: &str) -> String { let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); let in_ws = ctx.workspaces.iter().any(|w| { - let wc = w.canonicalize().unwrap_or_else(|_| w.to_path_buf()); + let wc = w.canonicalize().unwrap_or_else(|_| w.clone()); canon.starts_with(&wc) }); - if !in_ws { + if in_ws { + format!("path '{}' does not exist (resolved to {})", rel, canon.display()) + } else { format!( "path '{}' is outside all workspace roots. Workspace roots: {}", rel, ctx.workspaces.iter().map(|w| w.display().to_string()).collect::>().join(", ") ) - } else { - format!("path '{}' does not exist (resolved to {})", rel, canon.display()) } } diff --git a/src/tool/fs/read.rs b/src/tool/fs/read.rs index 6f82875..710328d 100644 --- a/src/tool/fs/read.rs +++ b/src/tool/fs/read.rs @@ -1,3 +1,4 @@ +#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)] //! Tool: `read` — display file contents with line numbers. use std::fs; @@ -47,7 +48,7 @@ impl Tool for Read { /// exist; a "is a directory" message if the path points at a directory. fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let rel = arg_str(args, "path")?; - let limit = args.get("limit").and_then(|v| v.as_u64()).map(|v| v as usize); + let limit = args.get("limit").and_then(serde_json::Value::as_u64).map(|v| v as usize); let path: PathBuf = match resolve_path(&ctx.workspaces, &rel) { Ok(p) => p, Err(_e) => return Ok(not_found_help(ctx, &PathBuf::from(&rel), &rel)), @@ -56,10 +57,10 @@ impl Tool for Read { return Ok(not_found_help(ctx, &path, &rel)); } if path.is_dir() { - return Ok(format!("'{}' is a directory, not a file. Use ls or glob to list directory contents.", rel)); + return Ok(format!("'{rel}' is a directory, not a file. Use ls or glob to list directory contents.")); } let content = fs::read_to_string(&path) - .map_err(|e| anyhow!("failed to read '{}': {}", rel, e))?; + .map_err(|e| anyhow!("failed to read '{rel}': {e}"))?; let lines: Vec<&str> = content.lines().collect(); let total = lines.len(); let take = limit.unwrap_or(total).min(total); diff --git a/src/tool/fs/write.rs b/src/tool/fs/write.rs index 1ff6856..a3d9375 100644 --- a/src/tool/fs/write.rs +++ b/src/tool/fs/write.rs @@ -60,18 +60,16 @@ impl Tool for Write { let path = resolve_path(&ctx.workspaces, &rel)?; if let Some(parent) = path.parent() { fs::create_dir_all(parent) - .map_err(|e| anyhow!("failed to create parent directories for '{}': {}", rel, e))?; + .map_err(|e| anyhow!("failed to create parent directories for '{rel}': {e}"))?; } fs::write(&path, &content) - .map_err(|e| anyhow!("failed to write '{}': {}", rel, e))?; + .map_err(|e| anyhow!("failed to write '{rel}': {e}"))?; // Notify the LSP server of the on-disk change so diagnostics stay in // sync. Never fails the write itself: a lock failure or LSP error is // folded into the returned message instead of propagated as an Err. let lsp_note = if let Ok(mut lsp) = ctx.lsp_manager.lock() { - match lsp.did_change_file(&path) { - Ok(()) => String::new(), - Err(e) => format!(" (LSP: {})", e), - } + lsp.did_change_file(&path); + String::new() } else { String::new() }; diff --git a/src/tool/git_cred.rs b/src/tool/git_cred.rs index fb2ac0a..ed40001 100644 --- a/src/tool/git_cred.rs +++ b/src/tool/git_cred.rs @@ -48,11 +48,11 @@ impl Tool for GitCred { .arg("credential") .arg(operation) .output() - .map_err(|e| anyhow!("git credential failed: {}", e))?; + .map_err(|e| anyhow!("git credential failed: {e}"))?; if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - Ok(format!("{}{}", stdout, stderr)) + Ok(format!("{stdout}{stderr}")) } else { let stderr = String::from_utf8_lossy(&output.stderr).to_string(); anyhow::bail!("git credential '{}' failed: {}", operation, stderr.trim()) diff --git a/src/tool/git_operator.rs b/src/tool/git_operator.rs index 8b73eac..b52d52e 100644 --- a/src/tool/git_operator.rs +++ b/src/tool/git_operator.rs @@ -58,7 +58,7 @@ impl Tool for GitOperator { .and_then(|v| v.as_array()) .map(|arr| { arr.iter() - .filter_map(|v| v.as_str().map(|s| s.to_string())) + .filter_map(|v| v.as_str().map(std::string::ToString::to_string)) .collect() }) .ok_or_else(|| anyhow!("missing required argument: args"))?; @@ -67,12 +67,12 @@ impl Tool for GitOperator { // of which tool the model uses. let cmd_for_filter = format!("git {} {}", operation, arg_list.join(" ")); crate::tool::shell_filter::git::check_git_destructive(&cmd_for_filter) - .map_err(|e| anyhow!("blocked: {}", e))?; + .map_err(|e| anyhow!("blocked: {e}"))?; let output = Command::new("git") .arg(&operation) .args(&arg_list) .output() - .map_err(|e| anyhow!("git {} failed: {}", operation, e))?; + .map_err(|e| anyhow!("git {operation} failed: {e}"))?; let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stderr = String::from_utf8_lossy(&output.stderr).to_string(); let combined = if stderr.is_empty() { stdout.trim().to_string() } else { format!("{}\n{}", stdout.trim(), stderr.trim()) }; diff --git a/src/tool/git_worktree.rs b/src/tool/git_worktree.rs index e469951..eb356f6 100644 --- a/src/tool/git_worktree.rs +++ b/src/tool/git_worktree.rs @@ -37,7 +37,7 @@ impl Tool for GitWorktree { /// Create the worktree directory and run `git worktree add --checkout `. /// - /// Flow: extract name/base_ref → create worktree dir under `ctx.worktrees_dir` → + /// Flow: extract `name/base_ref` → create worktree dir under `ctx.worktrees_dir` → /// spawn `git worktree add` → combine stdout/stderr. /// /// Return: success message with combined output on success; error including exit @@ -56,18 +56,18 @@ impl Tool for GitWorktree { .to_string(); let worktree_path = ctx.worktrees_dir.join(&name); std::fs::create_dir_all(&worktree_path) - .map_err(|e| anyhow!("failed to create worktree directory: {}", e))?; + .map_err(|e| anyhow!("failed to create worktree directory: {e}"))?; let output = Command::new("git") .args(["worktree", "add", "--checkout"]) .arg(worktree_path.display().to_string()) .arg(&base_ref) .output() - .map_err(|e| anyhow!("git worktree add failed: {}", e))?; + .map_err(|e| anyhow!("git worktree add failed: {e}"))?; let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stderr = String::from_utf8_lossy(&output.stderr).to_string(); let combined = if stderr.is_empty() { stdout.trim().to_string() } else { format!("{}\n{}", stdout.trim(), stderr.trim()) }; if output.status.success() { - Ok(format!("created worktree '{}' from '{}'\n{}", name, base_ref, combined)) + Ok(format!("created worktree '{name}' from '{base_ref}'\n{combined}")) } else { anyhow::bail!("git worktree add failed (exit {}): {}", output.status.code().unwrap_or(-1), stderr.trim()) } diff --git a/src/tool/lsp/mod.rs b/src/tool/lsp/mod.rs index 66900d9..5f360a1 100644 --- a/src/tool/lsp/mod.rs +++ b/src/tool/lsp/mod.rs @@ -1,3 +1,5 @@ +#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)] +use std::fmt::Write; use serde_json::{json, Value}; use anyhow::{Result, anyhow}; @@ -59,7 +61,7 @@ impl Tool for LspConnect { .unwrap_or_default(); let mut manager = ctx.lsp_manager.lock() - .map_err(|e| anyhow!("LSP manager lock error: {}", e))?; + .map_err(|e| anyhow!("LSP manager lock error: {e}"))?; manager.connect(name, command, &extra_args, language_id)?; // Auto-register this server's known extensions so lsp_diagnostics / @@ -79,8 +81,7 @@ impl Tool for LspConnect { .unwrap_or_else(|_| "{}".to_string()); Ok(format!( - "Connected to LSP server '{}' (language: {})\nServer capabilities:\n{}", - name, language_id, caps_summary + "Connected to LSP server '{name}' (language: {language_id})\nServer capabilities:\n{caps_summary}" )) } } @@ -132,15 +133,15 @@ impl Tool for LspDiagnostics { let uri = path_to_lsp_uri(&abs_path.to_string_lossy()); let manager = ctx.lsp_manager.lock() - .map_err(|e| anyhow!("LSP manager lock error: {}", e))?; + .map_err(|e| anyhow!("LSP manager lock error: {e}"))?; let language_id = manager.get_language_id(server_name) - .ok_or_else(|| anyhow!("LSP server '{}' not found. Use lsp_connect first.", server_name))?; + .ok_or_else(|| anyhow!("LSP server '{server_name}' not found. Use lsp_connect first."))?; let client_arc = manager.get_client(server_name) - .ok_or_else(|| anyhow!("LSP server '{}' not found", server_name))?; + .ok_or_else(|| anyhow!("LSP server '{server_name}' not found"))?; drop(manager); let mut client = client_arc.lock() - .map_err(|e| anyhow!("LSP client lock error: {}", e))?; + .map_err(|e| anyhow!("LSP client lock error: {e}"))?; match client.collect_diagnostics(&uri, &language_id, text) { Ok(diags) => { @@ -151,7 +152,7 @@ impl Tool for LspDiagnostics { let mut output = String::from("Diagnostics:\n"); for d in &diags_array { let range = d.get("range").and_then(|r| r.get("start")); - let severity = match d.get("severity").and_then(|s| s.as_i64()).unwrap_or(0) { + let severity = match d.get("severity").and_then(serde_json::Value::as_i64).unwrap_or(0) { 1 => "ERROR", 2 => "WARNING", 3 => "INFO", @@ -159,13 +160,13 @@ impl Tool for LspDiagnostics { _ => "NOTE", }; let message = d.get("message").and_then(|m| m.as_str()).unwrap_or("?"); - let line = range.and_then(|r| r.get("line")).and_then(|l| l.as_i64()).unwrap_or(0); - let col = range.and_then(|r| r.get("character")).and_then(|c| c.as_i64()).unwrap_or(0); + let line = range.and_then(|r| r.get("line")).and_then(serde_json::Value::as_i64).unwrap_or(0); + let col = range.and_then(|r| r.get("character")).and_then(serde_json::Value::as_i64).unwrap_or(0); let code = d.get("code") .and_then(|c| c.as_str().or_else(|| c.as_i64().map(|n| Box::leak(Box::new(n.to_string()))).map(|s| s.as_str()))) .unwrap_or(""); - let code_str = if code.is_empty() { String::new() } else { format!(" [{}]", code) }; - output.push_str(&format!(" {}:{}:{} - {}{}: {}\n", rel_path, line + 1, col, severity, code_str, message)); + let code_str = if code.is_empty() { String::new() } else { format!(" [{code}]") }; + writeln!(output, " {}:{}:{} - {}{}: {}", rel_path, line + 1, col, severity, code_str, message).unwrap(); } Ok(output) } @@ -226,10 +227,10 @@ impl Tool for LspHover { .and_then(|v| v.as_str()) .ok_or_else(|| anyhow!("missing required argument: path"))?; let line = args.get("line") - .and_then(|v| v.as_i64()) + .and_then(serde_json::Value::as_i64) .ok_or_else(|| anyhow!("missing required argument: line"))? as u32; let column = args.get("column") - .and_then(|v| v.as_i64()) + .and_then(serde_json::Value::as_i64) .ok_or_else(|| anyhow!("missing required argument: column"))? as u32; let server_name = resolve_server_name(ctx, args, rel_path)?; let server_name = server_name.as_str(); @@ -238,20 +239,20 @@ impl Tool for LspHover { let uri = path_to_lsp_uri(&abs_path.to_string_lossy()); let file_content = std::fs::read_to_string(&abs_path) - .map_err(|e| anyhow!("failed to read file '{}': {}", rel_path, e))?; + .map_err(|e| anyhow!("failed to read file '{rel_path}': {e}"))?; let manager = ctx.lsp_manager.lock() - .map_err(|e| anyhow!("LSP manager lock error: {}", e))?; + .map_err(|e| anyhow!("LSP manager lock error: {e}"))?; let language_id = manager.get_language_id(server_name) .unwrap_or_else(|| { args.get("language_id").and_then(|v| v.as_str()).unwrap_or("plaintext").to_string() }); let client_arc = manager.get_client(server_name) - .ok_or_else(|| anyhow!("LSP server '{}' not found. Use lsp_connect first.", server_name))?; + .ok_or_else(|| anyhow!("LSP server '{server_name}' not found. Use lsp_connect first."))?; drop(manager); let mut client = client_arc.lock() - .map_err(|e| anyhow!("LSP client lock error: {}", e))?; + .map_err(|e| anyhow!("LSP client lock error: {e}"))?; client.did_open(&uri, &language_id, 1, &file_content)?; let result = client.hover(&uri, line, column); @@ -267,9 +268,9 @@ impl Tool for LspHover { let mut output = String::new(); if let Some(range_val) = range { if let Some(start) = range_val.get("start") { - let rl = start.get("line").and_then(|l| l.as_i64()).unwrap_or(0); - let rc = start.get("character").and_then(|c| c.as_i64()).unwrap_or(0); - output.push_str(&format!("Range: {}:{}\n", rl + 1, rc + 1)); + let rl = start.get("line").and_then(serde_json::Value::as_i64).unwrap_or(0); + let rc = start.get("character").and_then(serde_json::Value::as_i64).unwrap_or(0); + writeln!(output, "Range: {}:{}", rl + 1, rc + 1).unwrap(); } } if let Some(contents_val) = contents { @@ -292,7 +293,7 @@ fn format_hover_contents(contents: &Value) -> String { } Value::Object(map) => { if let Some(kind) = map.get("kind").and_then(|k| k.as_str()) { - out.push_str(&format!("[{kind}] ")); + write!(out, "[{kind}] ").unwrap(); } if let Some(value) = map.get("value").and_then(|v| v.as_str()) { out.push_str(value); @@ -355,10 +356,10 @@ impl Tool for LspCompletion { .and_then(|v| v.as_str()) .ok_or_else(|| anyhow!("missing required argument: path"))?; let line = args.get("line") - .and_then(|v| v.as_i64()) + .and_then(serde_json::Value::as_i64) .ok_or_else(|| anyhow!("missing required argument: line"))? as u32; let column = args.get("column") - .and_then(|v| v.as_i64()) + .and_then(serde_json::Value::as_i64) .ok_or_else(|| anyhow!("missing required argument: column"))? as u32; let server_name = resolve_server_name(ctx, args, rel_path)?; let server_name = server_name.as_str(); @@ -367,18 +368,18 @@ impl Tool for LspCompletion { let uri = path_to_lsp_uri(&abs_path.to_string_lossy()); let file_content = std::fs::read_to_string(&abs_path) - .map_err(|e| anyhow!("failed to read file '{}': {}", rel_path, e))?; + .map_err(|e| anyhow!("failed to read file '{rel_path}': {e}"))?; let manager = ctx.lsp_manager.lock() - .map_err(|e| anyhow!("LSP manager lock error: {}", e))?; + .map_err(|e| anyhow!("LSP manager lock error: {e}"))?; let language_id = manager.get_language_id(server_name) .unwrap_or_else(|| "plaintext".to_string()); let client_arc = manager.get_client(server_name) - .ok_or_else(|| anyhow!("LSP server '{}' not found. Use lsp_connect first.", server_name))?; + .ok_or_else(|| anyhow!("LSP server '{server_name}' not found. Use lsp_connect first."))?; drop(manager); let mut client = client_arc.lock() - .map_err(|e| anyhow!("LSP client lock error: {}", e))?; + .map_err(|e| anyhow!("LSP client lock error: {e}"))?; client.did_open(&uri, &language_id, 1, &file_content)?; let result = client.completion(&uri, line, column); @@ -401,7 +402,7 @@ impl Tool for LspCompletion { let mut output = format!("{} completion suggestions at {}:{}:\n", items.len(), line + 1, column + 1); for (i, item) in items.iter().enumerate().take(50) { let label = item.get("label").and_then(|l| l.as_str()).unwrap_or("?"); - let kind = match item.get("kind").and_then(|k| k.as_i64()).unwrap_or(0) { + let kind = match item.get("kind").and_then(serde_json::Value::as_i64).unwrap_or(0) { 1 => "Text", 2 => "Method", 3 => "Function", @@ -430,11 +431,11 @@ impl Tool for LspCompletion { _ => "Other", }; let detail = item.get("detail").and_then(|d| d.as_str()).unwrap_or(""); - let detail_str = if detail.is_empty() { String::new() } else { format!(" - {}", detail) }; - output.push_str(&format!(" {}. [{}] {}{}\n", i + 1, kind, label, detail_str)); + let detail_str = if detail.is_empty() { String::new() } else { format!(" - {detail}") }; + writeln!(output, " {}. [{}] {}{}", i + 1, kind, label, detail_str).unwrap(); } if items.len() > 50 { - output.push_str(&format!(" ... and {} more\n", items.len() - 50)); + writeln!(output, " ... and {} more", items.len() - 50).unwrap(); } Ok(output) } @@ -485,10 +486,10 @@ impl Tool for LspDefinition { .and_then(|v| v.as_str()) .ok_or_else(|| anyhow!("missing required argument: path"))?; let line = args.get("line") - .and_then(|v| v.as_i64()) + .and_then(serde_json::Value::as_i64) .ok_or_else(|| anyhow!("missing required argument: line"))? as u32; let column = args.get("column") - .and_then(|v| v.as_i64()) + .and_then(serde_json::Value::as_i64) .ok_or_else(|| anyhow!("missing required argument: column"))? as u32; let server_name = resolve_server_name(ctx, args, rel_path)?; let server_name = server_name.as_str(); @@ -497,18 +498,18 @@ impl Tool for LspDefinition { let uri = path_to_lsp_uri(&abs_path.to_string_lossy()); let file_content = std::fs::read_to_string(&abs_path) - .map_err(|e| anyhow!("failed to read file '{}': {}", rel_path, e))?; + .map_err(|e| anyhow!("failed to read file '{rel_path}': {e}"))?; let manager = ctx.lsp_manager.lock() - .map_err(|e| anyhow!("LSP manager lock error: {}", e))?; + .map_err(|e| anyhow!("LSP manager lock error: {e}"))?; let language_id = manager.get_language_id(server_name) .unwrap_or_else(|| "plaintext".to_string()); let client_arc = manager.get_client(server_name) - .ok_or_else(|| anyhow!("LSP server '{}' not found. Use lsp_connect first.", server_name))?; + .ok_or_else(|| anyhow!("LSP server '{server_name}' not found. Use lsp_connect first."))?; drop(manager); let mut client = client_arc.lock() - .map_err(|e| anyhow!("LSP client lock error: {}", e))?; + .map_err(|e| anyhow!("LSP client lock error: {e}"))?; client.did_open(&uri, &language_id, 1, &file_content)?; let result = client.goto_definition(&uri, line, column); @@ -534,13 +535,13 @@ impl Tool for LspDefinition { let target_uri = loc.get("uri").and_then(|u| u.as_str()).unwrap_or("?"); let target_range = loc.get("range").or_else(|| loc.get("targetRange")); let target_start = target_range.and_then(|r| r.get("start")); - let tl = target_start.and_then(|s| s.get("line")).and_then(|l| l.as_i64()).unwrap_or(0); - let tc = target_start.and_then(|s| s.get("character")).and_then(|c| c.as_i64()).unwrap_or(0); + let tl = target_start.and_then(|s| s.get("line")).and_then(serde_json::Value::as_i64).unwrap_or(0); + let tc = target_start.and_then(|s| s.get("character")).and_then(serde_json::Value::as_i64).unwrap_or(0); let path_str = target_uri.strip_prefix("file://").unwrap_or(target_uri); - output.push_str(&format!(" {}. {}:{}:{}\n", i + 1, path_str, tl + 1, tc + 1)); + writeln!(output, " {}. {}:{}:{}", i + 1, path_str, tl + 1, tc + 1).unwrap(); } if locations.len() > 10 { - output.push_str(&format!(" ... and {} more locations\n", locations.len() - 10)); + writeln!(output, " ... and {} more", locations.len() - 10).unwrap(); } Ok(output) } @@ -591,10 +592,10 @@ impl Tool for LspReferences { .and_then(|v| v.as_str()) .ok_or_else(|| anyhow!("missing required argument: path"))?; let line = args.get("line") - .and_then(|v| v.as_i64()) + .and_then(serde_json::Value::as_i64) .ok_or_else(|| anyhow!("missing required argument: line"))? as u32; let column = args.get("column") - .and_then(|v| v.as_i64()) + .and_then(serde_json::Value::as_i64) .ok_or_else(|| anyhow!("missing required argument: column"))? as u32; let server_name = resolve_server_name(ctx, args, rel_path)?; let server_name = server_name.as_str(); @@ -603,18 +604,18 @@ impl Tool for LspReferences { let uri = path_to_lsp_uri(&abs_path.to_string_lossy()); let file_content = std::fs::read_to_string(&abs_path) - .map_err(|e| anyhow!("failed to read file '{}': {}", rel_path, e))?; + .map_err(|e| anyhow!("failed to read file '{rel_path}': {e}"))?; let manager = ctx.lsp_manager.lock() - .map_err(|e| anyhow!("LSP manager lock error: {}", e))?; + .map_err(|e| anyhow!("LSP manager lock error: {e}"))?; let language_id = manager.get_language_id(server_name) .unwrap_or_else(|| "plaintext".to_string()); let client_arc = manager.get_client(server_name) - .ok_or_else(|| anyhow!("LSP server '{}' not found. Use lsp_connect first.", server_name))?; + .ok_or_else(|| anyhow!("LSP server '{server_name}' not found. Use lsp_connect first."))?; drop(manager); let mut client = client_arc.lock() - .map_err(|e| anyhow!("LSP client lock error: {}", e))?; + .map_err(|e| anyhow!("LSP client lock error: {e}"))?; client.did_open(&uri, &language_id, 1, &file_content)?; let result = client.references(&uri, line, column); @@ -631,13 +632,13 @@ impl Tool for LspReferences { for (i, loc) in locations.iter().enumerate().take(50) { let target_uri = loc.get("uri").and_then(|u| u.as_str()).unwrap_or("?"); let range = loc.get("range").and_then(|r| r.get("start")); - let rl = range.and_then(|s| s.get("line")).and_then(|l| l.as_i64()).unwrap_or(0); - let rc = range.and_then(|s| s.get("character")).and_then(|c| c.as_i64()).unwrap_or(0); + let rl = range.and_then(|s| s.get("line")).and_then(serde_json::Value::as_i64).unwrap_or(0); + let rc = range.and_then(|s| s.get("character")).and_then(serde_json::Value::as_i64).unwrap_or(0); let path_str = target_uri.strip_prefix("file://").unwrap_or(target_uri); - output.push_str(&format!(" {}. {}:{}:{}\n", i + 1, path_str, rl + 1, rc + 1)); + writeln!(output, " {}. {}:{}:{}", i + 1, path_str, rl + 1, rc + 1).unwrap(); } if locations.len() > 50 { - output.push_str(&format!(" ... and {} more references\n", locations.len() - 50)); + writeln!(output, " ... and {} more references", locations.len() - 50).unwrap(); } Ok(output) } @@ -676,12 +677,12 @@ impl Tool for LspDisconnect { .ok_or_else(|| anyhow!("missing required argument: name"))?; let mut manager = ctx.lsp_manager.lock() - .map_err(|e| anyhow!("LSP manager lock error: {}", e))?; + .map_err(|e| anyhow!("LSP manager lock error: {e}"))?; if manager.disconnect(name) { - Ok(format!("Disconnected from LSP server '{}'", name)) + Ok(format!("Disconnected from LSP server '{name}'")) } else { - Err(anyhow!("LSP server '{}' not found", name)) + Err(anyhow!("LSP server '{name}' not found")) } } } @@ -718,7 +719,7 @@ fn known_extensions_for(language_id: &str) -> &[&'static str] { /// connected server's language is known to use that extension. fn auto_detect_server(ctx: &ToolCtx, path: &str) -> Option { let ext = std::path::Path::new(path).extension().and_then(|e| e.to_str())?; - let dot_ext = format!(".{}", ext); + let dot_ext = format!(".{ext}"); if let Ok(mgr) = ctx.lsp_manager.lock() { for s in &mgr.servers { let exts = known_extensions_for(&s.language_id); @@ -754,15 +755,13 @@ fn resolve_server_name(ctx: &ToolCtx, args: &Value, path: &str) -> Result".to_string()); + .and_then(|e| e.to_str()).map_or_else(|| "".to_string(), |e| format!(".{e}")); let available = ctx.lsp_manager.lock().ok() .map(|mgr| { mgr.list_servers() .iter() - .map(|(name, lang, _)| format!("{} ({})", name, lang)) + .map(|(name, lang, _)| format!("{name} ({lang})")) .collect::>() .join(", ") }) @@ -770,8 +769,6 @@ fn resolve_server_name(ctx: &ToolCtx, args: &Value, path: &str) -> Result Result { if let Some(name) = args.get("name").and_then(|v| v.as_str()) { if name.is_empty() { - return list_all(ctx); + return Ok(list_all(ctx)); } let memory = Memory::read(&ctx.memory_dir, name) - .map_err(|e| anyhow!("memory '{}' not found: {}", name, e))?; + .map_err(|e| anyhow!("memory '{name}' not found: {e}"))?; Ok(format!( "---\nname: {}\ndescription: {}\nkind: {}\nlifecycle: {}\n---\n\n{}", memory.name, @@ -52,7 +53,7 @@ impl Tool for Recall { memory.content, )) } else { - list_all(ctx) + Ok(list_all(ctx)) } } } @@ -63,18 +64,18 @@ impl Tool for Recall { /// fall back to bare name if the file can't be parsed. /// /// Return: `Ok` with the formatted index (never fails; missing dir yields "(no memory entries)"). -fn list_all(ctx: &ToolCtx) -> Result { +fn list_all(ctx: &ToolCtx) -> String { let names = Memory::list(&ctx.memory_dir); if names.is_empty() { - return Ok("(no memory entries)".to_string()); + return "(no memory entries)".to_string(); } - let mut lines = format!("Memory index ({} entries):\n", names.len()); + let mut lines = String::new(); for name in &names { if let Ok(mem) = Memory::read(&ctx.memory_dir, name) { - lines.push_str(&format!("- {} [{}]: {}\n", name, mem.kind, mem.description)); + let _ = writeln!(lines, "- {} [{}]: {}", name, mem.kind, mem.description); } else { - lines.push_str(&format!("- {}\n", name)); + let _ = writeln!(lines, "- {name}"); } } - Ok(lines) + lines } diff --git a/src/tool/memory/remember.rs b/src/tool/memory/remember.rs index fbc3474..0ab69e3 100644 --- a/src/tool/memory/remember.rs +++ b/src/tool/memory/remember.rs @@ -89,8 +89,8 @@ impl Tool for Remember { }; memory.write(&ctx.memory_dir) - .map_err(|e| anyhow!("failed to write memory '{}': {}", name, e))?; + .map_err(|e| anyhow!("failed to write memory '{name}': {e}"))?; - Ok(format!("saved memory '{}' ({})", name, kind)) + Ok(format!("saved memory '{name}' ({kind})")) } } diff --git a/src/tool/mod.rs b/src/tool/mod.rs index b6274e3..e43b405 100644 --- a/src/tool/mod.rs +++ b/src/tool/mod.rs @@ -228,7 +228,7 @@ pub fn resolve_path(workspaces: &[PathBuf], rel: &str) -> Result { } else { (0, rel) }; - let base = workspaces.get(ws_idx).ok_or_else(|| anyhow::anyhow!("workspace index {} out of range", ws_idx))?; + let base = workspaces.get(ws_idx).ok_or_else(|| anyhow::anyhow!("workspace index {ws_idx} out of range"))?; let abs = if path.is_empty() { base.clone() } else { @@ -239,32 +239,27 @@ pub fn resolve_path(workspaces: &[PathBuf], rel: &str) -> Result { // workspace root first and then resolve parent-dir (`../`) traversal // component-by-component so that `Path::starts_with` cannot be // bypassed by unnormalised intermediate segments. - let canon = match abs.canonicalize() { - Ok(c) => c, - Err(_) => { - let base_canon = workspaces - .iter() - .filter_map(|w| w.canonicalize().ok()) - .next() - .unwrap_or_else(|| base.clone()); - let mut resolved = base_canon.clone(); - if let Ok(rel_components) = abs.strip_prefix(&base_canon) { - for comp in rel_components.components() { - match comp { - std::path::Component::ParentDir => { - resolved.pop(); - } - std::path::Component::CurDir => {} - c => resolved.push(c), + let canon = if let Ok(c) = abs.canonicalize() { c } else { + let base_canon = workspaces + .iter().find_map(|w| w.canonicalize().ok()) + .unwrap_or_else(|| base.clone()); + let mut resolved = base_canon.clone(); + if let Ok(rel_components) = abs.strip_prefix(&base_canon) { + for comp in rel_components.components() { + match comp { + std::path::Component::ParentDir => { + resolved.pop(); } + std::path::Component::CurDir => {} + c => resolved.push(c), } } - resolved } + resolved }; if workspaces.iter().any(|w| canon.starts_with(w)) { Ok(canon) } else { - anyhow::bail!("path '{}' is outside all workspace roots", rel) + anyhow::bail!("path '{rel}' is outside all workspace roots") } } diff --git a/src/tool/search.rs b/src/tool/search.rs index 096572d..e4942c6 100644 --- a/src/tool/search.rs +++ b/src/tool/search.rs @@ -59,10 +59,10 @@ impl Tool for Grep { .to_string(); let path = resolve_path(&ctx.workspaces, &rel)?; if !path.exists() { - anyhow::bail!("path '{}' does not exist", rel); + anyhow::bail!("path '{rel}' does not exist"); } if !path.is_dir() { - anyhow::bail!("path '{}' is not a directory", rel); + anyhow::bail!("path '{rel}' is not a directory"); } let mut results: Vec<(String, usize, String)> = Vec::new(); for entry in Walk::new(&path).flatten() { @@ -83,10 +83,10 @@ impl Tool for Grep { } } if results.is_empty() { - return Ok(format!("no matches found for '{}' in {}", pattern, rel)); + return Ok(format!("no matches found for '{pattern}' in {rel}")); } let output = results.iter() - .map(|(f, line, text)| format!("{}:{}:{}", f, line, text)) + .map(|(f, line, text)| format!("{f}:{line}:{text}")) .collect::>() .join("\n"); Ok(format!("found {} matches:\n{}", results.len(), output)) @@ -144,14 +144,14 @@ impl Tool for Glob { .to_string(); let root = resolve_path(&ctx.workspaces, &rel)?; if !root.exists() || !root.is_dir() { - anyhow::bail!("path '{}' is not a valid directory", rel); + anyhow::bail!("path '{rel}' is not a valid directory"); } let mut builder = GlobSetBuilder::new(); let full_pattern = root.join(&pat_str).display().to_string(); builder.add(GlobBuilder::new(&full_pattern).build() - .map_err(|e| anyhow!("invalid glob pattern '{}': {}", pat_str, e))?); + .map_err(|e| anyhow!("invalid glob pattern '{pat_str}': {e}"))?); let glob_set = builder.build() - .map_err(|e| anyhow!("failed to build glob set: {}", e))?; + .map_err(|e| anyhow!("failed to build glob set: {e}"))?; let mut matches: Vec = Vec::new(); for entry in Walk::new(&root).flatten() { let p = entry.path(); @@ -165,7 +165,7 @@ impl Tool for Glob { } matches.sort(); if matches.is_empty() { - return Ok(format!("no files match '{}' in {}", pat_str, rel)); + return Ok(format!("no files match '{pat_str}' in {rel}")); } Ok(matches.join("\n")) } diff --git a/src/tool/shell.rs b/src/tool/shell.rs index 3936f2d..74d24b9 100644 --- a/src/tool/shell.rs +++ b/src/tool/shell.rs @@ -63,13 +63,13 @@ impl Tool for Bash { .and_then(|v| v.as_str()) .ok_or_else(|| anyhow!("missing required argument: command"))? .to_string(); - let timeout_ms = args.get("timeout").and_then(|v| v.as_u64()).unwrap_or(120_000).min(600_000); + let timeout_ms = args.get("timeout").and_then(serde_json::Value::as_u64).unwrap_or(120_000).min(600_000); // Only gate destructive git operations; credential reads are allowed // locally since the AI needs access, and the real threat is committing // secrets to a public repo (handled by git pre-commit hooks / user). super::shell_filter::git::check_git_destructive(&cmd) - .map_err(|e| anyhow!("blocked: {}", e))?; - let run_in_background = args.get("run_in_background").and_then(|v| v.as_bool()).unwrap_or(false); + .map_err(|e| anyhow!("blocked: {e}"))?; + let run_in_background = args.get("run_in_background").and_then(serde_json::Value::as_bool).unwrap_or(false); if run_in_background { let job = crate::app::bgbash::job::spawn_bash_job(cmd); return Ok(format!("Background job: {}", job.id)); @@ -80,7 +80,7 @@ impl Tool for Bash { .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .spawn() - .map_err(|e| anyhow!("failed to spawn bash: {}", e))?; + .map_err(|e| anyhow!("failed to spawn bash: {e}"))?; let start = std::time::Instant::now(); let timeout = Duration::from_millis(timeout_ms); loop { @@ -88,16 +88,16 @@ impl Tool for Bash { Ok(Some(status)) => { let elapsed = start.elapsed().as_secs_f64(); let output = child.wait_with_output() - .map_err(|e| anyhow!("failed to collect output: {}", e))?; + .map_err(|e| anyhow!("failed to collect output: {e}"))?; let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - let combined = if stderr.is_empty() { stdout } else { format!("{}\n{}", stdout, stderr) }; + let combined = if stderr.is_empty() { stdout } else { format!("{stdout}\n{stderr}") }; let trimmed = combined.trim().to_string(); if status.success() { return Ok(if trimmed.is_empty() { - format!("Command completed in {:.2}s (exit code 0)", elapsed) + format!("Command completed in {elapsed:.2}s (exit code 0)") } else { - format!("{}\n\nExit code: 0 ({:.2}s)", trimmed, elapsed) + format!("{trimmed}\n\nExit code: 0 ({elapsed:.2}s)") }); } return Ok(format!("{}\n\nExit code: {} ({:.2}s)", trimmed, status.code().unwrap_or(-1), elapsed)); @@ -106,12 +106,12 @@ impl Tool for Bash { if start.elapsed() > timeout { let _ = child.kill(); let _ = child.wait(); - anyhow::bail!("command timed out after {}ms", timeout_ms); + anyhow::bail!("command timed out after {timeout_ms}ms"); } std::thread::sleep(Duration::from_millis(10)); } Err(e) => { - anyhow::bail!("failed to wait for command: {}", e); + anyhow::bail!("failed to wait for command: {e}"); } } } diff --git a/src/tool/shell_filter/git.rs b/src/tool/shell_filter/git.rs index 6b0b006..d8c9d83 100644 --- a/src/tool/shell_filter/git.rs +++ b/src/tool/shell_filter/git.rs @@ -56,7 +56,7 @@ pub fn check_git_destructive(cmd: &str) -> Result<()> { let cmd_normalized = super::normalize_ansi_c_quoting(&cmd_no_quotes); for pattern in &patterns { if cmd_lower.contains(pattern) || cmd_no_quotes.contains(pattern) || cmd_normalized.contains(pattern) { - anyhow::bail!("destructive git operation blocked: '{}'", pattern); + anyhow::bail!("destructive git operation blocked: '{pattern}'"); } } // Additional check: any `+` prefixed refspec in a `git push` is a @@ -69,7 +69,7 @@ pub fn check_git_destructive(cmd: &str) -> Result<()> { &cmd_no_quotes }; if check_push.contains("push") { - let push_end = cmd_no_quotes.find("push").map(|i| i + 4).unwrap_or(0); + let push_end = cmd_no_quotes.find("push").map_or(0, |i| i + 4); let after_push = &cmd_no_quotes[push_end..]; if after_push.contains('+') { anyhow::bail!("destructive git operation blocked: force push via +refspec"); diff --git a/src/tool/shell_filter/mod.rs b/src/tool/shell_filter/mod.rs index af87383..98dd8c7 100644 --- a/src/tool/shell_filter/mod.rs +++ b/src/tool/shell_filter/mod.rs @@ -34,7 +34,7 @@ pub(crate) fn normalize_ansi_c_quoting(input: &str) -> String { Some('\'') => decoded.push('\''), Some('x' | 'X') => { // \xHH — hex escape (2 hex digits) - let hex: String = chars.by_ref().take(2).take_while(|c| c.is_ascii_hexdigit()).collect(); + let hex: String = chars.by_ref().take(2).take_while(char::is_ascii_hexdigit).collect(); if hex.len() == 2 { if let Ok(byte) = u8::from_str_radix(&hex, 16) { decoded.push(byte as char); @@ -47,7 +47,7 @@ pub(crate) fn normalize_ansi_c_quoting(input: &str) -> String { } Some('u') => { // \uNNNN — unicode escape (4 hex digits) - let hex: String = chars.by_ref().take(4).take_while(|c| c.is_ascii_hexdigit()).collect(); + let hex: String = chars.by_ref().take(4).take_while(char::is_ascii_hexdigit).collect(); if hex.len() == 4 { if let Ok(code) = u32::from_str_radix(&hex, 16) { if let Some(c) = char::from_u32(code) { diff --git a/src/tool/spawn.rs b/src/tool/spawn.rs index c16fd0d..4dd5618 100644 --- a/src/tool/spawn.rs +++ b/src/tool/spawn.rs @@ -51,12 +51,13 @@ impl Tool for SpawnAgents { }) } - fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result { + fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { + use std::sync::{Arc, Mutex}; let agents: Vec = args.get("agents") .and_then(|v| v.as_array()) .ok_or_else(|| anyhow!("missing required argument: agents"))? .iter() - .filter_map(|v| v.as_str().map(|s| s.to_string())) + .filter_map(|v| v.as_str().map(std::string::ToString::to_string)) .collect(); if agents.is_empty() { @@ -67,9 +68,8 @@ impl Tool for SpawnAgents { } let max_concurrency = args.get("max_concurrency") - .and_then(|v| v.as_u64()) - .map(|v| v.min(10) as usize) - .unwrap_or(10); + .and_then(serde_json::Value::as_u64) + .map_or(10, |v| v.min(10) as usize); let agent_count = agents.len(); let primitives: Vec = agents @@ -78,8 +78,8 @@ impl Tool for SpawnAgents { .collect(); let wf = WorkflowScript { - name: format!("parallel-{}-agents", agent_count), - description: format!("Auto-spawned parallel workflow with {} agents", agent_count), + name: format!("parallel-{agent_count}-agents"), + description: format!("Auto-spawned parallel workflow with {agent_count} agents"), script: ScriptPrimitive::Parallel(primitives), options: ScriptOptions { max_concurrency, @@ -88,8 +88,7 @@ impl Tool for SpawnAgents { }, }; - use std::sync::{Arc, Mutex}; - let live: Option = _ctx.turn_events.as_ref().map(|turn_events| { + let live: Option = ctx.turn_events.as_ref().map(|turn_events| { let turn_events = turn_events.clone(); let f: crate::app::workflow::engine::LiveStateFn = Arc::new(move |agent_id: String, agent_name: String, status| { if let Ok(mut q) = turn_events.lock() { @@ -113,12 +112,12 @@ impl Tool for SpawnAgents { max_concurrency, true, live.as_ref(), - &_ctx.session_dir, - &_ctx.workspaces, + &ctx.session_dir, + &ctx.workspaces, &findings, None, // no per-agent timeout for spawn_agents )?; - format_results(results, "parallel") + Ok(format_results(&results, "parallel")) } } @@ -150,12 +149,13 @@ impl Tool for SpawnPipeline { }) } - fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result { + fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { + use std::sync::{Arc, Mutex}; let stages: Vec = args.get("stages") .and_then(|v| v.as_array()) .ok_or_else(|| anyhow!("missing required argument: stages"))? .iter() - .filter_map(|v| v.as_str().map(|s| s.to_string())) + .filter_map(|v| v.as_str().map(std::string::ToString::to_string)) .collect(); if stages.is_empty() { @@ -178,8 +178,7 @@ impl Tool for SpawnPipeline { }, }; - use std::sync::{Arc, Mutex}; - let live: Option = _ctx.turn_events.as_ref().map(|turn_events| { + let live: Option = ctx.turn_events.as_ref().map(|turn_events| { let turn_events = turn_events.clone(); let f: crate::app::workflow::engine::LiveStateFn = Arc::new(move |agent_id: String, agent_name: String, status| { if let Ok(mut q) = turn_events.lock() { @@ -202,24 +201,24 @@ impl Tool for SpawnPipeline { 1, false, live.as_ref(), - &_ctx.session_dir, - &_ctx.workspaces, + &ctx.session_dir, + &ctx.workspaces, &findings, None, // no per-agent timeout for spawn_pipeline )?; - format_results(results, "pipeline") + Ok(format_results(&results, "pipeline")) } } /// Format a list of agent results into a readable summary string. -fn format_results(results: Vec, mode: &str) -> Result { +fn format_results(results: &[String], mode: &str) -> String { if results.is_empty() { - return Ok(format!("{} workflow completed with no output", mode)); + return format!("{mode} workflow completed with no output"); } let formatted: Vec = results .iter() .enumerate() .map(|(i, r)| format!("=== Agent {} ===\n{}", i + 1, r.trim())) .collect(); - Ok(formatted.join("\n\n")) + formatted.join("\n\n") } diff --git a/src/tool/utility/dir_cache_update.rs b/src/tool/utility/dir_cache_update.rs index fdaee64..19e6841 100644 --- a/src/tool/utility/dir_cache_update.rs +++ b/src/tool/utility/dir_cache_update.rs @@ -69,19 +69,19 @@ impl Tool for DirCacheUpdate { // Create a one-shot runtime so this tool works from any thread (the // agent turn runs on a std::thread that has no tokio context). let rt = tokio::runtime::Runtime::new() - .map_err(|e| anyhow!("failed to create temp runtime: {}", e))?; + .map_err(|e| anyhow!("failed to create temp runtime: {e}"))?; rt.block_on(async { let cache = dc.write().await; cache.set(entries).await; }); - Ok(format!("cached {} entries for {}", count, rel)) + Ok(format!("cached {count} entries for {rel}")) } } /// Non-recursively list the immediate entries of `path`. /// -/// Flow: read_dir → flatten Ok entries → collect their paths. +/// Flow: `read_dir` → flatten Ok entries → collect their paths. /// /// Why: silently skips unreadable entries (e.g. permission errors) /// rather than failing the whole cache update. diff --git a/src/tool/utility/dir_list.rs b/src/tool/utility/dir_list.rs index a365063..8dd6354 100644 --- a/src/tool/utility/dir_list.rs +++ b/src/tool/utility/dir_list.rs @@ -67,13 +67,13 @@ impl Tool for DirList { } let entries: Vec = fs::read_dir(&path) - .map_err(|e| anyhow!("failed to read directory '{}': {}", rel, e))? - .filter_map(|e| e.ok()) + .map_err(|e| anyhow!("failed to read directory '{rel}': {e}"))? + .filter_map(std::result::Result::ok) .map(|e| { let name = e.file_name().to_string_lossy().to_string(); - let is_dir = e.file_type().map(|t| t.is_dir()).unwrap_or(false); + let is_dir = e.file_type().is_ok_and(|t| t.is_dir()); if is_dir { - format!("{}/", name) + format!("{name}/") } else { name } diff --git a/src/tool/utility/pong.rs b/src/tool/utility/pong.rs index 9538b5d..6f0b4c5 100644 --- a/src/tool/utility/pong.rs +++ b/src/tool/utility/pong.rs @@ -39,6 +39,6 @@ impl Tool for Pong { let msg = args.get("message") .and_then(|v| v.as_str()) .unwrap_or("pong"); - Ok(format!("pong: {}", msg)) + Ok(format!("pong: {msg}")) } } diff --git a/src/tool/utility/todofinish.rs b/src/tool/utility/todofinish.rs index 51c8290..31481f6 100644 --- a/src/tool/utility/todofinish.rs +++ b/src/tool/utility/todofinish.rs @@ -36,9 +36,9 @@ impl Tool for Todofinish { } let content = std::fs::read_to_string(&path) - .map_err(|e| anyhow!("failed to read todo.md: {}", e))?; + .map_err(|e| anyhow!("failed to read todo.md: {e}"))?; - let task_index = args.get("task_index").and_then(|v| v.as_i64()); + let task_index = args.get("task_index").and_then(serde_json::Value::as_i64); let mut new_content = String::new(); let mut task_count = 0; @@ -70,10 +70,10 @@ impl Tool for Todofinish { } std::fs::write(&path, new_content) - .map_err(|e| anyhow!("failed to write to todo.md: {}", e))?; + .map_err(|e| anyhow!("failed to write to todo.md: {e}"))?; if let Some(idx) = task_index { - Ok(format!("Successfully marked task {} as finished.", idx)) + Ok(format!("Successfully marked task {idx} as finished.")) } else { Ok("Successfully marked ALL tasks as finished.".to_string()) } diff --git a/src/tool/utility/todowrite.rs b/src/tool/utility/todowrite.rs index a3668cc..0e05400 100644 --- a/src/tool/utility/todowrite.rs +++ b/src/tool/utility/todowrite.rs @@ -59,17 +59,17 @@ impl Tool for Todowrite { let path: PathBuf = ctx.session_dir.join("todo.md"); let now = chrono::Utc::now(); let timestamp = now.format("%Y-%m-%d %H:%M:%S"); - let line = format!("- [ ] {} ({})\n", task, timestamp); + let line = format!("- [ ] {task} ({timestamp})\n"); fs::OpenOptions::new() .create(true) .append(true) .open(&path) - .map_err(|e| anyhow!("failed to open todo.md: {}", e))? + .map_err(|e| anyhow!("failed to open todo.md: {e}"))? .write_all(line.as_bytes()) - .map_err(|e| anyhow!("failed to write to todo.md: {}", e))?; + .map_err(|e| anyhow!("failed to write to todo.md: {e}"))?; - Ok(format!("added task to todo.md: {}", task)) + Ok(format!("added task to todo.md: {task}")) } } diff --git a/src/tool/workflow.rs b/src/tool/workflow.rs index b915d39..9505111 100644 --- a/src/tool/workflow.rs +++ b/src/tool/workflow.rs @@ -58,14 +58,14 @@ impl Tool for WorkflowRun { /// /// Return: the workflow engine's output string, or an error if the /// script argument is missing or fails to parse as JSON. - fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result { + fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let script_str = args.get("script") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow!("missing required argument: script"))?; let workflow_script: crate::app::workflow::script::WorkflowScript = serde_json::from_str(script_str) - .map_err(|e| anyhow!("failed to parse workflow script: {}", e))?; + .map_err(|e| anyhow!("failed to parse workflow script: {e}"))?; let workflow_args: std::collections::HashMap = args.get("args") .and_then(|v| v.as_object()) @@ -77,7 +77,7 @@ impl Tool for WorkflowRun { .unwrap_or_default(); crate::app::workflow::engine::run_workflow( - &workflow_script, &workflow_args, &_ctx.session_dir, &_ctx.workspaces, + &workflow_script, &workflow_args, &ctx.session_dir, &ctx.workspaces, ) } } @@ -177,7 +177,7 @@ impl Tool for CompanyPipeline { }) } - fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result { + fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let request = args.get("request") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow!("missing required argument: request"))?; @@ -190,17 +190,17 @@ impl Tool for CompanyPipeline { "quick" => { crate::app::workflow::company::run_company_pipeline_quick( request, - &_ctx.session_dir, - &_ctx.workspaces, - _ctx.turn_events.as_ref(), + &ctx.session_dir, + &ctx.workspaces, + ctx.turn_events.as_ref(), ) } _ => { crate::app::workflow::company::run_company_pipeline( request, - &_ctx.session_dir, - &_ctx.workspaces, - _ctx.turn_events.as_ref(), + &ctx.session_dir, + &ctx.workspaces, + ctx.turn_events.as_ref(), ) } } diff --git a/src/view/chat.rs b/src/view/chat.rs index 359ceff..45f763d 100644 --- a/src/view/chat.rs +++ b/src/view/chat.rs @@ -1,3 +1,4 @@ +#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)] //! Chat transcript panel rendering — message cards with role badges. //! //! Flow: `draw_chat` turns `state.transcript_cache.messages` into a @@ -19,7 +20,7 @@ use ratatui::Frame; use super::theme::Theme; /// Break a flat run of styled spans into `Line`s at embedded `\n` boundaries. -fn split_spans_into_lines<'a>(spans: Vec>) -> Vec> { +fn split_spans_into_lines(spans: Vec>) -> Vec> { let mut lines = Vec::new(); let mut current_spans = Vec::new(); @@ -76,10 +77,11 @@ fn format_timestamp(ts: i64) -> String { let secs = ts / 1000; let mins = (secs / 60) % 60; let hrs = (secs / 3600) % 24; - format!("{:02}:{:02}", hrs, mins) + format!("{hrs:02}:{mins:02}") } /// Render the scrollable chat transcript panel with message card styling. +#[allow(clippy::too_many_lines)] pub fn draw_chat(frame: &mut Frame, area: Rect, state: &crate::app::state::rest::AppStateRest) { let messages = &state.transcript_cache.messages; let scroll_offset = state.scroll.offset; @@ -95,7 +97,7 @@ pub fn draw_chat(frame: &mut Frame, area: Rect, state: &crate::app::state::rest: }; // ── Render messages as cards ───────────────────────────────────────── - for (_msg_idx, msg) in messages.iter().enumerate() { + for msg in messages { let accent = role_accent_color(&msg.role); let badge = role_badge(&msg.role); let label = role_label(&msg.role); @@ -119,12 +121,12 @@ pub fn draw_chat(frame: &mut Frame, area: Rect, state: &crate::app::state::rest: ), // Role name Span::styled( - format!(" {}", label), + format!(" {label}"), Style::default().fg(accent).add_modifier(Modifier::BOLD), ), // Timestamp Span::styled( - if ts_str.is_empty() { String::new() } else { format!(" {}", ts_str) }, + if ts_str.is_empty() { String::new() } else { format!(" {ts_str}") }, Style::default().fg(Theme::TEXT_DIM), ), ]); @@ -176,7 +178,7 @@ pub fn draw_chat(frame: &mut Frame, area: Rect, state: &crate::app::state::rest: .add_modifier(Modifier::BOLD), ), Span::styled( - format!(" {} ", spinner), + format!(" {spinner} "), Style::default().fg(Theme::ROLE_ASSISTANT).add_modifier(Modifier::BOLD), ), Span::styled( diff --git a/src/view/markdown.rs b/src/view/markdown.rs index 8359d0d..a652a08 100644 --- a/src/view/markdown.rs +++ b/src/view/markdown.rs @@ -14,12 +14,13 @@ use super::theme::Theme; /// Render a markdown string into styled terminal spans, word-wrapped to `width`. /// -/// Flow: pulldown_cmark parses `text` into an event stream → each +/// Flow: `pulldown_cmark` parses `text` into an event stream → each /// Start/End/Text/Code/Break event is translated into styled `Span`s → /// if `width > 0`, a second pass wraps long lines. /// /// Return: a flat vec of styled spans; `chat::split_spans_into_lines` /// turns it back into `Line`s for the Paragraph widget. +#[allow(clippy::too_many_lines)] pub fn render_markdown(text: &str, width: u16) -> Vec> { let mut spans = Vec::new(); let parser = pulldown_cmark::Parser::new(text); @@ -61,9 +62,6 @@ pub fn render_markdown(text: &str, width: u16) -> Vec> { pulldown_cmark::Tag::Paragraph => { first_in_paragraph = true; } - pulldown_cmark::Tag::Emphasis => {} - pulldown_cmark::Tag::Strong => {} - pulldown_cmark::Tag::List(_) => {} pulldown_cmark::Tag::Item => { // List item bullet spans.push(Span::styled( @@ -79,7 +77,7 @@ pub fn render_markdown(text: &str, width: u16) -> Vec> { // We push the URL as a tooltip-like suffix // After the link text ends, we'll add the URL spans.push(Span::styled( - format!("]({})", dest_url), + format!("]({dest_url})"), Style::default().fg(Theme::TEXT_MUTED).add_modifier(Modifier::ITALIC), )); } @@ -111,14 +109,7 @@ pub fn render_markdown(text: &str, width: u16) -> Vec> { first_in_paragraph = true; spans.push(Span::raw("\n\n")); } - pulldown_cmark::TagEnd::Emphasis => {} - pulldown_cmark::TagEnd::Strong => {} - pulldown_cmark::TagEnd::List(_) => {} - pulldown_cmark::TagEnd::Item => { - spans.push(Span::raw("\n")); - } - pulldown_cmark::TagEnd::Link => {} - pulldown_cmark::TagEnd::BlockQuote(_) => { + pulldown_cmark::TagEnd::Item | pulldown_cmark::TagEnd::BlockQuote(_) => { spans.push(Span::raw("\n")); } _ => {} @@ -128,7 +119,7 @@ pub fn render_markdown(text: &str, width: u16) -> Vec> { let s = text.to_string(); if in_code_block { spans.push(Span::styled( - format!(" {}", s), + format!(" {s}"), Style::default().fg(Theme::ACCENT_TEAL).bg(Theme::CODE_BG), )); } else if in_heading { @@ -138,14 +129,9 @@ pub fn render_markdown(text: &str, width: u16) -> Vec> { 3 => Theme::ACCENT_PURPLE, _ => Theme::TEXT, }; - let prefix = match heading_level { - 1 => " ", - 2 => " ", - 3 => " ", - _ => " ", - }; + let prefix = " "; spans.push(Span::styled( - format!("{}{}", prefix, s), + format!("{prefix}{s}"), Style::default().fg(color).add_modifier(Modifier::BOLD), )); } else { @@ -160,7 +146,7 @@ pub fn render_markdown(text: &str, width: u16) -> Vec> { pulldown_cmark::Event::Code(text) => { // Inline code with background spans.push(Span::styled( - format!(" {} ", text), + format!(" {text} "), Style::default() .fg(Theme::ACCENT_TEAL) .bg(Theme::CODE_BAR) @@ -195,10 +181,10 @@ pub fn render_markdown(text: &str, width: u16) -> Vec> { spans_out.push(Span::styled(text_str.to_string(), style)); - if !text_str.contains('\n') { - line_len += remaining; - } else { + if text_str.contains('\n') { line_len = text_str.split('\n').next_back().unwrap_or("").len(); + } else { + line_len += remaining; } } spans = spans_out; diff --git a/src/view/mod.rs b/src/view/mod.rs index ffe49ca..5851d20 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -1,3 +1,4 @@ +#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)] //! Top-level TUI render pipeline: layouts the terminal into chat / input //! / status regions, dispatches overlay rendering with glassmorphism-style //! centered panels, and floats toast notifications over the top-right corner. @@ -124,6 +125,7 @@ fn render_main_panel( /// - A top accent border strip (colored per variant) /// - A title line with icon /// - Content area with proper spacing +#[allow(clippy::too_many_lines)] fn render_overlay( frame: &mut Frame, area: Rect, @@ -172,12 +174,12 @@ fn render_overlay( )), Line::from(Span::styled( format!(" Max tokens: {}", - state.settings.max_tokens.map(|v| v.to_string()).unwrap_or_else(|| "auto".to_string())), + state.settings.max_tokens.map_or_else(|| "auto".to_string(), |v| v.to_string())), Style::default().fg(Theme::TEXT), )), Line::from(Span::styled( format!(" Temperature: {}", - state.settings.temperature.map(|v| format!("{:.1}", v)).unwrap_or_else(|| "auto".to_string())), + state.settings.temperature.map_or_else(|| "auto".to_string(), |v| format!("{v:.1}"))), Style::default().fg(Theme::TEXT), )), Line::from(Span::styled( @@ -261,11 +263,11 @@ fn render_overlay( input_text.as_str() } }; - let masked = if !input_text.is_empty() { - let suffix = if input_text.len() > 8 { "****" } else { "" }; - format!("{}{}", display, suffix) - } else { + let masked = if input_text.is_empty() { display.to_string() + } else { + let suffix = if input_text.len() > 8 { "****" } else { "" }; + format!("{display}{suffix}") }; let lines = vec![ Line::from(Span::styled( @@ -329,9 +331,9 @@ fn render_overlay( let selected = i == current_idx; lines.push(Line::from(Span::styled( if selected { - format!(" ▸ {} (active)", l) + format!(" ▸ {l} (active)") } else { - format!(" {}", l) + format!(" {l}") }, if selected { Style::default().fg(Theme::HIGHLIGHT).add_modifier(Modifier::BOLD) @@ -386,7 +388,7 @@ fn render_overlay( )), Line::from(Span::raw("")), Line::from(Span::styled( - format!(" Messages: {}", msg_count), + format!(" Messages: {msg_count}"), Style::default().fg(Theme::INFO), )), Line::from(Span::styled( @@ -427,7 +429,7 @@ fn render_overlay( }; let preview: String = msg.content.chars().take(70).collect(); lines.push(Line::from(Span::styled( - format!(" [{}] {}", role_str, preview), + format!(" [{role_str}] {preview}"), Style::default().fg( if matches!(msg.role, crate::dto::chat::message::Role::User) { Theme::INFO @@ -484,7 +486,7 @@ fn render_overlay( let (label, style) = match item { crate::app::mode::learning::LearningItem::Pending { name, .. } => { ( - format!("{}[Pending] {}", prefix, name), + format!("{prefix}[Pending] {name}"), if is_selected { Style::default().fg(Theme::WARNING).bg(Theme::HIGHLIGHT_DIM) .add_modifier(Modifier::BOLD) @@ -496,7 +498,7 @@ fn render_overlay( crate::app::mode::learning::LearningItem::Stored { name, lifecycle, .. } => { let status = if lifecycle == "stale" { "Stale" } else { "Active" }; ( - format!("{}[{}] {}", prefix, status, name), + format!("{prefix}[{status}] {name}"), if is_selected { Style::default().fg(Theme::TEXT).bg(Theme::HIGHLIGHT_DIM) .add_modifier(Modifier::BOLD) @@ -539,7 +541,7 @@ fn render_overlay( " Name:", Style::default().fg(Theme::TEXT_DIM), ))); right_lines.push(Line::from(Span::styled( - format!(" {}", name), + format!(" {name}"), Style::default().fg(Theme::TEXT).add_modifier(Modifier::BOLD), ))); right_lines.push(Line::from(Span::raw(""))); @@ -548,11 +550,11 @@ fn render_overlay( Style::default().fg(Theme::WARNING), ))); right_lines.push(Line::from(Span::styled( - format!(" Scope: {}", scope), + format!(" Scope: {scope}"), Style::default().fg(Theme::TEXT), ))); right_lines.push(Line::from(Span::styled( - format!(" Confidence: {}", confidence), + format!(" Confidence: {confidence}"), Style::default().fg(Theme::TEXT), ))); right_lines.push(Line::from(Span::raw(""))); @@ -561,7 +563,7 @@ fn render_overlay( ))); for line in content.lines() { right_lines.push(Line::from(Span::styled( - format!(" {}", line), + format!(" {line}"), Style::default().fg(Theme::TEXT), ))); } @@ -578,7 +580,7 @@ fn render_overlay( " Name:", Style::default().fg(Theme::TEXT_DIM), ))); right_lines.push(Line::from(Span::styled( - format!(" {}", name), + format!(" {name}"), Style::default().fg(Theme::TEXT).add_modifier(Modifier::BOLD), ))); right_lines.push(Line::from(Span::raw(""))); @@ -588,15 +590,15 @@ fn render_overlay( Theme::SUCCESS }; right_lines.push(Line::from(Span::styled( - format!(" Status: {}", lifecycle), + format!(" Status: {lifecycle}"), Style::default().fg(status_color), ))); right_lines.push(Line::from(Span::styled( - format!(" Scope: {}", scope), + format!(" Scope: {scope}"), Style::default().fg(Theme::TEXT), ))); right_lines.push(Line::from(Span::styled( - format!(" Description: {}", description), + format!(" Description: {description}"), Style::default().fg(Theme::TEXT), ))); right_lines.push(Line::from(Span::raw(""))); @@ -605,7 +607,7 @@ fn render_overlay( ))); for line in content.lines() { right_lines.push(Line::from(Span::styled( - format!(" {}", line), + format!(" {line}"), Style::default().fg(Theme::TEXT), ))); } @@ -635,7 +637,7 @@ fn render_overlay( .border_style(Style::default().fg(Theme::INFO)); let runtime = state.session_runtime.as_ref(); let (tokens_in, tokens_out, api_calls, review_tokens, session_start) = runtime - .map(|r| { + .map_or((0, 0, 0, 0, 0), |r| { ( r.usage.tokens_in, r.usage.tokens_out, @@ -643,18 +645,16 @@ fn render_overlay( r.usage.review_tokens, r.session_start, ) - }) - .unwrap_or((0, 0, 0, 0, 0)); + }); let (edit_count, lesson_count, review_count, consec_empty) = runtime - .map(|r| { + .map_or((0, 0, 0, 0), |r| { ( r.edit_count, r.lesson_count, r.review_count, r.consecutive_empty_reviews, ) - }) - .unwrap_or((0, 0, 0, 0)); + }); let elapsed_ms = chrono::Utc::now().timestamp_millis().saturating_sub(session_start); let hours = elapsed_ms / 3_600_000; let minutes = (elapsed_ms % 3_600_000) / 60_000; @@ -669,19 +669,19 @@ fn render_overlay( )), Line::from(Span::raw("")), Line::from(Span::styled( - format!(" Main agent: {} tokens", main_tokens), + format!(" Main agent: {main_tokens} tokens"), Style::default().fg(Theme::TEXT), )), Line::from(Span::styled( - format!(" Self-learning: {} tokens", self_learning_total), + format!(" Self-learning: {self_learning_total} tokens"), Style::default().fg(Theme::TEXT_MUTED), )), Line::from(Span::styled( - format!(" Total: {} tokens", total_tokens), + format!(" Total: {total_tokens} tokens"), Style::default().fg(Theme::TEXT).add_modifier(Modifier::BOLD), )), Line::from(Span::styled( - format!(" API calls: {}", api_calls), + format!(" API calls: {api_calls}"), Style::default().fg(Theme::TEXT), )), Line::from(Span::raw("")), @@ -690,21 +690,21 @@ fn render_overlay( Style::default().fg(Theme::INFO).add_modifier(Modifier::BOLD), )), Line::from(Span::styled( - format!(" Edits: {}", edit_count), + format!(" Edits: {edit_count}"), Style::default().fg(Theme::TEXT), )), Line::from(Span::styled( - format!(" Reviews: {}", review_count), + format!(" Reviews: {review_count}"), Style::default().fg(Theme::TEXT), )), Line::from(Span::styled( - format!(" Lessons: {}", lesson_count), + format!(" Lessons: {lesson_count}"), Style::default().fg(Theme::TEXT_MUTED), )), Line::from(Span::styled( format!(" Empty reviews: {}", if consec_empty > 3 { - format!("{} ⚠", consec_empty) + format!("{consec_empty} ⚠") } else { consec_empty.to_string() }, @@ -713,7 +713,7 @@ fn render_overlay( )), Line::from(Span::raw("")), Line::from(Span::styled( - format!(" Session: {}h {}m {}s", hours, minutes, seconds), + format!(" Session: {hours}h {minutes}m {seconds}s"), Style::default().fg(Theme::TEXT_DIM), )), ]; @@ -757,7 +757,7 @@ fn render_overlay( let is_selected = i == state.misc.selected_index; let prefix = if is_selected { " ▸ " } else { " " }; let model_str = cfg.default_model.as_deref().unwrap_or("(any)"); - let label = format!("{}{} ({})", prefix, name, model_str); + let label = format!("{prefix}{name} ({model_str})"); let style = if is_current { Style::default().fg(Theme::HIGHLIGHT).add_modifier(Modifier::BOLD) } else if is_selected { @@ -842,7 +842,7 @@ fn render_input_bar( } else { Style::default().fg(Theme::TEXT) }; - let label = format!("{}{}", prefix, candidate); + let label = format!("{prefix}{candidate}"); lines.push(Line::from(Span::styled(label, style))); } let dropdown = Paragraph::new(lines).block(dropdown_block); @@ -901,7 +901,7 @@ fn render_input_bar( // ──────────────────────────────────────────────────────────────────────────── /// Render active toasts as a floating stack at top-right of the terminal. -/// Each toast auto-expires after its lifetime_ms. Max 4 visible at once. +/// Each toast auto-expires after its `lifetime_ms`. Max 4 visible at once. /// /// Toasts are stacked vertically with a 1-line gap. Each has a colored /// left border and a subtle background. diff --git a/src/view/status.rs b/src/view/status.rs index 5f5c571..31ed49c 100644 --- a/src/view/status.rs +++ b/src/view/status.rs @@ -1,3 +1,4 @@ +#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)] //! Status bar rendering for the TUI — modern segmented bar design. //! //! Flow: `draw_status_bar` reads live connection/turn state off @@ -20,14 +21,15 @@ use super::theme::Theme; /// Layout (left-to-right, space-filling): /// LEFT: [zesdex] + status indicator (READY/PROG/NOAPI) /// CENTER: spinner + optional contextual info -/// RIGHT: provider · model · ↑tokens_in ↓tokens_out +/// RIGHT: provider · model · ↑`tokens_in` ↓`tokens_out` pub fn draw_status_bar(frame: &mut Frame, area: Rect, state: &crate::app::state::rest::AppStateRest) { + use ratatui::layout::{Constraint, Direction, Layout}; let spinner_frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; // ── Agent status badge ──────────────────────────────────────────────── let (status_text, badge_bg, status_fg) = if state.turn_in_flight() { let f = spinner_frames[(state.misc.tick_count as usize / 2) % spinner_frames.len()]; - (format!(" {} PROG ", f), Theme::MODE_YOLO, Theme::BG) + (format!(" {f} PROG "), Theme::MODE_YOLO, Theme::BG) } else if state.misc.api_connected { (" READY ".to_string(), Theme::MODE_AUTO, Theme::BG) } else { @@ -61,7 +63,7 @@ pub fn draw_status_bar(frame: &mut Frame, area: Rect, state: &crate::app::state: let total_chars: usize = rt.messages.iter() .filter_map(|m| m.content.as_deref()) - .map(|c| c.len()) + .map(str::len) .sum(); let current_tokens = total_chars / 4; @@ -69,8 +71,8 @@ pub fn draw_status_bar(frame: &mut Frame, area: Rect, state: &crate::app::state: if rt.usage.last_tokens_in > 0 || rt.usage.last_tokens_out > 0 { parts.push(format!("↑{} ↓{}", rt.usage.last_tokens_in, rt.usage.last_tokens_out)); } - let max_str = max_tokens.map(|v| v.to_string()).unwrap_or_else(|| "?".to_string()); - parts.push(format!("{}/{}", current_tokens, max_str)); + let max_str = max_tokens.map_or_else(|| "?".to_string(), |v| v.to_string()); + parts.push(format!("{current_tokens}/{max_str}")); parts.push(state.settings.provider.clone()); parts.push(state.settings.model.clone()); @@ -79,7 +81,7 @@ pub fn draw_status_bar(frame: &mut Frame, area: Rect, state: &crate::app::state: let max_tokens = state.app_config.model_roles.values() .find(|role| role.provider == state.settings.provider && role.model == state.settings.model) .and_then(|role| role.context_window); - let max_str = max_tokens.map(|v| v.to_string()).unwrap_or_else(|| "?".to_string()); + let max_str = max_tokens.map_or_else(|| "?".to_string(), |v| v.to_string()); format!(" 0/{} · {} · {} ", max_str, state.settings.provider, state.settings.model) }; @@ -92,7 +94,6 @@ pub fn draw_status_bar(frame: &mut Frame, area: Rect, state: &crate::app::state: )); // Render the bar using two columns - use ratatui::layout::{Constraint, Direction, Layout}; let chunks = Layout::default() .direction(Direction::Horizontal) .constraints([ diff --git a/src/view/workflow.rs b/src/view/workflow.rs index 7437a93..65ee691 100644 --- a/src/view/workflow.rs +++ b/src/view/workflow.rs @@ -55,6 +55,7 @@ fn is_company_pipeline(agents: &[crate::app::workflow::engine::WorkflowAgent]) - } /// Render the workflow status panel. +#[allow(clippy::too_many_lines)] pub fn draw_workflow_panel(frame: &mut Frame, area: Rect, state: &crate::app::state::rest::AppStateRest) { use ratatui::layout::{Constraint, Direction, Layout}; @@ -179,7 +180,7 @@ pub fn draw_workflow_panel(frame: &mut Frame, area: Rect, state: &crate::app::st // Agent card header card_lines.push(Line::from(vec![ Span::styled( - format!(" {} ", icon), + format!(" {icon} "), Style::default().fg(color).add_modifier(Modifier::BOLD), ), Span::styled( @@ -187,7 +188,7 @@ pub fn draw_workflow_panel(frame: &mut Frame, area: Rect, state: &crate::app::st Style::default().fg(Theme::TEXT).add_modifier(Modifier::BOLD), ), Span::styled( - format!(" [{}]", label), + format!(" [{label}]"), Style::default().fg(color), ), Span::styled( @@ -240,18 +241,18 @@ fn build_session_lines(state: &crate::app::state::rest::AppStateRest) -> Vec 0 { lines.push(Line::from(vec![ Span::styled(" Pending ", Style::default().fg(Theme::TEXT_DIM)), - Span::styled(format!(" {}", pending), Style::default().fg(Theme::WARNING)), + Span::styled(format!(" {pending}"), Style::default().fg(Theme::WARNING)), ])); } if bash_count > 0 { lines.push(Line::from(vec![ Span::styled(" Bash jobs ", Style::default().fg(Theme::TEXT_DIM)), - Span::styled(format!(" {}", bash_count), Style::default().fg(Theme::WARNING)), + Span::styled(format!(" {bash_count}"), Style::default().fg(Theme::WARNING)), ])); } } else {