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 const NAME: &'static str = "copy_path";
59
60 fn kind() -> ToolKind {
61 ToolKind::Move
62 }
63
64 fn initial_title(
65 &self,
66 input: Result<Self::Input, serde_json::Value>,
67 _cx: &mut App,
68 ) -> ui::SharedString {
69 if let Ok(input) = input {
70 let src = MarkdownInlineCode(&input.source_path);
71 let dest = MarkdownInlineCode(&input.destination_path);
72 format!("Copy {src} to {dest}").into()
73 } else {
74 "Copy path".into()
75 }
76 }
77
78 fn run(
79 self: Arc<Self>,
80 input: Self::Input,
81 event_stream: ToolCallEventStream,
82 cx: &mut App,
83 ) -> Task<Result<Self::Output>> {
84 let settings = AgentSettings::get_global(cx);
85
86 let source_decision =
87 decide_permission_from_settings(Self::NAME, &input.source_path, settings);
88 if let ToolPermissionDecision::Deny(reason) = source_decision {
89 return Task::ready(Err(anyhow!("{}", reason)));
90 }
91
92 let dest_decision =
93 decide_permission_from_settings(Self::NAME, &input.destination_path, settings);
94 if let ToolPermissionDecision::Deny(reason) = dest_decision {
95 return Task::ready(Err(anyhow!("{}", reason)));
96 }
97
98 let needs_confirmation = matches!(source_decision, ToolPermissionDecision::Confirm)
99 || matches!(dest_decision, ToolPermissionDecision::Confirm);
100
101 let authorize = if needs_confirmation {
102 let src = MarkdownInlineCode(&input.source_path);
103 let dest = MarkdownInlineCode(&input.destination_path);
104 let context = crate::ToolPermissionContext {
105 tool_name: Self::NAME.to_string(),
106 input_value: input.source_path.clone(),
107 };
108 Some(event_stream.authorize(format!("Copy {src} to {dest}"), context, cx))
109 } else {
110 None
111 };
112
113 let copy_task = self.project.update(cx, |project, cx| {
114 match project
115 .find_project_path(&input.source_path, cx)
116 .and_then(|project_path| project.entry_for_path(&project_path, cx))
117 {
118 Some(entity) => match project.find_project_path(&input.destination_path, cx) {
119 Some(project_path) => project.copy_entry(entity.id, project_path, cx),
120 None => Task::ready(Err(anyhow!(
121 "Destination path {} was outside the project.",
122 input.destination_path
123 ))),
124 },
125 None => Task::ready(Err(anyhow!(
126 "Source path {} was not found in the project.",
127 input.source_path
128 ))),
129 }
130 });
131
132 cx.background_spawn(async move {
133 if let Some(authorize) = authorize {
134 authorize.await?;
135 }
136
137 let result = futures::select! {
138 result = copy_task.fuse() => result,
139 _ = event_stream.cancelled_by_user().fuse() => {
140 anyhow::bail!("Copy cancelled by user");
141 }
142 };
143 let _ = result.with_context(|| {
144 format!(
145 "Copying {} to {}",
146 input.source_path, input.destination_path
147 )
148 })?;
149 Ok(format!(
150 "Copied {} to {}",
151 input.source_path, input.destination_path
152 ))
153 })
154 }
155}