1use crate::{AgentTool, ToolCallEventStream};
2use action_log::ActionLog;
3use agent_client_protocol::ToolKind;
4use anyhow::{Context as _, Result, anyhow};
5use futures::{SinkExt, StreamExt, channel::mpsc};
6use gpui::{App, AppContext, Entity, SharedString, Task};
7use project::{Project, ProjectPath};
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10use std::sync::Arc;
11
12/// Deletes the file or directory (and the directory's contents, recursively) at
13/// the specified path in the project, and returns confirmation of the deletion.
14#[derive(Debug, Serialize, Deserialize, JsonSchema)]
15pub struct DeletePathToolInput {
16 /// The path of the file or directory to delete.
17 ///
18 /// <example>
19 /// If the project has the following files:
20 ///
21 /// - directory1/a/something.txt
22 /// - directory2/a/things.txt
23 /// - directory3/a/other.txt
24 ///
25 /// You can delete the first file by providing a path of "directory1/a/something.txt"
26 /// </example>
27 pub path: String,
28}
29
30pub struct DeletePathTool {
31 project: Entity<Project>,
32 action_log: Entity<ActionLog>,
33}
34
35impl DeletePathTool {
36 pub fn new(project: Entity<Project>, action_log: Entity<ActionLog>) -> Self {
37 Self {
38 project,
39 action_log,
40 }
41 }
42}
43
44impl AgentTool for DeletePathTool {
45 type Input = DeletePathToolInput;
46 type Output = String;
47
48 fn name(&self) -> SharedString {
49 "delete_path".into()
50 }
51
52 fn kind(&self) -> ToolKind {
53 ToolKind::Delete
54 }
55
56 fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
57 if let Ok(input) = input {
58 format!("Delete “`{}`”", input.path).into()
59 } else {
60 "Delete path".into()
61 }
62 }
63
64 fn run(
65 self: Arc<Self>,
66 input: Self::Input,
67 _event_stream: ToolCallEventStream,
68 cx: &mut App,
69 ) -> Task<Result<Self::Output>> {
70 let path = input.path;
71 let Some(project_path) = self.project.read(cx).find_project_path(&path, cx) else {
72 return Task::ready(Err(anyhow!(
73 "Couldn't delete {path} because that path isn't in this project."
74 )));
75 };
76
77 let Some(worktree) = self
78 .project
79 .read(cx)
80 .worktree_for_id(project_path.worktree_id, cx)
81 else {
82 return Task::ready(Err(anyhow!(
83 "Couldn't delete {path} because that path isn't in this project."
84 )));
85 };
86
87 let worktree_snapshot = worktree.read(cx).snapshot();
88 let (mut paths_tx, mut paths_rx) = mpsc::channel(256);
89 cx.background_spawn({
90 let project_path = project_path.clone();
91 async move {
92 for entry in
93 worktree_snapshot.traverse_from_path(true, false, false, &project_path.path)
94 {
95 if !entry.path.starts_with(&project_path.path) {
96 break;
97 }
98 paths_tx
99 .send(ProjectPath {
100 worktree_id: project_path.worktree_id,
101 path: entry.path.clone(),
102 })
103 .await?;
104 }
105 anyhow::Ok(())
106 }
107 })
108 .detach();
109
110 let project = self.project.clone();
111 let action_log = self.action_log.clone();
112 cx.spawn(async move |cx| {
113 while let Some(path) = paths_rx.next().await {
114 if let Ok(buffer) = project
115 .update(cx, |project, cx| project.open_buffer(path, cx))?
116 .await
117 {
118 action_log.update(cx, |action_log, cx| {
119 action_log.will_delete_buffer(buffer.clone(), cx)
120 })?;
121 }
122 }
123
124 let deletion_task = project
125 .update(cx, |project, cx| {
126 project.delete_file(project_path, false, cx)
127 })?
128 .with_context(|| {
129 format!("Couldn't delete {path} because that path isn't in this project.")
130 })?;
131 deletion_task
132 .await
133 .with_context(|| format!("Deleting {path}"))?;
134 Ok(format!("Deleted {path}"))
135 })
136 }
137}