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                                .border_b_1()
1282                                .border_color(self.tool_card_border_color(cx))
1283                                .bg(self.tool_card_header_bg(cx))
1284                        } else {
1285                            this.opacity(0.8).hover(|style| style.opacity(1.))
1286                        }
1287                    })
1288                    .child(
1289                        h_flex()
1290                            .group(&card_header_id)
1291                            .relative()
1292                            .w_full()
1293                            .text_size(self.tool_name_font_size())
1294                            .child(self.render_tool_call_icon(
1295                                card_header_id,
1296                                entry_ix,
1297                                is_collapsible,
1298                                is_open,
1299                                tool_call,
1300                                cx,
1301                            ))
1302                            .child(if tool_call.locations.len() == 1 {
1303                                let name = tool_call.locations[0]
1304                                    .path
1305                                    .file_name()
1306                                    .unwrap_or_default()
1307                                    .display()
1308                                    .to_string();
1309
1310                                h_flex()
1311                                    .id(("open-tool-call-location", entry_ix))
1312                                    .w_full()
1313                                    .max_w_full()
1314                                    .px_1p5()
1315                                    .rounded_sm()
1316                                    .overflow_x_scroll()
1317                                    .opacity(0.8)
1318                                    .hover(|label| {
1319                                        label.opacity(1.).bg(cx
1320                                            .theme()
1321                                            .colors()
1322                                            .element_hover
1323                                            .opacity(0.5))
1324                                    })
1325                                    .child(name)
1326                                    .tooltip(Tooltip::text("Jump to File"))
1327                                    .on_click(cx.listener(move |this, _, window, cx| {
1328                                        this.open_tool_call_location(entry_ix, 0, window, cx);
1329                                    }))
1330                                    .into_any_element()
1331                            } else {
1332                                h_flex()
1333                                    .id("non-card-label-container")
1334                                    .w_full()
1335                                    .relative()
1336                                    .ml_1p5()
1337                                    .overflow_hidden()
1338                                    .child(
1339                                        h_flex()
1340                                            .id("non-card-label")
1341                                            .pr_8()
1342                                            .w_full()
1343                                            .overflow_x_scroll()
1344                                            .child(self.render_markdown(
1345                                                tool_call.label.clone(),
1346                                                default_markdown_style(
1347                                                    needs_confirmation || is_edit || has_diff,
1348                                                    window,
1349                                                    cx,
1350                                                ),
1351                                            )),
1352                                    )
1353                                    .child(gradient_overlay(gradient_color))
1354                                    .on_click(cx.listener({
1355                                        let id = tool_call.id.clone();
1356                                        move |this: &mut Self, _, _, cx: &mut Context<Self>| {
1357                                            if is_open {
1358                                                this.expanded_tool_calls.remove(&id);
1359                                            } else {
1360                                                this.expanded_tool_calls.insert(id.clone());
1361                                            }
1362                                            cx.notify();
1363                                        }
1364                                    }))
1365                                    .into_any()
1366                            }),
1367                    )
1368                    .children(status_icon),
1369            )
1370            .when(is_open, |this| this.child(tool_output_display))
1371    }
1372
1373    fn render_tool_call_content(
1374        &self,
1375        content: &ToolCallContent,
1376        tool_call: &ToolCall,
1377        window: &Window,
1378        cx: &Context<Self>,
1379    ) -> AnyElement {
1380        match content {
1381            ToolCallContent::ContentBlock(content) => {
1382                if let Some(resource_link) = content.resource_link() {
1383                    self.render_resource_link(resource_link, cx)
1384                } else if let Some(markdown) = content.markdown() {
1385                    self.render_markdown_output(markdown.clone(), tool_call.id.clone(), window, cx)
1386                } else {
1387                    Empty.into_any_element()
1388                }
1389            }
1390            ToolCallContent::Diff(diff) => self.render_diff_editor(&diff.read(cx).multibuffer()),
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(&self, multibuffer: &Entity<MultiBuffer>) -> AnyElement {
1535        v_flex()
1536            .h_full()
1537            .child(
1538                if let Some(editor) = self.diff_editors.get(&multibuffer.entity_id()) {
1539                    editor.clone().into_any_element()
1540                } else {
1541                    Empty.into_any()
1542                },
1543            )
1544            .into_any()
1545    }
1546
1547    fn render_terminal_tool_call(
1548        &self,
1549        terminal: &Entity<acp_thread::Terminal>,
1550        tool_call: &ToolCall,
1551        window: &Window,
1552        cx: &Context<Self>,
1553    ) -> AnyElement {
1554        let terminal_data = terminal.read(cx);
1555        let working_dir = terminal_data.working_dir();
1556        let command = terminal_data.command();
1557        let started_at = terminal_data.started_at();
1558
1559        let tool_failed = matches!(
1560            &tool_call.status,
1561            ToolCallStatus::Rejected
1562                | ToolCallStatus::Canceled
1563                | ToolCallStatus::Allowed {
1564                    status: acp::ToolCallStatus::Failed,
1565                    ..
1566                }
1567        );
1568
1569        let output = terminal_data.output();
1570        let command_finished = output.is_some();
1571        let truncated_output = output.is_some_and(|output| output.was_content_truncated);
1572        let output_line_count = output.map(|output| output.content_line_count).unwrap_or(0);
1573
1574        let command_failed = command_finished
1575            && output.is_some_and(|o| o.exit_status.is_none_or(|status| !status.success()));
1576
1577        let time_elapsed = if let Some(output) = output {
1578            output.ended_at.duration_since(started_at)
1579        } else {
1580            started_at.elapsed()
1581        };
1582
1583        let header_bg = cx
1584            .theme()
1585            .colors()
1586            .element_background
1587            .blend(cx.theme().colors().editor_foreground.opacity(0.025));
1588        let border_color = cx.theme().colors().border.opacity(0.6);
1589
1590        let working_dir = working_dir
1591            .as_ref()
1592            .map(|path| format!("{}", path.display()))
1593            .unwrap_or_else(|| "current directory".to_string());
1594
1595        let header = h_flex()
1596            .id(SharedString::from(format!(
1597                "terminal-tool-header-{}",
1598                terminal.entity_id()
1599            )))
1600            .flex_none()
1601            .gap_1()
1602            .justify_between()
1603            .rounded_t_md()
1604            .child(
1605                div()
1606                    .id(("command-target-path", terminal.entity_id()))
1607                    .w_full()
1608                    .max_w_full()
1609                    .overflow_x_scroll()
1610                    .child(
1611                        Label::new(working_dir)
1612                            .buffer_font(cx)
1613                            .size(LabelSize::XSmall)
1614                            .color(Color::Muted),
1615                    ),
1616            )
1617            .when(!command_finished, |header| {
1618                header
1619                    .gap_1p5()
1620                    .child(
1621                        Button::new(
1622                            SharedString::from(format!("stop-terminal-{}", terminal.entity_id())),
1623                            "Stop",
1624                        )
1625                        .icon(IconName::Stop)
1626                        .icon_position(IconPosition::Start)
1627                        .icon_size(IconSize::Small)
1628                        .icon_color(Color::Error)
1629                        .label_size(LabelSize::Small)
1630                        .tooltip(move |window, cx| {
1631                            Tooltip::with_meta(
1632                                "Stop This Command",
1633                                None,
1634                                "Also possible by placing your cursor inside the terminal and using regular terminal bindings.",
1635                                window,
1636                                cx,
1637                            )
1638                        })
1639                        .on_click({
1640                            let terminal = terminal.clone();
1641                            cx.listener(move |_this, _event, _window, cx| {
1642                                let inner_terminal = terminal.read(cx).inner().clone();
1643                                inner_terminal.update(cx, |inner_terminal, _cx| {
1644                                    inner_terminal.kill_active_task();
1645                                });
1646                            })
1647                        }),
1648                    )
1649                    .child(Divider::vertical())
1650                    .child(
1651                        Icon::new(IconName::ArrowCircle)
1652                            .size(IconSize::XSmall)
1653                            .color(Color::Info)
1654                            .with_animation(
1655                                "arrow-circle",
1656                                Animation::new(Duration::from_secs(2)).repeat(),
1657                                |icon, delta| {
1658                                    icon.transform(Transformation::rotate(percentage(delta)))
1659                                },
1660                            ),
1661                    )
1662            })
1663            .when(tool_failed || command_failed, |header| {
1664                header.child(
1665                    div()
1666                        .id(("terminal-tool-error-code-indicator", terminal.entity_id()))
1667                        .child(
1668                            Icon::new(IconName::Close)
1669                                .size(IconSize::Small)
1670                                .color(Color::Error),
1671                        )
1672                        .when_some(output.and_then(|o| o.exit_status), |this, status| {
1673                            this.tooltip(Tooltip::text(format!(
1674                                "Exited with code {}",
1675                                status.code().unwrap_or(-1),
1676                            )))
1677                        }),
1678                )
1679            })
1680            .when(truncated_output, |header| {
1681                let tooltip = if let Some(output) = output {
1682                    if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
1683                        "Output exceeded terminal max lines and was \
1684                            truncated, the model received the first 16 KB."
1685                            .to_string()
1686                    } else {
1687                        format!(
1688                            "Output is {} long—to avoid unexpected token usage, \
1689                                only 16 KB was sent back to the model.",
1690                            format_file_size(output.original_content_len as u64, true),
1691                        )
1692                    }
1693                } else {
1694                    "Output was truncated".to_string()
1695                };
1696
1697                header.child(
1698                    h_flex()
1699                        .id(("terminal-tool-truncated-label", terminal.entity_id()))
1700                        .gap_1()
1701                        .child(
1702                            Icon::new(IconName::Info)
1703                                .size(IconSize::XSmall)
1704                                .color(Color::Ignored),
1705                        )
1706                        .child(
1707                            Label::new("Truncated")
1708                                .color(Color::Muted)
1709                                .size(LabelSize::XSmall),
1710                        )
1711                        .tooltip(Tooltip::text(tooltip)),
1712                )
1713            })
1714            .when(time_elapsed > Duration::from_secs(10), |header| {
1715                header.child(
1716                    Label::new(format!("({})", duration_alt_display(time_elapsed)))
1717                        .buffer_font(cx)
1718                        .color(Color::Muted)
1719                        .size(LabelSize::XSmall),
1720                )
1721            })
1722            .child(
1723                Disclosure::new(
1724                    SharedString::from(format!(
1725                        "terminal-tool-disclosure-{}",
1726                        terminal.entity_id()
1727                    )),
1728                    self.terminal_expanded,
1729                )
1730                .opened_icon(IconName::ChevronUp)
1731                .closed_icon(IconName::ChevronDown)
1732                .on_click(cx.listener(move |this, _event, _window, _cx| {
1733                    this.terminal_expanded = !this.terminal_expanded;
1734                })),
1735            );
1736
1737        let show_output =
1738            self.terminal_expanded && self.terminal_views.contains_key(&terminal.entity_id());
1739
1740        v_flex()
1741            .mb_2()
1742            .border_1()
1743            .when(tool_failed || command_failed, |card| card.border_dashed())
1744            .border_color(border_color)
1745            .rounded_lg()
1746            .overflow_hidden()
1747            .child(
1748                v_flex()
1749                    .pt_1()
1750                    .pb_2()
1751                    .px_2()
1752                    .gap_0p5()
1753                    .bg(header_bg)
1754                    .text_xs()
1755                    .child(header)
1756                    .child(
1757                        MarkdownElement::new(
1758                            command.clone(),
1759                            terminal_command_markdown_style(window, cx),
1760                        )
1761                        .code_block_renderer(
1762                            markdown::CodeBlockRenderer::Default {
1763                                copy_button: false,
1764                                copy_button_on_hover: true,
1765                                border: false,
1766                            },
1767                        ),
1768                    ),
1769            )
1770            .when(show_output, |this| {
1771                let terminal_view = self.terminal_views.get(&terminal.entity_id()).unwrap();
1772
1773                this.child(
1774                    div()
1775                        .pt_2()
1776                        .border_t_1()
1777                        .when(tool_failed || command_failed, |card| card.border_dashed())
1778                        .border_color(border_color)
1779                        .bg(cx.theme().colors().editor_background)
1780                        .rounded_b_md()
1781                        .text_ui_sm(cx)
1782                        .child(terminal_view.clone()),
1783                )
1784            })
1785            .into_any()
1786    }
1787
1788    fn render_agent_logo(&self) -> AnyElement {
1789        Icon::new(self.agent.logo())
1790            .color(Color::Muted)
1791            .size(IconSize::XLarge)
1792            .into_any_element()
1793    }
1794
1795    fn render_error_agent_logo(&self) -> AnyElement {
1796        let logo = Icon::new(self.agent.logo())
1797            .color(Color::Muted)
1798            .size(IconSize::XLarge)
1799            .into_any_element();
1800
1801        h_flex()
1802            .relative()
1803            .justify_center()
1804            .child(div().opacity(0.3).child(logo))
1805            .child(
1806                h_flex().absolute().right_1().bottom_0().child(
1807                    Icon::new(IconName::XCircle)
1808                        .color(Color::Error)
1809                        .size(IconSize::Small),
1810                ),
1811            )
1812            .into_any_element()
1813    }
1814
1815    fn render_empty_state(&self, cx: &App) -> AnyElement {
1816        let loading = matches!(&self.thread_state, ThreadState::Loading { .. });
1817
1818        v_flex()
1819            .size_full()
1820            .items_center()
1821            .justify_center()
1822            .child(if loading {
1823                h_flex()
1824                    .justify_center()
1825                    .child(self.render_agent_logo())
1826                    .with_animation(
1827                        "pulsating_icon",
1828                        Animation::new(Duration::from_secs(2))
1829                            .repeat()
1830                            .with_easing(pulsating_between(0.4, 1.0)),
1831                        |icon, delta| icon.opacity(delta),
1832                    )
1833                    .into_any()
1834            } else {
1835                self.render_agent_logo().into_any_element()
1836            })
1837            .child(h_flex().mt_4().mb_1().justify_center().child(if loading {
1838                div()
1839                    .child(LoadingLabel::new("").size(LabelSize::Large))
1840                    .into_any_element()
1841            } else {
1842                Headline::new(self.agent.empty_state_headline())
1843                    .size(HeadlineSize::Medium)
1844                    .into_any_element()
1845            }))
1846            .child(
1847                div()
1848                    .max_w_1_2()
1849                    .text_sm()
1850                    .text_center()
1851                    .map(|this| {
1852                        if loading {
1853                            this.invisible()
1854                        } else {
1855                            this.text_color(cx.theme().colors().text_muted)
1856                        }
1857                    })
1858                    .child(self.agent.empty_state_message()),
1859            )
1860            .into_any()
1861    }
1862
1863    fn render_pending_auth_state(&self) -> AnyElement {
1864        v_flex()
1865            .items_center()
1866            .justify_center()
1867            .child(self.render_error_agent_logo())
1868            .child(
1869                h_flex()
1870                    .mt_4()
1871                    .mb_1()
1872                    .justify_center()
1873                    .child(Headline::new("Not Authenticated").size(HeadlineSize::Medium)),
1874            )
1875            .into_any()
1876    }
1877
1878    fn render_server_exited(&self, status: ExitStatus, _cx: &Context<Self>) -> AnyElement {
1879        v_flex()
1880            .items_center()
1881            .justify_center()
1882            .child(self.render_error_agent_logo())
1883            .child(
1884                v_flex()
1885                    .mt_4()
1886                    .mb_2()
1887                    .gap_0p5()
1888                    .text_center()
1889                    .items_center()
1890                    .child(Headline::new("Server exited unexpectedly").size(HeadlineSize::Medium))
1891                    .child(
1892                        Label::new(format!("Exit status: {}", status.code().unwrap_or(-127)))
1893                            .size(LabelSize::Small)
1894                            .color(Color::Muted),
1895                    ),
1896            )
1897            .into_any_element()
1898    }
1899
1900    fn render_load_error(&self, e: &LoadError, cx: &Context<Self>) -> AnyElement {
1901        let mut container = v_flex()
1902            .items_center()
1903            .justify_center()
1904            .child(self.render_error_agent_logo())
1905            .child(
1906                v_flex()
1907                    .mt_4()
1908                    .mb_2()
1909                    .gap_0p5()
1910                    .text_center()
1911                    .items_center()
1912                    .child(Headline::new("Failed to launch").size(HeadlineSize::Medium))
1913                    .child(
1914                        Label::new(e.to_string())
1915                            .size(LabelSize::Small)
1916                            .color(Color::Muted),
1917                    ),
1918            );
1919
1920        if let LoadError::Unsupported {
1921            upgrade_message,
1922            upgrade_command,
1923            ..
1924        } = &e
1925        {
1926            let upgrade_message = upgrade_message.clone();
1927            let upgrade_command = upgrade_command.clone();
1928            container = container.child(Button::new("upgrade", upgrade_message).on_click(
1929                cx.listener(move |this, _, window, cx| {
1930                    this.workspace
1931                        .update(cx, |workspace, cx| {
1932                            let project = workspace.project().read(cx);
1933                            let cwd = project.first_project_directory(cx);
1934                            let shell = project.terminal_settings(&cwd, cx).shell.clone();
1935                            let spawn_in_terminal = task::SpawnInTerminal {
1936                                id: task::TaskId("install".to_string()),
1937                                full_label: upgrade_command.clone(),
1938                                label: upgrade_command.clone(),
1939                                command: Some(upgrade_command.clone()),
1940                                args: Vec::new(),
1941                                command_label: upgrade_command.clone(),
1942                                cwd,
1943                                env: Default::default(),
1944                                use_new_terminal: true,
1945                                allow_concurrent_runs: true,
1946                                reveal: Default::default(),
1947                                reveal_target: Default::default(),
1948                                hide: Default::default(),
1949                                shell,
1950                                show_summary: true,
1951                                show_command: true,
1952                                show_rerun: false,
1953                            };
1954                            workspace
1955                                .spawn_in_terminal(spawn_in_terminal, window, cx)
1956                                .detach();
1957                        })
1958                        .ok();
1959                }),
1960            ));
1961        }
1962
1963        container.into_any()
1964    }
1965
1966    fn render_activity_bar(
1967        &self,
1968        thread_entity: &Entity<AcpThread>,
1969        window: &mut Window,
1970        cx: &Context<Self>,
1971    ) -> Option<AnyElement> {
1972        let thread = thread_entity.read(cx);
1973        let action_log = thread.action_log();
1974        let changed_buffers = action_log.read(cx).changed_buffers(cx);
1975        let plan = thread.plan();
1976
1977        if changed_buffers.is_empty() && plan.is_empty() {
1978            return None;
1979        }
1980
1981        let editor_bg_color = cx.theme().colors().editor_background;
1982        let active_color = cx.theme().colors().element_selected;
1983        let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
1984
1985        let pending_edits = thread.has_pending_edit_tool_calls();
1986
1987        v_flex()
1988            .mt_1()
1989            .mx_2()
1990            .bg(bg_edit_files_disclosure)
1991            .border_1()
1992            .border_b_0()
1993            .border_color(cx.theme().colors().border)
1994            .rounded_t_md()
1995            .shadow(vec![gpui::BoxShadow {
1996                color: gpui::black().opacity(0.15),
1997                offset: point(px(1.), px(-1.)),
1998                blur_radius: px(3.),
1999                spread_radius: px(0.),
2000            }])
2001            .when(!plan.is_empty(), |this| {
2002                this.child(self.render_plan_summary(plan, window, cx))
2003                    .when(self.plan_expanded, |parent| {
2004                        parent.child(self.render_plan_entries(plan, window, cx))
2005                    })
2006            })
2007            .when(!changed_buffers.is_empty(), |this| {
2008                this.child(Divider::horizontal().color(DividerColor::Border))
2009                    .child(self.render_edits_summary(
2010                        action_log,
2011                        &changed_buffers,
2012                        self.edits_expanded,
2013                        pending_edits,
2014                        window,
2015                        cx,
2016                    ))
2017                    .when(self.edits_expanded, |parent| {
2018                        parent.child(self.render_edited_files(
2019                            action_log,
2020                            &changed_buffers,
2021                            pending_edits,
2022                            cx,
2023                        ))
2024                    })
2025            })
2026            .into_any()
2027            .into()
2028    }
2029
2030    fn render_plan_summary(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
2031        let stats = plan.stats();
2032
2033        let title = if let Some(entry) = stats.in_progress_entry
2034            && !self.plan_expanded
2035        {
2036            h_flex()
2037                .w_full()
2038                .cursor_default()
2039                .gap_1()
2040                .text_xs()
2041                .text_color(cx.theme().colors().text_muted)
2042                .justify_between()
2043                .child(
2044                    h_flex()
2045                        .gap_1()
2046                        .child(
2047                            Label::new("Current:")
2048                                .size(LabelSize::Small)
2049                                .color(Color::Muted),
2050                        )
2051                        .child(MarkdownElement::new(
2052                            entry.content.clone(),
2053                            plan_label_markdown_style(&entry.status, window, cx),
2054                        )),
2055                )
2056                .when(stats.pending > 0, |this| {
2057                    this.child(
2058                        Label::new(format!("{} left", stats.pending))
2059                            .size(LabelSize::Small)
2060                            .color(Color::Muted)
2061                            .mr_1(),
2062                    )
2063                })
2064        } else {
2065            let status_label = if stats.pending == 0 {
2066                "All Done".to_string()
2067            } else if stats.completed == 0 {
2068                format!("{} Tasks", plan.entries.len())
2069            } else {
2070                format!("{}/{}", stats.completed, plan.entries.len())
2071            };
2072
2073            h_flex()
2074                .w_full()
2075                .gap_1()
2076                .justify_between()
2077                .child(
2078                    Label::new("Plan")
2079                        .size(LabelSize::Small)
2080                        .color(Color::Muted),
2081                )
2082                .child(
2083                    Label::new(status_label)
2084                        .size(LabelSize::Small)
2085                        .color(Color::Muted)
2086                        .mr_1(),
2087                )
2088        };
2089
2090        h_flex()
2091            .p_1()
2092            .justify_between()
2093            .when(self.plan_expanded, |this| {
2094                this.border_b_1().border_color(cx.theme().colors().border)
2095            })
2096            .child(
2097                h_flex()
2098                    .id("plan_summary")
2099                    .w_full()
2100                    .gap_1()
2101                    .child(Disclosure::new("plan_disclosure", self.plan_expanded))
2102                    .child(title)
2103                    .on_click(cx.listener(|this, _, _, cx| {
2104                        this.plan_expanded = !this.plan_expanded;
2105                        cx.notify();
2106                    })),
2107            )
2108    }
2109
2110    fn render_plan_entries(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
2111        v_flex().children(plan.entries.iter().enumerate().flat_map(|(index, entry)| {
2112            let element = h_flex()
2113                .py_1()
2114                .px_2()
2115                .gap_2()
2116                .justify_between()
2117                .bg(cx.theme().colors().editor_background)
2118                .when(index < plan.entries.len() - 1, |parent| {
2119                    parent.border_color(cx.theme().colors().border).border_b_1()
2120                })
2121                .child(
2122                    h_flex()
2123                        .id(("plan_entry", index))
2124                        .gap_1p5()
2125                        .max_w_full()
2126                        .overflow_x_scroll()
2127                        .text_xs()
2128                        .text_color(cx.theme().colors().text_muted)
2129                        .child(match entry.status {
2130                            acp::PlanEntryStatus::Pending => Icon::new(IconName::TodoPending)
2131                                .size(IconSize::Small)
2132                                .color(Color::Muted)
2133                                .into_any_element(),
2134                            acp::PlanEntryStatus::InProgress => Icon::new(IconName::TodoProgress)
2135                                .size(IconSize::Small)
2136                                .color(Color::Accent)
2137                                .with_animation(
2138                                    "running",
2139                                    Animation::new(Duration::from_secs(2)).repeat(),
2140                                    |icon, delta| {
2141                                        icon.transform(Transformation::rotate(percentage(delta)))
2142                                    },
2143                                )
2144                                .into_any_element(),
2145                            acp::PlanEntryStatus::Completed => Icon::new(IconName::TodoComplete)
2146                                .size(IconSize::Small)
2147                                .color(Color::Success)
2148                                .into_any_element(),
2149                        })
2150                        .child(MarkdownElement::new(
2151                            entry.content.clone(),
2152                            plan_label_markdown_style(&entry.status, window, cx),
2153                        )),
2154                );
2155
2156            Some(element)
2157        }))
2158    }
2159
2160    fn render_edits_summary(
2161        &self,
2162        action_log: &Entity<ActionLog>,
2163        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
2164        expanded: bool,
2165        pending_edits: bool,
2166        window: &mut Window,
2167        cx: &Context<Self>,
2168    ) -> Div {
2169        const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete.";
2170
2171        let focus_handle = self.focus_handle(cx);
2172
2173        h_flex()
2174            .p_1()
2175            .justify_between()
2176            .when(expanded, |this| {
2177                this.border_b_1().border_color(cx.theme().colors().border)
2178            })
2179            .child(
2180                h_flex()
2181                    .id("edits-container")
2182                    .w_full()
2183                    .gap_1()
2184                    .child(Disclosure::new("edits-disclosure", expanded))
2185                    .map(|this| {
2186                        if pending_edits {
2187                            this.child(
2188                                Label::new(format!(
2189                                    "Editing {} {}",
2190                                    changed_buffers.len(),
2191                                    if changed_buffers.len() == 1 {
2192                                        "file"
2193                                    } else {
2194                                        "files"
2195                                    }
2196                                ))
2197                                .color(Color::Muted)
2198                                .size(LabelSize::Small)
2199                                .with_animation(
2200                                    "edit-label",
2201                                    Animation::new(Duration::from_secs(2))
2202                                        .repeat()
2203                                        .with_easing(pulsating_between(0.3, 0.7)),
2204                                    |label, delta| label.alpha(delta),
2205                                ),
2206                            )
2207                        } else {
2208                            this.child(
2209                                Label::new("Edits")
2210                                    .size(LabelSize::Small)
2211                                    .color(Color::Muted),
2212                            )
2213                            .child(Label::new("").size(LabelSize::XSmall).color(Color::Muted))
2214                            .child(
2215                                Label::new(format!(
2216                                    "{} {}",
2217                                    changed_buffers.len(),
2218                                    if changed_buffers.len() == 1 {
2219                                        "file"
2220                                    } else {
2221                                        "files"
2222                                    }
2223                                ))
2224                                .size(LabelSize::Small)
2225                                .color(Color::Muted),
2226                            )
2227                        }
2228                    })
2229                    .on_click(cx.listener(|this, _, _, cx| {
2230                        this.edits_expanded = !this.edits_expanded;
2231                        cx.notify();
2232                    })),
2233            )
2234            .child(
2235                h_flex()
2236                    .gap_1()
2237                    .child(
2238                        IconButton::new("review-changes", IconName::ListTodo)
2239                            .icon_size(IconSize::Small)
2240                            .tooltip({
2241                                let focus_handle = focus_handle.clone();
2242                                move |window, cx| {
2243                                    Tooltip::for_action_in(
2244                                        "Review Changes",
2245                                        &OpenAgentDiff,
2246                                        &focus_handle,
2247                                        window,
2248                                        cx,
2249                                    )
2250                                }
2251                            })
2252                            .on_click(cx.listener(|_, _, window, cx| {
2253                                window.dispatch_action(OpenAgentDiff.boxed_clone(), cx);
2254                            })),
2255                    )
2256                    .child(Divider::vertical().color(DividerColor::Border))
2257                    .child(
2258                        Button::new("reject-all-changes", "Reject All")
2259                            .label_size(LabelSize::Small)
2260                            .disabled(pending_edits)
2261                            .when(pending_edits, |this| {
2262                                this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
2263                            })
2264                            .key_binding(
2265                                KeyBinding::for_action_in(
2266                                    &RejectAll,
2267                                    &focus_handle.clone(),
2268                                    window,
2269                                    cx,
2270                                )
2271                                .map(|kb| kb.size(rems_from_px(10.))),
2272                            )
2273                            .on_click({
2274                                let action_log = action_log.clone();
2275                                cx.listener(move |_, _, _, cx| {
2276                                    action_log.update(cx, |action_log, cx| {
2277                                        action_log.reject_all_edits(cx).detach();
2278                                    })
2279                                })
2280                            }),
2281                    )
2282                    .child(
2283                        Button::new("keep-all-changes", "Keep All")
2284                            .label_size(LabelSize::Small)
2285                            .disabled(pending_edits)
2286                            .when(pending_edits, |this| {
2287                                this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
2288                            })
2289                            .key_binding(
2290                                KeyBinding::for_action_in(&KeepAll, &focus_handle, window, cx)
2291                                    .map(|kb| kb.size(rems_from_px(10.))),
2292                            )
2293                            .on_click({
2294                                let action_log = action_log.clone();
2295                                cx.listener(move |_, _, _, cx| {
2296                                    action_log.update(cx, |action_log, cx| {
2297                                        action_log.keep_all_edits(cx);
2298                                    })
2299                                })
2300                            }),
2301                    ),
2302            )
2303    }
2304
2305    fn render_edited_files(
2306        &self,
2307        action_log: &Entity<ActionLog>,
2308        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
2309        pending_edits: bool,
2310        cx: &Context<Self>,
2311    ) -> Div {
2312        let editor_bg_color = cx.theme().colors().editor_background;
2313
2314        v_flex().children(changed_buffers.into_iter().enumerate().flat_map(
2315            |(index, (buffer, _diff))| {
2316                let file = buffer.read(cx).file()?;
2317                let path = file.path();
2318
2319                let file_path = path.parent().and_then(|parent| {
2320                    let parent_str = parent.to_string_lossy();
2321
2322                    if parent_str.is_empty() {
2323                        None
2324                    } else {
2325                        Some(
2326                            Label::new(format!("/{}{}", parent_str, std::path::MAIN_SEPARATOR_STR))
2327                                .color(Color::Muted)
2328                                .size(LabelSize::XSmall)
2329                                .buffer_font(cx),
2330                        )
2331                    }
2332                });
2333
2334                let file_name = path.file_name().map(|name| {
2335                    Label::new(name.to_string_lossy().to_string())
2336                        .size(LabelSize::XSmall)
2337                        .buffer_font(cx)
2338                });
2339
2340                let file_icon = FileIcons::get_icon(&path, cx)
2341                    .map(Icon::from_path)
2342                    .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
2343                    .unwrap_or_else(|| {
2344                        Icon::new(IconName::File)
2345                            .color(Color::Muted)
2346                            .size(IconSize::Small)
2347                    });
2348
2349                let overlay_gradient = linear_gradient(
2350                    90.,
2351                    linear_color_stop(editor_bg_color, 1.),
2352                    linear_color_stop(editor_bg_color.opacity(0.2), 0.),
2353                );
2354
2355                let element = h_flex()
2356                    .group("edited-code")
2357                    .id(("file-container", index))
2358                    .relative()
2359                    .py_1()
2360                    .pl_2()
2361                    .pr_1()
2362                    .gap_2()
2363                    .justify_between()
2364                    .bg(editor_bg_color)
2365                    .when(index < changed_buffers.len() - 1, |parent| {
2366                        parent.border_color(cx.theme().colors().border).border_b_1()
2367                    })
2368                    .child(
2369                        h_flex()
2370                            .id(("file-name", index))
2371                            .pr_8()
2372                            .gap_1p5()
2373                            .max_w_full()
2374                            .overflow_x_scroll()
2375                            .child(file_icon)
2376                            .child(h_flex().gap_0p5().children(file_name).children(file_path))
2377                            .on_click({
2378                                let buffer = buffer.clone();
2379                                cx.listener(move |this, _, window, cx| {
2380                                    this.open_edited_buffer(&buffer, window, cx);
2381                                })
2382                            }),
2383                    )
2384                    .child(
2385                        h_flex()
2386                            .gap_1()
2387                            .visible_on_hover("edited-code")
2388                            .child(
2389                                Button::new("review", "Review")
2390                                    .label_size(LabelSize::Small)
2391                                    .on_click({
2392                                        let buffer = buffer.clone();
2393                                        cx.listener(move |this, _, window, cx| {
2394                                            this.open_edited_buffer(&buffer, window, cx);
2395                                        })
2396                                    }),
2397                            )
2398                            .child(Divider::vertical().color(DividerColor::BorderVariant))
2399                            .child(
2400                                Button::new("reject-file", "Reject")
2401                                    .label_size(LabelSize::Small)
2402                                    .disabled(pending_edits)
2403                                    .on_click({
2404                                        let buffer = buffer.clone();
2405                                        let action_log = action_log.clone();
2406                                        move |_, _, cx| {
2407                                            action_log.update(cx, |action_log, cx| {
2408                                                action_log
2409                                                    .reject_edits_in_ranges(
2410                                                        buffer.clone(),
2411                                                        vec![Anchor::MIN..Anchor::MAX],
2412                                                        cx,
2413                                                    )
2414                                                    .detach_and_log_err(cx);
2415                                            })
2416                                        }
2417                                    }),
2418                            )
2419                            .child(
2420                                Button::new("keep-file", "Keep")
2421                                    .label_size(LabelSize::Small)
2422                                    .disabled(pending_edits)
2423                                    .on_click({
2424                                        let buffer = buffer.clone();
2425                                        let action_log = action_log.clone();
2426                                        move |_, _, cx| {
2427                                            action_log.update(cx, |action_log, cx| {
2428                                                action_log.keep_edits_in_range(
2429                                                    buffer.clone(),
2430                                                    Anchor::MIN..Anchor::MAX,
2431                                                    cx,
2432                                                );
2433                                            })
2434                                        }
2435                                    }),
2436                            ),
2437                    )
2438                    .child(
2439                        div()
2440                            .id("gradient-overlay")
2441                            .absolute()
2442                            .h_full()
2443                            .w_12()
2444                            .top_0()
2445                            .bottom_0()
2446                            .right(px(152.))
2447                            .bg(overlay_gradient),
2448                    );
2449
2450                Some(element)
2451            },
2452        ))
2453    }
2454
2455    fn render_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
2456        let focus_handle = self.message_editor.focus_handle(cx);
2457        let editor_bg_color = cx.theme().colors().editor_background;
2458        let (expand_icon, expand_tooltip) = if self.editor_expanded {
2459            (IconName::Minimize, "Minimize Message Editor")
2460        } else {
2461            (IconName::Maximize, "Expand Message Editor")
2462        };
2463
2464        v_flex()
2465            .on_action(cx.listener(Self::expand_message_editor))
2466            .p_2()
2467            .gap_2()
2468            .border_t_1()
2469            .border_color(cx.theme().colors().border)
2470            .bg(editor_bg_color)
2471            .when(self.editor_expanded, |this| {
2472                this.h(vh(0.8, window)).size_full().justify_between()
2473            })
2474            .child(
2475                v_flex()
2476                    .relative()
2477                    .size_full()
2478                    .pt_1()
2479                    .pr_2p5()
2480                    .child(div().flex_1().child({
2481                        let settings = ThemeSettings::get_global(cx);
2482                        let font_size = TextSize::Small
2483                            .rems(cx)
2484                            .to_pixels(settings.agent_font_size(cx));
2485                        let line_height = settings.buffer_line_height.value() * font_size;
2486
2487                        let text_style = TextStyle {
2488                            color: cx.theme().colors().text,
2489                            font_family: settings.buffer_font.family.clone(),
2490                            font_fallbacks: settings.buffer_font.fallbacks.clone(),
2491                            font_features: settings.buffer_font.features.clone(),
2492                            font_size: font_size.into(),
2493                            line_height: line_height.into(),
2494                            ..Default::default()
2495                        };
2496
2497                        EditorElement::new(
2498                            &self.message_editor,
2499                            EditorStyle {
2500                                background: editor_bg_color,
2501                                local_player: cx.theme().players().local(),
2502                                text: text_style,
2503                                syntax: cx.theme().syntax().clone(),
2504                                ..Default::default()
2505                            },
2506                        )
2507                    }))
2508                    .child(
2509                        h_flex()
2510                            .absolute()
2511                            .top_0()
2512                            .right_0()
2513                            .opacity(0.5)
2514                            .hover(|this| this.opacity(1.0))
2515                            .child(
2516                                IconButton::new("toggle-height", expand_icon)
2517                                    .icon_size(IconSize::Small)
2518                                    .icon_color(Color::Muted)
2519                                    .tooltip({
2520                                        let focus_handle = focus_handle.clone();
2521                                        move |window, cx| {
2522                                            Tooltip::for_action_in(
2523                                                expand_tooltip,
2524                                                &ExpandMessageEditor,
2525                                                &focus_handle,
2526                                                window,
2527                                                cx,
2528                                            )
2529                                        }
2530                                    })
2531                                    .on_click(cx.listener(|_, _, window, cx| {
2532                                        window.dispatch_action(Box::new(ExpandMessageEditor), cx);
2533                                    })),
2534                            ),
2535                    ),
2536            )
2537            .child(
2538                h_flex()
2539                    .flex_none()
2540                    .justify_between()
2541                    .child(self.render_follow_toggle(cx))
2542                    .child(self.render_send_button(cx)),
2543            )
2544            .into_any()
2545    }
2546
2547    fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
2548        if self.thread().map_or(true, |thread| {
2549            thread.read(cx).status() == ThreadStatus::Idle
2550        }) {
2551            let is_editor_empty = self.message_editor.read(cx).is_empty(cx);
2552            IconButton::new("send-message", IconName::Send)
2553                .icon_color(Color::Accent)
2554                .style(ButtonStyle::Filled)
2555                .disabled(self.thread().is_none() || is_editor_empty)
2556                .when(!is_editor_empty, |button| {
2557                    button.tooltip(move |window, cx| Tooltip::for_action("Send", &Chat, window, cx))
2558                })
2559                .when(is_editor_empty, |button| {
2560                    button.tooltip(Tooltip::text("Type a message to submit"))
2561                })
2562                .on_click(cx.listener(|this, _, window, cx| {
2563                    this.chat(&Chat, window, cx);
2564                }))
2565                .into_any_element()
2566        } else {
2567            IconButton::new("stop-generation", IconName::Stop)
2568                .icon_color(Color::Error)
2569                .style(ButtonStyle::Tinted(ui::TintColor::Error))
2570                .tooltip(move |window, cx| {
2571                    Tooltip::for_action("Stop Generation", &editor::actions::Cancel, window, cx)
2572                })
2573                .on_click(cx.listener(|this, _event, _, cx| this.cancel(cx)))
2574                .into_any_element()
2575        }
2576    }
2577
2578    fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
2579        let following = self
2580            .workspace
2581            .read_with(cx, |workspace, _| {
2582                workspace.is_being_followed(CollaboratorId::Agent)
2583            })
2584            .unwrap_or(false);
2585
2586        IconButton::new("follow-agent", IconName::Crosshair)
2587            .icon_size(IconSize::Small)
2588            .icon_color(Color::Muted)
2589            .toggle_state(following)
2590            .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
2591            .tooltip(move |window, cx| {
2592                if following {
2593                    Tooltip::for_action("Stop Following Agent", &Follow, window, cx)
2594                } else {
2595                    Tooltip::with_meta(
2596                        "Follow Agent",
2597                        Some(&Follow),
2598                        "Track the agent's location as it reads and edits files.",
2599                        window,
2600                        cx,
2601                    )
2602                }
2603            })
2604            .on_click(cx.listener(move |this, _, window, cx| {
2605                this.workspace
2606                    .update(cx, |workspace, cx| {
2607                        if following {
2608                            workspace.unfollow(CollaboratorId::Agent, window, cx);
2609                        } else {
2610                            workspace.follow(CollaboratorId::Agent, window, cx);
2611                        }
2612                    })
2613                    .ok();
2614            }))
2615    }
2616
2617    fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
2618        let workspace = self.workspace.clone();
2619        MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
2620            Self::open_link(text, &workspace, window, cx);
2621        })
2622    }
2623
2624    fn open_link(
2625        url: SharedString,
2626        workspace: &WeakEntity<Workspace>,
2627        window: &mut Window,
2628        cx: &mut App,
2629    ) {
2630        let Some(workspace) = workspace.upgrade() else {
2631            cx.open_url(&url);
2632            return;
2633        };
2634
2635        if let Some(mention) = MentionUri::parse(&url).log_err() {
2636            workspace.update(cx, |workspace, cx| match mention {
2637                MentionUri::File(path) => {
2638                    let project = workspace.project();
2639                    let Some((path, entry)) = project.update(cx, |project, cx| {
2640                        let path = project.find_project_path(path, cx)?;
2641                        let entry = project.entry_for_path(&path, cx)?;
2642                        Some((path, entry))
2643                    }) else {
2644                        return;
2645                    };
2646
2647                    if entry.is_dir() {
2648                        project.update(cx, |_, cx| {
2649                            cx.emit(project::Event::RevealInProjectPanel(entry.id));
2650                        });
2651                    } else {
2652                        workspace
2653                            .open_path(path, None, true, window, cx)
2654                            .detach_and_log_err(cx);
2655                    }
2656                }
2657                _ => {
2658                    // TODO
2659                    unimplemented!()
2660                }
2661            })
2662        } else {
2663            cx.open_url(&url);
2664        }
2665    }
2666
2667    fn open_tool_call_location(
2668        &self,
2669        entry_ix: usize,
2670        location_ix: usize,
2671        window: &mut Window,
2672        cx: &mut Context<Self>,
2673    ) -> Option<()> {
2674        let location = self
2675            .thread()?
2676            .read(cx)
2677            .entries()
2678            .get(entry_ix)?
2679            .locations()?
2680            .get(location_ix)?;
2681
2682        let project_path = self
2683            .project
2684            .read(cx)
2685            .find_project_path(&location.path, cx)?;
2686
2687        let open_task = self
2688            .workspace
2689            .update(cx, |worskpace, cx| {
2690                worskpace.open_path(project_path, None, true, window, cx)
2691            })
2692            .log_err()?;
2693
2694        window
2695            .spawn(cx, async move |cx| {
2696                let item = open_task.await?;
2697
2698                let Some(active_editor) = item.downcast::<Editor>() else {
2699                    return anyhow::Ok(());
2700                };
2701
2702                active_editor.update_in(cx, |editor, window, cx| {
2703                    let snapshot = editor.buffer().read(cx).snapshot(cx);
2704                    let first_hunk = editor
2705                        .diff_hunks_in_ranges(
2706                            &[editor::Anchor::min()..editor::Anchor::max()],
2707                            &snapshot,
2708                        )
2709                        .next();
2710                    if let Some(first_hunk) = first_hunk {
2711                        let first_hunk_start = first_hunk.multi_buffer_range().start;
2712                        editor.change_selections(Default::default(), window, cx, |selections| {
2713                            selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
2714                        })
2715                    }
2716                })?;
2717
2718                anyhow::Ok(())
2719            })
2720            .detach_and_log_err(cx);
2721
2722        None
2723    }
2724
2725    pub fn open_thread_as_markdown(
2726        &self,
2727        workspace: Entity<Workspace>,
2728        window: &mut Window,
2729        cx: &mut App,
2730    ) -> Task<anyhow::Result<()>> {
2731        let markdown_language_task = workspace
2732            .read(cx)
2733            .app_state()
2734            .languages
2735            .language_for_name("Markdown");
2736
2737        let (thread_summary, markdown) = if let Some(thread) = self.thread() {
2738            let thread = thread.read(cx);
2739            (thread.title().to_string(), thread.to_markdown(cx))
2740        } else {
2741            return Task::ready(Ok(()));
2742        };
2743
2744        window.spawn(cx, async move |cx| {
2745            let markdown_language = markdown_language_task.await?;
2746
2747            workspace.update_in(cx, |workspace, window, cx| {
2748                let project = workspace.project().clone();
2749
2750                if !project.read(cx).is_local() {
2751                    anyhow::bail!("failed to open active thread as markdown in remote project");
2752                }
2753
2754                let buffer = project.update(cx, |project, cx| {
2755                    project.create_local_buffer(&markdown, Some(markdown_language), cx)
2756                });
2757                let buffer = cx.new(|cx| {
2758                    MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
2759                });
2760
2761                workspace.add_item_to_active_pane(
2762                    Box::new(cx.new(|cx| {
2763                        let mut editor =
2764                            Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
2765                        editor.set_breadcrumb_header(thread_summary);
2766                        editor
2767                    })),
2768                    None,
2769                    true,
2770                    window,
2771                    cx,
2772                );
2773
2774                anyhow::Ok(())
2775            })??;
2776            anyhow::Ok(())
2777        })
2778    }
2779
2780    fn scroll_to_top(&mut self, cx: &mut Context<Self>) {
2781        self.list_state.scroll_to(ListOffset::default());
2782        cx.notify();
2783    }
2784
2785    pub fn scroll_to_bottom(&mut self, cx: &mut Context<Self>) {
2786        if let Some(thread) = self.thread() {
2787            let entry_count = thread.read(cx).entries().len();
2788            self.list_state.reset(entry_count);
2789            cx.notify();
2790        }
2791    }
2792
2793    fn notify_with_sound(
2794        &mut self,
2795        caption: impl Into<SharedString>,
2796        icon: IconName,
2797        window: &mut Window,
2798        cx: &mut Context<Self>,
2799    ) {
2800        self.play_notification_sound(window, cx);
2801        self.show_notification(caption, icon, window, cx);
2802    }
2803
2804    fn play_notification_sound(&self, window: &Window, cx: &mut App) {
2805        let settings = AgentSettings::get_global(cx);
2806        if settings.play_sound_when_agent_done && !window.is_window_active() {
2807            Audio::play_sound(Sound::AgentDone, cx);
2808        }
2809    }
2810
2811    fn show_notification(
2812        &mut self,
2813        caption: impl Into<SharedString>,
2814        icon: IconName,
2815        window: &mut Window,
2816        cx: &mut Context<Self>,
2817    ) {
2818        if window.is_window_active() || !self.notifications.is_empty() {
2819            return;
2820        }
2821
2822        let title = self.title(cx);
2823
2824        match AgentSettings::get_global(cx).notify_when_agent_waiting {
2825            NotifyWhenAgentWaiting::PrimaryScreen => {
2826                if let Some(primary) = cx.primary_display() {
2827                    self.pop_up(icon, caption.into(), title, window, primary, cx);
2828                }
2829            }
2830            NotifyWhenAgentWaiting::AllScreens => {
2831                let caption = caption.into();
2832                for screen in cx.displays() {
2833                    self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx);
2834                }
2835            }
2836            NotifyWhenAgentWaiting::Never => {
2837                // Don't show anything
2838            }
2839        }
2840    }
2841
2842    fn pop_up(
2843        &mut self,
2844        icon: IconName,
2845        caption: SharedString,
2846        title: SharedString,
2847        window: &mut Window,
2848        screen: Rc<dyn PlatformDisplay>,
2849        cx: &mut Context<Self>,
2850    ) {
2851        let options = AgentNotification::window_options(screen, cx);
2852
2853        let project_name = self.workspace.upgrade().and_then(|workspace| {
2854            workspace
2855                .read(cx)
2856                .project()
2857                .read(cx)
2858                .visible_worktrees(cx)
2859                .next()
2860                .map(|worktree| worktree.read(cx).root_name().to_string())
2861        });
2862
2863        if let Some(screen_window) = cx
2864            .open_window(options, |_, cx| {
2865                cx.new(|_| {
2866                    AgentNotification::new(title.clone(), caption.clone(), icon, project_name)
2867                })
2868            })
2869            .log_err()
2870        {
2871            if let Some(pop_up) = screen_window.entity(cx).log_err() {
2872                self.notification_subscriptions
2873                    .entry(screen_window)
2874                    .or_insert_with(Vec::new)
2875                    .push(cx.subscribe_in(&pop_up, window, {
2876                        |this, _, event, window, cx| match event {
2877                            AgentNotificationEvent::Accepted => {
2878                                let handle = window.window_handle();
2879                                cx.activate(true);
2880
2881                                let workspace_handle = this.workspace.clone();
2882
2883                                // If there are multiple Zed windows, activate the correct one.
2884                                cx.defer(move |cx| {
2885                                    handle
2886                                        .update(cx, |_view, window, _cx| {
2887                                            window.activate_window();
2888
2889                                            if let Some(workspace) = workspace_handle.upgrade() {
2890                                                workspace.update(_cx, |workspace, cx| {
2891                                                    workspace.focus_panel::<AgentPanel>(window, cx);
2892                                                });
2893                                            }
2894                                        })
2895                                        .log_err();
2896                                });
2897
2898                                this.dismiss_notifications(cx);
2899                            }
2900                            AgentNotificationEvent::Dismissed => {
2901                                this.dismiss_notifications(cx);
2902                            }
2903                        }
2904                    }));
2905
2906                self.notifications.push(screen_window);
2907
2908                // If the user manually refocuses the original window, dismiss the popup.
2909                self.notification_subscriptions
2910                    .entry(screen_window)
2911                    .or_insert_with(Vec::new)
2912                    .push({
2913                        let pop_up_weak = pop_up.downgrade();
2914
2915                        cx.observe_window_activation(window, move |_, window, cx| {
2916                            if window.is_window_active() {
2917                                if let Some(pop_up) = pop_up_weak.upgrade() {
2918                                    pop_up.update(cx, |_, cx| {
2919                                        cx.emit(AgentNotificationEvent::Dismissed);
2920                                    });
2921                                }
2922                            }
2923                        })
2924                    });
2925            }
2926        }
2927    }
2928
2929    fn dismiss_notifications(&mut self, cx: &mut Context<Self>) {
2930        for window in self.notifications.drain(..) {
2931            window
2932                .update(cx, |_, window, _| {
2933                    window.remove_window();
2934                })
2935                .ok();
2936
2937            self.notification_subscriptions.remove(&window);
2938        }
2939    }
2940
2941    fn render_thread_controls(&self, cx: &Context<Self>) -> impl IntoElement {
2942        let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
2943            .icon_size(IconSize::XSmall)
2944            .icon_color(Color::Ignored)
2945            .tooltip(Tooltip::text("Open Thread as Markdown"))
2946            .on_click(cx.listener(move |this, _, window, cx| {
2947                if let Some(workspace) = this.workspace.upgrade() {
2948                    this.open_thread_as_markdown(workspace, window, cx)
2949                        .detach_and_log_err(cx);
2950                }
2951            }));
2952
2953        let scroll_to_top = IconButton::new("scroll_to_top", IconName::ArrowUp)
2954            .icon_size(IconSize::XSmall)
2955            .icon_color(Color::Ignored)
2956            .tooltip(Tooltip::text("Scroll To Top"))
2957            .on_click(cx.listener(move |this, _, _, cx| {
2958                this.scroll_to_top(cx);
2959            }));
2960
2961        h_flex()
2962            .w_full()
2963            .mr_1()
2964            .pb_2()
2965            .gap_1()
2966            .px(RESPONSE_PADDING_X)
2967            .opacity(0.4)
2968            .hover(|style| style.opacity(1.))
2969            .flex_wrap()
2970            .justify_end()
2971            .child(open_as_markdown)
2972            .child(scroll_to_top)
2973    }
2974
2975    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
2976        div()
2977            .id("acp-thread-scrollbar")
2978            .occlude()
2979            .on_mouse_move(cx.listener(|_, _, _, cx| {
2980                cx.notify();
2981                cx.stop_propagation()
2982            }))
2983            .on_hover(|_, _, cx| {
2984                cx.stop_propagation();
2985            })
2986            .on_any_mouse_down(|_, _, cx| {
2987                cx.stop_propagation();
2988            })
2989            .on_mouse_up(
2990                MouseButton::Left,
2991                cx.listener(|_, _, _, cx| {
2992                    cx.stop_propagation();
2993                }),
2994            )
2995            .on_scroll_wheel(cx.listener(|_, _, _, cx| {
2996                cx.notify();
2997            }))
2998            .h_full()
2999            .absolute()
3000            .right_1()
3001            .top_1()
3002            .bottom_0()
3003            .w(px(12.))
3004            .cursor_default()
3005            .children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx)))
3006    }
3007
3008    fn settings_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
3009        for diff_editor in self.diff_editors.values() {
3010            diff_editor.update(cx, |diff_editor, cx| {
3011                diff_editor.set_text_style_refinement(diff_editor_text_style_refinement(cx));
3012                cx.notify();
3013            })
3014        }
3015    }
3016
3017    pub(crate) fn insert_dragged_files(
3018        &self,
3019        paths: Vec<project::ProjectPath>,
3020        _added_worktrees: Vec<Entity<project::Worktree>>,
3021        window: &mut Window,
3022        cx: &mut Context<'_, Self>,
3023    ) {
3024        let buffer = self.message_editor.read(cx).buffer().clone();
3025        let Some((&excerpt_id, _, _)) = buffer.read(cx).snapshot(cx).as_singleton() else {
3026            return;
3027        };
3028        let Some(buffer) = buffer.read(cx).as_singleton() else {
3029            return;
3030        };
3031        for path in paths {
3032            let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else {
3033                continue;
3034            };
3035            let Some(abs_path) = self.project.read(cx).absolute_path(&path, cx) else {
3036                continue;
3037            };
3038
3039            let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
3040            let path_prefix = abs_path
3041                .file_name()
3042                .unwrap_or(path.path.as_os_str())
3043                .display()
3044                .to_string();
3045            let completion = ContextPickerCompletionProvider::completion_for_path(
3046                path,
3047                &path_prefix,
3048                false,
3049                entry.is_dir(),
3050                excerpt_id,
3051                anchor..anchor,
3052                self.message_editor.clone(),
3053                self.mention_set.clone(),
3054                self.project.clone(),
3055                cx,
3056            );
3057
3058            self.message_editor.update(cx, |message_editor, cx| {
3059                message_editor.edit(
3060                    [(
3061                        multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
3062                        completion.new_text,
3063                    )],
3064                    cx,
3065                );
3066            });
3067            if let Some(confirm) = completion.confirm.clone() {
3068                confirm(CompletionIntent::Complete, window, cx);
3069            }
3070        }
3071    }
3072}
3073
3074impl Focusable for AcpThreadView {
3075    fn focus_handle(&self, cx: &App) -> FocusHandle {
3076        self.message_editor.focus_handle(cx)
3077    }
3078}
3079
3080impl Render for AcpThreadView {
3081    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3082        v_flex()
3083            .size_full()
3084            .key_context("AcpThread")
3085            .on_action(cx.listener(Self::chat))
3086            .on_action(cx.listener(Self::previous_history_message))
3087            .on_action(cx.listener(Self::next_history_message))
3088            .on_action(cx.listener(Self::open_agent_diff))
3089            .bg(cx.theme().colors().panel_background)
3090            .child(match &self.thread_state {
3091                ThreadState::Unauthenticated { connection } => v_flex()
3092                    .p_2()
3093                    .flex_1()
3094                    .items_center()
3095                    .justify_center()
3096                    .child(self.render_pending_auth_state())
3097                    .child(h_flex().mt_1p5().justify_center().children(
3098                        connection.auth_methods().into_iter().map(|method| {
3099                            Button::new(
3100                                SharedString::from(method.id.0.clone()),
3101                                method.name.clone(),
3102                            )
3103                            .on_click({
3104                                let method_id = method.id.clone();
3105                                cx.listener(move |this, _, window, cx| {
3106                                    this.authenticate(method_id.clone(), window, cx)
3107                                })
3108                            })
3109                        }),
3110                    )),
3111                ThreadState::Loading { .. } => v_flex().flex_1().child(self.render_empty_state(cx)),
3112                ThreadState::LoadError(e) => v_flex()
3113                    .p_2()
3114                    .flex_1()
3115                    .items_center()
3116                    .justify_center()
3117                    .child(self.render_load_error(e, cx)),
3118                ThreadState::ServerExited { status } => v_flex()
3119                    .p_2()
3120                    .flex_1()
3121                    .items_center()
3122                    .justify_center()
3123                    .child(self.render_server_exited(*status, cx)),
3124                ThreadState::Ready { thread, .. } => {
3125                    let thread_clone = thread.clone();
3126
3127                    v_flex().flex_1().map(|this| {
3128                        if self.list_state.item_count() > 0 {
3129                            this.child(
3130                                list(
3131                                    self.list_state.clone(),
3132                                    cx.processor(|this, index: usize, window, cx| {
3133                                        let Some((entry, len)) = this.thread().and_then(|thread| {
3134                                            let entries = &thread.read(cx).entries();
3135                                            Some((entries.get(index)?, entries.len()))
3136                                        }) else {
3137                                            return Empty.into_any();
3138                                        };
3139                                        this.render_entry(index, len, entry, window, cx)
3140                                    }),
3141                                )
3142                                .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
3143                                .flex_grow()
3144                                .into_any(),
3145                            )
3146                            .child(self.render_vertical_scrollbar(cx))
3147                            .children(match thread_clone.read(cx).status() {
3148                                ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => {
3149                                    None
3150                                }
3151                                ThreadStatus::Generating => div()
3152                                    .px_5()
3153                                    .py_2()
3154                                    .child(LoadingLabel::new("").size(LabelSize::Small))
3155                                    .into(),
3156                            })
3157                            .children(self.render_activity_bar(&thread_clone, window, cx))
3158                        } else {
3159                            this.child(self.render_empty_state(cx))
3160                        }
3161                    })
3162                }
3163            })
3164            .when_some(self.last_error.clone(), |el, error| {
3165                el.child(
3166                    div()
3167                        .p_2()
3168                        .text_xs()
3169                        .border_t_1()
3170                        .border_color(cx.theme().colors().border)
3171                        .bg(cx.theme().status().error_background)
3172                        .child(
3173                            self.render_markdown(error, default_markdown_style(false, window, cx)),
3174                        ),
3175                )
3176            })
3177            .child(self.render_message_editor(window, cx))
3178    }
3179}
3180
3181fn user_message_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
3182    let mut style = default_markdown_style(false, window, cx);
3183    let mut text_style = window.text_style();
3184    let theme_settings = ThemeSettings::get_global(cx);
3185
3186    let buffer_font = theme_settings.buffer_font.family.clone();
3187    let buffer_font_size = TextSize::Small.rems(cx);
3188
3189    text_style.refine(&TextStyleRefinement {
3190        font_family: Some(buffer_font),
3191        font_size: Some(buffer_font_size.into()),
3192        ..Default::default()
3193    });
3194
3195    style.base_text_style = text_style;
3196    style.link_callback = Some(Rc::new(move |url, cx| {
3197        if MentionUri::parse(url).is_ok() {
3198            let colors = cx.theme().colors();
3199            Some(TextStyleRefinement {
3200                background_color: Some(colors.element_background),
3201                ..Default::default()
3202            })
3203        } else {
3204            None
3205        }
3206    }));
3207    style
3208}
3209
3210fn default_markdown_style(buffer_font: bool, window: &Window, cx: &App) -> MarkdownStyle {
3211    let theme_settings = ThemeSettings::get_global(cx);
3212    let colors = cx.theme().colors();
3213
3214    let buffer_font_size = TextSize::Small.rems(cx);
3215
3216    let mut text_style = window.text_style();
3217    let line_height = buffer_font_size * 1.75;
3218
3219    let font_family = if buffer_font {
3220        theme_settings.buffer_font.family.clone()
3221    } else {
3222        theme_settings.ui_font.family.clone()
3223    };
3224
3225    let font_size = if buffer_font {
3226        TextSize::Small.rems(cx)
3227    } else {
3228        TextSize::Default.rems(cx)
3229    };
3230
3231    text_style.refine(&TextStyleRefinement {
3232        font_family: Some(font_family),
3233        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
3234        font_features: Some(theme_settings.ui_font.features.clone()),
3235        font_size: Some(font_size.into()),
3236        line_height: Some(line_height.into()),
3237        color: Some(cx.theme().colors().text),
3238        ..Default::default()
3239    });
3240
3241    MarkdownStyle {
3242        base_text_style: text_style.clone(),
3243        syntax: cx.theme().syntax().clone(),
3244        selection_background_color: cx.theme().colors().element_selection_background,
3245        code_block_overflow_x_scroll: true,
3246        table_overflow_x_scroll: true,
3247        heading_level_styles: Some(HeadingLevelStyles {
3248            h1: Some(TextStyleRefinement {
3249                font_size: Some(rems(1.15).into()),
3250                ..Default::default()
3251            }),
3252            h2: Some(TextStyleRefinement {
3253                font_size: Some(rems(1.1).into()),
3254                ..Default::default()
3255            }),
3256            h3: Some(TextStyleRefinement {
3257                font_size: Some(rems(1.05).into()),
3258                ..Default::default()
3259            }),
3260            h4: Some(TextStyleRefinement {
3261                font_size: Some(rems(1.).into()),
3262                ..Default::default()
3263            }),
3264            h5: Some(TextStyleRefinement {
3265                font_size: Some(rems(0.95).into()),
3266                ..Default::default()
3267            }),
3268            h6: Some(TextStyleRefinement {
3269                font_size: Some(rems(0.875).into()),
3270                ..Default::default()
3271            }),
3272        }),
3273        code_block: StyleRefinement {
3274            padding: EdgesRefinement {
3275                top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
3276                left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
3277                right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
3278                bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
3279            },
3280            margin: EdgesRefinement {
3281                top: Some(Length::Definite(Pixels(8.).into())),
3282                left: Some(Length::Definite(Pixels(0.).into())),
3283                right: Some(Length::Definite(Pixels(0.).into())),
3284                bottom: Some(Length::Definite(Pixels(12.).into())),
3285            },
3286            border_style: Some(BorderStyle::Solid),
3287            border_widths: EdgesRefinement {
3288                top: Some(AbsoluteLength::Pixels(Pixels(1.))),
3289                left: Some(AbsoluteLength::Pixels(Pixels(1.))),
3290                right: Some(AbsoluteLength::Pixels(Pixels(1.))),
3291                bottom: Some(AbsoluteLength::Pixels(Pixels(1.))),
3292            },
3293            border_color: Some(colors.border_variant),
3294            background: Some(colors.editor_background.into()),
3295            text: Some(TextStyleRefinement {
3296                font_family: Some(theme_settings.buffer_font.family.clone()),
3297                font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
3298                font_features: Some(theme_settings.buffer_font.features.clone()),
3299                font_size: Some(buffer_font_size.into()),
3300                ..Default::default()
3301            }),
3302            ..Default::default()
3303        },
3304        inline_code: TextStyleRefinement {
3305            font_family: Some(theme_settings.buffer_font.family.clone()),
3306            font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
3307            font_features: Some(theme_settings.buffer_font.features.clone()),
3308            font_size: Some(buffer_font_size.into()),
3309            background_color: Some(colors.editor_foreground.opacity(0.08)),
3310            ..Default::default()
3311        },
3312        link: TextStyleRefinement {
3313            background_color: Some(colors.editor_foreground.opacity(0.025)),
3314            underline: Some(UnderlineStyle {
3315                color: Some(colors.text_accent.opacity(0.5)),
3316                thickness: px(1.),
3317                ..Default::default()
3318            }),
3319            ..Default::default()
3320        },
3321        ..Default::default()
3322    }
3323}
3324
3325fn plan_label_markdown_style(
3326    status: &acp::PlanEntryStatus,
3327    window: &Window,
3328    cx: &App,
3329) -> MarkdownStyle {
3330    let default_md_style = default_markdown_style(false, window, cx);
3331
3332    MarkdownStyle {
3333        base_text_style: TextStyle {
3334            color: cx.theme().colors().text_muted,
3335            strikethrough: if matches!(status, acp::PlanEntryStatus::Completed) {
3336                Some(gpui::StrikethroughStyle {
3337                    thickness: px(1.),
3338                    color: Some(cx.theme().colors().text_muted.opacity(0.8)),
3339                })
3340            } else {
3341                None
3342            },
3343            ..default_md_style.base_text_style
3344        },
3345        ..default_md_style
3346    }
3347}
3348
3349fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement {
3350    TextStyleRefinement {
3351        font_size: Some(
3352            TextSize::Small
3353                .rems(cx)
3354                .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
3355                .into(),
3356        ),
3357        ..Default::default()
3358    }
3359}
3360
3361fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
3362    let default_md_style = default_markdown_style(true, window, cx);
3363
3364    MarkdownStyle {
3365        base_text_style: TextStyle {
3366            ..default_md_style.base_text_style
3367        },
3368        selection_background_color: cx.theme().colors().element_selection_background,
3369        ..Default::default()
3370    }
3371}
3372
3373#[cfg(test)]
3374mod tests {
3375    use agent_client_protocol::SessionId;
3376    use editor::EditorSettings;
3377    use fs::FakeFs;
3378    use futures::future::try_join_all;
3379    use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
3380    use lsp::{CompletionContext, CompletionTriggerKind};
3381    use project::CompletionIntent;
3382    use rand::Rng;
3383    use serde_json::json;
3384    use settings::SettingsStore;
3385    use util::path;
3386
3387    use super::*;
3388
3389    #[gpui::test]
3390    async fn test_drop(cx: &mut TestAppContext) {
3391        init_test(cx);
3392
3393        let (thread_view, _cx) = setup_thread_view(StubAgentServer::default(), cx).await;
3394        let weak_view = thread_view.downgrade();
3395        drop(thread_view);
3396        assert!(!weak_view.is_upgradable());
3397    }
3398
3399    #[gpui::test]
3400    async fn test_notification_for_stop_event(cx: &mut TestAppContext) {
3401        init_test(cx);
3402
3403        let (thread_view, cx) = setup_thread_view(StubAgentServer::default(), cx).await;
3404
3405        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
3406        message_editor.update_in(cx, |editor, window, cx| {
3407            editor.set_text("Hello", window, cx);
3408        });
3409
3410        cx.deactivate_window();
3411
3412        thread_view.update_in(cx, |thread_view, window, cx| {
3413            thread_view.chat(&Chat, window, cx);
3414        });
3415
3416        cx.run_until_parked();
3417
3418        assert!(
3419            cx.windows()
3420                .iter()
3421                .any(|window| window.downcast::<AgentNotification>().is_some())
3422        );
3423    }
3424
3425    #[gpui::test]
3426    async fn test_notification_for_error(cx: &mut TestAppContext) {
3427        init_test(cx);
3428
3429        let (thread_view, cx) =
3430            setup_thread_view(StubAgentServer::new(SaboteurAgentConnection), cx).await;
3431
3432        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
3433        message_editor.update_in(cx, |editor, window, cx| {
3434            editor.set_text("Hello", window, cx);
3435        });
3436
3437        cx.deactivate_window();
3438
3439        thread_view.update_in(cx, |thread_view, window, cx| {
3440            thread_view.chat(&Chat, window, cx);
3441        });
3442
3443        cx.run_until_parked();
3444
3445        assert!(
3446            cx.windows()
3447                .iter()
3448                .any(|window| window.downcast::<AgentNotification>().is_some())
3449        );
3450    }
3451
3452    #[gpui::test]
3453    async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) {
3454        init_test(cx);
3455
3456        let tool_call_id = acp::ToolCallId("1".into());
3457        let tool_call = acp::ToolCall {
3458            id: tool_call_id.clone(),
3459            title: "Label".into(),
3460            kind: acp::ToolKind::Edit,
3461            status: acp::ToolCallStatus::Pending,
3462            content: vec!["hi".into()],
3463            locations: vec![],
3464            raw_input: None,
3465            raw_output: None,
3466        };
3467        let connection = StubAgentConnection::new(vec![acp::SessionUpdate::ToolCall(tool_call)])
3468            .with_permission_requests(HashMap::from_iter([(
3469                tool_call_id,
3470                vec![acp::PermissionOption {
3471                    id: acp::PermissionOptionId("1".into()),
3472                    name: "Allow".into(),
3473                    kind: acp::PermissionOptionKind::AllowOnce,
3474                }],
3475            )]));
3476        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
3477
3478        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
3479        message_editor.update_in(cx, |editor, window, cx| {
3480            editor.set_text("Hello", window, cx);
3481        });
3482
3483        cx.deactivate_window();
3484
3485        thread_view.update_in(cx, |thread_view, window, cx| {
3486            thread_view.chat(&Chat, window, cx);
3487        });
3488
3489        cx.run_until_parked();
3490
3491        assert!(
3492            cx.windows()
3493                .iter()
3494                .any(|window| window.downcast::<AgentNotification>().is_some())
3495        );
3496    }
3497
3498    #[gpui::test]
3499    async fn test_crease_removal(cx: &mut TestAppContext) {
3500        init_test(cx);
3501
3502        let fs = FakeFs::new(cx.executor());
3503        fs.insert_tree("/project", json!({"file": ""})).await;
3504        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3505        let agent = StubAgentServer::default();
3506        let (workspace, cx) =
3507            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3508        let thread_view = cx.update(|window, cx| {
3509            cx.new(|cx| {
3510                AcpThreadView::new(
3511                    Rc::new(agent),
3512                    workspace.downgrade(),
3513                    project,
3514                    Rc::new(RefCell::new(MessageHistory::default())),
3515                    1,
3516                    None,
3517                    window,
3518                    cx,
3519                )
3520            })
3521        });
3522
3523        cx.run_until_parked();
3524
3525        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
3526        let excerpt_id = message_editor.update(cx, |editor, cx| {
3527            editor
3528                .buffer()
3529                .read(cx)
3530                .excerpt_ids()
3531                .into_iter()
3532                .next()
3533                .unwrap()
3534        });
3535        let completions = message_editor.update_in(cx, |editor, window, cx| {
3536            editor.set_text("Hello @", window, cx);
3537            let buffer = editor.buffer().read(cx).as_singleton().unwrap();
3538            let completion_provider = editor.completion_provider().unwrap();
3539            completion_provider.completions(
3540                excerpt_id,
3541                &buffer,
3542                Anchor::MAX,
3543                CompletionContext {
3544                    trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
3545                    trigger_character: Some("@".into()),
3546                },
3547                window,
3548                cx,
3549            )
3550        });
3551        let [_, completion]: [_; 2] = completions
3552            .await
3553            .unwrap()
3554            .into_iter()
3555            .flat_map(|response| response.completions)
3556            .collect::<Vec<_>>()
3557            .try_into()
3558            .unwrap();
3559
3560        message_editor.update_in(cx, |editor, window, cx| {
3561            let snapshot = editor.buffer().read(cx).snapshot(cx);
3562            let start = snapshot
3563                .anchor_in_excerpt(excerpt_id, completion.replace_range.start)
3564                .unwrap();
3565            let end = snapshot
3566                .anchor_in_excerpt(excerpt_id, completion.replace_range.end)
3567                .unwrap();
3568            editor.edit([(start..end, completion.new_text)], cx);
3569            (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
3570        });
3571
3572        cx.run_until_parked();
3573
3574        // Backspace over the inserted crease (and the following space).
3575        message_editor.update_in(cx, |editor, window, cx| {
3576            editor.backspace(&Default::default(), window, cx);
3577            editor.backspace(&Default::default(), window, cx);
3578        });
3579
3580        thread_view.update_in(cx, |thread_view, window, cx| {
3581            thread_view.chat(&Chat, window, cx);
3582        });
3583
3584        cx.run_until_parked();
3585
3586        let content = thread_view.update_in(cx, |thread_view, _window, _cx| {
3587            thread_view
3588                .message_history
3589                .borrow()
3590                .items()
3591                .iter()
3592                .flatten()
3593                .cloned()
3594                .collect::<Vec<_>>()
3595        });
3596
3597        // We don't send a resource link for the deleted crease.
3598        pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
3599    }
3600
3601    async fn setup_thread_view(
3602        agent: impl AgentServer + 'static,
3603        cx: &mut TestAppContext,
3604    ) -> (Entity<AcpThreadView>, &mut VisualTestContext) {
3605        let fs = FakeFs::new(cx.executor());
3606        let project = Project::test(fs, [], cx).await;
3607        let (workspace, cx) =
3608            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3609
3610        let thread_view = cx.update(|window, cx| {
3611            cx.new(|cx| {
3612                AcpThreadView::new(
3613                    Rc::new(agent),
3614                    workspace.downgrade(),
3615                    project,
3616                    Rc::new(RefCell::new(MessageHistory::default())),
3617                    1,
3618                    None,
3619                    window,
3620                    cx,
3621                )
3622            })
3623        });
3624        cx.run_until_parked();
3625        (thread_view, cx)
3626    }
3627
3628    struct StubAgentServer<C> {
3629        connection: C,
3630    }
3631
3632    impl<C> StubAgentServer<C> {
3633        fn new(connection: C) -> Self {
3634            Self { connection }
3635        }
3636    }
3637
3638    impl StubAgentServer<StubAgentConnection> {
3639        fn default() -> Self {
3640            Self::new(StubAgentConnection::default())
3641        }
3642    }
3643
3644    impl<C> AgentServer for StubAgentServer<C>
3645    where
3646        C: 'static + AgentConnection + Send + Clone,
3647    {
3648        fn logo(&self) -> ui::IconName {
3649            unimplemented!()
3650        }
3651
3652        fn name(&self) -> &'static str {
3653            unimplemented!()
3654        }
3655
3656        fn empty_state_headline(&self) -> &'static str {
3657            unimplemented!()
3658        }
3659
3660        fn empty_state_message(&self) -> &'static str {
3661            unimplemented!()
3662        }
3663
3664        fn connect(
3665            &self,
3666            _root_dir: &Path,
3667            _project: &Entity<Project>,
3668            _cx: &mut App,
3669        ) -> Task<gpui::Result<Rc<dyn AgentConnection>>> {
3670            Task::ready(Ok(Rc::new(self.connection.clone())))
3671        }
3672    }
3673
3674    #[derive(Clone, Default)]
3675    struct StubAgentConnection {
3676        sessions: Arc<Mutex<HashMap<acp::SessionId, WeakEntity<AcpThread>>>>,
3677        permission_requests: HashMap<acp::ToolCallId, Vec<acp::PermissionOption>>,
3678        updates: Vec<acp::SessionUpdate>,
3679    }
3680
3681    impl StubAgentConnection {
3682        fn new(updates: Vec<acp::SessionUpdate>) -> Self {
3683            Self {
3684                updates,
3685                permission_requests: HashMap::default(),
3686                sessions: Arc::default(),
3687            }
3688        }
3689
3690        fn with_permission_requests(
3691            mut self,
3692            permission_requests: HashMap<acp::ToolCallId, Vec<acp::PermissionOption>>,
3693        ) -> Self {
3694            self.permission_requests = permission_requests;
3695            self
3696        }
3697    }
3698
3699    impl AgentConnection for StubAgentConnection {
3700        fn auth_methods(&self) -> &[acp::AuthMethod] {
3701            &[]
3702        }
3703
3704        fn new_thread(
3705            self: Rc<Self>,
3706            project: Entity<Project>,
3707            _cwd: &Path,
3708            cx: &mut gpui::AsyncApp,
3709        ) -> Task<gpui::Result<Entity<AcpThread>>> {
3710            let session_id = SessionId(
3711                rand::thread_rng()
3712                    .sample_iter(&rand::distributions::Alphanumeric)
3713                    .take(7)
3714                    .map(char::from)
3715                    .collect::<String>()
3716                    .into(),
3717            );
3718            let thread = cx
3719                .new(|cx| AcpThread::new("Test", self.clone(), project, session_id.clone(), cx))
3720                .unwrap();
3721            self.sessions.lock().insert(session_id, thread.downgrade());
3722            Task::ready(Ok(thread))
3723        }
3724
3725        fn authenticate(
3726            &self,
3727            _method_id: acp::AuthMethodId,
3728            _cx: &mut App,
3729        ) -> Task<gpui::Result<()>> {
3730            unimplemented!()
3731        }
3732
3733        fn prompt(
3734            &self,
3735            params: acp::PromptRequest,
3736            cx: &mut App,
3737        ) -> Task<gpui::Result<acp::PromptResponse>> {
3738            let sessions = self.sessions.lock();
3739            let thread = sessions.get(&params.session_id).unwrap();
3740            let mut tasks = vec![];
3741            for update in &self.updates {
3742                let thread = thread.clone();
3743                let update = update.clone();
3744                let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) = &update
3745                    && let Some(options) = self.permission_requests.get(&tool_call.id)
3746                {
3747                    Some((tool_call.clone(), options.clone()))
3748                } else {
3749                    None
3750                };
3751                let task = cx.spawn(async move |cx| {
3752                    if let Some((tool_call, options)) = permission_request {
3753                        let permission = thread.update(cx, |thread, cx| {
3754                            thread.request_tool_call_authorization(
3755                                tool_call.clone(),
3756                                options.clone(),
3757                                cx,
3758                            )
3759                        })?;
3760                        permission.await?;
3761                    }
3762                    thread.update(cx, |thread, cx| {
3763                        thread.handle_session_update(update.clone(), cx).unwrap();
3764                    })?;
3765                    anyhow::Ok(())
3766                });
3767                tasks.push(task);
3768            }
3769            cx.spawn(async move |_| {
3770                try_join_all(tasks).await?;
3771                Ok(acp::PromptResponse {
3772                    stop_reason: acp::StopReason::EndTurn,
3773                })
3774            })
3775        }
3776
3777        fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
3778            unimplemented!()
3779        }
3780    }
3781
3782    #[derive(Clone)]
3783    struct SaboteurAgentConnection;
3784
3785    impl AgentConnection for SaboteurAgentConnection {
3786        fn new_thread(
3787            self: Rc<Self>,
3788            project: Entity<Project>,
3789            _cwd: &Path,
3790            cx: &mut gpui::AsyncApp,
3791        ) -> Task<gpui::Result<Entity<AcpThread>>> {
3792            Task::ready(Ok(cx
3793                .new(|cx| {
3794                    AcpThread::new(
3795                        "SaboteurAgentConnection",
3796                        self,
3797                        project,
3798                        SessionId("test".into()),
3799                        cx,
3800                    )
3801                })
3802                .unwrap()))
3803        }
3804
3805        fn auth_methods(&self) -> &[acp::AuthMethod] {
3806            &[]
3807        }
3808
3809        fn authenticate(
3810            &self,
3811            _method_id: acp::AuthMethodId,
3812            _cx: &mut App,
3813        ) -> Task<gpui::Result<()>> {
3814            unimplemented!()
3815        }
3816
3817        fn prompt(
3818            &self,
3819            _params: acp::PromptRequest,
3820            _cx: &mut App,
3821        ) -> Task<gpui::Result<acp::PromptResponse>> {
3822            Task::ready(Err(anyhow::anyhow!("Error prompting")))
3823        }
3824
3825        fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
3826            unimplemented!()
3827        }
3828    }
3829
3830    fn init_test(cx: &mut TestAppContext) {
3831        cx.update(|cx| {
3832            let settings_store = SettingsStore::test(cx);
3833            cx.set_global(settings_store);
3834            language::init(cx);
3835            Project::init_settings(cx);
3836            AgentSettings::register(cx);
3837            workspace::init_settings(cx);
3838            ThemeSettings::register(cx);
3839            release_channel::init(SemanticVersion::default(), cx);
3840            EditorSettings::register(cx);
3841        });
3842    }
3843}