agent_ui: Remove Keep/Reject only with feature flag (#46656)

Michael Benfield created

Release Notes:

- N/A

Change summary

crates/agent_ui/src/acp/thread_view.rs | 263 ++++++++++++++++++++++-----
1 file changed, 215 insertions(+), 48 deletions(-)

Detailed changes

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -4,7 +4,7 @@ use acp_thread::{
     RetryStatus, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus, UserMessageId,
 };
 use acp_thread::{AgentConnection, Plan};
-use action_log::ActionLogTelemetry;
+use action_log::{ActionLog, ActionLogTelemetry};
 use agent::{NativeAgentServer, NativeAgentSessionList, SharedThread, ThreadStore};
 use agent_client_protocol::{self as acp, PromptCapabilities};
 use agent_servers::{AgentServer, AgentServerDelegate};
@@ -20,7 +20,7 @@ use editor::scroll::Autoscroll;
 use editor::{
     Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects, SizingBehavior,
 };
-use feature_flags::{AgentSharingFeatureFlag, FeatureFlagAppExt};
+use feature_flags::{AgentSharingFeatureFlag, AgentV2FeatureFlag, FeatureFlagAppExt};
 use file_icons::FileIcons;
 use fs::Fs;
 use futures::FutureExt as _;
@@ -45,7 +45,7 @@ use std::sync::Arc;
 use std::time::Instant;
 use std::{collections::BTreeMap, rc::Rc, time::Duration};
 use terminal_view::terminal_panel::TerminalPanel;
-use text::ToPoint as _;
+use text::{Anchor, ToPoint as _};
 use theme::{AgentFontSize, ThemeSettings};
 use ui::{
     Callout, CommonAnimationExt, ContextMenu, ContextMenuEntry, CopyButton, DiffStat, Disclosure,
@@ -71,8 +71,8 @@ use crate::ui::{AgentNotification, AgentNotificationEvent, BurnModeTooltip, Usag
 use crate::{
     AgentDiffPane, AgentPanel, AllowAlways, AllowOnce, ClearMessageQueue, ContinueThread,
     ContinueWithBurnMode, CycleFavoriteModels, CycleModeSelector, ExpandMessageEditor, Follow,
-    KeepAll, NewThread, OpenHistory, QueueMessage, RejectAll, RejectOnce, SendNextQueuedMessage,
-    ToggleBurnMode, ToggleProfileSelector,
+    KeepAll, NewThread, OpenAgentDiff, OpenHistory, QueueMessage, RejectAll, RejectOnce,
+    SendNextQueuedMessage, ToggleBurnMode, ToggleProfileSelector,
 };
 
 const STOPWATCH_THRESHOLD: Duration = Duration::from_secs(1);
@@ -4419,6 +4419,7 @@ impl AcpThreadView {
     ) -> Option<AnyElement> {
         let thread = thread_entity.read(cx);
         let action_log = thread.action_log();
+        let telemetry = ActionLogTelemetry::from(thread);
         let changed_buffers = action_log.read(cx).changed_buffers(cx);
         let plan = thread.plan();
 
@@ -4433,6 +4434,8 @@ impl AcpThreadView {
         // block you from using the panel.
         let pending_edits = false;
 
+        let use_keep_reject_buttons = !cx.has_flag::<AgentV2FeatureFlag>();
+
         v_flex()
             .mt_1()
             .mx_2()
@@ -4461,10 +4464,18 @@ impl AcpThreadView {
                     &changed_buffers,
                     self.edits_expanded,
                     pending_edits,
+                    use_keep_reject_buttons,
                     cx,
                 ))
                 .when(self.edits_expanded, |parent| {
-                    parent.child(self.render_edited_files(&changed_buffers, cx))
+                    parent.child(self.render_edited_files(
+                        action_log,
+                        telemetry.clone(),
+                        &changed_buffers,
+                        pending_edits,
+                        use_keep_reject_buttons,
+                        cx,
+                    ))
                 })
             })
             .when(!self.message_queue.is_empty(), |this| {
@@ -4640,8 +4651,11 @@ impl AcpThreadView {
         changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
         expanded: bool,
         pending_edits: bool,
+        use_keep_reject_buttons: bool,
         cx: &Context<Self>,
     ) -> Div {
+        const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete.";
+
         let focus_handle = self.focus_handle(cx);
 
         h_flex()
@@ -4719,22 +4733,91 @@ impl AcpThreadView {
                         cx.notify();
                     })),
             )
-            .child(
-                Button::new("review-changes", "Review Changes")
-                    .label_size(LabelSize::Small)
-                    .key_binding(
-                        KeyBinding::for_action_in(&git_ui::project_diff::Diff, &focus_handle, cx)
+            .when(use_keep_reject_buttons, |this| {
+                this.child(
+                    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,
+                                            cx,
+                                        )
+                                    }
+                                })
+                                .on_click(cx.listener(|_, _, window, cx| {
+                                    window.dispatch_action(OpenAgentDiff.boxed_clone(), cx);
+                                })),
+                        )
+                        .child(Divider::vertical().color(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(),
+                                        cx,
+                                    )
+                                    .map(|kb| kb.size(rems_from_px(10.))),
+                                )
+                                .on_click(cx.listener(move |this, _, window, cx| {
+                                    this.reject_all(&RejectAll, window, cx);
+                                })),
+                        )
+                        .child(
+                            Button::new("keep-all-changes", "Keep 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, cx)
+                                        .map(|kb| kb.size(rems_from_px(10.))),
+                                )
+                                .on_click(cx.listener(move |this, _, window, cx| {
+                                    this.keep_all(&KeepAll, window, cx);
+                                })),
+                        ),
+                )
+            })
+            .when(!use_keep_reject_buttons, |this| {
+                this.child(
+                    Button::new("review-changes", "Review Changes")
+                        .label_size(LabelSize::Small)
+                        .key_binding(
+                            KeyBinding::for_action_in(
+                                &git_ui::project_diff::Diff,
+                                &focus_handle,
+                                cx,
+                            )
                             .map(|kb| kb.size(rems_from_px(10.))),
-                    )
-                    .on_click(cx.listener(move |_, _, window, cx| {
-                        window.dispatch_action(git_ui::project_diff::Diff.boxed_clone(), cx);
-                    })),
-            )
+                        )
+                        .on_click(cx.listener(move |_, _, window, cx| {
+                            window.dispatch_action(git_ui::project_diff::Diff.boxed_clone(), cx);
+                        })),
+                )
+            })
     }
 
     fn render_edited_files(
         &self,
+        action_log: &Entity<ActionLog>,
+        telemetry: ActionLogTelemetry,
         changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
+        pending_edits: bool,
+        use_keep_reject_buttons: bool,
         cx: &Context<Self>,
     ) -> impl IntoElement {
         let editor_bg_color = cx.theme().colors().editor_background;
@@ -4858,39 +4941,123 @@ impl AcpThreadView {
                                             .bg(overlay_gradient),
                                     ),
                             )
