delete_path_tool.rs

  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}