copy_path_tool.rs

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