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