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