diff --git a/crates/assistant_tools/src/assistant_tools.rs b/crates/assistant_tools/src/assistant_tools.rs index daa471e6e5f77fdb75b4e6b6a12ad7f92ae66e3b..53611c8fbfbbe5200c691ff1c4151af7c02fef99 100644 --- a/crates/assistant_tools/src/assistant_tools.rs +++ b/crates/assistant_tools/src/assistant_tools.rs @@ -4,6 +4,7 @@ mod diagnostics_tool; mod edit_files_tool; mod fetch_tool; mod list_directory_tool; +mod move_path_tool; mod now_tool; mod path_search_tool; mod read_file_tool; @@ -15,6 +16,7 @@ use std::sync::Arc; use assistant_tool::ToolRegistry; use gpui::App; use http_client::HttpClientWithUrl; +use move_path_tool::MovePathTool; use crate::bash_tool::BashTool; use crate::delete_path_tool::DeletePathTool; @@ -35,6 +37,7 @@ pub fn init(http_client: Arc, cx: &mut App) { let registry = ToolRegistry::global(cx); registry.register_tool(BashTool); registry.register_tool(DeletePathTool); + registry.register_tool(MovePathTool); registry.register_tool(DiagnosticsTool); registry.register_tool(EditFilesTool); registry.register_tool(ListDirectoryTool); diff --git a/crates/assistant_tools/src/move_path_tool.rs b/crates/assistant_tools/src/move_path_tool.rs new file mode 100644 index 0000000000000000000000000000000000000000..09b04fcad8258f92aa29abde171ae59b1e1fead2 --- /dev/null +++ b/crates/assistant_tools/src/move_path_tool.rs @@ -0,0 +1,125 @@ +use anyhow::{anyhow, Result}; +use assistant_tool::{ActionLog, Tool}; +use gpui::{App, AppContext, Entity, Task}; +use language_model::LanguageModelRequestMessage; +use project::Project; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::{path::Path, sync::Arc}; + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct MovePathToolInput { + /// The source path of the file or directory to move/rename. + /// + /// + /// If the project has the following files: + /// + /// - directory1/a/something.txt + /// - directory2/a/things.txt + /// - directory3/a/other.txt + /// + /// You can move the first file by providing a source_path of "directory1/a/something.txt" + /// + pub source_path: String, + + /// The destination path where the file or directory should be moved/renamed to. + /// If the paths are the same except for the filename, then this will be a rename. + /// + /// + /// To move "directory1/a/something.txt" to "directory2/b/renamed.txt", + /// provide a destination_path of "directory2/b/renamed.txt" + /// + pub destination_path: String, +} + +pub struct MovePathTool; + +impl Tool for MovePathTool { + fn name(&self) -> String { + "move-path".into() + } + + fn needs_confirmation(&self) -> bool { + true + } + + fn description(&self) -> String { + include_str!("./move_path_tool/description.md").into() + } + + fn input_schema(&self) -> serde_json::Value { + let schema = schemars::schema_for!(MovePathToolInput); + serde_json::to_value(&schema).unwrap() + } + + fn ui_text(&self, input: &serde_json::Value) -> String { + match serde_json::from_value::(input.clone()) { + Ok(input) => { + let src = input.source_path.as_str(); + let dest = input.destination_path.as_str(); + let src_path = Path::new(src); + let dest_path = Path::new(dest); + + match dest_path + .file_name() + .and_then(|os_str| os_str.to_os_string().into_string().ok()) + { + Some(filename) if src_path.parent() == dest_path.parent() => { + format!("Rename `{src}` to `{filename}`") + } + _ => { + format!("Move `{src}` to `{dest}`") + } + } + } + Err(_) => "Move path".to_string(), + } + } + + fn run( + self: Arc, + input: serde_json::Value, + _messages: &[LanguageModelRequestMessage], + project: Entity, + _action_log: Entity, + cx: &mut App, + ) -> Task> { + let input = match serde_json::from_value::(input) { + Ok(input) => input, + Err(err) => return Task::ready(Err(anyhow!(err))), + }; + let rename_task = project.update(cx, |project, cx| { + match project + .find_project_path(&input.source_path, cx) + .and_then(|project_path| project.entry_for_path(&project_path, cx)) + { + Some(entity) => match project.find_project_path(&input.destination_path, cx) { + Some(project_path) => project.rename_entry(entity.id, project_path.path, cx), + None => Task::ready(Err(anyhow!( + "Destination path {} was outside the project.", + input.destination_path + ))), + }, + None => Task::ready(Err(anyhow!( + "Source path {} was not found in the project.", + input.source_path + ))), + } + }); + + cx.background_spawn(async move { + match rename_task.await { + Ok(_) => Ok(format!( + "Moved {} to {}", + input.source_path, input.destination_path + )), + Err(err) => Err(anyhow!( + "Failed to move {} to {}: {}", + input.source_path, + input.destination_path, + err + )), + } + }) + } +} diff --git a/crates/assistant_tools/src/move_path_tool/description.md b/crates/assistant_tools/src/move_path_tool/description.md new file mode 100644 index 0000000000000000000000000000000000000000..76bc3003d003c44afdd9036cb6691d5fc432291d --- /dev/null +++ b/crates/assistant_tools/src/move_path_tool/description.md @@ -0,0 +1,5 @@ +Moves or rename a file or directory in the project, and returns confirmation that the move succeeded. +If the source and destination directories are the same, but the filename is different, this performs +a rename. Otherwise, it performs a move. + +This tool should be used when it's desirable to move or rename a file or directory without changing its contents at all.