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