use rig::tool::{Tool, ToolContext};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::{path::PathBuf, sync::Arc};
use tokio::sync::Mutex;
use crate::common::{
package_resolver::PackageResolver,
path_traversal::{DirectoryListing, PathTraversal},
};
#[derive(Debug, Deserialize, Serialize)]
pub struct ListDirArgs {
path: PathBuf,
#[serde(default)]
start: Option<usize>,
#[serde(default)]
root: Option<String>,
}
pub struct ListDir {
package_resolver: Arc<Mutex<PackageResolver>>,
path_traversal: Arc<Mutex<PathTraversal>>,
max_results: Option<usize>,
has_pacmans: bool, ignore_description: String,
}
impl ListDir {
pub async fn new(
package_resolver: Arc<Mutex<PackageResolver>>,
path_traversal: Arc<Mutex<PathTraversal>>,
max_results: Option<usize>,
) -> Self {
let has_pacmans = !package_resolver.lock().await.list_pacmans().is_empty();
let ignore_description = {
let pt = path_traversal.lock().await;
super::ignore_description(pt.observe_hidden(), pt.observe_ignore())
};
Self {
package_resolver,
path_traversal,
max_results,
has_pacmans,
ignore_description,
}
}
}
impl Tool for ListDir {
const NAME: &'static str = "list_dir";
type Error = super::ToolError;
type Args = ListDirArgs;
type Output = DirectoryListing;
#[tracing::instrument(skip(self, _context))]
async fn call(
&self,
_context: &mut ToolContext,
args: Self::Args,
) -> Result<Self::Output, Self::Error> {
let package_resolver = self.package_resolver.lock().await;
let path_traversal = self.path_traversal.lock().await;
let package_root = package_resolver
.package_root(args.root.as_deref())?
.map(PathBuf::from);
let mut result = path_traversal.list_dir(&args.path, false, package_root.as_deref())?;
result.total_entries = result.entries.len();
if let Some(max_results) = self.max_results
&& result.entries.len() > max_results
{
result.entries.truncate(max_results);
result.truncated = true;
}
Ok(result)
}
fn description(&self) -> String {
if self.has_pacmans {
format!(
"List the files and directories in the given directory (relative to the selected root namespace), \
returning {} entries. Use an empty string \"\" for the selected root namespace. \
Results include both files and subdirectories with their types.{}",
match self.max_results {
Some(val) => format!("at most {}", val),
None => "all".to_string(),
},
self.ignore_description
)
} else {
format!(
"List the files and directories in the given directory (relative to workspace root), \
returning {} entries. Use an empty string \"\" for the workspace root. \
Results include both files and subdirectories with their types.{}",
match self.max_results {
Some(val) => format!("at most {}", val),
None => "all".to_string(),
},
self.ignore_description
)
}
}
fn parameters(&self) -> serde_json::Value {
let path_description = format!(
"Relative path to the directory *within* the {}. \
Use an empty string `\"\"` to list the root of the selected scope. \
**Never** include {}absolute paths here. \
Example: `path: \"\"`{}.",
if self.has_pacmans {
"selected root namespace"
} else {
"workspace root"
},
if self.has_pacmans {
"package identifiers (like `cargo:...`) or "
} else {
""
},
if self.has_pacmans {
", `root: \"cargo:rig@0.42.0\"`"
} else {
""
}
);
let mut params = json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": path_description
},
},
"required": ["path"]
});
if self.has_pacmans {
if let Some(props) = params["properties"].as_object_mut() {
props.insert(
"root".to_string(),
json!({
"type": "string",
"description": "Identifier of a package or dependency (e.g., `cargo:crate@version`) to use as the starting namespace. \
If omitted, defaults to the workspace root. \
This acts as a scope selector, completely independent of the relative `path` parameter.",
}),
);
}
}
params
}
fn map_error(&self, error: Self::Error) -> rig::tool::ToolExecutionError {
rig::tool::ToolExecutionError::from_error(error)
}
}