From 16494d4b1ec90cd38b40a98f04ff67819d6cb576 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Mon, 20 Jul 2026 15:53:20 +0700 Subject: [PATCH] Enhance logging and documentation across utility tools and TUI overlays - Added tracing instrumentation and improved logging messages in the Pong, Todofinish, and Todowrite tools for better debugging and monitoring. - Enhanced documentation comments for clarity on tool functionalities and workflows. - Implemented tracing in WorkflowRun, NoteFinding, ReadFindings, and HiveMind tools to track execution phases and findings. - Updated TUI overlays (e.g., Bash, Clear Confirm, Editor, Effort Level, Help, Key Input, Learning, Loading, MCP, Model Selector, Plan, Quit Confirm, Rewind, Settings, Todo, Usage) with debug logging to capture rendering details. - Improved the status bar and workflow panel rendering with additional debug information. - Added tracing to various utility functions to facilitate better performance monitoring and error tracking. --- apps/infrastructure/src/subagent/engine.rs | 3 +- apps/infrastructure/src/subagent/gating.rs | 9 ++++++ apps/infrastructure/src/subagent/provider.rs | 5 ++++ apps/infrastructure/src/subagent/spawn.rs | 3 +- apps/infrastructure/src/subagent/tools.rs | 8 ++++- apps/infrastructure/src/subagent/workspace.rs | 6 ++++ apps/infrastructure/src/tools/bash_tools.rs | 15 ++++++++-- apps/infrastructure/src/tools/fs/delete.rs | 8 +++++ apps/infrastructure/src/tools/fs/edit.rs | 9 ++++++ apps/infrastructure/src/tools/fs/helpers.rs | 2 ++ apps/infrastructure/src/tools/fs/read.rs | 8 +++++ apps/infrastructure/src/tools/fs/write.rs | 9 ++++++ apps/infrastructure/src/tools/git/git_cred.rs | 13 +++++++++ .../src/tools/git/git_operator.rs | 13 +++++++++ .../src/tools/git/git_worktree.rs | 13 +++++++++ .../src/tools/lsp/completion.rs | 13 ++++++++- apps/infrastructure/src/tools/lsp/connect.rs | 14 ++++++++- .../src/tools/lsp/definition.rs | 14 ++++++++- .../src/tools/lsp/diagnostics.rs | 14 ++++++++- .../src/tools/lsp/disconnect.rs | 14 ++++++++- apps/infrastructure/src/tools/lsp/hover.rs | 14 ++++++++- .../src/tools/lsp/references.rs | 14 ++++++++- .../infrastructure/src/tools/memory/forget.rs | 11 +++++++ .../infrastructure/src/tools/memory/recall.rs | 13 +++++++++ .../src/tools/memory/remember.rs | 13 +++++++++ apps/infrastructure/src/tools/mod.rs | 24 ++++++++++++++- apps/infrastructure/src/tools/plan.rs | 27 ++++++++++++++--- apps/infrastructure/src/tools/search.rs | 23 +++++++++++++++ .../src/tools/sequential_think.rs | 11 +++++++ apps/infrastructure/src/tools/shell.rs | 16 ++++++++++ .../src/tools/shell_filter/credentials.rs | 9 ++++++ .../src/tools/shell_filter/git.rs | 9 ++++++ apps/infrastructure/src/tools/spawn.rs | 14 ++++++++- apps/infrastructure/src/tools/utility/cd.rs | 10 +++++++ .../src/tools/utility/dir_cache_update.rs | 13 +++++++++ .../src/tools/utility/dir_list.rs | 12 ++++++++ apps/infrastructure/src/tools/utility/pong.rs | 9 ++++++ .../src/tools/utility/todofinish.rs | 15 ++++++++++ .../src/tools/utility/todowrite.rs | 14 +++++++++ apps/infrastructure/src/tools/workflow.rs | 29 +++++++++++++++++-- apps/infrastructure/src/workflow/docs.rs | 3 +- .../src/workflow/engine/execution.rs | 3 +- apps/infrastructure/src/workflow/script.rs | 3 +- apps/interfaces/tui/src/action.rs | 6 ++-- apps/interfaces/tui/src/controller/command.rs | 8 ++++- apps/interfaces/tui/src/controller/input.rs | 21 ++++++++++++-- apps/interfaces/tui/src/run.rs | 17 +++++++++++ apps/interfaces/tui/src/state.rs | 7 +++++ apps/interfaces/tui/src/turn.rs | 15 ++++++++++ apps/interfaces/tui/src/view/overlays/bash.rs | 3 ++ .../tui/src/view/overlays/clear_confirm.rs | 3 ++ .../tui/src/view/overlays/editor.rs | 3 ++ .../tui/src/view/overlays/effort.rs | 4 +++ apps/interfaces/tui/src/view/overlays/help.rs | 3 ++ .../tui/src/view/overlays/key_input.rs | 4 +++ .../tui/src/view/overlays/learning.rs | 4 +++ .../tui/src/view/overlays/loading.rs | 3 ++ apps/interfaces/tui/src/view/overlays/mcp.rs | 3 ++ apps/interfaces/tui/src/view/overlays/mod.rs | 6 ++++ .../tui/src/view/overlays/model_selector.rs | 3 ++ apps/interfaces/tui/src/view/overlays/plan.rs | 4 +++ .../tui/src/view/overlays/quit_confirm.rs | 3 ++ .../tui/src/view/overlays/rewind.rs | 4 +++ .../tui/src/view/overlays/settings.rs | 3 ++ apps/interfaces/tui/src/view/overlays/todo.rs | 4 +++ .../interfaces/tui/src/view/overlays/usage.rs | 4 +++ apps/interfaces/tui/src/view/sidebar.rs | 9 ++++++ apps/interfaces/tui/src/view/status.rs | 7 ++++- apps/interfaces/tui/src/view/workflow.rs | 14 ++++++++- 69 files changed, 637 insertions(+), 30 deletions(-) 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(