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