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. /// /// <example> /// If the project has the following root directories: /// /// - directory1 /// - directory2 /// /// You can list the contents of `directory1` by using the path `directory1`. /// </example> /// /// <example> /// 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`. /// </example> 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<serde_json::Value> { json_schema_for::<ListDirectoryToolInput>(format) } fn ui_text(&self, input: &serde_json::Value) -> String { match serde_json::from_value::<ListDirectoryToolInput>(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<Self>, input: serde_json::Value, _messages: &[LanguageModelRequestMessage], project: Entity<Project>, _action_log: Entity<ActionLog>, cx: &mut App, ) -> ToolResult { let input = match serde_json::from_value::<ListDirectoryToolInput>(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::<Vec<_>>() .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() }}
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.
///
/// <example>
/// If the project has the following root directories:
///
/// - directory1
/// - directory2
///
/// You can list the contents of `directory1` by using the path `directory1`.
/// </example>
///
/// <example>
/// 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`.
/// </example>
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<serde_json::Value> {
json_schema_for::<ListDirectoryToolInput>(format)
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<ListDirectoryToolInput>(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<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<ListDirectoryToolInput>(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::<Vec<_>>()
.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()
}
}