//! Tool for listing the immediate contents of a workspace directory. //! //! Flow: resolve the requested path against workspace roots → validate //! it exists and is a directory → read its direct children with //! `fs::read_dir`, tagging subdirectories with a trailing `/` → format //! into a header + newline-joined listing. //! //! Why: gives the agent a quick, one-level view of the workspace //! structure without pulling in the full recursive directory cache. use std::fs; use serde_json::{json, Value}; use anyhow::{Result, anyhow}; use super::super::Tool; use super::super::ToolCtx; /// Tool that lists the immediate contents of a workspace directory. pub struct DirList; impl Tool for DirList { fn name(&self) -> &'static str { "dir_list" } fn description(&self) -> &'static str { "List files and directories in a directory. Use this to explore the workspace structure." } fn parameters(&self) -> Value { json!({ "type": "object", "properties": { "path": { "type": "string", "description": "Directory path to list (relative to workspace root)" } }, "required": ["path"] }) } /// List the immediate entries of the requested workspace directory. /// /// Flow: extract `path` argument → resolve against workspace roots → /// short-circuit with a plain message if the path doesn't exist or /// isn't a directory → `read_dir` → map each entry to its name /// (appending `/` for subdirectories) → join into a formatted listing /// with an entry-count header showing the canonicalized path. /// /// Why: entries whose metadata fails to read (`e.ok()` filter) are /// silently skipped rather than aborting the whole listing. /// /// Return: header + newline-joined entry names, or an error if the /// `path` argument is missing or `read_dir` fails outright. fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let rel = args.get("path") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow!("missing required argument: path"))?; let path = super::super::resolve_path(&ctx.workspaces, rel)?; if !path.exists() { return Ok(format!("path '{}' does not exist (resolved to {})", rel, path.display())); } if !path.is_dir() { return Ok(format!("path '{}' is not a directory (resolved to {})", rel, path.display())); } let entries: Vec = fs::read_dir(&path) .map_err(|e| anyhow!("failed to read directory '{rel}': {e}"))? .filter_map(std::result::Result::ok) .map(|e| { let name = e.file_name().to_string_lossy().to_string(); let is_dir = e.file_type().is_ok_and(|t| t.is_dir()); if is_dir { format!("{name}/") } else { name } }) .collect(); let canon = path.canonicalize().unwrap_or(path); let header = format!("{} entries in {}:\n", entries.len(), canon.display()); if entries.is_empty() { Ok(format!("{} (empty directory)", header.trim())) } else { Ok(header + &entries.join("\n")) } } }