copy_path_tool.rs

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