thread_view.rs

   1use acp_thread::{
   2    AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk,
   3    LoadError, MentionUri, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus, UserMessageId,
   4};
   5use acp_thread::{AgentConnection, Plan};
   6use action_log::ActionLog;
   7use agent_client_protocol as acp;
   8use agent_servers::AgentServer;
   9use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
  10use audio::{Audio, Sound};
  11use buffer_diff::BufferDiff;
  12use collections::{HashMap, HashSet};
  13use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};
  14use file_icons::FileIcons;
  15use gpui::{
  16    Action, Animation, AnimationExt, App, BorderStyle, ClickEvent, EdgesRefinement, Empty, Entity,
  17    EntityId, FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton,
  18    PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle,
  19    TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div,
  20    linear_color_stop, linear_gradient, list, percentage, point, prelude::*, pulsating_between,
  21};
  22use language::Buffer;
  23use language::language_settings::SoftWrap;
  24use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
  25use project::Project;
  26use rope::Point;
  27use settings::{Settings as _, SettingsStore};
  28use std::{collections::BTreeMap, process::ExitStatus, rc::Rc, time::Duration};
  29use terminal_view::TerminalView;
  30use text::Anchor;
  31use theme::ThemeSettings;
  32use ui::{
  33    Disclosure, Divider, DividerColor, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState,
  34    Tooltip, prelude::*,
  35};
  36use util::{ResultExt, size::format_file_size, time::duration_alt_display};
  37use workspace::{CollaboratorId, Workspace};
  38use zed_actions::agent::{Chat, ToggleModelSelector};
  39
  40use crate::acp::AcpModelSelectorPopover;
  41use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
  42use crate::agent_diff::AgentDiff;
  43use crate::ui::{AgentNotification, AgentNotificationEvent};
  44use crate::{
  45    AgentDiffPane, AgentPanel, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll,
  46};
  47
  48const RESPONSE_PADDING_X: Pixels = px(19.);
  49
  50pub const MIN_EDITOR_LINES: usize = 4;
  51pub const MAX_EDITOR_LINES: usize = 8;
  52
  53pub struct AcpThreadView {
  54    agent: Rc<dyn AgentServer>,
  55    workspace: WeakEntity<Workspace>,
  56    project: Entity<Project>,
  57    thread_state: ThreadState,
  58    diff_editors: HashMap<EntityId, Entity<Editor>>,
  59    terminal_views: HashMap<EntityId, Entity<TerminalView>>,
  60    message_editor: Entity<MessageEditor>,
  61    model_selector: Option<Entity<AcpModelSelectorPopover>>,
  62    notifications: Vec<WindowHandle<AgentNotification>>,
  63    notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
  64    last_error: Option<Entity<Markdown>>,
  65    list_state: ListState,
  66    scrollbar_state: ScrollbarState,
  67    auth_task: Option<Task<()>>,
  68    expanded_tool_calls: HashSet<acp::ToolCallId>,
  69    expanded_thinking_blocks: HashSet<(usize, usize)>,
  70    edits_expanded: bool,
  71    plan_expanded: bool,
  72    editor_expanded: bool,
  73    terminal_expanded: bool,
  74    editing_message: Option<EditingMessage>,
  75    _cancel_task: Option<Task<()>>,
  76    _subscriptions: [Subscription; 2],
  77}
  78
  79struct EditingMessage {
  80    message_id: UserMessageId,
  81    editor: Entity<MessageEditor>,
  82    _subscription: Subscription,
  83}
  84
  85enum ThreadState {
  86    Loading {
  87        _task: Task<()>,
  88    },
  89    Ready {
  90        thread: Entity<AcpThread>,
  91        _subscription: [Subscription; 2],
  92    },
  93    LoadError(LoadError),
  94    Unauthenticated {
  95        connection: Rc<dyn AgentConnection>,
  96    },
  97    ServerExited {
  98        status: ExitStatus,
  99    },
 100}
 101
 102impl AcpThreadView {
 103    pub fn new(
 104        agent: Rc<dyn AgentServer>,
 105        workspace: WeakEntity<Workspace>,
 106        project: Entity<Project>,
 107        window: &mut Window,
 108        cx: &mut Context<Self>,
 109    ) -> Self {
 110        let message_editor = cx.new(|cx| {
 111            MessageEditor::new(
 112                workspace.clone(),
 113                project.clone(),
 114                editor::EditorMode::AutoHeight {
 115                    min_lines: MIN_EDITOR_LINES,
 116                    max_lines: Some(MAX_EDITOR_LINES),
 117                },
 118                window,
 119                cx,
 120            )
 121        });
 122
 123        let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0));
 124
 125        let subscriptions = [
 126            cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
 127            cx.subscribe_in(&message_editor, window, Self::on_message_editor_event),
 128        ];
 129
 130        Self {
 131            agent: agent.clone(),
 132            workspace: workspace.clone(),
 133            project: project.clone(),
 134            thread_state: Self::initial_state(agent, workspace, project, window, cx),
 135            message_editor,
 136            model_selector: None,
 137            notifications: Vec::new(),
 138            notification_subscriptions: HashMap::default(),
 139            diff_editors: Default::default(),
 140            terminal_views: Default::default(),
 141            list_state: list_state.clone(),
 142            scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()),
 143            last_error: None,
 144            auth_task: None,
 145            expanded_tool_calls: HashSet::default(),
 146            expanded_thinking_blocks: HashSet::default(),
 147            editing_message: None,
 148            edits_expanded: false,
 149            plan_expanded: false,
 150            editor_expanded: false,
 151            terminal_expanded: true,
 152            _subscriptions: subscriptions,
 153            _cancel_task: None,
 154        }
 155    }
 156
 157    fn initial_state(
 158        agent: Rc<dyn AgentServer>,
 159        workspace: WeakEntity<Workspace>,
 160        project: Entity<Project>,
 161        window: &mut Window,
 162        cx: &mut Context<Self>,
 163    ) -> ThreadState {
 164        let root_dir = project
 165            .read(cx)
 166            .visible_worktrees(cx)
 167            .next()
 168            .map(|worktree| worktree.read(cx).abs_path())
 169            .unwrap_or_else(|| paths::home_dir().as_path().into());
 170
 171        let connect_task = agent.connect(&root_dir, &project, cx);
 172        let load_task = cx.spawn_in(window, async move |this, cx| {
 173            let connection = match connect_task.await {
 174                Ok(connection) => connection,
 175                Err(err) => {
 176                    this.update(cx, |this, cx| {
 177                        this.handle_load_error(err, cx);
 178                        cx.notify();
 179                    })
 180                    .log_err();
 181                    return;
 182                }
 183            };
 184
 185            // this.update_in(cx, |_this, _window, cx| {
 186            //     let status = connection.exit_status(cx);
 187            //     cx.spawn(async move |this, cx| {
 188            //         let status = status.await.ok();
 189            //         this.update(cx, |this, cx| {
 190            //             this.thread_state = ThreadState::ServerExited { status };
 191            //             cx.notify();
 192            //         })
 193            //         .ok();
 194            //     })
 195            //     .detach();
 196            // })
 197            // .ok();
 198
 199            let result = match connection
 200                .clone()
 201                .new_thread(project.clone(), &root_dir, cx)
 202                .await
 203            {
 204                Err(e) => {
 205                    let mut cx = cx.clone();
 206                    if e.is::<acp_thread::AuthRequired>() {
 207                        this.update(&mut cx, |this, cx| {
 208                            this.thread_state = ThreadState::Unauthenticated { connection };
 209                            cx.notify();
 210                        })
 211                        .ok();
 212                        return;
 213                    } else {
 214                        Err(e)
 215                    }
 216                }
 217                Ok(thread) => Ok(thread),
 218            };
 219
 220            this.update_in(cx, |this, window, cx| {
 221                match result {
 222                    Ok(thread) => {
 223                        let thread_subscription =
 224                            cx.subscribe_in(&thread, window, Self::handle_thread_event);
 225
 226                        let action_log = thread.read(cx).action_log().clone();
 227                        let action_log_subscription =
 228                            cx.observe(&action_log, |_, _, cx| cx.notify());
 229
 230                        this.list_state
 231                            .splice(0..0, thread.read(cx).entries().len());
 232
 233                        AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
 234
 235                        this.model_selector =
 236                            thread
 237                                .read(cx)
 238                                .connection()
 239                                .model_selector()
 240                                .map(|selector| {
 241                                    cx.new(|cx| {
 242                                        AcpModelSelectorPopover::new(
 243                                            thread.read(cx).session_id().clone(),
 244                                            selector,
 245                                            PopoverMenuHandle::default(),
 246                                            this.focus_handle(cx),
 247                                            window,
 248                                            cx,
 249                                        )
 250                                    })
 251                                });
 252
 253                        this.thread_state = ThreadState::Ready {
 254                            thread,
 255                            _subscription: [thread_subscription, action_log_subscription],
 256                        };
 257
 258                        cx.notify();
 259                    }
 260                    Err(err) => {
 261                        this.handle_load_error(err, cx);
 262                    }
 263                };
 264            })
 265            .log_err();
 266        });
 267
 268        ThreadState::Loading { _task: load_task }
 269    }
 270
 271    fn handle_load_error(&mut self, err: anyhow::Error, cx: &mut Context<Self>) {
 272        if let Some(load_err) = err.downcast_ref::<LoadError>() {
 273            self.thread_state = ThreadState::LoadError(load_err.clone());
 274        } else {
 275            self.thread_state = ThreadState::LoadError(LoadError::Other(err.to_string().into()))
 276        }
 277        cx.notify();
 278    }
 279
 280    pub fn thread(&self) -> Option<&Entity<AcpThread>> {
 281        match &self.thread_state {
 282            ThreadState::Ready { thread, .. } => Some(thread),
 283            ThreadState::Unauthenticated { .. }
 284            | ThreadState::Loading { .. }
 285            | ThreadState::LoadError(..)
 286            | ThreadState::ServerExited { .. } => None,
 287        }
 288    }
 289
 290    pub fn title(&self, cx: &App) -> SharedString {
 291        match &self.thread_state {
 292            ThreadState::Ready { thread, .. } => thread.read(cx).title(),
 293            ThreadState::Loading { .. } => "Loading…".into(),
 294            ThreadState::LoadError(_) => "Failed to load".into(),
 295            ThreadState::Unauthenticated { .. } => "Not authenticated".into(),
 296            ThreadState::ServerExited { .. } => "Server exited unexpectedly".into(),
 297        }
 298    }
 299
 300    pub fn cancel_generation(&mut self, cx: &mut Context<Self>) {
 301        self.last_error.take();
 302
 303        if let Some(thread) = self.thread() {
 304            self._cancel_task = Some(thread.update(cx, |thread, cx| thread.cancel(cx)));
 305        }
 306    }
 307
 308    pub fn expand_message_editor(
 309        &mut self,
 310        _: &ExpandMessageEditor,
 311        _window: &mut Window,
 312        cx: &mut Context<Self>,
 313    ) {
 314        self.set_editor_is_expanded(!self.editor_expanded, cx);
 315        cx.notify();
 316    }
 317
 318    fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {
 319        self.editor_expanded = is_expanded;
 320        self.message_editor.update(cx, |editor, cx| {
 321            if is_expanded {
 322                editor.set_mode(
 323                    EditorMode::Full {
 324                        scale_ui_elements_with_buffer_font_size: false,
 325                        show_active_line_background: false,
 326                        sized_by_content: false,
 327                    },
 328                    cx,
 329                )
 330            } else {
 331                editor.set_mode(
 332                    EditorMode::AutoHeight {
 333                        min_lines: MIN_EDITOR_LINES,
 334                        max_lines: Some(MAX_EDITOR_LINES),
 335                    },
 336                    cx,
 337                )
 338            }
 339        });
 340        cx.notify();
 341    }
 342
 343    pub fn on_message_editor_event(
 344        &mut self,
 345        _: &Entity<MessageEditor>,
 346        event: &MessageEditorEvent,
 347        window: &mut Window,
 348        cx: &mut Context<Self>,
 349    ) {
 350        match event {
 351            MessageEditorEvent::Send => self.send(window, cx),
 352            MessageEditorEvent::Cancel => self.cancel_generation(cx),
 353        }
 354    }
 355
 356    fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 357        self.last_error.take();
 358
 359        let Some(thread) = self.thread().cloned() else {
 360            return;
 361        };
 362
 363        let contents = self
 364            .message_editor
 365            .update(cx, |message_editor, cx| message_editor.contents(cx));
 366        let task = cx.spawn_in(window, async move |this, cx| {
 367            let contents = contents.await?;
 368
 369            if contents.is_empty() {
 370                return Ok(());
 371            }
 372
 373            this.update_in(cx, |this, window, cx| {
 374                this.set_editor_is_expanded(false, cx);
 375                this.scroll_to_bottom(cx);
 376                this.message_editor.update(cx, |message_editor, cx| {
 377                    message_editor.clear(window, cx);
 378                });
 379            })?;
 380            let send = thread.update(cx, |thread, cx| thread.send(contents, cx))?;
 381            send.await
 382        });
 383
 384        cx.spawn(async move |this, cx| {
 385            if let Err(e) = task.await {
 386                this.update(cx, |this, cx| {
 387                    this.last_error =
 388                        Some(cx.new(|cx| Markdown::new(e.to_string().into(), None, None, cx)));
 389                    cx.notify()
 390                })
 391                .ok();
 392            }
 393        })
 394        .detach();
 395    }
 396
 397    fn send_impl(
 398        &mut self,
 399        contents: Task<anyhow::Result<Vec<acp::ContentBlock>>>,
 400        window: &mut Window,
 401        cx: &mut Context<Self>,
 402    ) {
 403        let Some(thread) = self.thread().cloned() else {
 404            return;
 405        };
 406        let task = cx.spawn_in(window, async move |this, cx| {
 407            let contents = contents.await?;
 408
 409            if contents.is_empty() {
 410                return Ok(());
 411            }
 412
 413            this.update_in(cx, |this, window, cx| {
 414                this.set_editor_is_expanded(false, cx);
 415                this.scroll_to_bottom(cx);
 416                this.message_editor.update(cx, |message_editor, cx| {
 417                    message_editor.clear(window, cx);
 418                });
 419            })?;
 420            let send = thread.update(cx, |thread, cx| thread.send(contents, cx))?;
 421            send.await
 422        });
 423
 424        cx.spawn(async move |this, cx| {
 425            if let Err(e) = task.await {
 426                this.update(cx, |this, cx| {
 427                    this.last_error =
 428                        Some(cx.new(|cx| Markdown::new(e.to_string().into(), None, None, cx)));
 429                    cx.notify()
 430                })
 431                .ok();
 432            }
 433        })
 434        .detach();
 435    }
 436
 437    fn cancel_editing(&mut self, _: &ClickEvent, _window: &mut Window, cx: &mut Context<Self>) {
 438        self.editing_message.take();
 439        cx.notify();
 440    }
 441
 442    fn regenerate(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
 443        let Some(editing_message) = self.editing_message.take() else {
 444            return;
 445        };
 446
 447        self.last_error.take();
 448
 449        let Some(thread) = self.thread().cloned() else {
 450            return;
 451        };
 452
 453        let rewind = thread.update(cx, |thread, cx| {
 454            thread.rewind(editing_message.message_id, cx)
 455        });
 456
 457        let contents = editing_message
 458            .editor
 459            .update(cx, |message_editor, cx| message_editor.contents(cx));
 460        let task = cx.foreground_executor().spawn(async move {
 461            rewind.await?;
 462            contents.await
 463        });
 464        self.send_impl(task, window, cx);
 465    }
 466
 467    fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) {
 468        if let Some(thread) = self.thread() {
 469            AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err();
 470        }
 471    }
 472
 473    fn open_edited_buffer(
 474        &mut self,
 475        buffer: &Entity<Buffer>,
 476        window: &mut Window,
 477        cx: &mut Context<Self>,
 478    ) {
 479        let Some(thread) = self.thread() else {
 480            return;
 481        };
 482
 483        let Some(diff) =
 484            AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err()
 485        else {
 486            return;
 487        };
 488
 489        diff.update(cx, |diff, cx| {
 490            diff.move_to_path(PathKey::for_buffer(&buffer, cx), window, cx)
 491        })
 492    }
 493
 494    fn handle_thread_event(
 495        &mut self,
 496        thread: &Entity<AcpThread>,
 497        event: &AcpThreadEvent,
 498        window: &mut Window,
 499        cx: &mut Context<Self>,
 500    ) {
 501        match event {
 502            AcpThreadEvent::NewEntry => {
 503                let index = thread.read(cx).entries().len() - 1;
 504                self.sync_thread_entry_view(index, window, cx);
 505                self.list_state.splice(index..index, 1);
 506            }
 507            AcpThreadEvent::EntryUpdated(index) => {
 508                self.sync_thread_entry_view(*index, window, cx);
 509                self.list_state.splice(*index..index + 1, 1);
 510            }
 511            AcpThreadEvent::EntriesRemoved(range) => {
 512                // TODO: Clean up unused diff editors and terminal views
 513                self.list_state.splice(range.clone(), 0);
 514            }
 515            AcpThreadEvent::ToolAuthorizationRequired => {
 516                self.notify_with_sound("Waiting for tool confirmation", IconName::Info, window, cx);
 517            }
 518            AcpThreadEvent::Stopped => {
 519                let used_tools = thread.read(cx).used_tools_since_last_user_message();
 520                self.notify_with_sound(
 521                    if used_tools {
 522                        "Finished running tools"
 523                    } else {
 524                        "New message"
 525                    },
 526                    IconName::ZedAssistant,
 527                    window,
 528                    cx,
 529                );
 530            }
 531            AcpThreadEvent::Error => {
 532                self.notify_with_sound(
 533                    "Agent stopped due to an error",
 534                    IconName::Warning,
 535                    window,
 536                    cx,
 537                );
 538            }
 539            AcpThreadEvent::ServerExited(status) => {
 540                self.thread_state = ThreadState::ServerExited { status: *status };
 541            }
 542        }
 543        cx.notify();
 544    }
 545
 546    fn sync_thread_entry_view(
 547        &mut self,
 548        entry_ix: usize,
 549        window: &mut Window,
 550        cx: &mut Context<Self>,
 551    ) {
 552        self.sync_diff_multibuffers(entry_ix, window, cx);
 553        self.sync_terminals(entry_ix, window, cx);
 554    }
 555
 556    fn sync_diff_multibuffers(
 557        &mut self,
 558        entry_ix: usize,
 559        window: &mut Window,
 560        cx: &mut Context<Self>,
 561    ) {
 562        let Some(multibuffers) = self.entry_diff_multibuffers(entry_ix, cx) else {
 563            return;
 564        };
 565
 566        let multibuffers = multibuffers.collect::<Vec<_>>();
 567
 568        for multibuffer in multibuffers {
 569            if self.diff_editors.contains_key(&multibuffer.entity_id()) {
 570                return;
 571            }
 572
 573            let editor = cx.new(|cx| {
 574                let mut editor = Editor::new(
 575                    EditorMode::Full {
 576                        scale_ui_elements_with_buffer_font_size: false,
 577                        show_active_line_background: false,
 578                        sized_by_content: true,
 579                    },
 580                    multibuffer.clone(),
 581                    None,
 582                    window,
 583                    cx,
 584                );
 585                editor.set_show_gutter(false, cx);
 586                editor.disable_inline_diagnostics();
 587                editor.disable_expand_excerpt_buttons(cx);
 588                editor.set_show_vertical_scrollbar(false, cx);
 589                editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
 590                editor.set_soft_wrap_mode(SoftWrap::None, cx);
 591                editor.scroll_manager.set_forbid_vertical_scroll(true);
 592                editor.set_show_indent_guides(false, cx);
 593                editor.set_read_only(true);
 594                editor.set_show_breakpoints(false, cx);
 595                editor.set_show_code_actions(false, cx);
 596                editor.set_show_git_diff_gutter(false, cx);
 597                editor.set_expand_all_diff_hunks(cx);
 598                editor.set_text_style_refinement(diff_editor_text_style_refinement(cx));
 599                editor
 600            });
 601            let entity_id = multibuffer.entity_id();
 602            cx.observe_release(&multibuffer, move |this, _, _| {
 603                this.diff_editors.remove(&entity_id);
 604            })
 605            .detach();
 606
 607            self.diff_editors.insert(entity_id, editor);
 608        }
 609    }
 610
 611    fn entry_diff_multibuffers(
 612        &self,
 613        entry_ix: usize,
 614        cx: &App,
 615    ) -> Option<impl Iterator<Item = Entity<MultiBuffer>>> {
 616        let entry = self.thread()?.read(cx).entries().get(entry_ix)?;
 617        Some(
 618            entry
 619                .diffs()
 620                .map(|diff| diff.read(cx).multibuffer().clone()),
 621        )
 622    }
 623
 624    fn sync_terminals(&mut self, entry_ix: usize, window: &mut Window, cx: &mut Context<Self>) {
 625        let Some(terminals) = self.entry_terminals(entry_ix, cx) else {
 626            return;
 627        };
 628
 629        let terminals = terminals.collect::<Vec<_>>();
 630
 631        for terminal in terminals {
 632            if self.terminal_views.contains_key(&terminal.entity_id()) {
 633                return;
 634            }
 635
 636            let terminal_view = cx.new(|cx| {
 637                let mut view = TerminalView::new(
 638                    terminal.read(cx).inner().clone(),
 639                    self.workspace.clone(),
 640                    None,
 641                    self.project.downgrade(),
 642                    window,
 643                    cx,
 644                );
 645                view.set_embedded_mode(Some(1000), cx);
 646                view
 647            });
 648
 649            let entity_id = terminal.entity_id();
 650            cx.observe_release(&terminal, move |this, _, _| {
 651                this.terminal_views.remove(&entity_id);
 652            })
 653            .detach();
 654
 655            self.terminal_views.insert(entity_id, terminal_view);
 656        }
 657    }
 658
 659    fn entry_terminals(
 660        &self,
 661        entry_ix: usize,
 662        cx: &App,
 663    ) -> Option<impl Iterator<Item = Entity<acp_thread::Terminal>>> {
 664        let entry = self.thread()?.read(cx).entries().get(entry_ix)?;
 665        Some(entry.terminals().map(|terminal| terminal.clone()))
 666    }
 667
 668    fn authenticate(
 669        &mut self,
 670        method: acp::AuthMethodId,
 671        window: &mut Window,
 672        cx: &mut Context<Self>,
 673    ) {
 674        let ThreadState::Unauthenticated { ref connection } = self.thread_state else {
 675            return;
 676        };
 677
 678        self.last_error.take();
 679        let authenticate = connection.authenticate(method, cx);
 680        self.auth_task = Some(cx.spawn_in(window, {
 681            let project = self.project.clone();
 682            let agent = self.agent.clone();
 683            async move |this, cx| {
 684                let result = authenticate.await;
 685
 686                this.update_in(cx, |this, window, cx| {
 687                    if let Err(err) = result {
 688                        this.last_error = Some(cx.new(|cx| {
 689                            Markdown::new(format!("Error: {err}").into(), None, None, cx)
 690                        }))
 691                    } else {
 692                        this.thread_state = Self::initial_state(
 693                            agent,
 694                            this.workspace.clone(),
 695                            project.clone(),
 696                            window,
 697                            cx,
 698                        )
 699                    }
 700                    this.auth_task.take()
 701                })
 702                .ok();
 703            }
 704        }));
 705    }
 706
 707    fn authorize_tool_call(
 708        &mut self,
 709        tool_call_id: acp::ToolCallId,
 710        option_id: acp::PermissionOptionId,
 711        option_kind: acp::PermissionOptionKind,
 712        cx: &mut Context<Self>,
 713    ) {
 714        let Some(thread) = self.thread() else {
 715            return;
 716        };
 717        thread.update(cx, |thread, cx| {
 718            thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx);
 719        });
 720        cx.notify();
 721    }
 722
 723    fn rewind(&mut self, message_id: &UserMessageId, cx: &mut Context<Self>) {
 724        let Some(thread) = self.thread() else {
 725            return;
 726        };
 727        thread
 728            .update(cx, |thread, cx| thread.rewind(message_id.clone(), cx))
 729            .detach_and_log_err(cx);
 730        cx.notify();
 731    }
 732
 733    fn render_entry(
 734        &self,
 735        index: usize,
 736        total_entries: usize,
 737        entry: &AgentThreadEntry,
 738        window: &mut Window,
 739        cx: &Context<Self>,
 740    ) -> AnyElement {
 741        let primary = match &entry {
 742            AgentThreadEntry::UserMessage(message) => div()
 743                .id(("user_message", index))
 744                .py_4()
 745                .px_2()
 746                .children(message.id.clone().and_then(|message_id| {
 747                    message.checkpoint.as_ref()?;
 748
 749                    Some(
 750                        Button::new("restore-checkpoint", "Restore Checkpoint")
 751                            .icon(IconName::Undo)
 752                            .icon_size(IconSize::XSmall)
 753                            .icon_position(IconPosition::Start)
 754                            .label_size(LabelSize::XSmall)
 755                            .on_click(cx.listener(move |this, _, _window, cx| {
 756                                this.rewind(&message_id, cx);
 757                            })),
 758                    )
 759                }))
 760                .child(
 761                    v_flex()
 762                        .p_3()
 763                        .gap_1p5()
 764                        .rounded_lg()
 765                        .shadow_md()
 766                        .bg(cx.theme().colors().editor_background)
 767                        .border_1()
 768                        .border_color(cx.theme().colors().border)
 769                        .text_xs()
 770                        .id("message")
 771                        .on_click(cx.listener({
 772                            move |this, _, window, cx| this.start_editing_message(index, window, cx)
 773                        }))
 774                        .children(
 775                            if let Some(editing) = self.editing_message.as_ref()
 776                                && Some(&editing.message_id) == message.id.as_ref()
 777                            {
 778                                Some(
 779                                    self.render_edit_message_editor(editing, cx)
 780                                        .into_any_element(),
 781                                )
 782                            } else {
 783                                message.content.markdown().map(|md| {
 784                                    self.render_markdown(
 785                                        md.clone(),
 786                                        user_message_markdown_style(window, cx),
 787                                    )
 788                                    .into_any_element()
 789                                })
 790                            },
 791                        ),
 792                )
 793                .into_any(),
 794            AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
 795                let style = default_markdown_style(false, window, cx);
 796                let message_body = v_flex()
 797                    .w_full()
 798                    .gap_2p5()
 799                    .children(chunks.iter().enumerate().filter_map(
 800                        |(chunk_ix, chunk)| match chunk {
 801                            AssistantMessageChunk::Message { block } => {
 802                                block.markdown().map(|md| {
 803                                    self.render_markdown(md.clone(), style.clone())
 804                                        .into_any_element()
 805                                })
 806                            }
 807                            AssistantMessageChunk::Thought { block } => {
 808                                block.markdown().map(|md| {
 809                                    self.render_thinking_block(
 810                                        index,
 811                                        chunk_ix,
 812                                        md.clone(),
 813                                        window,
 814                                        cx,
 815                                    )
 816                                    .into_any_element()
 817                                })
 818                            }
 819                        },
 820                    ))
 821                    .into_any();
 822
 823                v_flex()
 824                    .px_5()
 825                    .py_1()
 826                    .when(index + 1 == total_entries, |this| this.pb_4())
 827                    .w_full()
 828                    .text_ui(cx)
 829                    .child(message_body)
 830                    .into_any()
 831            }
 832            AgentThreadEntry::ToolCall(tool_call) => {
 833                let has_terminals = tool_call.terminals().next().is_some();
 834
 835                div().w_full().py_1p5().px_5().map(|this| {
 836                    if has_terminals {
 837                        this.children(tool_call.terminals().map(|terminal| {
 838                            self.render_terminal_tool_call(terminal, tool_call, window, cx)
 839                        }))
 840                    } else {
 841                        this.child(self.render_tool_call(index, tool_call, window, cx))
 842                    }
 843                })
 844            }
 845            .into_any(),
 846        };
 847
 848        let Some(thread) = self.thread() else {
 849            return primary;
 850        };
 851
 852        let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
 853        if index == total_entries - 1 && !is_generating {
 854            v_flex()
 855                .w_full()
 856                .child(primary)
 857                .child(self.render_thread_controls(cx))
 858                .into_any_element()
 859        } else {
 860            primary
 861        }
 862    }
 863
 864    fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
 865        cx.theme()
 866            .colors()
 867            .element_background
 868            .blend(cx.theme().colors().editor_foreground.opacity(0.025))
 869    }
 870
 871    fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
 872        cx.theme().colors().border.opacity(0.6)
 873    }
 874
 875    fn tool_name_font_size(&self) -> Rems {
 876        rems_from_px(13.)
 877    }
 878
 879    fn render_thinking_block(
 880        &self,
 881        entry_ix: usize,
 882        chunk_ix: usize,
 883        chunk: Entity<Markdown>,
 884        window: &Window,
 885        cx: &Context<Self>,
 886    ) -> AnyElement {
 887        let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix));
 888        let card_header_id = SharedString::from("inner-card-header");
 889        let key = (entry_ix, chunk_ix);
 890        let is_open = self.expanded_thinking_blocks.contains(&key);
 891
 892        v_flex()
 893            .child(
 894                h_flex()
 895                    .id(header_id)
 896                    .group(&card_header_id)
 897                    .relative()
 898                    .w_full()
 899                    .gap_1p5()
 900                    .opacity(0.8)
 901                    .hover(|style| style.opacity(1.))
 902                    .child(
 903                        h_flex()
 904                            .size_4()
 905                            .justify_center()
 906                            .child(
 907                                div()
 908                                    .group_hover(&card_header_id, |s| s.invisible().w_0())
 909                                    .child(
 910                                        Icon::new(IconName::ToolThink)
 911                                            .size(IconSize::Small)
 912                                            .color(Color::Muted),
 913                                    ),
 914                            )
 915                            .child(
 916                                h_flex()
 917                                    .absolute()
 918                                    .inset_0()
 919                                    .invisible()
 920                                    .justify_center()
 921                                    .group_hover(&card_header_id, |s| s.visible())
 922                                    .child(
 923                                        Disclosure::new(("expand", entry_ix), is_open)
 924                                            .opened_icon(IconName::ChevronUp)
 925                                            .closed_icon(IconName::ChevronRight)
 926                                            .on_click(cx.listener({
 927                                                move |this, _event, _window, cx| {
 928                                                    if is_open {
 929                                                        this.expanded_thinking_blocks.remove(&key);
 930                                                    } else {
 931                                                        this.expanded_thinking_blocks.insert(key);
 932                                                    }
 933                                                    cx.notify();
 934                                                }
 935                                            })),
 936                                    ),
 937                            ),
 938                    )
 939                    .child(
 940                        div()
 941                            .text_size(self.tool_name_font_size())
 942                            .child("Thinking"),
 943                    )
 944                    .on_click(cx.listener({
 945                        move |this, _event, _window, cx| {
 946                            if is_open {
 947                                this.expanded_thinking_blocks.remove(&key);
 948                            } else {
 949                                this.expanded_thinking_blocks.insert(key);
 950                            }
 951                            cx.notify();
 952                        }
 953                    })),
 954            )
 955            .when(is_open, |this| {
 956                this.child(
 957                    div()
 958                        .relative()
 959                        .mt_1p5()
 960                        .ml(px(7.))
 961                        .pl_4()
 962                        .border_l_1()
 963                        .border_color(self.tool_card_border_color(cx))
 964                        .text_ui_sm(cx)
 965                        .child(
 966                            self.render_markdown(chunk, default_markdown_style(false, window, cx)),
 967                        ),
 968                )
 969            })
 970            .into_any_element()
 971    }
 972
 973    fn render_tool_call_icon(
 974        &self,
 975        group_name: SharedString,
 976        entry_ix: usize,
 977        is_collapsible: bool,
 978        is_open: bool,
 979        tool_call: &ToolCall,
 980        cx: &Context<Self>,
 981    ) -> Div {
 982        let tool_icon = Icon::new(match tool_call.kind {
 983            acp::ToolKind::Read => IconName::ToolRead,
 984            acp::ToolKind::Edit => IconName::ToolPencil,
 985            acp::ToolKind::Delete => IconName::ToolDeleteFile,
 986            acp::ToolKind::Move => IconName::ArrowRightLeft,
 987            acp::ToolKind::Search => IconName::ToolSearch,
 988            acp::ToolKind::Execute => IconName::ToolTerminal,
 989            acp::ToolKind::Think => IconName::ToolThink,
 990            acp::ToolKind::Fetch => IconName::ToolWeb,
 991            acp::ToolKind::Other => IconName::ToolHammer,
 992        })
 993        .size(IconSize::Small)
 994        .color(Color::Muted);
 995
 996        let base_container = h_flex().size_4().justify_center();
 997
 998        if is_collapsible {
 999            base_container
1000                .child(
1001                    div()
1002                        .group_hover(&group_name, |s| s.invisible().w_0())
1003                        .child(tool_icon),
1004                )
1005                .child(
1006                    h_flex()
1007                        .absolute()
1008                        .inset_0()
1009                        .invisible()
1010                        .justify_center()
1011                        .group_hover(&group_name, |s| s.visible())
1012                        .child(
1013                            Disclosure::new(("expand", entry_ix), is_open)
1014                                .opened_icon(IconName::ChevronUp)
1015                                .closed_icon(IconName::ChevronRight)
1016                                .on_click(cx.listener({
1017                                    let id = tool_call.id.clone();
1018                                    move |this: &mut Self, _, _, cx: &mut Context<Self>| {
1019                                        if is_open {
1020                                            this.expanded_tool_calls.remove(&id);
1021                                        } else {
1022                                            this.expanded_tool_calls.insert(id.clone());
1023                                        }
1024                                        cx.notify();
1025                                    }
1026                                })),
1027                        ),
1028                )
1029        } else {
1030            base_container.child(tool_icon)
1031        }
1032    }
1033
1034    fn render_tool_call(
1035        &self,
1036        entry_ix: usize,
1037        tool_call: &ToolCall,
1038        window: &Window,
1039        cx: &Context<Self>,
1040    ) -> Div {
1041        let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix));
1042        let card_header_id = SharedString::from("inner-tool-call-header");
1043
1044        let status_icon = match &tool_call.status {
1045            ToolCallStatus::Allowed {
1046                status: acp::ToolCallStatus::Pending,
1047            }
1048            | ToolCallStatus::WaitingForConfirmation { .. } => None,
1049            ToolCallStatus::Allowed {
1050                status: acp::ToolCallStatus::InProgress,
1051                ..
1052            } => Some(
1053                Icon::new(IconName::ArrowCircle)
1054                    .color(Color::Accent)
1055                    .size(IconSize::Small)
1056                    .with_animation(
1057                        "running",
1058                        Animation::new(Duration::from_secs(2)).repeat(),
1059                        |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
1060                    )
1061                    .into_any(),
1062            ),
1063            ToolCallStatus::Allowed {
1064                status: acp::ToolCallStatus::Completed,
1065                ..
1066            } => None,
1067            ToolCallStatus::Rejected
1068            | ToolCallStatus::Canceled
1069            | ToolCallStatus::Allowed {
1070                status: acp::ToolCallStatus::Failed,
1071                ..
1072            } => Some(
1073                Icon::new(IconName::Close)
1074                    .color(Color::Error)
1075                    .size(IconSize::Small)
1076                    .into_any_element(),
1077            ),
1078        };
1079
1080        let needs_confirmation = matches!(
1081            tool_call.status,
1082            ToolCallStatus::WaitingForConfirmation { .. }
1083        );
1084        let is_edit = matches!(tool_call.kind, acp::ToolKind::Edit);
1085        let has_diff = tool_call
1086            .content
1087            .iter()
1088            .any(|content| matches!(content, ToolCallContent::Diff { .. }));
1089        let has_nonempty_diff = tool_call.content.iter().any(|content| match content {
1090            ToolCallContent::Diff(diff) => diff.read(cx).has_revealed_range(cx),
1091            _ => false,
1092        });
1093        let use_card_layout = needs_confirmation || is_edit || has_diff;
1094
1095        let is_collapsible = !tool_call.content.is_empty() && !use_card_layout;
1096
1097        let is_open = tool_call.content.is_empty()
1098            || needs_confirmation
1099            || has_nonempty_diff
1100            || self.expanded_tool_calls.contains(&tool_call.id);
1101
1102        let gradient_overlay = |color: Hsla| {
1103            div()
1104                .absolute()
1105                .top_0()
1106                .right_0()
1107                .w_12()
1108                .h_full()
1109                .bg(linear_gradient(
1110                    90.,
1111                    linear_color_stop(color, 1.),
1112                    linear_color_stop(color.opacity(0.2), 0.),
1113                ))
1114        };
1115        let gradient_color = if use_card_layout {
1116            self.tool_card_header_bg(cx)
1117        } else {
1118            cx.theme().colors().panel_background
1119        };
1120
1121        let tool_output_display = match &tool_call.status {
1122            ToolCallStatus::WaitingForConfirmation { options, .. } => v_flex()
1123                .w_full()
1124                .children(tool_call.content.iter().map(|content| {
1125                    div()
1126                        .child(self.render_tool_call_content(content, tool_call, window, cx))
1127                        .into_any_element()
1128                }))
1129                .child(self.render_permission_buttons(
1130                    options,
1131                    entry_ix,
1132                    tool_call.id.clone(),
1133                    tool_call.content.is_empty(),
1134                    cx,
1135                )),
1136            ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => v_flex()
1137                .w_full()
1138                .children(tool_call.content.iter().map(|content| {
1139                    div()
1140                        .child(self.render_tool_call_content(content, tool_call, window, cx))
1141                        .into_any_element()
1142                })),
1143            ToolCallStatus::Rejected => v_flex().size_0(),
1144        };
1145
1146        v_flex()
1147            .when(use_card_layout, |this| {
1148                this.rounded_lg()
1149                    .border_1()
1150                    .border_color(self.tool_card_border_color(cx))
1151                    .bg(cx.theme().colors().editor_background)
1152                    .overflow_hidden()
1153            })
1154            .child(
1155                h_flex()
1156                    .id(header_id)
1157                    .w_full()
1158                    .gap_1()
1159                    .justify_between()
1160                    .map(|this| {
1161                        if use_card_layout {
1162                            this.pl_2()
1163                                .pr_1()
1164                                .py_1()
1165                                .rounded_t_md()
1166                                .bg(self.tool_card_header_bg(cx))
1167                        } else {
1168                            this.opacity(0.8).hover(|style| style.opacity(1.))
1169                        }
1170                    })
1171                    .child(
1172                        h_flex()
1173                            .group(&card_header_id)
1174                            .relative()
1175                            .w_full()
1176                            .text_size(self.tool_name_font_size())
1177                            .child(self.render_tool_call_icon(
1178                                card_header_id,
1179                                entry_ix,
1180                                is_collapsible,
1181                                is_open,
1182                                tool_call,
1183                                cx,
1184                            ))
1185                            .child(if tool_call.locations.len() == 1 {
1186                                let name = tool_call.locations[0]
1187                                    .path
1188                                    .file_name()
1189                                    .unwrap_or_default()
1190                                    .display()
1191                                    .to_string();
1192
1193                                h_flex()
1194                                    .id(("open-tool-call-location", entry_ix))
1195                                    .w_full()
1196                                    .max_w_full()
1197                                    .px_1p5()
1198                                    .rounded_sm()
1199                                    .overflow_x_scroll()
1200                                    .opacity(0.8)
1201                                    .hover(|label| {
1202                                        label.opacity(1.).bg(cx
1203                                            .theme()
1204                                            .colors()
1205                                            .element_hover
1206                                            .opacity(0.5))
1207                                    })
1208                                    .child(name)
1209                                    .tooltip(Tooltip::text("Jump to File"))
1210                                    .on_click(cx.listener(move |this, _, window, cx| {
1211                                        this.open_tool_call_location(entry_ix, 0, window, cx);
1212                                    }))
1213                                    .into_any_element()
1214                            } else {
1215                                h_flex()
1216                                    .id("non-card-label-container")
1217                                    .w_full()
1218                                    .relative()
1219                                    .ml_1p5()
1220                                    .overflow_hidden()
1221                                    .child(
1222                                        h_flex()
1223                                            .id("non-card-label")
1224                                            .pr_8()
1225                                            .w_full()
1226                                            .overflow_x_scroll()
1227                                            .child(self.render_markdown(
1228                                                tool_call.label.clone(),
1229                                                default_markdown_style(
1230                                                    needs_confirmation || is_edit || has_diff,
1231                                                    window,
1232                                                    cx,
1233                                                ),
1234                                            )),
1235                                    )
1236                                    .child(gradient_overlay(gradient_color))
1237                                    .on_click(cx.listener({
1238                                        let id = tool_call.id.clone();
1239                                        move |this: &mut Self, _, _, cx: &mut Context<Self>| {
1240                                            if is_open {
1241                                                this.expanded_tool_calls.remove(&id);
1242                                            } else {
1243                                                this.expanded_tool_calls.insert(id.clone());
1244                                            }
1245                                            cx.notify();
1246                                        }
1247                                    }))
1248                                    .into_any()
1249                            }),
1250                    )
1251                    .children(status_icon),
1252            )
1253            .when(is_open, |this| this.child(tool_output_display))
1254    }
1255
1256    fn render_tool_call_content(
1257        &self,
1258        content: &ToolCallContent,
1259        tool_call: &ToolCall,
1260        window: &Window,
1261        cx: &Context<Self>,
1262    ) -> AnyElement {
1263        match content {
1264            ToolCallContent::ContentBlock(content) => {
1265                if let Some(resource_link) = content.resource_link() {
1266                    self.render_resource_link(resource_link, cx)
1267                } else if let Some(markdown) = content.markdown() {
1268                    self.render_markdown_output(markdown.clone(), tool_call.id.clone(), window, cx)
1269                } else {
1270                    Empty.into_any_element()
1271                }
1272            }
1273            ToolCallContent::Diff(diff) => {
1274                self.render_diff_editor(&diff.read(cx).multibuffer(), cx)
1275            }
1276            ToolCallContent::Terminal(terminal) => {
1277                self.render_terminal_tool_call(terminal, tool_call, window, cx)
1278            }
1279        }
1280    }
1281
1282    fn render_markdown_output(
1283        &self,
1284        markdown: Entity<Markdown>,
1285        tool_call_id: acp::ToolCallId,
1286        window: &Window,
1287        cx: &Context<Self>,
1288    ) -> AnyElement {
1289        let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id.clone()));
1290
1291        v_flex()
1292            .mt_1p5()
1293            .ml(px(7.))
1294            .px_3p5()
1295            .gap_2()
1296            .border_l_1()
1297            .border_color(self.tool_card_border_color(cx))
1298            .text_sm()
1299            .text_color(cx.theme().colors().text_muted)
1300            .child(self.render_markdown(markdown, default_markdown_style(false, window, cx)))
1301            .child(
1302                Button::new(button_id, "Collapse Output")
1303                    .full_width()
1304                    .style(ButtonStyle::Outlined)
1305                    .label_size(LabelSize::Small)
1306                    .icon(IconName::ChevronUp)
1307                    .icon_color(Color::Muted)
1308                    .icon_position(IconPosition::Start)
1309                    .on_click(cx.listener({
1310                        let id = tool_call_id.clone();
1311                        move |this: &mut Self, _, _, cx: &mut Context<Self>| {
1312                            this.expanded_tool_calls.remove(&id);
1313                            cx.notify();
1314                        }
1315                    })),
1316            )
1317            .into_any_element()
1318    }
1319
1320    fn render_resource_link(
1321        &self,
1322        resource_link: &acp::ResourceLink,
1323        cx: &Context<Self>,
1324    ) -> AnyElement {
1325        let uri: SharedString = resource_link.uri.clone().into();
1326
1327        let label: SharedString = if let Some(path) = resource_link.uri.strip_prefix("file://") {
1328            path.to_string().into()
1329        } else {
1330            uri.clone()
1331        };
1332
1333        let button_id = SharedString::from(format!("item-{}", uri.clone()));
1334
1335        div()
1336            .ml(px(7.))
1337            .pl_2p5()
1338            .border_l_1()
1339            .border_color(self.tool_card_border_color(cx))
1340            .overflow_hidden()
1341            .child(
1342                Button::new(button_id, label)
1343                    .label_size(LabelSize::Small)
1344                    .color(Color::Muted)
1345                    .icon(IconName::ArrowUpRight)
1346                    .icon_size(IconSize::XSmall)
1347                    .icon_color(Color::Muted)
1348                    .truncate(true)
1349                    .on_click(cx.listener({
1350                        let workspace = self.workspace.clone();
1351                        move |_, _, window, cx: &mut Context<Self>| {
1352                            Self::open_link(uri.clone(), &workspace, window, cx);
1353                        }
1354                    })),
1355            )
1356            .into_any_element()
1357    }
1358
1359    fn render_permission_buttons(
1360        &self,
1361        options: &[acp::PermissionOption],
1362        entry_ix: usize,
1363        tool_call_id: acp::ToolCallId,
1364        empty_content: bool,
1365        cx: &Context<Self>,
1366    ) -> Div {
1367        h_flex()
1368            .py_1()
1369            .pl_2()
1370            .pr_1()
1371            .gap_1()
1372            .justify_between()
1373            .flex_wrap()
1374            .when(!empty_content, |this| {
1375                this.border_t_1()
1376                    .border_color(self.tool_card_border_color(cx))
1377            })
1378            .child(
1379                div()
1380                    .min_w(rems_from_px(145.))
1381                    .child(LoadingLabel::new("Waiting for Confirmation").size(LabelSize::Small)),
1382            )
1383            .child(h_flex().gap_0p5().children(options.iter().map(|option| {
1384                let option_id = SharedString::from(option.id.0.clone());
1385                Button::new((option_id, entry_ix), option.name.clone())
1386                    .map(|this| match option.kind {
1387                        acp::PermissionOptionKind::AllowOnce => {
1388                            this.icon(IconName::Check).icon_color(Color::Success)
1389                        }
1390                        acp::PermissionOptionKind::AllowAlways => {
1391                            this.icon(IconName::CheckDouble).icon_color(Color::Success)
1392                        }
1393                        acp::PermissionOptionKind::RejectOnce => {
1394                            this.icon(IconName::Close).icon_color(Color::Error)
1395                        }
1396                        acp::PermissionOptionKind::RejectAlways => {
1397                            this.icon(IconName::Close).icon_color(Color::Error)
1398                        }
1399                    })
1400                    .icon_position(IconPosition::Start)
1401                    .icon_size(IconSize::XSmall)
1402                    .label_size(LabelSize::Small)
1403                    .on_click(cx.listener({
1404                        let tool_call_id = tool_call_id.clone();
1405                        let option_id = option.id.clone();
1406                        let option_kind = option.kind;
1407                        move |this, _, _, cx| {
1408                            this.authorize_tool_call(
1409                                tool_call_id.clone(),
1410                                option_id.clone(),
1411                                option_kind,
1412                                cx,
1413                            );
1414                        }
1415                    }))
1416            })))
1417    }
1418
1419    fn render_diff_editor(
1420        &self,
1421        multibuffer: &Entity<MultiBuffer>,
1422        cx: &Context<Self>,
1423    ) -> AnyElement {
1424        v_flex()
1425            .h_full()
1426            .border_t_1()
1427            .border_color(self.tool_card_border_color(cx))
1428            .child(
1429                if let Some(editor) = self.diff_editors.get(&multibuffer.entity_id()) {
1430                    editor.clone().into_any_element()
1431                } else {
1432                    Empty.into_any()
1433                },
1434            )
1435            .into_any()
1436    }
1437
1438    fn render_terminal_tool_call(
1439        &self,
1440        terminal: &Entity<acp_thread::Terminal>,
1441        tool_call: &ToolCall,
1442        window: &Window,
1443        cx: &Context<Self>,
1444    ) -> AnyElement {
1445        let terminal_data = terminal.read(cx);
1446        let working_dir = terminal_data.working_dir();
1447        let command = terminal_data.command();
1448        let started_at = terminal_data.started_at();
1449
1450        let tool_failed = matches!(
1451            &tool_call.status,
1452            ToolCallStatus::Rejected
1453                | ToolCallStatus::Canceled
1454                | ToolCallStatus::Allowed {
1455                    status: acp::ToolCallStatus::Failed,
1456                    ..
1457                }
1458        );
1459
1460        let output = terminal_data.output();
1461        let command_finished = output.is_some();
1462        let truncated_output = output.is_some_and(|output| output.was_content_truncated);
1463        let output_line_count = output.map(|output| output.content_line_count).unwrap_or(0);
1464
1465        let command_failed = command_finished
1466            && output.is_some_and(|o| o.exit_status.is_none_or(|status| !status.success()));
1467
1468        let time_elapsed = if let Some(output) = output {
1469            output.ended_at.duration_since(started_at)
1470        } else {
1471            started_at.elapsed()
1472        };
1473
1474        let header_bg = cx
1475            .theme()
1476            .colors()
1477            .element_background
1478            .blend(cx.theme().colors().editor_foreground.opacity(0.025));
1479        let border_color = cx.theme().colors().border.opacity(0.6);
1480
1481        let working_dir = working_dir
1482            .as_ref()
1483            .map(|path| format!("{}", path.display()))
1484            .unwrap_or_else(|| "current directory".to_string());
1485
1486        let header = h_flex()
1487            .id(SharedString::from(format!(
1488                "terminal-tool-header-{}",
1489                terminal.entity_id()
1490            )))
1491            .flex_none()
1492            .gap_1()
1493            .justify_between()
1494            .rounded_t_md()
1495            .child(
1496                div()
1497                    .id(("command-target-path", terminal.entity_id()))
1498                    .w_full()
1499                    .max_w_full()
1500                    .overflow_x_scroll()
1501                    .child(
1502                        Label::new(working_dir)
1503                            .buffer_font(cx)
1504                            .size(LabelSize::XSmall)
1505                            .color(Color::Muted),
1506                    ),
1507            )
1508            .when(!command_finished, |header| {
1509                header
1510                    .gap_1p5()
1511                    .child(
1512                        Button::new(
1513                            SharedString::from(format!("stop-terminal-{}", terminal.entity_id())),
1514                            "Stop",
1515                        )
1516                        .icon(IconName::Stop)
1517                        .icon_position(IconPosition::Start)
1518                        .icon_size(IconSize::Small)
1519                        .icon_color(Color::Error)
1520                        .label_size(LabelSize::Small)
1521                        .tooltip(move |window, cx| {
1522                            Tooltip::with_meta(
1523                                "Stop This Command",
1524                                None,
1525                                "Also possible by placing your cursor inside the terminal and using regular terminal bindings.",
1526                                window,
1527                                cx,
1528                            )
1529                        })
1530                        .on_click({
1531                            let terminal = terminal.clone();
1532                            cx.listener(move |_this, _event, _window, cx| {
1533                                let inner_terminal = terminal.read(cx).inner().clone();
1534                                inner_terminal.update(cx, |inner_terminal, _cx| {
1535                                    inner_terminal.kill_active_task();
1536                                });
1537                            })
1538                        }),
1539                    )
1540                    .child(Divider::vertical())
1541                    .child(
1542                        Icon::new(IconName::ArrowCircle)
1543                            .size(IconSize::XSmall)
1544                            .color(Color::Info)
1545                            .with_animation(
1546                                "arrow-circle",
1547                                Animation::new(Duration::from_secs(2)).repeat(),
1548                                |icon, delta| {
1549                                    icon.transform(Transformation::rotate(percentage(delta)))
1550                                },
1551                            ),
1552                    )
1553            })
1554            .when(tool_failed || command_failed, |header| {
1555                header.child(
1556                    div()
1557                        .id(("terminal-tool-error-code-indicator", terminal.entity_id()))
1558                        .child(
1559                            Icon::new(IconName::Close)
1560                                .size(IconSize::Small)
1561                                .color(Color::Error),
1562                        )
1563                        .when_some(output.and_then(|o| o.exit_status), |this, status| {
1564                            this.tooltip(Tooltip::text(format!(
1565                                "Exited with code {}",
1566                                status.code().unwrap_or(-1),
1567                            )))
1568                        }),
1569                )
1570            })
1571            .when(truncated_output, |header| {
1572                let tooltip = if let Some(output) = output {
1573                    if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
1574                        "Output exceeded terminal max lines and was \
1575                            truncated, the model received the first 16 KB."
1576                            .to_string()
1577                    } else {
1578                        format!(
1579                            "Output is {} long—to avoid unexpected token usage, \
1580                                only 16 KB was sent back to the model.",
1581                            format_file_size(output.original_content_len as u64, true),
1582                        )
1583                    }
1584                } else {
1585                    "Output was truncated".to_string()
1586                };
1587
1588                header.child(
1589                    h_flex()
1590                        .id(("terminal-tool-truncated-label", terminal.entity_id()))
1591                        .gap_1()
1592                        .child(
1593                            Icon::new(IconName::Info)
1594                                .size(IconSize::XSmall)
1595                                .color(Color::Ignored),
1596                        )
1597                        .child(
1598                            Label::new("Truncated")
1599                                .color(Color::Muted)
1600                                .size(LabelSize::XSmall),
1601                        )
1602                        .tooltip(Tooltip::text(tooltip)),
1603                )
1604            })
1605            .when(time_elapsed > Duration::from_secs(10), |header| {
1606                header.child(
1607                    Label::new(format!("({})", duration_alt_display(time_elapsed)))
1608                        .buffer_font(cx)
1609                        .color(Color::Muted)
1610                        .size(LabelSize::XSmall),
1611                )
1612            })
1613            .child(
1614                Disclosure::new(
1615                    SharedString::from(format!(
1616                        "terminal-tool-disclosure-{}",
1617                        terminal.entity_id()
1618                    )),
1619                    self.terminal_expanded,
1620                )
1621                .opened_icon(IconName::ChevronUp)
1622                .closed_icon(IconName::ChevronDown)
1623                .on_click(cx.listener(move |this, _event, _window, _cx| {
1624                    this.terminal_expanded = !this.terminal_expanded;
1625                })),
1626            );
1627
1628        let show_output =
1629            self.terminal_expanded && self.terminal_views.contains_key(&terminal.entity_id());
1630
1631        v_flex()
1632            .mb_2()
1633            .border_1()
1634            .when(tool_failed || command_failed, |card| card.border_dashed())
1635            .border_color(border_color)
1636            .rounded_lg()
1637            .overflow_hidden()
1638            .child(
1639                v_flex()
1640                    .py_1p5()
1641                    .pl_2()
1642                    .pr_1p5()
1643                    .gap_0p5()
1644                    .bg(header_bg)
1645                    .text_xs()
1646                    .child(header)
1647                    .child(
1648                        MarkdownElement::new(
1649                            command.clone(),
1650                            terminal_command_markdown_style(window, cx),
1651                        )
1652                        .code_block_renderer(
1653                            markdown::CodeBlockRenderer::Default {
1654                                copy_button: false,
1655                                copy_button_on_hover: true,
1656                                border: false,
1657                            },
1658                        ),
1659                    ),
1660            )
1661            .when(show_output, |this| {
1662                let terminal_view = self.terminal_views.get(&terminal.entity_id()).unwrap();
1663
1664                this.child(
1665                    div()
1666                        .pt_2()
1667                        .border_t_1()
1668                        .when(tool_failed || command_failed, |card| card.border_dashed())
1669                        .border_color(border_color)
1670                        .bg(cx.theme().colors().editor_background)
1671                        .rounded_b_md()
1672                        .text_ui_sm(cx)
1673                        .child(terminal_view.clone()),
1674                )
1675            })
1676            .into_any()
1677    }
1678
1679    fn render_agent_logo(&self) -> AnyElement {
1680        Icon::new(self.agent.logo())
1681            .color(Color::Muted)
1682            .size(IconSize::XLarge)
1683            .into_any_element()
1684    }
1685
1686    fn render_error_agent_logo(&self) -> AnyElement {
1687        let logo = Icon::new(self.agent.logo())
1688            .color(Color::Muted)
1689            .size(IconSize::XLarge)
1690            .into_any_element();
1691
1692        h_flex()
1693            .relative()
1694            .justify_center()
1695            .child(div().opacity(0.3).child(logo))
1696            .child(
1697                h_flex().absolute().right_1().bottom_0().child(
1698                    Icon::new(IconName::XCircle)
1699                        .color(Color::Error)
1700                        .size(IconSize::Small),
1701                ),
1702            )
1703            .into_any_element()
1704    }
1705
1706    fn render_empty_state(&self, cx: &App) -> AnyElement {
1707        let loading = matches!(&self.thread_state, ThreadState::Loading { .. });
1708
1709        v_flex()
1710            .size_full()
1711            .items_center()
1712            .justify_center()
1713            .child(if loading {
1714                h_flex()
1715                    .justify_center()
1716                    .child(self.render_agent_logo())
1717                    .with_animation(
1718                        "pulsating_icon",
1719                        Animation::new(Duration::from_secs(2))
1720                            .repeat()
1721                            .with_easing(pulsating_between(0.4, 1.0)),
1722                        |icon, delta| icon.opacity(delta),
1723                    )
1724                    .into_any()
1725            } else {
1726                self.render_agent_logo().into_any_element()
1727            })
1728            .child(h_flex().mt_4().mb_1().justify_center().child(if loading {
1729                div()
1730                    .child(LoadingLabel::new("").size(LabelSize::Large))
1731                    .into_any_element()
1732            } else {
1733                Headline::new(self.agent.empty_state_headline())
1734                    .size(HeadlineSize::Medium)
1735                    .into_any_element()
1736            }))
1737            .child(
1738                div()
1739                    .max_w_1_2()
1740                    .text_sm()
1741                    .text_center()
1742                    .map(|this| {
1743                        if loading {
1744                            this.invisible()
1745                        } else {
1746                            this.text_color(cx.theme().colors().text_muted)
1747                        }
1748                    })
1749                    .child(self.agent.empty_state_message()),
1750            )
1751            .into_any()
1752    }
1753
1754    fn render_pending_auth_state(&self) -> AnyElement {
1755        v_flex()
1756            .items_center()
1757            .justify_center()
1758            .child(self.render_error_agent_logo())
1759            .child(
1760                h_flex()
1761                    .mt_4()
1762                    .mb_1()
1763                    .justify_center()
1764                    .child(Headline::new("Not Authenticated").size(HeadlineSize::Medium)),
1765            )
1766            .into_any()
1767    }
1768
1769    fn render_server_exited(&self, status: ExitStatus, _cx: &Context<Self>) -> AnyElement {
1770        v_flex()
1771            .items_center()
1772            .justify_center()
1773            .child(self.render_error_agent_logo())
1774            .child(
1775                v_flex()
1776                    .mt_4()
1777                    .mb_2()
1778                    .gap_0p5()
1779                    .text_center()
1780                    .items_center()
1781                    .child(Headline::new("Server exited unexpectedly").size(HeadlineSize::Medium))
1782                    .child(
1783                        Label::new(format!("Exit status: {}", status.code().unwrap_or(-127)))
1784                            .size(LabelSize::Small)
1785                            .color(Color::Muted),
1786                    ),
1787            )
1788            .into_any_element()
1789    }
1790
1791    fn render_load_error(&self, e: &LoadError, cx: &Context<Self>) -> AnyElement {
1792        let mut container = v_flex()
1793            .items_center()
1794            .justify_center()
1795            .child(self.render_error_agent_logo())
1796            .child(
1797                v_flex()
1798                    .mt_4()
1799                    .mb_2()
1800                    .gap_0p5()
1801                    .text_center()
1802                    .items_center()
1803                    .child(Headline::new("Failed to launch").size(HeadlineSize::Medium))
1804                    .child(
1805                        Label::new(e.to_string())
1806                            .size(LabelSize::Small)
1807                            .color(Color::Muted),
1808                    ),
1809            );
1810
1811        if let LoadError::Unsupported {
1812            upgrade_message,
1813            upgrade_command,
1814            ..
1815        } = &e
1816        {
1817            let upgrade_message = upgrade_message.clone();
1818            let upgrade_command = upgrade_command.clone();
1819            container = container.child(Button::new("upgrade", upgrade_message).on_click(
1820                cx.listener(move |this, _, window, cx| {
1821                    this.workspace
1822                        .update(cx, |workspace, cx| {
1823                            let project = workspace.project().read(cx);
1824                            let cwd = project.first_project_directory(cx);
1825                            let shell = project.terminal_settings(&cwd, cx).shell.clone();
1826                            let spawn_in_terminal = task::SpawnInTerminal {
1827                                id: task::TaskId("install".to_string()),
1828                                full_label: upgrade_command.clone(),
1829                                label: upgrade_command.clone(),
1830                                command: Some(upgrade_command.clone()),
1831                                args: Vec::new(),
1832                                command_label: upgrade_command.clone(),
1833                                cwd,
1834                                env: Default::default(),
1835                                use_new_terminal: true,
1836                                allow_concurrent_runs: true,
1837                                reveal: Default::default(),
1838                                reveal_target: Default::default(),
1839                                hide: Default::default(),
1840                                shell,
1841                                show_summary: true,
1842                                show_command: true,
1843                                show_rerun: false,
1844                            };
1845                            workspace
1846                                .spawn_in_terminal(spawn_in_terminal, window, cx)
1847                                .detach();
1848                        })
1849                        .ok();
1850                }),
1851            ));
1852        }
1853
1854        container.into_any()
1855    }
1856
1857    fn render_activity_bar(
1858        &self,
1859        thread_entity: &Entity<AcpThread>,
1860        window: &mut Window,
1861        cx: &Context<Self>,
1862    ) -> Option<AnyElement> {
1863        let thread = thread_entity.read(cx);
1864        let action_log = thread.action_log();
1865        let changed_buffers = action_log.read(cx).changed_buffers(cx);
1866        let plan = thread.plan();
1867
1868        if changed_buffers.is_empty() && plan.is_empty() {
1869            return None;
1870        }
1871
1872        let editor_bg_color = cx.theme().colors().editor_background;
1873        let active_color = cx.theme().colors().element_selected;
1874        let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
1875
1876        let pending_edits = thread.has_pending_edit_tool_calls();
1877
1878        v_flex()
1879            .mt_1()
1880            .mx_2()
1881            .bg(bg_edit_files_disclosure)
1882            .border_1()
1883            .border_b_0()
1884            .border_color(cx.theme().colors().border)
1885            .rounded_t_md()
1886            .shadow(vec![gpui::BoxShadow {
1887                color: gpui::black().opacity(0.15),
1888                offset: point(px(1.), px(-1.)),
1889                blur_radius: px(3.),
1890                spread_radius: px(0.),
1891            }])
1892            .when(!plan.is_empty(), |this| {
1893                this.child(self.render_plan_summary(plan, window, cx))
1894                    .when(self.plan_expanded, |parent| {
1895                        parent.child(self.render_plan_entries(plan, window, cx))
1896                    })
1897            })
1898            .when(!plan.is_empty() && !changed_buffers.is_empty(), |this| {
1899                this.child(Divider::horizontal().color(DividerColor::Border))
1900            })
1901            .when(!changed_buffers.is_empty(), |this| {
1902                this.child(self.render_edits_summary(
1903                    action_log,
1904                    &changed_buffers,
1905                    self.edits_expanded,
1906                    pending_edits,
1907                    window,
1908                    cx,
1909                ))
1910                .when(self.edits_expanded, |parent| {
1911                    parent.child(self.render_edited_files(
1912                        action_log,
1913                        &changed_buffers,
1914                        pending_edits,
1915                        cx,
1916                    ))
1917                })
1918            })
1919            .into_any()
1920            .into()
1921    }
1922
1923    fn render_plan_summary(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
1924        let stats = plan.stats();
1925
1926        let title = if let Some(entry) = stats.in_progress_entry
1927            && !self.plan_expanded
1928        {
1929            h_flex()
1930                .w_full()
1931                .cursor_default()
1932                .gap_1()
1933                .text_xs()
1934                .text_color(cx.theme().colors().text_muted)
1935                .justify_between()
1936                .child(
1937                    h_flex()
1938                        .gap_1()
1939                        .child(
1940                            Label::new("Current:")
1941                                .size(LabelSize::Small)
1942                                .color(Color::Muted),
1943                        )
1944                        .child(MarkdownElement::new(
1945                            entry.content.clone(),
1946                            plan_label_markdown_style(&entry.status, window, cx),
1947                        )),
1948                )
1949                .when(stats.pending > 0, |this| {
1950                    this.child(
1951                        Label::new(format!("{} left", stats.pending))
1952                            .size(LabelSize::Small)
1953                            .color(Color::Muted)
1954                            .mr_1(),
1955                    )
1956                })
1957        } else {
1958            let status_label = if stats.pending == 0 {
1959                "All Done".to_string()
1960            } else if stats.completed == 0 {
1961                format!("{} Tasks", plan.entries.len())
1962            } else {
1963                format!("{}/{}", stats.completed, plan.entries.len())
1964            };
1965
1966            h_flex()
1967                .w_full()
1968                .gap_1()
1969                .justify_between()
1970                .child(
1971                    Label::new("Plan")
1972                        .size(LabelSize::Small)
1973                        .color(Color::Muted),
1974                )
1975                .child(
1976                    Label::new(status_label)
1977                        .size(LabelSize::Small)
1978                        .color(Color::Muted)
1979                        .mr_1(),
1980                )
1981        };
1982
1983        h_flex()
1984            .p_1()
1985            .justify_between()
1986            .when(self.plan_expanded, |this| {
1987                this.border_b_1().border_color(cx.theme().colors().border)
1988            })
1989            .child(
1990                h_flex()
1991                    .id("plan_summary")
1992                    .w_full()
1993                    .gap_1()
1994                    .child(Disclosure::new("plan_disclosure", self.plan_expanded))
1995                    .child(title)
1996                    .on_click(cx.listener(|this, _, _, cx| {
1997                        this.plan_expanded = !this.plan_expanded;
1998                        cx.notify();
1999                    })),
2000            )
2001    }
2002
2003    fn render_plan_entries(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
2004        v_flex().children(plan.entries.iter().enumerate().flat_map(|(index, entry)| {
2005            let element = h_flex()
2006                .py_1()
2007                .px_2()
2008                .gap_2()
2009                .justify_between()
2010                .bg(cx.theme().colors().editor_background)
2011                .when(index < plan.entries.len() - 1, |parent| {
2012                    parent.border_color(cx.theme().colors().border).border_b_1()
2013                })
2014                .child(
2015                    h_flex()
2016                        .id(("plan_entry", index))
2017                        .gap_1p5()
2018                        .max_w_full()
2019                        .overflow_x_scroll()
2020                        .text_xs()
2021                        .text_color(cx.theme().colors().text_muted)
2022                        .child(match entry.status {
2023                            acp::PlanEntryStatus::Pending => Icon::new(IconName::TodoPending)
2024                                .size(IconSize::Small)
2025                                .color(Color::Muted)
2026                                .into_any_element(),
2027                            acp::PlanEntryStatus::InProgress => Icon::new(IconName::TodoProgress)
2028                                .size(IconSize::Small)
2029                                .color(Color::Accent)
2030                                .with_animation(
2031                                    "running",
2032                                    Animation::new(Duration::from_secs(2)).repeat(),
2033                                    |icon, delta| {
2034                                        icon.transform(Transformation::rotate(percentage(delta)))
2035                                    },
2036                                )
2037                                .into_any_element(),
2038                            acp::PlanEntryStatus::Completed => Icon::new(IconName::TodoComplete)
2039                                .size(IconSize::Small)
2040                                .color(Color::Success)
2041                                .into_any_element(),
2042                        })
2043                        .child(MarkdownElement::new(
2044                            entry.content.clone(),
2045                            plan_label_markdown_style(&entry.status, window, cx),
2046                        )),
2047                );
2048
2049            Some(element)
2050        }))
2051    }
2052
2053    fn render_edits_summary(
2054        &self,
2055        action_log: &Entity<ActionLog>,
2056        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
2057        expanded: bool,
2058        pending_edits: bool,
2059        window: &mut Window,
2060        cx: &Context<Self>,
2061    ) -> Div {
2062        const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete.";
2063
2064        let focus_handle = self.focus_handle(cx);
2065
2066        h_flex()
2067            .p_1()
2068            .justify_between()
2069            .when(expanded, |this| {
2070                this.border_b_1().border_color(cx.theme().colors().border)
2071            })
2072            .child(
2073                h_flex()
2074                    .id("edits-container")
2075                    .w_full()
2076                    .gap_1()
2077                    .child(Disclosure::new("edits-disclosure", expanded))
2078                    .map(|this| {
2079                        if pending_edits {
2080                            this.child(
2081                                Label::new(format!(
2082                                    "Editing {} {}",
2083                                    changed_buffers.len(),
2084                                    if changed_buffers.len() == 1 {
2085                                        "file"
2086                                    } else {
2087                                        "files"
2088                                    }
2089                                ))
2090                                .color(Color::Muted)
2091                                .size(LabelSize::Small)
2092                                .with_animation(
2093                                    "edit-label",
2094                                    Animation::new(Duration::from_secs(2))
2095                                        .repeat()
2096                                        .with_easing(pulsating_between(0.3, 0.7)),
2097                                    |label, delta| label.alpha(delta),
2098                                ),
2099                            )
2100                        } else {
2101                            this.child(
2102                                Label::new("Edits")
2103                                    .size(LabelSize::Small)
2104                                    .color(Color::Muted),
2105                            )
2106                            .child(Label::new("").size(LabelSize::XSmall).color(Color::Muted))
2107                            .child(
2108                                Label::new(format!(
2109                                    "{} {}",
2110                                    changed_buffers.len(),
2111                                    if changed_buffers.len() == 1 {
2112                                        "file"
2113                                    } else {
2114                                        "files"
2115                                    }
2116                                ))
2117                                .size(LabelSize::Small)
2118                                .color(Color::Muted),
2119                            )
2120                        }
2121                    })
2122                    .on_click(cx.listener(|this, _, _, cx| {
2123                        this.edits_expanded = !this.edits_expanded;
2124                        cx.notify();
2125                    })),
2126            )
2127            .child(
2128                h_flex()
2129                    .gap_1()
2130                    .child(
2131                        IconButton::new("review-changes", IconName::ListTodo)
2132                            .icon_size(IconSize::Small)
2133                            .tooltip({
2134                                let focus_handle = focus_handle.clone();
2135                                move |window, cx| {
2136                                    Tooltip::for_action_in(
2137                                        "Review Changes",
2138                                        &OpenAgentDiff,
2139                                        &focus_handle,
2140                                        window,
2141                                        cx,
2142                                    )
2143                                }
2144                            })
2145                            .on_click(cx.listener(|_, _, window, cx| {
2146                                window.dispatch_action(OpenAgentDiff.boxed_clone(), cx);
2147                            })),
2148                    )
2149                    .child(Divider::vertical().color(DividerColor::Border))
2150                    .child(
2151                        Button::new("reject-all-changes", "Reject All")
2152                            .label_size(LabelSize::Small)
2153                            .disabled(pending_edits)
2154                            .when(pending_edits, |this| {
2155                                this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
2156                            })
2157                            .key_binding(
2158                                KeyBinding::for_action_in(
2159                                    &RejectAll,
2160                                    &focus_handle.clone(),
2161                                    window,
2162                                    cx,
2163                                )
2164                                .map(|kb| kb.size(rems_from_px(10.))),
2165                            )
2166                            .on_click({
2167                                let action_log = action_log.clone();
2168                                cx.listener(move |_, _, _, cx| {
2169                                    action_log.update(cx, |action_log, cx| {
2170                                        action_log.reject_all_edits(cx).detach();
2171                                    })
2172                                })
2173                            }),
2174                    )
2175                    .child(
2176                        Button::new("keep-all-changes", "Keep All")
2177                            .label_size(LabelSize::Small)
2178                            .disabled(pending_edits)
2179                            .when(pending_edits, |this| {
2180                                this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
2181                            })
2182                            .key_binding(
2183                                KeyBinding::for_action_in(&KeepAll, &focus_handle, window, cx)
2184                                    .map(|kb| kb.size(rems_from_px(10.))),
2185                            )
2186                            .on_click({
2187                                let action_log = action_log.clone();
2188                                cx.listener(move |_, _, _, cx| {
2189                                    action_log.update(cx, |action_log, cx| {
2190                                        action_log.keep_all_edits(cx);
2191                                    })
2192                                })
2193                            }),
2194                    ),
2195            )
2196    }
2197
2198    fn render_edited_files(
2199        &self,
2200        action_log: &Entity<ActionLog>,
2201        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
2202        pending_edits: bool,
2203        cx: &Context<Self>,
2204    ) -> Div {
2205        let editor_bg_color = cx.theme().colors().editor_background;
2206
2207        v_flex().children(changed_buffers.into_iter().enumerate().flat_map(
2208            |(index, (buffer, _diff))| {
2209                let file = buffer.read(cx).file()?;
2210                let path = file.path();
2211
2212                let file_path = path.parent().and_then(|parent| {
2213                    let parent_str = parent.to_string_lossy();
2214
2215                    if parent_str.is_empty() {
2216                        None
2217                    } else {
2218                        Some(
2219                            Label::new(format!("/{}{}", parent_str, std::path::MAIN_SEPARATOR_STR))
2220                                .color(Color::Muted)
2221                                .size(LabelSize::XSmall)
2222                                .buffer_font(cx),
2223                        )
2224                    }
2225                });
2226
2227                let file_name = path.file_name().map(|name| {
2228                    Label::new(name.to_string_lossy().to_string())
2229                        .size(LabelSize::XSmall)
2230                        .buffer_font(cx)
2231                });
2232
2233                let file_icon = FileIcons::get_icon(&path, cx)
2234                    .map(Icon::from_path)
2235                    .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
2236                    .unwrap_or_else(|| {
2237                        Icon::new(IconName::File)
2238                            .color(Color::Muted)
2239                            .size(IconSize::Small)
2240                    });
2241
2242                let overlay_gradient = linear_gradient(
2243                    90.,
2244                    linear_color_stop(editor_bg_color, 1.),
2245                    linear_color_stop(editor_bg_color.opacity(0.2), 0.),
2246                );
2247
2248                let element = h_flex()
2249                    .group("edited-code")
2250                    .id(("file-container", index))
2251                    .relative()
2252                    .py_1()
2253                    .pl_2()
2254                    .pr_1()
2255                    .gap_2()
2256                    .justify_between()
2257                    .bg(editor_bg_color)
2258                    .when(index < changed_buffers.len() - 1, |parent| {
2259                        parent.border_color(cx.theme().colors().border).border_b_1()
2260                    })
2261                    .child(
2262                        h_flex()
2263                            .id(("file-name", index))
2264                            .pr_8()
2265                            .gap_1p5()
2266                            .max_w_full()
2267                            .overflow_x_scroll()
2268                            .child(file_icon)
2269                            .child(h_flex().gap_0p5().children(file_name).children(file_path))
2270                            .on_click({
2271                                let buffer = buffer.clone();
2272                                cx.listener(move |this, _, window, cx| {
2273                                    this.open_edited_buffer(&buffer, window, cx);
2274                                })
2275                            }),
2276                    )
2277                    .child(
2278                        h_flex()
2279                            .gap_1()
2280                            .visible_on_hover("edited-code")
2281                            .child(
2282                                Button::new("review", "Review")
2283                                    .label_size(LabelSize::Small)
2284                                    .on_click({
2285                                        let buffer = buffer.clone();
2286                                        cx.listener(move |this, _, window, cx| {
2287                                            this.open_edited_buffer(&buffer, window, cx);
2288                                        })
2289                                    }),
2290                            )
2291                            .child(Divider::vertical().color(DividerColor::BorderVariant))
2292                            .child(
2293                                Button::new("reject-file", "Reject")
2294                                    .label_size(LabelSize::Small)
2295                                    .disabled(pending_edits)
2296                                    .on_click({
2297                                        let buffer = buffer.clone();
2298                                        let action_log = action_log.clone();
2299                                        move |_, _, cx| {
2300                                            action_log.update(cx, |action_log, cx| {
2301                                                action_log
2302                                                    .reject_edits_in_ranges(
2303                                                        buffer.clone(),
2304                                                        vec![Anchor::MIN..Anchor::MAX],
2305                                                        cx,
2306                                                    )
2307                                                    .detach_and_log_err(cx);
2308                                            })
2309                                        }
2310                                    }),
2311                            )
2312                            .child(
2313                                Button::new("keep-file", "Keep")
2314                                    .label_size(LabelSize::Small)
2315                                    .disabled(pending_edits)
2316                                    .on_click({
2317                                        let buffer = buffer.clone();
2318                                        let action_log = action_log.clone();
2319                                        move |_, _, cx| {
2320                                            action_log.update(cx, |action_log, cx| {
2321                                                action_log.keep_edits_in_range(
2322                                                    buffer.clone(),
2323                                                    Anchor::MIN..Anchor::MAX,
2324                                                    cx,
2325                                                );
2326                                            })
2327                                        }
2328                                    }),
2329                            ),
2330                    )
2331                    .child(
2332                        div()
2333                            .id("gradient-overlay")
2334                            .absolute()
2335                            .h_full()
2336                            .w_12()
2337                            .top_0()
2338                            .bottom_0()
2339                            .right(px(152.))
2340                            .bg(overlay_gradient),
2341                    );
2342
2343                Some(element)
2344            },
2345        ))
2346    }
2347
2348    fn render_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
2349        let focus_handle = self.message_editor.focus_handle(cx);
2350        let editor_bg_color = cx.theme().colors().editor_background;
2351        let (expand_icon, expand_tooltip) = if self.editor_expanded {
2352            (IconName::Minimize, "Minimize Message Editor")
2353        } else {
2354            (IconName::Maximize, "Expand Message Editor")
2355        };
2356
2357        v_flex()
2358            .on_action(cx.listener(Self::expand_message_editor))
2359            .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
2360                if let Some(model_selector) = this.model_selector.as_ref() {
2361                    model_selector
2362                        .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
2363                }
2364            }))
2365            .p_2()
2366            .gap_2()
2367            .border_t_1()
2368            .border_color(cx.theme().colors().border)
2369            .bg(editor_bg_color)
2370            .when(self.editor_expanded, |this| {
2371                this.h(vh(0.8, window)).size_full().justify_between()
2372            })
2373            .child(
2374                v_flex()
2375                    .relative()
2376                    .size_full()
2377                    .pt_1()
2378                    .pr_2p5()
2379                    .child(self.message_editor.clone())
2380                    .child(
2381                        h_flex()
2382                            .absolute()
2383                            .top_0()
2384                            .right_0()
2385                            .opacity(0.5)
2386                            .hover(|this| this.opacity(1.0))
2387                            .child(
2388                                IconButton::new("toggle-height", expand_icon)
2389                                    .icon_size(IconSize::Small)
2390                                    .icon_color(Color::Muted)
2391                                    .tooltip({
2392                                        let focus_handle = focus_handle.clone();
2393                                        move |window, cx| {
2394                                            Tooltip::for_action_in(
2395                                                expand_tooltip,
2396                                                &ExpandMessageEditor,
2397                                                &focus_handle,
2398                                                window,
2399                                                cx,
2400                                            )
2401                                        }
2402                                    })
2403                                    .on_click(cx.listener(|_, _, window, cx| {
2404                                        window.dispatch_action(Box::new(ExpandMessageEditor), cx);
2405                                    })),
2406                            ),
2407                    ),
2408            )
2409            .child(
2410                h_flex()
2411                    .flex_none()
2412                    .justify_between()
2413                    .child(self.render_follow_toggle(cx))
2414                    .child(
2415                        h_flex()
2416                            .gap_1()
2417                            .children(self.model_selector.clone())
2418                            .child(self.render_send_button(cx)),
2419                    ),
2420            )
2421            .into_any()
2422    }
2423
2424    fn start_editing_message(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
2425        let Some(thread) = self.thread() else {
2426            return;
2427        };
2428        let Some(AgentThreadEntry::UserMessage(message)) = thread.read(cx).entries().get(index)
2429        else {
2430            return;
2431        };
2432        let Some(message_id) = message.id.clone() else {
2433            return;
2434        };
2435        let chunks = message.chunks.clone();
2436        let editor = cx.new(|cx| {
2437            let mut editor = MessageEditor::new(
2438                self.workspace.clone(),
2439                self.project.clone(),
2440                editor::EditorMode::AutoHeight {
2441                    min_lines: 1,
2442                    max_lines: None,
2443                },
2444                window,
2445                cx,
2446            );
2447            editor.set_message(&chunks, window, cx);
2448            editor
2449        });
2450        let subscription =
2451            cx.subscribe_in(&editor, window, |this, _, event, window, cx| match event {
2452                MessageEditorEvent::Send => {
2453                    this.regenerate(&Default::default(), window, cx);
2454                }
2455                MessageEditorEvent::Cancel => {
2456                    this.cancel_editing(&Default::default(), window, cx);
2457                }
2458            });
2459
2460        self.editing_message.replace(EditingMessage {
2461            message_id: message_id.clone(),
2462            editor,
2463            _subscription: subscription,
2464        });
2465        cx.notify();
2466    }
2467
2468    fn render_edit_message_editor(&self, editing: &EditingMessage, cx: &Context<Self>) -> Div {
2469        v_flex().child(editing.editor.clone()).child(
2470            h_flex()
2471                .gap_1()
2472                .child(Icon::new(IconName::Warning))
2473                .child(
2474                    Label::new("Editing will restart the thread from this point.")
2475                        .color(Color::Muted)
2476                        .size(LabelSize::XSmall),
2477                )
2478                .child(self.render_editing_message_editor_buttons(editing, cx)),
2479        )
2480    }
2481
2482    fn render_editing_message_editor_buttons(
2483        &self,
2484        editing: &EditingMessage,
2485        cx: &Context<Self>,
2486    ) -> Div {
2487        h_flex()
2488            .gap_0p5()
2489            .child(
2490                IconButton::new("cancel-edit-message", IconName::Close)
2491                    .shape(ui::IconButtonShape::Square)
2492                    .icon_color(Color::Error)
2493                    .icon_size(IconSize::Small)
2494                    .tooltip({
2495                        let focus_handle = editing.editor.focus_handle(cx);
2496                        move |window, cx| {
2497                            Tooltip::for_action_in(
2498                                "Cancel Edit",
2499                                &menu::Cancel,
2500                                &focus_handle,
2501                                window,
2502                                cx,
2503                            )
2504                        }
2505                    })
2506                    .on_click(cx.listener(Self::cancel_editing)),
2507            )
2508            .child(
2509                IconButton::new("confirm-edit-message", IconName::Return)
2510                    .disabled(editing.editor.read(cx).is_empty(cx))
2511                    .shape(ui::IconButtonShape::Square)
2512                    .icon_color(Color::Muted)
2513                    .icon_size(IconSize::Small)
2514                    .tooltip({
2515                        let focus_handle = editing.editor.focus_handle(cx);
2516                        move |window, cx| {
2517                            Tooltip::for_action_in(
2518                                "Regenerate",
2519                                &menu::Confirm,
2520                                &focus_handle,
2521                                window,
2522                                cx,
2523                            )
2524                        }
2525                    })
2526                    .on_click(cx.listener(Self::regenerate)),
2527            )
2528    }
2529
2530    fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
2531        if self.thread().map_or(true, |thread| {
2532            thread.read(cx).status() == ThreadStatus::Idle
2533        }) {
2534            let is_editor_empty = self.message_editor.read(cx).is_empty(cx);
2535            IconButton::new("send-message", IconName::Send)
2536                .icon_color(Color::Accent)
2537                .style(ButtonStyle::Filled)
2538                .disabled(self.thread().is_none() || is_editor_empty)
2539                .when(!is_editor_empty, |button| {
2540                    button.tooltip(move |window, cx| Tooltip::for_action("Send", &Chat, window, cx))
2541                })
2542                .when(is_editor_empty, |button| {
2543                    button.tooltip(Tooltip::text("Type a message to submit"))
2544                })
2545                .on_click(cx.listener(|this, _, window, cx| {
2546                    this.send(window, cx);
2547                }))
2548                .into_any_element()
2549        } else {
2550            IconButton::new("stop-generation", IconName::Stop)
2551                .icon_color(Color::Error)
2552                .style(ButtonStyle::Tinted(ui::TintColor::Error))
2553                .tooltip(move |window, cx| {
2554                    Tooltip::for_action("Stop Generation", &editor::actions::Cancel, window, cx)
2555                })
2556                .on_click(cx.listener(|this, _event, _, cx| this.cancel_generation(cx)))
2557                .into_any_element()
2558        }
2559    }
2560
2561    fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
2562        let following = self
2563            .workspace
2564            .read_with(cx, |workspace, _| {
2565                workspace.is_being_followed(CollaboratorId::Agent)
2566            })
2567            .unwrap_or(false);
2568
2569        IconButton::new("follow-agent", IconName::Crosshair)
2570            .icon_size(IconSize::Small)
2571            .icon_color(Color::Muted)
2572            .toggle_state(following)
2573            .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
2574            .tooltip(move |window, cx| {
2575                if following {
2576                    Tooltip::for_action("Stop Following Agent", &Follow, window, cx)
2577                } else {
2578                    Tooltip::with_meta(
2579                        "Follow Agent",
2580                        Some(&Follow),
2581                        "Track the agent's location as it reads and edits files.",
2582                        window,
2583                        cx,
2584                    )
2585                }
2586            })
2587            .on_click(cx.listener(move |this, _, window, cx| {
2588                this.workspace
2589                    .update(cx, |workspace, cx| {
2590                        if following {
2591                            workspace.unfollow(CollaboratorId::Agent, window, cx);
2592                        } else {
2593                            workspace.follow(CollaboratorId::Agent, window, cx);
2594                        }
2595                    })
2596                    .ok();
2597            }))
2598    }
2599
2600    fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
2601        let workspace = self.workspace.clone();
2602        MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
2603            Self::open_link(text, &workspace, window, cx);
2604        })
2605    }
2606
2607    fn open_link(
2608        url: SharedString,
2609        workspace: &WeakEntity<Workspace>,
2610        window: &mut Window,
2611        cx: &mut App,
2612    ) {
2613        let Some(workspace) = workspace.upgrade() else {
2614            cx.open_url(&url);
2615            return;
2616        };
2617
2618        if let Some(mention) = MentionUri::parse(&url).log_err() {
2619            workspace.update(cx, |workspace, cx| match mention {
2620                MentionUri::File(path) => {
2621                    let project = workspace.project();
2622                    let Some((path, entry)) = project.update(cx, |project, cx| {
2623                        let path = project.find_project_path(path, cx)?;
2624                        let entry = project.entry_for_path(&path, cx)?;
2625                        Some((path, entry))
2626                    }) else {
2627                        return;
2628                    };
2629
2630                    if entry.is_dir() {
2631                        project.update(cx, |_, cx| {
2632                            cx.emit(project::Event::RevealInProjectPanel(entry.id));
2633                        });
2634                    } else {
2635                        workspace
2636                            .open_path(path, None, true, window, cx)
2637                            .detach_and_log_err(cx);
2638                    }
2639                }
2640                _ => {
2641                    // TODO
2642                    unimplemented!()
2643                }
2644            })
2645        } else {
2646            cx.open_url(&url);
2647        }
2648    }
2649
2650    fn open_tool_call_location(
2651        &self,
2652        entry_ix: usize,
2653        location_ix: usize,
2654        window: &mut Window,
2655        cx: &mut Context<Self>,
2656    ) -> Option<()> {
2657        let (tool_call_location, agent_location) = self
2658            .thread()?
2659            .read(cx)
2660            .entries()
2661            .get(entry_ix)?
2662            .location(location_ix)?;
2663
2664        let project_path = self
2665            .project
2666            .read(cx)
2667            .find_project_path(&tool_call_location.path, cx)?;
2668
2669        let open_task = self
2670            .workspace
2671            .update(cx, |workspace, cx| {
2672                workspace.open_path(project_path, None, true, window, cx)
2673            })
2674            .log_err()?;
2675        window
2676            .spawn(cx, async move |cx| {
2677                let item = open_task.await?;
2678
2679                let Some(active_editor) = item.downcast::<Editor>() else {
2680                    return anyhow::Ok(());
2681                };
2682
2683                active_editor.update_in(cx, |editor, window, cx| {
2684                    let multibuffer = editor.buffer().read(cx);
2685                    let buffer = multibuffer.as_singleton();
2686                    if agent_location.buffer.upgrade() == buffer {
2687                        let excerpt_id = multibuffer.excerpt_ids().first().cloned();
2688                        let anchor = editor::Anchor::in_buffer(
2689                            excerpt_id.unwrap(),
2690                            buffer.unwrap().read(cx).remote_id(),
2691                            agent_location.position,
2692                        );
2693                        editor.change_selections(Default::default(), window, cx, |selections| {
2694                            selections.select_anchor_ranges([anchor..anchor]);
2695                        })
2696                    } else {
2697                        let row = tool_call_location.line.unwrap_or_default();
2698                        editor.change_selections(Default::default(), window, cx, |selections| {
2699                            selections.select_ranges([Point::new(row, 0)..Point::new(row, 0)]);
2700                        })
2701                    }
2702                })?;
2703
2704                anyhow::Ok(())
2705            })
2706            .detach_and_log_err(cx);
2707
2708        None
2709    }
2710
2711    pub fn open_thread_as_markdown(
2712        &self,
2713        workspace: Entity<Workspace>,
2714        window: &mut Window,
2715        cx: &mut App,
2716    ) -> Task<anyhow::Result<()>> {
2717        let markdown_language_task = workspace
2718            .read(cx)
2719            .app_state()
2720            .languages
2721            .language_for_name("Markdown");
2722
2723        let (thread_summary, markdown) = if let Some(thread) = self.thread() {
2724            let thread = thread.read(cx);
2725            (thread.title().to_string(), thread.to_markdown(cx))
2726        } else {
2727            return Task::ready(Ok(()));
2728        };
2729
2730        window.spawn(cx, async move |cx| {
2731            let markdown_language = markdown_language_task.await?;
2732
2733            workspace.update_in(cx, |workspace, window, cx| {
2734                let project = workspace.project().clone();
2735
2736                if !project.read(cx).is_local() {
2737                    anyhow::bail!("failed to open active thread as markdown in remote project");
2738                }
2739
2740                let buffer = project.update(cx, |project, cx| {
2741                    project.create_local_buffer(&markdown, Some(markdown_language), cx)
2742                });
2743                let buffer = cx.new(|cx| {
2744                    MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
2745                });
2746
2747                workspace.add_item_to_active_pane(
2748                    Box::new(cx.new(|cx| {
2749                        let mut editor =
2750                            Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
2751                        editor.set_breadcrumb_header(thread_summary);
2752                        editor
2753                    })),
2754                    None,
2755                    true,
2756                    window,
2757                    cx,
2758                );
2759
2760                anyhow::Ok(())
2761            })??;
2762            anyhow::Ok(())
2763        })
2764    }
2765
2766    fn scroll_to_top(&mut self, cx: &mut Context<Self>) {
2767        self.list_state.scroll_to(ListOffset::default());
2768        cx.notify();
2769    }
2770
2771    pub fn scroll_to_bottom(&mut self, cx: &mut Context<Self>) {
2772        if let Some(thread) = self.thread() {
2773            let entry_count = thread.read(cx).entries().len();
2774            self.list_state.reset(entry_count);
2775            cx.notify();
2776        }
2777    }
2778
2779    fn notify_with_sound(
2780        &mut self,
2781        caption: impl Into<SharedString>,
2782        icon: IconName,
2783        window: &mut Window,
2784        cx: &mut Context<Self>,
2785    ) {
2786        self.play_notification_sound(window, cx);
2787        self.show_notification(caption, icon, window, cx);
2788    }
2789
2790    fn play_notification_sound(&self, window: &Window, cx: &mut App) {
2791        let settings = AgentSettings::get_global(cx);
2792        if settings.play_sound_when_agent_done && !window.is_window_active() {
2793            Audio::play_sound(Sound::AgentDone, cx);
2794        }
2795    }
2796
2797    fn show_notification(
2798        &mut self,
2799        caption: impl Into<SharedString>,
2800        icon: IconName,
2801        window: &mut Window,
2802        cx: &mut Context<Self>,
2803    ) {
2804        if window.is_window_active() || !self.notifications.is_empty() {
2805            return;
2806        }
2807
2808        let title = self.title(cx);
2809
2810        match AgentSettings::get_global(cx).notify_when_agent_waiting {
2811            NotifyWhenAgentWaiting::PrimaryScreen => {
2812                if let Some(primary) = cx.primary_display() {
2813                    self.pop_up(icon, caption.into(), title, window, primary, cx);
2814                }
2815            }
2816            NotifyWhenAgentWaiting::AllScreens => {
2817                let caption = caption.into();
2818                for screen in cx.displays() {
2819                    self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx);
2820                }
2821            }
2822            NotifyWhenAgentWaiting::Never => {
2823                // Don't show anything
2824            }
2825        }
2826    }
2827
2828    fn pop_up(
2829        &mut self,
2830        icon: IconName,
2831        caption: SharedString,
2832        title: SharedString,
2833        window: &mut Window,
2834        screen: Rc<dyn PlatformDisplay>,
2835        cx: &mut Context<Self>,
2836    ) {
2837        let options = AgentNotification::window_options(screen, cx);
2838
2839        let project_name = self.workspace.upgrade().and_then(|workspace| {
2840            workspace
2841                .read(cx)
2842                .project()
2843                .read(cx)
2844                .visible_worktrees(cx)
2845                .next()
2846                .map(|worktree| worktree.read(cx).root_name().to_string())
2847        });
2848
2849        if let Some(screen_window) = cx
2850            .open_window(options, |_, cx| {
2851                cx.new(|_| {
2852                    AgentNotification::new(title.clone(), caption.clone(), icon, project_name)
2853                })
2854            })
2855            .log_err()
2856        {
2857            if let Some(pop_up) = screen_window.entity(cx).log_err() {
2858                self.notification_subscriptions
2859                    .entry(screen_window)
2860                    .or_insert_with(Vec::new)
2861                    .push(cx.subscribe_in(&pop_up, window, {
2862                        |this, _, event, window, cx| match event {
2863                            AgentNotificationEvent::Accepted => {
2864                                let handle = window.window_handle();
2865                                cx.activate(true);
2866
2867                                let workspace_handle = this.workspace.clone();
2868
2869                                // If there are multiple Zed windows, activate the correct one.
2870                                cx.defer(move |cx| {
2871                                    handle
2872                                        .update(cx, |_view, window, _cx| {
2873                                            window.activate_window();
2874
2875                                            if let Some(workspace) = workspace_handle.upgrade() {
2876                                                workspace.update(_cx, |workspace, cx| {
2877                                                    workspace.focus_panel::<AgentPanel>(window, cx);
2878                                                });
2879                                            }
2880                                        })
2881                                        .log_err();
2882                                });
2883
2884                                this.dismiss_notifications(cx);
2885                            }
2886                            AgentNotificationEvent::Dismissed => {
2887                                this.dismiss_notifications(cx);
2888                            }
2889                        }
2890                    }));
2891
2892                self.notifications.push(screen_window);
2893
2894                // If the user manually refocuses the original window, dismiss the popup.
2895                self.notification_subscriptions
2896                    .entry(screen_window)
2897                    .or_insert_with(Vec::new)
2898                    .push({
2899                        let pop_up_weak = pop_up.downgrade();
2900
2901                        cx.observe_window_activation(window, move |_, window, cx| {
2902                            if window.is_window_active() {
2903                                if let Some(pop_up) = pop_up_weak.upgrade() {
2904                                    pop_up.update(cx, |_, cx| {
2905                                        cx.emit(AgentNotificationEvent::Dismissed);
2906                                    });
2907                                }
2908                            }
2909                        })
2910                    });
2911            }
2912        }
2913    }
2914
2915    fn dismiss_notifications(&mut self, cx: &mut Context<Self>) {
2916        for window in self.notifications.drain(..) {
2917            window
2918                .update(cx, |_, window, _| {
2919                    window.remove_window();
2920                })
2921                .ok();
2922
2923            self.notification_subscriptions.remove(&window);
2924        }
2925    }
2926
2927    fn render_thread_controls(&self, cx: &Context<Self>) -> impl IntoElement {
2928        let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
2929            .shape(ui::IconButtonShape::Square)
2930            .icon_size(IconSize::Small)
2931            .icon_color(Color::Ignored)
2932            .tooltip(Tooltip::text("Open Thread as Markdown"))
2933            .on_click(cx.listener(move |this, _, window, cx| {
2934                if let Some(workspace) = this.workspace.upgrade() {
2935                    this.open_thread_as_markdown(workspace, window, cx)
2936                        .detach_and_log_err(cx);
2937                }
2938            }));
2939
2940        let scroll_to_top = IconButton::new("scroll_to_top", IconName::ArrowUp)
2941            .shape(ui::IconButtonShape::Square)
2942            .icon_size(IconSize::Small)
2943            .icon_color(Color::Ignored)
2944            .tooltip(Tooltip::text("Scroll To Top"))
2945            .on_click(cx.listener(move |this, _, _, cx| {
2946                this.scroll_to_top(cx);
2947            }));
2948
2949        h_flex()
2950            .w_full()
2951            .mr_1()
2952            .pb_2()
2953            .px(RESPONSE_PADDING_X)
2954            .opacity(0.4)
2955            .hover(|style| style.opacity(1.))
2956            .flex_wrap()
2957            .justify_end()
2958            .child(open_as_markdown)
2959            .child(scroll_to_top)
2960    }
2961
2962    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
2963        div()
2964            .id("acp-thread-scrollbar")
2965            .occlude()
2966            .on_mouse_move(cx.listener(|_, _, _, cx| {
2967                cx.notify();
2968                cx.stop_propagation()
2969            }))
2970            .on_hover(|_, _, cx| {
2971                cx.stop_propagation();
2972            })
2973            .on_any_mouse_down(|_, _, cx| {
2974                cx.stop_propagation();
2975            })
2976            .on_mouse_up(
2977                MouseButton::Left,
2978                cx.listener(|_, _, _, cx| {
2979                    cx.stop_propagation();
2980                }),
2981            )
2982            .on_scroll_wheel(cx.listener(|_, _, _, cx| {
2983                cx.notify();
2984            }))
2985            .h_full()
2986            .absolute()
2987            .right_1()
2988            .top_1()
2989            .bottom_0()
2990            .w(px(12.))
2991            .cursor_default()
2992            .children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx)))
2993    }
2994
2995    fn settings_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
2996        for diff_editor in self.diff_editors.values() {
2997            diff_editor.update(cx, |diff_editor, cx| {
2998                diff_editor.set_text_style_refinement(diff_editor_text_style_refinement(cx));
2999                cx.notify();
3000            })
3001        }
3002    }
3003
3004    pub(crate) fn insert_dragged_files(
3005        &self,
3006        paths: Vec<project::ProjectPath>,
3007        _added_worktrees: Vec<Entity<project::Worktree>>,
3008        window: &mut Window,
3009        cx: &mut Context<Self>,
3010    ) {
3011        self.message_editor.update(cx, |message_editor, cx| {
3012            message_editor.insert_dragged_files(paths, window, cx);
3013        })
3014    }
3015}
3016
3017impl Focusable for AcpThreadView {
3018    fn focus_handle(&self, cx: &App) -> FocusHandle {
3019        self.message_editor.focus_handle(cx)
3020    }
3021}
3022
3023impl Render for AcpThreadView {
3024    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3025        let has_messages = self.list_state.item_count() > 0;
3026
3027        v_flex()
3028            .size_full()
3029            .key_context("AcpThread")
3030            .on_action(cx.listener(Self::open_agent_diff))
3031            .bg(cx.theme().colors().panel_background)
3032            .child(match &self.thread_state {
3033                ThreadState::Unauthenticated { connection } => v_flex()
3034                    .p_2()
3035                    .flex_1()
3036                    .items_center()
3037                    .justify_center()
3038                    .child(self.render_pending_auth_state())
3039                    .child(h_flex().mt_1p5().justify_center().children(
3040                        connection.auth_methods().into_iter().map(|method| {
3041                            Button::new(
3042                                SharedString::from(method.id.0.clone()),
3043                                method.name.clone(),
3044                            )
3045                            .on_click({
3046                                let method_id = method.id.clone();
3047                                cx.listener(move |this, _, window, cx| {
3048                                    this.authenticate(method_id.clone(), window, cx)
3049                                })
3050                            })
3051                        }),
3052                    )),
3053                ThreadState::Loading { .. } => v_flex().flex_1().child(self.render_empty_state(cx)),
3054                ThreadState::LoadError(e) => v_flex()
3055                    .p_2()
3056                    .flex_1()
3057                    .items_center()
3058                    .justify_center()
3059                    .child(self.render_load_error(e, cx)),
3060                ThreadState::ServerExited { status } => v_flex()
3061                    .p_2()
3062                    .flex_1()
3063                    .items_center()
3064                    .justify_center()
3065                    .child(self.render_server_exited(*status, cx)),
3066                ThreadState::Ready { thread, .. } => {
3067                    let thread_clone = thread.clone();
3068
3069                    v_flex().flex_1().map(|this| {
3070                        if has_messages {
3071                            this.child(
3072                                list(
3073                                    self.list_state.clone(),
3074                                    cx.processor(|this, index: usize, window, cx| {
3075                                        let Some((entry, len)) = this.thread().and_then(|thread| {
3076                                            let entries = &thread.read(cx).entries();
3077                                            Some((entries.get(index)?, entries.len()))
3078                                        }) else {
3079                                            return Empty.into_any();
3080                                        };
3081                                        this.render_entry(index, len, entry, window, cx)
3082                                    }),
3083                                )
3084                                .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
3085                                .flex_grow()
3086                                .into_any(),
3087                            )
3088                            .child(self.render_vertical_scrollbar(cx))
3089                            .children(
3090                                match thread_clone.read(cx).status() {
3091                                    ThreadStatus::Idle
3092                                    | ThreadStatus::WaitingForToolConfirmation => None,
3093                                    ThreadStatus::Generating => div()
3094                                        .px_5()
3095                                        .py_2()
3096                                        .child(LoadingLabel::new("").size(LabelSize::Small))
3097                                        .into(),
3098                                },
3099                            )
3100                        } else {
3101                            this.child(self.render_empty_state(cx))
3102                        }
3103                    })
3104                }
3105            })
3106            // The activity bar is intentionally rendered outside of the ThreadState::Ready match
3107            // above so that the scrollbar doesn't render behind it. The current setup allows
3108            // the scrollbar to stop exactly at the activity bar start.
3109            .when(has_messages, |this| match &self.thread_state {
3110                ThreadState::Ready { thread, .. } => {
3111                    this.children(self.render_activity_bar(thread, window, cx))
3112                }
3113                _ => this,
3114            })
3115            .when_some(self.last_error.clone(), |el, error| {
3116                el.child(
3117                    div()
3118                        .p_2()
3119                        .text_xs()
3120                        .border_t_1()
3121                        .border_color(cx.theme().colors().border)
3122                        .bg(cx.theme().status().error_background)
3123                        .child(
3124                            self.render_markdown(error, default_markdown_style(false, window, cx)),
3125                        ),
3126                )
3127            })
3128            .child(self.render_message_editor(window, cx))
3129    }
3130}
3131
3132fn user_message_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
3133    let mut style = default_markdown_style(false, window, cx);
3134    let mut text_style = window.text_style();
3135    let theme_settings = ThemeSettings::get_global(cx);
3136
3137    let buffer_font = theme_settings.buffer_font.family.clone();
3138    let buffer_font_size = TextSize::Small.rems(cx);
3139
3140    text_style.refine(&TextStyleRefinement {
3141        font_family: Some(buffer_font),
3142        font_size: Some(buffer_font_size.into()),
3143        ..Default::default()
3144    });
3145
3146    style.base_text_style = text_style;
3147    style.link_callback = Some(Rc::new(move |url, cx| {
3148        if MentionUri::parse(url).is_ok() {
3149            let colors = cx.theme().colors();
3150            Some(TextStyleRefinement {
3151                background_color: Some(colors.element_background),
3152                ..Default::default()
3153            })
3154        } else {
3155            None
3156        }
3157    }));
3158    style
3159}
3160
3161fn default_markdown_style(buffer_font: bool, window: &Window, cx: &App) -> MarkdownStyle {
3162    let theme_settings = ThemeSettings::get_global(cx);
3163    let colors = cx.theme().colors();
3164
3165    let buffer_font_size = TextSize::Small.rems(cx);
3166
3167    let mut text_style = window.text_style();
3168    let line_height = buffer_font_size * 1.75;
3169
3170    let font_family = if buffer_font {
3171        theme_settings.buffer_font.family.clone()
3172    } else {
3173        theme_settings.ui_font.family.clone()
3174    };
3175
3176    let font_size = if buffer_font {
3177        TextSize::Small.rems(cx)
3178    } else {
3179        TextSize::Default.rems(cx)
3180    };
3181
3182    text_style.refine(&TextStyleRefinement {
3183        font_family: Some(font_family),
3184        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
3185        font_features: Some(theme_settings.ui_font.features.clone()),
3186        font_size: Some(font_size.into()),
3187        line_height: Some(line_height.into()),
3188        color: Some(cx.theme().colors().text),
3189        ..Default::default()
3190    });
3191
3192    MarkdownStyle {
3193        base_text_style: text_style.clone(),
3194        syntax: cx.theme().syntax().clone(),
3195        selection_background_color: cx.theme().colors().element_selection_background,
3196        code_block_overflow_x_scroll: true,
3197        table_overflow_x_scroll: true,
3198        heading_level_styles: Some(HeadingLevelStyles {
3199            h1: Some(TextStyleRefinement {
3200                font_size: Some(rems(1.15).into()),
3201                ..Default::default()
3202            }),
3203            h2: Some(TextStyleRefinement {
3204                font_size: Some(rems(1.1).into()),
3205                ..Default::default()
3206            }),
3207            h3: Some(TextStyleRefinement {
3208                font_size: Some(rems(1.05).into()),
3209                ..Default::default()
3210            }),
3211            h4: Some(TextStyleRefinement {
3212                font_size: Some(rems(1.).into()),
3213                ..Default::default()
3214            }),
3215            h5: Some(TextStyleRefinement {
3216                font_size: Some(rems(0.95).into()),
3217                ..Default::default()
3218            }),
3219            h6: Some(TextStyleRefinement {
3220                font_size: Some(rems(0.875).into()),
3221                ..Default::default()
3222            }),
3223        }),
3224        code_block: StyleRefinement {
3225            padding: EdgesRefinement {
3226                top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
3227                left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
3228                right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
3229                bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
3230            },
3231            margin: EdgesRefinement {
3232                top: Some(Length::Definite(Pixels(8.).into())),
3233                left: Some(Length::Definite(Pixels(0.).into())),
3234                right: Some(Length::Definite(Pixels(0.).into())),
3235                bottom: Some(Length::Definite(Pixels(12.).into())),
3236            },
3237            border_style: Some(BorderStyle::Solid),
3238            border_widths: EdgesRefinement {
3239                top: Some(AbsoluteLength::Pixels(Pixels(1.))),
3240                left: Some(AbsoluteLength::Pixels(Pixels(1.))),
3241                right: Some(AbsoluteLength::Pixels(Pixels(1.))),
3242                bottom: Some(AbsoluteLength::Pixels(Pixels(1.))),
3243            },
3244            border_color: Some(colors.border_variant),
3245            background: Some(colors.editor_background.into()),
3246            text: Some(TextStyleRefinement {
3247                font_family: Some(theme_settings.buffer_font.family.clone()),
3248                font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
3249                font_features: Some(theme_settings.buffer_font.features.clone()),
3250                font_size: Some(buffer_font_size.into()),
3251                ..Default::default()
3252            }),
3253            ..Default::default()
3254        },
3255        inline_code: TextStyleRefinement {
3256            font_family: Some(theme_settings.buffer_font.family.clone()),
3257            font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
3258            font_features: Some(theme_settings.buffer_font.features.clone()),
3259            font_size: Some(buffer_font_size.into()),
3260            background_color: Some(colors.editor_foreground.opacity(0.08)),
3261            ..Default::default()
3262        },
3263        link: TextStyleRefinement {
3264            background_color: Some(colors.editor_foreground.opacity(0.025)),
3265            underline: Some(UnderlineStyle {
3266                color: Some(colors.text_accent.opacity(0.5)),
3267                thickness: px(1.),
3268                ..Default::default()
3269            }),
3270            ..Default::default()
3271        },
3272        ..Default::default()
3273    }
3274}
3275
3276fn plan_label_markdown_style(
3277    status: &acp::PlanEntryStatus,
3278    window: &Window,
3279    cx: &App,
3280) -> MarkdownStyle {
3281    let default_md_style = default_markdown_style(false, window, cx);
3282
3283    MarkdownStyle {
3284        base_text_style: TextStyle {
3285            color: cx.theme().colors().text_muted,
3286            strikethrough: if matches!(status, acp::PlanEntryStatus::Completed) {
3287                Some(gpui::StrikethroughStyle {
3288                    thickness: px(1.),
3289                    color: Some(cx.theme().colors().text_muted.opacity(0.8)),
3290                })
3291            } else {
3292                None
3293            },
3294            ..default_md_style.base_text_style
3295        },
3296        ..default_md_style
3297    }
3298}
3299
3300fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement {
3301    TextStyleRefinement {
3302        font_size: Some(
3303            TextSize::Small
3304                .rems(cx)
3305                .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
3306                .into(),
3307        ),
3308        ..Default::default()
3309    }
3310}
3311
3312fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
3313    let default_md_style = default_markdown_style(true, window, cx);
3314
3315    MarkdownStyle {
3316        base_text_style: TextStyle {
3317            ..default_md_style.base_text_style
3318        },
3319        selection_background_color: cx.theme().colors().element_selection_background,
3320        ..Default::default()
3321    }
3322}
3323
3324#[cfg(test)]
3325pub(crate) mod tests {
3326    use std::{path::Path, sync::Arc};
3327
3328    use agent_client_protocol::SessionId;
3329    use editor::EditorSettings;
3330    use fs::FakeFs;
3331    use futures::future::try_join_all;
3332    use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
3333    use parking_lot::Mutex;
3334    use rand::Rng;
3335    use settings::SettingsStore;
3336
3337    use super::*;
3338
3339    #[gpui::test]
3340    async fn test_drop(cx: &mut TestAppContext) {
3341        init_test(cx);
3342
3343        let (thread_view, _cx) = setup_thread_view(StubAgentServer::default(), cx).await;
3344        let weak_view = thread_view.downgrade();
3345        drop(thread_view);
3346        assert!(!weak_view.is_upgradable());
3347    }
3348
3349    #[gpui::test]
3350    async fn test_notification_for_stop_event(cx: &mut TestAppContext) {
3351        init_test(cx);
3352
3353        let (thread_view, cx) = setup_thread_view(StubAgentServer::default(), cx).await;
3354
3355        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
3356        message_editor.update_in(cx, |editor, window, cx| {
3357            editor.set_text("Hello", window, cx);
3358        });
3359
3360        cx.deactivate_window();
3361
3362        thread_view.update_in(cx, |thread_view, window, cx| {
3363            thread_view.send(window, cx);
3364        });
3365
3366        cx.run_until_parked();
3367
3368        assert!(
3369            cx.windows()
3370                .iter()
3371                .any(|window| window.downcast::<AgentNotification>().is_some())
3372        );
3373    }
3374
3375    #[gpui::test]
3376    async fn test_notification_for_error(cx: &mut TestAppContext) {
3377        init_test(cx);
3378
3379        let (thread_view, cx) =
3380            setup_thread_view(StubAgentServer::new(SaboteurAgentConnection), cx).await;
3381
3382        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
3383        message_editor.update_in(cx, |editor, window, cx| {
3384            editor.set_text("Hello", window, cx);
3385        });
3386
3387        cx.deactivate_window();
3388
3389        thread_view.update_in(cx, |thread_view, window, cx| {
3390            thread_view.send(window, cx);
3391        });
3392
3393        cx.run_until_parked();
3394
3395        assert!(
3396            cx.windows()
3397                .iter()
3398                .any(|window| window.downcast::<AgentNotification>().is_some())
3399        );
3400    }
3401
3402    #[gpui::test]
3403    async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) {
3404        init_test(cx);
3405
3406        let tool_call_id = acp::ToolCallId("1".into());
3407        let tool_call = acp::ToolCall {
3408            id: tool_call_id.clone(),
3409            title: "Label".into(),
3410            kind: acp::ToolKind::Edit,
3411            status: acp::ToolCallStatus::Pending,
3412            content: vec!["hi".into()],
3413            locations: vec![],
3414            raw_input: None,
3415            raw_output: None,
3416        };
3417        let connection = StubAgentConnection::new(vec![acp::SessionUpdate::ToolCall(tool_call)])
3418            .with_permission_requests(HashMap::from_iter([(
3419                tool_call_id,
3420                vec![acp::PermissionOption {
3421                    id: acp::PermissionOptionId("1".into()),
3422                    name: "Allow".into(),
3423                    kind: acp::PermissionOptionKind::AllowOnce,
3424                }],
3425            )]));
3426        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
3427
3428        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
3429        message_editor.update_in(cx, |editor, window, cx| {
3430            editor.set_text("Hello", window, cx);
3431        });
3432
3433        cx.deactivate_window();
3434
3435        thread_view.update_in(cx, |thread_view, window, cx| {
3436            thread_view.send(window, cx);
3437        });
3438
3439        cx.run_until_parked();
3440
3441        assert!(
3442            cx.windows()
3443                .iter()
3444                .any(|window| window.downcast::<AgentNotification>().is_some())
3445        );
3446    }
3447
3448    async fn setup_thread_view(
3449        agent: impl AgentServer + 'static,
3450        cx: &mut TestAppContext,
3451    ) -> (Entity<AcpThreadView>, &mut VisualTestContext) {
3452        let fs = FakeFs::new(cx.executor());
3453        let project = Project::test(fs, [], cx).await;
3454        let (workspace, cx) =
3455            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3456
3457        let thread_view = cx.update(|window, cx| {
3458            cx.new(|cx| {
3459                AcpThreadView::new(Rc::new(agent), workspace.downgrade(), project, window, cx)
3460            })
3461        });
3462        cx.run_until_parked();
3463        (thread_view, cx)
3464    }
3465
3466    struct StubAgentServer<C> {
3467        connection: C,
3468    }
3469
3470    impl<C> StubAgentServer<C> {
3471        fn new(connection: C) -> Self {
3472            Self { connection }
3473        }
3474    }
3475
3476    impl StubAgentServer<StubAgentConnection> {
3477        fn default() -> Self {
3478            Self::new(StubAgentConnection::default())
3479        }
3480    }
3481
3482    impl<C> AgentServer for StubAgentServer<C>
3483    where
3484        C: 'static + AgentConnection + Send + Clone,
3485    {
3486        fn logo(&self) -> ui::IconName {
3487            unimplemented!()
3488        }
3489
3490        fn name(&self) -> &'static str {
3491            unimplemented!()
3492        }
3493
3494        fn empty_state_headline(&self) -> &'static str {
3495            unimplemented!()
3496        }
3497
3498        fn empty_state_message(&self) -> &'static str {
3499            unimplemented!()
3500        }
3501
3502        fn connect(
3503            &self,
3504            _root_dir: &Path,
3505            _project: &Entity<Project>,
3506            _cx: &mut App,
3507        ) -> Task<gpui::Result<Rc<dyn AgentConnection>>> {
3508            Task::ready(Ok(Rc::new(self.connection.clone())))
3509        }
3510    }
3511
3512    #[derive(Clone, Default)]
3513    struct StubAgentConnection {
3514        sessions: Arc<Mutex<HashMap<acp::SessionId, WeakEntity<AcpThread>>>>,
3515        permission_requests: HashMap<acp::ToolCallId, Vec<acp::PermissionOption>>,
3516        updates: Vec<acp::SessionUpdate>,
3517    }
3518
3519    impl StubAgentConnection {
3520        fn new(updates: Vec<acp::SessionUpdate>) -> Self {
3521            Self {
3522                updates,
3523                permission_requests: HashMap::default(),
3524                sessions: Arc::default(),
3525            }
3526        }
3527
3528        fn with_permission_requests(
3529            mut self,
3530            permission_requests: HashMap<acp::ToolCallId, Vec<acp::PermissionOption>>,
3531        ) -> Self {
3532            self.permission_requests = permission_requests;
3533            self
3534        }
3535    }
3536
3537    impl AgentConnection for StubAgentConnection {
3538        fn auth_methods(&self) -> &[acp::AuthMethod] {
3539            &[]
3540        }
3541
3542        fn new_thread(
3543            self: Rc<Self>,
3544            project: Entity<Project>,
3545            _cwd: &Path,
3546            cx: &mut gpui::AsyncApp,
3547        ) -> Task<gpui::Result<Entity<AcpThread>>> {
3548            let session_id = SessionId(
3549                rand::thread_rng()
3550                    .sample_iter(&rand::distributions::Alphanumeric)
3551                    .take(7)
3552                    .map(char::from)
3553                    .collect::<String>()
3554                    .into(),
3555            );
3556            let thread = cx
3557                .new(|cx| AcpThread::new("Test", self.clone(), project, session_id.clone(), cx))
3558                .unwrap();
3559            self.sessions.lock().insert(session_id, thread.downgrade());
3560            Task::ready(Ok(thread))
3561        }
3562
3563        fn authenticate(
3564            &self,
3565            _method_id: acp::AuthMethodId,
3566            _cx: &mut App,
3567        ) -> Task<gpui::Result<()>> {
3568            unimplemented!()
3569        }
3570
3571        fn prompt(
3572            &self,
3573            _id: Option<acp_thread::UserMessageId>,
3574            params: acp::PromptRequest,
3575            cx: &mut App,
3576        ) -> Task<gpui::Result<acp::PromptResponse>> {
3577            let sessions = self.sessions.lock();
3578            let thread = sessions.get(&params.session_id).unwrap();
3579            let mut tasks = vec![];
3580            for update in &self.updates {
3581                let thread = thread.clone();
3582                let update = update.clone();
3583                let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) = &update
3584                    && let Some(options) = self.permission_requests.get(&tool_call.id)
3585                {
3586                    Some((tool_call.clone(), options.clone()))
3587                } else {
3588                    None
3589                };
3590                let task = cx.spawn(async move |cx| {
3591                    if let Some((tool_call, options)) = permission_request {
3592                        let permission = thread.update(cx, |thread, cx| {
3593                            thread.request_tool_call_authorization(
3594                                tool_call.clone(),
3595                                options.clone(),
3596                                cx,
3597                            )
3598                        })?;
3599                        permission.await?;
3600                    }
3601                    thread.update(cx, |thread, cx| {
3602                        thread.handle_session_update(update.clone(), cx).unwrap();
3603                    })?;
3604                    anyhow::Ok(())
3605                });
3606                tasks.push(task);
3607            }
3608            cx.spawn(async move |_| {
3609                try_join_all(tasks).await?;
3610                Ok(acp::PromptResponse {
3611                    stop_reason: acp::StopReason::EndTurn,
3612                })
3613            })
3614        }
3615
3616        fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
3617            unimplemented!()
3618        }
3619    }
3620
3621    #[derive(Clone)]
3622    struct SaboteurAgentConnection;
3623
3624    impl AgentConnection for SaboteurAgentConnection {
3625        fn new_thread(
3626            self: Rc<Self>,
3627            project: Entity<Project>,
3628            _cwd: &Path,
3629            cx: &mut gpui::AsyncApp,
3630        ) -> Task<gpui::Result<Entity<AcpThread>>> {
3631            Task::ready(Ok(cx
3632                .new(|cx| {
3633                    AcpThread::new(
3634                        "SaboteurAgentConnection",
3635                        self,
3636                        project,
3637                        SessionId("test".into()),
3638                        cx,
3639                    )
3640                })
3641                .unwrap()))
3642        }
3643
3644        fn auth_methods(&self) -> &[acp::AuthMethod] {
3645            &[]
3646        }
3647
3648        fn authenticate(
3649            &self,
3650            _method_id: acp::AuthMethodId,
3651            _cx: &mut App,
3652        ) -> Task<gpui::Result<()>> {
3653            unimplemented!()
3654        }
3655
3656        fn prompt(
3657            &self,
3658            _id: Option<acp_thread::UserMessageId>,
3659            _params: acp::PromptRequest,
3660            _cx: &mut App,
3661        ) -> Task<gpui::Result<acp::PromptResponse>> {
3662            Task::ready(Err(anyhow::anyhow!("Error prompting")))
3663        }
3664
3665        fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
3666            unimplemented!()
3667        }
3668    }
3669
3670    pub(crate) fn init_test(cx: &mut TestAppContext) {
3671        cx.update(|cx| {
3672            let settings_store = SettingsStore::test(cx);
3673            cx.set_global(settings_store);
3674            language::init(cx);
3675            Project::init_settings(cx);
3676            AgentSettings::register(cx);
3677            workspace::init_settings(cx);
3678            ThemeSettings::register(cx);
3679            release_channel::init(SemanticVersion::default(), cx);
3680            EditorSettings::register(cx);
3681        });
3682    }
3683}