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.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<dyn Tool>]` slice to
|
||||
/// `Vec<ToolDef>` 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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<PathBuf> {
|
||||
let ws = base_dir.join("subagent-workspaces").join(agent_id);
|
||||
std::fs::create_dir_all(&ws)?;
|
||||
|
||||
@@ -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<String> {
|
||||
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<String> {
|
||||
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}'")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> {
|
||||
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")
|
||||
|
||||
@@ -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<String> {
|
||||
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,
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
@@ -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<String> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> {
|
||||
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(),
|
||||
|
||||
@@ -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<String> {
|
||||
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),
|
||||
|
||||
@@ -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<String> {
|
||||
let operation = crate::tools::arg_str(args, "operation")?;
|
||||
let extra_args: Vec<String> = 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> {
|
||||
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"]),
|
||||
)?;
|
||||
|
||||
@@ -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<String> {
|
||||
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()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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<String> {
|
||||
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}'"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> {
|
||||
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()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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<String> {
|
||||
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()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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<String> {
|
||||
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}'"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> {
|
||||
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()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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<String> {
|
||||
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()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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<String> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> {
|
||||
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")))
|
||||
|
||||
@@ -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<String> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> {
|
||||
}
|
||||
|
||||
/// 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<String> {
|
||||
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<String> {
|
||||
.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<PathBuf> {
|
||||
let (ws_idx, path) = if rel.starts_with('[') {
|
||||
let close = rel
|
||||
@@ -284,15 +297,21 @@ pub fn resolve_path(workspaces: &[PathBuf], rel: &str) -> Result<PathBuf> {
|
||||
}
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String> {
|
||||
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<String> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> {
|
||||
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<String> {
|
||||
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"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> {
|
||||
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,
|
||||
|
||||
@@ -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<String> {
|
||||
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));
|
||||
|
||||
@@ -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<String> {
|
||||
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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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<String> {
|
||||
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<String> {
|
||||
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(),
|
||||
|
||||
@@ -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<String> {
|
||||
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()))
|
||||
}
|
||||
|
||||
@@ -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<RwLock<DirCache>>` 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<String> {
|
||||
let paths: Vec<String> = args
|
||||
.get("paths")
|
||||
@@ -47,6 +58,7 @@ impl crate::tools::Tool for DirCacheUpdate {
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> {
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -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<String> {
|
||||
info!("pong invoked — responding with 'pong'");
|
||||
Ok("pong".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> {
|
||||
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))
|
||||
|
||||
@@ -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<String> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> {
|
||||
let yaml = arg_str(args, "workflow_yaml")?;
|
||||
let script = WorkflowScript::parse(&yaml)?;
|
||||
@@ -66,6 +72,7 @@ impl Tool for WorkflowRun {
|
||||
let result: Vec<String> =
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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 })?;
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Self> {
|
||||
let parsed: serde_json::Value = serde_yaml_ng::from_str(yaml)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to parse workflow YAML: {e}"))?;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<crate::action::Action> {
|
||||
match cmd {
|
||||
Command::Help => {
|
||||
@@ -155,11 +159,13 @@ pub fn apply_command(cmd: Command) -> Vec<crate::action::Action> {
|
||||
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);
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
//! 3. The main match handles navigation, auto-complete, editing, and shortcuts.
|
||||
//! 4. Multi-key actions return `Vec<Action>`.
|
||||
|
||||
use tracing::debug;
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
|
||||
use crate::action::Action;
|
||||
@@ -25,8 +27,9 @@ fn mark(state: &mut AppStateRest) -> Vec<Action> {
|
||||
/// based on the current application state.
|
||||
///
|
||||
/// Return: `Vec<Action>` so a single key can produce multiple queued actions.
|
||||
#[tracing::instrument(skip(state))]
|
||||
pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec<Action> {
|
||||
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<Action> {
|
||||
}
|
||||
|
||||
/// 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<Action> {
|
||||
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<Action> {
|
||||
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();
|
||||
|
||||
@@ -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<CrosstermBackend<io::Stdout>>,
|
||||
) -> 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<CrosstermBackend<io::Stdout>>,
|
||||
) -> 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();
|
||||
|
||||
@@ -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<LearningItem> {
|
||||
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() {
|
||||
|
||||
@@ -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<ChatMessage>,
|
||||
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);
|
||||
|
||||
|
||||
@@ -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<Line> = state
|
||||
.session_runtime
|
||||
|
||||
@@ -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 ",
|
||||
|
||||
@@ -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 ",
|
||||
|
||||
@@ -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 ",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 ",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 ",
|
||||
|
||||
@@ -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 ",
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 ",
|
||||
|
||||
@@ -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 ",
|
||||
|
||||
@@ -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 ",
|
||||
|
||||
@@ -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 ",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 ",
|
||||
|
||||
@@ -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 ",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||
|
||||
@@ -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<Line<'static>> {
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
lines.push(Line::from(Span::styled(
|
||||
|
||||
Reference in New Issue
Block a user