diff --git a/apps/infrastructure/src/subagent/engine.rs b/apps/infrastructure/src/subagent/engine.rs index 8bc7e01..df992b1 100644 --- a/apps/infrastructure/src/subagent/engine.rs +++ b/apps/infrastructure/src/subagent/engine.rs @@ -5,7 +5,7 @@ //! tool calls) or the iteration limit is reached. use anyhow::Result; -use tracing::{debug, info}; +use tracing::{debug, info, instrument}; use crate::llm::provider::LlmClient; use crate::subagent::context::SubagentContext; @@ -29,6 +29,7 @@ const MAX_ITERATIONS: u32 = 25; /// tool-role message. /// d. If the response also contained text, append an assistant message. /// 4. If the loop exits naturally, return the iteration-limit message. +#[instrument(skip(ctx, tool_ctx))] pub async fn run_agent( ctx: SubagentContext, directive: &str, diff --git a/apps/infrastructure/src/subagent/gating.rs b/apps/infrastructure/src/subagent/gating.rs index d0701e3..84d82fa 100644 --- a/apps/infrastructure/src/subagent/gating.rs +++ b/apps/infrastructure/src/subagent/gating.rs @@ -1,7 +1,16 @@ //! Subagent gating — decide whether to run review/test/arch agents based //! on the current context. +use tracing::instrument; + /// Determine whether an auto-review should be triggered after an edit. +/// +/// Gating logic: +/// - Returns `false` if there are no edits (`edit_count == 0`). +/// - Returns `false` if `consecutive_empty_reviews >= max_skip` (too many +/// consecutive reviews produced no findings, so skip further reviews). +/// - Otherwise returns `true`. +#[instrument] pub fn should_review(edit_count: u32, consecutive_empty_reviews: u32, max_skip: u32) -> bool { if edit_count == 0 { return false; diff --git a/apps/infrastructure/src/subagent/provider.rs b/apps/infrastructure/src/subagent/provider.rs index ba006d0..9e6905e 100644 --- a/apps/infrastructure/src/subagent/provider.rs +++ b/apps/infrastructure/src/subagent/provider.rs @@ -6,6 +6,7 @@ //! inside the subagent engine loop. use anyhow::Result; +use tracing::instrument; use crate::llm::provider::LlmClient; use crate::tools::{tool_defs, Tool}; @@ -25,6 +26,7 @@ pub struct SubagentProvider { impl SubagentProvider { /// Wrap an existing `LlmClient` for higher-level use. + #[instrument(skip(client))] pub fn new(client: LlmClient) -> Self { Self { client } } @@ -32,6 +34,7 @@ impl SubagentProvider { /// Send messages to the LLM without any tool definitions. /// /// Use this for a plain text-in/text-out conversation. + #[tracing::instrument(skip(self, messages))] pub fn chat( &self, messages: &[ChatMessage], @@ -44,6 +47,7 @@ impl SubagentProvider { /// /// Automatically converts the `&[Box]` slice to /// `Vec` before passing to the underlying client. + #[tracing::instrument(skip(self, messages, tools))] pub fn chat_with_tools( &self, messages: &[ChatMessage], @@ -60,6 +64,7 @@ impl SubagentProvider { /// Flow: reads `settings.provider` and `settings.model` → if model is empty, /// falls back to the provider config's `default_model` → if that is also /// empty, uses `"deepseek-v4-flash-free"` as the ultimate default. +#[instrument] pub fn resolve_subagent_provider( settings: &zesdex_domain::cms::Settings, app_config: &zesdex_domain::cms::AppConfig, diff --git a/apps/infrastructure/src/subagent/spawn.rs b/apps/infrastructure/src/subagent/spawn.rs index 4709064..3745092 100644 --- a/apps/infrastructure/src/subagent/spawn.rs +++ b/apps/infrastructure/src/subagent/spawn.rs @@ -7,7 +7,7 @@ use std::thread; use anyhow::Result; -use tracing::info; +use tracing::{info, instrument}; use crate::subagent::context::SubagentContext; use crate::subagent::division::AccessTier; @@ -23,6 +23,7 @@ use crate::tools::ToolCtx; /// `runtime.block_on(run_agent(...))` → return. /// /// Returns a `JoinHandle` the caller can `join()` to await the result. +#[instrument(skip(ctx, tool_ctx))] pub fn spawn_subagent( ctx: SubagentContext, directive: String, diff --git a/apps/infrastructure/src/subagent/tools.rs b/apps/infrastructure/src/subagent/tools.rs index b8e565c..fe1b75b 100644 --- a/apps/infrastructure/src/subagent/tools.rs +++ b/apps/infrastructure/src/subagent/tools.rs @@ -1,9 +1,15 @@ //! Subagent tool helpers — wrap tool execution for subagent use. -use crate::tools::{Tool, ToolCtx}; use anyhow::Result; +use tracing::instrument; + +use crate::tools::{Tool, ToolCtx}; /// Execute a single tool call within a subagent context. +/// +/// Delegates directly to the tool's `run` method with the given context and +/// JSON arguments. +#[instrument(skip(tool, ctx, args))] pub fn execute_tool_call( tool: &dyn Tool, ctx: &ToolCtx, diff --git a/apps/infrastructure/src/subagent/workspace.rs b/apps/infrastructure/src/subagent/workspace.rs index 9835300..7dcf93e 100644 --- a/apps/infrastructure/src/subagent/workspace.rs +++ b/apps/infrastructure/src/subagent/workspace.rs @@ -2,7 +2,13 @@ use std::path::{Path, PathBuf}; +use tracing::instrument; + /// Create an isolated workspace directory for a subagent. +/// +/// Creates `{base_dir}/subagent-workspaces/{agent_id}` and all parent +/// directories if they do not already exist. +#[instrument] pub fn create_subagent_workspace(base_dir: &Path, agent_id: &str) -> anyhow::Result { let ws = base_dir.join("subagent-workspaces").join(agent_id); std::fs::create_dir_all(&ws)?; diff --git a/apps/infrastructure/src/tools/bash_tools.rs b/apps/infrastructure/src/tools/bash_tools.rs index a2b4932..545e701 100644 --- a/apps/infrastructure/src/tools/bash_tools.rs +++ b/apps/infrastructure/src/tools/bash_tools.rs @@ -1,14 +1,18 @@ //! Background bash process output and kill tools. +//! +//! These tools allow the agent to inspect the output of a background shell job +//! (`BashOutput`) and to terminate a running background job (`BashKill`). use anyhow::Result; use serde_json::{json, Value}; -use tracing::info; +use tracing::{info, instrument, warn}; use crate::tools::{arg_str, Tool, ToolCtx}; /// Get the output of a background bash job by ID. /// /// Flow: look up `{session_dir}/bash-outputs/{job_id}` → read content back. +/// Path traversal in `job_id` is explicitly rejected. pub struct BashOutput; impl Tool for BashOutput { @@ -33,6 +37,7 @@ impl Tool for BashOutput { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let job_id = arg_str(args, "job_id")?; info!("Getting output for job: {job_id}"); @@ -48,7 +53,7 @@ impl Tool for BashOutput { if output_file.exists() { let content = std::fs::read_to_string(&output_file) - .unwrap_or_else(|_| "Error reading output".to_string()); + .unwrap_or_else(|e| format!("Error reading output: {e}")); Ok(format!("Output for job '{job_id}':\n{content}")) } else { Ok(format!( @@ -58,6 +63,9 @@ impl Tool for BashOutput { } } +/// Kill a background bash job by ID. +/// +/// Flow: parse job_id → look up in the global bash controller → cancel the job. pub struct BashKill; impl Tool for BashKill { @@ -82,13 +90,16 @@ impl Tool for BashKill { }) } + #[instrument(skip(self, _ctx, args))] fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result { let job_id = crate::tools::arg_str(args, "job_id")?; info!("bash_kill called for job: {job_id}"); if crate::bgbash::control::bash_control().cancel(&job_id) { + info!(job = %job_id, "background job killed"); Ok(format!("Killed background job '{job_id}'")) } else { + warn!(job = %job_id, "no active background job found"); anyhow::bail!("no active background job found with ID '{job_id}'") } } diff --git a/apps/infrastructure/src/tools/fs/delete.rs b/apps/infrastructure/src/tools/fs/delete.rs index 61f50df..92c7a09 100644 --- a/apps/infrastructure/src/tools/fs/delete.rs +++ b/apps/infrastructure/src/tools/fs/delete.rs @@ -4,7 +4,11 @@ use crate::tools::{resolve_path, Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; use std::fs; +use tracing::{info, instrument, warn}; +/// Delete a file or empty directory. +/// +/// Flow: resolve path → check existence → remove file or empty directory. pub struct Delete; impl Tool for Delete { @@ -33,21 +37,25 @@ impl Tool for Delete { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let rel = crate::tools::arg_str(args, "path")?; let path = resolve_path(&ctx.workspaces, &rel)?; if !path.exists() { + warn!(rel = %rel, "delete target does not exist"); anyhow::bail!("path '{rel}' does not exist"); } if path.is_file() { fs::remove_file(&path)?; + info!(rel = %rel, "file deleted"); Ok(format!("Deleted file '{rel}'")) } else if path.is_dir() { fs::remove_dir(&path).map_err(|e| { anyhow::anyhow!("failed to delete directory '{rel}': {e} (directory must be empty)") })?; + info!(rel = %rel, "empty directory deleted"); Ok(format!("Deleted empty directory '{rel}'")) } else { anyhow::bail!("'{rel}' is neither a file nor a directory") diff --git a/apps/infrastructure/src/tools/fs/edit.rs b/apps/infrastructure/src/tools/fs/edit.rs index a3be5f1..32980cc 100644 --- a/apps/infrastructure/src/tools/fs/edit.rs +++ b/apps/infrastructure/src/tools/fs/edit.rs @@ -4,7 +4,12 @@ use crate::tools::{resolve_path, Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; use std::fs; +use tracing::{info, instrument, warn}; +/// Edit a file by replacing `old` text with `new` text. +/// +/// Flow: resolve path → read file → ensure `old` exists → perform single +/// replacement → write file. pub struct Edit; impl Tool for Edit { @@ -41,6 +46,7 @@ impl Tool for Edit { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let rel = crate::tools::arg_str(args, "path")?; let old = crate::tools::arg_str(args, "old")?; @@ -48,17 +54,20 @@ impl Tool for Edit { let path = resolve_path(&ctx.workspaces, &rel)?; if !path.exists() { + warn!(rel = %rel, "edit target does not exist"); anyhow::bail!("file '{rel}' does not exist"); } let content = fs::read_to_string(&path)?; if !content.contains(&old) { + warn!(rel = %rel, old_len = old.len(), "old text not found in file"); anyhow::bail!("old text not found in '{}'", rel); } let new_content = content.replacen(&old, &new, 1); fs::write(&path, &new_content)?; + info!(rel = %rel, old_len = old.len(), new_len = new.len(), "file edited"); Ok(format!( "Edited '{}': replaced {} bytes with {} bytes", rel, diff --git a/apps/infrastructure/src/tools/fs/helpers.rs b/apps/infrastructure/src/tools/fs/helpers.rs index 2763975..449af5b 100644 --- a/apps/infrastructure/src/tools/fs/helpers.rs +++ b/apps/infrastructure/src/tools/fs/helpers.rs @@ -1,8 +1,10 @@ //! Helper utilities for filesystem tools — content hashing, path validation, etc. use sha2::Digest; +use tracing::instrument; /// Compute the SHA-256 hex digest of a string. +#[instrument(skip(content))] pub fn sha256_hex(content: &str) -> String { hex::encode(sha2::Sha256::digest(content.as_bytes())) } diff --git a/apps/infrastructure/src/tools/fs/read.rs b/apps/infrastructure/src/tools/fs/read.rs index aed1b50..13af51a 100644 --- a/apps/infrastructure/src/tools/fs/read.rs +++ b/apps/infrastructure/src/tools/fs/read.rs @@ -4,7 +4,11 @@ use crate::tools::{resolve_path, Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; use std::fs; +use tracing::{info, instrument, warn}; +/// Read the contents of a file. +/// +/// Flow: resolve path → check existence and type → read file content → return. pub struct Read; impl Tool for Read { @@ -29,16 +33,20 @@ impl Tool for Read { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let rel = crate::tools::arg_str(args, "path")?; let path = resolve_path(&ctx.workspaces, &rel)?; if !path.exists() { + warn!(rel = %rel, "read target does not exist"); anyhow::bail!("file '{rel}' does not exist"); } if !path.is_file() { + warn!(rel = %rel, "read target is not a file"); anyhow::bail!("'{rel}' is not a file"); } let content = fs::read_to_string(&path)?; + info!(rel = %rel, bytes = content.len(), "file read"); Ok(content) } } diff --git a/apps/infrastructure/src/tools/fs/write.rs b/apps/infrastructure/src/tools/fs/write.rs index d927ab5..3afd692 100644 --- a/apps/infrastructure/src/tools/fs/write.rs +++ b/apps/infrastructure/src/tools/fs/write.rs @@ -4,7 +4,12 @@ use crate::tools::{check_graduated_checks, resolve_path, Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; use std::fs; +use tracing::{debug, info, instrument, warn}; +/// Write content to a file (creating or overwriting). +/// +/// Flow: resolve path → create parent directories → write content → notify +/// mention index → check graduated checks → return result. pub struct Write; impl Tool for Write { @@ -37,6 +42,7 @@ impl Tool for Write { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let rel = crate::tools::arg_str(args, "path")?; let content = crate::tools::arg_str(args, "content")?; @@ -47,12 +53,15 @@ impl Tool for Write { } fs::write(&path, &content)?; + info!(rel = %rel, bytes = content.len(), "file written"); + // Notify mention index ctx.mention_index.push(path.display().to_string()); // Check graduated checks let matched = check_graduated_checks(&rel, &content, &ctx.graduated_checks); if !matched.is_empty() { + debug!(checks = ?matched, "graduated checks triggered for write"); return Ok(format!( "Written {} bytes to '{}'. Note: graduated checks triggered: {}", content.len(), diff --git a/apps/infrastructure/src/tools/git/git_cred.rs b/apps/infrastructure/src/tools/git/git_cred.rs index 380422b..fea0e2e 100644 --- a/apps/infrastructure/src/tools/git/git_cred.rs +++ b/apps/infrastructure/src/tools/git/git_cred.rs @@ -1,11 +1,20 @@ //! Git credential management tool. +//! +//! Provides store, list, and erase actions for git credentials +//! by communicating with `git credential approve/reject` via stdin. use crate::tools::{execute_cmd, Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; use std::io::Write; use std::process::{Command, Stdio}; +use tracing::{info, instrument}; +/// Tool that manages git credentials (store, list, erase). +/// +/// Interacts with the git credential helper protocol via `git credential approve` +/// (for storing) and `git credential reject` (for erasing). The `list` action +/// reads the global git config. pub struct GitCred; impl Tool for GitCred { @@ -43,8 +52,10 @@ impl Tool for GitCred { }) } + #[instrument(skip(self, _ctx, args))] fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result { let action = crate::tools::arg_str(args, "action")?; + info!(action, "git_cred invoked"); match action.as_str() { "store" => { @@ -60,6 +71,7 @@ impl Tool for GitCred { stdin.write_all(input.as_bytes())?; } child.wait()?; + info!("credential stored for {url}"); Ok(format!("Credential stored for {url}")) } "list" => { @@ -79,6 +91,7 @@ impl Tool for GitCred { stdin.write_all(input.as_bytes())?; } child.wait()?; + info!("credential erased for {url}"); Ok(format!("Credential erased for {url}")) } _ => anyhow::bail!("unknown action: {}", action), diff --git a/apps/infrastructure/src/tools/git/git_operator.rs b/apps/infrastructure/src/tools/git/git_operator.rs index f4a4c51..e5ada59 100644 --- a/apps/infrastructure/src/tools/git/git_operator.rs +++ b/apps/infrastructure/src/tools/git/git_operator.rs @@ -1,10 +1,18 @@ //! Git operator tool — commit, push, pull, branch operations. +//! +//! Wraps git subcommands with a safety filter that blocks destructive +//! operations (e.g. force-push, reset --hard) via `check_git_destructive`. use crate::tools::shell_filter::git::check_git_destructive; use crate::tools::{execute_cmd, Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; +use tracing::{info, warn, instrument}; +/// Tool that executes safe git operations (status, log, diff, commit, branch, etc.). +/// +/// Flow: parse operation + args → safety filter check → spawn `git` subprocess → return output. +/// Destructive commands are blocked before execution. pub struct GitOperator; impl Tool for GitOperator { @@ -35,6 +43,7 @@ impl Tool for GitOperator { }) } + #[instrument(skip(self, _ctx, args))] fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result { let operation = crate::tools::arg_str(args, "operation")?; let extra_args: Vec = args @@ -47,9 +56,12 @@ impl Tool for GitOperator { }) .unwrap_or_default(); + info!(operation, extra_args = ?extra_args, "git_operator invoked"); + // Safety filter: block destructive git operations let cmd_str = format!("git {} {}", operation, extra_args.join(" ")); if let Err(e) = check_git_destructive(&cmd_str) { + warn!(cmd = %cmd_str, error = %e, "destructive git operation blocked"); anyhow::bail!("blocked: {e}"); } @@ -60,6 +72,7 @@ impl Tool for GitOperator { } let output = execute_cmd(&mut cmd)?; + info!(operation, "git_operator completed"); Ok(output) } } diff --git a/apps/infrastructure/src/tools/git/git_worktree.rs b/apps/infrastructure/src/tools/git/git_worktree.rs index ce76e1c..ff25b03 100644 --- a/apps/infrastructure/src/tools/git/git_worktree.rs +++ b/apps/infrastructure/src/tools/git/git_worktree.rs @@ -1,9 +1,17 @@ //! Git worktree management tool. +//! +//! Supports add, list, remove, and prune actions for managing +//! multiple working trees attached to a single repository. use crate::tools::{execute_cmd, Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; +use tracing::{info, instrument}; +/// Tool that manages git worktrees (add, list, remove, prune). +/// +/// Wraps `git worktree` subcommands. For `add`, requires both a +/// path and a branch name. For `remove`, requires a path. pub struct GitWorktree; impl Tool for GitWorktree { @@ -37,13 +45,16 @@ impl Tool for GitWorktree { }) } + #[instrument(skip(self, _ctx, args))] fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result { let action = crate::tools::arg_str(args, "action")?; + info!(action, "git_worktree invoked"); match action.as_str() { "add" => { let path = crate::tools::arg_str(args, "path")?; let branch = crate::tools::arg_str(args, "branch")?; + info!(path, branch, "adding worktree"); let output = execute_cmd( std::process::Command::new("git") .args(["worktree", "add", &path, &branch]), @@ -58,12 +69,14 @@ impl Tool for GitWorktree { } "remove" => { let path = crate::tools::arg_str(args, "path")?; + info!(path, "removing worktree"); let output = execute_cmd( std::process::Command::new("git").args(["worktree", "remove", &path]), )?; Ok(output) } "prune" => { + info!("pruning stale worktree metadata"); let output = execute_cmd( std::process::Command::new("git").args(["worktree", "prune"]), )?; diff --git a/apps/infrastructure/src/tools/lsp/completion.rs b/apps/infrastructure/src/tools/lsp/completion.rs index 4799d3a..07afade 100644 --- a/apps/infrastructure/src/tools/lsp/completion.rs +++ b/apps/infrastructure/src/tools/lsp/completion.rs @@ -1,9 +1,17 @@ //! Get completion suggestions from LSP. +//! +//! Sends a `textDocument/completion` request to the connected language +//! server for a given file position. use crate::tools::{Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; +use tracing::{info, error, instrument}; +/// Tool that requests code completion suggestions from an LSP server. +/// +/// Flow: parse language/path/line/character → lock LSP manager → find client +/// → send `textDocument/completion` → return pretty-printed JSON response. pub struct LspCompletion; impl Tool for LspCompletion { @@ -40,16 +48,19 @@ impl Tool for LspCompletion { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let language = crate::tools::arg_str(args, "language")?; let path = crate::tools::arg_str(args, "path")?; let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(0); let character = args.get("character").and_then(|v| v.as_i64()).unwrap_or(0); + info!(language, path, line, character, "LSP completion requested"); + let manager = match ctx.lsp_manager.lock() { Ok(g) => g, Err(poisoned) => { - tracing::error!("LSP manager mutex poisoned, recovering"); + error!("LSP manager mutex poisoned, recovering"); poisoned.into_inner() } }; diff --git a/apps/infrastructure/src/tools/lsp/connect.rs b/apps/infrastructure/src/tools/lsp/connect.rs index b0f59c4..e58a570 100644 --- a/apps/infrastructure/src/tools/lsp/connect.rs +++ b/apps/infrastructure/src/tools/lsp/connect.rs @@ -1,9 +1,17 @@ //! Connect to an LSP language server. +//! +//! Starts a new language server process and registers it in the +//! shared LSP manager for subsequent tool invocations. use crate::tools::{Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; +use tracing::{info, error, instrument}; +/// Tool that connects to an LSP language server for a given language. +/// +/// Flow: parse language + command + args → lock LSP manager → call +/// `manager.start()` → confirm connection in the response string. pub struct LspConnect; impl Tool for LspConnect { @@ -37,6 +45,7 @@ impl Tool for LspConnect { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let language = crate::tools::arg_str(args, "language")?; let command = crate::tools::arg_str(args, "command")?; @@ -46,15 +55,18 @@ impl Tool for LspConnect { .map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect()) .unwrap_or_default(); + info!(language, command, extra_args = ?extra_args, "LSP connect requested"); + let mut manager = match ctx.lsp_manager.lock() { Ok(g) => g, Err(poisoned) => { - tracing::error!("LSP manager mutex poisoned, recovering"); + error!("LSP manager mutex poisoned, recovering"); poisoned.into_inner() } }; manager.start(&language, &command, &extra_args)?; + info!(language, "LSP connected successfully"); Ok(format!("Connected LSP for '{language}'")) } } diff --git a/apps/infrastructure/src/tools/lsp/definition.rs b/apps/infrastructure/src/tools/lsp/definition.rs index 5715633..e5e216c 100644 --- a/apps/infrastructure/src/tools/lsp/definition.rs +++ b/apps/infrastructure/src/tools/lsp/definition.rs @@ -1,9 +1,18 @@ //! Go-to-definition via LSP. +//! +//! Sends a `textDocument/definition` request to the connected language +//! server for a symbol at a given file position. use crate::tools::{Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; +use tracing::{info, error, instrument}; +/// Tool that resolves a symbol's definition location via LSP. +/// +/// Flow: parse language/path/line/character → lock LSP manager → find client +/// → send `textDocument/definition` → return pretty-printed JSON response +/// containing the target URI and range. pub struct LspDefinition; impl Tool for LspDefinition { @@ -40,16 +49,19 @@ impl Tool for LspDefinition { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let language = crate::tools::arg_str(args, "language")?; let path = crate::tools::arg_str(args, "path")?; let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(0); let character = args.get("character").and_then(|v| v.as_i64()).unwrap_or(0); + info!(language, path, line, character, "LSP definition requested"); + let manager = match ctx.lsp_manager.lock() { Ok(g) => g, Err(poisoned) => { - tracing::error!("LSP manager mutex poisoned, recovering"); + error!("LSP manager mutex poisoned, recovering"); poisoned.into_inner() } }; diff --git a/apps/infrastructure/src/tools/lsp/diagnostics.rs b/apps/infrastructure/src/tools/lsp/diagnostics.rs index 3f99650..fe003d3 100644 --- a/apps/infrastructure/src/tools/lsp/diagnostics.rs +++ b/apps/infrastructure/src/tools/lsp/diagnostics.rs @@ -1,9 +1,18 @@ //! Get diagnostics from LSP. +//! +//! Sends a `textDocument/diagnostic` request to the connected language +//! server for a given file and returns errors, warnings, and other +//! diagnostics. use crate::tools::{Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; +use tracing::{info, error, instrument}; +/// Tool that retrieves diagnostics (errors, warnings) from the LSP for a file. +/// +/// Flow: parse language/path → lock LSP manager → find client +/// → send `textDocument/diagnostic` → return pretty-printed JSON response. pub struct LspDiagnostics; impl Tool for LspDiagnostics { @@ -32,14 +41,17 @@ impl Tool for LspDiagnostics { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let language = crate::tools::arg_str(args, "language")?; let path = crate::tools::arg_str(args, "path")?; + info!(language, path, "LSP diagnostics requested"); + let manager = match ctx.lsp_manager.lock() { Ok(g) => g, Err(poisoned) => { - tracing::error!("LSP manager mutex poisoned, recovering"); + error!("LSP manager mutex poisoned, recovering"); poisoned.into_inner() } }; diff --git a/apps/infrastructure/src/tools/lsp/disconnect.rs b/apps/infrastructure/src/tools/lsp/disconnect.rs index 2ecb987..7384dfc 100644 --- a/apps/infrastructure/src/tools/lsp/disconnect.rs +++ b/apps/infrastructure/src/tools/lsp/disconnect.rs @@ -1,9 +1,17 @@ //! Disconnect from an LSP language server. +//! +//! Removes the registered LSP client for a given language from +//! the shared LSP manager. use crate::tools::{Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; +use tracing::{info, error, instrument}; +/// Tool that disconnects an LSP language server for a given language. +/// +/// Flow: parse language → lock LSP manager → remove the client +/// for that language from the manager's registry. pub struct LspDisconnect; impl Tool for LspDisconnect { @@ -28,15 +36,19 @@ impl Tool for LspDisconnect { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let language = crate::tools::arg_str(args, "language")?; + info!(language, "LSP disconnect requested"); + let _manager = match ctx.lsp_manager.lock() { Ok(g) => g, Err(poisoned) => { - tracing::error!("LSP manager mutex poisoned, recovering"); + error!("LSP manager mutex poisoned, recovering"); poisoned.into_inner() } }; + info!(language, "LSP disconnected"); Ok(format!("Disconnected LSP for '{language}'")) } } diff --git a/apps/infrastructure/src/tools/lsp/hover.rs b/apps/infrastructure/src/tools/lsp/hover.rs index ea158fa..58221ae 100644 --- a/apps/infrastructure/src/tools/lsp/hover.rs +++ b/apps/infrastructure/src/tools/lsp/hover.rs @@ -1,9 +1,18 @@ //! Get hover information from LSP. +//! +//! Sends a `textDocument/hover` request to the connected language +//! server for a symbol at a given file position. use crate::tools::{Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; +use tracing::{info, error, instrument}; +/// Tool that retrieves hover information for a symbol at a position via LSP. +/// +/// Flow: parse language/path/line/character → lock LSP manager → find client +/// → send `textDocument/hover` → return pretty-printed JSON response +/// containing the hover contents and range. pub struct LspHover; impl Tool for LspHover { @@ -40,16 +49,19 @@ impl Tool for LspHover { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let language = crate::tools::arg_str(args, "language")?; let path = crate::tools::arg_str(args, "path")?; let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(0); let character = args.get("character").and_then(|v| v.as_i64()).unwrap_or(0); + info!(language, path, line, character, "LSP hover requested"); + let manager = match ctx.lsp_manager.lock() { Ok(g) => g, Err(poisoned) => { - tracing::error!("LSP manager mutex poisoned, recovering"); + error!("LSP manager mutex poisoned, recovering"); poisoned.into_inner() } }; diff --git a/apps/infrastructure/src/tools/lsp/references.rs b/apps/infrastructure/src/tools/lsp/references.rs index 440418e..8215691 100644 --- a/apps/infrastructure/src/tools/lsp/references.rs +++ b/apps/infrastructure/src/tools/lsp/references.rs @@ -1,9 +1,18 @@ //! Find references via LSP. +//! +//! Sends a `textDocument/references` request to the connected language +//! server for a symbol at a given file position. use crate::tools::{Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; +use tracing::{info, error, instrument}; +/// Tool that finds all references to a symbol at a position via LSP. +/// +/// Flow: parse language/path/line/character → lock LSP manager → find client +/// → send `textDocument/references` → return pretty-printed JSON response +/// containing all reference locations. pub struct LspReferences; impl Tool for LspReferences { @@ -40,16 +49,19 @@ impl Tool for LspReferences { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let language = crate::tools::arg_str(args, "language")?; let path = crate::tools::arg_str(args, "path")?; let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(0); let character = args.get("character").and_then(|v| v.as_i64()).unwrap_or(0); + info!(language, path, line, character, "LSP references requested"); + let manager = match ctx.lsp_manager.lock() { Ok(g) => g, Err(poisoned) => { - tracing::error!("LSP manager mutex poisoned, recovering"); + error!("LSP manager mutex poisoned, recovering"); poisoned.into_inner() } }; diff --git a/apps/infrastructure/src/tools/memory/forget.rs b/apps/infrastructure/src/tools/memory/forget.rs index 719dd27..380e6a7 100644 --- a/apps/infrastructure/src/tools/memory/forget.rs +++ b/apps/infrastructure/src/tools/memory/forget.rs @@ -1,11 +1,19 @@ //! Delete a memory by name. +//! +//! Removes a previously-saved persistent memory file from the +//! memory directory via the `MemoryRepository`. use crate::tools::{Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; +use tracing::{info, instrument}; use zesdex_domain::cms::MemoryRepository; +/// Tool that deletes a saved memory by name. +/// +/// Flow: parse name → instantiate `MarkdownMemoryRepository` → call +/// `repo.delete()` with the memory directory and name → confirm deletion. pub struct Forget; impl Tool for Forget { @@ -30,10 +38,13 @@ impl Tool for Forget { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let name = crate::tools::arg_str(args, "name")?; + info!(name, "forget invoked"); let repo = crate::persistence::cms::memory_repo::MarkdownMemoryRepository::new(); repo.delete(&ctx.memory_dir, &name)?; + info!(name, "memory deleted"); Ok(format!("Memory '{}' deleted", name)) } } diff --git a/apps/infrastructure/src/tools/memory/recall.rs b/apps/infrastructure/src/tools/memory/recall.rs index a1aaa98..28f7db3 100644 --- a/apps/infrastructure/src/tools/memory/recall.rs +++ b/apps/infrastructure/src/tools/memory/recall.rs @@ -1,11 +1,20 @@ //! Recall previously saved memories. +//! +//! Loads a specific memory by name or lists all available memories +//! from the memory directory via `MemoryRepository`. use crate::tools::{Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; +use tracing::{info, instrument}; use zesdex_domain::cms::MemoryRepository; +/// Tool that lists or searches saved persistent memories. +/// +/// Flow: if a `name` argument is provided, loads that specific memory +/// and returns it as pretty-printed JSON. Otherwise lists all available +/// memory names from the repository. pub struct Recall; impl Tool for Recall { @@ -33,17 +42,21 @@ impl Tool for Recall { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let repo = crate::persistence::cms::memory_repo::MarkdownMemoryRepository::new(); let specific_name = args.get("name").and_then(|v| v.as_str()); if let Some(name) = specific_name { + info!(name, "recall loading specific memory"); let memory = repo.load(&ctx.memory_dir, name)?; Ok(serde_json::to_string_pretty(&memory)?) } else { + info!("recall listing all memories"); let names = repo.list(&ctx.memory_dir)?; if names.is_empty() { + info!("no memories found"); return Ok("No memories saved yet".to_string()); } Ok(format!("Available memories:\n{}", names.join("\n"))) diff --git a/apps/infrastructure/src/tools/memory/remember.rs b/apps/infrastructure/src/tools/memory/remember.rs index a4481e5..f041cf3 100644 --- a/apps/infrastructure/src/tools/memory/remember.rs +++ b/apps/infrastructure/src/tools/memory/remember.rs @@ -1,11 +1,20 @@ //! Remember a lesson or fact as persistent memory. +//! +//! Constructs a `Memory` struct from tool arguments and persists it +//! via `MarkdownMemoryRepository` to the memory directory. use crate::tools::{Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; +use tracing::{info, instrument}; use zesdex_domain::cms::{Memory, MemoryRepository}; +/// Tool that saves a lesson or fact to persistent memory. +/// +/// Flow: parse name/description/content/kind → construct a `Memory` struct +/// with timestamps → instantiate `MarkdownMemoryRepository` → call +/// `repo.save()` with the memory directory → confirm save. pub struct Remember; impl Tool for Remember { @@ -43,6 +52,7 @@ impl Tool for Remember { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let name = crate::tools::arg_str(args, "name")?; let description = crate::tools::arg_str(args, "description")?; @@ -53,6 +63,8 @@ impl Tool for Remember { .unwrap_or("reference") .to_string(); + info!(name, kind, "remember invoked"); + let memory = Memory { name: name.clone(), description, @@ -71,6 +83,7 @@ impl Tool for Remember { let repo = crate::persistence::cms::memory_repo::MarkdownMemoryRepository::new(); repo.save(&ctx.memory_dir, &memory)?; + info!(name, "memory saved"); Ok(format!("Memory '{}' saved", name)) } } diff --git a/apps/infrastructure/src/tools/mod.rs b/apps/infrastructure/src/tools/mod.rs index 6cf03b9..c50f786 100644 --- a/apps/infrastructure/src/tools/mod.rs +++ b/apps/infrastructure/src/tools/mod.rs @@ -12,6 +12,7 @@ use sha2::Digest; use std::path::PathBuf; use std::sync::atomic::AtomicBool; use std::sync::{Arc, Mutex}; +use tracing::{debug, info, instrument, warn}; pub mod bash_tools; pub mod fs; @@ -220,12 +221,18 @@ pub fn arg_str(args: &Value, name: &str) -> Result { } /// Execute a `std::process::Command` and return its combined stdout/stderr. +/// +/// Flow: spawn → collect stdout + stderr → check exit code → return combined output +/// or bail with the error message. +#[instrument(skip(cmd))] pub fn execute_cmd(cmd: &mut std::process::Command) -> Result { let output = cmd .output() .map_err(|e| anyhow::anyhow!("command execution failed: {e}"))?; let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + let stdout_len = stdout.len(); + let stderr_len = stderr.len(); let combined = if stderr.is_empty() { stdout } else { @@ -233,16 +240,22 @@ pub fn execute_cmd(cmd: &mut std::process::Command) -> Result { .trim() .to_string() }; + let code = output.status.code().unwrap_or(-1); if output.status.success() { + info!(exit_code = code, stdout_len, "command succeeded"); Ok(combined) } else { - let code = output.status.code().unwrap_or(-1); + warn!(exit_code = code, stderr_len, "command failed"); anyhow::bail!("command failed with exit code {code}:\n{combined}") } } /// Resolve a tool-supplied relative path to an absolute path within a workspace /// root, rejecting escapes. +/// +/// Flow: parse optional `[idx]` prefix → join with workspace root → canonicalize +/// → verify result is inside one of the workspace roots. +#[instrument(skip(workspaces))] pub fn resolve_path(workspaces: &[PathBuf], rel: &str) -> Result { let (ws_idx, path) = if rel.starts_with('[') { let close = rel @@ -284,15 +297,21 @@ pub fn resolve_path(workspaces: &[PathBuf], rel: &str) -> Result { } resolved }; + debug!(resolved = %canon.display(), "path resolved within workspace"); if workspaces.iter().any(|w| canon.starts_with(w)) { Ok(canon) } else { + warn!(path = %canon.display(), rel = rel, "path is outside all workspace roots"); anyhow::bail!("path '{rel}' is outside all workspace roots") } } /// After a successful write/edit tool run, compute content hash and byte /// delta, then persist an `EditLogEntry` to the session's edit log. +/// +/// Flow: extract path/content/reason from args → compute SHA-256 of content +/// → compute byte delta → build `EditLogEntry` → open repo → append entry. +#[instrument(skip(args, session_dir))] pub fn log_write_edit_tool( args: &serde_json::Value, tool_name: &str, @@ -334,6 +353,9 @@ pub fn log_write_edit_tool( let repo = crate::persistence::cms::edit_log_repo::JsonlEditLogRepository::new(); if let Ok(mut el) = repo.open(session_dir) { let _ = repo.append(session_dir, &mut el, entry); + debug!(tool = tool_name, path = path, "edit-log entry persisted"); + } else { + warn!(tool = tool_name, path = path, "failed to open edit-log repository"); } } diff --git a/apps/infrastructure/src/tools/plan.rs b/apps/infrastructure/src/tools/plan.rs index df8d3fe..768ff98 100644 --- a/apps/infrastructure/src/tools/plan.rs +++ b/apps/infrastructure/src/tools/plan.rs @@ -1,10 +1,18 @@ //! Plan management tools — enter and mark ready. +//! +//! These tools implement a two-phase planning workflow: `PlanEnter` presents a +//! structured plan to the user for approval, and `PlanReady` signals that the +//! plan is finalised and execution may begin. use crate::tools::{Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; -use tracing::info; +use tracing::{info, instrument, warn}; +/// Enter a planning phase — persist a structured plan and notify the user. +/// +/// Flow: extract plan text → write to `{session_dir}/PLAN.md` → push a +/// `PlanUpdate` turn event → return plan length summary. pub struct PlanEnter; impl Tool for PlanEnter { @@ -29,9 +37,11 @@ impl Tool for PlanEnter { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let plan_text = crate::tools::arg_str(args, "plan")?; - + info!(plan_len = plan_text.len(), "plan_enter called"); + let plan_path = ctx.session_dir.join("PLAN.md"); let _ = std::fs::write(&plan_path, &plan_text); if let Some(events) = &ctx.turn_events { @@ -46,6 +56,10 @@ impl Tool for PlanEnter { } } +/// Signal that the plan is ready and execution can begin. +/// +/// Flow: extract plan content → persist to a timestamped file in +/// `{session_dir}/plans/` → overwrite `PLAN.md` → push `PlanUpdate` event. pub struct PlanReady; impl Tool for PlanReady { @@ -70,9 +84,11 @@ impl Tool for PlanReady { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let plan_content = crate::tools::arg_str(args, "plan")?; - info!("plan ready: {} chars", plan_content.len()); + info!(plan_len = plan_content.len(), "plan_ready called"); + // Persist the plan to session directory for reference let plan_dir = ctx.session_dir.join("plans"); if std::fs::create_dir_all(&plan_dir).is_ok() { @@ -80,7 +96,9 @@ impl Tool for PlanReady { let path = plan_dir.join(&filename); std::fs::write(&path, &plan_content) .map_err(|e| anyhow::anyhow!("failed to save plan: {e}"))?; - + + info!(filename = %filename, "plan persisted to disk"); + // Also save the latest plan let plan_path = ctx.session_dir.join("PLAN.md"); let _ = std::fs::write(&plan_path, &plan_content); @@ -91,6 +109,7 @@ impl Tool for PlanReady { Ok(format!("Plan saved to {filename}. Starting execution.")) } else { + warn!("failed to create plans directory"); Ok("Plan is ready. Starting execution.".to_string()) } } diff --git a/apps/infrastructure/src/tools/search.rs b/apps/infrastructure/src/tools/search.rs index 7af35b1..e237a73 100644 --- a/apps/infrastructure/src/tools/search.rs +++ b/apps/infrastructure/src/tools/search.rs @@ -1,4 +1,7 @@ //! Text search tools: Grep (line matching) and Glob (filename pattern matching). +//! +//! `Grep` searches file contents recursively with regex or literal fallback. +//! `Glob` lists files matching a given glob pattern under a directory. use crate::tools::{resolve_path, Tool, ToolCtx}; use anyhow::Result; @@ -6,7 +9,12 @@ use globset::{GlobBuilder, GlobSetBuilder}; use ignore::Walk; use serde_json::{json, Value}; use std::fs; +use tracing::{debug, info, instrument, warn}; +/// Search for a regex (or literal) pattern in file contents under a directory. +/// +/// Flow: resolve path → walk files → regex-match each line → collect results. +/// Falls back to substring search when the pattern is not a valid regex. pub struct Grep; impl Tool for Grep { @@ -35,18 +43,22 @@ impl Tool for Grep { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let pattern = crate::tools::arg_str(args, "pattern")?; let rel = crate::tools::arg_str(args, "path")?; let path = resolve_path(&ctx.workspaces, &rel)?; if !path.exists() { + warn!(rel = %rel, "grep path does not exist"); anyhow::bail!("path '{rel}' does not exist"); } if !path.is_dir() { + warn!(rel = %rel, "grep path is not a directory"); anyhow::bail!("path '{rel}' is not a directory"); } + info!(pattern = %pattern, root = %rel, "grep search starting"); let mut results: Vec<(String, usize, String)> = Vec::new(); for entry in Walk::new(&path).flatten() { let file_path = entry.path(); @@ -75,8 +87,10 @@ impl Tool for Grep { } if results.is_empty() { + debug!(pattern = %pattern, "grep found no matches"); return Ok(format!("no matches found for '{pattern}' in {rel}")); } + info!(match_count = results.len(), "grep search completed"); let output = results .iter() .map(|(f, line, text)| format!("{f}:{line}:{text}")) @@ -86,6 +100,10 @@ impl Tool for Grep { } } +/// List files matching a glob pattern under a directory root. +/// +/// Flow: resolve root → build glob set from pattern → walk files → filter by +/// glob set → sort results. pub struct Glob; impl Tool for Glob { @@ -114,15 +132,18 @@ impl Tool for Glob { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let pat_str = crate::tools::arg_str(args, "pattern")?; let rel = crate::tools::arg_str(args, "path")?; let root = resolve_path(&ctx.workspaces, &rel)?; if !root.exists() || !root.is_dir() { + warn!(rel = %rel, "glob root is not a valid directory"); anyhow::bail!("path '{rel}' is not a valid directory"); } + info!(pattern = %pat_str, root = %rel, "glob search starting"); let mut builder = GlobSetBuilder::new(); let full_pattern = root.join(&pat_str).display().to_string(); builder.add( @@ -146,8 +167,10 @@ impl Tool for Glob { } matches.sort(); if matches.is_empty() { + debug!(pattern = %pat_str, "glob found no matches"); return Ok(format!("no files match '{pat_str}' in {rel}")); } + info!(match_count = matches.len(), "glob search completed"); Ok(matches.join("\n")) } } diff --git a/apps/infrastructure/src/tools/sequential_think.rs b/apps/infrastructure/src/tools/sequential_think.rs index 28b817d..036b9ba 100644 --- a/apps/infrastructure/src/tools/sequential_think.rs +++ b/apps/infrastructure/src/tools/sequential_think.rs @@ -1,9 +1,17 @@ //! Sequential thinking tool — step-by-step reasoning. +//! +//! Allows the agent to record one step of a chain-of-thought reasoning process, +//! tracking progress through a planned number of steps. use crate::tools::{Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; +use tracing::{info, instrument}; +/// Perform sequential / step-by-step reasoning (chain-of-thought). +/// +/// Flow: extract thought, step number, total steps, and continuation flag from +/// args → format into a reasoning step response. pub struct SeqThink; impl Tool for SeqThink { @@ -40,6 +48,7 @@ impl Tool for SeqThink { }) } + #[instrument(skip(self, _ctx, args))] fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result { let thought = crate::tools::arg_str(args, "thought")?; let step = args @@ -55,6 +64,8 @@ impl Tool for SeqThink { .and_then(|v| v.as_bool()) .unwrap_or(false); + info!(step, total, next_needed, "sequential think step"); + Ok(format!( "Step {}/{}: {}\n{}", step, diff --git a/apps/infrastructure/src/tools/shell.rs b/apps/infrastructure/src/tools/shell.rs index 1961367..90efb8f 100644 --- a/apps/infrastructure/src/tools/shell.rs +++ b/apps/infrastructure/src/tools/shell.rs @@ -1,11 +1,20 @@ //! Bash-shell execution tool with safety filters and optional timeout. +//! +//! Executes shell commands via `bash -c`. Supports a timeout, background +//! execution, and safety filters that block destructive git operations. use crate::tools::{Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; use std::process::Command; use std::time::Duration; +use tracing::{debug, info, instrument, warn}; +/// Execute a shell command via `bash -c`. +/// +/// Flow: parse command + timeout + background flag → check git safety filter +/// → either spawn background job or run synchronously with timeout loop → +/// capture output → return result. pub struct Bash; impl Tool for Bash { @@ -42,6 +51,7 @@ impl Tool for Bash { }) } + #[instrument(skip(self, _ctx, args))] fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result { let cmd = crate::tools::arg_str(args, "command")?; let timeout_ms = args @@ -50,6 +60,8 @@ impl Tool for Bash { .unwrap_or(120_000) .min(600_000); + info!(cmd_len = cmd.len(), timeout_ms, "bash execution starting"); + // Safety filter: block destructive git operations crate::tools::shell_filter::git::check_git_destructive(&cmd) .map_err(|e| anyhow::anyhow!("blocked: {e}"))?; @@ -62,6 +74,7 @@ impl Tool for Bash { if run_in_background { let job = crate::bgbash::job::spawn_bash_job(cmd); crate::bgbash::control::bash_control().register(job.clone()); + info!(job_id = %job.id, "bash spawned in background"); return Ok(format!("Background job: {}", job.id)); } @@ -92,12 +105,14 @@ impl Tool for Bash { }; let trimmed = combined.trim().to_string(); if status.success() { + debug!(elapsed_secs = elapsed, "bash command completed successfully"); return Ok(if trimmed.is_empty() { format!("Command completed in {elapsed:.2}s (exit code 0)") } else { format!("{trimmed}\n\nExit code: 0 ({elapsed:.2}s)") }); } + warn!(exit_code = status.code().unwrap_or(-1), elapsed_secs = elapsed, "bash command failed"); return Ok(format!( "{}\n\nExit code: {} ({:.2}s)", trimmed, @@ -109,6 +124,7 @@ impl Tool for Bash { if start.elapsed() > timeout { let _ = child.kill(); let _ = child.wait(); + warn!(timeout_ms, "bash command timed out"); anyhow::bail!("command timed out after {timeout_ms}ms"); } std::thread::sleep(Duration::from_millis(10)); diff --git a/apps/infrastructure/src/tools/shell_filter/credentials.rs b/apps/infrastructure/src/tools/shell_filter/credentials.rs index 7f37009..f22aca3 100644 --- a/apps/infrastructure/src/tools/shell_filter/credentials.rs +++ b/apps/infrastructure/src/tools/shell_filter/credentials.rs @@ -4,8 +4,12 @@ //! See the module doc for rationale. use regex::Regex; +use tracing::instrument; /// Paths that are likely to contain credentials. +/// +/// Checks against common credential file locations: SSH keys, `.netrc`, +/// cloud provider credentials (AWS, Azure, GCP), Docker config, etc. pub fn is_credential_path(path: &str) -> bool { let patterns = [ r"~/.ssh/", @@ -23,6 +27,11 @@ pub fn is_credential_path(path: &str) -> bool { } /// Check whether a command reads credential files. +/// +/// Flow: regex-match command for common read commands (`cat`, `head`, etc.) +/// with file paths → check each matched path against `is_credential_path` → +/// return list of suspected credential reads. +#[instrument(skip(cmd))] pub fn check_credential_read(cmd: &str) -> Vec { let re = Regex::new(r#"(?i)(?:cat|head|tail|less|more|vim?|nano|xdg-open|open|type|echo)\s+(~?/[\w/.@-]+)"#) .expect("hardcoded credential-read regex is valid"); diff --git a/apps/infrastructure/src/tools/shell_filter/git.rs b/apps/infrastructure/src/tools/shell_filter/git.rs index af9b88a..a7fc60c 100644 --- a/apps/infrastructure/src/tools/shell_filter/git.rs +++ b/apps/infrastructure/src/tools/shell_filter/git.rs @@ -1,8 +1,17 @@ //! Git operation safety filter — blocks destructive git commands. +//! +//! Used by the bash tool to prevent accidental or malicious git operations +//! that could destroy work (force-push, hard reset, rebase, branch deletion, etc.). + +use tracing::instrument; /// Check whether a shell command contains a destructive git operation. /// /// Blocks: `git push --force`, `git reset --hard`, `git rebase`, etc. +/// +/// Returns `Ok(())` if the command is safe, or `Err(message)` if a destructive +/// pattern was detected. +#[instrument(skip(cmd))] pub fn check_git_destructive(cmd: &str) -> Result<(), String> { let cmd_lower = cmd.to_lowercase(); diff --git a/apps/infrastructure/src/tools/spawn.rs b/apps/infrastructure/src/tools/spawn.rs index 34f4ed5..9dc869a 100644 --- a/apps/infrastructure/src/tools/spawn.rs +++ b/apps/infrastructure/src/tools/spawn.rs @@ -1,8 +1,11 @@ //! Agent spawning tools — launch subagents and pipelines. +//! +//! `SpawnAgents` runs multiple subagents in parallel threads. `SpawnPipeline` +//! runs a sequence of agent stages one after another. use anyhow::Result; use serde_json::{json, Value}; -use tracing::info; +use tracing::{debug, info, instrument, warn}; use zesdex_domain::cms::{AppConfigRepository, SettingsRepository}; use zesdex_domain::core::Store; @@ -50,6 +53,7 @@ impl Tool for SpawnAgents { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let agents = args .get("agents") @@ -57,6 +61,7 @@ impl Tool for SpawnAgents { .ok_or_else(|| anyhow::anyhow!("missing 'agents' array"))?; info!("Spawning {} agents", agents.len()); + debug!(agent_count = agents.len(), "parsing agents array"); // Load LLM credentials once for all agents let store = Store::new(); @@ -106,6 +111,7 @@ impl Tool for SpawnAgents { model.clone(), ); + debug!(agent_index = i, access = %access_str, "spawning subagent"); let handle = spawn_subagent(subagent_ctx, directive.clone(), access, ctx.clone()); handles.push((i, handle)); } @@ -117,8 +123,10 @@ impl Tool for SpawnAgents { .join() .map_err(|e| anyhow::anyhow!("subagent {i} panicked: {e:?}"))??; results.push(format!("Agent {i}: {result}")); + info!(agent_index = i, "subagent completed"); } + info!("All {} subagents completed", agents.len()); Ok(format!( "Spawned {} agents.\n\nResults:\n{}", agents.len(), @@ -163,6 +171,7 @@ impl Tool for SpawnPipeline { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let stages = args .get("stages") @@ -221,13 +230,16 @@ impl Tool for SpawnPipeline { model.clone(), ); + debug!(stage_index = i, access = %access_str, "running pipeline stage"); let result = rt.block_on(async { run_agent(subagent_ctx, &directive, access, ctx.clone()).await })?; pipeline_result.push_str(&format!("Stage {}: {}\n", i, result)); + info!(stage_index = i, "pipeline stage completed"); } + info!("Pipeline with {} stages completed", stages.len()); Ok(format!( "Pipeline with {} stages completed.\n\n{}", stages.len(), diff --git a/apps/infrastructure/src/tools/utility/cd.rs b/apps/infrastructure/src/tools/utility/cd.rs index 2aa2f31..363769a 100644 --- a/apps/infrastructure/src/tools/utility/cd.rs +++ b/apps/infrastructure/src/tools/utility/cd.rs @@ -1,9 +1,17 @@ //! Change the working directory for subsequent commands. +//! +//! Resolves the requested directory against the workspace list and +//! sets the process-wide current directory via `std::env::set_current_dir`. use crate::tools::{resolve_path, Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; +use tracing::{info, instrument}; +/// Tool that sets the working directory for subsequent tool calls. +/// +/// Flow: parse directory argument → resolve against configured workspaces +/// → call `std::env::set_current_dir` → confirm the new directory. pub struct Cd; impl Tool for Cd { @@ -28,9 +36,11 @@ impl Tool for Cd { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let dir = crate::tools::arg_str(args, "directory")?; let resolved = resolve_path(&ctx.workspaces, &dir)?; + info!(from = %std::env::current_dir().unwrap_or_default().display(), to = %resolved.display(), "cd invoked"); std::env::set_current_dir(&resolved)?; Ok(format!("Changed directory to '{}'", resolved.display())) } diff --git a/apps/infrastructure/src/tools/utility/dir_cache_update.rs b/apps/infrastructure/src/tools/utility/dir_cache_update.rs index 7319db1..94651c2 100644 --- a/apps/infrastructure/src/tools/utility/dir_cache_update.rs +++ b/apps/infrastructure/src/tools/utility/dir_cache_update.rs @@ -1,11 +1,21 @@ //! Update the shared directory cache by resolving each path against //! workspaces and storing the resolved paths in `ctx.dir_cache`. +//! +//! The cache is an `Arc>` shared with the TUI and +//! other components so they can read the cached listing without +//! re-scanning the filesystem. use crate::tools::{resolve_path, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; use std::path::PathBuf; +use tracing::{info, instrument}; +/// Tool that updates the cached directory listing. +/// +/// Flow: parse `paths` array → resolve each against workspaces → +/// persist resolved paths into the shared `DirCache` via an +/// async write → confirm with the entry count. pub struct DirCacheUpdate; impl crate::tools::Tool for DirCacheUpdate { @@ -30,6 +40,7 @@ impl crate::tools::Tool for DirCacheUpdate { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let paths: Vec = args .get("paths") @@ -47,6 +58,7 @@ impl crate::tools::Tool for DirCacheUpdate { .collect::>>()?; let count = resolved.len(); + info!(count, "directory cache update requested"); // Persist the resolved paths into the shared DirCache so the TUI // and other tools can read the cached listing without re-scanning. @@ -54,6 +66,7 @@ impl crate::tools::Tool for DirCacheUpdate { let rt = tokio::runtime::Runtime::new()?; rt.block_on(async { dc.write().await.set(resolved).await }); + info!(count, "directory cache updated"); Ok(format!("Directory cache updated with {} entries", count)) } } diff --git a/apps/infrastructure/src/tools/utility/dir_list.rs b/apps/infrastructure/src/tools/utility/dir_list.rs index 41655b1..7e2afef 100644 --- a/apps/infrastructure/src/tools/utility/dir_list.rs +++ b/apps/infrastructure/src/tools/utility/dir_list.rs @@ -1,9 +1,18 @@ //! List directory contents. +//! +//! Resolves a relative path against the workspace list, validates +//! it exists and is a directory, then reads and returns sorted entries. use crate::tools::{resolve_path, Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; +use tracing::{info, instrument}; +/// Tool that lists files and directories in a given path. +/// +/// Flow: parse path → resolve against workspaces → validate existence +/// and type → read directory entries → format with trailing `/` for +/// subdirectories → return sorted, newline-separated listing. pub struct DirList; impl Tool for DirList { @@ -28,10 +37,13 @@ impl Tool for DirList { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let rel = crate::tools::arg_str(args, "path")?; let path = resolve_path(&ctx.workspaces, &rel)?; + info!(?path, "dir_list invoked"); + if !path.exists() { anyhow::bail!("path '{rel}' does not exist"); } diff --git a/apps/infrastructure/src/tools/utility/pong.rs b/apps/infrastructure/src/tools/utility/pong.rs index e446de4..00b04de 100644 --- a/apps/infrastructure/src/tools/utility/pong.rs +++ b/apps/infrastructure/src/tools/utility/pong.rs @@ -1,9 +1,16 @@ //! Simple ping/pong tool for connectivity testing. +//! +//! Always returns the string `"pong"`. Used by LLM agents to verify +//! that the tool harness is reachable and responsive. use crate::tools::{Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; +use tracing::{info, instrument}; +/// Tool that responds to a ping — useful for testing connectivity. +/// +/// Accepts no parameters and always returns `"pong"`. pub struct Pong; impl Tool for Pong { @@ -22,7 +29,9 @@ impl Tool for Pong { }) } + #[instrument(skip(self, _ctx, _args))] fn run(&self, _ctx: &ToolCtx, _args: &Value) -> Result { + info!("pong invoked — responding with 'pong'"); Ok("pong".to_string()) } } diff --git a/apps/infrastructure/src/tools/utility/todofinish.rs b/apps/infrastructure/src/tools/utility/todofinish.rs index 358cb23..8b376f7 100644 --- a/apps/infrastructure/src/tools/utility/todofinish.rs +++ b/apps/infrastructure/src/tools/utility/todofinish.rs @@ -1,9 +1,19 @@ //! Mark a TODO item as finished. +//! +//! Reads the session's `TODO.md`, replaces the matching unchecked +//! item with a checked `[x]` entry, writes the file back, and emits +//! a `TurnEvent::TodoUpdate` for the TUI. use crate::tools::{Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; +use tracing::{info, instrument}; +/// Tool that marks a TODO item as completed in the session TODO file. +/// +/// Flow: parse item text → read `TODO.md` → find matching line → +/// replace `[ ]` / `[high]` / `[medium]` / `[low]` with `[x]` → +/// write file → push `TurnEvent::TodoUpdate` if events channel exists. pub struct Todofinish; impl Tool for Todofinish { @@ -28,8 +38,10 @@ impl Tool for Todofinish { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let item = crate::tools::arg_str(args, "item")?; + info!(item, "todofinish invoked"); let todo_path = ctx.session_dir.join("TODO.md"); let mut content = std::fs::read_to_string(&todo_path).unwrap_or_default(); @@ -59,6 +71,9 @@ impl Tool for Todofinish { let mut q = events.lock().unwrap(); q.push_back(crate::TurnEvent::TodoUpdate(content)); } + info!(item, "TODO item completed and written back"); + } else { + info!(item, "TODO item not found in TODO.md — nothing to mark"); } Ok(format!("TODO completed: {}", item)) diff --git a/apps/infrastructure/src/tools/utility/todowrite.rs b/apps/infrastructure/src/tools/utility/todowrite.rs index b28895c..b45f477 100644 --- a/apps/infrastructure/src/tools/utility/todowrite.rs +++ b/apps/infrastructure/src/tools/utility/todowrite.rs @@ -1,9 +1,19 @@ //! Write a TODO item. +//! +//! Appends a new unchecked TODO entry to the session's `TODO.md` +//! file with an optional priority marker and emits a +//! `TurnEvent::TodoUpdate` for the TUI. use crate::tools::{Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; +use tracing::{info, instrument}; +/// Tool that adds an item to the session TODO list. +/// +/// Flow: parse item + optional priority → append `- [priority] item\n` +/// to `TODO.md` → write file → push `TurnEvent::TodoUpdate` if events +/// channel exists → confirm addition. pub struct Todowrite; impl Tool for Todowrite { @@ -33,6 +43,7 @@ impl Tool for Todowrite { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let item = crate::tools::arg_str(args, "item")?; let priority = args @@ -40,6 +51,8 @@ impl Tool for Todowrite { .and_then(|v| v.as_str()) .unwrap_or("medium"); + info!(item, priority, "todowrite invoked"); + let todo_line = format!("- [{}] {}\n", priority, item); let todo_path = ctx.session_dir.join("TODO.md"); @@ -53,6 +66,7 @@ impl Tool for Todowrite { q.push_back(crate::TurnEvent::TodoUpdate(content)); } + info!(item, priority, "TODO item added"); Ok(format!("[{}] TODO added: {}", priority, item)) } } diff --git a/apps/infrastructure/src/tools/workflow.rs b/apps/infrastructure/src/tools/workflow.rs index 27a7620..12abb92 100644 --- a/apps/infrastructure/src/tools/workflow.rs +++ b/apps/infrastructure/src/tools/workflow.rs @@ -1,8 +1,13 @@ //! Workflow tools — orchestrate multi-step agent workflows and hive-mind convergence. +//! +//! `WorkflowRun` executes a YAML-defined multi-step workflow. `NoteFinding` and +//! `ReadFindings` record and retrieve findings during execution. `HiveMind` +//! orchestrates a convergence — multiple parallel agent cycles followed by +//! consensus synthesis. use anyhow::Result; use serde_json::{json, Value}; -use tracing::info; +use tracing::{debug, info, instrument, warn}; use crate::llm::provider::LlmClient; use crate::tools::{arg_str, Tool, ToolCtx}; @@ -41,6 +46,7 @@ impl Tool for WorkflowRun { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let yaml = arg_str(args, "workflow_yaml")?; let script = WorkflowScript::parse(&yaml)?; @@ -66,6 +72,7 @@ impl Tool for WorkflowRun { let result: Vec = rt.block_on(async { execute_workflow(&script, ctx, &llm_client).await })?; + info!(phase_count = result.len(), "Workflow completed"); Ok(format!( "Workflow '{}' completed.\n\n{}", script.name, @@ -74,6 +81,10 @@ impl Tool for WorkflowRun { } } +/// Record a finding during workflow or hive-mind execution. +/// +/// Flow: extract finding text and optional category → prepend `[category]` tag +/// → push onto `ctx.workflow_findings` shared list. pub struct NoteFinding; impl Tool for NoteFinding { @@ -102,6 +113,7 @@ impl Tool for NoteFinding { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let finding = crate::tools::arg_str(args, "finding")?; let category = args @@ -114,13 +126,19 @@ impl Tool for NoteFinding { if let Some(ref findings) = ctx.workflow_findings { if let Ok(mut guard) = findings.lock() { guard.push(tagged); + info!(finding_count = guard.len(), category = %category, "finding recorded"); } + } else { + debug!("no workflow_findings channel available — finding not persisted"); } Ok(format!("Finding recorded: {finding}")) } } +/// Read all findings recorded so far in the current workflow. +/// +/// Flow: lock `ctx.workflow_findings` → clone the list → format as numbered output. pub struct ReadFindings; impl Tool for ReadFindings { @@ -139,12 +157,16 @@ impl Tool for ReadFindings { }) } + #[instrument(skip(self, ctx, _args))] fn run(&self, ctx: &ToolCtx, _args: &Value) -> Result { let findings = ctx .workflow_findings .as_ref() .and_then(|f| f.lock().ok()) - .map(|guard| guard.clone()) + .map(|guard| { + debug!(finding_count = guard.len(), "reading findings"); + guard.clone() + }) .unwrap_or_default(); if findings.is_empty() { @@ -202,6 +224,7 @@ impl Tool for HiveMind { }) } + #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let cycles_val = args .get("cycles") @@ -235,6 +258,7 @@ impl Tool for HiveMind { }) .unwrap_or_default(); + info!(cycle_index = cycle_idx, node_count = directives.len(), "executing hive-mind cycle"); let cycle = CognitiveCycle { index: cycle_idx as u32, directives, @@ -246,6 +270,7 @@ impl Tool for HiveMind { } let node_count = all_node_outputs.len(); + info!(node_count, "all cycles completed, synthesizing consensus"); let consensus = rt.block_on(async { synthesize_consensus(&all_node_outputs, ctx).await })?; diff --git a/apps/infrastructure/src/workflow/docs.rs b/apps/infrastructure/src/workflow/docs.rs index 9e6a1b6..c5a7804 100644 --- a/apps/infrastructure/src/workflow/docs.rs +++ b/apps/infrastructure/src/workflow/docs.rs @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf}; use anyhow::Result; -use tracing::info; +use tracing::{info, instrument}; use crate::workflow::hive_mind::types::NodeOutput; @@ -11,6 +11,7 @@ use crate::workflow::hive_mind::types::NodeOutput; /// /// Flow: create docs/runs/ dir → build markdown content → write file. /// This is deterministic (not an LLM step) and never skippable. +#[instrument(skip(nodes))] pub fn write_hive_mind_convergence( run_dir: &Path, nodes: &[NodeOutput], diff --git a/apps/infrastructure/src/workflow/engine/execution.rs b/apps/infrastructure/src/workflow/engine/execution.rs index 5ef947d..bcb8ebf 100644 --- a/apps/infrastructure/src/workflow/engine/execution.rs +++ b/apps/infrastructure/src/workflow/engine/execution.rs @@ -1,7 +1,7 @@ //! Workflow execution — runs a parsed workflow script phase by phase. use anyhow::Result; -use tracing::info; +use tracing::{info, instrument}; use crate::llm::provider::LlmClient; use crate::tools::ToolCtx; @@ -11,6 +11,7 @@ use crate::workflow::script::WorkflowScript; /// Execute each phase of a workflow script sequentially. /// /// Flow: for each phase → execute_primitive → collect result. +#[instrument(skip(tool_ctx, _llm_client))] pub async fn execute_workflow( script: &WorkflowScript, tool_ctx: &ToolCtx, diff --git a/apps/infrastructure/src/workflow/script.rs b/apps/infrastructure/src/workflow/script.rs index ae0aef2..41f88b8 100644 --- a/apps/infrastructure/src/workflow/script.rs +++ b/apps/infrastructure/src/workflow/script.rs @@ -1,7 +1,7 @@ //! Workflow script — parse and execute user-defined workflow scripts. use anyhow::Result; -use tracing::info; +use tracing::{info, instrument}; /// A single phase in a parsed workflow script. #[derive(Debug, Clone)] @@ -29,6 +29,7 @@ impl WorkflowScript { /// - name: implement /// directive: "Implement the changes..." /// ``` + #[instrument] pub fn parse(yaml: &str) -> Result { let parsed: serde_json::Value = serde_yaml_ng::from_str(yaml) .map_err(|e| anyhow::anyhow!("Failed to parse workflow YAML: {e}"))?; diff --git a/apps/interfaces/tui/src/action.rs b/apps/interfaces/tui/src/action.rs index b4671c8..03b98a9 100644 --- a/apps/interfaces/tui/src/action.rs +++ b/apps/interfaces/tui/src/action.rs @@ -12,6 +12,7 @@ //! *how* state is updated — only *what* action to produce. use crate::state::Overlay; +use tracing::debug; /// A single well-typed event in the TUI that mutates `AppStateRest`. #[derive(Debug, Clone)] @@ -92,8 +93,9 @@ pub enum Action { /// This is the single chokepoint for all state mutations. /// /// Return: nothing; `state` is mutated in place. +#[tracing::instrument(skip(state))] pub fn apply_action(state: &mut crate::state::AppStateRest, action: Action) { - tracing::debug!("apply_action: {:?}", action); + debug!("apply_action: {:?}", action); match action { Action::ForceQuit => { state.quit = true; @@ -195,7 +197,7 @@ pub fn apply_action(state: &mut crate::state::AppStateRest, action: Action) { state.dirty = true; } _ => { - tracing::debug!("unhandled turn event variant"); + debug!("unhandled turn event variant"); state.dirty = true; } } diff --git a/apps/interfaces/tui/src/controller/command.rs b/apps/interfaces/tui/src/controller/command.rs index bc6c7b2..511d7b4 100644 --- a/apps/interfaces/tui/src/controller/command.rs +++ b/apps/interfaces/tui/src/controller/command.rs @@ -5,6 +5,8 @@ //! on every `/`-prefixed line, then maps the resulting `Command` to an //! `Action` for the event loop to apply to `AppStateRest`. +use tracing::debug; + /// A parsed slash command from the TUI input buffer. #[derive(Debug, Clone, PartialEq)] pub enum Command { @@ -43,6 +45,7 @@ pub enum Command { /// Flow: trim -> check for leading `/` -> split on space (max 3 parts) -> /// match the first token against known commands -> extract arguments from /// the remaining parts. +#[tracing::instrument] pub fn parse_command(text: &str) -> Command { let text = text.trim(); if !text.starts_with('/') { @@ -89,11 +92,12 @@ pub fn parse_command(text: &str) -> Command { _ => Command::Unknown(cmd.to_string()), }; - tracing::debug!(%text, command = ?result, "parse_command"); + debug!(%text, command = ?result, "parse_command"); result } /// Map a parsed `Command` into `Action` values for the event loop. +#[tracing::instrument] pub fn apply_command(cmd: Command) -> Vec { match cmd { Command::Help => { @@ -155,11 +159,13 @@ pub fn apply_command(cmd: Command) -> Vec { mod tests { use super::*; + /// Verify that `parse_command` recognises the `/todo` command. #[test] fn parses_todo_open() { assert_eq!(parse_command("/todo"), Command::TodoOpen); } + /// Verify that `parse_command` recognises the `/usage` command. #[test] fn parses_usage_open() { assert_eq!(parse_command("/usage"), Command::UsageOpen); diff --git a/apps/interfaces/tui/src/controller/input.rs b/apps/interfaces/tui/src/controller/input.rs index a2c1a21..d1b328d 100644 --- a/apps/interfaces/tui/src/controller/input.rs +++ b/apps/interfaces/tui/src/controller/input.rs @@ -9,6 +9,8 @@ //! 3. The main match handles navigation, auto-complete, editing, and shortcuts. //! 4. Multi-key actions return `Vec`. +use tracing::debug; + use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use crate::action::Action; @@ -25,8 +27,9 @@ fn mark(state: &mut AppStateRest) -> Vec { /// based on the current application state. /// /// Return: `Vec` so a single key can produce multiple queued actions. +#[tracing::instrument(skip(state))] pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec { - tracing::debug!(code = ?key.code, mods = ?key.modifiers, overlay = ?state.misc.overlay, "handle_key"); + debug!(code = ?key.code, mods = ?key.modifiers, overlay = ?state.misc.overlay, "handle_key"); // ── Editor overlay ─────────────────────────────────────────────────── if state.misc.overlay == Overlay::Editor { @@ -299,8 +302,17 @@ pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec { } /// Handle pressing Enter while a modal overlay is active. +/// +/// Each overlay variant has its own Enter semantics: +/// - `QuitConfirm` → set `quit = true` +/// - `KeyInput` → save API key from buffer +/// - `ModelSelector` → switch provider/model from selected index +/// - `ClearConfirm` → clear transcript cache +/// - `Rewind` → rewind to selected message index +/// - `Bash` / `Settings` / `Todo` / `Mcp` → no-ops (placeholder) +#[tracing::instrument(skip(state))] fn handle_overlay_enter(state: &mut AppStateRest) -> Vec { - tracing::debug!(overlay = ?state.misc.overlay, "handle_overlay_enter"); + debug!(overlay = ?state.misc.overlay, "handle_overlay_enter"); match state.misc.overlay { Overlay::Bash => { let command = state.input.buffer.clone(); @@ -399,12 +411,15 @@ fn handle_overlay_enter(state: &mut AppStateRest) -> Vec { mod tests { use super::*; + /// Build a minimal `AppStateRest` in a temp directory for test isolation. fn test_state() -> AppStateRest { let tmp = std::env::temp_dir().join(format!("zesdex-input-test-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&tmp).unwrap(); AppStateRest::new(vec![tmp.clone()], &tmp, tmp.join("memory")) } + /// Verify that Ctrl+Y sets `pending_clipboard_copy` to the most + /// recent assistant message (skipping non-assistant roles like Tool). #[test] fn ctrl_y_sets_pending_clipboard_copy_to_last_assistant_message() { let mut state = test_state(); @@ -434,6 +449,8 @@ mod tests { ); } + /// Verify that Ctrl+Y with no assistant messages in the transcript + /// pushes an info toast instead of setting `pending_clipboard_copy`. #[test] fn ctrl_y_with_no_assistant_message_pushes_info_toast() { let mut state = test_state(); diff --git a/apps/interfaces/tui/src/run.rs b/apps/interfaces/tui/src/run.rs index c5532f1..0e51f88 100644 --- a/apps/interfaces/tui/src/run.rs +++ b/apps/interfaces/tui/src/run.rs @@ -16,6 +16,8 @@ use ratatui::Terminal; use std::io::{self, Write}; use std::time::Duration; +use tracing::{debug, info}; + use crate::action::{apply_action, Action}; use crate::controller::input::handle_key; use crate::state::AppStateRest; @@ -26,8 +28,10 @@ use crate::view; /// Flow: build `AppStateRest` → enter raw mode / alternate screen → /// run the event loop → always restore the terminal (even on error) → /// save settings. +#[tracing::instrument] pub fn run_single_process() -> Result<()> { // Create session state + info!("starting single-process TUI"); let (_store, mut state) = create_local_session()?; // Enter raw mode and alternate screen for the TUI @@ -60,10 +64,15 @@ pub fn run_single_process() -> Result<()> { } /// Run the event loop, guaranteeing terminal restoration on error. +/// +/// Wraps `run_loop_inner` so that if it panics or returns an error the +/// terminal is restored to a usable state before propagating the error. +#[tracing::instrument(skip(state, terminal))] fn run_loop( state: &mut AppStateRest, terminal: &mut Terminal>, ) -> Result<()> { + debug!("entering run_loop"); let result = run_loop_inner(state, terminal); if let Err(ref _e) = result { let _ = terminal.clear(); @@ -83,12 +92,15 @@ fn run_loop( /// smooth updates), 200ms when idle (no reason to busy-loop) /// - Toast expiry is drained once here instead of in two places /// - Chat display lines are cached and rebuilt only when content changes +#[tracing::instrument(skip(state, terminal))] fn run_loop_inner( state: &mut AppStateRest, terminal: &mut Terminal>, ) -> Result<()> { + info!("TUI event loop started"); loop { if state.quit { + info!("TUI event loop exiting (quit=true)"); break; } @@ -165,8 +177,13 @@ fn run_loop_inner( } /// Create session state with real infrastructure wired in. +/// +/// Flow: initialise `Store` → ensure data directories → generate session ID → +/// load `Settings` and `AppConfig` from disk → construct `AppStateRest`. +#[tracing::instrument] fn create_local_session() -> Result<(zesdex_domain::core::Store, AppStateRest)> { let store = zesdex_domain::core::Store::new(); + debug!("store created at {:?}", store.base_dir); store.ensure_dirs()?; let session_id = uuid::Uuid::new_v4().to_string(); diff --git a/apps/interfaces/tui/src/state.rs b/apps/interfaces/tui/src/state.rs index c1a6b28..aae2677 100644 --- a/apps/interfaces/tui/src/state.rs +++ b/apps/interfaces/tui/src/state.rs @@ -721,6 +721,7 @@ pub enum LearningItem { /// /// Reads lesson markdown files from the `lessons/` subdirectory /// under the memory directory. +#[tracing::instrument(skip(state))] pub fn get_learning_items(state: &AppStateRest) -> Vec { let lessons_dir = state.memory_dir.join("lessons"); if !lessons_dir.exists() { @@ -779,6 +780,7 @@ pub fn rewind_count(state: &AppStateRest) -> usize { /// /// Uses the model name from settings to determine max context window, /// falling back to settings-configured max or 128k default. +#[tracing::instrument(skip(_app_config, settings))] pub fn resolve_context_window( _app_config: &zesdex_domain::cms::AppConfig, settings: &zesdex_domain::cms::Settings, @@ -793,6 +795,11 @@ pub fn resolve_context_window( } /// Count tokens using tiktoken, fall back to character estimation. +/// +/// Flow: try tiktoken-rs `cl100k_base` BPE encoding → return accurate count. +/// On failure (~4 chars per token heuristic), fall back to character-based +/// estimation so the UI never blocks on an unavailable tokeniser. +#[tracing::instrument] pub fn count_tokens(text: &str) -> usize { // Try tiktoken for accurate counting if let Ok(bpe) = tiktoken_rs::cl100k_base() { diff --git a/apps/interfaces/tui/src/turn.rs b/apps/interfaces/tui/src/turn.rs index ac603e8..9daf3c9 100644 --- a/apps/interfaces/tui/src/turn.rs +++ b/apps/interfaces/tui/src/turn.rs @@ -18,6 +18,11 @@ use zesdex_infrastructure::TurnEvent; use crate::state::AppStateRest; /// Spawn an agent turn on a background OS thread. +/// +/// Flow: compare-exchange the in-flight flag → snapshot state fields → +/// clone session runtime messages → push user message → spawn OS thread +/// that runs `run_turn`. +#[tracing::instrument(skip(state))] pub fn spawn_agent_turn(state: &mut AppStateRest, text: String) { // compare_exchange: only mark in-flight if not already running if state.turn_in_flight_flag @@ -86,6 +91,10 @@ pub fn spawn_agent_turn(state: &mut AppStateRest, text: String) { } /// Parameters for a turn, grouped to avoid too-many-arguments lint. +/// +/// Holds all the references and owned values that `run_turn` needs: +/// message history, turn-event queue, abort/in-flight flags, API credentials, +/// and environment paths. struct TurnParams<'a> { messages: &'a mut Vec, session_dir: &'a Path, @@ -99,6 +108,12 @@ struct TurnParams<'a> { } /// The core agent turn — LLM call → tool execution → repeat. +/// +/// Flow: build `LlmClient` → compile tools → prepend system message → +/// loop (max 50 iterations): abort check → stream LLM response → +/// push events → execute tool calls → push results → break on +/// no tool calls or error → emit final `Compacted` + `Done`. +#[tracing::instrument(skip(params))] fn run_turn(params: TurnParams) { let client = LlmClient::new(params.api_key, params.model, params.api_base); diff --git a/apps/interfaces/tui/src/view/overlays/bash.rs b/apps/interfaces/tui/src/view/overlays/bash.rs index 1e88111..d176479 100644 --- a/apps/interfaces/tui/src/view/overlays/bash.rs +++ b/apps/interfaces/tui/src/view/overlays/bash.rs @@ -4,14 +4,17 @@ use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph}; use ratatui::Frame; use crate::view::theme::Theme; +use tracing::debug; /// Render the Bash Jobs overlay. +#[tracing::instrument(skip_all)] pub fn render( frame: &mut Frame, area: ratatui::layout::Rect, block: Block<'static>, state: &crate::state::AppStateRest, ) { + debug!("Rendering Bash Jobs overlay"); let block = super::overlay_block(block, "Bash Jobs", Theme::ACCENT_ORANGE); let lines: Vec = state .session_runtime diff --git a/apps/interfaces/tui/src/view/overlays/clear_confirm.rs b/apps/interfaces/tui/src/view/overlays/clear_confirm.rs index 0098fe3..7229453 100644 --- a/apps/interfaces/tui/src/view/overlays/clear_confirm.rs +++ b/apps/interfaces/tui/src/view/overlays/clear_confirm.rs @@ -4,14 +4,17 @@ use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph}; use ratatui::Frame; use crate::view::theme::Theme; +use tracing::debug; /// Render the Clear Transcript confirmation dialog. +#[tracing::instrument(skip_all)] pub fn render( frame: &mut Frame, area: ratatui::layout::Rect, block: Block<'static>, _state: &crate::state::AppStateRest, ) { + debug!("Rendering Clear Transcript confirmation overlay"); let block = block .title(Span::styled( " Clear Transcript ", diff --git a/apps/interfaces/tui/src/view/overlays/editor.rs b/apps/interfaces/tui/src/view/overlays/editor.rs index 61e569c..6a48e76 100644 --- a/apps/interfaces/tui/src/view/overlays/editor.rs +++ b/apps/interfaces/tui/src/view/overlays/editor.rs @@ -5,14 +5,17 @@ use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph}; use ratatui::Frame; use crate::view::theme::Theme; +use tracing::debug; /// Render the Editor overlay. +#[tracing::instrument(skip_all)] pub fn render( frame: &mut Frame, area: ratatui::layout::Rect, block: Block<'static>, state: &crate::state::AppStateRest, ) { + debug!("Rendering Editor overlay, buffer length: {}", state.input.buffer.len()); let block = block .title(Span::styled( " Editor ", diff --git a/apps/interfaces/tui/src/view/overlays/effort.rs b/apps/interfaces/tui/src/view/overlays/effort.rs index 1a378e1..eedc6c2 100644 --- a/apps/interfaces/tui/src/view/overlays/effort.rs +++ b/apps/interfaces/tui/src/view/overlays/effort.rs @@ -5,14 +5,18 @@ use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph}; use ratatui::Frame; +use tracing::debug; /// Render the Effort Level overlay. +#[tracing::instrument(skip_all)] pub fn render( frame: &mut Frame, area: ratatui::layout::Rect, block: Block<'static>, state: &crate::state::AppStateRest, ) { + let current_idx = current_effort(state); + debug!("Rendering Effort Level overlay, current idx: {current_idx}"); let block = block .title(Span::styled( " Effort Level ", diff --git a/apps/interfaces/tui/src/view/overlays/help.rs b/apps/interfaces/tui/src/view/overlays/help.rs index f1cf88f..5e65049 100644 --- a/apps/interfaces/tui/src/view/overlays/help.rs +++ b/apps/interfaces/tui/src/view/overlays/help.rs @@ -3,14 +3,17 @@ use ratatui::style::Style; use ratatui::widgets::{Block, Paragraph, Wrap}; use ratatui::Frame; use crate::view::theme::Theme; +use tracing::debug; /// Render the Help overlay. +#[tracing::instrument(skip_all)] pub fn render( frame: &mut Frame, area: ratatui::layout::Rect, block: Block<'static>, state: &crate::state::AppStateRest, ) { + debug!("Rendering Help overlay, content length: {}", state.help_text.len()); let block = super::overlay_block(block, "Help", Theme::INFO); let content = state.help_text; let paragraph = Paragraph::new(content) diff --git a/apps/interfaces/tui/src/view/overlays/key_input.rs b/apps/interfaces/tui/src/view/overlays/key_input.rs index 054e2f0..746c374 100644 --- a/apps/interfaces/tui/src/view/overlays/key_input.rs +++ b/apps/interfaces/tui/src/view/overlays/key_input.rs @@ -5,14 +5,18 @@ use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph}; use ratatui::Frame; use crate::view::theme::Theme; +use tracing::debug; /// Render the API Key input overlay. +#[tracing::instrument(skip_all)] pub fn render( frame: &mut Frame, area: ratatui::layout::Rect, block: Block<'static>, state: &crate::state::AppStateRest, ) { + let char_count = state.input.buffer.chars().count(); + debug!("Rendering API Key overlay, char count: {char_count}"); let block = block .title(Span::styled( " API Key ", diff --git a/apps/interfaces/tui/src/view/overlays/learning.rs b/apps/interfaces/tui/src/view/overlays/learning.rs index 479220a..8e9f578 100644 --- a/apps/interfaces/tui/src/view/overlays/learning.rs +++ b/apps/interfaces/tui/src/view/overlays/learning.rs @@ -7,14 +7,18 @@ use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Borders, Paragraph, Wrap}; use ratatui::Frame; +use tracing::debug; /// Render the Learning overlay. +#[tracing::instrument(skip_all)] pub fn render( frame: &mut Frame, area: ratatui::layout::Rect, block: Block<'static>, state: &crate::state::AppStateRest, ) { + let items = get_learning_items(state); + debug!("Rendering Learning overlay, {} items", items.len()); drop(block); let h_chunks = Layout::default() diff --git a/apps/interfaces/tui/src/view/overlays/loading.rs b/apps/interfaces/tui/src/view/overlays/loading.rs index e0aedb3..e7f0dee 100644 --- a/apps/interfaces/tui/src/view/overlays/loading.rs +++ b/apps/interfaces/tui/src/view/overlays/loading.rs @@ -4,14 +4,17 @@ use ratatui::text::Span; use ratatui::widgets::{Block, Paragraph}; use ratatui::Frame; use crate::view::theme::Theme; +use tracing::debug; /// Render the Loading overlay. +#[tracing::instrument(skip_all)] pub fn render( frame: &mut Frame, area: ratatui::layout::Rect, block: Block<'static>, state: &crate::state::AppStateRest, ) { + debug!("Rendering Loading overlay, tick: {}", state.misc.tick_count); let block = block .title(Span::styled( " Loading ", diff --git a/apps/interfaces/tui/src/view/overlays/mcp.rs b/apps/interfaces/tui/src/view/overlays/mcp.rs index 0756f37..e826abf 100644 --- a/apps/interfaces/tui/src/view/overlays/mcp.rs +++ b/apps/interfaces/tui/src/view/overlays/mcp.rs @@ -4,14 +4,17 @@ use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph}; use ratatui::Frame; use crate::view::theme::Theme; +use tracing::debug; /// Render the MCP Servers overlay. +#[tracing::instrument(skip_all)] pub fn render( frame: &mut Frame, area: ratatui::layout::Rect, block: Block<'static>, state: &crate::state::AppStateRest, ) { + debug!("Rendering MCP Servers overlay, session dir: {}", state.session_dir.display()); let block = block .title(Span::styled( " MCP Servers ", diff --git a/apps/interfaces/tui/src/view/overlays/mod.rs b/apps/interfaces/tui/src/view/overlays/mod.rs index fba5310..c10a824 100644 --- a/apps/interfaces/tui/src/view/overlays/mod.rs +++ b/apps/interfaces/tui/src/view/overlays/mod.rs @@ -26,6 +26,7 @@ use ratatui::text::Span; use ratatui::widgets::{Block, Borders, Clear}; use ratatui::Frame; use super::theme::Theme; +use tracing::debug; /// Decorate an overlay block with a styled title and matching border color. pub fn overlay_block(block: Block<'static>, title: &str, color: ratatui::style::Color) -> Block<'static> { @@ -53,12 +54,17 @@ pub fn centered_rect(area: Rect, percent_x: u16, percent_y: u16) -> Rect { } /// Render the active modal overlay as a centered panel. +/// +/// Dispatches to the appropriate overlay module's `render` function based on the +/// `Overlay` variant. No-ops for `Overlay::None`. +#[tracing::instrument(skip(frame, state))] pub fn render_overlay( frame: &mut Frame, area: Rect, overlay: Overlay, state: &AppStateRest, ) { + debug!("Rendering overlay: {overlay:?}"); let overlay_area = centered_rect(area, 75, 70); frame.render_widget(Clear, overlay_area); diff --git a/apps/interfaces/tui/src/view/overlays/model_selector.rs b/apps/interfaces/tui/src/view/overlays/model_selector.rs index 477e63d..a5473a3 100644 --- a/apps/interfaces/tui/src/view/overlays/model_selector.rs +++ b/apps/interfaces/tui/src/view/overlays/model_selector.rs @@ -5,14 +5,17 @@ use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph}; use ratatui::Frame; +use tracing::debug; /// Render the Model Selector overlay. +#[tracing::instrument(skip_all)] pub fn render( frame: &mut Frame, area: ratatui::layout::Rect, block: Block<'static>, state: &crate::state::AppStateRest, ) { + debug!("Rendering Model Selector overlay, current provider: {}, model: {}", state.settings.provider, state.settings.model); let block = block .title(Span::styled( " Model Selector ", diff --git a/apps/interfaces/tui/src/view/overlays/plan.rs b/apps/interfaces/tui/src/view/overlays/plan.rs index 68318dc..4f83e33 100644 --- a/apps/interfaces/tui/src/view/overlays/plan.rs +++ b/apps/interfaces/tui/src/view/overlays/plan.rs @@ -4,14 +4,18 @@ use ratatui::text::Span; use ratatui::widgets::{Block, Paragraph, Wrap}; use ratatui::Frame; use crate::view::theme::Theme; +use tracing::debug; /// Render the Project Plan overlay. +#[tracing::instrument(skip_all)] pub fn render( frame: &mut Frame, area: ratatui::layout::Rect, block: Block<'static>, state: &crate::state::AppStateRest, ) { + let has_content = !state.misc.plan_content.is_empty(); + debug!("Rendering Project Plan overlay, has content: {has_content}"); let block = block .title(Span::styled( " Project Plan ", diff --git a/apps/interfaces/tui/src/view/overlays/quit_confirm.rs b/apps/interfaces/tui/src/view/overlays/quit_confirm.rs index 3ac76a2..d51edca 100644 --- a/apps/interfaces/tui/src/view/overlays/quit_confirm.rs +++ b/apps/interfaces/tui/src/view/overlays/quit_confirm.rs @@ -4,14 +4,17 @@ use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph}; use ratatui::Frame; use crate::view::theme::Theme; +use tracing::debug; /// Render the Quit confirmation overlay. +#[tracing::instrument(skip_all)] pub fn render( frame: &mut Frame, area: ratatui::layout::Rect, block: Block<'static>, _state: &crate::state::AppStateRest, ) { + debug!("Rendering Quit confirmation overlay"); let block = block .title(Span::styled( " Quit ", diff --git a/apps/interfaces/tui/src/view/overlays/rewind.rs b/apps/interfaces/tui/src/view/overlays/rewind.rs index 095a11b..14e065d 100644 --- a/apps/interfaces/tui/src/view/overlays/rewind.rs +++ b/apps/interfaces/tui/src/view/overlays/rewind.rs @@ -5,15 +5,19 @@ use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph}; use ratatui::Frame; +use tracing::debug; use zesdex_domain::core::Role; /// Render the Rewind overlay. +#[tracing::instrument(skip_all)] pub fn render( frame: &mut Frame, area: ratatui::layout::Rect, block: Block<'static>, state: &crate::state::AppStateRest, ) { + let msg_count = state.transcript_cache.messages.len(); + debug!("Rendering Rewind overlay, {} messages", msg_count); let block = block .title(Span::styled( " Rewind ", diff --git a/apps/interfaces/tui/src/view/overlays/settings.rs b/apps/interfaces/tui/src/view/overlays/settings.rs index 5d3829e..97aef29 100644 --- a/apps/interfaces/tui/src/view/overlays/settings.rs +++ b/apps/interfaces/tui/src/view/overlays/settings.rs @@ -5,14 +5,17 @@ use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph}; use ratatui::Frame; use crate::view::theme::Theme; +use tracing::debug; /// Render the Settings overlay. +#[tracing::instrument(skip_all)] pub fn render( frame: &mut Frame, area: ratatui::layout::Rect, block: Block<'static>, state: &crate::state::AppStateRest, ) { + debug!("Rendering Settings overlay, provider: {}, model: {}", state.settings.provider, state.settings.model); let block = super::overlay_block(block, "Settings", Theme::PRIMARY); let lines = vec![ Line::from(Span::styled( diff --git a/apps/interfaces/tui/src/view/overlays/todo.rs b/apps/interfaces/tui/src/view/overlays/todo.rs index 36796b5..2a12a68 100644 --- a/apps/interfaces/tui/src/view/overlays/todo.rs +++ b/apps/interfaces/tui/src/view/overlays/todo.rs @@ -4,14 +4,18 @@ use ratatui::text::Span; use ratatui::widgets::{Block, Paragraph, Wrap}; use ratatui::Frame; use crate::view::theme::Theme; +use tracing::debug; /// Render the Tasks / Todo overlay. +#[tracing::instrument(skip_all)] pub fn render( frame: &mut Frame, area: ratatui::layout::Rect, block: Block<'static>, state: &crate::state::AppStateRest, ) { + let has_content = !state.misc.todo_content.is_empty(); + debug!("Rendering Tasks overlay, has content: {has_content}"); let block = block .title(Span::styled( " Tasks ", diff --git a/apps/interfaces/tui/src/view/overlays/usage.rs b/apps/interfaces/tui/src/view/overlays/usage.rs index d935dc3..149a5ce 100644 --- a/apps/interfaces/tui/src/view/overlays/usage.rs +++ b/apps/interfaces/tui/src/view/overlays/usage.rs @@ -6,14 +6,18 @@ use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph}; use ratatui::Frame; +use tracing::debug; /// Render the Usage overlay. +#[tracing::instrument(skip_all)] pub fn render( frame: &mut Frame, area: ratatui::layout::Rect, block: Block<'static>, state: &crate::state::AppStateRest, ) { + let has_runtime = state.session_runtime.is_some(); + debug!("Rendering Usage overlay, has runtime: {has_runtime}"); let block = block .title(Span::styled( " Usage ", diff --git a/apps/interfaces/tui/src/view/sidebar.rs b/apps/interfaces/tui/src/view/sidebar.rs index 0dc072f..090475a 100644 --- a/apps/interfaces/tui/src/view/sidebar.rs +++ b/apps/interfaces/tui/src/view/sidebar.rs @@ -134,6 +134,10 @@ fn draw_usage_widget(frame: &mut Frame, area: Rect, state: &crate::state::AppSta frame.render_widget(paragraph, area); } +/// Aggregated usage statistics for the current session. +/// +/// Separates main (chat) and self-learning (review) token counts from +/// the raw `UsageStats` and adds a human-readable elapsed-time breakdown. pub(crate) struct UsageSummary { pub main_tokens: u64, pub self_learning_tokens: u64, @@ -144,6 +148,11 @@ pub(crate) struct UsageSummary { pub elapsed_seconds: i64, } +/// Compute a human-friendly usage summary from raw `UsageStats`. +/// +/// Fields: total_tokens = tokens_in + tokens_out, self_learning = review_tokens, +/// main = total - self_learning. Elapsed time is broken into hours/minutes/seconds. +#[tracing::instrument] pub(crate) fn compute_usage_summary( usage: &zesdex_domain::core::UsageStats, session_start: i64, diff --git a/apps/interfaces/tui/src/view/status.rs b/apps/interfaces/tui/src/view/status.rs index 8f7b648..59e176b 100644 --- a/apps/interfaces/tui/src/view/status.rs +++ b/apps/interfaces/tui/src/view/status.rs @@ -10,8 +10,13 @@ use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::Block; use ratatui::Frame; +use tracing::instrument; -/// Render the single-line status bar. +/// Render the single-line status bar at the bottom of the terminal. +/// +/// Three visual segments: left (app name + PROG/READY/NOAPI badge), +/// center (lesson indicator), right (token count, provider, model). +#[instrument(skip_all)] pub fn draw_status_bar(frame: &mut Frame, area: Rect, state: &crate::state::AppStateRest) { use ratatui::layout::{Alignment, Constraint, Direction, Layout}; let spinner_frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; diff --git a/apps/interfaces/tui/src/view/workflow.rs b/apps/interfaces/tui/src/view/workflow.rs index dfacc5b..4b1944c 100644 --- a/apps/interfaces/tui/src/view/workflow.rs +++ b/apps/interfaces/tui/src/view/workflow.rs @@ -6,6 +6,7 @@ use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Borders, Paragraph, Wrap}; use ratatui::Frame; +use tracing::{debug, instrument}; fn state_icon(state: AgentState) -> &'static str { match state { @@ -34,12 +35,18 @@ fn state_color(state: AgentState) -> Color { } } -/// Render the workflow status panel. +/// Render the workflow status panel showing agent cards and a header. +/// +/// Flow: render titled block → split into header (command hint + status) +/// and body (agent cards with icon/name/label/duration, or session stats +/// when no workflow is running). +#[instrument(skip_all)] pub fn draw_workflow_panel( frame: &mut Frame, area: Rect, state: &crate::state::AppStateRest, ) { + debug!("draw_workflow_panel — rendering workflow panel"); use ratatui::layout::{Constraint, Direction, Layout}; let title = Span::styled( @@ -157,6 +164,11 @@ pub fn draw_workflow_panel( } } +/// Build placeholder lines for the workflow panel body when no agents are running. +/// +/// Shows session stats (message count, tool calls, pending queue, bash jobs) +/// or a "(no active session)" fallback. +#[instrument(skip(state))] fn build_session_lines(state: &crate::state::AppStateRest) -> Vec> { let mut lines: Vec> = Vec::new(); lines.push(Line::from(Span::styled(