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