Add copy-path tool (#27371)

Richard Feldman created

<img width="631" alt="Screenshot 2025-03-24 at 11 01 10 AM"
src="https://github.com/user-attachments/assets/7e144619-83d0-4455-8d80-cc7ec6a7b03e"
/>

Release Notes:

- N/A

Change summary

crates/assistant_tools/src/assistant_tools.rs            |   3 
crates/assistant_tools/src/copy_path_tool.rs             | 114 ++++++++++
crates/assistant_tools/src/copy_path_tool/description.md |   6 
3 files changed, 123 insertions(+)

Detailed changes

crates/assistant_tools/src/assistant_tools.rs 🔗

@@ -1,4 +1,5 @@
 mod bash_tool;
+mod copy_path_tool;
 mod delete_path_tool;
 mod diagnostics_tool;
 mod edit_files_tool;
@@ -14,6 +15,7 @@ mod thinking_tool;
 use std::sync::Arc;
 
 use assistant_tool::ToolRegistry;
+use copy_path_tool::CopyPathTool;
 use gpui::App;
 use http_client::HttpClientWithUrl;
 use move_path_tool::MovePathTool;
@@ -36,6 +38,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
 
     let registry = ToolRegistry::global(cx);
     registry.register_tool(BashTool);
+    registry.register_tool(CopyPathTool);
     registry.register_tool(DeletePathTool);
     registry.register_tool(MovePathTool);
     registry.register_tool(DiagnosticsTool);

crates/assistant_tools/src/copy_path_tool.rs 🔗

@@ -0,0 +1,114 @@
+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::sync::Arc;
+
+#[derive(Debug, Serialize, Deserialize, JsonSchema)]
+pub struct CopyPathToolInput {
+    /// The source path of the file or directory to copy.
+    /// If a directory is specified, its contents will be copied recursively (like `cp -r`).
+    ///
+    /// <example>
+    /// If the project has the following files:
+    ///
+    /// - directory1/a/something.txt
+    /// - directory2/a/things.txt
+    /// - directory3/a/other.txt
+    ///
+    /// You can copy 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 copied to.
+    ///
+    /// <example>
+    /// To copy "directory1/a/something.txt" to "directory2/b/copy.txt",
+    /// provide a destination_path of "directory2/b/copy.txt"
+    /// </example>
+    pub destination_path: String,
+}
+
+pub struct CopyPathTool;
+
+impl Tool for CopyPathTool {
+    fn name(&self) -> String {
+        "copy-path".into()
+    }
+
+    fn needs_confirmation(&self) -> bool {
+        true
+    }
+
+    fn description(&self) -> String {
+        include_str!("./copy_path_tool/description.md").into()
+    }
+
+    fn input_schema(&self) -> serde_json::Value {
+        let schema = schemars::schema_for!(CopyPathToolInput);
+        serde_json::to_value(&schema).unwrap()
+    }
+
+    fn ui_text(&self, input: &serde_json::Value) -> String {
+        match serde_json::from_value::<CopyPathToolInput>(input.clone()) {
+            Ok(input) => {
+                let src = input.source_path.as_str();
+                let dest = input.destination_path.as_str();
+                format!("Copy `{src}` to `{dest}`")
+            }
+            Err(_) => "Copy 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::<CopyPathToolInput>(input) {
+            Ok(input) => input,
+            Err(err) => return Task::ready(Err(anyhow!(err))),
+        };
+        let copy_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.copy_entry(entity.id, None, 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 copy_task.await {
+                Ok(_) => Ok(format!(
+                    "Copied {} to {}",
+                    input.source_path, input.destination_path
+                )),
+                Err(err) => Err(anyhow!(
+                    "Failed to copy {} to {}: {}",
+                    input.source_path,
+                    input.destination_path,
+                    err
+                )),
+            }
+        })
+    }
+}

crates/assistant_tools/src/copy_path_tool/description.md 🔗

@@ -0,0 +1,6 @@
+Copies a file or directory in the project, and returns confirmation that the copy succeeded.
+Directory contents will be copied recursively (like `cp -r`).
+
+This tool should be used when it's desirable to create a copy of a file or directory without modifying the original.
+It's much more efficient than doing this by separately reading and then writing the file or directory's contents,
+so this tool should be preferred over that approach whenever copying is the goal.