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