@@ -6,7 +6,7 @@ use crate::agent_model_selector::{AgentModelSelector, ModelType};
use crate::context::{AgentContextKey, ContextCreasesAddon, ContextLoadResult, load_context};
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
use crate::ui::{
- AnimatedLabel, MaxModeTooltip,
+ MaxModeTooltip,
preview::{AgentPreview, UsageCallout},
};
use agent_settings::{AgentSettings, CompletionMode};
@@ -27,7 +27,7 @@ use gpui::{
Animation, AnimationExt, App, ClipboardEntry, Entity, EventEmitter, Focusable, Subscription,
Task, TextStyle, WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
};
-use language::{Buffer, Language};
+use language::{Buffer, Language, Point};
use language_model::{
ConfiguredModel, LanguageModelRequestMessage, MessageContent, RequestUsage,
ZED_CLOUD_PROVIDER_ID,
@@ -51,9 +51,9 @@ use crate::profile_selector::ProfileSelector;
use crate::thread::{MessageCrease, Thread, TokenUsageRatio};
use crate::thread_store::{TextThreadStore, ThreadStore};
use crate::{
- ActiveThread, AgentDiffPane, Chat, ChatWithFollow, ExpandMessageEditor, Follow, NewThread,
- OpenAgentDiff, RemoveAllContext, ToggleBurnMode, ToggleContextPicker, ToggleProfileSelector,
- register_agent_preview,
+ ActiveThread, AgentDiffPane, Chat, ChatWithFollow, ExpandMessageEditor, Follow, KeepAll,
+ NewThread, OpenAgentDiff, RejectAll, RemoveAllContext, ToggleBurnMode, ToggleContextPicker,
+ ToggleProfileSelector, register_agent_preview,
};
#[derive(RegisterComponent)]
@@ -459,11 +459,20 @@ impl MessageEditor {
}
fn handle_review_click(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ if self.thread.read(cx).has_pending_edit_tool_uses() {
+ return;
+ }
+
self.edits_expanded = true;
AgentDiffPane::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err();
cx.notify();
}
+ fn handle_edit_bar_expand(&mut self, cx: &mut Context<Self>) {
+ self.edits_expanded = !self.edits_expanded;
+ cx.notify();
+ }
+
fn handle_file_click(
&self,
buffer: Entity<Buffer>,
@@ -494,6 +503,40 @@ impl MessageEditor {
});
}
+ fn handle_accept_all(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
+ if self.thread.read(cx).has_pending_edit_tool_uses() {
+ return;
+ }
+
+ self.thread.update(cx, |thread, cx| {
+ thread.keep_all_edits(cx);
+ });
+ cx.notify();
+ }
+
+ fn handle_reject_all(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
+ if self.thread.read(cx).has_pending_edit_tool_uses() {
+ return;
+ }
+
+ // Since there's no reject_all_edits method in the thread API,
+ // we need to iterate through all buffers and reject their edits
+ let action_log = self.thread.read(cx).action_log().clone();
+ let changed_buffers = action_log.read(cx).changed_buffers(cx);
+
+ for (buffer, _) in changed_buffers {
+ 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 render_max_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
let thread = self.thread.read(cx);
let model = thread.configured_model();
@@ -615,6 +658,12 @@ impl MessageEditor {
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::expand_message_editor))
.on_action(cx.listener(Self::toggle_burn_mode))
+ .on_action(
+ cx.listener(|this, _: &KeepAll, window, cx| this.handle_accept_all(window, cx)),
+ )
+ .on_action(
+ cx.listener(|this, _: &RejectAll, window, cx| this.handle_reject_all(window, cx)),
+ )
.capture_action(cx.listener(Self::paste))
.gap_2()
.p_2()
@@ -870,7 +919,10 @@ impl MessageEditor {
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
let is_edit_changes_expanded = self.edits_expanded;
- let is_generating = self.thread.read(cx).is_generating();
+ let thread = self.thread.read(cx);
+ let pending_edits = thread.has_pending_edit_tool_uses();
+
+ const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete.";
v_flex()
.mt_1()
@@ -888,31 +940,28 @@ impl MessageEditor {
}])
.child(
h_flex()
- .id("edits-container")
- .cursor_pointer()
- .p_1p5()
+ .p_1()
.justify_between()
.when(is_edit_changes_expanded, |this| {
this.border_b_1().border_color(border_color)
})
- .on_click(
- cx.listener(|this, _, window, cx| this.handle_review_click(window, cx)),
- )
.child(
h_flex()
+ .id("edits-container")
+ .cursor_pointer()
+ .w_full()
.gap_1()
.child(
Disclosure::new("edits-disclosure", is_edit_changes_expanded)
- .on_click(cx.listener(|this, _ev, _window, cx| {
- this.edits_expanded = !this.edits_expanded;
- cx.notify();
+ .on_click(cx.listener(|this, _, _, cx| {
+ this.handle_edit_bar_expand(cx)
})),
)
.map(|this| {
- if is_generating {
+ if pending_edits {
this.child(
- AnimatedLabel::new(format!(
- "Editing {} {}",
+ Label::new(format!(
+ "Editing {} {}…",
changed_buffers.len(),
if changed_buffers.len() == 1 {
"file"
@@ -920,7 +969,15 @@ impl MessageEditor {
"files"
}
))
- .size(LabelSize::Small),
+ .color(Color::Muted)
+ .size(LabelSize::Small)
+ .with_animation(
+ "edit-label",
+ Animation::new(Duration::from_secs(2))
+ .repeat()
+ .with_easing(pulsating_between(0.3, 0.7)),
+ |label, delta| label.alpha(delta),
+ ),
)
} else {
this.child(
@@ -945,23 +1002,74 @@ impl MessageEditor {
.color(Color::Muted),
)
}
- }),
+ })
+ .on_click(
+ cx.listener(|this, _, _, cx| this.handle_edit_bar_expand(cx)),
+ ),
)
.child(
- Button::new("review", "Review Changes")
- .label_size(LabelSize::Small)
- .key_binding(
- KeyBinding::for_action_in(
- &OpenAgentDiff,
- &focus_handle,
- window,
- cx,
- )
- .map(|kb| kb.size(rems_from_px(12.))),
+ h_flex()
+ .gap_1()
+ .child(
+ IconButton::new("review-changes", IconName::ListTodo)
+ .icon_size(IconSize::Small)
+ .tooltip({
+ let focus_handle = focus_handle.clone();
+ move |window, cx| {
+ Tooltip::for_action_in(
+ "Review Changes",
+ &OpenAgentDiff,
+ &focus_handle,
+ window,
+ cx,
+ )
+ }
+ })
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.handle_review_click(window, cx)
+ })),
+ )
+ .child(ui::Divider::vertical().color(ui::DividerColor::Border))
+ .child(
+ Button::new("reject-all-changes", "Reject All")
+ .label_size(LabelSize::Small)
+ .disabled(pending_edits)
+ .when(pending_edits, |this| {
+ this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
+ })
+ .key_binding(
+ KeyBinding::for_action_in(
+ &RejectAll,
+ &focus_handle.clone(),
+ window,
+ cx,
+ )
+ .map(|kb| kb.size(rems_from_px(10.))),
+ )
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.handle_reject_all(window, cx)
+ })),
)
- .on_click(cx.listener(|this, _, window, cx| {
- this.handle_review_click(window, cx)
- })),
+ .child(
+ Button::new("accept-all-changes", "Accept All")
+ .label_size(LabelSize::Small)
+ .disabled(pending_edits)
+ .when(pending_edits, |this| {
+ this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
+ })
+ .key_binding(
+ KeyBinding::for_action_in(
+ &KeepAll,
+ &focus_handle,
+ window,
+ cx,
+ )
+ .map(|kb| kb.size(rems_from_px(10.))),
+ )
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.handle_accept_all(window, cx)
+ })),
+ ),
),
)
.when(is_edit_changes_expanded, |parent| {