thread_view.rs

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