copy_path_tool.rs

  1use crate::{AgentTool, ToolCallEventStream};
  2use agent_client_protocol::ToolKind;
  3use anyhow::{Context as _, Result, anyhow};
  4use futures::FutureExt as _;
  5use gpui::{App, AppContext, Entity, Task};
  6use project::Project;
  7use schemars::JsonSchema;
  8use serde::{Deserialize, Serialize};
  9use std::sync::Arc;
 10use util::markdown::MarkdownInlineCode;
 11
 12/// Copies a file or directory in the project, and returns confirmation that the copy succeeded.
 13/// Directory contents will be copied recursively.
 14///
 15/// This tool should be used when it's desirable to create a copy of a file or directory without modifying the original.
 16/// 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.
 17#[derive(Debug, Serialize, Deserialize, JsonSchema)]
 18pub struct CopyPathToolInput {
 19    /// The source path of the file or directory to copy.
 20    /// If a directory is specified, its contents will be copied recursively.
 21    ///
 22    /// <example>
 23    /// If the project has the following files:
 24    ///
 25    /// - directory1/a/something.txt
 26    /// - directory2/a/things.txt
 27    /// - directory3/a/other.txt
 28    ///
 29    /// You can copy the first file by providing a source_path of "directory1/a/something.txt"
 30    /// </example>
 31    pub source_path: String,
 32    /// The destination path where the file or directory should be copied to.
 33    ///
 34    /// <example>
 35    /// To copy "directory1/a/something.txt" to "directory2/b/copy.txt", provide a destination_path of "directory2/b/copy.txt"
 36    /// </example>
 37    pub destination_path: String,
 38}
 39
 40pub struct CopyPathTool {
 41    project: Entity<Project>,
 42}
 43
 44impl CopyPathTool {
 45    pub fn new(project: Entity<Project>) -> Self {
 46        Self { project }
 47    }
 48}
 49
 50impl AgentTool for CopyPathTool {
 51    type Input = CopyPathToolInput;
 52    type Output = String;
 53
 54    fn name() -> &'static str {
 55        "copy_path"
 56    }
 57
 58    fn kind() -> ToolKind {
 59        ToolKind::Move
 60    }
 61
 62    fn initial_title(
 63        &self,
 64        input: Result<Self::Input, serde_json::Value>,
 65        _cx: &mut App,
 66    ) -> ui::SharedString {
 67        if let Ok(input) = input {
 68            let src = MarkdownInlineCode(&input.source_path);
 69            let dest = MarkdownInlineCode(&input.destination_path);
 70            format!("Copy {src} to {dest}").into()
 71        } else {
 72            "Copy path".into()
 73        }
 74    }
 75
 76    fn run(
 77        self: Arc<Self>,
 78        input: Self::Input,
 79        event_stream: ToolCallEventStream,
 80        cx: &mut App,
 81    ) -> Task<Result<Self::Output>> {
 82        let copy_task = self.project.update(cx, |project, cx| {
 83            match project
 84                .find_project_path(&input.source_path, cx)
 85                .and_then(|project_path| project.entry_for_path(&project_path, cx))
 86            {
 87                Some(entity) => match project.find_project_path(&input.destination_path, cx) {
 88                    Some(project_path) => project.copy_entry(entity.id, project_path, cx),
 89                    None => Task::ready(Err(anyhow!(
 90                        "Destination path {} was outside the project.",
 91                        input.destination_path
 92                    ))),
 93                },
 94                None => Task::ready(Err(anyhow!(
 95                    "Source path {} was not found in the project.",
 96                    input.source_path
 97                ))),
 98            }
 99        });
100
101        cx.background_spawn(async move {
102            let result = futures::select! {
103                result = copy_task.fuse() => result,
104                _ = event_stream.cancelled_by_user().fuse() => {
105                    anyhow::bail!("Copy cancelled by user");
106                }
107            };
108            let _ = result.with_context(|| {
109                format!(
110                    "Copying {} to {}",
111                    input.source_path, input.destination_path
112                )
113            })?;
114            Ok(format!(
115                "Copied {} to {}",
116                input.source_path, input.destination_path
117            ))
118        })
119    }
120}