1use crate::{
2 AgentTool, ToolCallEventStream, ToolPermissionDecision, decide_permission_from_settings,
3};
4use action_log::ActionLog;
5use agent_client_protocol::ToolKind;
6use agent_settings::AgentSettings;
7use anyhow::{Context as _, Result, anyhow};
8use futures::{FutureExt as _, SinkExt, StreamExt, channel::mpsc};
9use gpui::{App, AppContext, Entity, SharedString, Task};
10use project::{Project, ProjectPath};
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13use settings::Settings;
14use std::sync::Arc;
15use util::markdown::MarkdownInlineCode;
16
17/// Deletes the file or directory (and the directory's contents, recursively) at the specified path in the project, and returns confirmation of the deletion.
18#[derive(Debug, Serialize, Deserialize, JsonSchema)]
19pub struct DeletePathToolInput {
20 /// The path of the file or directory to delete.
21 ///
22 /// <example>
23 /// If the project has the following files:
24 ///
25 /// - directory1/a/something.txt
26 /// - directory2/a/things.txt
27 /// - directory3/a/other.txt
28 ///
29 /// You can delete the first file by providing a path of "directory1/a/something.txt"
30 /// </example>
31 pub path: String,
32}
33
34pub struct DeletePathTool {
35 project: Entity<Project>,
36 action_log: Entity<ActionLog>,
37}
38
39impl DeletePathTool {
40 pub fn new(project: Entity<Project>, action_log: Entity<ActionLog>) -> Self {
41 Self {
42 project,
43 action_log,
44 }
45 }
46}
47
48impl AgentTool for DeletePathTool {
49 type Input = DeletePathToolInput;
50 type Output = String;
51
52 fn name() -> &'static str {
53 "delete_path"
54 }
55
56 fn kind() -> ToolKind {
57 ToolKind::Delete
58 }
59
60 fn initial_title(
61 &self,
62 input: Result<Self::Input, serde_json::Value>,
63 _cx: &mut App,
64 ) -> SharedString {
65 if let Ok(input) = input {
66 format!("Delete “`{}`”", input.path).into()
67 } else {
68 "Delete path".into()
69 }
70 }
71
72 fn run(
73 self: Arc<Self>,
74 input: Self::Input,
75 event_stream: ToolCallEventStream,
76 cx: &mut App,
77 ) -> Task<Result<Self::Output>> {
78 let path = input.path;
79
80 let settings = AgentSettings::get_global(cx);
81 let decision = decide_permission_from_settings(Self::name(), &path, settings);
82
83 let authorize = match decision {
84 ToolPermissionDecision::Allow => None,
85 ToolPermissionDecision::Deny(reason) => {
86 return Task::ready(Err(anyhow!("{}", reason)));
87 }
88 ToolPermissionDecision::Confirm => {
89 let context = crate::ToolPermissionContext {
90 tool_name: "delete_path".to_string(),
91 input_value: path.clone(),
92 };
93 Some(event_stream.authorize(
94 format!("Delete {}", MarkdownInlineCode(&path)),
95 context,
96 cx,
97 ))
98 }
99 };
100
101 let Some(project_path) = self.project.read(cx).find_project_path(&path, cx) else {
102 return Task::ready(Err(anyhow!(
103 "Couldn't delete {path} because that path isn't in this project."
104 )));
105 };
106
107 let Some(worktree) = self
108 .project
109 .read(cx)
110 .worktree_for_id(project_path.worktree_id, cx)
111 else {
112 return Task::ready(Err(anyhow!(
113 "Couldn't delete {path} because that path isn't in this project."
114 )));
115 };
116
117 let worktree_snapshot = worktree.read(cx).snapshot();
118 let (mut paths_tx, mut paths_rx) = mpsc::channel(256);
119 cx.background_spawn({
120 let project_path = project_path.clone();
121 async move {
122 for entry in
123 worktree_snapshot.traverse_from_path(true, false, false, &project_path.path)
124 {
125 if !entry.path.starts_with(&project_path.path) {
126 break;
127 }
128 paths_tx
129 .send(ProjectPath {
130 worktree_id: project_path.worktree_id,
131 path: entry.path.clone(),
132 })
133 .await?;
134 }
135 anyhow::Ok(())
136 }
137 })
138 .detach();
139
140 let project = self.project.clone();
141 let action_log = self.action_log.clone();
142 cx.spawn(async move |cx| {
143 if let Some(authorize) = authorize {
144 authorize.await?;
145 }
146
147 loop {
148 let path_result = futures::select! {
149 path = paths_rx.next().fuse() => path,
150 _ = event_stream.cancelled_by_user().fuse() => {
151 anyhow::bail!("Delete cancelled by user");
152 }
153 };
154 let Some(path) = path_result else {
155 break;
156 };
157 if let Ok(buffer) = project
158 .update(cx, |project, cx| project.open_buffer(path, cx))
159 .await
160 {
161 action_log.update(cx, |action_log, cx| {
162 action_log.will_delete_buffer(buffer.clone(), cx)
163 });
164 }
165 }
166
167 let deletion_task = project
168 .update(cx, |project, cx| {
169 project.delete_file(project_path, false, cx)
170 })
171 .with_context(|| {
172 format!("Couldn't delete {path} because that path isn't in this project.")
173 })?;
174
175 futures::select! {
176 result = deletion_task.fuse() => {
177 result.with_context(|| format!("Deleting {path}"))?;
178 }
179 _ = event_stream.cancelled_by_user().fuse() => {
180 anyhow::bail!("Delete cancelled by user");
181 }
182 }
183 Ok(format!("Deleted {path}"))
184 })
185 }
186}