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