thread_view.rs

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