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::{path::Path, sync::Arc};
9use util::markdown::MarkdownInlineCode;
10
11/// Moves or rename a file or directory in the project, and returns confirmation
12/// that the move succeeded.
13///
14/// If the source and destination directories are the same, but the filename is
15/// different, this performs a rename. Otherwise, it performs a move.
16///
17/// This tool should be used when it's desirable to move or rename a file or
18/// directory without changing its contents at all.
19#[derive(Debug, Serialize, Deserialize, JsonSchema)]
20pub struct MovePathToolInput {
21 /// The source path of the file or directory to move/rename.
22 ///
23 /// <example>
24 /// If the project has the following files:
25 ///
26 /// - directory1/a/something.txt
27 /// - directory2/a/things.txt
28 /// - directory3/a/other.txt
29 ///
30 /// You can move the first file by providing a source_path of "directory1/a/something.txt"
31 /// </example>
32 pub source_path: String,
33
34 /// The destination path where the file or directory should be moved/renamed to.
35 /// If the paths are the same except for the filename, then this will be a rename.
36 ///
37 /// <example>
38 /// To move "directory1/a/something.txt" to "directory2/b/renamed.txt",
39 /// provide a destination_path of "directory2/b/renamed.txt"
40 /// </example>
41 pub destination_path: String,
42}
43
44pub struct MovePathTool {
45 project: Entity<Project>,
46}
47
48impl MovePathTool {
49 pub fn new(project: Entity<Project>) -> Self {
50 Self { project }
51 }
52}
53
54impl AgentTool for MovePathTool {
55 type Input = MovePathToolInput;
56 type Output = String;
57
58 fn name(&self) -> SharedString {
59 "move_path".into()
60 }
61
62 fn kind(&self) -> ToolKind {
63 ToolKind::Move
64 }
65
66 fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
67 if let Ok(input) = input {
68 let src = MarkdownInlineCode(&input.source_path);
69 let dest = MarkdownInlineCode(&input.destination_path);
70 let src_path = Path::new(&input.source_path);
71 let dest_path = Path::new(&input.destination_path);
72
73 match dest_path
74 .file_name()
75 .and_then(|os_str| os_str.to_os_string().into_string().ok())
76 {
77 Some(filename) if src_path.parent() == dest_path.parent() => {
78 let filename = MarkdownInlineCode(&filename);
79 format!("Rename {src} to {filename}").into()
80 }
81 _ => format!("Move {src} to {dest}").into(),
82 }
83 } else {
84 "Move path".into()
85 }
86 }
87
88 fn run(
89 self: Arc<Self>,
90 input: Self::Input,
91 _event_stream: ToolCallEventStream,
92 cx: &mut App,
93 ) -> Task<Result<Self::Output>> {
94 let rename_task = self.project.update(cx, |project, cx| {
95 match project
96 .find_project_path(&input.source_path, cx)
97 .and_then(|project_path| project.entry_for_path(&project_path, cx))
98 {
99 Some(entity) => match project.find_project_path(&input.destination_path, cx) {
100 Some(project_path) => project.rename_entry(entity.id, project_path.path, cx),
101 None => Task::ready(Err(anyhow!(
102 "Destination path {} was outside the project.",
103 input.destination_path
104 ))),
105 },
106 None => Task::ready(Err(anyhow!(
107 "Source path {} was not found in the project.",
108 input.source_path
109 ))),
110 }
111 });
112
113 cx.background_spawn(async move {
114 let _ = rename_task.await.with_context(|| {
115 format!("Moving {} to {}", input.source_path, input.destination_path)
116 })?;
117 Ok(format!(
118 "Moved {} to {}",
119 input.source_path, input.destination_path
120 ))
121 })
122 }
123}