thread_view.rs

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