ZIm/crates/assistant_tools/src/delete_path_tool.rs
Danilo Leal 2645591cd5
agent: Allow to accept and reject all via the panel (#31971)
This PR introduces the "Reject All" and "Accept All" buttons in the
panel's edit bar, which appears as soon as the agent starts editing a
file. I'm also adding here a new method to the thread called
`has_pending_edit_tool_uses`, which is a more specific way of knowing,
in comparison to the `is_generating` method, whether or not the
reject/accept all actions can be triggered.

Previously, without this new method, you'd be waiting for the whole
generation to end (e.g., the agent would be generating markdown with
things like change summary) to be able to click those buttons, when the
edit was already there, ready for you. It always felt like waiting for
the whole thing was unnecessary when you really wanted to just wait for
the _edits_ to be done, as so to avoid any potential conflicting state.

<img
src="https://github.com/user-attachments/assets/0927f3a6-c9ee-46ae-8f7b-97157d39a7b5"
width="500"/>

---

Release Notes:

- agent: Added ability to reject and accept all changes from the agent
panel.

---------

Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-06-03 15:20:25 -03:00

143 lines
4.6 KiB
Rust

use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::{SinkExt, StreamExt, channel::mpsc};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::{Project, ProjectPath};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use ui::IconName;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DeletePathToolInput {
/// The path of the file or directory to delete.
///
/// <example>
/// If the project has the following files:
///
/// - directory1/a/something.txt
/// - directory2/a/things.txt
/// - directory3/a/other.txt
///
/// You can delete the first file by providing a path of "directory1/a/something.txt"
/// </example>
pub path: String,
}
pub struct DeletePathTool;
impl Tool for DeletePathTool {
fn name(&self) -> String {
"delete_path".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}
fn may_perform_edits(&self) -> bool {
true
}
fn description(&self) -> String {
include_str!("./delete_path_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::FileDelete
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<DeletePathToolInput>(format)
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<DeletePathToolInput>(input.clone()) {
Ok(input) => format!("Delete “`{}`”", input.path),
Err(_) => "Delete path".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_request: Arc<LanguageModelRequest>,
project: Entity<Project>,
action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let path_str = match serde_json::from_value::<DeletePathToolInput>(input) {
Ok(input) => input.path,
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let Some(project_path) = project.read(cx).find_project_path(&path_str, cx) else {
return Task::ready(Err(anyhow!(
"Couldn't delete {path_str} because that path isn't in this project."
)))
.into();
};
let Some(worktree) = project
.read(cx)
.worktree_for_id(project_path.worktree_id, cx)
else {
return Task::ready(Err(anyhow!(
"Couldn't delete {path_str} because that path isn't in this project."
)))
.into();
};
let worktree_snapshot = worktree.read(cx).snapshot();
let (mut paths_tx, mut paths_rx) = mpsc::channel(256);
cx.background_spawn({
let project_path = project_path.clone();
async move {
for entry in
worktree_snapshot.traverse_from_path(true, false, false, &project_path.path)
{
if !entry.path.starts_with(&project_path.path) {
break;
}
paths_tx
.send(ProjectPath {
worktree_id: project_path.worktree_id,
path: entry.path.clone(),
})
.await?;
}
anyhow::Ok(())
}
})
.detach();
cx.spawn(async move |cx| {
while let Some(path) = paths_rx.next().await {
if let Ok(buffer) = project
.update(cx, |project, cx| project.open_buffer(path, cx))?
.await
{
action_log.update(cx, |action_log, cx| {
action_log.will_delete_buffer(buffer.clone(), cx)
})?;
}
}
let deletion_task = project
.update(cx, |project, cx| {
project.delete_file(project_path, false, cx)
})?
.with_context(|| {
format!("Couldn't delete {path_str} because that path isn't in this project.")
})?;
deletion_task
.await
.with_context(|| format!("Deleting {path_str}"))?;
Ok(format!("Deleted {path_str}").into())
})
.into()
}
}