thread_view.rs

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