delete_path_tool.rs

  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}