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