Add move_path tool (#27366)

Richard Feldman and Marshall Bowers created

<img width="629" alt="Screenshot 2025-03-24 at 10 06 39 AM"
src="https://github.com/user-attachments/assets/b099fcc0-b2f4-44ee-8c8f-416808363689"
/>

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>

Change summary

crates/assistant_tools/src/assistant_tools.rs            |   3 
crates/assistant_tools/src/move_path_tool.rs             | 125 ++++++++++
crates/assistant_tools/src/move_path_tool/description.md |   5 
3 files changed, 133 insertions(+)

Detailed changes

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<HttpClientWithUrl>, 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);

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.
+    ///
+    /// <example>
+    /// 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"
+    /// </example>
+    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.
+    ///
+    /// <example>
+    /// To move "directory1/a/something.txt" to "directory2/b/renamed.txt",
+    /// provide a destination_path of "directory2/b/renamed.txt"
+    /// </example>
+    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::<MovePathToolInput>(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<Self>,
+        input: serde_json::Value,
+        _messages: &[LanguageModelRequestMessage],
+        project: Entity<Project>,
+        _action_log: Entity<ActionLog>,
+        cx: &mut App,
+    ) -> Task<Result<String>> {
+        let input = match serde_json::from_value::<MovePathToolInput>(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
+                )),
+            }
+        })
+    }
+}

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.