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