agent: Enable accepting and rejecting individual file changes from message editor (#32801)

Danilo Leal created

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:

<img
src="https://github.com/user-attachments/assets/8c0843d3-9bf1-4588-8b42-4cd8d0798a68"
width="500" />

Release Notes:

- agent: Enable accepting and rejecting individual file changes from
message editor.

Change summary

crates/agent/src/message_editor.rs | 152 +++++++++++++++++++++----------
1 file changed, 104 insertions(+), 48 deletions(-)

Detailed changes

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<Buffer>,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        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<Buffer>,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        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<Self>) -> Option<AnyElement> {
         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)
                         },