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