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 const NAME: &'static str = "delete_path";
53
54 fn kind() -> ToolKind {
55 ToolKind::Delete
56 }
57
58 fn initial_title(
59 &self,
60 input: Result<Self::Input, serde_json::Value>,
61 _cx: &mut App,
62 ) -> SharedString {
63 if let Ok(input) = input {
64 format!("Delete “`{}`”", input.path).into()
65 } else {
66 "Delete path".into()
67 }
68 }
69
70 fn run(
71 self: Arc<Self>,
72 input: Self::Input,
73 event_stream: ToolCallEventStream,
74 cx: &mut App,
75 ) -> Task<Result<Self::Output>> {
76 let path = input.path;
77
78 let settings = AgentSettings::get_global(cx);
79 let decision = decide_permission_from_settings(Self::NAME, &path, settings);
80
81 let authorize = match decision {
82 ToolPermissionDecision::Allow => None,
83 ToolPermissionDecision::Deny(reason) => {
84 return Task::ready(Err(anyhow!("{}", reason)));
85 }
86 ToolPermissionDecision::Confirm => {
87 let context = crate::ToolPermissionContext {
88 tool_name: Self::NAME.to_string(),
89 input_value: path.clone(),
90 };
91 Some(event_stream.authorize(
92 format!("Delete {}", MarkdownInlineCode(&path)),
93 context,
94 cx,
95 ))
96 }
97 };
98
99 let Some(project_path) = self.project.read(cx).find_project_path(&path, cx) else {
100 return Task::ready(Err(anyhow!(
101 "Couldn't delete {path} because that path isn't in this project."
102 )));
103 };
104
105 let Some(worktree) = self
106 .project
107 .read(cx)
108 .worktree_for_id(project_path.worktree_id, cx)
109 else {
110 return Task::ready(Err(anyhow!(
111 "Couldn't delete {path} because that path isn't in this project."
112 )));
113 };
114
115 let worktree_snapshot = worktree.read(cx).snapshot();
116 let (mut paths_tx, mut paths_rx) = mpsc::channel(256);
117 cx.background_spawn({
118 let project_path = project_path.clone();
119 async move {
120 for entry in
121 worktree_snapshot.traverse_from_path(true, false, false, &project_path.path)
122 {
123 if !entry.path.starts_with(&project_path.path) {
124 break;
125 }
126 paths_tx
127 .send(ProjectPath {
128 worktree_id: project_path.worktree_id,
129 path: entry.path.clone(),
130 })
131 .await?;
132 }
133 anyhow::Ok(())
134 }
135 })
136 .detach();
137
138 let project = self.project.clone();
139 let action_log = self.action_log.clone();
140 cx.spawn(async move |cx| {
141 if let Some(authorize) = authorize {
142 authorize.await?;
143 }
144
145 loop {
146 let path_result = futures::select! {
147 path = paths_rx.next().fuse() => path,
148 _ = event_stream.cancelled_by_user().fuse() => {
149 anyhow::bail!("Delete cancelled by user");
150 }
151 };
152 let Some(path) = path_result else {
153 break;
154 };
155 if let Ok(buffer) = project
156 .update(cx, |project, cx| project.open_buffer(path, cx))
157 .await
158 {
159 action_log.update(cx, |action_log, cx| {
160 action_log.will_delete_buffer(buffer.clone(), cx)
161 });
162 }
163 }
164
165 let deletion_task = project
166 .update(cx, |project, cx| {
167 project.delete_file(project_path, false, cx)
168 })
169 .with_context(|| {
170 format!("Couldn't delete {path} because that path isn't in this project.")
171 })?;
172
173 futures::select! {
174 result = deletion_task.fuse() => {
175 result.with_context(|| format!("Deleting {path}"))?;
176 }
177 _ = event_stream.cancelled_by_user().fuse() => {
178 anyhow::bail!("Delete cancelled by user");
179 }
180 }
181 Ok(format!("Deleted {path}"))
182 })
183 }
184}