use crate::schema::json_schema_for; use anyhow::{Result, anyhow}; use assistant_tool::{ActionLog, Tool, ToolResult}; use gpui::{App, Entity, Task}; use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{fmt::Write, path::Path, sync::Arc}; use ui::IconName; use util::markdown::MarkdownString; #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct ListDirectoryToolInput { /// The relative path of the directory to list. /// /// This path should never be absolute, and the first component /// of the path should always be a root directory in a project. /// /// /// If the project has the following root directories: /// /// - directory1 /// - directory2 /// /// You can list the contents of `directory1` by using the path `directory1`. /// /// /// /// If the project has the following root directories: /// /// - foo /// - bar /// /// If you wanna list contents in the directory `foo/baz`, you should use the path `foo/baz`. /// pub path: String, } pub struct ListDirectoryTool; impl Tool for ListDirectoryTool { fn name(&self) -> String { "list_directory".into() } fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { false } fn description(&self) -> String { include_str!("./list_directory_tool/description.md").into() } fn icon(&self) -> IconName { IconName::Folder } fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { json_schema_for::(format) } fn ui_text(&self, input: &serde_json::Value) -> String { match serde_json::from_value::(input.clone()) { Ok(input) => { let path = MarkdownString::inline_code(&input.path); format!("List the {path} directory's contents") } Err(_) => "List directory".to_string(), } } fn run( self: Arc, input: serde_json::Value, _messages: &[LanguageModelRequestMessage], project: Entity, _action_log: Entity, cx: &mut App, ) -> ToolResult { let input = match serde_json::from_value::(input) { Ok(input) => input, Err(err) => return Task::ready(Err(anyhow!(err))).into(), }; // Sometimes models will return these even though we tell it to give a path and not a glob. // When this happens, just list the root worktree directories. if matches!(input.path.as_str(), "." | "" | "./" | "*") { let output = project .read(cx) .worktrees(cx) .filter_map(|worktree| { worktree.read(cx).root_entry().and_then(|entry| { if entry.is_dir() { entry.path.to_str() } else { None } }) }) .collect::>() .join("\n"); return Task::ready(Ok(output)).into(); } let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else { return Task::ready(Err(anyhow!("Path {} not found in project", input.path))).into(); }; let Some(worktree) = project .read(cx) .worktree_for_id(project_path.worktree_id, cx) else { return Task::ready(Err(anyhow!("Worktree not found"))).into(); }; let worktree = worktree.read(cx); let Some(entry) = worktree.entry_for_path(&project_path.path) else { return Task::ready(Err(anyhow!("Path not found: {}", input.path))).into(); }; if !entry.is_dir() { return Task::ready(Err(anyhow!("{} is not a directory.", input.path))).into(); } let mut output = String::new(); for entry in worktree.child_entries(&project_path.path) { writeln!( output, "{}", Path::new(worktree.root_name()).join(&entry.path).display(), ) .unwrap(); } if output.is_empty() { return Task::ready(Ok(format!("{} is empty.", input.path))).into(); } Task::ready(Ok(output)).into() } }