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