thread_view.rs

   1use acp_thread::{
   2    AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk,
   3    AuthRequired, LoadError, MentionUri, RetryStatus, ThreadStatus, ToolCall, ToolCallContent,
   4    ToolCallStatus, UserMessageId,
   5};
   6use acp_thread::{AgentConnection, Plan};
   7use action_log::ActionLog;
   8use agent_client_protocol::{self as acp, PromptCapabilities};
   9use agent_servers::{AgentServer, AgentServerDelegate};
  10use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting};
  11use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer};
  12use anyhow::{Context as _, Result, anyhow, bail};
  13use audio::{Audio, Sound};
  14use buffer_diff::BufferDiff;
  15use client::zed_urls;
  16use collections::{HashMap, HashSet};
  17use editor::scroll::Autoscroll;
  18use editor::{Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects};
  19use file_icons::FileIcons;
  20use fs::Fs;
  21use futures::FutureExt as _;
  22use gpui::{
  23    Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem,
  24    CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length,
  25    ListOffset, ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement,
  26    Subscription, Task, TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, Window,
  27    WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, point, prelude::*,
  28    pulsating_between,
  29};
  30use language::Buffer;
  31
  32use language_model::LanguageModelRegistry;
  33use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
  34use project::{Project, ProjectEntryId};
  35use prompt_store::{PromptId, PromptStore};
  36use rope::Point;
  37use settings::{Settings as _, SettingsStore};
  38use std::cell::{Cell, RefCell};
  39use std::path::Path;
  40use std::sync::Arc;
  41use std::time::Instant;
  42use std::{collections::BTreeMap, rc::Rc, time::Duration};
  43use terminal_view::terminal_panel::TerminalPanel;
  44use text::Anchor;
  45use theme::{AgentFontSize, ThemeSettings};
  46use ui::{
  47    Callout, CommonAnimationExt, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding,
  48    PopoverMenuHandle, Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*,
  49};
  50use util::{ResultExt, size::format_file_size, time::duration_alt_display};
  51use workspace::{CollaboratorId, Workspace};
  52use zed_actions::agent::{Chat, ToggleModelSelector};
  53use zed_actions::assistant::OpenRulesLibrary;
  54
  55use super::entry_view_state::EntryViewState;
  56use crate::acp::AcpModelSelectorPopover;
  57use crate::acp::entry_view_state::{EntryViewEvent, ViewEvent};
  58use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
  59use crate::agent_diff::AgentDiff;
  60use crate::profile_selector::{ProfileProvider, ProfileSelector};
  61
  62use crate::ui::preview::UsageCallout;
  63use crate::ui::{
  64    AgentNotification, AgentNotificationEvent, BurnModeTooltip, UnavailableEditingTooltip,
  65};
  66use crate::{
  67    AgentDiffPane, AgentPanel, ContinueThread, ContinueWithBurnMode, ExpandMessageEditor, Follow,
  68    KeepAll, OpenAgentDiff, OpenHistory, RejectAll, ToggleBurnMode, ToggleProfileSelector,
  69};
  70
  71pub const MIN_EDITOR_LINES: usize = 4;
  72pub const MAX_EDITOR_LINES: usize = 8;
  73
  74#[derive(Copy, Clone, Debug, PartialEq, Eq)]
  75enum ThreadFeedback {
  76    Positive,
  77    Negative,
  78}
  79
  80#[derive(Debug)]
  81enum ThreadError {
  82    PaymentRequired,
  83    ModelRequestLimitReached(cloud_llm_client::Plan),
  84    ToolUseLimitReached,
  85    Refusal,
  86    AuthenticationRequired(SharedString),
  87    Other(SharedString),
  88}
  89
  90impl ThreadError {
  91    fn from_err(error: anyhow::Error, agent: &Rc<dyn AgentServer>) -> Self {
  92        if error.is::<language_model::PaymentRequiredError>() {
  93            Self::PaymentRequired
  94        } else if error.is::<language_model::ToolUseLimitReachedError>() {
  95            Self::ToolUseLimitReached
  96        } else if let Some(error) =
  97            error.downcast_ref::<language_model::ModelRequestLimitReachedError>()
  98        {
  99            Self::ModelRequestLimitReached(error.plan)
 100        } else if let Some(acp_error) = error.downcast_ref::<acp::Error>()
 101            && acp_error.code == acp::ErrorCode::AUTH_REQUIRED.code
 102        {
 103            Self::AuthenticationRequired(acp_error.message.clone().into())
 104        } else {
 105            let string = error.to_string();
 106            // TODO: we should have Gemini return better errors here.
 107            if agent.clone().downcast::<agent_servers::Gemini>().is_some()
 108                && string.contains("Could not load the default credentials")
 109                || string.contains("API key not valid")
 110                || string.contains("Request had invalid authentication credentials")
 111            {
 112                Self::AuthenticationRequired(string.into())
 113            } else {
 114                Self::Other(error.to_string().into())
 115            }
 116        }
 117    }
 118}
 119
 120impl ProfileProvider for Entity<agent2::Thread> {
 121    fn profile_id(&self, cx: &App) -> AgentProfileId {
 122        self.read(cx).profile().clone()
 123    }
 124
 125    fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App) {
 126        self.update(cx, |thread, _cx| {
 127            thread.set_profile(profile_id);
 128        });
 129    }
 130
 131    fn profiles_supported(&self, cx: &App) -> bool {
 132        self.read(cx)
 133            .model()
 134            .is_some_and(|model| model.supports_tools())
 135    }
 136}
 137
 138#[derive(Default)]
 139struct ThreadFeedbackState {
 140    feedback: Option<ThreadFeedback>,
 141    comments_editor: Option<Entity<Editor>>,
 142}
 143
 144impl ThreadFeedbackState {
 145    pub fn submit(
 146        &mut self,
 147        thread: Entity<AcpThread>,
 148        feedback: ThreadFeedback,
 149        window: &mut Window,
 150        cx: &mut App,
 151    ) {
 152        let Some(telemetry) = thread.read(cx).connection().telemetry() else {
 153            return;
 154        };
 155
 156        if self.feedback == Some(feedback) {
 157            return;
 158        }
 159
 160        self.feedback = Some(feedback);
 161        match feedback {
 162            ThreadFeedback::Positive => {
 163                self.comments_editor = None;
 164            }
 165            ThreadFeedback::Negative => {
 166                self.comments_editor = Some(Self::build_feedback_comments_editor(window, cx));
 167            }
 168        }
 169        let session_id = thread.read(cx).session_id().clone();
 170        let agent_name = telemetry.agent_name();
 171        let task = telemetry.thread_data(&session_id, cx);
 172        let rating = match feedback {
 173            ThreadFeedback::Positive => "positive",
 174            ThreadFeedback::Negative => "negative",
 175        };
 176        cx.background_spawn(async move {
 177            let thread = task.await?;
 178            telemetry::event!(
 179                "Agent Thread Rated",
 180                session_id = session_id,
 181                rating = rating,
 182                agent = agent_name,
 183                thread = thread
 184            );
 185            anyhow::Ok(())
 186        })
 187        .detach_and_log_err(cx);
 188    }
 189
 190    pub fn submit_comments(&mut self, thread: Entity<AcpThread>, cx: &mut App) {
 191        let Some(telemetry) = thread.read(cx).connection().telemetry() else {
 192            return;
 193        };
 194
 195        let Some(comments) = self
 196            .comments_editor
 197            .as_ref()
 198            .map(|editor| editor.read(cx).text(cx))
 199            .filter(|text| !text.trim().is_empty())
 200        else {
 201            return;
 202        };
 203
 204        self.comments_editor.take();
 205
 206        let session_id = thread.read(cx).session_id().clone();
 207        let agent_name = telemetry.agent_name();
 208        let task = telemetry.thread_data(&session_id, cx);
 209        cx.background_spawn(async move {
 210            let thread = task.await?;
 211            telemetry::event!(
 212                "Agent Thread Feedback Comments",
 213                session_id = session_id,
 214                comments = comments,
 215                agent = agent_name,
 216                thread = thread
 217            );
 218            anyhow::Ok(())
 219        })
 220        .detach_and_log_err(cx);
 221    }
 222
 223    pub fn clear(&mut self) {
 224        *self = Self::default()
 225    }
 226
 227    pub fn dismiss_comments(&mut self) {
 228        self.comments_editor.take();
 229    }
 230
 231    fn build_feedback_comments_editor(window: &mut Window, cx: &mut App) -> Entity<Editor> {
 232        let buffer = cx.new(|cx| {
 233            let empty_string = String::new();
 234            MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx)
 235        });
 236
 237        let editor = cx.new(|cx| {
 238            let mut editor = Editor::new(
 239                editor::EditorMode::AutoHeight {
 240                    min_lines: 1,
 241                    max_lines: Some(4),
 242                },
 243                buffer,
 244                None,
 245                window,
 246                cx,
 247            );
 248            editor.set_placeholder_text(
 249                "What went wrong? Share your feedback so we can improve.",
 250                cx,
 251            );
 252            editor
 253        });
 254
 255        editor.read(cx).focus_handle(cx).focus(window);
 256        editor
 257    }
 258}
 259
 260pub struct AcpThreadView {
 261    agent: Rc<dyn AgentServer>,
 262    workspace: WeakEntity<Workspace>,
 263    project: Entity<Project>,
 264    thread_state: ThreadState,
 265    login: Option<task::SpawnInTerminal>,
 266    history_store: Entity<HistoryStore>,
 267    hovered_recent_history_item: Option<usize>,
 268    entry_view_state: Entity<EntryViewState>,
 269    message_editor: Entity<MessageEditor>,
 270    focus_handle: FocusHandle,
 271    model_selector: Option<Entity<AcpModelSelectorPopover>>,
 272    profile_selector: Option<Entity<ProfileSelector>>,
 273    notifications: Vec<WindowHandle<AgentNotification>>,
 274    notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
 275    thread_retry_status: Option<RetryStatus>,
 276    thread_error: Option<ThreadError>,
 277    thread_feedback: ThreadFeedbackState,
 278    list_state: ListState,
 279    scrollbar_state: ScrollbarState,
 280    auth_task: Option<Task<()>>,
 281    expanded_tool_calls: HashSet<acp::ToolCallId>,
 282    expanded_thinking_blocks: HashSet<(usize, usize)>,
 283    edits_expanded: bool,
 284    plan_expanded: bool,
 285    editor_expanded: bool,
 286    should_be_following: bool,
 287    editing_message: Option<usize>,
 288    prompt_capabilities: Rc<Cell<PromptCapabilities>>,
 289    available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
 290    is_loading_contents: bool,
 291    new_server_version_available: Option<SharedString>,
 292    _cancel_task: Option<Task<()>>,
 293    _subscriptions: [Subscription; 4],
 294}
 295
 296enum ThreadState {
 297    Loading(Entity<LoadingView>),
 298    Ready {
 299        thread: Entity<AcpThread>,
 300        title_editor: Option<Entity<Editor>>,
 301        _subscriptions: Vec<Subscription>,
 302    },
 303    LoadError(LoadError),
 304    Unauthenticated {
 305        connection: Rc<dyn AgentConnection>,
 306        description: Option<Entity<Markdown>>,
 307        configuration_view: Option<AnyView>,
 308        pending_auth_method: Option<acp::AuthMethodId>,
 309        _subscription: Option<Subscription>,
 310    },
 311}
 312
 313struct LoadingView {
 314    title: SharedString,
 315    _load_task: Task<()>,
 316    _update_title_task: Task<anyhow::Result<()>>,
 317}
 318
 319impl AcpThreadView {
 320    pub fn new(
 321        agent: Rc<dyn AgentServer>,
 322        resume_thread: Option<DbThreadMetadata>,
 323        summarize_thread: Option<DbThreadMetadata>,
 324        workspace: WeakEntity<Workspace>,
 325        project: Entity<Project>,
 326        history_store: Entity<HistoryStore>,
 327        prompt_store: Option<Entity<PromptStore>>,
 328        window: &mut Window,
 329        cx: &mut Context<Self>,
 330    ) -> Self {
 331        let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
 332        let available_commands = Rc::new(RefCell::new(vec![]));
 333
 334        let placeholder = if agent.name() == "Zed Agent" {
 335            format!("Message the {} — @ to include context", agent.name())
 336        } else if agent.name() == "Claude Code" || !available_commands.borrow().is_empty() {
 337            format!(
 338                "Message {} — @ to include context, / for commands",
 339                agent.name()
 340            )
 341        } else {
 342            format!("Message {} — @ to include context", agent.name())
 343        };
 344
 345        let message_editor = cx.new(|cx| {
 346            let mut editor = MessageEditor::new(
 347                workspace.clone(),
 348                project.clone(),
 349                history_store.clone(),
 350                prompt_store.clone(),
 351                prompt_capabilities.clone(),
 352                available_commands.clone(),
 353                agent.name(),
 354                placeholder,
 355                editor::EditorMode::AutoHeight {
 356                    min_lines: MIN_EDITOR_LINES,
 357                    max_lines: Some(MAX_EDITOR_LINES),
 358                },
 359                window,
 360                cx,
 361            );
 362            if let Some(entry) = summarize_thread {
 363                editor.insert_thread_summary(entry, window, cx);
 364            }
 365            editor
 366        });
 367
 368        let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0));
 369
 370        let entry_view_state = cx.new(|_| {
 371            EntryViewState::new(
 372                workspace.clone(),
 373                project.clone(),
 374                history_store.clone(),
 375                prompt_store.clone(),
 376                prompt_capabilities.clone(),
 377                available_commands.clone(),
 378                agent.name(),
 379            )
 380        });
 381
 382        let subscriptions = [
 383            cx.observe_global_in::<SettingsStore>(window, Self::agent_font_size_changed),
 384            cx.observe_global_in::<AgentFontSize>(window, Self::agent_font_size_changed),
 385            cx.subscribe_in(&message_editor, window, Self::handle_message_editor_event),
 386            cx.subscribe_in(&entry_view_state, window, Self::handle_entry_view_event),
 387        ];
 388
 389        Self {
 390            agent: agent.clone(),
 391            workspace: workspace.clone(),
 392            project: project.clone(),
 393            entry_view_state,
 394            thread_state: Self::initial_state(agent, resume_thread, workspace, project, window, cx),
 395            login: None,
 396            message_editor,
 397            model_selector: None,
 398            profile_selector: None,
 399            notifications: Vec::new(),
 400            notification_subscriptions: HashMap::default(),
 401            list_state: list_state.clone(),
 402            scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()),
 403            thread_retry_status: None,
 404            thread_error: None,
 405            thread_feedback: Default::default(),
 406            auth_task: None,
 407            expanded_tool_calls: HashSet::default(),
 408            expanded_thinking_blocks: HashSet::default(),
 409            editing_message: None,
 410            edits_expanded: false,
 411            plan_expanded: false,
 412            prompt_capabilities,
 413            available_commands,
 414            editor_expanded: false,
 415            should_be_following: false,
 416            history_store,
 417            hovered_recent_history_item: None,
 418            is_loading_contents: false,
 419            _subscriptions: subscriptions,
 420            _cancel_task: None,
 421            focus_handle: cx.focus_handle(),
 422            new_server_version_available: None,
 423        }
 424    }
 425
 426    fn reset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 427        self.thread_state = Self::initial_state(
 428            self.agent.clone(),
 429            None,
 430            self.workspace.clone(),
 431            self.project.clone(),
 432            window,
 433            cx,
 434        );
 435        self.available_commands.replace(vec![]);
 436        self.new_server_version_available.take();
 437        cx.notify();
 438    }
 439
 440    fn initial_state(
 441        agent: Rc<dyn AgentServer>,
 442        resume_thread: Option<DbThreadMetadata>,
 443        workspace: WeakEntity<Workspace>,
 444        project: Entity<Project>,
 445        window: &mut Window,
 446        cx: &mut Context<Self>,
 447    ) -> ThreadState {
 448        if project.read(cx).is_via_collab()
 449            && agent.clone().downcast::<NativeAgentServer>().is_none()
 450        {
 451            return ThreadState::LoadError(LoadError::Other(
 452                "External agents are not yet supported in shared projects.".into(),
 453            ));
 454        }
 455        let mut worktrees = project.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
 456        // Pick the first non-single-file worktree for the root directory if there are any,
 457        // and otherwise the parent of a single-file worktree, falling back to $HOME if there are no visible worktrees.
 458        worktrees.sort_by(|l, r| {
 459            l.read(cx)
 460                .is_single_file()
 461                .cmp(&r.read(cx).is_single_file())
 462        });
 463        let root_dir = worktrees
 464            .into_iter()
 465            .filter_map(|worktree| {
 466                if worktree.read(cx).is_single_file() {
 467                    Some(worktree.read(cx).abs_path().parent()?.into())
 468                } else {
 469                    Some(worktree.read(cx).abs_path())
 470                }
 471            })
 472            .next();
 473        let (status_tx, mut status_rx) = watch::channel("Loading…".into());
 474        let (new_version_available_tx, mut new_version_available_rx) = watch::channel(None);
 475        let delegate = AgentServerDelegate::new(
 476            project.read(cx).agent_server_store().clone(),
 477            project.clone(),
 478            Some(status_tx),
 479            Some(new_version_available_tx),
 480        );
 481
 482        let connect_task = agent.connect(root_dir.as_deref(), delegate, cx);
 483        let load_task = cx.spawn_in(window, async move |this, cx| {
 484            let connection = match connect_task.await {
 485                Ok((connection, login)) => {
 486                    this.update(cx, |this, _| this.login = login).ok();
 487                    connection
 488                }
 489                Err(err) => {
 490                    this.update_in(cx, |this, window, cx| {
 491                        if err.downcast_ref::<LoadError>().is_some() {
 492                            this.handle_load_error(err, window, cx);
 493                        } else {
 494                            this.handle_thread_error(err, cx);
 495                        }
 496                        cx.notify();
 497                    })
 498                    .log_err();
 499                    return;
 500                }
 501            };
 502
 503            let result = if let Some(native_agent) = connection
 504                .clone()
 505                .downcast::<agent2::NativeAgentConnection>()
 506                && let Some(resume) = resume_thread.clone()
 507            {
 508                cx.update(|_, cx| {
 509                    native_agent
 510                        .0
 511                        .update(cx, |agent, cx| agent.open_thread(resume.id, cx))
 512                })
 513                .log_err()
 514            } else {
 515                let root_dir = if let Some(acp_agent) = connection
 516                    .clone()
 517                    .downcast::<agent_servers::AcpConnection>()
 518                {
 519                    acp_agent.root_dir().into()
 520                } else {
 521                    root_dir.unwrap_or(paths::home_dir().as_path().into())
 522                };
 523                cx.update(|_, cx| {
 524                    connection
 525                        .clone()
 526                        .new_thread(project.clone(), &root_dir, cx)
 527                })
 528                .log_err()
 529            };
 530
 531            let Some(result) = result else {
 532                return;
 533            };
 534
 535            let result = match result.await {
 536                Err(e) => match e.downcast::<acp_thread::AuthRequired>() {
 537                    Ok(err) => {
 538                        cx.update(|window, cx| {
 539                            Self::handle_auth_required(this, err, agent, connection, window, cx)
 540                        })
 541                        .log_err();
 542                        return;
 543                    }
 544                    Err(err) => Err(err),
 545                },
 546                Ok(thread) => Ok(thread),
 547            };
 548
 549            this.update_in(cx, |this, window, cx| {
 550                match result {
 551                    Ok(thread) => {
 552                        let action_log = thread.read(cx).action_log().clone();
 553
 554                        this.prompt_capabilities
 555                            .set(thread.read(cx).prompt_capabilities());
 556
 557                        let count = thread.read(cx).entries().len();
 558                        this.entry_view_state.update(cx, |view_state, cx| {
 559                            for ix in 0..count {
 560                                view_state.sync_entry(ix, &thread, window, cx);
 561                            }
 562                            this.list_state.splice_focusable(
 563                                0..0,
 564                                (0..count).map(|ix| view_state.entry(ix)?.focus_handle(cx)),
 565                            );
 566                        });
 567
 568                        if let Some(resume) = resume_thread {
 569                            this.history_store.update(cx, |history, cx| {
 570                                history.push_recently_opened_entry(
 571                                    HistoryEntryId::AcpThread(resume.id),
 572                                    cx,
 573                                );
 574                            });
 575                        }
 576
 577                        AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
 578
 579                        this.model_selector =
 580                            thread
 581                                .read(cx)
 582                                .connection()
 583                                .model_selector()
 584                                .map(|selector| {
 585                                    cx.new(|cx| {
 586                                        AcpModelSelectorPopover::new(
 587                                            thread.read(cx).session_id().clone(),
 588                                            selector,
 589                                            PopoverMenuHandle::default(),
 590                                            this.focus_handle(cx),
 591                                            window,
 592                                            cx,
 593                                        )
 594                                    })
 595                                });
 596
 597                        let mut subscriptions = vec![
 598                            cx.subscribe_in(&thread, window, Self::handle_thread_event),
 599                            cx.observe(&action_log, |_, _, cx| cx.notify()),
 600                        ];
 601
 602                        let title_editor =
 603                            if thread.update(cx, |thread, cx| thread.can_set_title(cx)) {
 604                                let editor = cx.new(|cx| {
 605                                    let mut editor = Editor::single_line(window, cx);
 606                                    editor.set_text(thread.read(cx).title(), window, cx);
 607                                    editor
 608                                });
 609                                subscriptions.push(cx.subscribe_in(
 610                                    &editor,
 611                                    window,
 612                                    Self::handle_title_editor_event,
 613                                ));
 614                                Some(editor)
 615                            } else {
 616                                None
 617                            };
 618                        this.thread_state = ThreadState::Ready {
 619                            thread,
 620                            title_editor,
 621                            _subscriptions: subscriptions,
 622                        };
 623                        this.message_editor.focus_handle(cx).focus(window);
 624
 625                        this.profile_selector = this.as_native_thread(cx).map(|thread| {
 626                            cx.new(|cx| {
 627                                ProfileSelector::new(
 628                                    <dyn Fs>::global(cx),
 629                                    Arc::new(thread.clone()),
 630                                    this.focus_handle(cx),
 631                                    cx,
 632                                )
 633                            })
 634                        });
 635
 636                        cx.notify();
 637                    }
 638                    Err(err) => {
 639                        this.handle_load_error(err, window, cx);
 640                    }
 641                };
 642            })
 643            .log_err();
 644        });
 645
 646        cx.spawn(async move |this, cx| {
 647            while let Ok(new_version) = new_version_available_rx.recv().await {
 648                if let Some(new_version) = new_version {
 649                    this.update(cx, |this, cx| {
 650                        this.new_server_version_available = Some(new_version.into());
 651                        cx.notify();
 652                    })
 653                    .log_err();
 654                }
 655            }
 656        })
 657        .detach();
 658
 659        let loading_view = cx.new(|cx| {
 660            let update_title_task = cx.spawn(async move |this, cx| {
 661                loop {
 662                    let status = status_rx.recv().await?;
 663                    this.update(cx, |this: &mut LoadingView, cx| {
 664                        this.title = status;
 665                        cx.notify();
 666                    })?;
 667                }
 668            });
 669
 670            LoadingView {
 671                title: "Loading…".into(),
 672                _load_task: load_task,
 673                _update_title_task: update_title_task,
 674            }
 675        });
 676
 677        ThreadState::Loading(loading_view)
 678    }
 679
 680    fn handle_auth_required(
 681        this: WeakEntity<Self>,
 682        err: AuthRequired,
 683        agent: Rc<dyn AgentServer>,
 684        connection: Rc<dyn AgentConnection>,
 685        window: &mut Window,
 686        cx: &mut App,
 687    ) {
 688        let agent_name = agent.name();
 689        let (configuration_view, subscription) = if let Some(provider_id) = err.provider_id {
 690            let registry = LanguageModelRegistry::global(cx);
 691
 692            let sub = window.subscribe(&registry, cx, {
 693                let provider_id = provider_id.clone();
 694                let this = this.clone();
 695                move |_, ev, window, cx| {
 696                    if let language_model::Event::ProviderStateChanged(updated_provider_id) = &ev
 697                        && &provider_id == updated_provider_id
 698                        && LanguageModelRegistry::global(cx)
 699                            .read(cx)
 700                            .provider(&provider_id)
 701                            .map_or(false, |provider| provider.is_authenticated(cx))
 702                    {
 703                        this.update(cx, |this, cx| {
 704                            this.reset(window, cx);
 705                        })
 706                        .ok();
 707                    }
 708                }
 709            });
 710
 711            let view = registry.read(cx).provider(&provider_id).map(|provider| {
 712                provider.configuration_view(
 713                    language_model::ConfigurationViewTargetAgent::Other(agent_name.clone()),
 714                    window,
 715                    cx,
 716                )
 717            });
 718
 719            (view, Some(sub))
 720        } else {
 721            (None, None)
 722        };
 723
 724        this.update(cx, |this, cx| {
 725            this.thread_state = ThreadState::Unauthenticated {
 726                pending_auth_method: None,
 727                connection,
 728                configuration_view,
 729                description: err
 730                    .description
 731                    .clone()
 732                    .map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))),
 733                _subscription: subscription,
 734            };
 735            if this.message_editor.focus_handle(cx).is_focused(window) {
 736                this.focus_handle.focus(window)
 737            }
 738            cx.notify();
 739        })
 740        .ok();
 741    }
 742
 743    fn handle_load_error(
 744        &mut self,
 745        err: anyhow::Error,
 746        window: &mut Window,
 747        cx: &mut Context<Self>,
 748    ) {
 749        if let Some(load_err) = err.downcast_ref::<LoadError>() {
 750            self.thread_state = ThreadState::LoadError(load_err.clone());
 751        } else {
 752            self.thread_state = ThreadState::LoadError(LoadError::Other(err.to_string().into()))
 753        }
 754        if self.message_editor.focus_handle(cx).is_focused(window) {
 755            self.focus_handle.focus(window)
 756        }
 757        cx.notify();
 758    }
 759
 760    pub fn workspace(&self) -> &WeakEntity<Workspace> {
 761        &self.workspace
 762    }
 763
 764    pub fn thread(&self) -> Option<&Entity<AcpThread>> {
 765        match &self.thread_state {
 766            ThreadState::Ready { thread, .. } => Some(thread),
 767            ThreadState::Unauthenticated { .. }
 768            | ThreadState::Loading { .. }
 769            | ThreadState::LoadError { .. } => None,
 770        }
 771    }
 772
 773    pub fn title(&self, cx: &App) -> SharedString {
 774        match &self.thread_state {
 775            ThreadState::Ready { .. } | ThreadState::Unauthenticated { .. } => "New Thread".into(),
 776            ThreadState::Loading(loading_view) => loading_view.read(cx).title.clone(),
 777            ThreadState::LoadError(error) => match error {
 778                LoadError::Unsupported { .. } => format!("Upgrade {}", self.agent.name()).into(),
 779                LoadError::FailedToInstall(_) => {
 780                    format!("Failed to Install {}", self.agent.name()).into()
 781                }
 782                LoadError::Exited { .. } => format!("{} Exited", self.agent.name()).into(),
 783                LoadError::Other(_) => format!("Error Loading {}", self.agent.name()).into(),
 784            },
 785        }
 786    }
 787
 788    pub fn title_editor(&self) -> Option<Entity<Editor>> {
 789        if let ThreadState::Ready { title_editor, .. } = &self.thread_state {
 790            title_editor.clone()
 791        } else {
 792            None
 793        }
 794    }
 795
 796    pub fn cancel_generation(&mut self, cx: &mut Context<Self>) {
 797        self.thread_error.take();
 798        self.thread_retry_status.take();
 799
 800        if let Some(thread) = self.thread() {
 801            self._cancel_task = Some(thread.update(cx, |thread, cx| thread.cancel(cx)));
 802        }
 803    }
 804
 805    pub fn expand_message_editor(
 806        &mut self,
 807        _: &ExpandMessageEditor,
 808        _window: &mut Window,
 809        cx: &mut Context<Self>,
 810    ) {
 811        self.set_editor_is_expanded(!self.editor_expanded, cx);
 812        cx.notify();
 813    }
 814
 815    fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {
 816        self.editor_expanded = is_expanded;
 817        self.message_editor.update(cx, |editor, cx| {
 818            if is_expanded {
 819                editor.set_mode(
 820                    EditorMode::Full {
 821                        scale_ui_elements_with_buffer_font_size: false,
 822                        show_active_line_background: false,
 823                        sized_by_content: false,
 824                    },
 825                    cx,
 826                )
 827            } else {
 828                editor.set_mode(
 829                    EditorMode::AutoHeight {
 830                        min_lines: MIN_EDITOR_LINES,
 831                        max_lines: Some(MAX_EDITOR_LINES),
 832                    },
 833                    cx,
 834                )
 835            }
 836        });
 837        cx.notify();
 838    }
 839
 840    pub fn handle_title_editor_event(
 841        &mut self,
 842        title_editor: &Entity<Editor>,
 843        event: &EditorEvent,
 844        window: &mut Window,
 845        cx: &mut Context<Self>,
 846    ) {
 847        let Some(thread) = self.thread() else { return };
 848
 849        match event {
 850            EditorEvent::BufferEdited => {
 851                let new_title = title_editor.read(cx).text(cx);
 852                thread.update(cx, |thread, cx| {
 853                    thread
 854                        .set_title(new_title.into(), cx)
 855                        .detach_and_log_err(cx);
 856                })
 857            }
 858            EditorEvent::Blurred => {
 859                if title_editor.read(cx).text(cx).is_empty() {
 860                    title_editor.update(cx, |editor, cx| {
 861                        editor.set_text("New Thread", window, cx);
 862                    });
 863                }
 864            }
 865            _ => {}
 866        }
 867    }
 868
 869    pub fn handle_message_editor_event(
 870        &mut self,
 871        _: &Entity<MessageEditor>,
 872        event: &MessageEditorEvent,
 873        window: &mut Window,
 874        cx: &mut Context<Self>,
 875    ) {
 876        match event {
 877            MessageEditorEvent::Send => self.send(window, cx),
 878            MessageEditorEvent::Cancel => self.cancel_generation(cx),
 879            MessageEditorEvent::Focus => {
 880                self.cancel_editing(&Default::default(), window, cx);
 881            }
 882            MessageEditorEvent::LostFocus => {}
 883        }
 884    }
 885
 886    pub fn handle_entry_view_event(
 887        &mut self,
 888        _: &Entity<EntryViewState>,
 889        event: &EntryViewEvent,
 890        window: &mut Window,
 891        cx: &mut Context<Self>,
 892    ) {
 893        match &event.view_event {
 894            ViewEvent::NewDiff(tool_call_id) => {
 895                if AgentSettings::get_global(cx).expand_edit_card {
 896                    self.expanded_tool_calls.insert(tool_call_id.clone());
 897                }
 898            }
 899            ViewEvent::NewTerminal(tool_call_id) => {
 900                if AgentSettings::get_global(cx).expand_terminal_card {
 901                    self.expanded_tool_calls.insert(tool_call_id.clone());
 902                }
 903            }
 904            ViewEvent::TerminalMovedToBackground(tool_call_id) => {
 905                self.expanded_tool_calls.remove(tool_call_id);
 906            }
 907            ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => {
 908                if let Some(thread) = self.thread()
 909                    && let Some(AgentThreadEntry::UserMessage(user_message)) =
 910                        thread.read(cx).entries().get(event.entry_index)
 911                    && user_message.id.is_some()
 912                {
 913                    self.editing_message = Some(event.entry_index);
 914                    cx.notify();
 915                }
 916            }
 917            ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::LostFocus) => {
 918                if let Some(thread) = self.thread()
 919                    && let Some(AgentThreadEntry::UserMessage(user_message)) =
 920                        thread.read(cx).entries().get(event.entry_index)
 921                    && user_message.id.is_some()
 922                {
 923                    if editor.read(cx).text(cx).as_str() == user_message.content.to_markdown(cx) {
 924                        self.editing_message = None;
 925                        cx.notify();
 926                    }
 927                }
 928            }
 929            ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => {
 930                self.regenerate(event.entry_index, editor.clone(), window, cx);
 931            }
 932            ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Cancel) => {
 933                self.cancel_editing(&Default::default(), window, cx);
 934            }
 935        }
 936    }
 937
 938    fn resume_chat(&mut self, cx: &mut Context<Self>) {
 939        self.thread_error.take();
 940        let Some(thread) = self.thread() else {
 941            return;
 942        };
 943        if !thread.read(cx).can_resume(cx) {
 944            return;
 945        }
 946
 947        let task = thread.update(cx, |thread, cx| thread.resume(cx));
 948        cx.spawn(async move |this, cx| {
 949            let result = task.await;
 950
 951            this.update(cx, |this, cx| {
 952                if let Err(err) = result {
 953                    this.handle_thread_error(err, cx);
 954                }
 955            })
 956        })
 957        .detach();
 958    }
 959
 960    fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 961        let Some(thread) = self.thread() else { return };
 962
 963        if self.is_loading_contents {
 964            return;
 965        }
 966
 967        self.history_store.update(cx, |history, cx| {
 968            history.push_recently_opened_entry(
 969                HistoryEntryId::AcpThread(thread.read(cx).session_id().clone()),
 970                cx,
 971            );
 972        });
 973
 974        if thread.read(cx).status() != ThreadStatus::Idle {
 975            self.stop_current_and_send_new_message(window, cx);
 976            return;
 977        }
 978
 979        let text = self.message_editor.read(cx).text(cx);
 980        let text = text.trim();
 981        if text == "/login" || text == "/logout" {
 982            let ThreadState::Ready { thread, .. } = &self.thread_state else {
 983                return;
 984            };
 985
 986            let connection = thread.read(cx).connection().clone();
 987            if !connection
 988                .auth_methods()
 989                .iter()
 990                .any(|method| method.id.0.as_ref() == "claude-login")
 991            {
 992                return;
 993            };
 994            let this = cx.weak_entity();
 995            let agent = self.agent.clone();
 996            window.defer(cx, |window, cx| {
 997                Self::handle_auth_required(
 998                    this,
 999                    AuthRequired {
1000                        description: None,
1001                        provider_id: None,
1002                    },
1003                    agent,
1004                    connection,
1005                    window,
1006                    cx,
1007                );
1008            });
1009            cx.notify();
1010            return;
1011        }
1012
1013        let contents = self
1014            .message_editor
1015            .update(cx, |message_editor, cx| message_editor.contents(cx));
1016        self.send_impl(contents, window, cx)
1017    }
1018
1019    fn stop_current_and_send_new_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1020        let Some(thread) = self.thread().cloned() else {
1021            return;
1022        };
1023
1024        let cancelled = thread.update(cx, |thread, cx| thread.cancel(cx));
1025
1026        let contents = self
1027            .message_editor
1028            .update(cx, |message_editor, cx| message_editor.contents(cx));
1029
1030        cx.spawn_in(window, async move |this, cx| {
1031            cancelled.await;
1032
1033            this.update_in(cx, |this, window, cx| {
1034                this.send_impl(contents, window, cx);
1035            })
1036            .ok();
1037        })
1038        .detach();
1039    }
1040
1041    fn send_impl(
1042        &mut self,
1043        contents: Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>>,
1044        window: &mut Window,
1045        cx: &mut Context<Self>,
1046    ) {
1047        let agent_telemetry_id = self.agent.telemetry_id();
1048
1049        self.thread_error.take();
1050        self.editing_message.take();
1051        self.thread_feedback.clear();
1052
1053        let Some(thread) = self.thread() else {
1054            return;
1055        };
1056        let thread = thread.downgrade();
1057        if self.should_be_following {
1058            self.workspace
1059                .update(cx, |workspace, cx| {
1060                    workspace.follow(CollaboratorId::Agent, window, cx);
1061                })
1062                .ok();
1063        }
1064
1065        self.is_loading_contents = true;
1066        let guard = cx.new(|_| ());
1067        cx.observe_release(&guard, |this, _guard, cx| {
1068            this.is_loading_contents = false;
1069            cx.notify();
1070        })
1071        .detach();
1072
1073        let task = cx.spawn_in(window, async move |this, cx| {
1074            let (contents, tracked_buffers) = contents.await?;
1075
1076            if contents.is_empty() {
1077                return Ok(());
1078            }
1079
1080            this.update_in(cx, |this, window, cx| {
1081                this.set_editor_is_expanded(false, cx);
1082                this.scroll_to_bottom(cx);
1083                this.message_editor.update(cx, |message_editor, cx| {
1084                    message_editor.clear(window, cx);
1085                });
1086            })?;
1087            let send = thread.update(cx, |thread, cx| {
1088                thread.action_log().update(cx, |action_log, cx| {
1089                    for buffer in tracked_buffers {
1090                        action_log.buffer_read(buffer, cx)
1091                    }
1092                });
1093                drop(guard);
1094
1095                telemetry::event!("Agent Message Sent", agent = agent_telemetry_id);
1096
1097                thread.send(contents, cx)
1098            })?;
1099            send.await
1100        });
1101
1102        cx.spawn(async move |this, cx| {
1103            if let Err(err) = task.await {
1104                this.update(cx, |this, cx| {
1105                    this.handle_thread_error(err, cx);
1106                })
1107                .ok();
1108            } else {
1109                this.update(cx, |this, cx| {
1110                    this.should_be_following = this
1111                        .workspace
1112                        .update(cx, |workspace, _| {
1113                            workspace.is_being_followed(CollaboratorId::Agent)
1114                        })
1115                        .unwrap_or_default();
1116                })
1117                .ok();
1118            }
1119        })
1120        .detach();
1121    }
1122
1123    fn cancel_editing(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
1124        let Some(thread) = self.thread().cloned() else {
1125            return;
1126        };
1127
1128        if let Some(index) = self.editing_message.take()
1129            && let Some(editor) = self
1130                .entry_view_state
1131                .read(cx)
1132                .entry(index)
1133                .and_then(|e| e.message_editor())
1134                .cloned()
1135        {
1136            editor.update(cx, |editor, cx| {
1137                if let Some(user_message) = thread
1138                    .read(cx)
1139                    .entries()
1140                    .get(index)
1141                    .and_then(|e| e.user_message())
1142                {
1143                    editor.set_message(user_message.chunks.clone(), window, cx);
1144                }
1145            })
1146        };
1147        self.focus_handle(cx).focus(window);
1148        cx.notify();
1149    }
1150
1151    fn regenerate(
1152        &mut self,
1153        entry_ix: usize,
1154        message_editor: Entity<MessageEditor>,
1155        window: &mut Window,
1156        cx: &mut Context<Self>,
1157    ) {
1158        let Some(thread) = self.thread().cloned() else {
1159            return;
1160        };
1161        if self.is_loading_contents {
1162            return;
1163        }
1164
1165        let Some(user_message_id) = thread.update(cx, |thread, _| {
1166            thread.entries().get(entry_ix)?.user_message()?.id.clone()
1167        }) else {
1168            return;
1169        };
1170
1171        cx.spawn_in(window, async move |this, cx| {
1172            thread
1173                .update(cx, |thread, cx| thread.rewind(user_message_id, cx))?
1174                .await?;
1175            let contents =
1176                message_editor.update(cx, |message_editor, cx| message_editor.contents(cx))?;
1177            this.update_in(cx, |this, window, cx| {
1178                this.send_impl(contents, window, cx);
1179            })?;
1180            anyhow::Ok(())
1181        })
1182        .detach();
1183    }
1184
1185    fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) {
1186        if let Some(thread) = self.thread() {
1187            AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err();
1188        }
1189    }
1190
1191    fn open_edited_buffer(
1192        &mut self,
1193        buffer: &Entity<Buffer>,
1194        window: &mut Window,
1195        cx: &mut Context<Self>,
1196    ) {
1197        let Some(thread) = self.thread() else {
1198            return;
1199        };
1200
1201        let Some(diff) =
1202            AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err()
1203        else {
1204            return;
1205        };
1206
1207        diff.update(cx, |diff, cx| {
1208            diff.move_to_path(PathKey::for_buffer(buffer, cx), window, cx)
1209        })
1210    }
1211
1212    fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
1213        let Some(thread) = self.as_native_thread(cx) else {
1214            return;
1215        };
1216        let project_context = thread.read(cx).project_context().read(cx);
1217
1218        let project_entry_ids = project_context
1219            .worktrees
1220            .iter()
1221            .flat_map(|worktree| worktree.rules_file.as_ref())
1222            .map(|rules_file| ProjectEntryId::from_usize(rules_file.project_entry_id))
1223            .collect::<Vec<_>>();
1224
1225        self.workspace
1226            .update(cx, move |workspace, cx| {
1227                // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
1228                // files clear. For example, if rules file 1 is already open but rules file 2 is not,
1229                // this would open and focus rules file 2 in a tab that is not next to rules file 1.
1230                let project = workspace.project().read(cx);
1231                let project_paths = project_entry_ids
1232                    .into_iter()
1233                    .flat_map(|entry_id| project.path_for_entry(entry_id, cx))
1234                    .collect::<Vec<_>>();
1235                for project_path in project_paths {
1236                    workspace
1237                        .open_path(project_path, None, true, window, cx)
1238                        .detach_and_log_err(cx);
1239                }
1240            })
1241            .ok();
1242    }
1243
1244    fn handle_thread_error(&mut self, error: anyhow::Error, cx: &mut Context<Self>) {
1245        self.thread_error = Some(ThreadError::from_err(error, &self.agent));
1246        cx.notify();
1247    }
1248
1249    fn clear_thread_error(&mut self, cx: &mut Context<Self>) {
1250        self.thread_error = None;
1251        cx.notify();
1252    }
1253
1254    fn handle_thread_event(
1255        &mut self,
1256        thread: &Entity<AcpThread>,
1257        event: &AcpThreadEvent,
1258        window: &mut Window,
1259        cx: &mut Context<Self>,
1260    ) {
1261        match event {
1262            AcpThreadEvent::NewEntry => {
1263                let len = thread.read(cx).entries().len();
1264                let index = len - 1;
1265                self.entry_view_state.update(cx, |view_state, cx| {
1266                    view_state.sync_entry(index, thread, window, cx);
1267                    self.list_state.splice_focusable(
1268                        index..index,
1269                        [view_state
1270                            .entry(index)
1271                            .and_then(|entry| entry.focus_handle(cx))],
1272                    );
1273                });
1274            }
1275            AcpThreadEvent::EntryUpdated(index) => {
1276                self.entry_view_state.update(cx, |view_state, cx| {
1277                    view_state.sync_entry(*index, thread, window, cx)
1278                });
1279            }
1280            AcpThreadEvent::EntriesRemoved(range) => {
1281                self.entry_view_state
1282                    .update(cx, |view_state, _cx| view_state.remove(range.clone()));
1283                self.list_state.splice(range.clone(), 0);
1284            }
1285            AcpThreadEvent::ToolAuthorizationRequired => {
1286                self.notify_with_sound("Waiting for tool confirmation", IconName::Info, window, cx);
1287            }
1288            AcpThreadEvent::Retry(retry) => {
1289                self.thread_retry_status = Some(retry.clone());
1290            }
1291            AcpThreadEvent::Stopped => {
1292                self.thread_retry_status.take();
1293                let used_tools = thread.read(cx).used_tools_since_last_user_message();
1294                self.notify_with_sound(
1295                    if used_tools {
1296                        "Finished running tools"
1297                    } else {
1298                        "New message"
1299                    },
1300                    IconName::ZedAssistant,
1301                    window,
1302                    cx,
1303                );
1304            }
1305            AcpThreadEvent::Refusal => {
1306                self.thread_retry_status.take();
1307                self.thread_error = Some(ThreadError::Refusal);
1308                let model_or_agent_name = self.get_current_model_name(cx);
1309                let notification_message =
1310                    format!("{} refused to respond to this request", model_or_agent_name);
1311                self.notify_with_sound(&notification_message, IconName::Warning, window, cx);
1312            }
1313            AcpThreadEvent::Error => {
1314                self.thread_retry_status.take();
1315                self.notify_with_sound(
1316                    "Agent stopped due to an error",
1317                    IconName::Warning,
1318                    window,
1319                    cx,
1320                );
1321            }
1322            AcpThreadEvent::LoadError(error) => {
1323                self.thread_retry_status.take();
1324                self.thread_state = ThreadState::LoadError(error.clone());
1325                if self.message_editor.focus_handle(cx).is_focused(window) {
1326                    self.focus_handle.focus(window)
1327                }
1328            }
1329            AcpThreadEvent::TitleUpdated => {
1330                let title = thread.read(cx).title();
1331                if let Some(title_editor) = self.title_editor() {
1332                    title_editor.update(cx, |editor, cx| {
1333                        if editor.text(cx) != title {
1334                            editor.set_text(title, window, cx);
1335                        }
1336                    });
1337                }
1338            }
1339            AcpThreadEvent::PromptCapabilitiesUpdated => {
1340                self.prompt_capabilities
1341                    .set(thread.read(cx).prompt_capabilities());
1342            }
1343            AcpThreadEvent::TokenUsageUpdated => {}
1344            AcpThreadEvent::AvailableCommandsUpdated(available_commands) => {
1345                let mut available_commands = available_commands.clone();
1346
1347                if thread
1348                    .read(cx)
1349                    .connection()
1350                    .auth_methods()
1351                    .iter()
1352                    .any(|method| method.id.0.as_ref() == "claude-login")
1353                {
1354                    available_commands.push(acp::AvailableCommand {
1355                        name: "login".to_owned(),
1356                        description: "Authenticate".to_owned(),
1357                        input: None,
1358                    });
1359                    available_commands.push(acp::AvailableCommand {
1360                        name: "logout".to_owned(),
1361                        description: "Authenticate".to_owned(),
1362                        input: None,
1363                    });
1364                }
1365
1366                self.available_commands.replace(available_commands);
1367            }
1368        }
1369        cx.notify();
1370    }
1371
1372    fn authenticate(
1373        &mut self,
1374        method: acp::AuthMethodId,
1375        window: &mut Window,
1376        cx: &mut Context<Self>,
1377    ) {
1378        let ThreadState::Unauthenticated {
1379            connection,
1380            pending_auth_method,
1381            configuration_view,
1382            ..
1383        } = &mut self.thread_state
1384        else {
1385            return;
1386        };
1387
1388        if method.0.as_ref() == "gemini-api-key" {
1389            let registry = LanguageModelRegistry::global(cx);
1390            let provider = registry
1391                .read(cx)
1392                .provider(&language_model::GOOGLE_PROVIDER_ID)
1393                .unwrap();
1394            if !provider.is_authenticated(cx) {
1395                let this = cx.weak_entity();
1396                let agent = self.agent.clone();
1397                let connection = connection.clone();
1398                window.defer(cx, |window, cx| {
1399                    Self::handle_auth_required(
1400                        this,
1401                        AuthRequired {
1402                            description: Some("GEMINI_API_KEY must be set".to_owned()),
1403                            provider_id: Some(language_model::GOOGLE_PROVIDER_ID),
1404                        },
1405                        agent,
1406                        connection,
1407                        window,
1408                        cx,
1409                    );
1410                });
1411                return;
1412            }
1413        } else if method.0.as_ref() == "anthropic-api-key" {
1414            let registry = LanguageModelRegistry::global(cx);
1415            let provider = registry
1416                .read(cx)
1417                .provider(&language_model::ANTHROPIC_PROVIDER_ID)
1418                .unwrap();
1419            let this = cx.weak_entity();
1420            let agent = self.agent.clone();
1421            let connection = connection.clone();
1422            window.defer(cx, move |window, cx| {
1423                if !provider.is_authenticated(cx) {
1424                    Self::handle_auth_required(
1425                        this,
1426                        AuthRequired {
1427                            description: Some("ANTHROPIC_API_KEY must be set".to_owned()),
1428                            provider_id: Some(language_model::ANTHROPIC_PROVIDER_ID),
1429                        },
1430                        agent,
1431                        connection,
1432                        window,
1433                        cx,
1434                    );
1435                } else {
1436                    this.update(cx, |this, cx| {
1437                        this.thread_state = Self::initial_state(
1438                            agent,
1439                            None,
1440                            this.workspace.clone(),
1441                            this.project.clone(),
1442                            window,
1443                            cx,
1444                        )
1445                    })
1446                    .ok();
1447                }
1448            });
1449            return;
1450        } else if method.0.as_ref() == "vertex-ai"
1451            && std::env::var("GOOGLE_API_KEY").is_err()
1452            && (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()
1453                || (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()))
1454        {
1455            let this = cx.weak_entity();
1456            let agent = self.agent.clone();
1457            let connection = connection.clone();
1458
1459            window.defer(cx, |window, cx| {
1460                    Self::handle_auth_required(
1461                        this,
1462                        AuthRequired {
1463                            description: Some(
1464                                "GOOGLE_API_KEY must be set in the environment to use Vertex AI authentication for Gemini CLI. Please export it and restart Zed."
1465                                    .to_owned(),
1466                            ),
1467                            provider_id: None,
1468                        },
1469                        agent,
1470                        connection,
1471                        window,
1472                        cx,
1473                    )
1474                });
1475            return;
1476        }
1477
1478        self.thread_error.take();
1479        configuration_view.take();
1480        pending_auth_method.replace(method.clone());
1481        let authenticate = if (method.0.as_ref() == "claude-login"
1482            || method.0.as_ref() == "spawn-gemini-cli")
1483            && let Some(login) = self.login.clone()
1484        {
1485            if let Some(workspace) = self.workspace.upgrade() {
1486                Self::spawn_external_agent_login(login, workspace, false, window, cx)
1487            } else {
1488                Task::ready(Ok(()))
1489            }
1490        } else {
1491            connection.authenticate(method, cx)
1492        };
1493        cx.notify();
1494        self.auth_task =
1495            Some(cx.spawn_in(window, {
1496                let agent = self.agent.clone();
1497                async move |this, cx| {
1498                    let result = authenticate.await;
1499
1500                    match &result {
1501                        Ok(_) => telemetry::event!(
1502                            "Authenticate Agent Succeeded",
1503                            agent = agent.telemetry_id()
1504                        ),
1505                        Err(_) => {
1506                            telemetry::event!(
1507                                "Authenticate Agent Failed",
1508                                agent = agent.telemetry_id(),
1509                            )
1510                        }
1511                    }
1512
1513                    this.update_in(cx, |this, window, cx| {
1514                        if let Err(err) = result {
1515                            if let ThreadState::Unauthenticated {
1516                                pending_auth_method,
1517                                ..
1518                            } = &mut this.thread_state
1519                            {
1520                                pending_auth_method.take();
1521                            }
1522                            this.handle_thread_error(err, cx);
1523                        } else {
1524                            this.reset(window, cx);
1525                        }
1526                        this.auth_task.take()
1527                    })
1528                    .ok();
1529                }
1530            }));
1531    }
1532
1533    fn spawn_external_agent_login(
1534        login: task::SpawnInTerminal,
1535        workspace: Entity<Workspace>,
1536        previous_attempt: bool,
1537        window: &mut Window,
1538        cx: &mut App,
1539    ) -> Task<Result<()>> {
1540        let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
1541            return Task::ready(Ok(()));
1542        };
1543        let project = workspace.read(cx).project().clone();
1544        let cwd = project.read(cx).first_project_directory(cx);
1545        let shell = project.read(cx).terminal_settings(&cwd, cx).shell.clone();
1546
1547        window.spawn(cx, async move |cx| {
1548            let mut task = login.clone();
1549            task.command = task
1550                .command
1551                .map(|command| anyhow::Ok(shlex::try_quote(&command)?.to_string()))
1552                .transpose()?;
1553            task.args = task
1554                .args
1555                .iter()
1556                .map(|arg| {
1557                    Ok(shlex::try_quote(arg)
1558                        .context("Failed to quote argument")?
1559                        .to_string())
1560                })
1561                .collect::<Result<Vec<_>>>()?;
1562            task.full_label = task.label.clone();
1563            task.id = task::TaskId(format!("external-agent-{}-login", task.label));
1564            task.command_label = task.label.clone();
1565            task.use_new_terminal = true;
1566            task.allow_concurrent_runs = true;
1567            task.hide = task::HideStrategy::Always;
1568            task.shell = shell;
1569
1570            let terminal = terminal_panel.update_in(cx, |terminal_panel, window, cx| {
1571                terminal_panel.spawn_task(&login, window, cx)
1572            })?;
1573
1574            let terminal = terminal.await?;
1575            let mut exit_status = terminal
1576                .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
1577                .fuse();
1578
1579            let logged_in = cx
1580                .spawn({
1581                    let terminal = terminal.clone();
1582                    async move |cx| {
1583                        loop {
1584                            cx.background_executor().timer(Duration::from_secs(1)).await;
1585                            let content =
1586                                terminal.update(cx, |terminal, _cx| terminal.get_content())?;
1587                            if content.contains("Login successful")
1588                                || content.contains("Type your message")
1589                            {
1590                                return anyhow::Ok(());
1591                            }
1592                        }
1593                    }
1594                })
1595                .fuse();
1596            futures::pin_mut!(logged_in);
1597            futures::select_biased! {
1598                result = logged_in => {
1599                    if let Err(e) = result {
1600                        log::error!("{e}");
1601                        return Err(anyhow!("exited before logging in"));
1602                    }
1603                }
1604                _ = exit_status => {
1605                    if !previous_attempt && project.read_with(cx, |project, _| project.is_via_remote_server())? && login.label.contains("gemini") {
1606                        return cx.update(|window, cx| Self::spawn_external_agent_login(login, workspace, true, window, cx))?.await
1607                    }
1608                    return Err(anyhow!("exited before logging in"));
1609                }
1610            }
1611            terminal.update(cx, |terminal, _| terminal.kill_active_task())?;
1612            Ok(())
1613        })
1614    }
1615
1616    fn authorize_tool_call(
1617        &mut self,
1618        tool_call_id: acp::ToolCallId,
1619        option_id: acp::PermissionOptionId,
1620        option_kind: acp::PermissionOptionKind,
1621        window: &mut Window,
1622        cx: &mut Context<Self>,
1623    ) {
1624        let Some(thread) = self.thread() else {
1625            return;
1626        };
1627        thread.update(cx, |thread, cx| {
1628            thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx);
1629        });
1630        if self.should_be_following {
1631            self.workspace
1632                .update(cx, |workspace, cx| {
1633                    workspace.follow(CollaboratorId::Agent, window, cx);
1634                })
1635                .ok();
1636        }
1637        cx.notify();
1638    }
1639
1640    fn restore_checkpoint(&mut self, message_id: &UserMessageId, cx: &mut Context<Self>) {
1641        let Some(thread) = self.thread() else {
1642            return;
1643        };
1644
1645        thread
1646            .update(cx, |thread, cx| {
1647                thread.restore_checkpoint(message_id.clone(), cx)
1648            })
1649            .detach_and_log_err(cx);
1650    }
1651
1652    fn render_entry(
1653        &self,
1654        entry_ix: usize,
1655        total_entries: usize,
1656        entry: &AgentThreadEntry,
1657        window: &mut Window,
1658        cx: &Context<Self>,
1659    ) -> AnyElement {
1660        let primary = match &entry {
1661            AgentThreadEntry::UserMessage(message) => {
1662                let Some(editor) = self
1663                    .entry_view_state
1664                    .read(cx)
1665                    .entry(entry_ix)
1666                    .and_then(|entry| entry.message_editor())
1667                    .cloned()
1668                else {
1669                    return Empty.into_any_element();
1670                };
1671
1672                let editing = self.editing_message == Some(entry_ix);
1673                let editor_focus = editor.focus_handle(cx).is_focused(window);
1674                let focus_border = cx.theme().colors().border_focused;
1675
1676                let rules_item = if entry_ix == 0 {
1677                    self.render_rules_item(cx)
1678                } else {
1679                    None
1680                };
1681
1682                let has_checkpoint_button = message
1683                    .checkpoint
1684                    .as_ref()
1685                    .is_some_and(|checkpoint| checkpoint.show);
1686
1687                let agent_name = self.agent.name();
1688
1689                v_flex()
1690                    .id(("user_message", entry_ix))
1691                    .map(|this| {
1692                        if entry_ix == 0 && !has_checkpoint_button && rules_item.is_none()  {
1693                            this.pt(rems_from_px(18.))
1694                        } else if rules_item.is_some() {
1695                            this.pt_3()
1696                        } else {
1697                            this.pt_2()
1698                        }
1699                    })
1700                    .pb_3()
1701                    .px_2()
1702                    .gap_1p5()
1703                    .w_full()
1704                    .children(rules_item)
1705                    .children(message.id.clone().and_then(|message_id| {
1706                        message.checkpoint.as_ref()?.show.then(|| {
1707                            h_flex()
1708                                .px_3()
1709                                .gap_2()
1710                                .child(Divider::horizontal())
1711                                .child(
1712                                    Button::new("restore-checkpoint", "Restore Checkpoint")
1713                                        .icon(IconName::Undo)
1714                                        .icon_size(IconSize::XSmall)
1715                                        .icon_position(IconPosition::Start)
1716                                        .label_size(LabelSize::XSmall)
1717                                        .icon_color(Color::Muted)
1718                                        .color(Color::Muted)
1719                                        .tooltip(Tooltip::text("Restores all files in the project to the content they had at this point in the conversation."))
1720                                        .on_click(cx.listener(move |this, _, _window, cx| {
1721                                            this.restore_checkpoint(&message_id, cx);
1722                                        }))
1723                                )
1724                                .child(Divider::horizontal())
1725                        })
1726                    }))
1727                    .child(
1728                        div()
1729                            .relative()
1730                            .child(
1731                                div()
1732                                    .py_3()
1733                                    .px_2()
1734                                    .rounded_md()
1735                                    .shadow_md()
1736                                    .bg(cx.theme().colors().editor_background)
1737                                    .border_1()
1738                                    .when(editing && !editor_focus, |this| this.border_dashed())
1739                                    .border_color(cx.theme().colors().border)
1740                                    .map(|this|{
1741                                        if editing && editor_focus {
1742                                            this.border_color(focus_border)
1743                                        } else if message.id.is_some() {
1744                                            this.hover(|s| s.border_color(focus_border.opacity(0.8)))
1745                                        } else {
1746                                            this
1747                                        }
1748                                    })
1749                                    .text_xs()
1750                                    .child(editor.clone().into_any_element()),
1751                            )
1752                            .when(editor_focus, |this| {
1753                                let base_container = h_flex()
1754                                    .absolute()
1755                                    .top_neg_3p5()
1756                                    .right_3()
1757                                    .gap_1()
1758                                    .rounded_sm()
1759                                    .border_1()
1760                                    .border_color(cx.theme().colors().border)
1761                                    .bg(cx.theme().colors().editor_background)
1762                                    .overflow_hidden();
1763
1764                                if message.id.is_some() {
1765                                    this.child(
1766                                        base_container
1767                                            .child(
1768                                                IconButton::new("cancel", IconName::Close)
1769                                                    .disabled(self.is_loading_contents)
1770                                                    .icon_color(Color::Error)
1771                                                    .icon_size(IconSize::XSmall)
1772                                                    .on_click(cx.listener(Self::cancel_editing))
1773                                            )
1774                                            .child(
1775                                                if self.is_loading_contents {
1776                                                    div()
1777                                                        .id("loading-edited-message-content")
1778                                                        .tooltip(Tooltip::text("Loading Added Context…"))
1779                                                        .child(loading_contents_spinner(IconSize::XSmall))
1780                                                        .into_any_element()
1781                                                } else {
1782                                                    IconButton::new("regenerate", IconName::Return)
1783                                                        .icon_color(Color::Muted)
1784                                                        .icon_size(IconSize::XSmall)
1785                                                        .tooltip(Tooltip::text(
1786                                                            "Editing will restart the thread from this point."
1787                                                        ))
1788                                                        .on_click(cx.listener({
1789                                                            let editor = editor.clone();
1790                                                            move |this, _, window, cx| {
1791                                                                this.regenerate(
1792                                                                    entry_ix, editor.clone(), window, cx,
1793                                                                );
1794                                                            }
1795                                                        })).into_any_element()
1796                                                }
1797                                            )
1798                                    )
1799                                } else {
1800                                    this.child(
1801                                        base_container
1802                                            .border_dashed()
1803                                            .child(
1804                                                IconButton::new("editing_unavailable", IconName::PencilUnavailable)
1805                                                    .icon_size(IconSize::Small)
1806                                                    .icon_color(Color::Muted)
1807                                                    .style(ButtonStyle::Transparent)
1808                                                    .tooltip(move |_window, cx| {
1809                                                        cx.new(|_| UnavailableEditingTooltip::new(agent_name.clone()))
1810                                                            .into()
1811                                                    })
1812                                            )
1813                                    )
1814                                }
1815                            }),
1816                    )
1817                    .into_any()
1818            }
1819            AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
1820                let is_last = entry_ix + 1 == total_entries;
1821
1822                let style = default_markdown_style(false, false, window, cx);
1823                let message_body = v_flex()
1824                    .w_full()
1825                    .gap_3()
1826                    .children(chunks.iter().enumerate().filter_map(
1827                        |(chunk_ix, chunk)| match chunk {
1828                            AssistantMessageChunk::Message { block } => {
1829                                block.markdown().map(|md| {
1830                                    self.render_markdown(md.clone(), style.clone())
1831                                        .into_any_element()
1832                                })
1833                            }
1834                            AssistantMessageChunk::Thought { block } => {
1835                                block.markdown().map(|md| {
1836                                    self.render_thinking_block(
1837                                        entry_ix,
1838                                        chunk_ix,
1839                                        md.clone(),
1840                                        window,
1841                                        cx,
1842                                    )
1843                                    .into_any_element()
1844                                })
1845                            }
1846                        },
1847                    ))
1848                    .into_any();
1849
1850                v_flex()
1851                    .px_5()
1852                    .py_1p5()
1853                    .when(is_last, |this| this.pb_4())
1854                    .w_full()
1855                    .text_ui(cx)
1856                    .child(message_body)
1857                    .into_any()
1858            }
1859            AgentThreadEntry::ToolCall(tool_call) => {
1860                let has_terminals = tool_call.terminals().next().is_some();
1861
1862                div().w_full().map(|this| {
1863                    if has_terminals {
1864                        this.children(tool_call.terminals().map(|terminal| {
1865                            self.render_terminal_tool_call(
1866                                entry_ix, terminal, tool_call, window, cx,
1867                            )
1868                        }))
1869                    } else {
1870                        this.child(self.render_tool_call(entry_ix, tool_call, window, cx))
1871                    }
1872                })
1873            }
1874            .into_any(),
1875        };
1876
1877        let Some(thread) = self.thread() else {
1878            return primary;
1879        };
1880
1881        let primary = if entry_ix == total_entries - 1 {
1882            v_flex()
1883                .w_full()
1884                .child(primary)
1885                .child(self.render_thread_controls(&thread, cx))
1886                .when_some(
1887                    self.thread_feedback.comments_editor.clone(),
1888                    |this, editor| this.child(Self::render_feedback_feedback_editor(editor, cx)),
1889                )
1890                .into_any_element()
1891        } else {
1892            primary
1893        };
1894
1895        if let Some(editing_index) = self.editing_message.as_ref()
1896            && *editing_index < entry_ix
1897        {
1898            let backdrop = div()
1899                .id(("backdrop", entry_ix))
1900                .size_full()
1901                .absolute()
1902                .inset_0()
1903                .bg(cx.theme().colors().panel_background)
1904                .opacity(0.8)
1905                .block_mouse_except_scroll()
1906                .on_click(cx.listener(Self::cancel_editing));
1907
1908            div()
1909                .relative()
1910                .child(primary)
1911                .child(backdrop)
1912                .into_any_element()
1913        } else {
1914            primary
1915        }
1916    }
1917
1918    fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
1919        cx.theme()
1920            .colors()
1921            .element_background
1922            .blend(cx.theme().colors().editor_foreground.opacity(0.025))
1923    }
1924
1925    fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
1926        cx.theme().colors().border.opacity(0.8)
1927    }
1928
1929    fn tool_name_font_size(&self) -> Rems {
1930        rems_from_px(13.)
1931    }
1932
1933    fn render_thinking_block(
1934        &self,
1935        entry_ix: usize,
1936        chunk_ix: usize,
1937        chunk: Entity<Markdown>,
1938        window: &Window,
1939        cx: &Context<Self>,
1940    ) -> AnyElement {
1941        let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix));
1942        let card_header_id = SharedString::from("inner-card-header");
1943
1944        let key = (entry_ix, chunk_ix);
1945
1946        let is_open = self.expanded_thinking_blocks.contains(&key);
1947
1948        let scroll_handle = self
1949            .entry_view_state
1950            .read(cx)
1951            .entry(entry_ix)
1952            .and_then(|entry| entry.scroll_handle_for_assistant_message_chunk(chunk_ix));
1953
1954        let thinking_content = {
1955            div()
1956                .id(("thinking-content", chunk_ix))
1957                .when_some(scroll_handle, |this, scroll_handle| {
1958                    this.track_scroll(&scroll_handle)
1959                })
1960                .text_ui_sm(cx)
1961                .overflow_hidden()
1962                .child(
1963                    self.render_markdown(chunk, default_markdown_style(false, false, window, cx)),
1964                )
1965        };
1966
1967        v_flex()
1968            .gap_1()
1969            .child(
1970                h_flex()
1971                    .id(header_id)
1972                    .group(&card_header_id)
1973                    .relative()
1974                    .w_full()
1975                    .pr_1()
1976                    .justify_between()
1977                    .child(
1978                        h_flex()
1979                            .h(window.line_height() - px(2.))
1980                            .gap_1p5()
1981                            .overflow_hidden()
1982                            .child(
1983                                Icon::new(IconName::ToolThink)
1984                                    .size(IconSize::Small)
1985                                    .color(Color::Muted),
1986                            )
1987                            .child(
1988                                div()
1989                                    .text_size(self.tool_name_font_size())
1990                                    .text_color(cx.theme().colors().text_muted)
1991                                    .child("Thinking"),
1992                            ),
1993                    )
1994                    .child(
1995                        Disclosure::new(("expand", entry_ix), is_open)
1996                            .opened_icon(IconName::ChevronUp)
1997                            .closed_icon(IconName::ChevronDown)
1998                            .visible_on_hover(&card_header_id)
1999                            .on_click(cx.listener({
2000                                move |this, _event, _window, cx| {
2001                                    if is_open {
2002                                        this.expanded_thinking_blocks.remove(&key);
2003                                    } else {
2004                                        this.expanded_thinking_blocks.insert(key);
2005                                    }
2006                                    cx.notify();
2007                                }
2008                            })),
2009                    )
2010                    .on_click(cx.listener({
2011                        move |this, _event, _window, cx| {
2012                            if is_open {
2013                                this.expanded_thinking_blocks.remove(&key);
2014                            } else {
2015                                this.expanded_thinking_blocks.insert(key);
2016                            }
2017                            cx.notify();
2018                        }
2019                    })),
2020            )
2021            .when(is_open, |this| {
2022                this.child(
2023                    div()
2024                        .ml_1p5()
2025                        .pl_3p5()
2026                        .border_l_1()
2027                        .border_color(self.tool_card_border_color(cx))
2028                        .child(thinking_content),
2029                )
2030            })
2031            .into_any_element()
2032    }
2033
2034    fn render_tool_call(
2035        &self,
2036        entry_ix: usize,
2037        tool_call: &ToolCall,
2038        window: &Window,
2039        cx: &Context<Self>,
2040    ) -> Div {
2041        let has_location = tool_call.locations.len() == 1;
2042        let card_header_id = SharedString::from("inner-tool-call-header");
2043
2044        let tool_icon = if tool_call.kind == acp::ToolKind::Edit && has_location {
2045            FileIcons::get_icon(&tool_call.locations[0].path, cx)
2046                .map(Icon::from_path)
2047                .unwrap_or(Icon::new(IconName::ToolPencil))
2048        } else {
2049            Icon::new(match tool_call.kind {
2050                acp::ToolKind::Read => IconName::ToolSearch,
2051                acp::ToolKind::Edit => IconName::ToolPencil,
2052                acp::ToolKind::Delete => IconName::ToolDeleteFile,
2053                acp::ToolKind::Move => IconName::ArrowRightLeft,
2054                acp::ToolKind::Search => IconName::ToolSearch,
2055                acp::ToolKind::Execute => IconName::ToolTerminal,
2056                acp::ToolKind::Think => IconName::ToolThink,
2057                acp::ToolKind::Fetch => IconName::ToolWeb,
2058                acp::ToolKind::Other => IconName::ToolHammer,
2059            })
2060        }
2061        .size(IconSize::Small)
2062        .color(Color::Muted);
2063
2064        let failed_or_canceled = match &tool_call.status {
2065            ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => true,
2066            _ => false,
2067        };
2068
2069        let needs_confirmation = matches!(
2070            tool_call.status,
2071            ToolCallStatus::WaitingForConfirmation { .. }
2072        );
2073        let is_edit =
2074            matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some();
2075        let use_card_layout = needs_confirmation || is_edit;
2076
2077        let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
2078
2079        let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
2080
2081        let gradient_overlay = {
2082            div()
2083                .absolute()
2084                .top_0()
2085                .right_0()
2086                .w_12()
2087                .h_full()
2088                .map(|this| {
2089                    if use_card_layout {
2090                        this.bg(linear_gradient(
2091                            90.,
2092                            linear_color_stop(self.tool_card_header_bg(cx), 1.),
2093                            linear_color_stop(self.tool_card_header_bg(cx).opacity(0.2), 0.),
2094                        ))
2095                    } else {
2096                        this.bg(linear_gradient(
2097                            90.,
2098                            linear_color_stop(cx.theme().colors().panel_background, 1.),
2099                            linear_color_stop(
2100                                cx.theme().colors().panel_background.opacity(0.2),
2101                                0.,
2102                            ),
2103                        ))
2104                    }
2105                })
2106        };
2107
2108        let tool_output_display = if is_open {
2109            match &tool_call.status {
2110                ToolCallStatus::WaitingForConfirmation { options, .. } => v_flex()
2111                    .w_full()
2112                    .children(tool_call.content.iter().map(|content| {
2113                        div()
2114                            .child(self.render_tool_call_content(
2115                                entry_ix,
2116                                content,
2117                                tool_call,
2118                                use_card_layout,
2119                                window,
2120                                cx,
2121                            ))
2122                            .into_any_element()
2123                    }))
2124                    .child(self.render_permission_buttons(
2125                        options,
2126                        entry_ix,
2127                        tool_call.id.clone(),
2128                        cx,
2129                    ))
2130                    .into_any(),
2131                ToolCallStatus::Pending | ToolCallStatus::InProgress
2132                    if is_edit
2133                        && tool_call.content.is_empty()
2134                        && self.as_native_connection(cx).is_some() =>
2135                {
2136                    self.render_diff_loading(cx).into_any()
2137                }
2138                ToolCallStatus::Pending
2139                | ToolCallStatus::InProgress
2140                | ToolCallStatus::Completed
2141                | ToolCallStatus::Failed
2142                | ToolCallStatus::Canceled => v_flex()
2143                    .w_full()
2144                    .children(tool_call.content.iter().map(|content| {
2145                        div().child(self.render_tool_call_content(
2146                            entry_ix,
2147                            content,
2148                            tool_call,
2149                            use_card_layout,
2150                            window,
2151                            cx,
2152                        ))
2153                    }))
2154                    .into_any(),
2155                ToolCallStatus::Rejected => Empty.into_any(),
2156            }
2157            .into()
2158        } else {
2159            None
2160        };
2161
2162        v_flex()
2163            .map(|this| {
2164                if use_card_layout {
2165                    this.my_1p5()
2166                        .rounded_md()
2167                        .border_1()
2168                        .border_color(self.tool_card_border_color(cx))
2169                        .bg(cx.theme().colors().editor_background)
2170                        .overflow_hidden()
2171                } else {
2172                    this.my_1()
2173                }
2174            })
2175            .map(|this| {
2176                if has_location && !use_card_layout {
2177                    this.ml_4()
2178                } else {
2179                    this.ml_5()
2180                }
2181            })
2182            .mr_5()
2183            .child(
2184                h_flex()
2185                    .group(&card_header_id)
2186                    .relative()
2187                    .w_full()
2188                    .gap_1()
2189                    .justify_between()
2190                    .when(use_card_layout, |this| {
2191                        this.p_0p5()
2192                            .rounded_t(rems_from_px(5.))
2193                            .bg(self.tool_card_header_bg(cx))
2194                    })
2195                    .child(
2196                        h_flex()
2197                            .relative()
2198                            .w_full()
2199                            .h(window.line_height() - px(2.))
2200                            .text_size(self.tool_name_font_size())
2201                            .gap_1p5()
2202                            .when(has_location || use_card_layout, |this| this.px_1())
2203                            .when(has_location, |this| {
2204                                this.cursor(CursorStyle::PointingHand)
2205                                    .rounded(rems_from_px(3.)) // Concentric border radius
2206                                    .hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.5)))
2207                            })
2208                            .overflow_hidden()
2209                            .child(tool_icon)
2210                            .child(if has_location {
2211                                h_flex()
2212                                    .id(("open-tool-call-location", entry_ix))
2213                                    .w_full()
2214                                    .map(|this| {
2215                                        if use_card_layout {
2216                                            this.text_color(cx.theme().colors().text)
2217                                        } else {
2218                                            this.text_color(cx.theme().colors().text_muted)
2219                                        }
2220                                    })
2221                                    .child(self.render_markdown(
2222                                        tool_call.label.clone(),
2223                                        MarkdownStyle {
2224                                            prevent_mouse_interaction: true,
2225                                            ..default_markdown_style(false, true, window, cx)
2226                                        },
2227                                    ))
2228                                    .tooltip(Tooltip::text("Jump to File"))
2229                                    .on_click(cx.listener(move |this, _, window, cx| {
2230                                        this.open_tool_call_location(entry_ix, 0, window, cx);
2231                                    }))
2232                                    .into_any_element()
2233                            } else {
2234                                h_flex()
2235                                    .w_full()
2236                                    .child(self.render_markdown(
2237                                        tool_call.label.clone(),
2238                                        default_markdown_style(false, true, window, cx),
2239                                    ))
2240                                    .into_any()
2241                            })
2242                            .when(!has_location, |this| this.child(gradient_overlay)),
2243                    )
2244                    .when(is_collapsible || failed_or_canceled, |this| {
2245                        this.child(
2246                            h_flex()
2247                                .px_1()
2248                                .gap_px()
2249                                .when(is_collapsible, |this| {
2250                                    this.child(
2251                                    Disclosure::new(("expand", entry_ix), is_open)
2252                                        .opened_icon(IconName::ChevronUp)
2253                                        .closed_icon(IconName::ChevronDown)
2254                                        .visible_on_hover(&card_header_id)
2255                                        .on_click(cx.listener({
2256                                            let id = tool_call.id.clone();
2257                                            move |this: &mut Self, _, _, cx: &mut Context<Self>| {
2258                                                if is_open {
2259                                                    this.expanded_tool_calls.remove(&id);
2260                                                } else {
2261                                                    this.expanded_tool_calls.insert(id.clone());
2262                                                }
2263                                                cx.notify();
2264                                            }
2265                                        })),
2266                                )
2267                                })
2268                                .when(failed_or_canceled, |this| {
2269                                    this.child(
2270                                        Icon::new(IconName::Close)
2271                                            .color(Color::Error)
2272                                            .size(IconSize::Small),
2273                                    )
2274                                }),
2275                        )
2276                    }),
2277            )
2278            .children(tool_output_display)
2279    }
2280
2281    fn render_tool_call_content(
2282        &self,
2283        entry_ix: usize,
2284        content: &ToolCallContent,
2285        tool_call: &ToolCall,
2286        card_layout: bool,
2287        window: &Window,
2288        cx: &Context<Self>,
2289    ) -> AnyElement {
2290        match content {
2291            ToolCallContent::ContentBlock(content) => {
2292                if let Some(resource_link) = content.resource_link() {
2293                    self.render_resource_link(resource_link, cx)
2294                } else if let Some(markdown) = content.markdown() {
2295                    self.render_markdown_output(
2296                        markdown.clone(),
2297                        tool_call.id.clone(),
2298                        card_layout,
2299                        window,
2300                        cx,
2301                    )
2302                } else {
2303                    Empty.into_any_element()
2304                }
2305            }
2306            ToolCallContent::Diff(diff) => self.render_diff_editor(entry_ix, diff, tool_call, cx),
2307            ToolCallContent::Terminal(terminal) => {
2308                self.render_terminal_tool_call(entry_ix, terminal, tool_call, window, cx)
2309            }
2310        }
2311    }
2312
2313    fn render_markdown_output(
2314        &self,
2315        markdown: Entity<Markdown>,
2316        tool_call_id: acp::ToolCallId,
2317        card_layout: bool,
2318        window: &Window,
2319        cx: &Context<Self>,
2320    ) -> AnyElement {
2321        let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id));
2322
2323        v_flex()
2324            .mt_1p5()
2325            .gap_2()
2326            .when(!card_layout, |this| {
2327                this.ml(rems(0.4))
2328                    .px_3p5()
2329                    .border_l_1()
2330                    .border_color(self.tool_card_border_color(cx))
2331            })
2332            .when(card_layout, |this| {
2333                this.p_2()
2334                    .border_t_1()
2335                    .border_color(self.tool_card_border_color(cx))
2336            })
2337            .text_sm()
2338            .text_color(cx.theme().colors().text_muted)
2339            .child(self.render_markdown(markdown, default_markdown_style(false, false, window, cx)))
2340            .when(!card_layout, |this| {
2341                this.child(
2342                    IconButton::new(button_id, IconName::ChevronUp)
2343                        .full_width()
2344                        .style(ButtonStyle::Outlined)
2345                        .icon_color(Color::Muted)
2346                        .on_click(cx.listener({
2347                            move |this: &mut Self, _, _, cx: &mut Context<Self>| {
2348                                this.expanded_tool_calls.remove(&tool_call_id);
2349                                cx.notify();
2350                            }
2351                        })),
2352                )
2353            })
2354            .into_any_element()
2355    }
2356
2357    fn render_resource_link(
2358        &self,
2359        resource_link: &acp::ResourceLink,
2360        cx: &Context<Self>,
2361    ) -> AnyElement {
2362        let uri: SharedString = resource_link.uri.clone().into();
2363        let is_file = resource_link.uri.strip_prefix("file://");
2364
2365        let label: SharedString = if let Some(abs_path) = is_file {
2366            if let Some(project_path) = self
2367                .project
2368                .read(cx)
2369                .project_path_for_absolute_path(&Path::new(abs_path), cx)
2370                && let Some(worktree) = self
2371                    .project
2372                    .read(cx)
2373                    .worktree_for_id(project_path.worktree_id, cx)
2374            {
2375                worktree
2376                    .read(cx)
2377                    .full_path(&project_path.path)
2378                    .to_string_lossy()
2379                    .to_string()
2380                    .into()
2381            } else {
2382                abs_path.to_string().into()
2383            }
2384        } else {
2385            uri.clone()
2386        };
2387
2388        let button_id = SharedString::from(format!("item-{}", uri));
2389
2390        div()
2391            .ml(rems(0.4))
2392            .pl_2p5()
2393            .border_l_1()
2394            .border_color(self.tool_card_border_color(cx))
2395            .overflow_hidden()
2396            .child(
2397                Button::new(button_id, label)
2398                    .label_size(LabelSize::Small)
2399                    .color(Color::Muted)
2400                    .truncate(true)
2401                    .when(is_file.is_none(), |this| {
2402                        this.icon(IconName::ArrowUpRight)
2403                            .icon_size(IconSize::XSmall)
2404                            .icon_color(Color::Muted)
2405                    })
2406                    .on_click(cx.listener({
2407                        let workspace = self.workspace.clone();
2408                        move |_, _, window, cx: &mut Context<Self>| {
2409                            Self::open_link(uri.clone(), &workspace, window, cx);
2410                        }
2411                    })),
2412            )
2413            .into_any_element()
2414    }
2415
2416    fn render_permission_buttons(
2417        &self,
2418        options: &[acp::PermissionOption],
2419        entry_ix: usize,
2420        tool_call_id: acp::ToolCallId,
2421        cx: &Context<Self>,
2422    ) -> Div {
2423        h_flex()
2424            .py_1()
2425            .pl_2()
2426            .pr_1()
2427            .gap_1()
2428            .justify_between()
2429            .flex_wrap()
2430            .border_t_1()
2431            .border_color(self.tool_card_border_color(cx))
2432            .child(
2433                div()
2434                    .min_w(rems_from_px(145.))
2435                    .child(LoadingLabel::new("Waiting for Confirmation").size(LabelSize::Small)),
2436            )
2437            .child(h_flex().gap_0p5().children(options.iter().map(|option| {
2438                let option_id = SharedString::from(option.id.0.clone());
2439                Button::new((option_id, entry_ix), option.name.clone())
2440                    .map(|this| match option.kind {
2441                        acp::PermissionOptionKind::AllowOnce => {
2442                            this.icon(IconName::Check).icon_color(Color::Success)
2443                        }
2444                        acp::PermissionOptionKind::AllowAlways => {
2445                            this.icon(IconName::CheckDouble).icon_color(Color::Success)
2446                        }
2447                        acp::PermissionOptionKind::RejectOnce => {
2448                            this.icon(IconName::Close).icon_color(Color::Error)
2449                        }
2450                        acp::PermissionOptionKind::RejectAlways => {
2451                            this.icon(IconName::Close).icon_color(Color::Error)
2452                        }
2453                    })
2454                    .icon_position(IconPosition::Start)
2455                    .icon_size(IconSize::XSmall)
2456                    .label_size(LabelSize::Small)
2457                    .on_click(cx.listener({
2458                        let tool_call_id = tool_call_id.clone();
2459                        let option_id = option.id.clone();
2460                        let option_kind = option.kind;
2461                        move |this, _, window, cx| {
2462                            this.authorize_tool_call(
2463                                tool_call_id.clone(),
2464                                option_id.clone(),
2465                                option_kind,
2466                                window,
2467                                cx,
2468                            );
2469                        }
2470                    }))
2471            })))
2472    }
2473
2474    fn render_diff_loading(&self, cx: &Context<Self>) -> AnyElement {
2475        let bar = |n: u64, width_class: &str| {
2476            let bg_color = cx.theme().colors().element_active;
2477            let base = h_flex().h_1().rounded_full();
2478
2479            let modified = match width_class {
2480                "w_4_5" => base.w_3_4(),
2481                "w_1_4" => base.w_1_4(),
2482                "w_2_4" => base.w_2_4(),
2483                "w_3_5" => base.w_3_5(),
2484                "w_2_5" => base.w_2_5(),
2485                _ => base.w_1_2(),
2486            };
2487
2488            modified.with_animation(
2489                ElementId::Integer(n),
2490                Animation::new(Duration::from_secs(2)).repeat(),
2491                move |tab, delta| {
2492                    let delta = (delta - 0.15 * n as f32) / 0.7;
2493                    let delta = 1.0 - (0.5 - delta).abs() * 2.;
2494                    let delta = ease_in_out(delta.clamp(0., 1.));
2495                    let delta = 0.1 + 0.9 * delta;
2496
2497                    tab.bg(bg_color.opacity(delta))
2498                },
2499            )
2500        };
2501
2502        v_flex()
2503            .p_3()
2504            .gap_1()
2505            .rounded_b_md()
2506            .bg(cx.theme().colors().editor_background)
2507            .child(bar(0, "w_4_5"))
2508            .child(bar(1, "w_1_4"))
2509            .child(bar(2, "w_2_4"))
2510            .child(bar(3, "w_3_5"))
2511            .child(bar(4, "w_2_5"))
2512            .into_any_element()
2513    }
2514
2515    fn render_diff_editor(
2516        &self,
2517        entry_ix: usize,
2518        diff: &Entity<acp_thread::Diff>,
2519        tool_call: &ToolCall,
2520        cx: &Context<Self>,
2521    ) -> AnyElement {
2522        let tool_progress = matches!(
2523            &tool_call.status,
2524            ToolCallStatus::InProgress | ToolCallStatus::Pending
2525        );
2526
2527        v_flex()
2528            .h_full()
2529            .border_t_1()
2530            .border_color(self.tool_card_border_color(cx))
2531            .child(
2532                if let Some(entry) = self.entry_view_state.read(cx).entry(entry_ix)
2533                    && let Some(editor) = entry.editor_for_diff(diff)
2534                    && diff.read(cx).has_revealed_range(cx)
2535                {
2536                    editor.into_any_element()
2537                } else if tool_progress && self.as_native_connection(cx).is_some() {
2538                    self.render_diff_loading(cx)
2539                } else {
2540                    Empty.into_any()
2541                },
2542            )
2543            .into_any()
2544    }
2545
2546    fn render_terminal_tool_call(
2547        &self,
2548        entry_ix: usize,
2549        terminal: &Entity<acp_thread::Terminal>,
2550        tool_call: &ToolCall,
2551        window: &Window,
2552        cx: &Context<Self>,
2553    ) -> AnyElement {
2554        let terminal_data = terminal.read(cx);
2555        let working_dir = terminal_data.working_dir();
2556        let command = terminal_data.command();
2557        let started_at = terminal_data.started_at();
2558
2559        let tool_failed = matches!(
2560            &tool_call.status,
2561            ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed
2562        );
2563
2564        let output = terminal_data.output();
2565        let command_finished = output.is_some();
2566        let truncated_output =
2567            output.is_some_and(|output| output.original_content_len > output.content.len());
2568        let output_line_count = output.map(|output| output.content_line_count).unwrap_or(0);
2569
2570        let command_failed = command_finished
2571            && output.is_some_and(|o| o.exit_status.is_none_or(|status| !status.success()));
2572
2573        let time_elapsed = if let Some(output) = output {
2574            output.ended_at.duration_since(started_at)
2575        } else {
2576            started_at.elapsed()
2577        };
2578
2579        let header_id =
2580            SharedString::from(format!("terminal-tool-header-{}", terminal.entity_id()));
2581        let header_group = SharedString::from(format!(
2582            "terminal-tool-header-group-{}",
2583            terminal.entity_id()
2584        ));
2585        let header_bg = cx
2586            .theme()
2587            .colors()
2588            .element_background
2589            .blend(cx.theme().colors().editor_foreground.opacity(0.025));
2590        let border_color = cx.theme().colors().border.opacity(0.6);
2591
2592        let working_dir = working_dir
2593            .as_ref()
2594            .map(|path| format!("{}", path.display()))
2595            .unwrap_or_else(|| "current directory".to_string());
2596
2597        let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
2598
2599        let header = h_flex()
2600            .id(header_id)
2601            .flex_none()
2602            .gap_1()
2603            .justify_between()
2604            .rounded_t_md()
2605            .child(
2606                div()
2607                    .id(("command-target-path", terminal.entity_id()))
2608                    .w_full()
2609                    .max_w_full()
2610                    .overflow_x_scroll()
2611                    .child(
2612                        Label::new(working_dir)
2613                            .buffer_font(cx)
2614                            .size(LabelSize::XSmall)
2615                            .color(Color::Muted),
2616                    ),
2617            )
2618            .when(!command_finished, |header| {
2619                header
2620                    .gap_1p5()
2621                    .child(
2622                        Button::new(
2623                            SharedString::from(format!("stop-terminal-{}", terminal.entity_id())),
2624                            "Stop",
2625                        )
2626                        .icon(IconName::Stop)
2627                        .icon_position(IconPosition::Start)
2628                        .icon_size(IconSize::Small)
2629                        .icon_color(Color::Error)
2630                        .label_size(LabelSize::Small)
2631                        .tooltip(move |window, cx| {
2632                            Tooltip::with_meta(
2633                                "Stop This Command",
2634                                None,
2635                                "Also possible by placing your cursor inside the terminal and using regular terminal bindings.",
2636                                window,
2637                                cx,
2638                            )
2639                        })
2640                        .on_click({
2641                            let terminal = terminal.clone();
2642                            cx.listener(move |_this, _event, _window, cx| {
2643                                let inner_terminal = terminal.read(cx).inner().clone();
2644                                inner_terminal.update(cx, |inner_terminal, _cx| {
2645                                    inner_terminal.kill_active_task();
2646                                });
2647                            })
2648                        }),
2649                    )
2650                    .child(Divider::vertical())
2651                    .child(
2652                        Icon::new(IconName::ArrowCircle)
2653                            .size(IconSize::XSmall)
2654                            .color(Color::Info)
2655                            .with_rotate_animation(2)
2656                    )
2657            })
2658            .when(truncated_output, |header| {
2659                let tooltip = if let Some(output) = output {
2660                    if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
2661                       format!("Output exceeded terminal max lines and was \
2662                            truncated, the model received the first {}.", format_file_size(output.content.len() as u64, true))
2663                    } else {
2664                        format!(
2665                            "Output is {} long, and to avoid unexpected token usage, \
2666                                only {} was sent back to the agent.",
2667                            format_file_size(output.original_content_len as u64, true),
2668                             format_file_size(output.content.len() as u64, true)
2669                        )
2670                    }
2671                } else {
2672                    "Output was truncated".to_string()
2673                };
2674
2675                header.child(
2676                    h_flex()
2677                        .id(("terminal-tool-truncated-label", terminal.entity_id()))
2678                        .gap_1()
2679                        .child(
2680                            Icon::new(IconName::Info)
2681                                .size(IconSize::XSmall)
2682                                .color(Color::Ignored),
2683                        )
2684                        .child(
2685                            Label::new("Truncated")
2686                                .color(Color::Muted)
2687                                .size(LabelSize::XSmall),
2688                        )
2689                        .tooltip(Tooltip::text(tooltip)),
2690                )
2691            })
2692            .when(time_elapsed > Duration::from_secs(10), |header| {
2693                header.child(
2694                    Label::new(format!("({})", duration_alt_display(time_elapsed)))
2695                        .buffer_font(cx)
2696                        .color(Color::Muted)
2697                        .size(LabelSize::XSmall),
2698                )
2699            })
2700            .when(tool_failed || command_failed, |header| {
2701                header.child(
2702                    div()
2703                        .id(("terminal-tool-error-code-indicator", terminal.entity_id()))
2704                        .child(
2705                            Icon::new(IconName::Close)
2706                                .size(IconSize::Small)
2707                                .color(Color::Error),
2708                        )
2709                        .when_some(output.and_then(|o| o.exit_status), |this, status| {
2710                            this.tooltip(Tooltip::text(format!(
2711                                "Exited with code {}",
2712                                status.code().unwrap_or(-1),
2713                            )))
2714                        }),
2715                )
2716            })
2717            .child(
2718                Disclosure::new(
2719                    SharedString::from(format!(
2720                        "terminal-tool-disclosure-{}",
2721                        terminal.entity_id()
2722                    )),
2723                    is_expanded,
2724                )
2725                .opened_icon(IconName::ChevronUp)
2726                .closed_icon(IconName::ChevronDown)
2727                .visible_on_hover(&header_group)
2728                .on_click(cx.listener({
2729                    let id = tool_call.id.clone();
2730                    move |this, _event, _window, _cx| {
2731                        if is_expanded {
2732                            this.expanded_tool_calls.remove(&id);
2733                        } else {
2734                            this.expanded_tool_calls.insert(id.clone());
2735                        }
2736                    }
2737                })),
2738            );
2739
2740        let terminal_view = self
2741            .entry_view_state
2742            .read(cx)
2743            .entry(entry_ix)
2744            .and_then(|entry| entry.terminal(terminal));
2745        let show_output = is_expanded && terminal_view.is_some();
2746
2747        v_flex()
2748            .my_1p5()
2749            .mx_5()
2750            .border_1()
2751            .when(tool_failed || command_failed, |card| card.border_dashed())
2752            .border_color(border_color)
2753            .rounded_md()
2754            .overflow_hidden()
2755            .child(
2756                v_flex()
2757                    .group(&header_group)
2758                    .py_1p5()
2759                    .pr_1p5()
2760                    .pl_2()
2761                    .gap_0p5()
2762                    .bg(header_bg)
2763                    .text_xs()
2764                    .child(header)
2765                    .child(
2766                        MarkdownElement::new(
2767                            command.clone(),
2768                            terminal_command_markdown_style(window, cx),
2769                        )
2770                        .code_block_renderer(
2771                            markdown::CodeBlockRenderer::Default {
2772                                copy_button: false,
2773                                copy_button_on_hover: true,
2774                                border: false,
2775                            },
2776                        ),
2777                    ),
2778            )
2779            .when(show_output, |this| {
2780                this.child(
2781                    div()
2782                        .pt_2()
2783                        .border_t_1()
2784                        .when(tool_failed || command_failed, |card| card.border_dashed())
2785                        .border_color(border_color)
2786                        .bg(cx.theme().colors().editor_background)
2787                        .rounded_b_md()
2788                        .text_ui_sm(cx)
2789                        .h_full()
2790                        .children(terminal_view.map(|terminal_view| {
2791                            if terminal_view
2792                                .read(cx)
2793                                .content_mode(window, cx)
2794                                .is_scrollable()
2795                            {
2796                                div().h_72().child(terminal_view).into_any_element()
2797                            } else {
2798                                terminal_view.into_any_element()
2799                            }
2800                        })),
2801                )
2802            })
2803            .into_any()
2804    }
2805
2806    fn render_rules_item(&self, cx: &Context<Self>) -> Option<AnyElement> {
2807        let project_context = self
2808            .as_native_thread(cx)?
2809            .read(cx)
2810            .project_context()
2811            .read(cx);
2812
2813        let user_rules_text = if project_context.user_rules.is_empty() {
2814            None
2815        } else if project_context.user_rules.len() == 1 {
2816            let user_rules = &project_context.user_rules[0];
2817
2818            match user_rules.title.as_ref() {
2819                Some(title) => Some(format!("Using \"{title}\" user rule")),
2820                None => Some("Using user rule".into()),
2821            }
2822        } else {
2823            Some(format!(
2824                "Using {} user rules",
2825                project_context.user_rules.len()
2826            ))
2827        };
2828
2829        let first_user_rules_id = project_context
2830            .user_rules
2831            .first()
2832            .map(|user_rules| user_rules.uuid.0);
2833
2834        let rules_files = project_context
2835            .worktrees
2836            .iter()
2837            .filter_map(|worktree| worktree.rules_file.as_ref())
2838            .collect::<Vec<_>>();
2839
2840        let rules_file_text = match rules_files.as_slice() {
2841            &[] => None,
2842            &[rules_file] => Some(format!(
2843                "Using project {:?} file",
2844                rules_file.path_in_worktree
2845            )),
2846            rules_files => Some(format!("Using {} project rules files", rules_files.len())),
2847        };
2848
2849        if user_rules_text.is_none() && rules_file_text.is_none() {
2850            return None;
2851        }
2852
2853        let has_both = user_rules_text.is_some() && rules_file_text.is_some();
2854
2855        Some(
2856            h_flex()
2857                .px_2p5()
2858                .child(
2859                    Icon::new(IconName::Attach)
2860                        .size(IconSize::XSmall)
2861                        .color(Color::Disabled),
2862                )
2863                .when_some(user_rules_text, |parent, user_rules_text| {
2864                    parent.child(
2865                        h_flex()
2866                            .id("user-rules")
2867                            .ml_1()
2868                            .mr_1p5()
2869                            .child(
2870                                Label::new(user_rules_text)
2871                                    .size(LabelSize::XSmall)
2872                                    .color(Color::Muted)
2873                                    .truncate(),
2874                            )
2875                            .hover(|s| s.bg(cx.theme().colors().element_hover))
2876                            .tooltip(Tooltip::text("View User Rules"))
2877                            .on_click(move |_event, window, cx| {
2878                                window.dispatch_action(
2879                                    Box::new(OpenRulesLibrary {
2880                                        prompt_to_select: first_user_rules_id,
2881                                    }),
2882                                    cx,
2883                                )
2884                            }),
2885                    )
2886                })
2887                .when(has_both, |this| {
2888                    this.child(
2889                        Label::new("")
2890                            .size(LabelSize::XSmall)
2891                            .color(Color::Disabled),
2892                    )
2893                })
2894                .when_some(rules_file_text, |parent, rules_file_text| {
2895                    parent.child(
2896                        h_flex()
2897                            .id("project-rules")
2898                            .ml_1p5()
2899                            .child(
2900                                Label::new(rules_file_text)
2901                                    .size(LabelSize::XSmall)
2902                                    .color(Color::Muted),
2903                            )
2904                            .hover(|s| s.bg(cx.theme().colors().element_hover))
2905                            .tooltip(Tooltip::text("View Project Rules"))
2906                            .on_click(cx.listener(Self::handle_open_rules)),
2907                    )
2908                })
2909                .into_any(),
2910        )
2911    }
2912
2913    fn render_empty_state_section_header(
2914        &self,
2915        label: impl Into<SharedString>,
2916        action_slot: Option<AnyElement>,
2917        cx: &mut Context<Self>,
2918    ) -> impl IntoElement {
2919        div().pl_1().pr_1p5().child(
2920            h_flex()
2921                .mt_2()
2922                .pl_1p5()
2923                .pb_1()
2924                .w_full()
2925                .justify_between()
2926                .border_b_1()
2927                .border_color(cx.theme().colors().border_variant)
2928                .child(
2929                    Label::new(label.into())
2930                        .size(LabelSize::Small)
2931                        .color(Color::Muted),
2932                )
2933                .children(action_slot),
2934        )
2935    }
2936
2937    fn render_recent_history(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
2938        let render_history = self
2939            .agent
2940            .clone()
2941            .downcast::<agent2::NativeAgentServer>()
2942            .is_some()
2943            && self
2944                .history_store
2945                .update(cx, |history_store, cx| !history_store.is_empty(cx));
2946
2947        v_flex()
2948            .size_full()
2949            .when(render_history, |this| {
2950                let recent_history: Vec<_> = self.history_store.update(cx, |history_store, _| {
2951                    history_store.entries().take(3).collect()
2952                });
2953                this.justify_end().child(
2954                    v_flex()
2955                        .child(
2956                            self.render_empty_state_section_header(
2957                                "Recent",
2958                                Some(
2959                                    Button::new("view-history", "View All")
2960                                        .style(ButtonStyle::Subtle)
2961                                        .label_size(LabelSize::Small)
2962                                        .key_binding(
2963                                            KeyBinding::for_action_in(
2964                                                &OpenHistory,
2965                                                &self.focus_handle(cx),
2966                                                window,
2967                                                cx,
2968                                            )
2969                                            .map(|kb| kb.size(rems_from_px(12.))),
2970                                        )
2971                                        .on_click(move |_event, window, cx| {
2972                                            window.dispatch_action(OpenHistory.boxed_clone(), cx);
2973                                        })
2974                                        .into_any_element(),
2975                                ),
2976                                cx,
2977                            ),
2978                        )
2979                        .child(
2980                            v_flex().p_1().pr_1p5().gap_1().children(
2981                                recent_history
2982                                    .into_iter()
2983                                    .enumerate()
2984                                    .map(|(index, entry)| {
2985                                        // TODO: Add keyboard navigation.
2986                                        let is_hovered =
2987                                            self.hovered_recent_history_item == Some(index);
2988                                        crate::acp::thread_history::AcpHistoryEntryElement::new(
2989                                            entry,
2990                                            cx.entity().downgrade(),
2991                                        )
2992                                        .hovered(is_hovered)
2993                                        .on_hover(cx.listener(
2994                                            move |this, is_hovered, _window, cx| {
2995                                                if *is_hovered {
2996                                                    this.hovered_recent_history_item = Some(index);
2997                                                } else if this.hovered_recent_history_item
2998                                                    == Some(index)
2999                                                {
3000                                                    this.hovered_recent_history_item = None;
3001                                                }
3002                                                cx.notify();
3003                                            },
3004                                        ))
3005                                        .into_any_element()
3006                                    }),
3007                            ),
3008                        ),
3009                )
3010            })
3011            .into_any()
3012    }
3013
3014    fn render_auth_required_state(
3015        &self,
3016        connection: &Rc<dyn AgentConnection>,
3017        description: Option<&Entity<Markdown>>,
3018        configuration_view: Option<&AnyView>,
3019        pending_auth_method: Option<&acp::AuthMethodId>,
3020        window: &mut Window,
3021        cx: &Context<Self>,
3022    ) -> Div {
3023        let show_description =
3024            configuration_view.is_none() && description.is_none() && pending_auth_method.is_none();
3025
3026        let auth_methods = connection.auth_methods();
3027
3028        v_flex().flex_1().size_full().justify_end().child(
3029            v_flex()
3030                .p_2()
3031                .pr_3()
3032                .w_full()
3033                .gap_1()
3034                .border_t_1()
3035                .border_color(cx.theme().colors().border)
3036                .bg(cx.theme().status().warning.opacity(0.04))
3037                .child(
3038                    h_flex()
3039                        .gap_1p5()
3040                        .child(
3041                            Icon::new(IconName::Warning)
3042                                .color(Color::Warning)
3043                                .size(IconSize::Small),
3044                        )
3045                        .child(Label::new("Authentication Required").size(LabelSize::Small)),
3046                )
3047                .children(description.map(|desc| {
3048                    div().text_ui(cx).child(self.render_markdown(
3049                        desc.clone(),
3050                        default_markdown_style(false, false, window, cx),
3051                    ))
3052                }))
3053                .children(
3054                    configuration_view
3055                        .cloned()
3056                        .map(|view| div().w_full().child(view)),
3057                )
3058                .when(show_description, |el| {
3059                    el.child(
3060                        Label::new(format!(
3061                            "You are not currently authenticated with {}.{}",
3062                            self.agent.name(),
3063                            if auth_methods.len() > 1 {
3064                                " Please choose one of the following options:"
3065                            } else {
3066                                ""
3067                            }
3068                        ))
3069                        .size(LabelSize::Small)
3070                        .color(Color::Muted)
3071                        .mb_1()
3072                        .ml_5(),
3073                    )
3074                })
3075                .when_some(pending_auth_method, |el, _| {
3076                    el.child(
3077                        h_flex()
3078                            .py_4()
3079                            .w_full()
3080                            .justify_center()
3081                            .gap_1()
3082                            .child(
3083                                Icon::new(IconName::ArrowCircle)
3084                                    .size(IconSize::Small)
3085                                    .color(Color::Muted)
3086                                    .with_rotate_animation(2),
3087                            )
3088                            .child(Label::new("Authenticating…").size(LabelSize::Small)),
3089                    )
3090                })
3091                .when(!auth_methods.is_empty(), |this| {
3092                    this.child(
3093                        h_flex()
3094                            .justify_end()
3095                            .flex_wrap()
3096                            .gap_1()
3097                            .when(!show_description, |this| {
3098                                this.border_t_1()
3099                                    .mt_1()
3100                                    .pt_2()
3101                                    .border_color(cx.theme().colors().border.opacity(0.8))
3102                            })
3103                            .children(connection.auth_methods().iter().enumerate().rev().map(
3104                                |(ix, method)| {
3105                                    let (method_id, name) = if self
3106                                        .project
3107                                        .read(cx)
3108                                        .is_via_remote_server()
3109                                        && method.id.0.as_ref() == "oauth-personal"
3110                                        && method.name == "Log in with Google"
3111                                    {
3112                                        ("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
3113                                    } else {
3114                                        (method.id.0.clone(), method.name.clone())
3115                                    };
3116
3117                                    Button::new(SharedString::from(method_id.clone()), name)
3118                                        .when(ix == 0, |el| {
3119                                            el.style(ButtonStyle::Tinted(ui::TintColor::Warning))
3120                                        })
3121                                        .label_size(LabelSize::Small)
3122                                        .on_click({
3123                                            cx.listener(move |this, _, window, cx| {
3124                                                telemetry::event!(
3125                                                    "Authenticate Agent Started",
3126                                                    agent = this.agent.telemetry_id(),
3127                                                    method = method_id
3128                                                );
3129
3130                                                this.authenticate(
3131                                                    acp::AuthMethodId(method_id.clone()),
3132                                                    window,
3133                                                    cx,
3134                                                )
3135                                            })
3136                                        })
3137                                },
3138                            )),
3139                    )
3140                }),
3141        )
3142    }
3143
3144    fn render_load_error(
3145        &self,
3146        e: &LoadError,
3147        window: &mut Window,
3148        cx: &mut Context<Self>,
3149    ) -> AnyElement {
3150        let (title, message, action_slot): (_, SharedString, _) = match e {
3151            LoadError::Unsupported {
3152                command: path,
3153                current_version,
3154                minimum_version,
3155            } => {
3156                return self.render_unsupported(path, current_version, minimum_version, window, cx);
3157            }
3158            LoadError::FailedToInstall(msg) => (
3159                "Failed to Install",
3160                msg.into(),
3161                Some(self.create_copy_button(msg.to_string()).into_any_element()),
3162            ),
3163            LoadError::Exited { status } => (
3164                "Failed to Launch",
3165                format!("Server exited with status {status}").into(),
3166                None,
3167            ),
3168            LoadError::Other(msg) => (
3169                "Failed to Launch",
3170                msg.into(),
3171                Some(self.create_copy_button(msg.to_string()).into_any_element()),
3172            ),
3173        };
3174
3175        Callout::new()
3176            .severity(Severity::Error)
3177            .icon(IconName::XCircleFilled)
3178            .title(title)
3179            .description(message)
3180            .actions_slot(div().children(action_slot))
3181            .into_any_element()
3182    }
3183
3184    fn render_unsupported(
3185        &self,
3186        path: &SharedString,
3187        version: &SharedString,
3188        minimum_version: &SharedString,
3189        _window: &mut Window,
3190        cx: &mut Context<Self>,
3191    ) -> AnyElement {
3192        let (heading_label, description_label) = (
3193            format!("Upgrade {} to work with Zed", self.agent.name()),
3194            if version.is_empty() {
3195                format!(
3196                    "Currently using {}, which does not report a valid --version",
3197                    path,
3198                )
3199            } else {
3200                format!(
3201                    "Currently using {}, which is only version {} (need at least {minimum_version})",
3202                    path, version
3203                )
3204            },
3205        );
3206
3207        v_flex()
3208            .w_full()
3209            .p_3p5()
3210            .gap_2p5()
3211            .border_t_1()
3212            .border_color(cx.theme().colors().border)
3213            .bg(linear_gradient(
3214                180.,
3215                linear_color_stop(cx.theme().colors().editor_background.opacity(0.4), 4.),
3216                linear_color_stop(cx.theme().status().info_background.opacity(0.), 0.),
3217            ))
3218            .child(
3219                v_flex().gap_0p5().child(Label::new(heading_label)).child(
3220                    Label::new(description_label)
3221                        .size(LabelSize::Small)
3222                        .color(Color::Muted),
3223                ),
3224            )
3225            .into_any_element()
3226    }
3227
3228    fn render_activity_bar(
3229        &self,
3230        thread_entity: &Entity<AcpThread>,
3231        window: &mut Window,
3232        cx: &Context<Self>,
3233    ) -> Option<AnyElement> {
3234        let thread = thread_entity.read(cx);
3235        let action_log = thread.action_log();
3236        let changed_buffers = action_log.read(cx).changed_buffers(cx);
3237        let plan = thread.plan();
3238
3239        if changed_buffers.is_empty() && plan.is_empty() {
3240            return None;
3241        }
3242
3243        let editor_bg_color = cx.theme().colors().editor_background;
3244        let active_color = cx.theme().colors().element_selected;
3245        let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
3246
3247        // Temporarily always enable ACP edit controls. This is temporary, to lessen the
3248        // impact of a nasty bug that causes them to sometimes be disabled when they shouldn't
3249        // be, which blocks you from being able to accept or reject edits. This switches the
3250        // bug to be that sometimes it's enabled when it shouldn't be, which at least doesn't
3251        // block you from using the panel.
3252        let pending_edits = false;
3253
3254        v_flex()
3255            .mt_1()
3256            .mx_2()
3257            .bg(bg_edit_files_disclosure)
3258            .border_1()
3259            .border_b_0()
3260            .border_color(cx.theme().colors().border)
3261            .rounded_t_md()
3262            .shadow(vec![gpui::BoxShadow {
3263                color: gpui::black().opacity(0.15),
3264                offset: point(px(1.), px(-1.)),
3265                blur_radius: px(3.),
3266                spread_radius: px(0.),
3267            }])
3268            .when(!plan.is_empty(), |this| {
3269                this.child(self.render_plan_summary(plan, window, cx))
3270                    .when(self.plan_expanded, |parent| {
3271                        parent.child(self.render_plan_entries(plan, window, cx))
3272                    })
3273            })
3274            .when(!plan.is_empty() && !changed_buffers.is_empty(), |this| {
3275                this.child(Divider::horizontal().color(DividerColor::Border))
3276            })
3277            .when(!changed_buffers.is_empty(), |this| {
3278                this.child(self.render_edits_summary(
3279                    &changed_buffers,
3280                    self.edits_expanded,
3281                    pending_edits,
3282                    window,
3283                    cx,
3284                ))
3285                .when(self.edits_expanded, |parent| {
3286                    parent.child(self.render_edited_files(
3287                        action_log,
3288                        &changed_buffers,
3289                        pending_edits,
3290                        cx,
3291                    ))
3292                })
3293            })
3294            .into_any()
3295            .into()
3296    }
3297
3298    fn render_plan_summary(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
3299        let stats = plan.stats();
3300
3301        let title = if let Some(entry) = stats.in_progress_entry
3302            && !self.plan_expanded
3303        {
3304            h_flex()
3305                .w_full()
3306                .cursor_default()
3307                .gap_1()
3308                .text_xs()
3309                .text_color(cx.theme().colors().text_muted)
3310                .justify_between()
3311                .child(
3312                    h_flex()
3313                        .gap_1()
3314                        .child(
3315                            Label::new("Current:")
3316                                .size(LabelSize::Small)
3317                                .color(Color::Muted),
3318                        )
3319                        .child(MarkdownElement::new(
3320                            entry.content.clone(),
3321                            plan_label_markdown_style(&entry.status, window, cx),
3322                        )),
3323                )
3324                .when(stats.pending > 0, |this| {
3325                    this.child(
3326                        Label::new(format!("{} left", stats.pending))
3327                            .size(LabelSize::Small)
3328                            .color(Color::Muted)
3329                            .mr_1(),
3330                    )
3331                })
3332        } else {
3333            let status_label = if stats.pending == 0 {
3334                "All Done".to_string()
3335            } else if stats.completed == 0 {
3336                format!("{} Tasks", plan.entries.len())
3337            } else {
3338                format!("{}/{}", stats.completed, plan.entries.len())
3339            };
3340
3341            h_flex()
3342                .w_full()
3343                .gap_1()
3344                .justify_between()
3345                .child(
3346                    Label::new("Plan")
3347                        .size(LabelSize::Small)
3348                        .color(Color::Muted),
3349                )
3350                .child(
3351                    Label::new(status_label)
3352                        .size(LabelSize::Small)
3353                        .color(Color::Muted)
3354                        .mr_1(),
3355                )
3356        };
3357
3358        h_flex()
3359            .p_1()
3360            .justify_between()
3361            .when(self.plan_expanded, |this| {
3362                this.border_b_1().border_color(cx.theme().colors().border)
3363            })
3364            .child(
3365                h_flex()
3366                    .id("plan_summary")
3367                    .w_full()
3368                    .gap_1()
3369                    .child(Disclosure::new("plan_disclosure", self.plan_expanded))
3370                    .child(title)
3371                    .on_click(cx.listener(|this, _, _, cx| {
3372                        this.plan_expanded = !this.plan_expanded;
3373                        cx.notify();
3374                    })),
3375            )
3376    }
3377
3378    fn render_plan_entries(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
3379        v_flex().children(plan.entries.iter().enumerate().flat_map(|(index, entry)| {
3380            let element = h_flex()
3381                .py_1()
3382                .px_2()
3383                .gap_2()
3384                .justify_between()
3385                .bg(cx.theme().colors().editor_background)
3386                .when(index < plan.entries.len() - 1, |parent| {
3387                    parent.border_color(cx.theme().colors().border).border_b_1()
3388                })
3389                .child(
3390                    h_flex()
3391                        .id(("plan_entry", index))
3392                        .gap_1p5()
3393                        .max_w_full()
3394                        .overflow_x_scroll()
3395                        .text_xs()
3396                        .text_color(cx.theme().colors().text_muted)
3397                        .child(match entry.status {
3398                            acp::PlanEntryStatus::Pending => Icon::new(IconName::TodoPending)
3399                                .size(IconSize::Small)
3400                                .color(Color::Muted)
3401                                .into_any_element(),
3402                            acp::PlanEntryStatus::InProgress => Icon::new(IconName::TodoProgress)
3403                                .size(IconSize::Small)
3404                                .color(Color::Accent)
3405                                .with_rotate_animation(2)
3406                                .into_any_element(),
3407                            acp::PlanEntryStatus::Completed => Icon::new(IconName::TodoComplete)
3408                                .size(IconSize::Small)
3409                                .color(Color::Success)
3410                                .into_any_element(),
3411                        })
3412                        .child(MarkdownElement::new(
3413                            entry.content.clone(),
3414                            plan_label_markdown_style(&entry.status, window, cx),
3415                        )),
3416                );
3417
3418            Some(element)
3419        }))
3420    }
3421
3422    fn render_edits_summary(
3423        &self,
3424        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
3425        expanded: bool,
3426        pending_edits: bool,
3427        window: &mut Window,
3428        cx: &Context<Self>,
3429    ) -> Div {
3430        const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete.";
3431
3432        let focus_handle = self.focus_handle(cx);
3433
3434        h_flex()
3435            .p_1()
3436            .justify_between()
3437            .flex_wrap()
3438            .when(expanded, |this| {
3439                this.border_b_1().border_color(cx.theme().colors().border)
3440            })
3441            .child(
3442                h_flex()
3443                    .id("edits-container")
3444                    .gap_1()
3445                    .child(Disclosure::new("edits-disclosure", expanded))
3446                    .map(|this| {
3447                        if pending_edits {
3448                            this.child(
3449                                Label::new(format!(
3450                                    "Editing {} {}",
3451                                    changed_buffers.len(),
3452                                    if changed_buffers.len() == 1 {
3453                                        "file"
3454                                    } else {
3455                                        "files"
3456                                    }
3457                                ))
3458                                .color(Color::Muted)
3459                                .size(LabelSize::Small)
3460                                .with_animation(
3461                                    "edit-label",
3462                                    Animation::new(Duration::from_secs(2))
3463                                        .repeat()
3464                                        .with_easing(pulsating_between(0.3, 0.7)),
3465                                    |label, delta| label.alpha(delta),
3466                                ),
3467                            )
3468                        } else {
3469                            this.child(
3470                                Label::new("Edits")
3471                                    .size(LabelSize::Small)
3472                                    .color(Color::Muted),
3473                            )
3474                            .child(Label::new("").size(LabelSize::XSmall).color(Color::Muted))
3475                            .child(
3476                                Label::new(format!(
3477                                    "{} {}",
3478                                    changed_buffers.len(),
3479                                    if changed_buffers.len() == 1 {
3480                                        "file"
3481                                    } else {
3482                                        "files"
3483                                    }
3484                                ))
3485                                .size(LabelSize::Small)
3486                                .color(Color::Muted),
3487                            )
3488                        }
3489                    })
3490                    .on_click(cx.listener(|this, _, _, cx| {
3491                        this.edits_expanded = !this.edits_expanded;
3492                        cx.notify();
3493                    })),
3494            )
3495            .child(
3496                h_flex()
3497                    .gap_1()
3498                    .child(
3499                        IconButton::new("review-changes", IconName::ListTodo)
3500                            .icon_size(IconSize::Small)
3501                            .tooltip({
3502                                let focus_handle = focus_handle.clone();
3503                                move |window, cx| {
3504                                    Tooltip::for_action_in(
3505                                        "Review Changes",
3506                                        &OpenAgentDiff,
3507                                        &focus_handle,
3508                                        window,
3509                                        cx,
3510                                    )
3511                                }
3512                            })
3513                            .on_click(cx.listener(|_, _, window, cx| {
3514                                window.dispatch_action(OpenAgentDiff.boxed_clone(), cx);
3515                            })),
3516                    )
3517                    .child(Divider::vertical().color(DividerColor::Border))
3518                    .child(
3519                        Button::new("reject-all-changes", "Reject All")
3520                            .label_size(LabelSize::Small)
3521                            .disabled(pending_edits)
3522                            .when(pending_edits, |this| {
3523                                this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
3524                            })
3525                            .key_binding(
3526                                KeyBinding::for_action_in(
3527                                    &RejectAll,
3528                                    &focus_handle.clone(),
3529                                    window,
3530                                    cx,
3531                                )
3532                                .map(|kb| kb.size(rems_from_px(10.))),
3533                            )
3534                            .on_click(cx.listener(move |this, _, window, cx| {
3535                                this.reject_all(&RejectAll, window, cx);
3536                            })),
3537                    )
3538                    .child(
3539                        Button::new("keep-all-changes", "Keep All")
3540                            .label_size(LabelSize::Small)
3541                            .disabled(pending_edits)
3542                            .when(pending_edits, |this| {
3543                                this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
3544                            })
3545                            .key_binding(
3546                                KeyBinding::for_action_in(&KeepAll, &focus_handle, window, cx)
3547                                    .map(|kb| kb.size(rems_from_px(10.))),
3548                            )
3549                            .on_click(cx.listener(move |this, _, window, cx| {
3550                                this.keep_all(&KeepAll, window, cx);
3551                            })),
3552                    ),
3553            )
3554    }
3555
3556    fn render_edited_files(
3557        &self,
3558        action_log: &Entity<ActionLog>,
3559        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
3560        pending_edits: bool,
3561        cx: &Context<Self>,
3562    ) -> Div {
3563        let editor_bg_color = cx.theme().colors().editor_background;
3564
3565        v_flex().children(changed_buffers.iter().enumerate().flat_map(
3566            |(index, (buffer, _diff))| {
3567                let file = buffer.read(cx).file()?;
3568                let path = file.path();
3569
3570                let file_path = path.parent().and_then(|parent| {
3571                    let parent_str = parent.to_string_lossy();
3572
3573                    if parent_str.is_empty() {
3574                        None
3575                    } else {
3576                        Some(
3577                            Label::new(format!("/{}{}", parent_str, std::path::MAIN_SEPARATOR_STR))
3578                                .color(Color::Muted)
3579                                .size(LabelSize::XSmall)
3580                                .buffer_font(cx),
3581                        )
3582                    }
3583                });
3584
3585                let file_name = path.file_name().map(|name| {
3586                    Label::new(name.to_string_lossy().to_string())
3587                        .size(LabelSize::XSmall)
3588                        .buffer_font(cx)
3589                });
3590
3591                let file_icon = FileIcons::get_icon(path, cx)
3592                    .map(Icon::from_path)
3593                    .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
3594                    .unwrap_or_else(|| {
3595                        Icon::new(IconName::File)
3596                            .color(Color::Muted)
3597                            .size(IconSize::Small)
3598                    });
3599
3600                let overlay_gradient = linear_gradient(
3601                    90.,
3602                    linear_color_stop(editor_bg_color, 1.),
3603                    linear_color_stop(editor_bg_color.opacity(0.2), 0.),
3604                );
3605
3606                let element = h_flex()
3607                    .group("edited-code")
3608                    .id(("file-container", index))
3609                    .py_1()
3610                    .pl_2()
3611                    .pr_1()
3612                    .gap_2()
3613                    .justify_between()
3614                    .bg(editor_bg_color)
3615                    .when(index < changed_buffers.len() - 1, |parent| {
3616                        parent.border_color(cx.theme().colors().border).border_b_1()
3617                    })
3618                    .child(
3619                        h_flex()
3620                            .relative()
3621                            .id(("file-name", index))
3622                            .pr_8()
3623                            .gap_1p5()
3624                            .max_w_full()
3625                            .overflow_x_scroll()
3626                            .child(file_icon)
3627                            .child(h_flex().gap_0p5().children(file_name).children(file_path))
3628                            .child(
3629                                div()
3630                                    .absolute()
3631                                    .h_full()
3632                                    .w_12()
3633                                    .top_0()
3634                                    .bottom_0()
3635                                    .right_0()
3636                                    .bg(overlay_gradient),
3637                            )
3638                            .on_click({
3639                                let buffer = buffer.clone();
3640                                cx.listener(move |this, _, window, cx| {
3641                                    this.open_edited_buffer(&buffer, window, cx);
3642                                })
3643                            }),
3644                    )
3645                    .child(
3646                        h_flex()
3647                            .gap_1()
3648                            .visible_on_hover("edited-code")
3649                            .child(
3650                                Button::new("review", "Review")
3651                                    .label_size(LabelSize::Small)
3652                                    .on_click({
3653                                        let buffer = buffer.clone();
3654                                        cx.listener(move |this, _, window, cx| {
3655                                            this.open_edited_buffer(&buffer, window, cx);
3656                                        })
3657                                    }),
3658                            )
3659                            .child(Divider::vertical().color(DividerColor::BorderVariant))
3660                            .child(
3661                                Button::new("reject-file", "Reject")
3662                                    .label_size(LabelSize::Small)
3663                                    .disabled(pending_edits)
3664                                    .on_click({
3665                                        let buffer = buffer.clone();
3666                                        let action_log = action_log.clone();
3667                                        move |_, _, cx| {
3668                                            action_log.update(cx, |action_log, cx| {
3669                                                action_log
3670                                                    .reject_edits_in_ranges(
3671                                                        buffer.clone(),
3672                                                        vec![Anchor::MIN..Anchor::MAX],
3673                                                        cx,
3674                                                    )
3675                                                    .detach_and_log_err(cx);
3676                                            })
3677                                        }
3678                                    }),
3679                            )
3680                            .child(
3681                                Button::new("keep-file", "Keep")
3682                                    .label_size(LabelSize::Small)
3683                                    .disabled(pending_edits)
3684                                    .on_click({
3685                                        let buffer = buffer.clone();
3686                                        let action_log = action_log.clone();
3687                                        move |_, _, cx| {
3688                                            action_log.update(cx, |action_log, cx| {
3689                                                action_log.keep_edits_in_range(
3690                                                    buffer.clone(),
3691                                                    Anchor::MIN..Anchor::MAX,
3692                                                    cx,
3693                                                );
3694                                            })
3695                                        }
3696                                    }),
3697                            ),
3698                    );
3699
3700                Some(element)
3701            },
3702        ))
3703    }
3704
3705    fn render_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
3706        let focus_handle = self.message_editor.focus_handle(cx);
3707        let editor_bg_color = cx.theme().colors().editor_background;
3708        let (expand_icon, expand_tooltip) = if self.editor_expanded {
3709            (IconName::Minimize, "Minimize Message Editor")
3710        } else {
3711            (IconName::Maximize, "Expand Message Editor")
3712        };
3713
3714        let backdrop = div()
3715            .size_full()
3716            .absolute()
3717            .inset_0()
3718            .bg(cx.theme().colors().panel_background)
3719            .opacity(0.8)
3720            .block_mouse_except_scroll();
3721
3722        let enable_editor = match self.thread_state {
3723            ThreadState::Loading { .. } | ThreadState::Ready { .. } => true,
3724            ThreadState::Unauthenticated { .. } | ThreadState::LoadError(..) => false,
3725        };
3726
3727        v_flex()
3728            .on_action(cx.listener(Self::expand_message_editor))
3729            .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
3730                if let Some(profile_selector) = this.profile_selector.as_ref() {
3731                    profile_selector.read(cx).menu_handle().toggle(window, cx);
3732                }
3733            }))
3734            .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
3735                if let Some(model_selector) = this.model_selector.as_ref() {
3736                    model_selector
3737                        .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
3738                }
3739            }))
3740            .p_2()
3741            .gap_2()
3742            .border_t_1()
3743            .border_color(cx.theme().colors().border)
3744            .bg(editor_bg_color)
3745            .when(self.editor_expanded, |this| {
3746                this.h(vh(0.8, window)).size_full().justify_between()
3747            })
3748            .child(
3749                v_flex()
3750                    .relative()
3751                    .size_full()
3752                    .pt_1()
3753                    .pr_2p5()
3754                    .child(self.message_editor.clone())
3755                    .child(
3756                        h_flex()
3757                            .absolute()
3758                            .top_0()
3759                            .right_0()
3760                            .opacity(0.5)
3761                            .hover(|this| this.opacity(1.0))
3762                            .child(
3763                                IconButton::new("toggle-height", expand_icon)
3764                                    .icon_size(IconSize::Small)
3765                                    .icon_color(Color::Muted)
3766                                    .tooltip({
3767                                        move |window, cx| {
3768                                            Tooltip::for_action_in(
3769                                                expand_tooltip,
3770                                                &ExpandMessageEditor,
3771                                                &focus_handle,
3772                                                window,
3773                                                cx,
3774                                            )
3775                                        }
3776                                    })
3777                                    .on_click(cx.listener(|_, _, window, cx| {
3778                                        window.dispatch_action(Box::new(ExpandMessageEditor), cx);
3779                                    })),
3780                            ),
3781                    ),
3782            )
3783            .child(
3784                h_flex()
3785                    .flex_none()
3786                    .flex_wrap()
3787                    .justify_between()
3788                    .child(
3789                        h_flex()
3790                            .child(self.render_follow_toggle(cx))
3791                            .children(self.render_burn_mode_toggle(cx)),
3792                    )
3793                    .child(
3794                        h_flex()
3795                            .gap_1()
3796                            .children(self.render_token_usage(cx))
3797                            .children(self.profile_selector.clone())
3798                            .children(self.model_selector.clone())
3799                            .child(self.render_send_button(cx)),
3800                    ),
3801            )
3802            .when(!enable_editor, |this| this.child(backdrop))
3803            .into_any()
3804    }
3805
3806    pub(crate) fn as_native_connection(
3807        &self,
3808        cx: &App,
3809    ) -> Option<Rc<agent2::NativeAgentConnection>> {
3810        let acp_thread = self.thread()?.read(cx);
3811        acp_thread.connection().clone().downcast()
3812    }
3813
3814    pub(crate) fn as_native_thread(&self, cx: &App) -> Option<Entity<agent2::Thread>> {
3815        let acp_thread = self.thread()?.read(cx);
3816        self.as_native_connection(cx)?
3817            .thread(acp_thread.session_id(), cx)
3818    }
3819
3820    fn is_using_zed_ai_models(&self, cx: &App) -> bool {
3821        self.as_native_thread(cx)
3822            .and_then(|thread| thread.read(cx).model())
3823            .is_some_and(|model| model.provider_id() == language_model::ZED_CLOUD_PROVIDER_ID)
3824    }
3825
3826    fn render_token_usage(&self, cx: &mut Context<Self>) -> Option<Div> {
3827        let thread = self.thread()?.read(cx);
3828        let usage = thread.token_usage()?;
3829        let is_generating = thread.status() != ThreadStatus::Idle;
3830
3831        let used = crate::text_thread_editor::humanize_token_count(usage.used_tokens);
3832        let max = crate::text_thread_editor::humanize_token_count(usage.max_tokens);
3833
3834        Some(
3835            h_flex()
3836                .flex_shrink_0()
3837                .gap_0p5()
3838                .mr_1p5()
3839                .child(
3840                    Label::new(used)
3841                        .size(LabelSize::Small)
3842                        .color(Color::Muted)
3843                        .map(|label| {
3844                            if is_generating {
3845                                label
3846                                    .with_animation(
3847                                        "used-tokens-label",
3848                                        Animation::new(Duration::from_secs(2))
3849                                            .repeat()
3850                                            .with_easing(pulsating_between(0.3, 0.8)),
3851                                        |label, delta| label.alpha(delta),
3852                                    )
3853                                    .into_any()
3854                            } else {
3855                                label.into_any_element()
3856                            }
3857                        }),
3858                )
3859                .child(
3860                    Label::new("/")
3861                        .size(LabelSize::Small)
3862                        .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.5))),
3863                )
3864                .child(Label::new(max).size(LabelSize::Small).color(Color::Muted)),
3865        )
3866    }
3867
3868    fn toggle_burn_mode(
3869        &mut self,
3870        _: &ToggleBurnMode,
3871        _window: &mut Window,
3872        cx: &mut Context<Self>,
3873    ) {
3874        let Some(thread) = self.as_native_thread(cx) else {
3875            return;
3876        };
3877
3878        thread.update(cx, |thread, cx| {
3879            let current_mode = thread.completion_mode();
3880            thread.set_completion_mode(
3881                match current_mode {
3882                    CompletionMode::Burn => CompletionMode::Normal,
3883                    CompletionMode::Normal => CompletionMode::Burn,
3884                },
3885                cx,
3886            );
3887        });
3888    }
3889
3890    fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context<Self>) {
3891        let Some(thread) = self.thread() else {
3892            return;
3893        };
3894        let action_log = thread.read(cx).action_log().clone();
3895        action_log.update(cx, |action_log, cx| action_log.keep_all_edits(cx));
3896    }
3897
3898    fn reject_all(&mut self, _: &RejectAll, _window: &mut Window, cx: &mut Context<Self>) {
3899        let Some(thread) = self.thread() else {
3900            return;
3901        };
3902        let action_log = thread.read(cx).action_log().clone();
3903        action_log
3904            .update(cx, |action_log, cx| action_log.reject_all_edits(cx))
3905            .detach();
3906    }
3907
3908    fn render_burn_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
3909        let thread = self.as_native_thread(cx)?.read(cx);
3910
3911        if thread
3912            .model()
3913            .is_none_or(|model| !model.supports_burn_mode())
3914        {
3915            return None;
3916        }
3917
3918        let active_completion_mode = thread.completion_mode();
3919        let burn_mode_enabled = active_completion_mode == CompletionMode::Burn;
3920        let icon = if burn_mode_enabled {
3921            IconName::ZedBurnModeOn
3922        } else {
3923            IconName::ZedBurnMode
3924        };
3925
3926        Some(
3927            IconButton::new("burn-mode", icon)
3928                .icon_size(IconSize::Small)
3929                .icon_color(Color::Muted)
3930                .toggle_state(burn_mode_enabled)
3931                .selected_icon_color(Color::Error)
3932                .on_click(cx.listener(|this, _event, window, cx| {
3933                    this.toggle_burn_mode(&ToggleBurnMode, window, cx);
3934                }))
3935                .tooltip(move |_window, cx| {
3936                    cx.new(|_| BurnModeTooltip::new().selected(burn_mode_enabled))
3937                        .into()
3938                })
3939                .into_any_element(),
3940        )
3941    }
3942
3943    fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
3944        let is_editor_empty = self.message_editor.read(cx).is_empty(cx);
3945        let is_generating = self
3946            .thread()
3947            .is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle);
3948
3949        if self.is_loading_contents {
3950            div()
3951                .id("loading-message-content")
3952                .px_1()
3953                .tooltip(Tooltip::text("Loading Added Context…"))
3954                .child(loading_contents_spinner(IconSize::default()))
3955                .into_any_element()
3956        } else if is_generating && is_editor_empty {
3957            IconButton::new("stop-generation", IconName::Stop)
3958                .icon_color(Color::Error)
3959                .style(ButtonStyle::Tinted(ui::TintColor::Error))
3960                .tooltip(move |window, cx| {
3961                    Tooltip::for_action("Stop Generation", &editor::actions::Cancel, window, cx)
3962                })
3963                .on_click(cx.listener(|this, _event, _, cx| this.cancel_generation(cx)))
3964                .into_any_element()
3965        } else {
3966            let send_btn_tooltip = if is_editor_empty && !is_generating {
3967                "Type to Send"
3968            } else if is_generating {
3969                "Stop and Send Message"
3970            } else {
3971                "Send"
3972            };
3973
3974            IconButton::new("send-message", IconName::Send)
3975                .style(ButtonStyle::Filled)
3976                .map(|this| {
3977                    if is_editor_empty && !is_generating {
3978                        this.disabled(true).icon_color(Color::Muted)
3979                    } else {
3980                        this.icon_color(Color::Accent)
3981                    }
3982                })
3983                .tooltip(move |window, cx| Tooltip::for_action(send_btn_tooltip, &Chat, window, cx))
3984                .on_click(cx.listener(|this, _, window, cx| {
3985                    this.send(window, cx);
3986                }))
3987                .into_any_element()
3988        }
3989    }
3990
3991    fn is_following(&self, cx: &App) -> bool {
3992        match self.thread().map(|thread| thread.read(cx).status()) {
3993            Some(ThreadStatus::Generating) => self
3994                .workspace
3995                .read_with(cx, |workspace, _| {
3996                    workspace.is_being_followed(CollaboratorId::Agent)
3997                })
3998                .unwrap_or(false),
3999            _ => self.should_be_following,
4000        }
4001    }
4002
4003    fn toggle_following(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4004        let following = self.is_following(cx);
4005
4006        self.should_be_following = !following;
4007        if self.thread().map(|thread| thread.read(cx).status()) == Some(ThreadStatus::Generating) {
4008            self.workspace
4009                .update(cx, |workspace, cx| {
4010                    if following {
4011                        workspace.unfollow(CollaboratorId::Agent, window, cx);
4012                    } else {
4013                        workspace.follow(CollaboratorId::Agent, window, cx);
4014                    }
4015                })
4016                .ok();
4017        }
4018
4019        telemetry::event!("Follow Agent Selected", following = !following);
4020    }
4021
4022    fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
4023        let following = self.is_following(cx);
4024
4025        let tooltip_label = if following {
4026            if self.agent.name() == "Zed Agent" {
4027                format!("Stop Following the {}", self.agent.name())
4028            } else {
4029                format!("Stop Following {}", self.agent.name())
4030            }
4031        } else {
4032            if self.agent.name() == "Zed Agent" {
4033                format!("Follow the {}", self.agent.name())
4034            } else {
4035                format!("Follow {}", self.agent.name())
4036            }
4037        };
4038
4039        IconButton::new("follow-agent", IconName::Crosshair)
4040            .icon_size(IconSize::Small)
4041            .icon_color(Color::Muted)
4042            .toggle_state(following)
4043            .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
4044            .tooltip(move |window, cx| {
4045                if following {
4046                    Tooltip::for_action(tooltip_label.clone(), &Follow, window, cx)
4047                } else {
4048                    Tooltip::with_meta(
4049                        tooltip_label.clone(),
4050                        Some(&Follow),
4051                        "Track the agent's location as it reads and edits files.",
4052                        window,
4053                        cx,
4054                    )
4055                }
4056            })
4057            .on_click(cx.listener(move |this, _, window, cx| {
4058                this.toggle_following(window, cx);
4059            }))
4060    }
4061
4062    fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
4063        let workspace = self.workspace.clone();
4064        MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
4065            Self::open_link(text, &workspace, window, cx);
4066        })
4067    }
4068
4069    fn open_link(
4070        url: SharedString,
4071        workspace: &WeakEntity<Workspace>,
4072        window: &mut Window,
4073        cx: &mut App,
4074    ) {
4075        let Some(workspace) = workspace.upgrade() else {
4076            cx.open_url(&url);
4077            return;
4078        };
4079
4080        if let Some(mention) = MentionUri::parse(&url).log_err() {
4081            workspace.update(cx, |workspace, cx| match mention {
4082                MentionUri::File { abs_path } => {
4083                    let project = workspace.project();
4084                    let Some(path) =
4085                        project.update(cx, |project, cx| project.find_project_path(abs_path, cx))
4086                    else {
4087                        return;
4088                    };
4089
4090                    workspace
4091                        .open_path(path, None, true, window, cx)
4092                        .detach_and_log_err(cx);
4093                }
4094                MentionUri::PastedImage => {}
4095                MentionUri::Directory { abs_path } => {
4096                    let project = workspace.project();
4097                    let Some(entry_id) = project.update(cx, |project, cx| {
4098                        let path = project.find_project_path(abs_path, cx)?;
4099                        project.entry_for_path(&path, cx).map(|entry| entry.id)
4100                    }) else {
4101                        return;
4102                    };
4103
4104                    project.update(cx, |_, cx| {
4105                        cx.emit(project::Event::RevealInProjectPanel(entry_id));
4106                    });
4107                }
4108                MentionUri::Symbol {
4109                    abs_path: path,
4110                    line_range,
4111                    ..
4112                }
4113                | MentionUri::Selection {
4114                    abs_path: Some(path),
4115                    line_range,
4116                } => {
4117                    let project = workspace.project();
4118                    let Some(path) =
4119                        project.update(cx, |project, cx| project.find_project_path(path, cx))
4120                    else {
4121                        return;
4122                    };
4123
4124                    let item = workspace.open_path(path, None, true, window, cx);
4125                    window
4126                        .spawn(cx, async move |cx| {
4127                            let Some(editor) = item.await?.downcast::<Editor>() else {
4128                                return Ok(());
4129                            };
4130                            let range = Point::new(*line_range.start(), 0)
4131                                ..Point::new(*line_range.start(), 0);
4132                            editor
4133                                .update_in(cx, |editor, window, cx| {
4134                                    editor.change_selections(
4135                                        SelectionEffects::scroll(Autoscroll::center()),
4136                                        window,
4137                                        cx,
4138                                        |s| s.select_ranges(vec![range]),
4139                                    );
4140                                })
4141                                .ok();
4142                            anyhow::Ok(())
4143                        })
4144                        .detach_and_log_err(cx);
4145                }
4146                MentionUri::Selection { abs_path: None, .. } => {}
4147                MentionUri::Thread { id, name } => {
4148                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
4149                        panel.update(cx, |panel, cx| {
4150                            panel.load_agent_thread(
4151                                DbThreadMetadata {
4152                                    id,
4153                                    title: name.into(),
4154                                    updated_at: Default::default(),
4155                                },
4156                                window,
4157                                cx,
4158                            )
4159                        });
4160                    }
4161                }
4162                MentionUri::TextThread { path, .. } => {
4163                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
4164                        panel.update(cx, |panel, cx| {
4165                            panel
4166                                .open_saved_prompt_editor(path.as_path().into(), window, cx)
4167                                .detach_and_log_err(cx);
4168                        });
4169                    }
4170                }
4171                MentionUri::Rule { id, .. } => {
4172                    let PromptId::User { uuid } = id else {
4173                        return;
4174                    };
4175                    window.dispatch_action(
4176                        Box::new(OpenRulesLibrary {
4177                            prompt_to_select: Some(uuid.0),
4178                        }),
4179                        cx,
4180                    )
4181                }
4182                MentionUri::Fetch { url } => {
4183                    cx.open_url(url.as_str());
4184                }
4185            })
4186        } else {
4187            cx.open_url(&url);
4188        }
4189    }
4190
4191    fn open_tool_call_location(
4192        &self,
4193        entry_ix: usize,
4194        location_ix: usize,
4195        window: &mut Window,
4196        cx: &mut Context<Self>,
4197    ) -> Option<()> {
4198        let (tool_call_location, agent_location) = self
4199            .thread()?
4200            .read(cx)
4201            .entries()
4202            .get(entry_ix)?
4203            .location(location_ix)?;
4204
4205        let project_path = self
4206            .project
4207            .read(cx)
4208            .find_project_path(&tool_call_location.path, cx)?;
4209
4210        let open_task = self
4211            .workspace
4212            .update(cx, |workspace, cx| {
4213                workspace.open_path(project_path, None, true, window, cx)
4214            })
4215            .log_err()?;
4216        window
4217            .spawn(cx, async move |cx| {
4218                let item = open_task.await?;
4219
4220                let Some(active_editor) = item.downcast::<Editor>() else {
4221                    return anyhow::Ok(());
4222                };
4223
4224                active_editor.update_in(cx, |editor, window, cx| {
4225                    let multibuffer = editor.buffer().read(cx);
4226                    let buffer = multibuffer.as_singleton();
4227                    if agent_location.buffer.upgrade() == buffer {
4228                        let excerpt_id = multibuffer.excerpt_ids().first().cloned();
4229                        let anchor = editor::Anchor::in_buffer(
4230                            excerpt_id.unwrap(),
4231                            buffer.unwrap().read(cx).remote_id(),
4232                            agent_location.position,
4233                        );
4234                        editor.change_selections(Default::default(), window, cx, |selections| {
4235                            selections.select_anchor_ranges([anchor..anchor]);
4236                        })
4237                    } else {
4238                        let row = tool_call_location.line.unwrap_or_default();
4239                        editor.change_selections(Default::default(), window, cx, |selections| {
4240                            selections.select_ranges([Point::new(row, 0)..Point::new(row, 0)]);
4241                        })
4242                    }
4243                })?;
4244
4245                anyhow::Ok(())
4246            })
4247            .detach_and_log_err(cx);
4248
4249        None
4250    }
4251
4252    pub fn open_thread_as_markdown(
4253        &self,
4254        workspace: Entity<Workspace>,
4255        window: &mut Window,
4256        cx: &mut App,
4257    ) -> Task<Result<()>> {
4258        let markdown_language_task = workspace
4259            .read(cx)
4260            .app_state()
4261            .languages
4262            .language_for_name("Markdown");
4263
4264        let (thread_summary, markdown) = if let Some(thread) = self.thread() {
4265            let thread = thread.read(cx);
4266            (thread.title().to_string(), thread.to_markdown(cx))
4267        } else {
4268            return Task::ready(Ok(()));
4269        };
4270
4271        window.spawn(cx, async move |cx| {
4272            let markdown_language = markdown_language_task.await?;
4273
4274            workspace.update_in(cx, |workspace, window, cx| {
4275                let project = workspace.project().clone();
4276
4277                if !project.read(cx).is_local() {
4278                    bail!("failed to open active thread as markdown in remote project");
4279                }
4280
4281                let buffer = project.update(cx, |project, cx| {
4282                    project.create_local_buffer(&markdown, Some(markdown_language), true, cx)
4283                });
4284                let buffer = cx.new(|cx| {
4285                    MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
4286                });
4287
4288                workspace.add_item_to_active_pane(
4289                    Box::new(cx.new(|cx| {
4290                        let mut editor =
4291                            Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
4292                        editor.set_breadcrumb_header(thread_summary);
4293                        editor
4294                    })),
4295                    None,
4296                    true,
4297                    window,
4298                    cx,
4299                );
4300
4301                anyhow::Ok(())
4302            })??;
4303            anyhow::Ok(())
4304        })
4305    }
4306
4307    fn scroll_to_top(&mut self, cx: &mut Context<Self>) {
4308        self.list_state.scroll_to(ListOffset::default());
4309        cx.notify();
4310    }
4311
4312    pub fn scroll_to_bottom(&mut self, cx: &mut Context<Self>) {
4313        if let Some(thread) = self.thread() {
4314            let entry_count = thread.read(cx).entries().len();
4315            self.list_state.reset(entry_count);
4316            cx.notify();
4317        }
4318    }
4319
4320    fn notify_with_sound(
4321        &mut self,
4322        caption: impl Into<SharedString>,
4323        icon: IconName,
4324        window: &mut Window,
4325        cx: &mut Context<Self>,
4326    ) {
4327        self.play_notification_sound(window, cx);
4328        self.show_notification(caption, icon, window, cx);
4329    }
4330
4331    fn play_notification_sound(&self, window: &Window, cx: &mut App) {
4332        let settings = AgentSettings::get_global(cx);
4333        if settings.play_sound_when_agent_done && !window.is_window_active() {
4334            Audio::play_sound(Sound::AgentDone, cx);
4335        }
4336    }
4337
4338    fn show_notification(
4339        &mut self,
4340        caption: impl Into<SharedString>,
4341        icon: IconName,
4342        window: &mut Window,
4343        cx: &mut Context<Self>,
4344    ) {
4345        if window.is_window_active() || !self.notifications.is_empty() {
4346            return;
4347        }
4348
4349        // TODO: Change this once we have title summarization for external agents.
4350        let title = self.agent.name();
4351
4352        match AgentSettings::get_global(cx).notify_when_agent_waiting {
4353            NotifyWhenAgentWaiting::PrimaryScreen => {
4354                if let Some(primary) = cx.primary_display() {
4355                    self.pop_up(icon, caption.into(), title, window, primary, cx);
4356                }
4357            }
4358            NotifyWhenAgentWaiting::AllScreens => {
4359                let caption = caption.into();
4360                for screen in cx.displays() {
4361                    self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx);
4362                }
4363            }
4364            NotifyWhenAgentWaiting::Never => {
4365                // Don't show anything
4366            }
4367        }
4368    }
4369
4370    fn pop_up(
4371        &mut self,
4372        icon: IconName,
4373        caption: SharedString,
4374        title: SharedString,
4375        window: &mut Window,
4376        screen: Rc<dyn PlatformDisplay>,
4377        cx: &mut Context<Self>,
4378    ) {
4379        let options = AgentNotification::window_options(screen, cx);
4380
4381        let project_name = self.workspace.upgrade().and_then(|workspace| {
4382            workspace
4383                .read(cx)
4384                .project()
4385                .read(cx)
4386                .visible_worktrees(cx)
4387                .next()
4388                .map(|worktree| worktree.read(cx).root_name().to_string())
4389        });
4390
4391        if let Some(screen_window) = cx
4392            .open_window(options, |_, cx| {
4393                cx.new(|_| {
4394                    AgentNotification::new(title.clone(), caption.clone(), icon, project_name)
4395                })
4396            })
4397            .log_err()
4398            && let Some(pop_up) = screen_window.entity(cx).log_err()
4399        {
4400            self.notification_subscriptions
4401                .entry(screen_window)
4402                .or_insert_with(Vec::new)
4403                .push(cx.subscribe_in(&pop_up, window, {
4404                    |this, _, event, window, cx| match event {
4405                        AgentNotificationEvent::Accepted => {
4406                            let handle = window.window_handle();
4407                            cx.activate(true);
4408
4409                            let workspace_handle = this.workspace.clone();
4410
4411                            // If there are multiple Zed windows, activate the correct one.
4412                            cx.defer(move |cx| {
4413                                handle
4414                                    .update(cx, |_view, window, _cx| {
4415                                        window.activate_window();
4416
4417                                        if let Some(workspace) = workspace_handle.upgrade() {
4418                                            workspace.update(_cx, |workspace, cx| {
4419                                                workspace.focus_panel::<AgentPanel>(window, cx);
4420                                            });
4421                                        }
4422                                    })
4423                                    .log_err();
4424                            });
4425
4426                            this.dismiss_notifications(cx);
4427                        }
4428                        AgentNotificationEvent::Dismissed => {
4429                            this.dismiss_notifications(cx);
4430                        }
4431                    }
4432                }));
4433
4434            self.notifications.push(screen_window);
4435
4436            // If the user manually refocuses the original window, dismiss the popup.
4437            self.notification_subscriptions
4438                .entry(screen_window)
4439                .or_insert_with(Vec::new)
4440                .push({
4441                    let pop_up_weak = pop_up.downgrade();
4442
4443                    cx.observe_window_activation(window, move |_, window, cx| {
4444                        if window.is_window_active()
4445                            && let Some(pop_up) = pop_up_weak.upgrade()
4446                        {
4447                            pop_up.update(cx, |_, cx| {
4448                                cx.emit(AgentNotificationEvent::Dismissed);
4449                            });
4450                        }
4451                    })
4452                });
4453        }
4454    }
4455
4456    fn dismiss_notifications(&mut self, cx: &mut Context<Self>) {
4457        for window in self.notifications.drain(..) {
4458            window
4459                .update(cx, |_, window, _| {
4460                    window.remove_window();
4461                })
4462                .ok();
4463
4464            self.notification_subscriptions.remove(&window);
4465        }
4466    }
4467
4468    fn render_thread_controls(
4469        &self,
4470        thread: &Entity<AcpThread>,
4471        cx: &Context<Self>,
4472    ) -> impl IntoElement {
4473        let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
4474        if is_generating {
4475            return h_flex().id("thread-controls-container").child(
4476                div()
4477                    .py_2()
4478                    .px(rems_from_px(22.))
4479                    .child(SpinnerLabel::new().size(LabelSize::Small)),
4480            );
4481        }
4482
4483        let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
4484            .shape(ui::IconButtonShape::Square)
4485            .icon_size(IconSize::Small)
4486            .icon_color(Color::Ignored)
4487            .tooltip(Tooltip::text("Open Thread as Markdown"))
4488            .on_click(cx.listener(move |this, _, window, cx| {
4489                if let Some(workspace) = this.workspace.upgrade() {
4490                    this.open_thread_as_markdown(workspace, window, cx)
4491                        .detach_and_log_err(cx);
4492                }
4493            }));
4494
4495        let scroll_to_top = IconButton::new("scroll_to_top", IconName::ArrowUp)
4496            .shape(ui::IconButtonShape::Square)
4497            .icon_size(IconSize::Small)
4498            .icon_color(Color::Ignored)
4499            .tooltip(Tooltip::text("Scroll To Top"))
4500            .on_click(cx.listener(move |this, _, _, cx| {
4501                this.scroll_to_top(cx);
4502            }));
4503
4504        let mut container = h_flex()
4505            .id("thread-controls-container")
4506            .group("thread-controls-container")
4507            .w_full()
4508            .py_2()
4509            .px_5()
4510            .gap_px()
4511            .opacity(0.6)
4512            .hover(|style| style.opacity(1.))
4513            .flex_wrap()
4514            .justify_end();
4515
4516        if AgentSettings::get_global(cx).enable_feedback
4517            && self
4518                .thread()
4519                .is_some_and(|thread| thread.read(cx).connection().telemetry().is_some())
4520        {
4521            let feedback = self.thread_feedback.feedback;
4522
4523            container = container
4524                .child(
4525                    div().visible_on_hover("thread-controls-container").child(
4526                        Label::new(match feedback {
4527                            Some(ThreadFeedback::Positive) => "Thanks for your feedback!",
4528                            Some(ThreadFeedback::Negative) => {
4529                                "We appreciate your feedback and will use it to improve."
4530                            }
4531                            None => {
4532                                "Rating the thread sends all of your current conversation to the Zed team."
4533                            }
4534                        })
4535                        .color(Color::Muted)
4536                        .size(LabelSize::XSmall)
4537                        .truncate(),
4538                    ),
4539                )
4540                .child(
4541                    IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
4542                        .shape(ui::IconButtonShape::Square)
4543                        .icon_size(IconSize::Small)
4544                        .icon_color(match feedback {
4545                            Some(ThreadFeedback::Positive) => Color::Accent,
4546                            _ => Color::Ignored,
4547                        })
4548                        .tooltip(Tooltip::text("Helpful Response"))
4549                        .on_click(cx.listener(move |this, _, window, cx| {
4550                            this.handle_feedback_click(ThreadFeedback::Positive, window, cx);
4551                        })),
4552                )
4553                .child(
4554                    IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
4555                        .shape(ui::IconButtonShape::Square)
4556                        .icon_size(IconSize::Small)
4557                        .icon_color(match feedback {
4558                            Some(ThreadFeedback::Negative) => Color::Accent,
4559                            _ => Color::Ignored,
4560                        })
4561                        .tooltip(Tooltip::text("Not Helpful"))
4562                        .on_click(cx.listener(move |this, _, window, cx| {
4563                            this.handle_feedback_click(ThreadFeedback::Negative, window, cx);
4564                        })),
4565                );
4566        }
4567
4568        container.child(open_as_markdown).child(scroll_to_top)
4569    }
4570
4571    fn render_feedback_feedback_editor(editor: Entity<Editor>, cx: &Context<Self>) -> Div {
4572        h_flex()
4573            .key_context("AgentFeedbackMessageEditor")
4574            .on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| {
4575                this.thread_feedback.dismiss_comments();
4576                cx.notify();
4577            }))
4578            .on_action(cx.listener(move |this, _: &menu::Confirm, _window, cx| {
4579                this.submit_feedback_message(cx);
4580            }))
4581            .p_2()
4582            .mb_2()
4583            .mx_5()
4584            .gap_1()
4585            .rounded_md()
4586            .border_1()
4587            .border_color(cx.theme().colors().border)
4588            .bg(cx.theme().colors().editor_background)
4589            .child(div().w_full().child(editor))
4590            .child(
4591                h_flex()
4592                    .child(
4593                        IconButton::new("dismiss-feedback-message", IconName::Close)
4594                            .icon_color(Color::Error)
4595                            .icon_size(IconSize::XSmall)
4596                            .shape(ui::IconButtonShape::Square)
4597                            .on_click(cx.listener(move |this, _, _window, cx| {
4598                                this.thread_feedback.dismiss_comments();
4599                                cx.notify();
4600                            })),
4601                    )
4602                    .child(
4603                        IconButton::new("submit-feedback-message", IconName::Return)
4604                            .icon_size(IconSize::XSmall)
4605                            .shape(ui::IconButtonShape::Square)
4606                            .on_click(cx.listener(move |this, _, _window, cx| {
4607                                this.submit_feedback_message(cx);
4608                            })),
4609                    ),
4610            )
4611    }
4612
4613    fn handle_feedback_click(
4614        &mut self,
4615        feedback: ThreadFeedback,
4616        window: &mut Window,
4617        cx: &mut Context<Self>,
4618    ) {
4619        let Some(thread) = self.thread().cloned() else {
4620            return;
4621        };
4622
4623        self.thread_feedback.submit(thread, feedback, window, cx);
4624        cx.notify();
4625    }
4626
4627    fn submit_feedback_message(&mut self, cx: &mut Context<Self>) {
4628        let Some(thread) = self.thread().cloned() else {
4629            return;
4630        };
4631
4632        self.thread_feedback.submit_comments(thread, cx);
4633        cx.notify();
4634    }
4635
4636    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
4637        div()
4638            .id("acp-thread-scrollbar")
4639            .occlude()
4640            .on_mouse_move(cx.listener(|_, _, _, cx| {
4641                cx.notify();
4642                cx.stop_propagation()
4643            }))
4644            .on_hover(|_, _, cx| {
4645                cx.stop_propagation();
4646            })
4647            .on_any_mouse_down(|_, _, cx| {
4648                cx.stop_propagation();
4649            })
4650            .on_mouse_up(
4651                MouseButton::Left,
4652                cx.listener(|_, _, _, cx| {
4653                    cx.stop_propagation();
4654                }),
4655            )
4656            .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4657                cx.notify();
4658            }))
4659            .h_full()
4660            .absolute()
4661            .right_1()
4662            .top_1()
4663            .bottom_0()
4664            .w(px(12.))
4665            .cursor_default()
4666            .children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx)))
4667    }
4668
4669    fn render_token_limit_callout(
4670        &self,
4671        line_height: Pixels,
4672        cx: &mut Context<Self>,
4673    ) -> Option<Callout> {
4674        let token_usage = self.thread()?.read(cx).token_usage()?;
4675        let ratio = token_usage.ratio();
4676
4677        let (severity, title) = match ratio {
4678            acp_thread::TokenUsageRatio::Normal => return None,
4679            acp_thread::TokenUsageRatio::Warning => {
4680                (Severity::Warning, "Thread reaching the token limit soon")
4681            }
4682            acp_thread::TokenUsageRatio::Exceeded => {
4683                (Severity::Error, "Thread reached the token limit")
4684            }
4685        };
4686
4687        let burn_mode_available = self.as_native_thread(cx).is_some_and(|thread| {
4688            thread.read(cx).completion_mode() == CompletionMode::Normal
4689                && thread
4690                    .read(cx)
4691                    .model()
4692                    .is_some_and(|model| model.supports_burn_mode())
4693        });
4694
4695        let description = if burn_mode_available {
4696            "To continue, start a new thread from a summary or turn Burn Mode on."
4697        } else {
4698            "To continue, start a new thread from a summary."
4699        };
4700
4701        Some(
4702            Callout::new()
4703                .severity(severity)
4704                .line_height(line_height)
4705                .title(title)
4706                .description(description)
4707                .actions_slot(
4708                    h_flex()
4709                        .gap_0p5()
4710                        .child(
4711                            Button::new("start-new-thread", "Start New Thread")
4712                                .label_size(LabelSize::Small)
4713                                .on_click(cx.listener(|this, _, window, cx| {
4714                                    let Some(thread) = this.thread() else {
4715                                        return;
4716                                    };
4717                                    let session_id = thread.read(cx).session_id().clone();
4718                                    window.dispatch_action(
4719                                        crate::NewNativeAgentThreadFromSummary {
4720                                            from_session_id: session_id,
4721                                        }
4722                                        .boxed_clone(),
4723                                        cx,
4724                                    );
4725                                })),
4726                        )
4727                        .when(burn_mode_available, |this| {
4728                            this.child(
4729                                IconButton::new("burn-mode-callout", IconName::ZedBurnMode)
4730                                    .icon_size(IconSize::XSmall)
4731                                    .on_click(cx.listener(|this, _event, window, cx| {
4732                                        this.toggle_burn_mode(&ToggleBurnMode, window, cx);
4733                                    })),
4734                            )
4735                        }),
4736                ),
4737        )
4738    }
4739
4740    fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context<Self>) -> Option<Div> {
4741        if !self.is_using_zed_ai_models(cx) {
4742            return None;
4743        }
4744
4745        let user_store = self.project.read(cx).user_store().read(cx);
4746        if user_store.is_usage_based_billing_enabled() {
4747            return None;
4748        }
4749
4750        let plan = user_store.plan().unwrap_or(cloud_llm_client::Plan::ZedFree);
4751
4752        let usage = user_store.model_request_usage()?;
4753
4754        Some(
4755            div()
4756                .child(UsageCallout::new(plan, usage))
4757                .line_height(line_height),
4758        )
4759    }
4760
4761    fn agent_font_size_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
4762        self.entry_view_state.update(cx, |entry_view_state, cx| {
4763            entry_view_state.agent_font_size_changed(cx);
4764        });
4765    }
4766
4767    pub(crate) fn insert_dragged_files(
4768        &self,
4769        paths: Vec<project::ProjectPath>,
4770        added_worktrees: Vec<Entity<project::Worktree>>,
4771        window: &mut Window,
4772        cx: &mut Context<Self>,
4773    ) {
4774        self.message_editor.update(cx, |message_editor, cx| {
4775            message_editor.insert_dragged_files(paths, added_worktrees, window, cx);
4776        })
4777    }
4778
4779    pub(crate) fn insert_selections(&self, window: &mut Window, cx: &mut Context<Self>) {
4780        self.message_editor.update(cx, |message_editor, cx| {
4781            message_editor.insert_selections(window, cx);
4782        })
4783    }
4784
4785    fn render_thread_retry_status_callout(
4786        &self,
4787        _window: &mut Window,
4788        _cx: &mut Context<Self>,
4789    ) -> Option<Callout> {
4790        let state = self.thread_retry_status.as_ref()?;
4791
4792        let next_attempt_in = state
4793            .duration
4794            .saturating_sub(Instant::now().saturating_duration_since(state.started_at));
4795        if next_attempt_in.is_zero() {
4796            return None;
4797        }
4798
4799        let next_attempt_in_secs = next_attempt_in.as_secs() + 1;
4800
4801        let retry_message = if state.max_attempts == 1 {
4802            if next_attempt_in_secs == 1 {
4803                "Retrying. Next attempt in 1 second.".to_string()
4804            } else {
4805                format!("Retrying. Next attempt in {next_attempt_in_secs} seconds.")
4806            }
4807        } else if next_attempt_in_secs == 1 {
4808            format!(
4809                "Retrying. Next attempt in 1 second (Attempt {} of {}).",
4810                state.attempt, state.max_attempts,
4811            )
4812        } else {
4813            format!(
4814                "Retrying. Next attempt in {next_attempt_in_secs} seconds (Attempt {} of {}).",
4815                state.attempt, state.max_attempts,
4816            )
4817        };
4818
4819        Some(
4820            Callout::new()
4821                .severity(Severity::Warning)
4822                .title(state.last_error.clone())
4823                .description(retry_message),
4824        )
4825    }
4826
4827    fn render_thread_error(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
4828        let content = match self.thread_error.as_ref()? {
4829            ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx),
4830            ThreadError::Refusal => self.render_refusal_error(cx),
4831            ThreadError::AuthenticationRequired(error) => {
4832                self.render_authentication_required_error(error.clone(), cx)
4833            }
4834            ThreadError::PaymentRequired => self.render_payment_required_error(cx),
4835            ThreadError::ModelRequestLimitReached(plan) => {
4836                self.render_model_request_limit_reached_error(*plan, cx)
4837            }
4838            ThreadError::ToolUseLimitReached => {
4839                self.render_tool_use_limit_reached_error(window, cx)?
4840            }
4841        };
4842
4843        Some(div().child(content))
4844    }
4845
4846    fn render_new_version_callout(&self, version: &SharedString, cx: &mut Context<Self>) -> Div {
4847        v_flex().w_full().justify_end().child(
4848            h_flex()
4849                .p_2()
4850                .pr_3()
4851                .w_full()
4852                .gap_1p5()
4853                .border_t_1()
4854                .border_color(cx.theme().colors().border)
4855                .bg(cx.theme().colors().element_background)
4856                .child(
4857                    h_flex()
4858                        .flex_1()
4859                        .gap_1p5()
4860                        .child(
4861                            Icon::new(IconName::Download)
4862                                .color(Color::Accent)
4863                                .size(IconSize::Small),
4864                        )
4865                        .child(Label::new("New version available").size(LabelSize::Small)),
4866                )
4867                .child(
4868                    Button::new("update-button", format!("Update to v{}", version))
4869                        .label_size(LabelSize::Small)
4870                        .style(ButtonStyle::Tinted(TintColor::Accent))
4871                        .on_click(cx.listener(|this, _, window, cx| {
4872                            this.reset(window, cx);
4873                        })),
4874                ),
4875        )
4876    }
4877
4878    fn get_current_model_name(&self, cx: &App) -> SharedString {
4879        // For native agent (Zed Agent), use the specific model name (e.g., "Claude 3.5 Sonnet")
4880        // For ACP agents, use the agent name (e.g., "Claude Code", "Gemini CLI")
4881        // This provides better clarity about what refused the request
4882        if self
4883            .agent
4884            .clone()
4885            .downcast::<agent2::NativeAgentServer>()
4886            .is_some()
4887        {
4888            // Native agent - use the model name
4889            self.model_selector
4890                .as_ref()
4891                .and_then(|selector| selector.read(cx).active_model_name(cx))
4892                .unwrap_or_else(|| SharedString::from("The model"))
4893        } else {
4894            // ACP agent - use the agent name (e.g., "Claude Code", "Gemini CLI")
4895            self.agent.name()
4896        }
4897    }
4898
4899    fn render_refusal_error(&self, cx: &mut Context<'_, Self>) -> Callout {
4900        let model_or_agent_name = self.get_current_model_name(cx);
4901        let refusal_message = format!(
4902            "{} refused to respond to this prompt. This can happen when a model believes the prompt violates its content policy or safety guidelines, so rephrasing it can sometimes address the issue.",
4903            model_or_agent_name
4904        );
4905
4906        Callout::new()
4907            .severity(Severity::Error)
4908            .title("Request Refused")
4909            .icon(IconName::XCircle)
4910            .description(refusal_message.clone())
4911            .actions_slot(self.create_copy_button(&refusal_message))
4912            .dismiss_action(self.dismiss_error_button(cx))
4913    }
4914
4915    fn render_any_thread_error(&self, error: SharedString, cx: &mut Context<'_, Self>) -> Callout {
4916        let can_resume = self
4917            .thread()
4918            .map_or(false, |thread| thread.read(cx).can_resume(cx));
4919
4920        let can_enable_burn_mode = self.as_native_thread(cx).map_or(false, |thread| {
4921            let thread = thread.read(cx);
4922            let supports_burn_mode = thread
4923                .model()
4924                .map_or(false, |model| model.supports_burn_mode());
4925            supports_burn_mode && thread.completion_mode() == CompletionMode::Normal
4926        });
4927
4928        Callout::new()
4929            .severity(Severity::Error)
4930            .title("Error")
4931            .icon(IconName::XCircle)
4932            .description(error.clone())
4933            .actions_slot(
4934                h_flex()
4935                    .gap_0p5()
4936                    .when(can_resume && can_enable_burn_mode, |this| {
4937                        this.child(
4938                            Button::new("enable-burn-mode-and-retry", "Enable Burn Mode and Retry")
4939                                .icon(IconName::ZedBurnMode)
4940                                .icon_position(IconPosition::Start)
4941                                .icon_size(IconSize::Small)
4942                                .label_size(LabelSize::Small)
4943                                .on_click(cx.listener(|this, _, window, cx| {
4944                                    this.toggle_burn_mode(&ToggleBurnMode, window, cx);
4945                                    this.resume_chat(cx);
4946                                })),
4947                        )
4948                    })
4949                    .when(can_resume, |this| {
4950                        this.child(
4951                            Button::new("retry", "Retry")
4952                                .icon(IconName::RotateCw)
4953                                .icon_position(IconPosition::Start)
4954                                .icon_size(IconSize::Small)
4955                                .label_size(LabelSize::Small)
4956                                .on_click(cx.listener(|this, _, _window, cx| {
4957                                    this.resume_chat(cx);
4958                                })),
4959                        )
4960                    })
4961                    .child(self.create_copy_button(error.to_string())),
4962            )
4963            .dismiss_action(self.dismiss_error_button(cx))
4964    }
4965
4966    fn render_payment_required_error(&self, cx: &mut Context<Self>) -> Callout {
4967        const ERROR_MESSAGE: &str =
4968            "You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
4969
4970        Callout::new()
4971            .severity(Severity::Error)
4972            .icon(IconName::XCircle)
4973            .title("Free Usage Exceeded")
4974            .description(ERROR_MESSAGE)
4975            .actions_slot(
4976                h_flex()
4977                    .gap_0p5()
4978                    .child(self.upgrade_button(cx))
4979                    .child(self.create_copy_button(ERROR_MESSAGE)),
4980            )
4981            .dismiss_action(self.dismiss_error_button(cx))
4982    }
4983
4984    fn render_authentication_required_error(
4985        &self,
4986        error: SharedString,
4987        cx: &mut Context<Self>,
4988    ) -> Callout {
4989        Callout::new()
4990            .severity(Severity::Error)
4991            .title("Authentication Required")
4992            .icon(IconName::XCircle)
4993            .description(error.clone())
4994            .actions_slot(
4995                h_flex()
4996                    .gap_0p5()
4997                    .child(self.authenticate_button(cx))
4998                    .child(self.create_copy_button(error)),
4999            )
5000            .dismiss_action(self.dismiss_error_button(cx))
5001    }
5002
5003    fn render_model_request_limit_reached_error(
5004        &self,
5005        plan: cloud_llm_client::Plan,
5006        cx: &mut Context<Self>,
5007    ) -> Callout {
5008        let error_message = match plan {
5009            cloud_llm_client::Plan::ZedPro => "Upgrade to usage-based billing for more prompts.",
5010            cloud_llm_client::Plan::ZedProTrial | cloud_llm_client::Plan::ZedFree => {
5011                "Upgrade to Zed Pro for more prompts."
5012            }
5013            cloud_llm_client::Plan::ZedProV2 | cloud_llm_client::Plan::ZedProTrialV2 => "",
5014        };
5015
5016        Callout::new()
5017            .severity(Severity::Error)
5018            .title("Model Prompt Limit Reached")
5019            .icon(IconName::XCircle)
5020            .description(error_message)
5021            .actions_slot(
5022                h_flex()
5023                    .gap_0p5()
5024                    .child(self.upgrade_button(cx))
5025                    .child(self.create_copy_button(error_message)),
5026            )
5027            .dismiss_action(self.dismiss_error_button(cx))
5028    }
5029
5030    fn render_tool_use_limit_reached_error(
5031        &self,
5032        window: &mut Window,
5033        cx: &mut Context<Self>,
5034    ) -> Option<Callout> {
5035        let thread = self.as_native_thread(cx)?;
5036        let supports_burn_mode = thread
5037            .read(cx)
5038            .model()
5039            .is_some_and(|model| model.supports_burn_mode());
5040
5041        let focus_handle = self.focus_handle(cx);
5042
5043        Some(
5044            Callout::new()
5045                .icon(IconName::Info)
5046                .title("Consecutive tool use limit reached.")
5047                .actions_slot(
5048                    h_flex()
5049                        .gap_0p5()
5050                        .when(supports_burn_mode, |this| {
5051                            this.child(
5052                                Button::new("continue-burn-mode", "Continue with Burn Mode")
5053                                    .style(ButtonStyle::Filled)
5054                                    .style(ButtonStyle::Tinted(ui::TintColor::Accent))
5055                                    .layer(ElevationIndex::ModalSurface)
5056                                    .label_size(LabelSize::Small)
5057                                    .key_binding(
5058                                        KeyBinding::for_action_in(
5059                                            &ContinueWithBurnMode,
5060                                            &focus_handle,
5061                                            window,
5062                                            cx,
5063                                        )
5064                                        .map(|kb| kb.size(rems_from_px(10.))),
5065                                    )
5066                                    .tooltip(Tooltip::text(
5067                                        "Enable Burn Mode for unlimited tool use.",
5068                                    ))
5069                                    .on_click({
5070                                        cx.listener(move |this, _, _window, cx| {
5071                                            thread.update(cx, |thread, cx| {
5072                                                thread
5073                                                    .set_completion_mode(CompletionMode::Burn, cx);
5074                                            });
5075                                            this.resume_chat(cx);
5076                                        })
5077                                    }),
5078                            )
5079                        })
5080                        .child(
5081                            Button::new("continue-conversation", "Continue")
5082                                .layer(ElevationIndex::ModalSurface)
5083                                .label_size(LabelSize::Small)
5084                                .key_binding(
5085                                    KeyBinding::for_action_in(
5086                                        &ContinueThread,
5087                                        &focus_handle,
5088                                        window,
5089                                        cx,
5090                                    )
5091                                    .map(|kb| kb.size(rems_from_px(10.))),
5092                                )
5093                                .on_click(cx.listener(|this, _, _window, cx| {
5094                                    this.resume_chat(cx);
5095                                })),
5096                        ),
5097                ),
5098        )
5099    }
5100
5101    fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
5102        let message = message.into();
5103
5104        IconButton::new("copy", IconName::Copy)
5105            .icon_size(IconSize::Small)
5106            .icon_color(Color::Muted)
5107            .tooltip(Tooltip::text("Copy Error Message"))
5108            .on_click(move |_, _, cx| {
5109                cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
5110            })
5111    }
5112
5113    fn dismiss_error_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
5114        IconButton::new("dismiss", IconName::Close)
5115            .icon_size(IconSize::Small)
5116            .icon_color(Color::Muted)
5117            .tooltip(Tooltip::text("Dismiss Error"))
5118            .on_click(cx.listener({
5119                move |this, _, _, cx| {
5120                    this.clear_thread_error(cx);
5121                    cx.notify();
5122                }
5123            }))
5124    }
5125
5126    fn authenticate_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
5127        Button::new("authenticate", "Authenticate")
5128            .label_size(LabelSize::Small)
5129            .style(ButtonStyle::Filled)
5130            .on_click(cx.listener({
5131                move |this, _, window, cx| {
5132                    let agent = this.agent.clone();
5133                    let ThreadState::Ready { thread, .. } = &this.thread_state else {
5134                        return;
5135                    };
5136
5137                    let connection = thread.read(cx).connection().clone();
5138                    let err = AuthRequired {
5139                        description: None,
5140                        provider_id: None,
5141                    };
5142                    this.clear_thread_error(cx);
5143                    let this = cx.weak_entity();
5144                    window.defer(cx, |window, cx| {
5145                        Self::handle_auth_required(this, err, agent, connection, window, cx);
5146                    })
5147                }
5148            }))
5149    }
5150
5151    pub(crate) fn reauthenticate(&mut self, window: &mut Window, cx: &mut Context<Self>) {
5152        let agent = self.agent.clone();
5153        let ThreadState::Ready { thread, .. } = &self.thread_state else {
5154            return;
5155        };
5156
5157        let connection = thread.read(cx).connection().clone();
5158        let err = AuthRequired {
5159            description: None,
5160            provider_id: None,
5161        };
5162        self.clear_thread_error(cx);
5163        let this = cx.weak_entity();
5164        window.defer(cx, |window, cx| {
5165            Self::handle_auth_required(this, err, agent, connection, window, cx);
5166        })
5167    }
5168
5169    fn upgrade_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
5170        Button::new("upgrade", "Upgrade")
5171            .label_size(LabelSize::Small)
5172            .style(ButtonStyle::Tinted(ui::TintColor::Accent))
5173            .on_click(cx.listener({
5174                move |this, _, _, cx| {
5175                    this.clear_thread_error(cx);
5176                    cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx));
5177                }
5178            }))
5179    }
5180
5181    pub fn delete_history_entry(&mut self, entry: HistoryEntry, cx: &mut Context<Self>) {
5182        let task = match entry {
5183            HistoryEntry::AcpThread(thread) => self.history_store.update(cx, |history, cx| {
5184                history.delete_thread(thread.id.clone(), cx)
5185            }),
5186            HistoryEntry::TextThread(context) => self.history_store.update(cx, |history, cx| {
5187                history.delete_text_thread(context.path.clone(), cx)
5188            }),
5189        };
5190        task.detach_and_log_err(cx);
5191    }
5192}
5193
5194fn loading_contents_spinner(size: IconSize) -> AnyElement {
5195    Icon::new(IconName::LoadCircle)
5196        .size(size)
5197        .color(Color::Accent)
5198        .with_rotate_animation(3)
5199        .into_any_element()
5200}
5201
5202impl Focusable for AcpThreadView {
5203    fn focus_handle(&self, cx: &App) -> FocusHandle {
5204        match self.thread_state {
5205            ThreadState::Loading { .. } | ThreadState::Ready { .. } => {
5206                self.message_editor.focus_handle(cx)
5207            }
5208            ThreadState::LoadError(_) | ThreadState::Unauthenticated { .. } => {
5209                self.focus_handle.clone()
5210            }
5211        }
5212    }
5213}
5214
5215impl Render for AcpThreadView {
5216    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5217        let has_messages = self.list_state.item_count() > 0;
5218        let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5;
5219
5220        v_flex()
5221            .size_full()
5222            .key_context("AcpThread")
5223            .on_action(cx.listener(Self::open_agent_diff))
5224            .on_action(cx.listener(Self::toggle_burn_mode))
5225            .on_action(cx.listener(Self::keep_all))
5226            .on_action(cx.listener(Self::reject_all))
5227            .track_focus(&self.focus_handle)
5228            .bg(cx.theme().colors().panel_background)
5229            .child(match &self.thread_state {
5230                ThreadState::Unauthenticated {
5231                    connection,
5232                    description,
5233                    configuration_view,
5234                    pending_auth_method,
5235                    ..
5236                } => self.render_auth_required_state(
5237                    connection,
5238                    description.as_ref(),
5239                    configuration_view.as_ref(),
5240                    pending_auth_method.as_ref(),
5241                    window,
5242                    cx,
5243                ),
5244                ThreadState::Loading { .. } => v_flex()
5245                    .flex_1()
5246                    .child(self.render_recent_history(window, cx)),
5247                ThreadState::LoadError(e) => v_flex()
5248                    .flex_1()
5249                    .size_full()
5250                    .items_center()
5251                    .justify_end()
5252                    .child(self.render_load_error(e, window, cx)),
5253                ThreadState::Ready { .. } => v_flex().flex_1().map(|this| {
5254                    if has_messages {
5255                        this.child(
5256                            list(
5257                                self.list_state.clone(),
5258                                cx.processor(|this, index: usize, window, cx| {
5259                                    let Some((entry, len)) = this.thread().and_then(|thread| {
5260                                        let entries = &thread.read(cx).entries();
5261                                        Some((entries.get(index)?, entries.len()))
5262                                    }) else {
5263                                        return Empty.into_any();
5264                                    };
5265                                    this.render_entry(index, len, entry, window, cx)
5266                                }),
5267                            )
5268                            .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
5269                            .flex_grow()
5270                            .into_any(),
5271                        )
5272                        .child(self.render_vertical_scrollbar(cx))
5273                    } else {
5274                        this.child(self.render_recent_history(window, cx))
5275                    }
5276                }),
5277            })
5278            // The activity bar is intentionally rendered outside of the ThreadState::Ready match
5279            // above so that the scrollbar doesn't render behind it. The current setup allows
5280            // the scrollbar to stop exactly at the activity bar start.
5281            .when(has_messages, |this| match &self.thread_state {
5282                ThreadState::Ready { thread, .. } => {
5283                    this.children(self.render_activity_bar(thread, window, cx))
5284                }
5285                _ => this,
5286            })
5287            .children(self.render_thread_retry_status_callout(window, cx))
5288            .children(self.render_thread_error(window, cx))
5289            .when_some(
5290                self.new_server_version_available.as_ref().filter(|_| {
5291                    !has_messages || !matches!(self.thread_state, ThreadState::Ready { .. })
5292                }),
5293                |this, version| this.child(self.render_new_version_callout(&version, cx)),
5294            )
5295            .children(
5296                if let Some(usage_callout) = self.render_usage_callout(line_height, cx) {
5297                    Some(usage_callout.into_any_element())
5298                } else {
5299                    self.render_token_limit_callout(line_height, cx)
5300                        .map(|token_limit_callout| token_limit_callout.into_any_element())
5301                },
5302            )
5303            .child(self.render_message_editor(window, cx))
5304    }
5305}
5306
5307fn default_markdown_style(
5308    buffer_font: bool,
5309    muted_text: bool,
5310    window: &Window,
5311    cx: &App,
5312) -> MarkdownStyle {
5313    let theme_settings = ThemeSettings::get_global(cx);
5314    let colors = cx.theme().colors();
5315
5316    let buffer_font_size = TextSize::Small.rems(cx);
5317
5318    let mut text_style = window.text_style();
5319    let line_height = buffer_font_size * 1.75;
5320
5321    let font_family = if buffer_font {
5322        theme_settings.buffer_font.family.clone()
5323    } else {
5324        theme_settings.ui_font.family.clone()
5325    };
5326
5327    let font_size = if buffer_font {
5328        TextSize::Small.rems(cx)
5329    } else {
5330        TextSize::Default.rems(cx)
5331    };
5332
5333    let text_color = if muted_text {
5334        colors.text_muted
5335    } else {
5336        colors.text
5337    };
5338
5339    text_style.refine(&TextStyleRefinement {
5340        font_family: Some(font_family),
5341        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
5342        font_features: Some(theme_settings.ui_font.features.clone()),
5343        font_size: Some(font_size.into()),
5344        line_height: Some(line_height.into()),
5345        color: Some(text_color),
5346        ..Default::default()
5347    });
5348
5349    MarkdownStyle {
5350        base_text_style: text_style.clone(),
5351        syntax: cx.theme().syntax().clone(),
5352        selection_background_color: colors.element_selection_background,
5353        code_block_overflow_x_scroll: true,
5354        table_overflow_x_scroll: true,
5355        heading_level_styles: Some(HeadingLevelStyles {
5356            h1: Some(TextStyleRefinement {
5357                font_size: Some(rems(1.15).into()),
5358                ..Default::default()
5359            }),
5360            h2: Some(TextStyleRefinement {
5361                font_size: Some(rems(1.1).into()),
5362                ..Default::default()
5363            }),
5364            h3: Some(TextStyleRefinement {
5365                font_size: Some(rems(1.05).into()),
5366                ..Default::default()
5367            }),
5368            h4: Some(TextStyleRefinement {
5369                font_size: Some(rems(1.).into()),
5370                ..Default::default()
5371            }),
5372            h5: Some(TextStyleRefinement {
5373                font_size: Some(rems(0.95).into()),
5374                ..Default::default()
5375            }),
5376            h6: Some(TextStyleRefinement {
5377                font_size: Some(rems(0.875).into()),
5378                ..Default::default()
5379            }),
5380        }),
5381        code_block: StyleRefinement {
5382            padding: EdgesRefinement {
5383                top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
5384                left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
5385                right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
5386                bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
5387            },
5388            margin: EdgesRefinement {
5389                top: Some(Length::Definite(Pixels(8.).into())),
5390                left: Some(Length::Definite(Pixels(0.).into())),
5391                right: Some(Length::Definite(Pixels(0.).into())),
5392                bottom: Some(Length::Definite(Pixels(12.).into())),
5393            },
5394            border_style: Some(BorderStyle::Solid),
5395            border_widths: EdgesRefinement {
5396                top: Some(AbsoluteLength::Pixels(Pixels(1.))),
5397                left: Some(AbsoluteLength::Pixels(Pixels(1.))),
5398                right: Some(AbsoluteLength::Pixels(Pixels(1.))),
5399                bottom: Some(AbsoluteLength::Pixels(Pixels(1.))),
5400            },
5401            border_color: Some(colors.border_variant),
5402            background: Some(colors.editor_background.into()),
5403            text: Some(TextStyleRefinement {
5404                font_family: Some(theme_settings.buffer_font.family.clone()),
5405                font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
5406                font_features: Some(theme_settings.buffer_font.features.clone()),
5407                font_size: Some(buffer_font_size.into()),
5408                ..Default::default()
5409            }),
5410            ..Default::default()
5411        },
5412        inline_code: TextStyleRefinement {
5413            font_family: Some(theme_settings.buffer_font.family.clone()),
5414            font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
5415            font_features: Some(theme_settings.buffer_font.features.clone()),
5416            font_size: Some(buffer_font_size.into()),
5417            background_color: Some(colors.editor_foreground.opacity(0.08)),
5418            ..Default::default()
5419        },
5420        link: TextStyleRefinement {
5421            background_color: Some(colors.editor_foreground.opacity(0.025)),
5422            underline: Some(UnderlineStyle {
5423                color: Some(colors.text_accent.opacity(0.5)),
5424                thickness: px(1.),
5425                ..Default::default()
5426            }),
5427            ..Default::default()
5428        },
5429        ..Default::default()
5430    }
5431}
5432
5433fn plan_label_markdown_style(
5434    status: &acp::PlanEntryStatus,
5435    window: &Window,
5436    cx: &App,
5437) -> MarkdownStyle {
5438    let default_md_style = default_markdown_style(false, false, window, cx);
5439
5440    MarkdownStyle {
5441        base_text_style: TextStyle {
5442            color: cx.theme().colors().text_muted,
5443            strikethrough: if matches!(status, acp::PlanEntryStatus::Completed) {
5444                Some(gpui::StrikethroughStyle {
5445                    thickness: px(1.),
5446                    color: Some(cx.theme().colors().text_muted.opacity(0.8)),
5447                })
5448            } else {
5449                None
5450            },
5451            ..default_md_style.base_text_style
5452        },
5453        ..default_md_style
5454    }
5455}
5456
5457fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
5458    let default_md_style = default_markdown_style(true, false, window, cx);
5459
5460    MarkdownStyle {
5461        base_text_style: TextStyle {
5462            ..default_md_style.base_text_style
5463        },
5464        selection_background_color: cx.theme().colors().element_selection_background,
5465        ..Default::default()
5466    }
5467}
5468
5469#[cfg(test)]
5470pub(crate) mod tests {
5471    use acp_thread::StubAgentConnection;
5472    use agent_client_protocol::SessionId;
5473    use assistant_context::ContextStore;
5474    use editor::EditorSettings;
5475    use fs::FakeFs;
5476    use gpui::{EventEmitter, SemanticVersion, TestAppContext, VisualTestContext};
5477    use project::Project;
5478    use serde_json::json;
5479    use settings::SettingsStore;
5480    use std::any::Any;
5481    use std::path::Path;
5482    use workspace::Item;
5483
5484    use super::*;
5485
5486    #[gpui::test]
5487    async fn test_drop(cx: &mut TestAppContext) {
5488        init_test(cx);
5489
5490        let (thread_view, _cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
5491        let weak_view = thread_view.downgrade();
5492        drop(thread_view);
5493        assert!(!weak_view.is_upgradable());
5494    }
5495
5496    #[gpui::test]
5497    async fn test_notification_for_stop_event(cx: &mut TestAppContext) {
5498        init_test(cx);
5499
5500        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
5501
5502        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
5503        message_editor.update_in(cx, |editor, window, cx| {
5504            editor.set_text("Hello", window, cx);
5505        });
5506
5507        cx.deactivate_window();
5508
5509        thread_view.update_in(cx, |thread_view, window, cx| {
5510            thread_view.send(window, cx);
5511        });
5512
5513        cx.run_until_parked();
5514
5515        assert!(
5516            cx.windows()
5517                .iter()
5518                .any(|window| window.downcast::<AgentNotification>().is_some())
5519        );
5520    }
5521
5522    #[gpui::test]
5523    async fn test_notification_for_error(cx: &mut TestAppContext) {
5524        init_test(cx);
5525
5526        let (thread_view, cx) =
5527            setup_thread_view(StubAgentServer::new(SaboteurAgentConnection), cx).await;
5528
5529        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
5530        message_editor.update_in(cx, |editor, window, cx| {
5531            editor.set_text("Hello", window, cx);
5532        });
5533
5534        cx.deactivate_window();
5535
5536        thread_view.update_in(cx, |thread_view, window, cx| {
5537            thread_view.send(window, cx);
5538        });
5539
5540        cx.run_until_parked();
5541
5542        assert!(
5543            cx.windows()
5544                .iter()
5545                .any(|window| window.downcast::<AgentNotification>().is_some())
5546        );
5547    }
5548
5549    #[gpui::test]
5550    async fn test_refusal_handling(cx: &mut TestAppContext) {
5551        init_test(cx);
5552
5553        let (thread_view, cx) =
5554            setup_thread_view(StubAgentServer::new(RefusalAgentConnection), cx).await;
5555
5556        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
5557        message_editor.update_in(cx, |editor, window, cx| {
5558            editor.set_text("Do something harmful", window, cx);
5559        });
5560
5561        thread_view.update_in(cx, |thread_view, window, cx| {
5562            thread_view.send(window, cx);
5563        });
5564
5565        cx.run_until_parked();
5566
5567        // Check that the refusal error is set
5568        thread_view.read_with(cx, |thread_view, _cx| {
5569            assert!(
5570                matches!(thread_view.thread_error, Some(ThreadError::Refusal)),
5571                "Expected refusal error to be set"
5572            );
5573        });
5574    }
5575
5576    #[gpui::test]
5577    async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) {
5578        init_test(cx);
5579
5580        let tool_call_id = acp::ToolCallId("1".into());
5581        let tool_call = acp::ToolCall {
5582            id: tool_call_id.clone(),
5583            title: "Label".into(),
5584            kind: acp::ToolKind::Edit,
5585            status: acp::ToolCallStatus::Pending,
5586            content: vec!["hi".into()],
5587            locations: vec![],
5588            raw_input: None,
5589            raw_output: None,
5590        };
5591        let connection =
5592            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
5593                tool_call_id,
5594                vec![acp::PermissionOption {
5595                    id: acp::PermissionOptionId("1".into()),
5596                    name: "Allow".into(),
5597                    kind: acp::PermissionOptionKind::AllowOnce,
5598                }],
5599            )]));
5600
5601        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
5602
5603        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
5604
5605        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
5606        message_editor.update_in(cx, |editor, window, cx| {
5607            editor.set_text("Hello", window, cx);
5608        });
5609
5610        cx.deactivate_window();
5611
5612        thread_view.update_in(cx, |thread_view, window, cx| {
5613            thread_view.send(window, cx);
5614        });
5615
5616        cx.run_until_parked();
5617
5618        assert!(
5619            cx.windows()
5620                .iter()
5621                .any(|window| window.downcast::<AgentNotification>().is_some())
5622        );
5623    }
5624
5625    async fn setup_thread_view(
5626        agent: impl AgentServer + 'static,
5627        cx: &mut TestAppContext,
5628    ) -> (Entity<AcpThreadView>, &mut VisualTestContext) {
5629        let fs = FakeFs::new(cx.executor());
5630        let project = Project::test(fs, [], cx).await;
5631        let (workspace, cx) =
5632            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5633
5634        let context_store =
5635            cx.update(|_window, cx| cx.new(|cx| ContextStore::fake(project.clone(), cx)));
5636        let history_store =
5637            cx.update(|_window, cx| cx.new(|cx| HistoryStore::new(context_store, cx)));
5638
5639        let thread_view = cx.update(|window, cx| {
5640            cx.new(|cx| {
5641                AcpThreadView::new(
5642                    Rc::new(agent),
5643                    None,
5644                    None,
5645                    workspace.downgrade(),
5646                    project,
5647                    history_store,
5648                    None,
5649                    window,
5650                    cx,
5651                )
5652            })
5653        });
5654        cx.run_until_parked();
5655        (thread_view, cx)
5656    }
5657
5658    fn add_to_workspace(thread_view: Entity<AcpThreadView>, cx: &mut VisualTestContext) {
5659        let workspace = thread_view.read_with(cx, |thread_view, _cx| thread_view.workspace.clone());
5660
5661        workspace
5662            .update_in(cx, |workspace, window, cx| {
5663                workspace.add_item_to_active_pane(
5664                    Box::new(cx.new(|_| ThreadViewItem(thread_view.clone()))),
5665                    None,
5666                    true,
5667                    window,
5668                    cx,
5669                );
5670            })
5671            .unwrap();
5672    }
5673
5674    struct ThreadViewItem(Entity<AcpThreadView>);
5675
5676    impl Item for ThreadViewItem {
5677        type Event = ();
5678
5679        fn include_in_nav_history() -> bool {
5680            false
5681        }
5682
5683        fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
5684            "Test".into()
5685        }
5686    }
5687
5688    impl EventEmitter<()> for ThreadViewItem {}
5689
5690    impl Focusable for ThreadViewItem {
5691        fn focus_handle(&self, cx: &App) -> FocusHandle {
5692            self.0.read(cx).focus_handle(cx)
5693        }
5694    }
5695
5696    impl Render for ThreadViewItem {
5697        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
5698            self.0.clone().into_any_element()
5699        }
5700    }
5701
5702    struct StubAgentServer<C> {
5703        connection: C,
5704    }
5705
5706    impl<C> StubAgentServer<C> {
5707        fn new(connection: C) -> Self {
5708            Self { connection }
5709        }
5710    }
5711
5712    impl StubAgentServer<StubAgentConnection> {
5713        fn default_response() -> Self {
5714            let conn = StubAgentConnection::new();
5715            conn.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk {
5716                content: "Default response".into(),
5717            }]);
5718            Self::new(conn)
5719        }
5720    }
5721
5722    impl<C> AgentServer for StubAgentServer<C>
5723    where
5724        C: 'static + AgentConnection + Send + Clone,
5725    {
5726        fn telemetry_id(&self) -> &'static str {
5727            "test"
5728        }
5729
5730        fn logo(&self) -> ui::IconName {
5731            ui::IconName::Ai
5732        }
5733
5734        fn name(&self) -> SharedString {
5735            "Test".into()
5736        }
5737
5738        fn connect(
5739            &self,
5740            _root_dir: Option<&Path>,
5741            _delegate: AgentServerDelegate,
5742            _cx: &mut App,
5743        ) -> Task<gpui::Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
5744            Task::ready(Ok((Rc::new(self.connection.clone()), None)))
5745        }
5746
5747        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
5748            self
5749        }
5750    }
5751
5752    #[derive(Clone)]
5753    struct SaboteurAgentConnection;
5754
5755    impl AgentConnection for SaboteurAgentConnection {
5756        fn new_thread(
5757            self: Rc<Self>,
5758            project: Entity<Project>,
5759            _cwd: &Path,
5760            cx: &mut gpui::App,
5761        ) -> Task<gpui::Result<Entity<AcpThread>>> {
5762            Task::ready(Ok(cx.new(|cx| {
5763                let action_log = cx.new(|_| ActionLog::new(project.clone()));
5764                AcpThread::new(
5765                    "SaboteurAgentConnection",
5766                    self,
5767                    project,
5768                    action_log,
5769                    SessionId("test".into()),
5770                    watch::Receiver::constant(acp::PromptCapabilities {
5771                        image: true,
5772                        audio: true,
5773                        embedded_context: true,
5774                    }),
5775                    cx,
5776                )
5777            })))
5778        }
5779
5780        fn auth_methods(&self) -> &[acp::AuthMethod] {
5781            &[]
5782        }
5783
5784        fn authenticate(
5785            &self,
5786            _method_id: acp::AuthMethodId,
5787            _cx: &mut App,
5788        ) -> Task<gpui::Result<()>> {
5789            unimplemented!()
5790        }
5791
5792        fn prompt(
5793            &self,
5794            _id: Option<acp_thread::UserMessageId>,
5795            _params: acp::PromptRequest,
5796            _cx: &mut App,
5797        ) -> Task<gpui::Result<acp::PromptResponse>> {
5798            Task::ready(Err(anyhow::anyhow!("Error prompting")))
5799        }
5800
5801        fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
5802            unimplemented!()
5803        }
5804
5805        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
5806            self
5807        }
5808    }
5809
5810    /// Simulates a model which always returns a refusal response
5811    #[derive(Clone)]
5812    struct RefusalAgentConnection;
5813
5814    impl AgentConnection for RefusalAgentConnection {
5815        fn new_thread(
5816            self: Rc<Self>,
5817            project: Entity<Project>,
5818            _cwd: &Path,
5819            cx: &mut gpui::App,
5820        ) -> Task<gpui::Result<Entity<AcpThread>>> {
5821            Task::ready(Ok(cx.new(|cx| {
5822                let action_log = cx.new(|_| ActionLog::new(project.clone()));
5823                AcpThread::new(
5824                    "RefusalAgentConnection",
5825                    self,
5826                    project,
5827                    action_log,
5828                    SessionId("test".into()),
5829                    watch::Receiver::constant(acp::PromptCapabilities {
5830                        image: true,
5831                        audio: true,
5832                        embedded_context: true,
5833                    }),
5834                    cx,
5835                )
5836            })))
5837        }
5838
5839        fn auth_methods(&self) -> &[acp::AuthMethod] {
5840            &[]
5841        }
5842
5843        fn authenticate(
5844            &self,
5845            _method_id: acp::AuthMethodId,
5846            _cx: &mut App,
5847        ) -> Task<gpui::Result<()>> {
5848            unimplemented!()
5849        }
5850
5851        fn prompt(
5852            &self,
5853            _id: Option<acp_thread::UserMessageId>,
5854            _params: acp::PromptRequest,
5855            _cx: &mut App,
5856        ) -> Task<gpui::Result<acp::PromptResponse>> {
5857            Task::ready(Ok(acp::PromptResponse {
5858                stop_reason: acp::StopReason::Refusal,
5859            }))
5860        }
5861
5862        fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
5863            unimplemented!()
5864        }
5865
5866        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
5867            self
5868        }
5869    }
5870
5871    pub(crate) fn init_test(cx: &mut TestAppContext) {
5872        cx.update(|cx| {
5873            let settings_store = SettingsStore::test(cx);
5874            cx.set_global(settings_store);
5875            language::init(cx);
5876            Project::init_settings(cx);
5877            AgentSettings::register(cx);
5878            workspace::init_settings(cx);
5879            ThemeSettings::register(cx);
5880            release_channel::init(SemanticVersion::default(), cx);
5881            EditorSettings::register(cx);
5882            prompt_store::init(cx)
5883        });
5884    }
5885
5886    #[gpui::test]
5887    async fn test_rewind_views(cx: &mut TestAppContext) {
5888        init_test(cx);
5889
5890        let fs = FakeFs::new(cx.executor());
5891        fs.insert_tree(
5892            "/project",
5893            json!({
5894                "test1.txt": "old content 1",
5895                "test2.txt": "old content 2"
5896            }),
5897        )
5898        .await;
5899        let project = Project::test(fs, [Path::new("/project")], cx).await;
5900        let (workspace, cx) =
5901            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5902
5903        let context_store =
5904            cx.update(|_window, cx| cx.new(|cx| ContextStore::fake(project.clone(), cx)));
5905        let history_store =
5906            cx.update(|_window, cx| cx.new(|cx| HistoryStore::new(context_store, cx)));
5907
5908        let connection = Rc::new(StubAgentConnection::new());
5909        let thread_view = cx.update(|window, cx| {
5910            cx.new(|cx| {
5911                AcpThreadView::new(
5912                    Rc::new(StubAgentServer::new(connection.as_ref().clone())),
5913                    None,
5914                    None,
5915                    workspace.downgrade(),
5916                    project.clone(),
5917                    history_store.clone(),
5918                    None,
5919                    window,
5920                    cx,
5921                )
5922            })
5923        });
5924
5925        cx.run_until_parked();
5926
5927        let thread = thread_view
5928            .read_with(cx, |view, _| view.thread().cloned())
5929            .unwrap();
5930
5931        // First user message
5932        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(acp::ToolCall {
5933            id: acp::ToolCallId("tool1".into()),
5934            title: "Edit file 1".into(),
5935            kind: acp::ToolKind::Edit,
5936            status: acp::ToolCallStatus::Completed,
5937            content: vec![acp::ToolCallContent::Diff {
5938                diff: acp::Diff {
5939                    path: "/project/test1.txt".into(),
5940                    old_text: Some("old content 1".into()),
5941                    new_text: "new content 1".into(),
5942                },
5943            }],
5944            locations: vec![],
5945            raw_input: None,
5946            raw_output: None,
5947        })]);
5948
5949        thread
5950            .update(cx, |thread, cx| thread.send_raw("Give me a diff", cx))
5951            .await
5952            .unwrap();
5953        cx.run_until_parked();
5954
5955        thread.read_with(cx, |thread, _| {
5956            assert_eq!(thread.entries().len(), 2);
5957        });
5958
5959        thread_view.read_with(cx, |view, cx| {
5960            view.entry_view_state.read_with(cx, |entry_view_state, _| {
5961                assert!(
5962                    entry_view_state
5963                        .entry(0)
5964                        .unwrap()
5965                        .message_editor()
5966                        .is_some()
5967                );
5968                assert!(entry_view_state.entry(1).unwrap().has_content());
5969            });
5970        });
5971
5972        // Second user message
5973        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(acp::ToolCall {
5974            id: acp::ToolCallId("tool2".into()),
5975            title: "Edit file 2".into(),
5976            kind: acp::ToolKind::Edit,
5977            status: acp::ToolCallStatus::Completed,
5978            content: vec![acp::ToolCallContent::Diff {
5979                diff: acp::Diff {
5980                    path: "/project/test2.txt".into(),
5981                    old_text: Some("old content 2".into()),
5982                    new_text: "new content 2".into(),
5983                },
5984            }],
5985            locations: vec![],
5986            raw_input: None,
5987            raw_output: None,
5988        })]);
5989
5990        thread
5991            .update(cx, |thread, cx| thread.send_raw("Another one", cx))
5992            .await
5993            .unwrap();
5994        cx.run_until_parked();
5995
5996        let second_user_message_id = thread.read_with(cx, |thread, _| {
5997            assert_eq!(thread.entries().len(), 4);
5998            let AgentThreadEntry::UserMessage(user_message) = &thread.entries()[2] else {
5999                panic!();
6000            };
6001            user_message.id.clone().unwrap()
6002        });
6003
6004        thread_view.read_with(cx, |view, cx| {
6005            view.entry_view_state.read_with(cx, |entry_view_state, _| {
6006                assert!(
6007                    entry_view_state
6008                        .entry(0)
6009                        .unwrap()
6010                        .message_editor()
6011                        .is_some()
6012                );
6013                assert!(entry_view_state.entry(1).unwrap().has_content());
6014                assert!(
6015                    entry_view_state
6016                        .entry(2)
6017                        .unwrap()
6018                        .message_editor()
6019                        .is_some()
6020                );
6021                assert!(entry_view_state.entry(3).unwrap().has_content());
6022            });
6023        });
6024
6025        // Rewind to first message
6026        thread
6027            .update(cx, |thread, cx| thread.rewind(second_user_message_id, cx))
6028            .await
6029            .unwrap();
6030
6031        cx.run_until_parked();
6032
6033        thread.read_with(cx, |thread, _| {
6034            assert_eq!(thread.entries().len(), 2);
6035        });
6036
6037        thread_view.read_with(cx, |view, cx| {
6038            view.entry_view_state.read_with(cx, |entry_view_state, _| {
6039                assert!(
6040                    entry_view_state
6041                        .entry(0)
6042                        .unwrap()
6043                        .message_editor()
6044                        .is_some()
6045                );
6046                assert!(entry_view_state.entry(1).unwrap().has_content());
6047
6048                // Old views should be dropped
6049                assert!(entry_view_state.entry(2).is_none());
6050                assert!(entry_view_state.entry(3).is_none());
6051            });
6052        });
6053    }
6054
6055    #[gpui::test]
6056    async fn test_message_editing_cancel(cx: &mut TestAppContext) {
6057        init_test(cx);
6058
6059        let connection = StubAgentConnection::new();
6060
6061        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk {
6062            content: acp::ContentBlock::Text(acp::TextContent {
6063                text: "Response".into(),
6064                annotations: None,
6065            }),
6066        }]);
6067
6068        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
6069        add_to_workspace(thread_view.clone(), cx);
6070
6071        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
6072        message_editor.update_in(cx, |editor, window, cx| {
6073            editor.set_text("Original message to edit", window, cx);
6074        });
6075        thread_view.update_in(cx, |thread_view, window, cx| {
6076            thread_view.send(window, cx);
6077        });
6078
6079        cx.run_until_parked();
6080
6081        let user_message_editor = thread_view.read_with(cx, |view, cx| {
6082            assert_eq!(view.editing_message, None);
6083
6084            view.entry_view_state
6085                .read(cx)
6086                .entry(0)
6087                .unwrap()
6088                .message_editor()
6089                .unwrap()
6090                .clone()
6091        });
6092
6093        // Focus
6094        cx.focus(&user_message_editor);
6095        thread_view.read_with(cx, |view, _cx| {
6096            assert_eq!(view.editing_message, Some(0));
6097        });
6098
6099        // Edit
6100        user_message_editor.update_in(cx, |editor, window, cx| {
6101            editor.set_text("Edited message content", window, cx);
6102        });
6103
6104        // Cancel
6105        user_message_editor.update_in(cx, |_editor, window, cx| {
6106            window.dispatch_action(Box::new(editor::actions::Cancel), cx);
6107        });
6108
6109        thread_view.read_with(cx, |view, _cx| {
6110            assert_eq!(view.editing_message, None);
6111        });
6112
6113        user_message_editor.read_with(cx, |editor, cx| {
6114            assert_eq!(editor.text(cx), "Original message to edit");
6115        });
6116    }
6117
6118    #[gpui::test]
6119    async fn test_message_doesnt_send_if_empty(cx: &mut TestAppContext) {
6120        init_test(cx);
6121
6122        let connection = StubAgentConnection::new();
6123
6124        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
6125        add_to_workspace(thread_view.clone(), cx);
6126
6127        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
6128        let mut events = cx.events(&message_editor);
6129        message_editor.update_in(cx, |editor, window, cx| {
6130            editor.set_text("", window, cx);
6131        });
6132
6133        message_editor.update_in(cx, |_editor, window, cx| {
6134            window.dispatch_action(Box::new(Chat), cx);
6135        });
6136        cx.run_until_parked();
6137        // We shouldn't have received any messages
6138        assert!(matches!(
6139            events.try_next(),
6140            Err(futures::channel::mpsc::TryRecvError { .. })
6141        ));
6142    }
6143
6144    #[gpui::test]
6145    async fn test_message_editing_regenerate(cx: &mut TestAppContext) {
6146        init_test(cx);
6147
6148        let connection = StubAgentConnection::new();
6149
6150        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk {
6151            content: acp::ContentBlock::Text(acp::TextContent {
6152                text: "Response".into(),
6153                annotations: None,
6154            }),
6155        }]);
6156
6157        let (thread_view, cx) =
6158            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
6159        add_to_workspace(thread_view.clone(), cx);
6160
6161        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
6162        message_editor.update_in(cx, |editor, window, cx| {
6163            editor.set_text("Original message to edit", window, cx);
6164        });
6165        thread_view.update_in(cx, |thread_view, window, cx| {
6166            thread_view.send(window, cx);
6167        });
6168
6169        cx.run_until_parked();
6170
6171        let user_message_editor = thread_view.read_with(cx, |view, cx| {
6172            assert_eq!(view.editing_message, None);
6173            assert_eq!(view.thread().unwrap().read(cx).entries().len(), 2);
6174
6175            view.entry_view_state
6176                .read(cx)
6177                .entry(0)
6178                .unwrap()
6179                .message_editor()
6180                .unwrap()
6181                .clone()
6182        });
6183
6184        // Focus
6185        cx.focus(&user_message_editor);
6186
6187        // Edit
6188        user_message_editor.update_in(cx, |editor, window, cx| {
6189            editor.set_text("Edited message content", window, cx);
6190        });
6191
6192        // Send
6193        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk {
6194            content: acp::ContentBlock::Text(acp::TextContent {
6195                text: "New Response".into(),
6196                annotations: None,
6197            }),
6198        }]);
6199
6200        user_message_editor.update_in(cx, |_editor, window, cx| {
6201            window.dispatch_action(Box::new(Chat), cx);
6202        });
6203
6204        cx.run_until_parked();
6205
6206        thread_view.read_with(cx, |view, cx| {
6207            assert_eq!(view.editing_message, None);
6208
6209            let entries = view.thread().unwrap().read(cx).entries();
6210            assert_eq!(entries.len(), 2);
6211            assert_eq!(
6212                entries[0].to_markdown(cx),
6213                "## User\n\nEdited message content\n\n"
6214            );
6215            assert_eq!(
6216                entries[1].to_markdown(cx),
6217                "## Assistant\n\nNew Response\n\n"
6218            );
6219
6220            let new_editor = view.entry_view_state.read_with(cx, |state, _cx| {
6221                assert!(!state.entry(1).unwrap().has_content());
6222                state.entry(0).unwrap().message_editor().unwrap().clone()
6223            });
6224
6225            assert_eq!(new_editor.read(cx).text(cx), "Edited message content");
6226        })
6227    }
6228
6229    #[gpui::test]
6230    async fn test_message_editing_while_generating(cx: &mut TestAppContext) {
6231        init_test(cx);
6232
6233        let connection = StubAgentConnection::new();
6234
6235        let (thread_view, cx) =
6236            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
6237        add_to_workspace(thread_view.clone(), cx);
6238
6239        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
6240        message_editor.update_in(cx, |editor, window, cx| {
6241            editor.set_text("Original message to edit", window, cx);
6242        });
6243        thread_view.update_in(cx, |thread_view, window, cx| {
6244            thread_view.send(window, cx);
6245        });
6246
6247        cx.run_until_parked();
6248
6249        let (user_message_editor, session_id) = thread_view.read_with(cx, |view, cx| {
6250            let thread = view.thread().unwrap().read(cx);
6251            assert_eq!(thread.entries().len(), 1);
6252
6253            let editor = view
6254                .entry_view_state
6255                .read(cx)
6256                .entry(0)
6257                .unwrap()
6258                .message_editor()
6259                .unwrap()
6260                .clone();
6261
6262            (editor, thread.session_id().clone())
6263        });
6264
6265        // Focus
6266        cx.focus(&user_message_editor);
6267
6268        thread_view.read_with(cx, |view, _cx| {
6269            assert_eq!(view.editing_message, Some(0));
6270        });
6271
6272        // Edit
6273        user_message_editor.update_in(cx, |editor, window, cx| {
6274            editor.set_text("Edited message content", window, cx);
6275        });
6276
6277        thread_view.read_with(cx, |view, _cx| {
6278            assert_eq!(view.editing_message, Some(0));
6279        });
6280
6281        // Finish streaming response
6282        cx.update(|_, cx| {
6283            connection.send_update(
6284                session_id.clone(),
6285                acp::SessionUpdate::AgentMessageChunk {
6286                    content: acp::ContentBlock::Text(acp::TextContent {
6287                        text: "Response".into(),
6288                        annotations: None,
6289                    }),
6290                },
6291                cx,
6292            );
6293            connection.end_turn(session_id, acp::StopReason::EndTurn);
6294        });
6295
6296        thread_view.read_with(cx, |view, _cx| {
6297            assert_eq!(view.editing_message, Some(0));
6298        });
6299
6300        cx.run_until_parked();
6301
6302        // Should still be editing
6303        cx.update(|window, cx| {
6304            assert!(user_message_editor.focus_handle(cx).is_focused(window));
6305            assert_eq!(thread_view.read(cx).editing_message, Some(0));
6306            assert_eq!(
6307                user_message_editor.read(cx).text(cx),
6308                "Edited message content"
6309            );
6310        });
6311    }
6312
6313    #[gpui::test]
6314    async fn test_interrupt(cx: &mut TestAppContext) {
6315        init_test(cx);
6316
6317        let connection = StubAgentConnection::new();
6318
6319        let (thread_view, cx) =
6320            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
6321        add_to_workspace(thread_view.clone(), cx);
6322
6323        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
6324        message_editor.update_in(cx, |editor, window, cx| {
6325            editor.set_text("Message 1", window, cx);
6326        });
6327        thread_view.update_in(cx, |thread_view, window, cx| {
6328            thread_view.send(window, cx);
6329        });
6330
6331        let (thread, session_id) = thread_view.read_with(cx, |view, cx| {
6332            let thread = view.thread().unwrap();
6333
6334            (thread.clone(), thread.read(cx).session_id().clone())
6335        });
6336
6337        cx.run_until_parked();
6338
6339        cx.update(|_, cx| {
6340            connection.send_update(
6341                session_id.clone(),
6342                acp::SessionUpdate::AgentMessageChunk {
6343                    content: "Message 1 resp".into(),
6344                },
6345                cx,
6346            );
6347        });
6348
6349        cx.run_until_parked();
6350
6351        thread.read_with(cx, |thread, cx| {
6352            assert_eq!(
6353                thread.to_markdown(cx),
6354                indoc::indoc! {"
6355                    ## User
6356
6357                    Message 1
6358
6359                    ## Assistant
6360
6361                    Message 1 resp
6362
6363                "}
6364            )
6365        });
6366
6367        message_editor.update_in(cx, |editor, window, cx| {
6368            editor.set_text("Message 2", window, cx);
6369        });
6370        thread_view.update_in(cx, |thread_view, window, cx| {
6371            thread_view.send(window, cx);
6372        });
6373
6374        cx.update(|_, cx| {
6375            // Simulate a response sent after beginning to cancel
6376            connection.send_update(
6377                session_id.clone(),
6378                acp::SessionUpdate::AgentMessageChunk {
6379                    content: "onse".into(),
6380                },
6381                cx,
6382            );
6383        });
6384
6385        cx.run_until_parked();
6386
6387        // Last Message 1 response should appear before Message 2
6388        thread.read_with(cx, |thread, cx| {
6389            assert_eq!(
6390                thread.to_markdown(cx),
6391                indoc::indoc! {"
6392                    ## User
6393
6394                    Message 1
6395
6396                    ## Assistant
6397
6398                    Message 1 response
6399
6400                    ## User
6401
6402                    Message 2
6403
6404                "}
6405            )
6406        });
6407
6408        cx.update(|_, cx| {
6409            connection.send_update(
6410                session_id.clone(),
6411                acp::SessionUpdate::AgentMessageChunk {
6412                    content: "Message 2 response".into(),
6413                },
6414                cx,
6415            );
6416            connection.end_turn(session_id.clone(), acp::StopReason::EndTurn);
6417        });
6418
6419        cx.run_until_parked();
6420
6421        thread.read_with(cx, |thread, cx| {
6422            assert_eq!(
6423                thread.to_markdown(cx),
6424                indoc::indoc! {"
6425                    ## User
6426
6427                    Message 1
6428
6429                    ## Assistant
6430
6431                    Message 1 response
6432
6433                    ## User
6434
6435                    Message 2
6436
6437                    ## Assistant
6438
6439                    Message 2 response
6440
6441                "}
6442            )
6443        });
6444    }
6445}