1use crate::{AgentTool, ToolCallEventStream};
2use agent_client_protocol::ToolKind;
3use anyhow::{Context as _, Result, anyhow};
4use gpui::{App, AppContext, Entity, SharedString, Task};
5use project::Project;
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use std::sync::Arc;
9use util::markdown::MarkdownInlineCode;
10
11/// Copies a file or directory in the project, and returns confirmation that the
12/// copy succeeded.
13///
14/// Directory contents will be copied recursively (like `cp -r`).
15///
16/// This tool should be used when it's desirable to create a copy of a file or
17/// directory without modifying the original. It's much more efficient than
18/// doing this by separately reading and then writing the file or directory's
19/// contents, so this tool should be preferred over that approach whenever
20/// 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 (like `cp -r`).
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
37 /// The destination path where the file or directory should be copied to.
38 ///
39 /// <example>
40 /// To copy "directory1/a/something.txt" to "directory2/b/copy.txt",
41 /// 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 fn name(&self) -> SharedString {
61 "copy_path".into()
62 }
63
64 fn kind(&self) -> ToolKind {
65 ToolKind::Move
66 }
67
68 fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> 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 copy_task = self.project.update(cx, |project, cx| {
85 match project
86 .find_project_path(&input.source_path, cx)
87 .and_then(|project_path| project.entry_for_path(&project_path, cx))
88 {
89 Some(entity) => match project.find_project_path(&input.destination_path, cx) {
90 Some(project_path) => {
91 project.copy_entry(entity.id, None, project_path.path, cx)
92 }
93 None => Task::ready(Err(anyhow!(
94 "Destination path {} was outside the project.",
95 input.destination_path
96 ))),
97 },
98 None => Task::ready(Err(anyhow!(
99 "Source path {} was not found in the project.",
100 input.source_path
101 ))),
102 }
103 });
104
105 cx.background_spawn(async move {
106 let _ = copy_task.await.with_context(|| {
107 format!(
108 "Copying {} to {}",
109 input.source_path, input.destination_path
110 )
111 })?;
112 Ok(format!(
113 "Copied {} to {}",
114 input.source_path, input.destination_path
115 ))
116 })
117 }
118}