thread_view.rs

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