move_path_tool.rs

  1use crate::{
  2    AgentTool, ToolCallEventStream, ToolPermissionDecision, decide_permission_from_settings,
  3};
  4use agent_client_protocol::ToolKind;
  5use agent_settings::AgentSettings;
  6use anyhow::{Context as _, Result, anyhow};
  7use futures::FutureExt as _;
  8use gpui::{App, AppContext, Entity, SharedString, Task};
  9use project::Project;
 10use schemars::JsonSchema;
 11use serde::{Deserialize, Serialize};
 12use settings::Settings;
 13use std::{path::Path, sync::Arc};
 14use util::markdown::MarkdownInlineCode;
 15
 16/// Moves or rename a file or directory in the project, and returns confirmation that the move succeeded.
 17///
 18/// If the source and destination directories are the same, but the filename is different, this performs a rename. Otherwise, it performs a move.
 19///
 20/// This tool should be used when it's desirable to move or rename a file or directory without changing its contents at all.
 21#[derive(Debug, Serialize, Deserialize, JsonSchema)]
 22pub struct MovePathToolInput {
 23    /// The source path of the file or directory to move/rename.
 24    ///
 25    /// <example>
 26    /// If the project has the following files:
 27    ///
 28    /// - directory1/a/something.txt
 29    /// - directory2/a/things.txt
 30    /// - directory3/a/other.txt
 31    ///
 32    /// You can move the first file by providing a source_path of "directory1/a/something.txt"
 33    /// </example>
 34    pub source_path: String,
 35
 36    /// The destination path where the file or directory should be moved/renamed to.
 37    /// If the paths are the same except for the filename, then this will be a rename.
 38    ///
 39    /// <example>
 40    /// To move "directory1/a/something.txt" to "directory2/b/renamed.txt",
 41    /// provide a destination_path of "directory2/b/renamed.txt"
 42    /// </example>
 43    pub destination_path: String,
 44}
 45
 46pub struct MovePathTool {
 47    project: Entity<Project>,
 48}
 49
 50impl MovePathTool {
 51    pub fn new(project: Entity<Project>) -> Self {
 52        Self { project }
 53    }
 54}
 55
 56impl AgentTool for MovePathTool {
 57    type Input = MovePathToolInput;
 58    type Output = String;
 59
 60    fn name() -> &'static str {
 61        "move_path"
 62    }
 63
 64    fn kind() -> ToolKind {
 65        ToolKind::Move
 66    }
 67
 68    fn initial_title(
 69        &self,
 70        input: Result<Self::Input, serde_json::Value>,
 71        _cx: &mut App,
 72    ) -> SharedString {
 73        if let Ok(input) = input {
 74            let src = MarkdownInlineCode(&input.source_path);
 75            let dest = MarkdownInlineCode(&input.destination_path);
 76            let src_path = Path::new(&input.source_path);
 77            let dest_path = Path::new(&input.destination_path);
 78
 79            match dest_path
 80                .file_name()
 81                .and_then(|os_str| os_str.to_os_string().into_string().ok())
 82            {
 83                Some(filename) if src_path.parent() == dest_path.parent() => {
 84                    let filename = MarkdownInlineCode(&filename);
 85                    format!("Rename {src} to {filename}").into()
 86                }
 87                _ => format!("Move {src} to {dest}").into(),
 88            }
 89        } else {
 90            "Move path".into()
 91        }
 92    }
 93
 94    fn run(
 95        self: Arc<Self>,
 96        input: Self::Input,
 97        event_stream: ToolCallEventStream,
 98        cx: &mut App,
 99    ) -> Task<Result<Self::Output>> {
100        let settings = AgentSettings::get_global(cx);
101
102        let source_decision =
103            decide_permission_from_settings(Self::name(), &input.source_path, settings);
104        if let ToolPermissionDecision::Deny(reason) = source_decision {
105            return Task::ready(Err(anyhow!("{}", reason)));
106        }
107
108        let dest_decision =
109            decide_permission_from_settings(Self::name(), &input.destination_path, settings);
110        if let ToolPermissionDecision::Deny(reason) = dest_decision {
111            return Task::ready(Err(anyhow!("{}", reason)));
112        }
113
114        let needs_confirmation = matches!(source_decision, ToolPermissionDecision::Confirm)
115            || matches!(dest_decision, ToolPermissionDecision::Confirm);
116
117        let authorize = if needs_confirmation {
118            let src = MarkdownInlineCode(&input.source_path);
119            let dest = MarkdownInlineCode(&input.destination_path);
120            Some(event_stream.authorize(format!("Move {src} to {dest}"), cx))
121        } else {
122            None
123        };
124
125        let rename_task = self.project.update(cx, |project, cx| {
126            match project
127                .find_project_path(&input.source_path, cx)
128                .and_then(|project_path| project.entry_for_path(&project_path, cx))
129            {
130                Some(entity) => match project.find_project_path(&input.destination_path, cx) {
131                    Some(project_path) => project.rename_entry(entity.id, project_path, cx),
132                    None => Task::ready(Err(anyhow!(
133                        "Destination path {} was outside the project.",
134                        input.destination_path
135                    ))),
136                },
137                None => Task::ready(Err(anyhow!(
138                    "Source path {} was not found in the project.",
139                    input.source_path
140                ))),
141            }
142        });
143
144        cx.background_spawn(async move {
145            if let Some(authorize) = authorize {
146                authorize.await?;
147            }
148
149            let result = futures::select! {
150                result = rename_task.fuse() => result,
151                _ = event_stream.cancelled_by_user().fuse() => {
152                    anyhow::bail!("Move cancelled by user");
153                }
154            };
155            let _ = result.with_context(|| {
156                format!("Moving {} to {}", input.source_path, input.destination_path)
157            })?;
158            Ok(format!(
159                "Moved {} to {}",
160                input.source_path, input.destination_path
161            ))
162        })
163    }
164}