1use super::edit_file_tool::{
2 SensitiveSettingsKind, is_sensitive_settings_path, sensitive_settings_kind,
3};
4use crate::{AgentTool, ToolCallEventStream, ToolPermissionDecision, decide_permission_for_paths};
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 paths = vec![input.source_path.clone(), input.destination_path.clone()];
89 let decision = decide_permission_for_paths(Self::NAME, &paths, settings);
90 if let ToolPermissionDecision::Deny(reason) = decision {
91 return Task::ready(Err(anyhow!("{}", reason)));
92 }
93
94 let needs_confirmation = matches!(decision, ToolPermissionDecision::Confirm)
95 || (matches!(decision, ToolPermissionDecision::Allow)
96 && (is_sensitive_settings_path(Path::new(&input.source_path))
97 || is_sensitive_settings_path(Path::new(&input.destination_path))));
98
99 let authorize = if needs_confirmation {
100 let src = MarkdownInlineCode(&input.source_path);
101 let dest = MarkdownInlineCode(&input.destination_path);
102 let context = crate::ToolPermissionContext {
103 tool_name: Self::NAME.to_string(),
104 input_values: vec![input.source_path.clone(), input.destination_path.clone()],
105 };
106 let title = format!("Copy {src} to {dest}");
107 let sensitive_kind = sensitive_settings_kind(Path::new(&input.source_path))
108 .or_else(|| sensitive_settings_kind(Path::new(&input.destination_path)));
109 let title = match sensitive_kind {
110 Some(SensitiveSettingsKind::Local) => format!("{title} (local settings)"),
111 Some(SensitiveSettingsKind::Global) => format!("{title} (settings)"),
112 None => title,
113 };
114 Some(event_stream.authorize(title, context, cx))
115 } else {
116 None
117 };
118
119 let project = self.project.clone();
120 cx.spawn(async move |cx| {
121 if let Some(authorize) = authorize {
122 authorize.await?;
123 }
124
125 let copy_task = project.update(cx, |project, cx| {
126 match project
127 .find_project_path(&input.source_path, cx)
128 .and_then(|project_path| project.entry_for_path(&project_path, cx))
129 {
130 Some(entity) => match project.find_project_path(&input.destination_path, cx) {
131 Some(project_path) => Ok(project.copy_entry(entity.id, project_path, cx)),
132 None => Err(anyhow!(
133 "Destination path {} was outside the project.",
134 input.destination_path
135 )),
136 },
137 None => Err(anyhow!(
138 "Source path {} was not found in the project.",
139 input.source_path
140 )),
141 }
142 })?;
143
144 let result = futures::select! {
145 result = copy_task.fuse() => result,
146 _ = event_stream.cancelled_by_user().fuse() => {
147 anyhow::bail!("Copy cancelled by user");
148 }
149 };
150 result.with_context(|| {
151 format!(
152 "Copying {} to {}",
153 input.source_path, input.destination_path
154 )
155 })?;
156 Ok(format!(
157 "Copied {} to {}",
158 input.source_path, input.destination_path
159 ))
160 })
161 }
162}