-                            .child(
-                                h_flex()
-                                    .gap_1()
-                                    .visible_on_hover("edited-code")
-                                    .child(
-                                        Button::new("review", "Review")
-                                            .label_size(LabelSize::Small)
-                                            .on_click({
-                                                let buffer = buffer.clone();
-                                                let workspace = self.workspace.clone();
-                                                cx.listener(move |_, _, window, cx| {
-                                                    let Some(workspace) = workspace.upgrade() else {
-                                                        return;
-                                                    };
-                                                    let Some(file) = buffer.read(cx).file() else {
-                                                        return;
-                                                    };
-                                                    let project_path = project::ProjectPath {
-                                                        worktree_id: file.worktree_id(cx),
-                                                        path: file.path().clone(),
-                                                    };
-                                                    workspace.update(cx, |workspace, cx| {
-                                                        git_ui::project_diff::ProjectDiff::deploy_at_project_path(
-                                                            workspace,
-                                                            project_path,
-                                                            window,
+                            .when(use_keep_reject_buttons, |parent| {
+                                parent.child(
+                                    h_flex()
+                                        .gap_1()
+                                        .visible_on_hover("edited-code")
+                                        .child(
+                                            Button::new("review", "Review")
+                                                .label_size(LabelSize::Small)
+                                                .on_click({
+                                                    let buffer = buffer.clone();
+                                                    let workspace = self.workspace.clone();
+                                                    cx.listener(move |_, _, window, cx| {
+                                                        let Some(workspace) = workspace.upgrade() else {
+                                                            return;
+                                                        };
+                                                        let Some(file) = buffer.read(cx).file() else {
+                                                            return;
+                                                        };
+                                                        let project_path = project::ProjectPath {
+                                                            worktree_id: file.worktree_id(cx),
+                                                            path: file.path().clone(),
+                                                        };
+                                                        workspace.update(cx, |workspace, cx| {
+                                                            git_ui::project_diff::ProjectDiff::deploy_at_project_path(
+                                                                workspace,
+                                                                project_path,
+                                                                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();
+                                                    let action_log = action_log.clone();
+                                                    let telemetry = telemetry.clone();
+                                                    move |_, _, cx| {
+                                                        action_log.update(cx, |action_log, cx| {
+                                                            action_log
+                                                        .reject_edits_in_ranges(
+                                                            buffer.clone(),
+                                                            vec![Anchor::min_max_range_for_buffer(
+                                                                buffer.read(cx).remote_id(),
+                                                            )],
+                                                            Some(telemetry.clone()),
                                                             cx,
-                                                        );
-                                                    });
-                                                })
-                                            }),
-                                    ),
-                            );
+                                                        )
+                                                        .detach_and_log_err(cx);
+                                                        })
+                                                    }
+                                                }),
+                                        )
+                                        .child(
+                                            Button::new("keep-file", "Keep")
+                                                .label_size(LabelSize::Small)
+                                                .disabled(pending_edits)
+                                                .on_click({
+                                                    let buffer = buffer.clone();
+                                                    let action_log = action_log.clone();
+                                                    let telemetry = telemetry.clone();
+                                                    move |_, _, cx| {
+                                                        action_log.update(cx, |action_log, cx| {
+                                                            action_log.keep_edits_in_range(
+                                                                buffer.clone(),
+                                                                Anchor::min_max_range_for_buffer(
+                                                                    buffer.read(cx).remote_id(),
+                                                                ),
+                                                                Some(telemetry.clone()),
+                                                                cx,
+                                                            );
+                                                        })
+                                                    }
+                                                }),
+                                        ),
+                                )
+                            })
+                            .when(!use_keep_reject_buttons, |parent| {
+                                parent.child(
+                                    h_flex()
+                                        .gap_1()
+                                        .visible_on_hover("edited-code")
+                                        .child(
+                                            Button::new("review", "Review")
+                                                .label_size(LabelSize::Small)
+                                                .on_click({
+                                                    let buffer = buffer.clone();
+                                                    let workspace = self.workspace.clone();
+                                                    cx.listener(move |_, _, window, cx| {
+                                                        let Some(workspace) = workspace.upgrade() else {
+                                                            return;
+                                                        };
+                                                        let Some(file) = buffer.read(cx).file() else {
+                                                            return;
+                                                        };
+                                                        let project_path = project::ProjectPath {
+                                                            worktree_id: file.worktree_id(cx),
+                                                            path: file.path().clone(),
+                                                        };
+                                                        workspace.update(cx, |workspace, cx| {
+                                                            git_ui::project_diff::ProjectDiff::deploy_at_project_path(
+                                                                workspace,
+                                                                project_path,
+                                                                window,
+                                                                cx,
+                                                            );
+                                                        });
+                                                    })
+                                                }),
+                                        ),
+                                )
+                            });
 
                         Some(element)
                     }),