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:

<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.
This commit is contained in:
Danilo Leal 2025-06-16 11:28:37 -03:00 committed by GitHub
parent fceba6c795
commit c35f22dde0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -39,7 +39,9 @@ use proto::Plan;
use settings::Settings; use settings::Settings;
use std::time::Duration; use std::time::Duration;
use theme::ThemeSettings; 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 util::{ResultExt as _, maybe};
use workspace::{CollaboratorId, Workspace}; use workspace::{CollaboratorId, Workspace};
use zed_llm_client::CompletionIntent; use zed_llm_client::CompletionIntent;
@ -505,6 +507,46 @@ impl MessageEditor {
cx.notify(); 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> { fn render_burn_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
let thread = self.thread.read(cx); let thread = self.thread.read(cx);
let model = thread.configured_model(); let model = thread.configured_model();
@ -988,7 +1030,7 @@ impl MessageEditor {
this.handle_review_click(window, cx) this.handle_review_click(window, cx)
})), })),
) )
.child(ui::Divider::vertical().color(ui::DividerColor::Border)) .child(Divider::vertical().color(DividerColor::Border))
.child( .child(
Button::new("reject-all-changes", "Reject All") Button::new("reject-all-changes", "Reject All")
.label_size(LabelSize::Small) .label_size(LabelSize::Small)
@ -1038,7 +1080,7 @@ impl MessageEditor {
let file = buffer.read(cx).file()?; let file = buffer.read(cx).file()?;
let path = file.path(); 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(); let parent_str = parent.to_string_lossy();
if parent_str.is_empty() { 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()) Label::new(name.to_string_lossy().to_string())
.size(LabelSize::XSmall) .size(LabelSize::XSmall)
.buffer_font(cx) .buffer_font(cx)
@ -1072,36 +1114,22 @@ impl MessageEditor {
.size(IconSize::Small) .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( let overlay_gradient = linear_gradient(
90., 90.,
linear_color_stop(editor_bg_color, 1.), linear_color_stop(editor_bg_color, 1.),
linear_color_stop(editor_bg_color.opacity(0.2), 0.), 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() let element = h_flex()
.group("edited-code") .group("edited-code")
.id(("file-container", index)) .id(("file-container", index))
.cursor_pointer()
.relative() .relative()
.py_1() .py_1()
.pl_2() .pl_2()
.pr_1() .pr_1()
.gap_2() .gap_2()
.justify_between() .justify_between()
.bg(cx.theme().colors().editor_background) .bg(editor_bg_color)
.hover(|style| style.bg(hover_color))
.when(index < changed_buffers.len() - 1, |parent| { .when(index < changed_buffers.len() - 1, |parent| {
parent.border_color(border_color).border_b_1() parent.border_color(border_color).border_b_1()
}) })
@ -1116,47 +1144,75 @@ impl MessageEditor {
.child( .child(
h_flex() h_flex()
.gap_0p5() .gap_0p5()
.children(name_label) .children(file_name)
.children(parent_label), .children(file_path),
), // TODO: Implement line diff ), // TODO: Implement line diff
// .child(Label::new("+").color(Color::Created)) // .child(Label::new("+").color(Color::Created))
// .child(Label::new("-").color(Color::Deleted)), // .child(Label::new("-").color(Color::Deleted)),
) )
.child( .child(
div().visible_on_hover("edited-code").child( h_flex()
Button::new("review", "Review") .gap_1()
.label_size(LabelSize::Small) .visible_on_hover("edited-code")
.on_click({ .child(
let buffer = buffer.clone(); Button::new("review", "Review")
cx.listener(move |this, _, window, cx| { .label_size(LabelSize::Small)
this.handle_file_click( .on_click({
buffer.clone(), let buffer = buffer.clone();
window, cx.listener(move |this, _, window, cx| {
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( .child(
div() div()
.id("gradient-overlay") .id("gradient-overlay")
.absolute() .absolute()
.h_5_6() .h_full()
.w_12() .w_12()
.top_0()
.bottom_0() .bottom_0()
.right(px(52.)) .right(px(152.))
.bg(overlay_gradient) .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);
})
});
Some(element) Some(element)
}, },