thread_view.rs

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