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 && is_sensitive_settings_path(Path::new(&path))
85 {
86 decision = ToolPermissionDecision::Confirm;
87 }
88
89 let authorize = match decision {
90 ToolPermissionDecision::Allow => None,
91 ToolPermissionDecision::Deny(reason) => {
92 return Task::ready(Err(anyhow!("{}", reason)));
93 }
94 ToolPermissionDecision::Confirm => {
95 let context = crate::ToolPermissionContext {
96 tool_name: Self::NAME.to_string(),
97 input_values: vec![path.clone()],
98 };
99 let title = format!("Delete {}", MarkdownInlineCode(&path));
100 let title = match sensitive_settings_kind(Path::new(&path)) {
101 Some(SensitiveSettingsKind::Local) => format!("{title} (local settings)"),
102 Some(SensitiveSettingsKind::Global) => format!("{title} (settings)"),
103 None => title,
104 };
105 Some(event_stream.authorize(title, context, cx))
106 }
107 };
108
109 let project = self.project.clone();
110 let action_log = self.action_log.clone();
111 cx.spawn(async move |cx| {
112 if let Some(authorize) = authorize {
113 authorize.await?;
114 }
115
116 let (project_path, worktree_snapshot) = project.read_with(cx, |project, cx| {
117 let project_path = project.find_project_path(&path, cx).ok_or_else(|| {
118 anyhow!("Couldn't delete {path} because that path isn't in this project.")
119 })?;
120 let worktree = project
121 .worktree_for_id(project_path.worktree_id, cx)
122 .ok_or_else(|| {
123 anyhow!("Couldn't delete {path} because that path isn't in this project.")
124 })?;
125 let worktree_snapshot = worktree.read(cx).snapshot();
126 anyhow::Ok((project_path, worktree_snapshot))
127 })?;
128
129 let (mut paths_tx, mut paths_rx) = mpsc::channel(256);
130 cx.background_spawn({
131 let project_path = project_path.clone();
132 async move {
133 for entry in
134 worktree_snapshot.traverse_from_path(true, false, false, &project_path.path)
135 {
136 if !entry.path.starts_with(&project_path.path) {
137 break;
138 }
139 paths_tx
140 .send(ProjectPath {
141 worktree_id: project_path.worktree_id,
142 path: entry.path.clone(),
143 })
144 .await?;
145 }
146 anyhow::Ok(())
147 }
148 })
149 .detach();
150
151 loop {
152 let path_result = futures::select! {
153 path = paths_rx.next().fuse() => path,
154 _ = event_stream.cancelled_by_user().fuse() => {
155 anyhow::bail!("Delete cancelled by user");
156 }
157 };
158 let Some(path) = path_result else {
159 break;
160 };
161 if let Ok(buffer) = project
162 .update(cx, |project, cx| project.open_buffer(path, cx))
163 .await
164 {
165 action_log.update(cx, |action_log, cx| {
166 action_log.will_delete_buffer(buffer.clone(), cx)
167 });
168 }
169 }
170
171 let deletion_task = project
172 .update(cx, |project, cx| {
173 project.delete_file(project_path, false, cx)
174 })
175 .with_context(|| {
176 format!("Couldn't delete {path} because that path isn't in this project.")
177 })?;
178
179 futures::select! {
180 result = deletion_task.fuse() => {
181 result.with_context(|| format!("Deleting {path}"))?;
182 }
183 _ = event_stream.cancelled_by_user().fuse() => {
184 anyhow::bail!("Delete cancelled by user");
185 }
186 }
187 Ok(format!("Deleted {path}"))
188 })
189 }
190}