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