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 Some(event_stream.authorize(format!("Delete {}", MarkdownInlineCode(&path)), cx))
90 }
91 };
92
93 let Some(project_path) = self.project.read(cx).find_project_path(&path, cx) else {
94 return Task::ready(Err(anyhow!(
95 "Couldn't delete {path} because that path isn't in this project."
96 )));
97 };
98
99 let Some(worktree) = self
100 .project
101 .read(cx)
102 .worktree_for_id(project_path.worktree_id, cx)
103 else {
104 return Task::ready(Err(anyhow!(
105 "Couldn't delete {path} because that path isn't in this project."
106 )));
107 };
108
109 let worktree_snapshot = worktree.read(cx).snapshot();
110 let (mut paths_tx, mut paths_rx) = mpsc::channel(256);
111 cx.background_spawn({
112 let project_path = project_path.clone();
113 async move {
114 for entry in
115 worktree_snapshot.traverse_from_path(true, false, false, &project_path.path)
116 {
117 if !entry.path.starts_with(&project_path.path) {
118 break;
119 }
120 paths_tx
121 .send(ProjectPath {
122 worktree_id: project_path.worktree_id,
123 path: entry.path.clone(),
124 })
125 .await?;
126 }
127 anyhow::Ok(())
128 }
129 })
130 .detach();
131
132 let project = self.project.clone();
133 let action_log = self.action_log.clone();
134 cx.spawn(async move |cx| {
135 if let Some(authorize) = authorize {
136 authorize.await?;
137 }
138
139 loop {
140 let path_result = futures::select! {
141 path = paths_rx.next().fuse() => path,
142 _ = event_stream.cancelled_by_user().fuse() => {
143 anyhow::bail!("Delete cancelled by user");
144 }
145 };
146 let Some(path) = path_result else {
147 break;
148 };
149 if let Ok(buffer) = project
150 .update(cx, |project, cx| project.open_buffer(path, cx))
151 .await
152 {
153 action_log.update(cx, |action_log, cx| {
154 action_log.will_delete_buffer(buffer.clone(), cx)
155 });
156 }
157 }
158
159 let deletion_task = project
160 .update(cx, |project, cx| {
161 project.delete_file(project_path, false, cx)
162 })
163 .with_context(|| {
164 format!("Couldn't delete {path} because that path isn't in this project.")
165 })?;
166
167 futures::select! {
168 result = deletion_task.fuse() => {
169 result.with_context(|| format!("Deleting {path}"))?;
170 }
171 _ = event_stream.cancelled_by_user().fuse() => {
172 anyhow::bail!("Delete cancelled by user");
173 }
174 }
175 Ok(format!("Deleted {path}"))
176 })
177 }
178}