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