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