From c35f22dde0dd4adfae1a6522a991e3e32dc55214 Mon Sep 17 00:00:00 2001
From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Date: Mon, 16 Jun 2025 11:28:37 -0300
Subject: [PATCH] agent: Enable accepting and rejecting individual file changes
from message editor (#32801)
Previously, you could only accept and reject all changes from a specific
file by checking that file out on a tab. Now, you can do that via the
message editor changes summary bar. The buttons appear as you hover over
the file item in the accordion.
Here's what it looks like:
Release Notes:
- agent: Enable accepting and rejecting individual file changes from
message editor.
---
crates/agent/src/message_editor.rs | 152 ++++++++++++++++++++---------
1 file changed, 104 insertions(+), 48 deletions(-)
diff --git a/crates/agent/src/message_editor.rs b/crates/agent/src/message_editor.rs
index b10a20b3e0d1391c834797cbb524a38c8ca09a78..ee7ebd3092465f7b72cf24702786c4df4bea88aa 100644
--- a/crates/agent/src/message_editor.rs
+++ b/crates/agent/src/message_editor.rs
@@ -39,7 +39,9 @@ use proto::Plan;
use settings::Settings;
use std::time::Duration;
use theme::ThemeSettings;
-use ui::{Callout, Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
+use ui::{
+ Callout, Disclosure, Divider, DividerColor, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*,
+};
use util::{ResultExt as _, maybe};
use workspace::{CollaboratorId, Workspace};
use zed_llm_client::CompletionIntent;
@@ -505,6 +507,46 @@ impl MessageEditor {
cx.notify();
}
+ fn handle_reject_file_changes(
+ &mut self,
+ buffer: Entity,
+ _window: &mut Window,
+ cx: &mut Context,
+ ) {
+ if self.thread.read(cx).has_pending_edit_tool_uses() {
+ return;
+ }
+
+ self.thread.update(cx, |thread, cx| {
+ let buffer_snapshot = buffer.read(cx);
+ let start = buffer_snapshot.anchor_before(Point::new(0, 0));
+ let end = buffer_snapshot.anchor_after(buffer_snapshot.max_point());
+ thread
+ .reject_edits_in_ranges(buffer, vec![start..end], cx)
+ .detach();
+ });
+ cx.notify();
+ }
+
+ fn handle_accept_file_changes(
+ &mut self,
+ buffer: Entity,
+ _window: &mut Window,
+ cx: &mut Context,
+ ) {
+ if self.thread.read(cx).has_pending_edit_tool_uses() {
+ return;
+ }
+
+ self.thread.update(cx, |thread, cx| {
+ let buffer_snapshot = buffer.read(cx);
+ let start = buffer_snapshot.anchor_before(Point::new(0, 0));
+ let end = buffer_snapshot.anchor_after(buffer_snapshot.max_point());
+ thread.keep_edits_in_range(buffer, start..end, cx);
+ });
+ cx.notify();
+ }
+
fn render_burn_mode_toggle(&self, cx: &mut Context) -> Option {
let thread = self.thread.read(cx);
let model = thread.configured_model();
@@ -988,7 +1030,7 @@ impl MessageEditor {
this.handle_review_click(window, cx)
})),
)
- .child(ui::Divider::vertical().color(ui::DividerColor::Border))
+ .child(Divider::vertical().color(DividerColor::Border))
.child(
Button::new("reject-all-changes", "Reject All")
.label_size(LabelSize::Small)
@@ -1038,7 +1080,7 @@ impl MessageEditor {
let file = buffer.read(cx).file()?;
let path = file.path();
- let parent_label = path.parent().and_then(|parent| {
+ let file_path = path.parent().and_then(|parent| {
let parent_str = parent.to_string_lossy();
if parent_str.is_empty() {
@@ -1057,7 +1099,7 @@ impl MessageEditor {
}
});
- let name_label = path.file_name().map(|name| {
+ let file_name = path.file_name().map(|name| {
Label::new(name.to_string_lossy().to_string())
.size(LabelSize::XSmall)
.buffer_font(cx)
@@ -1072,36 +1114,22 @@ impl MessageEditor {
.size(IconSize::Small)
});
- let hover_color = cx
- .theme()
- .colors()
- .element_background
- .blend(cx.theme().colors().editor_foreground.opacity(0.025));
-
let overlay_gradient = linear_gradient(
90.,
linear_color_stop(editor_bg_color, 1.),
linear_color_stop(editor_bg_color.opacity(0.2), 0.),
);
- let overlay_gradient_hover = linear_gradient(
- 90.,
- linear_color_stop(hover_color, 1.),
- linear_color_stop(hover_color.opacity(0.2), 0.),
- );
-
let element = h_flex()
.group("edited-code")
.id(("file-container", index))
- .cursor_pointer()
.relative()
.py_1()
.pl_2()
.pr_1()
.gap_2()
.justify_between()
- .bg(cx.theme().colors().editor_background)
- .hover(|style| style.bg(hover_color))
+ .bg(editor_bg_color)
.when(index < changed_buffers.len() - 1, |parent| {
parent.border_color(border_color).border_b_1()
})
@@ -1116,47 +1144,75 @@ impl MessageEditor {
.child(
h_flex()
.gap_0p5()
- .children(name_label)
- .children(parent_label),
+ .children(file_name)
+ .children(file_path),
), // TODO: Implement line diff
// .child(Label::new("+").color(Color::Created))
// .child(Label::new("-").color(Color::Deleted)),
)
.child(
- div().visible_on_hover("edited-code").child(
- Button::new("review", "Review")
- .label_size(LabelSize::Small)
- .on_click({
- let buffer = buffer.clone();
- cx.listener(move |this, _, window, cx| {
- this.handle_file_click(
- buffer.clone(),
- window,
- cx,
- );
- })
- }),
- ),
+ h_flex()
+ .gap_1()
+ .visible_on_hover("edited-code")
+ .child(
+ Button::new("review", "Review")
+ .label_size(LabelSize::Small)
+ .on_click({
+ let buffer = buffer.clone();
+ cx.listener(move |this, _, window, cx| {
+ this.handle_file_click(
+ buffer.clone(),
+ window,
+ cx,
+ );
+ })
+ }),
+ )
+ .child(
+ Divider::vertical().color(DividerColor::BorderVariant),
+ )
+ .child(
+ Button::new("reject-file", "Reject")
+ .label_size(LabelSize::Small)
+ .disabled(pending_edits)
+ .on_click({
+ let buffer = buffer.clone();
+ cx.listener(move |this, _, window, cx| {
+ this.handle_reject_file_changes(
+ buffer.clone(),
+ window,
+ cx,
+ );
+ })
+ }),
+ )
+ .child(
+ Button::new("accept-file", "Accept")
+ .label_size(LabelSize::Small)
+ .disabled(pending_edits)
+ .on_click({
+ let buffer = buffer.clone();
+ cx.listener(move |this, _, window, cx| {
+ this.handle_accept_file_changes(
+ buffer.clone(),
+ window,
+ cx,
+ );
+ })
+ }),
+ ),
)
.child(
div()
.id("gradient-overlay")
.absolute()
- .h_5_6()
+ .h_full()
.w_12()
+ .top_0()
.bottom_0()
- .right(px(52.))
- .bg(overlay_gradient)
- .group_hover("edited-code", |style| {
- style.bg(overlay_gradient_hover)
- }),
- )
- .on_click({
- let buffer = buffer.clone();
- cx.listener(move |this, _, window, cx| {
- this.handle_file_click(buffer.clone(), window, cx);
- })
- });
+ .right(px(152.))
+ .bg(overlay_gradient),
+ );
Some(element)
},