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