agent_diff.rs

   1use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll};
   2use acp_thread::{AcpThread, AcpThreadEvent};
   3use action_log::ActionLogTelemetry;
   4use agent_settings::AgentSettings;
   5use anyhow::Result;
   6use buffer_diff::DiffHunkStatus;
   7use collections::{HashMap, HashSet};
   8use editor::{
   9    Direction, Editor, EditorEvent, EditorSettings, MultiBuffer, MultiBufferSnapshot,
  10    SelectionEffects, ToPoint,
  11    actions::{GoToHunk, GoToPreviousHunk},
  12    multibuffer_context_lines,
  13    scroll::Autoscroll,
  14};
  15use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
  16use gpui::{
  17    Action, AnyElement, App, AppContext, Empty, Entity, EventEmitter, FocusHandle, Focusable,
  18    Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
  19};
  20
  21use language::{Buffer, Capability, OffsetRangeExt, Point};
  22use multi_buffer::PathKey;
  23use project::{Project, ProjectItem, ProjectPath};
  24use settings::{Settings, SettingsStore};
  25use std::{
  26    any::{Any, TypeId},
  27    collections::hash_map::Entry,
  28    ops::Range,
  29    sync::Arc,
  30};
  31use ui::{CommonAnimationExt, IconButtonShape, KeyBinding, Tooltip, prelude::*, vertical_divider};
  32use util::ResultExt;
  33use workspace::{
  34    Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
  35    Workspace,
  36    item::{ItemEvent, SaveOptions, TabContentParams},
  37    searchable::SearchableItemHandle,
  38};
  39use zed_actions::assistant::ToggleFocus;
  40
  41pub struct AgentDiffPane {
  42    multibuffer: Entity<MultiBuffer>,
  43    editor: Entity<Editor>,
  44    thread: Entity<AcpThread>,
  45    focus_handle: FocusHandle,
  46    workspace: WeakEntity<Workspace>,
  47    title: SharedString,
  48    _subscriptions: Vec<Subscription>,
  49}
  50
  51impl AgentDiffPane {
  52    pub fn deploy(
  53        thread: Entity<AcpThread>,
  54        workspace: WeakEntity<Workspace>,
  55        window: &mut Window,
  56        cx: &mut App,
  57    ) -> Result<Entity<Self>> {
  58        workspace.update(cx, |workspace, cx| {
  59            Self::deploy_in_workspace(thread, workspace, window, cx)
  60        })
  61    }
  62
  63    pub fn deploy_in_workspace(
  64        thread: Entity<AcpThread>,
  65        workspace: &mut Workspace,
  66        window: &mut Window,
  67        cx: &mut Context<Workspace>,
  68    ) -> Entity<Self> {
  69        let existing_diff = workspace
  70            .items_of_type::<AgentDiffPane>(cx)
  71            .find(|diff| diff.read(cx).thread == thread);
  72
  73        if let Some(existing_diff) = existing_diff {
  74            workspace.activate_item(&existing_diff, true, true, window, cx);
  75            existing_diff
  76        } else {
  77            let agent_diff = cx
  78                .new(|cx| AgentDiffPane::new(thread.clone(), workspace.weak_handle(), window, cx));
  79            workspace.add_item_to_center(Box::new(agent_diff.clone()), window, cx);
  80            agent_diff
  81        }
  82    }
  83
  84    pub fn new(
  85        thread: Entity<AcpThread>,
  86        workspace: WeakEntity<Workspace>,
  87        window: &mut Window,
  88        cx: &mut Context<Self>,
  89    ) -> Self {
  90        let focus_handle = cx.focus_handle();
  91        let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
  92
  93        let project = thread.read(cx).project().clone();
  94        let editor = cx.new(|cx| {
  95            let mut editor =
  96                Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
  97            editor.disable_inline_diagnostics();
  98            editor.set_expand_all_diff_hunks(cx);
  99            editor.set_render_diff_hunk_controls(diff_hunk_controls(&thread), cx);
 100            editor.register_addon(AgentDiffAddon);
 101            editor
 102        });
 103
 104        let action_log = thread.read(cx).action_log().clone();
 105
 106        let mut this = Self {
 107            _subscriptions: vec![
 108                cx.observe_in(&action_log, window, |this, _action_log, window, cx| {
 109                    this.update_excerpts(window, cx)
 110                }),
 111                cx.subscribe(&thread, |this, _thread, event, cx| {
 112                    this.handle_acp_thread_event(event, cx)
 113                }),
 114            ],
 115            title: SharedString::default(),
 116            multibuffer,
 117            editor,
 118            thread,
 119            focus_handle,
 120            workspace,
 121        };
 122        this.update_excerpts(window, cx);
 123        this.update_title(cx);
 124        this
 125    }
 126
 127    fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 128        let changed_buffers = self
 129            .thread
 130            .read(cx)
 131            .action_log()
 132            .read(cx)
 133            .changed_buffers(cx);
 134
 135        // Sort edited files alphabetically for consistency with Git diff view
 136        let mut sorted_buffers: Vec<_> = changed_buffers.iter().collect();
 137        sorted_buffers.sort_by(|(buffer_a, _), (buffer_b, _)| {
 138            let path_a = buffer_a.read(cx).file().map(|f| f.path().clone());
 139            let path_b = buffer_b.read(cx).file().map(|f| f.path().clone());
 140            path_a.cmp(&path_b)
 141        });
 142
 143        let mut paths_to_delete = self
 144            .multibuffer
 145            .read(cx)
 146            .paths()
 147            .cloned()
 148            .collect::<HashSet<_>>();
 149
 150        for (buffer, diff_handle) in sorted_buffers {
 151            if buffer.read(cx).file().is_none() {
 152                continue;
 153            }
 154
 155            let path_key = PathKey::for_buffer(&buffer, cx);
 156            paths_to_delete.remove(&path_key);
 157
 158            let snapshot = buffer.read(cx).snapshot();
 159
 160            let diff_hunk_ranges = diff_handle
 161                .read(cx)
 162                .snapshot(cx)
 163                .hunks_intersecting_range(
 164                    language::Anchor::min_max_range_for_buffer(snapshot.remote_id()),
 165                    &snapshot,
 166                )
 167                .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
 168                .collect::<Vec<_>>();
 169
 170            let (was_empty, is_excerpt_newly_added) =
 171                self.multibuffer.update(cx, |multibuffer, cx| {
 172                    let was_empty = multibuffer.is_empty();
 173                    let (_, is_excerpt_newly_added) = multibuffer.set_excerpts_for_path(
 174                        path_key.clone(),
 175                        buffer.clone(),
 176                        diff_hunk_ranges,
 177                        multibuffer_context_lines(cx),
 178                        cx,
 179                    );
 180                    multibuffer.add_diff(diff_handle.clone(), cx);
 181                    (was_empty, is_excerpt_newly_added)
 182                });
 183
 184            self.editor.update(cx, |editor, cx| {
 185                if was_empty {
 186                    let first_hunk = editor
 187                        .diff_hunks_in_ranges(
 188                            &[editor::Anchor::min()..editor::Anchor::max()],
 189                            &self.multibuffer.read(cx).read(cx),
 190                        )
 191                        .next();
 192
 193                    if let Some(first_hunk) = first_hunk {
 194                        let first_hunk_start = first_hunk.multi_buffer_range().start;
 195                        editor.change_selections(Default::default(), window, cx, |selections| {
 196                            selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
 197                        })
 198                    }
 199                }
 200
 201                if is_excerpt_newly_added
 202                    && buffer
 203                        .read(cx)
 204                        .file()
 205                        .is_some_and(|file| file.disk_state().is_deleted())
 206                {
 207                    editor.fold_buffer(snapshot.text.remote_id(), cx)
 208                }
 209            });
 210        }
 211
 212        self.multibuffer.update(cx, |multibuffer, cx| {
 213            for path in paths_to_delete {
 214                multibuffer.remove_excerpts_for_path(path, cx);
 215            }
 216        });
 217
 218        if self.multibuffer.read(cx).is_empty()
 219            && self
 220                .editor
 221                .read(cx)
 222                .focus_handle(cx)
 223                .contains_focused(window, cx)
 224        {
 225            self.focus_handle.focus(window, cx);
 226        } else if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
 227            self.editor.update(cx, |editor, cx| {
 228                editor.focus_handle(cx).focus(window, cx);
 229            });
 230        }
 231    }
 232
 233    fn update_title(&mut self, cx: &mut Context<Self>) {
 234        let new_title = self.thread.read(cx).title();
 235        if new_title != self.title {
 236            self.title = new_title;
 237            cx.emit(EditorEvent::TitleChanged);
 238        }
 239    }
 240
 241    fn handle_acp_thread_event(&mut self, event: &AcpThreadEvent, cx: &mut Context<Self>) {
 242        if let AcpThreadEvent::TitleUpdated = event {
 243            self.update_title(cx)
 244        }
 245    }
 246
 247    pub fn move_to_path(&self, path_key: PathKey, window: &mut Window, cx: &mut App) {
 248        if let Some(position) = self.multibuffer.read(cx).location_for_path(&path_key, cx) {
 249            self.editor.update(cx, |editor, cx| {
 250                let first_hunk = editor
 251                    .diff_hunks_in_ranges(
 252                        &[position..editor::Anchor::max()],
 253                        &self.multibuffer.read(cx).read(cx),
 254                    )
 255                    .next();
 256
 257                if let Some(first_hunk) = first_hunk {
 258                    let first_hunk_start = first_hunk.multi_buffer_range().start;
 259                    editor.change_selections(Default::default(), window, cx, |selections| {
 260                        selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
 261                    })
 262                }
 263            });
 264        }
 265    }
 266
 267    fn keep(&mut self, _: &Keep, window: &mut Window, cx: &mut Context<Self>) {
 268        self.editor.update(cx, |editor, cx| {
 269            let snapshot = editor.buffer().read(cx).snapshot(cx);
 270            keep_edits_in_selection(editor, &snapshot, &self.thread, window, cx);
 271        });
 272    }
 273
 274    fn reject(&mut self, _: &Reject, window: &mut Window, cx: &mut Context<Self>) {
 275        self.editor.update(cx, |editor, cx| {
 276            let snapshot = editor.buffer().read(cx).snapshot(cx);
 277            reject_edits_in_selection(editor, &snapshot, &self.thread, window, cx);
 278        });
 279    }
 280
 281    fn reject_all(&mut self, _: &RejectAll, window: &mut Window, cx: &mut Context<Self>) {
 282        self.editor.update(cx, |editor, cx| {
 283            let snapshot = editor.buffer().read(cx).snapshot(cx);
 284            reject_edits_in_ranges(
 285                editor,
 286                &snapshot,
 287                &self.thread,
 288                vec![editor::Anchor::min()..editor::Anchor::max()],
 289                window,
 290                cx,
 291            );
 292        });
 293    }
 294
 295    fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context<Self>) {
 296        let telemetry = ActionLogTelemetry::from(self.thread.read(cx));
 297        let action_log = self.thread.read(cx).action_log().clone();
 298        action_log.update(cx, |action_log, cx| {
 299            action_log.keep_all_edits(Some(telemetry), cx)
 300        });
 301    }
 302}
 303
 304fn keep_edits_in_selection(
 305    editor: &mut Editor,
 306    buffer_snapshot: &MultiBufferSnapshot,
 307    thread: &Entity<AcpThread>,
 308    window: &mut Window,
 309    cx: &mut Context<Editor>,
 310) {
 311    let ranges = editor
 312        .selections
 313        .disjoint_anchor_ranges()
 314        .collect::<Vec<_>>();
 315
 316    keep_edits_in_ranges(editor, buffer_snapshot, thread, ranges, window, cx)
 317}
 318
 319fn reject_edits_in_selection(
 320    editor: &mut Editor,
 321    buffer_snapshot: &MultiBufferSnapshot,
 322    thread: &Entity<AcpThread>,
 323    window: &mut Window,
 324    cx: &mut Context<Editor>,
 325) {
 326    let ranges = editor
 327        .selections
 328        .disjoint_anchor_ranges()
 329        .collect::<Vec<_>>();
 330    reject_edits_in_ranges(editor, buffer_snapshot, thread, ranges, window, cx)
 331}
 332
 333fn keep_edits_in_ranges(
 334    editor: &mut Editor,
 335    buffer_snapshot: &MultiBufferSnapshot,
 336    thread: &Entity<AcpThread>,
 337    ranges: Vec<Range<editor::Anchor>>,
 338    window: &mut Window,
 339    cx: &mut Context<Editor>,
 340) {
 341    let diff_hunks_in_ranges = editor
 342        .diff_hunks_in_ranges(&ranges, buffer_snapshot)
 343        .collect::<Vec<_>>();
 344
 345    update_editor_selection(editor, buffer_snapshot, &diff_hunks_in_ranges, window, cx);
 346
 347    let multibuffer = editor.buffer().clone();
 348    for hunk in &diff_hunks_in_ranges {
 349        let buffer = multibuffer.read(cx).buffer(hunk.buffer_id);
 350        if let Some(buffer) = buffer {
 351            let action_log = thread.read(cx).action_log().clone();
 352            let telemetry = ActionLogTelemetry::from(thread.read(cx));
 353            action_log.update(cx, |action_log, cx| {
 354                action_log.keep_edits_in_range(
 355                    buffer,
 356                    hunk.buffer_range.clone(),
 357                    Some(telemetry),
 358                    cx,
 359                )
 360            });
 361        }
 362    }
 363}
 364
 365fn reject_edits_in_ranges(
 366    editor: &mut Editor,
 367    buffer_snapshot: &MultiBufferSnapshot,
 368    thread: &Entity<AcpThread>,
 369    ranges: Vec<Range<editor::Anchor>>,
 370    window: &mut Window,
 371    cx: &mut Context<Editor>,
 372) {
 373    let diff_hunks_in_ranges = editor
 374        .diff_hunks_in_ranges(&ranges, buffer_snapshot)
 375        .collect::<Vec<_>>();
 376
 377    update_editor_selection(editor, buffer_snapshot, &diff_hunks_in_ranges, window, cx);
 378
 379    let multibuffer = editor.buffer().clone();
 380
 381    let mut ranges_by_buffer = HashMap::default();
 382    for hunk in &diff_hunks_in_ranges {
 383        let buffer = multibuffer.read(cx).buffer(hunk.buffer_id);
 384        if let Some(buffer) = buffer {
 385            ranges_by_buffer
 386                .entry(buffer.clone())
 387                .or_insert_with(Vec::new)
 388                .push(hunk.buffer_range.clone());
 389        }
 390    }
 391
 392    let action_log = thread.read(cx).action_log().clone();
 393    let telemetry = ActionLogTelemetry::from(thread.read(cx));
 394    for (buffer, ranges) in ranges_by_buffer {
 395        action_log
 396            .update(cx, |action_log, cx| {
 397                action_log.reject_edits_in_ranges(buffer, ranges, Some(telemetry.clone()), cx)
 398            })
 399            .detach_and_log_err(cx);
 400    }
 401}
 402
 403fn update_editor_selection(
 404    editor: &mut Editor,
 405    buffer_snapshot: &MultiBufferSnapshot,
 406    diff_hunks: &[multi_buffer::MultiBufferDiffHunk],
 407    window: &mut Window,
 408    cx: &mut Context<Editor>,
 409) {
 410    let newest_cursor = editor
 411        .selections
 412        .newest::<Point>(&editor.display_snapshot(cx))
 413        .head();
 414
 415    if !diff_hunks.iter().any(|hunk| {
 416        hunk.row_range
 417            .contains(&multi_buffer::MultiBufferRow(newest_cursor.row))
 418    }) {
 419        return;
 420    }
 421
 422    let target_hunk = {
 423        diff_hunks
 424            .last()
 425            .and_then(|last_kept_hunk| {
 426                let last_kept_hunk_end = last_kept_hunk.multi_buffer_range().end;
 427                editor
 428                    .diff_hunks_in_ranges(
 429                        &[last_kept_hunk_end..editor::Anchor::max()],
 430                        buffer_snapshot,
 431                    )
 432                    .nth(1)
 433            })
 434            .or_else(|| {
 435                let first_kept_hunk = diff_hunks.first()?;
 436                let first_kept_hunk_start = first_kept_hunk.multi_buffer_range().start;
 437                editor
 438                    .diff_hunks_in_ranges(
 439                        &[editor::Anchor::min()..first_kept_hunk_start],
 440                        buffer_snapshot,
 441                    )
 442                    .next()
 443            })
 444    };
 445
 446    if let Some(target_hunk) = target_hunk {
 447        editor.change_selections(Default::default(), window, cx, |selections| {
 448            let next_hunk_start = target_hunk.multi_buffer_range().start;
 449            selections.select_anchor_ranges([next_hunk_start..next_hunk_start]);
 450        })
 451    }
 452}
 453
 454impl EventEmitter<EditorEvent> for AgentDiffPane {}
 455
 456impl Focusable for AgentDiffPane {
 457    fn focus_handle(&self, cx: &App) -> FocusHandle {
 458        if self.multibuffer.read(cx).is_empty() {
 459            self.focus_handle.clone()
 460        } else {
 461            self.editor.focus_handle(cx)
 462        }
 463    }
 464}
 465
 466impl Item for AgentDiffPane {
 467    type Event = EditorEvent;
 468
 469    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
 470        Some(Icon::new(IconName::ZedAssistant).color(Color::Muted))
 471    }
 472
 473    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
 474        Editor::to_item_events(event, f)
 475    }
 476
 477    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 478        self.editor
 479            .update(cx, |editor, cx| editor.deactivated(window, cx));
 480    }
 481
 482    fn navigate(
 483        &mut self,
 484        data: Arc<dyn Any + Send>,
 485        window: &mut Window,
 486        cx: &mut Context<Self>,
 487    ) -> bool {
 488        self.editor
 489            .update(cx, |editor, cx| editor.navigate(data, window, cx))
 490    }
 491
 492    fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
 493        Some("Agent Diff".into())
 494    }
 495
 496    fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
 497        let title = self.thread.read(cx).title();
 498        Label::new(format!("Review: {}", title))
 499            .color(if params.selected {
 500                Color::Default
 501            } else {
 502                Color::Muted
 503            })
 504            .into_any_element()
 505    }
 506
 507    fn telemetry_event_text(&self) -> Option<&'static str> {
 508        Some("Assistant Diff Opened")
 509    }
 510
 511    fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
 512        Some(Box::new(self.editor.clone()))
 513    }
 514
 515    fn for_each_project_item(
 516        &self,
 517        cx: &App,
 518        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
 519    ) {
 520        self.editor.for_each_project_item(cx, f)
 521    }
 522
 523    fn set_nav_history(
 524        &mut self,
 525        nav_history: ItemNavHistory,
 526        _: &mut Window,
 527        cx: &mut Context<Self>,
 528    ) {
 529        self.editor.update(cx, |editor, _| {
 530            editor.set_nav_history(Some(nav_history));
 531        });
 532    }
 533
 534    fn can_split(&self) -> bool {
 535        true
 536    }
 537
 538    fn clone_on_split(
 539        &self,
 540        _workspace_id: Option<workspace::WorkspaceId>,
 541        window: &mut Window,
 542        cx: &mut Context<Self>,
 543    ) -> Task<Option<Entity<Self>>>
 544    where
 545        Self: Sized,
 546    {
 547        Task::ready(Some(cx.new(|cx| {
 548            Self::new(self.thread.clone(), self.workspace.clone(), window, cx)
 549        })))
 550    }
 551
 552    fn is_dirty(&self, cx: &App) -> bool {
 553        self.multibuffer.read(cx).is_dirty(cx)
 554    }
 555
 556    fn has_conflict(&self, cx: &App) -> bool {
 557        self.multibuffer.read(cx).has_conflict(cx)
 558    }
 559
 560    fn can_save(&self, _: &App) -> bool {
 561        true
 562    }
 563
 564    fn save(
 565        &mut self,
 566        options: SaveOptions,
 567        project: Entity<Project>,
 568        window: &mut Window,
 569        cx: &mut Context<Self>,
 570    ) -> Task<Result<()>> {
 571        self.editor.save(options, project, window, cx)
 572    }
 573
 574    fn save_as(
 575        &mut self,
 576        _: Entity<Project>,
 577        _: ProjectPath,
 578        _window: &mut Window,
 579        _: &mut Context<Self>,
 580    ) -> Task<Result<()>> {
 581        unreachable!()
 582    }
 583
 584    fn reload(
 585        &mut self,
 586        project: Entity<Project>,
 587        window: &mut Window,
 588        cx: &mut Context<Self>,
 589    ) -> Task<Result<()>> {
 590        self.editor.reload(project, window, cx)
 591    }
 592
 593    fn act_as_type<'a>(
 594        &'a self,
 595        type_id: TypeId,
 596        self_handle: &'a Entity<Self>,
 597        _: &'a App,
 598    ) -> Option<gpui::AnyEntity> {
 599        if type_id == TypeId::of::<Self>() {
 600            Some(self_handle.clone().into())
 601        } else if type_id == TypeId::of::<Editor>() {
 602            Some(self.editor.clone().into())
 603        } else {
 604            None
 605        }
 606    }
 607
 608    fn added_to_workspace(
 609        &mut self,
 610        workspace: &mut Workspace,
 611        window: &mut Window,
 612        cx: &mut Context<Self>,
 613    ) {
 614        self.editor.update(cx, |editor, cx| {
 615            editor.added_to_workspace(workspace, window, cx)
 616        });
 617    }
 618
 619    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
 620        "Agent Diff".into()
 621    }
 622}
 623
 624impl Render for AgentDiffPane {
 625    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 626        let is_empty = self.multibuffer.read(cx).is_empty();
 627        let focus_handle = &self.focus_handle;
 628
 629        div()
 630            .track_focus(focus_handle)
 631            .key_context(if is_empty { "EmptyPane" } else { "AgentDiff" })
 632            .on_action(cx.listener(Self::keep))
 633            .on_action(cx.listener(Self::reject))
 634            .on_action(cx.listener(Self::reject_all))
 635            .on_action(cx.listener(Self::keep_all))
 636            .bg(cx.theme().colors().editor_background)
 637            .flex()
 638            .items_center()
 639            .justify_center()
 640            .size_full()
 641            .when(is_empty, |el| {
 642                el.child(
 643                    v_flex()
 644                        .items_center()
 645                        .gap_2()
 646                        .child("No changes to review")
 647                        .child(
 648                            Button::new("continue-iterating", "Continue Iterating")
 649                                .style(ButtonStyle::Filled)
 650                                .icon(IconName::ForwardArrow)
 651                                .icon_position(IconPosition::Start)
 652                                .icon_size(IconSize::Small)
 653                                .icon_color(Color::Muted)
 654                                .full_width()
 655                                .key_binding(KeyBinding::for_action_in(
 656                                    &ToggleFocus,
 657                                    &focus_handle.clone(),
 658                                    cx,
 659                                ))
 660                                .on_click(|_event, window, cx| {
 661                                    window.dispatch_action(ToggleFocus.boxed_clone(), cx)
 662                                }),
 663                        ),
 664                )
 665            })
 666            .when(!is_empty, |el| el.child(self.editor.clone()))
 667    }
 668}
 669
 670fn diff_hunk_controls(thread: &Entity<AcpThread>) -> editor::RenderDiffHunkControlsFn {
 671    let thread = thread.clone();
 672
 673    Arc::new(
 674        move |row, status, hunk_range, is_created_file, line_height, editor, _, cx| {
 675            {
 676                render_diff_hunk_controls(
 677                    row,
 678                    status,
 679                    hunk_range,
 680                    is_created_file,
 681                    line_height,
 682                    &thread,
 683                    editor,
 684                    cx,
 685                )
 686            }
 687        },
 688    )
 689}
 690
 691fn render_diff_hunk_controls(
 692    row: u32,
 693    _status: &DiffHunkStatus,
 694    hunk_range: Range<editor::Anchor>,
 695    is_created_file: bool,
 696    line_height: Pixels,
 697    thread: &Entity<AcpThread>,
 698    editor: &Entity<Editor>,
 699    cx: &mut App,
 700) -> AnyElement {
 701    let editor = editor.clone();
 702
 703    h_flex()
 704        .h(line_height)
 705        .mr_0p5()
 706        .gap_1()
 707        .px_0p5()
 708        .pb_1()
 709        .border_x_1()
 710        .border_b_1()
 711        .border_color(cx.theme().colors().border)
 712        .rounded_b_md()
 713        .bg(cx.theme().colors().editor_background)
 714        .gap_1()
 715        .block_mouse_except_scroll()
 716        .shadow_md()
 717        .children(vec![
 718            Button::new(("reject", row as u64), "Reject")
 719                .disabled(is_created_file)
 720                .key_binding(
 721                    KeyBinding::for_action_in(&Reject, &editor.read(cx).focus_handle(cx), cx)
 722                        .map(|kb| kb.size(rems_from_px(12.))),
 723                )
 724                .on_click({
 725                    let editor = editor.clone();
 726                    let thread = thread.clone();
 727                    move |_event, window, cx| {
 728                        editor.update(cx, |editor, cx| {
 729                            let snapshot = editor.buffer().read(cx).snapshot(cx);
 730                            reject_edits_in_ranges(
 731                                editor,
 732                                &snapshot,
 733                                &thread,
 734                                vec![hunk_range.start..hunk_range.start],
 735                                window,
 736                                cx,
 737                            );
 738                        })
 739                    }
 740                }),
 741            Button::new(("keep", row as u64), "Keep")
 742                .key_binding(
 743                    KeyBinding::for_action_in(&Keep, &editor.read(cx).focus_handle(cx), cx)
 744                        .map(|kb| kb.size(rems_from_px(12.))),
 745                )
 746                .on_click({
 747                    let editor = editor.clone();
 748                    let thread = thread.clone();
 749                    move |_event, window, cx| {
 750                        editor.update(cx, |editor, cx| {
 751                            let snapshot = editor.buffer().read(cx).snapshot(cx);
 752                            keep_edits_in_ranges(
 753                                editor,
 754                                &snapshot,
 755                                &thread,
 756                                vec![hunk_range.start..hunk_range.start],
 757                                window,
 758                                cx,
 759                            );
 760                        });
 761                    }
 762                }),
 763        ])
 764        .when(
 765            !editor.read(cx).buffer().read(cx).all_diff_hunks_expanded(),
 766            |el| {
 767                el.child(
 768                    IconButton::new(("next-hunk", row as u64), IconName::ArrowDown)
 769                        .shape(IconButtonShape::Square)
 770                        .icon_size(IconSize::Small)
 771                        // .disabled(!has_multiple_hunks)
 772                        .tooltip({
 773                            let focus_handle = editor.focus_handle(cx);
 774                            move |_window, cx| {
 775                                Tooltip::for_action_in("Next Hunk", &GoToHunk, &focus_handle, cx)
 776                            }
 777                        })
 778                        .on_click({
 779                            let editor = editor.clone();
 780                            move |_event, window, cx| {
 781                                editor.update(cx, |editor, cx| {
 782                                    let snapshot = editor.snapshot(window, cx);
 783                                    let position =
 784                                        hunk_range.end.to_point(&snapshot.buffer_snapshot());
 785                                    editor.go_to_hunk_before_or_after_position(
 786                                        &snapshot,
 787                                        position,
 788                                        Direction::Next,
 789                                        window,
 790                                        cx,
 791                                    );
 792                                    editor.expand_selected_diff_hunks(cx);
 793                                });
 794                            }
 795                        }),
 796                )
 797                .child(
 798                    IconButton::new(("prev-hunk", row as u64), IconName::ArrowUp)
 799                        .shape(IconButtonShape::Square)
 800                        .icon_size(IconSize::Small)
 801                        // .disabled(!has_multiple_hunks)
 802                        .tooltip({
 803                            let focus_handle = editor.focus_handle(cx);
 804                            move |_window, cx| {
 805                                Tooltip::for_action_in(
 806                                    "Previous Hunk",
 807                                    &GoToPreviousHunk,
 808                                    &focus_handle,
 809                                    cx,
 810                                )
 811                            }
 812                        })
 813                        .on_click({
 814                            let editor = editor.clone();
 815                            move |_event, window, cx| {
 816                                editor.update(cx, |editor, cx| {
 817                                    let snapshot = editor.snapshot(window, cx);
 818                                    let point =
 819                                        hunk_range.start.to_point(&snapshot.buffer_snapshot());
 820                                    editor.go_to_hunk_before_or_after_position(
 821                                        &snapshot,
 822                                        point,
 823                                        Direction::Prev,
 824                                        window,
 825                                        cx,
 826                                    );
 827                                    editor.expand_selected_diff_hunks(cx);
 828                                });
 829                            }
 830                        }),
 831                )
 832            },
 833        )
 834        .into_any_element()
 835}
 836
 837struct AgentDiffAddon;
 838
 839impl editor::Addon for AgentDiffAddon {
 840    fn to_any(&self) -> &dyn std::any::Any {
 841        self
 842    }
 843
 844    fn extend_key_context(&self, key_context: &mut gpui::KeyContext, _: &App) {
 845        key_context.add("agent_diff");
 846    }
 847}
 848
 849pub struct AgentDiffToolbar {
 850    active_item: Option<AgentDiffToolbarItem>,
 851    _settings_subscription: Subscription,
 852}
 853
 854pub enum AgentDiffToolbarItem {
 855    Pane(WeakEntity<AgentDiffPane>),
 856    Editor {
 857        editor: WeakEntity<Editor>,
 858        state: EditorState,
 859        _diff_subscription: Subscription,
 860    },
 861}
 862
 863impl AgentDiffToolbar {
 864    pub fn new(cx: &mut Context<Self>) -> Self {
 865        Self {
 866            active_item: None,
 867            _settings_subscription: cx.observe_global::<SettingsStore>(Self::update_location),
 868        }
 869    }
 870
 871    fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut Context<Self>) {
 872        let Some(active_item) = self.active_item.as_ref() else {
 873            return;
 874        };
 875
 876        match active_item {
 877            AgentDiffToolbarItem::Pane(agent_diff) => {
 878                if let Some(agent_diff) = agent_diff.upgrade() {
 879                    agent_diff.focus_handle(cx).focus(window, cx);
 880                }
 881            }
 882            AgentDiffToolbarItem::Editor { editor, .. } => {
 883                if let Some(editor) = editor.upgrade() {
 884                    editor.read(cx).focus_handle(cx).focus(window, cx);
 885                }
 886            }
 887        }
 888
 889        let action = action.boxed_clone();
 890        cx.defer(move |cx| {
 891            cx.dispatch_action(action.as_ref());
 892        })
 893    }
 894
 895    fn handle_diff_notify(&mut self, agent_diff: Entity<AgentDiff>, cx: &mut Context<Self>) {
 896        let Some(AgentDiffToolbarItem::Editor { editor, state, .. }) = self.active_item.as_mut()
 897        else {
 898            return;
 899        };
 900
 901        *state = agent_diff.read(cx).editor_state(editor);
 902        self.update_location(cx);
 903        cx.notify();
 904    }
 905
 906    fn update_location(&mut self, cx: &mut Context<Self>) {
 907        let location = self.location(cx);
 908        cx.emit(ToolbarItemEvent::ChangeLocation(location));
 909    }
 910
 911    fn location(&self, cx: &App) -> ToolbarItemLocation {
 912        if !EditorSettings::get_global(cx).toolbar.agent_review {
 913            return ToolbarItemLocation::Hidden;
 914        }
 915
 916        match &self.active_item {
 917            None => ToolbarItemLocation::Hidden,
 918            Some(AgentDiffToolbarItem::Pane(_)) => ToolbarItemLocation::PrimaryRight,
 919            Some(AgentDiffToolbarItem::Editor { state, .. }) => match state {
 920                EditorState::Reviewing => ToolbarItemLocation::PrimaryRight,
 921                EditorState::Idle => ToolbarItemLocation::Hidden,
 922            },
 923        }
 924    }
 925}
 926
 927impl EventEmitter<ToolbarItemEvent> for AgentDiffToolbar {}
 928
 929impl ToolbarItemView for AgentDiffToolbar {
 930    fn set_active_pane_item(
 931        &mut self,
 932        active_pane_item: Option<&dyn ItemHandle>,
 933        _: &mut Window,
 934        cx: &mut Context<Self>,
 935    ) -> ToolbarItemLocation {
 936        if let Some(item) = active_pane_item {
 937            if let Some(pane) = item.act_as::<AgentDiffPane>(cx) {
 938                self.active_item = Some(AgentDiffToolbarItem::Pane(pane.downgrade()));
 939                return self.location(cx);
 940            }
 941
 942            if let Some(editor) = item.act_as::<Editor>(cx)
 943                && editor.read(cx).mode().is_full()
 944            {
 945                let agent_diff = AgentDiff::global(cx);
 946
 947                self.active_item = Some(AgentDiffToolbarItem::Editor {
 948                    editor: editor.downgrade(),
 949                    state: agent_diff.read(cx).editor_state(&editor.downgrade()),
 950                    _diff_subscription: cx.observe(&agent_diff, Self::handle_diff_notify),
 951                });
 952
 953                return self.location(cx);
 954            }
 955        }
 956
 957        self.active_item = None;
 958        self.location(cx)
 959    }
 960
 961    fn pane_focus_update(
 962        &mut self,
 963        _pane_focused: bool,
 964        _window: &mut Window,
 965        _cx: &mut Context<Self>,
 966    ) {
 967    }
 968}
 969
 970impl Render for AgentDiffToolbar {
 971    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 972        let spinner_icon = div()
 973            .px_0p5()
 974            .id("generating")
 975            .tooltip(Tooltip::text("Generating Changes…"))
 976            .child(
 977                Icon::new(IconName::LoadCircle)
 978                    .size(IconSize::Small)
 979                    .color(Color::Accent)
 980                    .with_rotate_animation(3),
 981            )
 982            .into_any();
 983
 984        let Some(active_item) = self.active_item.as_ref() else {
 985            return Empty.into_any();
 986        };
 987
 988        match active_item {
 989            AgentDiffToolbarItem::Editor { editor, state, .. } => {
 990                let Some(editor) = editor.upgrade() else {
 991                    return Empty.into_any();
 992                };
 993
 994                let editor_focus_handle = editor.read(cx).focus_handle(cx);
 995
 996                let content = match state {
 997                    EditorState::Idle => return Empty.into_any(),
 998                    EditorState::Reviewing => vec![
 999                        h_flex()
1000                            .child(
1001                                IconButton::new("hunk-up", IconName::ArrowUp)
1002                                    .icon_size(IconSize::Small)
1003                                    .tooltip(Tooltip::for_action_title_in(
1004                                        "Previous Hunk",
1005                                        &GoToPreviousHunk,
1006                                        &editor_focus_handle,
1007                                    ))
1008                                    .on_click({
1009                                        let editor_focus_handle = editor_focus_handle.clone();
1010                                        move |_, window, cx| {
1011                                            editor_focus_handle.dispatch_action(
1012                                                &GoToPreviousHunk,
1013                                                window,
1014                                                cx,
1015                                            );
1016                                        }
1017                                    }),
1018                            )
1019                            .child(
1020                                IconButton::new("hunk-down", IconName::ArrowDown)
1021                                    .icon_size(IconSize::Small)
1022                                    .tooltip(Tooltip::for_action_title_in(
1023                                        "Next Hunk",
1024                                        &GoToHunk,
1025                                        &editor_focus_handle,
1026                                    ))
1027                                    .on_click({
1028                                        let editor_focus_handle = editor_focus_handle.clone();
1029                                        move |_, window, cx| {
1030                                            editor_focus_handle
1031                                                .dispatch_action(&GoToHunk, window, cx);
1032                                        }
1033                                    }),
1034                            )
1035                            .into_any_element(),
1036                        vertical_divider().into_any_element(),
1037                        h_flex()
1038                            .gap_0p5()
1039                            .child(
1040                                Button::new("reject-all", "Reject All")
1041                                    .key_binding({
1042                                        KeyBinding::for_action_in(
1043                                            &RejectAll,
1044                                            &editor_focus_handle,
1045                                            cx,
1046                                        )
1047                                        .map(|kb| kb.size(rems_from_px(12.)))
1048                                    })
1049                                    .on_click(cx.listener(|this, _, window, cx| {
1050                                        this.dispatch_action(&RejectAll, window, cx)
1051                                    })),
1052                            )
1053                            .child(
1054                                Button::new("keep-all", "Keep All")
1055                                    .key_binding({
1056                                        KeyBinding::for_action_in(
1057                                            &KeepAll,
1058                                            &editor_focus_handle,
1059                                            cx,
1060                                        )
1061                                        .map(|kb| kb.size(rems_from_px(12.)))
1062                                    })
1063                                    .on_click(cx.listener(|this, _, window, cx| {
1064                                        this.dispatch_action(&KeepAll, window, cx)
1065                                    })),
1066                            )
1067                            .into_any_element(),
1068                    ],
1069                };
1070
1071                h_flex()
1072                    .track_focus(&editor_focus_handle)
1073                    .size_full()
1074                    .px_1()
1075                    .mr_1()
1076                    .gap_1()
1077                    .children(content)
1078                    .child(vertical_divider())
1079                    .when_some(editor.read(cx).workspace(), |this, _workspace| {
1080                        this.child(
1081                            IconButton::new("review", IconName::ListTodo)
1082                                .icon_size(IconSize::Small)
1083                                .tooltip(Tooltip::for_action_title_in(
1084                                    "Review All Files",
1085                                    &OpenAgentDiff,
1086                                    &editor_focus_handle,
1087                                ))
1088                                .on_click({
1089                                    cx.listener(move |this, _, window, cx| {
1090                                        this.dispatch_action(&OpenAgentDiff, window, cx);
1091                                    })
1092                                }),
1093                        )
1094                    })
1095                    .child(vertical_divider())
1096                    .on_action({
1097                        let editor = editor.clone();
1098                        move |_action: &OpenAgentDiff, window, cx| {
1099                            AgentDiff::global(cx).update(cx, |agent_diff, cx| {
1100                                agent_diff.deploy_pane_from_editor(&editor, window, cx);
1101                            });
1102                        }
1103                    })
1104                    .into_any()
1105            }
1106            AgentDiffToolbarItem::Pane(agent_diff) => {
1107                let Some(agent_diff) = agent_diff.upgrade() else {
1108                    return Empty.into_any();
1109                };
1110
1111                let has_pending_edit_tool_use = agent_diff
1112                    .read(cx)
1113                    .thread
1114                    .read(cx)
1115                    .has_pending_edit_tool_calls();
1116
1117                if has_pending_edit_tool_use {
1118                    return div().px_2().child(spinner_icon).into_any();
1119                }
1120
1121                let is_empty = agent_diff.read(cx).multibuffer.read(cx).is_empty();
1122                if is_empty {
1123                    return Empty.into_any();
1124                }
1125
1126                let focus_handle = agent_diff.focus_handle(cx);
1127
1128                h_group_xl()
1129                    .my_neg_1()
1130                    .py_1()
1131                    .items_center()
1132                    .flex_wrap()
1133                    .child(
1134                        h_group_sm()
1135                            .child(
1136                                Button::new("reject-all", "Reject All")
1137                                    .key_binding({
1138                                        KeyBinding::for_action_in(&RejectAll, &focus_handle, cx)
1139                                            .map(|kb| kb.size(rems_from_px(12.)))
1140                                    })
1141                                    .on_click(cx.listener(|this, _, window, cx| {
1142                                        this.dispatch_action(&RejectAll, window, cx)
1143                                    })),
1144                            )
1145                            .child(
1146                                Button::new("keep-all", "Keep All")
1147                                    .key_binding({
1148                                        KeyBinding::for_action_in(&KeepAll, &focus_handle, cx)
1149                                            .map(|kb| kb.size(rems_from_px(12.)))
1150                                    })
1151                                    .on_click(cx.listener(|this, _, window, cx| {
1152                                        this.dispatch_action(&KeepAll, window, cx)
1153                                    })),
1154                            ),
1155                    )
1156                    .into_any()
1157            }
1158        }
1159    }
1160}
1161
1162#[derive(Default)]
1163pub struct AgentDiff {
1164    reviewing_editors: HashMap<WeakEntity<Editor>, EditorState>,
1165    workspace_threads: HashMap<WeakEntity<Workspace>, WorkspaceThread>,
1166}
1167
1168#[derive(Clone, Debug, PartialEq, Eq)]
1169pub enum EditorState {
1170    Idle,
1171    Reviewing,
1172}
1173
1174struct WorkspaceThread {
1175    thread: WeakEntity<AcpThread>,
1176    _thread_subscriptions: (Subscription, Subscription),
1177    singleton_editors: HashMap<WeakEntity<Buffer>, HashMap<WeakEntity<Editor>, Subscription>>,
1178    _settings_subscription: Subscription,
1179    _workspace_subscription: Option<Subscription>,
1180}
1181
1182struct AgentDiffGlobal(Entity<AgentDiff>);
1183
1184impl Global for AgentDiffGlobal {}
1185
1186impl AgentDiff {
1187    fn global(cx: &mut App) -> Entity<Self> {
1188        cx.try_global::<AgentDiffGlobal>()
1189            .map(|global| global.0.clone())
1190            .unwrap_or_else(|| {
1191                let entity = cx.new(|_cx| Self::default());
1192                let global = AgentDiffGlobal(entity.clone());
1193                cx.set_global(global);
1194                entity
1195            })
1196    }
1197
1198    pub fn set_active_thread(
1199        workspace: &WeakEntity<Workspace>,
1200        thread: Entity<AcpThread>,
1201        window: &mut Window,
1202        cx: &mut App,
1203    ) {
1204        Self::global(cx).update(cx, |this, cx| {
1205            this.register_active_thread_impl(workspace, thread, window, cx);
1206        });
1207    }
1208
1209    fn register_active_thread_impl(
1210        &mut self,
1211        workspace: &WeakEntity<Workspace>,
1212        thread: Entity<AcpThread>,
1213        window: &mut Window,
1214        cx: &mut Context<Self>,
1215    ) {
1216        let action_log = thread.read(cx).action_log().clone();
1217
1218        let action_log_subscription = cx.observe_in(&action_log, window, {
1219            let workspace = workspace.clone();
1220            move |this, _action_log, window, cx| {
1221                this.update_reviewing_editors(&workspace, window, cx);
1222            }
1223        });
1224
1225        let thread_subscription = cx.subscribe_in(&thread, window, {
1226            let workspace = workspace.clone();
1227            move |this, thread, event, window, cx| {
1228                this.handle_acp_thread_event(&workspace, thread, event, window, cx)
1229            }
1230        });
1231
1232        if let Some(workspace_thread) = self.workspace_threads.get_mut(workspace) {
1233            // replace thread and action log subscription, but keep editors
1234            workspace_thread.thread = thread.downgrade();
1235            workspace_thread._thread_subscriptions = (action_log_subscription, thread_subscription);
1236            self.update_reviewing_editors(workspace, window, cx);
1237            return;
1238        }
1239
1240        let settings_subscription = cx.observe_global_in::<SettingsStore>(window, {
1241            let workspace = workspace.clone();
1242            let mut was_active = AgentSettings::get_global(cx).single_file_review;
1243            move |this, window, cx| {
1244                let is_active = AgentSettings::get_global(cx).single_file_review;
1245                if was_active != is_active {
1246                    was_active = is_active;
1247                    this.update_reviewing_editors(&workspace, window, cx);
1248                }
1249            }
1250        });
1251
1252        let workspace_subscription = workspace
1253            .upgrade()
1254            .map(|workspace| cx.subscribe_in(&workspace, window, Self::handle_workspace_event));
1255
1256        self.workspace_threads.insert(
1257            workspace.clone(),
1258            WorkspaceThread {
1259                thread: thread.downgrade(),
1260                _thread_subscriptions: (action_log_subscription, thread_subscription),
1261                singleton_editors: HashMap::default(),
1262                _settings_subscription: settings_subscription,
1263                _workspace_subscription: workspace_subscription,
1264            },
1265        );
1266
1267        let workspace = workspace.clone();
1268        cx.defer_in(window, move |this, window, cx| {
1269            if let Some(workspace) = workspace.upgrade() {
1270                this.register_workspace(workspace, window, cx);
1271            }
1272        });
1273    }
1274
1275    fn register_workspace(
1276        &mut self,
1277        workspace: Entity<Workspace>,
1278        window: &mut Window,
1279        cx: &mut Context<Self>,
1280    ) {
1281        let agent_diff = cx.entity();
1282
1283        let editors = workspace.update(cx, |workspace, cx| {
1284            let agent_diff = agent_diff.clone();
1285
1286            Self::register_review_action::<Keep>(workspace, Self::keep, &agent_diff);
1287            Self::register_review_action::<Reject>(workspace, Self::reject, &agent_diff);
1288            Self::register_review_action::<KeepAll>(workspace, Self::keep_all, &agent_diff);
1289            Self::register_review_action::<RejectAll>(workspace, Self::reject_all, &agent_diff);
1290
1291            workspace.items_of_type(cx).collect::<Vec<_>>()
1292        });
1293
1294        let weak_workspace = workspace.downgrade();
1295
1296        for editor in editors {
1297            if let Some(buffer) = Self::full_editor_buffer(editor.read(cx), cx) {
1298                self.register_editor(weak_workspace.clone(), buffer, editor, window, cx);
1299            };
1300        }
1301
1302        self.update_reviewing_editors(&weak_workspace, window, cx);
1303    }
1304
1305    fn register_review_action<T: Action>(
1306        workspace: &mut Workspace,
1307        review: impl Fn(&Entity<Editor>, &Entity<AcpThread>, &mut Window, &mut App) -> PostReviewState
1308        + 'static,
1309        this: &Entity<AgentDiff>,
1310    ) {
1311        let this = this.clone();
1312        workspace.register_action(move |workspace, _: &T, window, cx| {
1313            let review = &review;
1314            let task = this.update(cx, |this, cx| {
1315                this.review_in_active_editor(workspace, review, window, cx)
1316            });
1317
1318            if let Some(task) = task {
1319                task.detach_and_log_err(cx);
1320            } else {
1321                cx.propagate();
1322            }
1323        });
1324    }
1325
1326    fn handle_acp_thread_event(
1327        &mut self,
1328        workspace: &WeakEntity<Workspace>,
1329        thread: &Entity<AcpThread>,
1330        event: &AcpThreadEvent,
1331        window: &mut Window,
1332        cx: &mut Context<Self>,
1333    ) {
1334        match event {
1335            AcpThreadEvent::NewEntry => {
1336                if thread
1337                    .read(cx)
1338                    .entries()
1339                    .last()
1340                    .is_some_and(|entry| entry.diffs().next().is_some())
1341                {
1342                    self.update_reviewing_editors(workspace, window, cx);
1343                }
1344            }
1345            AcpThreadEvent::EntryUpdated(ix) => {
1346                if thread
1347                    .read(cx)
1348                    .entries()
1349                    .get(*ix)
1350                    .is_some_and(|entry| entry.diffs().next().is_some())
1351                {
1352                    self.update_reviewing_editors(workspace, window, cx);
1353                }
1354            }
1355            AcpThreadEvent::Stopped
1356            | AcpThreadEvent::Error
1357            | AcpThreadEvent::LoadError(_)
1358            | AcpThreadEvent::Refusal => {
1359                self.update_reviewing_editors(workspace, window, cx);
1360            }
1361            AcpThreadEvent::TitleUpdated
1362            | AcpThreadEvent::TokenUsageUpdated
1363            | AcpThreadEvent::EntriesRemoved(_)
1364            | AcpThreadEvent::ToolAuthorizationRequired
1365            | AcpThreadEvent::PromptCapabilitiesUpdated
1366            | AcpThreadEvent::AvailableCommandsUpdated(_)
1367            | AcpThreadEvent::Retry(_)
1368            | AcpThreadEvent::ModeUpdated(_)
1369            | AcpThreadEvent::ConfigOptionsUpdated(_) => {}
1370        }
1371    }
1372
1373    fn handle_workspace_event(
1374        &mut self,
1375        workspace: &Entity<Workspace>,
1376        event: &workspace::Event,
1377        window: &mut Window,
1378        cx: &mut Context<Self>,
1379    ) {
1380        if let workspace::Event::ItemAdded { item } = event
1381            && let Some(editor) = item.downcast::<Editor>()
1382            && let Some(buffer) = Self::full_editor_buffer(editor.read(cx), cx)
1383        {
1384            self.register_editor(workspace.downgrade(), buffer, editor, window, cx);
1385        }
1386    }
1387
1388    fn full_editor_buffer(editor: &Editor, cx: &App) -> Option<WeakEntity<Buffer>> {
1389        if editor.mode().is_full() {
1390            editor
1391                .buffer()
1392                .read(cx)
1393                .as_singleton()
1394                .map(|buffer| buffer.downgrade())
1395        } else {
1396            None
1397        }
1398    }
1399
1400    fn register_editor(
1401        &mut self,
1402        workspace: WeakEntity<Workspace>,
1403        buffer: WeakEntity<Buffer>,
1404        editor: Entity<Editor>,
1405        window: &mut Window,
1406        cx: &mut Context<Self>,
1407    ) {
1408        let Some(workspace_thread) = self.workspace_threads.get_mut(&workspace) else {
1409            return;
1410        };
1411
1412        let weak_editor = editor.downgrade();
1413
1414        workspace_thread
1415            .singleton_editors
1416            .entry(buffer.clone())
1417            .or_default()
1418            .entry(weak_editor.clone())
1419            .or_insert_with(|| {
1420                let workspace = workspace.clone();
1421                cx.observe_release(&editor, move |this, _, _cx| {
1422                    let Some(active_thread) = this.workspace_threads.get_mut(&workspace) else {
1423                        return;
1424                    };
1425
1426                    if let Entry::Occupied(mut entry) =
1427                        active_thread.singleton_editors.entry(buffer)
1428                    {
1429                        let set = entry.get_mut();
1430                        set.remove(&weak_editor);
1431
1432                        if set.is_empty() {
1433                            entry.remove();
1434                        }
1435                    }
1436                })
1437            });
1438
1439        self.update_reviewing_editors(&workspace, window, cx);
1440    }
1441
1442    fn update_reviewing_editors(
1443        &mut self,
1444        workspace: &WeakEntity<Workspace>,
1445        window: &mut Window,
1446        cx: &mut Context<Self>,
1447    ) {
1448        if !AgentSettings::get_global(cx).single_file_review || cx.has_flag::<AgentV2FeatureFlag>()
1449        {
1450            for (editor, _) in self.reviewing_editors.drain() {
1451                editor
1452                    .update(cx, |editor, cx| {
1453                        editor.end_temporary_diff_override(cx);
1454                        editor.unregister_addon::<EditorAgentDiffAddon>();
1455                    })
1456                    .ok();
1457            }
1458            return;
1459        }
1460
1461        let Some(workspace_thread) = self.workspace_threads.get_mut(workspace) else {
1462            return;
1463        };
1464
1465        let Some(thread) = workspace_thread.thread.upgrade() else {
1466            return;
1467        };
1468
1469        let action_log = thread.read(cx).action_log();
1470        let changed_buffers = action_log.read(cx).changed_buffers(cx);
1471
1472        let mut unaffected = self.reviewing_editors.clone();
1473
1474        for (buffer, diff_handle) in changed_buffers {
1475            if buffer.read(cx).file().is_none() {
1476                continue;
1477            }
1478
1479            let Some(buffer_editors) = workspace_thread.singleton_editors.get(&buffer.downgrade())
1480            else {
1481                continue;
1482            };
1483
1484            for weak_editor in buffer_editors.keys() {
1485                let Some(editor) = weak_editor.upgrade() else {
1486                    continue;
1487                };
1488
1489                let multibuffer = editor.read(cx).buffer().clone();
1490                multibuffer.update(cx, |multibuffer, cx| {
1491                    multibuffer.add_diff(diff_handle.clone(), cx);
1492                });
1493
1494                let reviewing_state = EditorState::Reviewing;
1495
1496                let previous_state = self
1497                    .reviewing_editors
1498                    .insert(weak_editor.clone(), reviewing_state.clone());
1499
1500                if previous_state.is_none() {
1501                    editor.update(cx, |editor, cx| {
1502                        editor.start_temporary_diff_override();
1503                        editor.set_render_diff_hunk_controls(diff_hunk_controls(&thread), cx);
1504                        editor.set_expand_all_diff_hunks(cx);
1505                        editor.register_addon(EditorAgentDiffAddon);
1506                    });
1507                } else {
1508                    unaffected.remove(weak_editor);
1509                }
1510
1511                if reviewing_state == EditorState::Reviewing
1512                    && previous_state != Some(reviewing_state)
1513                {
1514                    // Jump to first hunk when we enter review mode
1515                    editor.update(cx, |editor, cx| {
1516                        let snapshot = multibuffer.read(cx).snapshot(cx);
1517                        if let Some(first_hunk) = snapshot.diff_hunks().next() {
1518                            let first_hunk_start = first_hunk.multi_buffer_range().start;
1519
1520                            editor.change_selections(
1521                                SelectionEffects::scroll(Autoscroll::center()),
1522                                window,
1523                                cx,
1524                                |selections| {
1525                                    selections.select_ranges([first_hunk_start..first_hunk_start])
1526                                },
1527                            );
1528                        }
1529                    });
1530                }
1531            }
1532        }
1533
1534        // Remove editors from this workspace that are no longer under review
1535        for (editor, _) in unaffected {
1536            // Note: We could avoid this check by storing `reviewing_editors` by Workspace,
1537            // but that would add another lookup in `AgentDiff::editor_state`
1538            // which gets called much more frequently.
1539            let in_workspace = editor
1540                .read_with(cx, |editor, _cx| editor.workspace())
1541                .ok()
1542                .flatten()
1543                .is_some_and(|editor_workspace| {
1544                    editor_workspace.entity_id() == workspace.entity_id()
1545                });
1546
1547            if in_workspace {
1548                editor
1549                    .update(cx, |editor, cx| {
1550                        editor.end_temporary_diff_override(cx);
1551                        editor.unregister_addon::<EditorAgentDiffAddon>();
1552                    })
1553                    .ok();
1554                self.reviewing_editors.remove(&editor);
1555            }
1556        }
1557
1558        cx.notify();
1559    }
1560
1561    fn editor_state(&self, editor: &WeakEntity<Editor>) -> EditorState {
1562        self.reviewing_editors
1563            .get(editor)
1564            .cloned()
1565            .unwrap_or(EditorState::Idle)
1566    }
1567
1568    fn deploy_pane_from_editor(&self, editor: &Entity<Editor>, window: &mut Window, cx: &mut App) {
1569        let Some(workspace) = editor.read(cx).workspace() else {
1570            return;
1571        };
1572
1573        let Some(WorkspaceThread { thread, .. }) =
1574            self.workspace_threads.get(&workspace.downgrade())
1575        else {
1576            return;
1577        };
1578
1579        let Some(thread) = thread.upgrade() else {
1580            return;
1581        };
1582
1583        AgentDiffPane::deploy(thread, workspace.downgrade(), window, cx).log_err();
1584    }
1585
1586    fn keep_all(
1587        editor: &Entity<Editor>,
1588        thread: &Entity<AcpThread>,
1589        window: &mut Window,
1590        cx: &mut App,
1591    ) -> PostReviewState {
1592        editor.update(cx, |editor, cx| {
1593            let snapshot = editor.buffer().read(cx).snapshot(cx);
1594            keep_edits_in_ranges(
1595                editor,
1596                &snapshot,
1597                thread,
1598                vec![editor::Anchor::min()..editor::Anchor::max()],
1599                window,
1600                cx,
1601            );
1602        });
1603        PostReviewState::AllReviewed
1604    }
1605
1606    fn reject_all(
1607        editor: &Entity<Editor>,
1608        thread: &Entity<AcpThread>,
1609        window: &mut Window,
1610        cx: &mut App,
1611    ) -> PostReviewState {
1612        editor.update(cx, |editor, cx| {
1613            let snapshot = editor.buffer().read(cx).snapshot(cx);
1614            reject_edits_in_ranges(
1615                editor,
1616                &snapshot,
1617                thread,
1618                vec![editor::Anchor::min()..editor::Anchor::max()],
1619                window,
1620                cx,
1621            );
1622        });
1623        PostReviewState::AllReviewed
1624    }
1625
1626    fn keep(
1627        editor: &Entity<Editor>,
1628        thread: &Entity<AcpThread>,
1629        window: &mut Window,
1630        cx: &mut App,
1631    ) -> PostReviewState {
1632        editor.update(cx, |editor, cx| {
1633            let snapshot = editor.buffer().read(cx).snapshot(cx);
1634            keep_edits_in_selection(editor, &snapshot, thread, window, cx);
1635            Self::post_review_state(&snapshot)
1636        })
1637    }
1638
1639    fn reject(
1640        editor: &Entity<Editor>,
1641        thread: &Entity<AcpThread>,
1642        window: &mut Window,
1643        cx: &mut App,
1644    ) -> PostReviewState {
1645        editor.update(cx, |editor, cx| {
1646            let snapshot = editor.buffer().read(cx).snapshot(cx);
1647            reject_edits_in_selection(editor, &snapshot, thread, window, cx);
1648            Self::post_review_state(&snapshot)
1649        })
1650    }
1651
1652    fn post_review_state(snapshot: &MultiBufferSnapshot) -> PostReviewState {
1653        for (i, _) in snapshot.diff_hunks().enumerate() {
1654            if i > 0 {
1655                return PostReviewState::Pending;
1656            }
1657        }
1658        PostReviewState::AllReviewed
1659    }
1660
1661    fn review_in_active_editor(
1662        &mut self,
1663        workspace: &mut Workspace,
1664        review: impl Fn(&Entity<Editor>, &Entity<AcpThread>, &mut Window, &mut App) -> PostReviewState,
1665        window: &mut Window,
1666        cx: &mut Context<Self>,
1667    ) -> Option<Task<Result<()>>> {
1668        let active_item = workspace.active_item(cx)?;
1669        let editor = active_item.act_as::<Editor>(cx)?;
1670
1671        if !matches!(
1672            self.editor_state(&editor.downgrade()),
1673            EditorState::Reviewing
1674        ) {
1675            return None;
1676        }
1677
1678        let WorkspaceThread { thread, .. } =
1679            self.workspace_threads.get(&workspace.weak_handle())?;
1680
1681        let thread = thread.upgrade()?;
1682
1683        if let PostReviewState::AllReviewed = review(&editor, &thread, window, cx)
1684            && let Some(curr_buffer) = editor.read(cx).buffer().read(cx).as_singleton()
1685        {
1686            let changed_buffers = thread.read(cx).action_log().read(cx).changed_buffers(cx);
1687
1688            let mut keys = changed_buffers.keys();
1689            keys.find(|k| *k == &curr_buffer);
1690            let next_project_path = keys
1691                .next()
1692                .filter(|k| *k != &curr_buffer)
1693                .and_then(|after| after.read(cx).project_path(cx));
1694
1695            if let Some(path) = next_project_path {
1696                let task = workspace.open_path(path, None, true, window, cx);
1697                let task = cx.spawn(async move |_, _cx| task.await.map(|_| ()));
1698                return Some(task);
1699            }
1700        }
1701
1702        Some(Task::ready(Ok(())))
1703    }
1704}
1705
1706enum PostReviewState {
1707    AllReviewed,
1708    Pending,
1709}
1710
1711pub struct EditorAgentDiffAddon;
1712
1713impl editor::Addon for EditorAgentDiffAddon {
1714    fn to_any(&self) -> &dyn std::any::Any {
1715        self
1716    }
1717
1718    fn extend_key_context(&self, key_context: &mut gpui::KeyContext, _: &App) {
1719        key_context.add("agent_diff");
1720        key_context.add("editor_agent_diff");
1721    }
1722}
1723
1724#[cfg(test)]
1725mod tests {
1726    use super::*;
1727    use crate::Keep;
1728    use acp_thread::AgentConnection as _;
1729    use editor::EditorSettings;
1730    use gpui::{TestAppContext, UpdateGlobal, VisualTestContext};
1731    use project::{FakeFs, Project};
1732    use serde_json::json;
1733    use settings::SettingsStore;
1734    use std::{path::Path, rc::Rc};
1735    use util::path;
1736
1737    #[gpui::test]
1738    async fn test_multibuffer_agent_diff(cx: &mut TestAppContext) {
1739        cx.update(|cx| {
1740            let settings_store = SettingsStore::test(cx);
1741            cx.set_global(settings_store);
1742            prompt_store::init(cx);
1743            theme::init(theme::LoadThemes::JustBase, cx);
1744            language_model::init_settings(cx);
1745        });
1746
1747        let fs = FakeFs::new(cx.executor());
1748        fs.insert_tree(
1749            path!("/test"),
1750            json!({"file1": "abc\ndef\nghi\njkl\nmno\npqr\nstu\nvwx\nyz"}),
1751        )
1752        .await;
1753        let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
1754        let buffer_path = project
1755            .read_with(cx, |project, cx| {
1756                project.find_project_path("test/file1", cx)
1757            })
1758            .unwrap();
1759
1760        let connection = Rc::new(acp_thread::StubAgentConnection::new());
1761        let thread = cx
1762            .update(|cx| {
1763                connection
1764                    .clone()
1765                    .new_thread(project.clone(), Path::new(path!("/test")), cx)
1766            })
1767            .await
1768            .unwrap();
1769
1770        let action_log = cx.read(|cx| thread.read(cx).action_log().clone());
1771
1772        let (workspace, cx) =
1773            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1774        let agent_diff = cx.new_window_entity(|window, cx| {
1775            AgentDiffPane::new(thread.clone(), workspace.downgrade(), window, cx)
1776        });
1777        let editor = agent_diff.read_with(cx, |diff, _cx| diff.editor.clone());
1778
1779        let buffer = project
1780            .update(cx, |project, cx| project.open_buffer(buffer_path, cx))
1781            .await
1782            .unwrap();
1783        cx.update(|_, cx| {
1784            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
1785            buffer.update(cx, |buffer, cx| {
1786                buffer
1787                    .edit(
1788                        [
1789                            (Point::new(1, 1)..Point::new(1, 2), "E"),
1790                            (Point::new(3, 2)..Point::new(3, 3), "L"),
1791                            (Point::new(5, 0)..Point::new(5, 1), "P"),
1792                            (Point::new(7, 1)..Point::new(7, 2), "W"),
1793                        ],
1794                        None,
1795                        cx,
1796                    )
1797                    .unwrap()
1798            });
1799            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1800        });
1801        cx.run_until_parked();
1802
1803        // When opening the assistant diff, the cursor is positioned on the first hunk.
1804        assert_eq!(
1805            editor.read_with(cx, |editor, cx| editor.text(cx)),
1806            "abc\ndef\ndEf\nghi\njkl\njkL\nmno\npqr\nPqr\nstu\nvwx\nvWx\nyz"
1807        );
1808        assert_eq!(
1809            editor
1810                .update(cx, |editor, cx| editor
1811                    .selections
1812                    .newest::<Point>(&editor.display_snapshot(cx)))
1813                .range(),
1814            Point::new(1, 0)..Point::new(1, 0)
1815        );
1816
1817        // After keeping a hunk, the cursor should be positioned on the second hunk.
1818        agent_diff.update_in(cx, |diff, window, cx| diff.keep(&Keep, window, cx));
1819        cx.run_until_parked();
1820        assert_eq!(
1821            editor.read_with(cx, |editor, cx| editor.text(cx)),
1822            "abc\ndEf\nghi\njkl\njkL\nmno\npqr\nPqr\nstu\nvwx\nvWx\nyz"
1823        );
1824        assert_eq!(
1825            editor
1826                .update(cx, |editor, cx| editor
1827                    .selections
1828                    .newest::<Point>(&editor.display_snapshot(cx)))
1829                .range(),
1830            Point::new(3, 0)..Point::new(3, 0)
1831        );
1832
1833        // Rejecting a hunk also moves the cursor to the next hunk, possibly cycling if it's at the end.
1834        editor.update_in(cx, |editor, window, cx| {
1835            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
1836                selections.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
1837            });
1838        });
1839        agent_diff.update_in(cx, |diff, window, cx| {
1840            diff.reject(&crate::Reject, window, cx)
1841        });
1842        cx.run_until_parked();
1843        assert_eq!(
1844            editor.read_with(cx, |editor, cx| editor.text(cx)),
1845            "abc\ndEf\nghi\njkl\njkL\nmno\npqr\nPqr\nstu\nvwx\nyz"
1846        );
1847        assert_eq!(
1848            editor
1849                .update(cx, |editor, cx| editor
1850                    .selections
1851                    .newest::<Point>(&editor.display_snapshot(cx)))
1852                .range(),
1853            Point::new(3, 0)..Point::new(3, 0)
1854        );
1855
1856        // Keeping a range that doesn't intersect the current selection doesn't move it.
1857        agent_diff.update_in(cx, |_diff, window, cx| {
1858            let position = editor
1859                .read(cx)
1860                .buffer()
1861                .read(cx)
1862                .read(cx)
1863                .anchor_before(Point::new(7, 0));
1864            editor.update(cx, |editor, cx| {
1865                let snapshot = editor.buffer().read(cx).snapshot(cx);
1866                keep_edits_in_ranges(
1867                    editor,
1868                    &snapshot,
1869                    &thread,
1870                    vec![position..position],
1871                    window,
1872                    cx,
1873                )
1874            });
1875        });
1876        cx.run_until_parked();
1877        assert_eq!(
1878            editor.read_with(cx, |editor, cx| editor.text(cx)),
1879            "abc\ndEf\nghi\njkl\njkL\nmno\nPqr\nstu\nvwx\nyz"
1880        );
1881        assert_eq!(
1882            editor
1883                .update(cx, |editor, cx| editor
1884                    .selections
1885                    .newest::<Point>(&editor.display_snapshot(cx)))
1886                .range(),
1887            Point::new(3, 0)..Point::new(3, 0)
1888        );
1889    }
1890
1891    // This test won't work with AgentV2FeatureFlag, and as it's not feasible
1892    // to disable a feature flag for tests and this agent diff code is likely
1893    // going away soon, let's ignore the test for now.
1894    #[ignore]
1895    #[gpui::test]
1896    async fn test_singleton_agent_diff(cx: &mut TestAppContext) {
1897        cx.update(|cx| {
1898            let settings_store = SettingsStore::test(cx);
1899            cx.set_global(settings_store);
1900            prompt_store::init(cx);
1901            theme::init(theme::LoadThemes::JustBase, cx);
1902            language_model::init_settings(cx);
1903            workspace::register_project_item::<Editor>(cx);
1904        });
1905
1906        let fs = FakeFs::new(cx.executor());
1907        fs.insert_tree(
1908            path!("/test"),
1909            json!({"file1": "abc\ndef\nghi\njkl\nmno\npqr\nstu\nvwx\nyz"}),
1910        )
1911        .await;
1912        fs.insert_tree(path!("/test"), json!({"file2": "abc\ndef\nghi"}))
1913            .await;
1914
1915        let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
1916        let buffer_path1 = project
1917            .read_with(cx, |project, cx| {
1918                project.find_project_path("test/file1", cx)
1919            })
1920            .unwrap();
1921        let buffer_path2 = project
1922            .read_with(cx, |project, cx| {
1923                project.find_project_path("test/file2", cx)
1924            })
1925            .unwrap();
1926
1927        let (workspace, cx) =
1928            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1929
1930        // Add the diff toolbar to the active pane
1931        let diff_toolbar = cx.new_window_entity(|_, cx| AgentDiffToolbar::new(cx));
1932
1933        workspace.update_in(cx, {
1934            let diff_toolbar = diff_toolbar.clone();
1935
1936            move |workspace, window, cx| {
1937                workspace.active_pane().update(cx, |pane, cx| {
1938                    pane.toolbar().update(cx, |toolbar, cx| {
1939                        toolbar.add_item(diff_toolbar, window, cx);
1940                    });
1941                })
1942            }
1943        });
1944
1945        let connection = Rc::new(acp_thread::StubAgentConnection::new());
1946        let thread = cx
1947            .update(|_, cx| {
1948                connection
1949                    .clone()
1950                    .new_thread(project.clone(), Path::new(path!("/test")), cx)
1951            })
1952            .await
1953            .unwrap();
1954        let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
1955
1956        // Set the active thread
1957        cx.update(|window, cx| {
1958            AgentDiff::set_active_thread(&workspace.downgrade(), thread.clone(), window, cx)
1959        });
1960
1961        let buffer1 = project
1962            .update(cx, |project, cx| {
1963                project.open_buffer(buffer_path1.clone(), cx)
1964            })
1965            .await
1966            .unwrap();
1967        let buffer2 = project
1968            .update(cx, |project, cx| {
1969                project.open_buffer(buffer_path2.clone(), cx)
1970            })
1971            .await
1972            .unwrap();
1973
1974        // Open an editor for buffer1
1975        let editor1 = cx.new_window_entity(|window, cx| {
1976            Editor::for_buffer(buffer1.clone(), Some(project.clone()), window, cx)
1977        });
1978
1979        workspace.update_in(cx, |workspace, window, cx| {
1980            workspace.add_item_to_active_pane(Box::new(editor1.clone()), None, true, window, cx);
1981        });
1982        cx.run_until_parked();
1983
1984        // Toolbar knows about the current editor, but it's hidden since there are no changes yet
1985        assert!(diff_toolbar.read_with(cx, |toolbar, _cx| matches!(
1986            toolbar.active_item,
1987            Some(AgentDiffToolbarItem::Editor {
1988                state: EditorState::Idle,
1989                ..
1990            })
1991        )));
1992        assert_eq!(
1993            diff_toolbar.read_with(cx, |toolbar, cx| toolbar.location(cx)),
1994            ToolbarItemLocation::Hidden
1995        );
1996
1997        // Make changes
1998        cx.update(|_, cx| {
1999            action_log.update(cx, |log, cx| log.buffer_read(buffer1.clone(), cx));
2000            buffer1.update(cx, |buffer, cx| {
2001                buffer
2002                    .edit(
2003                        [
2004                            (Point::new(1, 1)..Point::new(1, 2), "E"),
2005                            (Point::new(3, 2)..Point::new(3, 3), "L"),
2006                            (Point::new(5, 0)..Point::new(5, 1), "P"),
2007                            (Point::new(7, 1)..Point::new(7, 2), "W"),
2008                        ],
2009                        None,
2010                        cx,
2011                    )
2012                    .unwrap()
2013            });
2014            action_log.update(cx, |log, cx| log.buffer_edited(buffer1.clone(), cx));
2015
2016            action_log.update(cx, |log, cx| log.buffer_read(buffer2.clone(), cx));
2017            buffer2.update(cx, |buffer, cx| {
2018                buffer
2019                    .edit(
2020                        [
2021                            (Point::new(0, 0)..Point::new(0, 1), "A"),
2022                            (Point::new(2, 1)..Point::new(2, 2), "H"),
2023                        ],
2024                        None,
2025                        cx,
2026                    )
2027                    .unwrap();
2028            });
2029            action_log.update(cx, |log, cx| log.buffer_edited(buffer2.clone(), cx));
2030        });
2031        cx.run_until_parked();
2032
2033        // The already opened editor displays the diff and the cursor is at the first hunk
2034        assert_eq!(
2035            editor1.read_with(cx, |editor, cx| editor.text(cx)),
2036            "abc\ndef\ndEf\nghi\njkl\njkL\nmno\npqr\nPqr\nstu\nvwx\nvWx\nyz"
2037        );
2038        assert_eq!(
2039            editor1
2040                .update(cx, |editor, cx| editor
2041                    .selections
2042                    .newest::<Point>(&editor.display_snapshot(cx)))
2043                .range(),
2044            Point::new(1, 0)..Point::new(1, 0)
2045        );
2046
2047        // The toolbar is displayed in the right state
2048        assert_eq!(
2049            diff_toolbar.read_with(cx, |toolbar, cx| toolbar.location(cx)),
2050            ToolbarItemLocation::PrimaryRight
2051        );
2052        assert!(diff_toolbar.read_with(cx, |toolbar, _cx| matches!(
2053            toolbar.active_item,
2054            Some(AgentDiffToolbarItem::Editor {
2055                state: EditorState::Reviewing,
2056                ..
2057            })
2058        )));
2059
2060        // The toolbar respects its setting
2061        override_toolbar_agent_review_setting(false, cx);
2062        assert_eq!(
2063            diff_toolbar.read_with(cx, |toolbar, cx| toolbar.location(cx)),
2064            ToolbarItemLocation::Hidden
2065        );
2066        override_toolbar_agent_review_setting(true, cx);
2067        assert_eq!(
2068            diff_toolbar.read_with(cx, |toolbar, cx| toolbar.location(cx)),
2069            ToolbarItemLocation::PrimaryRight
2070        );
2071
2072        // After keeping a hunk, the cursor should be positioned on the second hunk.
2073        workspace.update(cx, |_, cx| {
2074            cx.dispatch_action(&Keep);
2075        });
2076        cx.run_until_parked();
2077        assert_eq!(
2078            editor1.read_with(cx, |editor, cx| editor.text(cx)),
2079            "abc\ndEf\nghi\njkl\njkL\nmno\npqr\nPqr\nstu\nvwx\nvWx\nyz"
2080        );
2081        assert_eq!(
2082            editor1
2083                .update(cx, |editor, cx| editor
2084                    .selections
2085                    .newest::<Point>(&editor.display_snapshot(cx)))
2086                .range(),
2087            Point::new(3, 0)..Point::new(3, 0)
2088        );
2089
2090        // Rejecting a hunk also moves the cursor to the next hunk, possibly cycling if it's at the end.
2091        editor1.update_in(cx, |editor, window, cx| {
2092            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
2093                selections.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
2094            });
2095        });
2096        workspace.update(cx, |_, cx| {
2097            cx.dispatch_action(&Reject);
2098        });
2099        cx.run_until_parked();
2100        assert_eq!(
2101            editor1.read_with(cx, |editor, cx| editor.text(cx)),
2102            "abc\ndEf\nghi\njkl\njkL\nmno\npqr\nPqr\nstu\nvwx\nyz"
2103        );
2104        assert_eq!(
2105            editor1
2106                .update(cx, |editor, cx| editor
2107                    .selections
2108                    .newest::<Point>(&editor.display_snapshot(cx)))
2109                .range(),
2110            Point::new(3, 0)..Point::new(3, 0)
2111        );
2112
2113        // Keeping a range that doesn't intersect the current selection doesn't move it.
2114        editor1.update_in(cx, |editor, window, cx| {
2115            let buffer = editor.buffer().read(cx);
2116            let position = buffer.read(cx).anchor_before(Point::new(7, 0));
2117            let snapshot = buffer.snapshot(cx);
2118            keep_edits_in_ranges(
2119                editor,
2120                &snapshot,
2121                &thread,
2122                vec![position..position],
2123                window,
2124                cx,
2125            )
2126        });
2127        cx.run_until_parked();
2128        assert_eq!(
2129            editor1.read_with(cx, |editor, cx| editor.text(cx)),
2130            "abc\ndEf\nghi\njkl\njkL\nmno\nPqr\nstu\nvwx\nyz"
2131        );
2132        assert_eq!(
2133            editor1
2134                .update(cx, |editor, cx| editor
2135                    .selections
2136                    .newest::<Point>(&editor.display_snapshot(cx)))
2137                .range(),
2138            Point::new(3, 0)..Point::new(3, 0)
2139        );
2140
2141        // Reviewing the last change opens the next changed buffer
2142        workspace
2143            .update_in(cx, |workspace, window, cx| {
2144                AgentDiff::global(cx).update(cx, |agent_diff, cx| {
2145                    agent_diff.review_in_active_editor(workspace, AgentDiff::keep, window, cx)
2146                })
2147            })
2148            .unwrap()
2149            .await
2150            .unwrap();
2151
2152        cx.run_until_parked();
2153
2154        let editor2 = workspace.update(cx, |workspace, cx| {
2155            workspace.active_item_as::<Editor>(cx).unwrap()
2156        });
2157
2158        let editor2_path = editor2
2159            .read_with(cx, |editor, cx| editor.project_path(cx))
2160            .unwrap();
2161        assert_eq!(editor2_path, buffer_path2);
2162
2163        assert_eq!(
2164            editor2.read_with(cx, |editor, cx| editor.text(cx)),
2165            "abc\nAbc\ndef\nghi\ngHi"
2166        );
2167        assert_eq!(
2168            editor2
2169                .update(cx, |editor, cx| editor
2170                    .selections
2171                    .newest::<Point>(&editor.display_snapshot(cx)))
2172                .range(),
2173            Point::new(0, 0)..Point::new(0, 0)
2174        );
2175
2176        // Editor 1 toolbar is hidden since all changes have been reviewed
2177        workspace.update_in(cx, |workspace, window, cx| {
2178            workspace.activate_item(&editor1, true, true, window, cx)
2179        });
2180
2181        assert!(diff_toolbar.read_with(cx, |toolbar, _cx| matches!(
2182            toolbar.active_item,
2183            Some(AgentDiffToolbarItem::Editor {
2184                state: EditorState::Idle,
2185                ..
2186            })
2187        )));
2188        assert_eq!(
2189            diff_toolbar.read_with(cx, |toolbar, cx| toolbar.location(cx)),
2190            ToolbarItemLocation::Hidden
2191        );
2192    }
2193
2194    fn override_toolbar_agent_review_setting(active: bool, cx: &mut VisualTestContext) {
2195        cx.update(|_window, cx| {
2196            SettingsStore::update_global(cx, |store, _cx| {
2197                let mut editor_settings = store.get::<EditorSettings>(None).clone();
2198                editor_settings.toolbar.agent_review = active;
2199                store.override_global(editor_settings);
2200            })
2201        });
2202        cx.run_until_parked();
2203    }
2204}