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 scroll_item = if is_at_top {
2762                        ContextMenuEntry::new("Scroll to Bottom").handler({
2763                            let entity = entity.clone();
2764                            move |_, cx| {
2765                                entity.update(cx, |this, cx| {
2766                                    this.scroll_to_bottom(cx);
2767                                });
2768                            }
2769                        })
2770                    } else {
2771                        ContextMenuEntry::new("Scroll to Top").handler({
2772                            let entity = entity.clone();
2773                            move |_, cx| {
2774                                entity.update(cx, |this, cx| {
2775                                    this.scroll_to_top(cx);
2776                                });
2777                            }
2778                        })
2779                    };
2780
2781                    let open_thread_as_markdown = ContextMenuEntry::new("Open Thread as Markdown")
2782                        .handler({
2783                            let entity = entity.clone();
2784                            let workspace = workspace.clone();
2785                            move |window, cx| {
2786                                if let Some(workspace) = workspace.upgrade() {
2787                                    entity
2788                                        .update(cx, |this, cx| {
2789                                            this.open_thread_as_markdown(workspace, window, cx)
2790                                        })
2791                                        .detach_and_log_err(cx);
2792                                }
2793                            }
2794                        });
2795
2796                    menu.when_some(focus, |menu, focus| menu.context(focus))
2797                        .action("Copy", Box::new(markdown::CopyAsMarkdown))
2798                        .separator()
2799                        .item(scroll_item)
2800                        .item(open_thread_as_markdown)
2801                })
2802            })
2803            .into_any_element()
2804    }
2805
2806    fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
2807        cx.theme()
2808            .colors()
2809            .element_background
2810            .blend(cx.theme().colors().editor_foreground.opacity(0.025))
2811    }
2812
2813    fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
2814        cx.theme().colors().border.opacity(0.8)
2815    }
2816
2817    fn tool_name_font_size(&self) -> Rems {
2818        rems_from_px(13.)
2819    }
2820
2821    fn render_thinking_block(
2822        &self,
2823        entry_ix: usize,
2824        chunk_ix: usize,
2825        chunk: Entity<Markdown>,
2826        window: &Window,
2827        cx: &Context<Self>,
2828    ) -> AnyElement {
2829        let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix));
2830        let card_header_id = SharedString::from("inner-card-header");
2831
2832        let key = (entry_ix, chunk_ix);
2833
2834        let is_open = self.expanded_thinking_blocks.contains(&key);
2835
2836        let scroll_handle = self
2837            .entry_view_state
2838            .read(cx)
2839            .entry(entry_ix)
2840            .and_then(|entry| entry.scroll_handle_for_assistant_message_chunk(chunk_ix));
2841
2842        let thinking_content = {
2843            div()
2844                .id(("thinking-content", chunk_ix))
2845                .when_some(scroll_handle, |this, scroll_handle| {
2846                    this.track_scroll(&scroll_handle)
2847                })
2848                .text_ui_sm(cx)
2849                .overflow_hidden()
2850                .child(
2851                    self.render_markdown(chunk, default_markdown_style(false, false, window, cx)),
2852                )
2853        };
2854
2855        v_flex()
2856            .gap_1()
2857            .child(
2858                h_flex()
2859                    .id(header_id)
2860                    .group(&card_header_id)
2861                    .relative()
2862                    .w_full()
2863                    .pr_1()
2864                    .justify_between()
2865                    .child(
2866                        h_flex()
2867                            .h(window.line_height() - px(2.))
2868                            .gap_1p5()
2869                            .overflow_hidden()
2870                            .child(
2871                                Icon::new(IconName::ToolThink)
2872                                    .size(IconSize::Small)
2873                                    .color(Color::Muted),
2874                            )
2875                            .child(
2876                                div()
2877                                    .text_size(self.tool_name_font_size())
2878                                    .text_color(cx.theme().colors().text_muted)
2879                                    .child("Thinking"),
2880                            ),
2881                    )
2882                    .child(
2883                        Disclosure::new(("expand", entry_ix), is_open)
2884                            .opened_icon(IconName::ChevronUp)
2885                            .closed_icon(IconName::ChevronDown)
2886                            .visible_on_hover(&card_header_id)
2887                            .on_click(cx.listener({
2888                                move |this, _event, _window, cx| {
2889                                    if is_open {
2890                                        this.expanded_thinking_blocks.remove(&key);
2891                                    } else {
2892                                        this.expanded_thinking_blocks.insert(key);
2893                                    }
2894                                    cx.notify();
2895                                }
2896                            })),
2897                    )
2898                    .on_click(cx.listener({
2899                        move |this, _event, _window, cx| {
2900                            if is_open {
2901                                this.expanded_thinking_blocks.remove(&key);
2902                            } else {
2903                                this.expanded_thinking_blocks.insert(key);
2904                            }
2905                            cx.notify();
2906                        }
2907                    })),
2908            )
2909            .when(is_open, |this| {
2910                this.child(
2911                    div()
2912                        .ml_1p5()
2913                        .pl_3p5()
2914                        .border_l_1()
2915                        .border_color(self.tool_card_border_color(cx))
2916                        .child(thinking_content),
2917                )
2918            })
2919            .into_any_element()
2920    }
2921
2922    fn render_tool_call(
2923        &self,
2924        entry_ix: usize,
2925        tool_call: &ToolCall,
2926        window: &Window,
2927        cx: &Context<Self>,
2928    ) -> Div {
2929        let has_location = tool_call.locations.len() == 1;
2930        let card_header_id = SharedString::from("inner-tool-call-header");
2931
2932        let failed_or_canceled = match &tool_call.status {
2933            ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => true,
2934            _ => false,
2935        };
2936
2937        let needs_confirmation = matches!(
2938            tool_call.status,
2939            ToolCallStatus::WaitingForConfirmation { .. }
2940        );
2941        let is_terminal_tool = matches!(tool_call.kind, acp::ToolKind::Execute);
2942
2943        let is_edit =
2944            matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some();
2945        let is_cancelled_edit = is_edit && matches!(tool_call.status, ToolCallStatus::Canceled);
2946        let has_revealed_diff = tool_call.diffs().next().is_some_and(|diff| {
2947            self.entry_view_state
2948                .read(cx)
2949                .entry(entry_ix)
2950                .and_then(|entry| entry.editor_for_diff(diff))
2951                .is_some()
2952                && diff.read(cx).has_revealed_range(cx)
2953        });
2954
2955        let use_card_layout = needs_confirmation || is_edit || is_terminal_tool;
2956
2957        let has_image_content = tool_call.content.iter().any(|c| c.image().is_some());
2958        let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
2959        let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
2960
2961        let should_show_raw_input = !is_terminal_tool && !is_edit && !has_image_content;
2962
2963        let input_output_header = |label: SharedString| {
2964            Label::new(label)
2965                .size(LabelSize::XSmall)
2966                .color(Color::Muted)
2967                .buffer_font(cx)
2968        };
2969
2970        let tool_output_display = if is_open {
2971            match &tool_call.status {
2972                ToolCallStatus::WaitingForConfirmation { options, .. } => v_flex()
2973                    .w_full()
2974                    .children(
2975                        tool_call
2976                            .content
2977                            .iter()
2978                            .enumerate()
2979                            .map(|(content_ix, content)| {
2980                                div()
2981                                    .child(self.render_tool_call_content(
2982                                        entry_ix,
2983                                        content,
2984                                        content_ix,
2985                                        tool_call,
2986                                        use_card_layout,
2987                                        has_image_content,
2988                                        failed_or_canceled,
2989                                        window,
2990                                        cx,
2991                                    ))
2992                                    .into_any_element()
2993                            }),
2994                    )
2995                    .when(should_show_raw_input, |this| {
2996                        let is_raw_input_expanded =
2997                            self.expanded_tool_call_raw_inputs.contains(&tool_call.id);
2998
2999                        let input_header = if is_raw_input_expanded {
3000                            "Raw Input:"
3001                        } else {
3002                            "View Raw Input"
3003                        };
3004
3005                        this.child(
3006                            v_flex()
3007                                .p_2()
3008                                .gap_1()
3009                                .border_t_1()
3010                                .border_color(self.tool_card_border_color(cx))
3011                                .child(
3012                                    h_flex()
3013                                        .id("disclosure_container")
3014                                        .pl_0p5()
3015                                        .gap_1()
3016                                        .justify_between()
3017                                        .rounded_xs()
3018                                        .hover(|s| s.bg(cx.theme().colors().element_hover))
3019                                        .child(input_output_header(input_header.into()))
3020                                        .child(
3021                                            Disclosure::new(
3022                                                ("raw-input-disclosure", entry_ix),
3023                                                is_raw_input_expanded,
3024                                            )
3025                                            .opened_icon(IconName::ChevronUp)
3026                                            .closed_icon(IconName::ChevronDown),
3027                                        )
3028                                        .on_click(cx.listener({
3029                                            let id = tool_call.id.clone();
3030
3031                                            move |this: &mut Self, _, _, cx| {
3032                                                if this.expanded_tool_call_raw_inputs.contains(&id)
3033                                                {
3034                                                    this.expanded_tool_call_raw_inputs.remove(&id);
3035                                                } else {
3036                                                    this.expanded_tool_call_raw_inputs
3037                                                        .insert(id.clone());
3038                                                }
3039                                                cx.notify();
3040                                            }
3041                                        })),
3042                                )
3043                                .when(is_raw_input_expanded, |this| {
3044                                    this.children(tool_call.raw_input_markdown.clone().map(
3045                                        |input| {
3046                                            self.render_markdown(
3047                                                input,
3048                                                default_markdown_style(false, false, window, cx),
3049                                            )
3050                                        },
3051                                    ))
3052                                }),
3053                        )
3054                    })
3055                    .child(self.render_permission_buttons(
3056                        tool_call.kind,
3057                        options,
3058                        entry_ix,
3059                        tool_call.id.clone(),
3060                        cx,
3061                    ))
3062                    .into_any(),
3063                ToolCallStatus::Pending | ToolCallStatus::InProgress
3064                    if is_edit
3065                        && tool_call.content.is_empty()
3066                        && self.as_native_connection(cx).is_some() =>
3067                {
3068                    self.render_diff_loading(cx).into_any()
3069                }
3070                ToolCallStatus::Pending
3071                | ToolCallStatus::InProgress
3072                | ToolCallStatus::Completed
3073                | ToolCallStatus::Failed
3074                | ToolCallStatus::Canceled => {
3075                    v_flex()
3076                        .when(should_show_raw_input, |this| {
3077                            this.mt_1p5().w_full().child(
3078                                v_flex()
3079                                    .ml(rems(0.4))
3080                                    .px_3p5()
3081                                    .pb_1()
3082                                    .gap_1()
3083                                    .border_l_1()
3084                                    .border_color(self.tool_card_border_color(cx))
3085                                    .child(input_output_header("Raw Input:".into()))
3086                                    .children(tool_call.raw_input_markdown.clone().map(|input| {
3087                                        div().id(("tool-call-raw-input-markdown", entry_ix)).child(
3088                                            self.render_markdown(
3089                                                input,
3090                                                default_markdown_style(false, false, window, cx),
3091                                            ),
3092                                        )
3093                                    }))
3094                                    .child(input_output_header("Output:".into())),
3095                            )
3096                        })
3097                        .children(tool_call.content.iter().enumerate().map(
3098                            |(content_ix, content)| {
3099                                div().id(("tool-call-output", entry_ix)).child(
3100                                    self.render_tool_call_content(
3101                                        entry_ix,
3102                                        content,
3103                                        content_ix,
3104                                        tool_call,
3105                                        use_card_layout,
3106                                        has_image_content,
3107                                        failed_or_canceled,
3108                                        window,
3109                                        cx,
3110                                    ),
3111                                )
3112                            },
3113                        ))
3114                        .into_any()
3115                }
3116                ToolCallStatus::Rejected => Empty.into_any(),
3117            }
3118            .into()
3119        } else {
3120            None
3121        };
3122
3123        v_flex()
3124            .map(|this| {
3125                if use_card_layout {
3126                    this.my_1p5()
3127                        .rounded_md()
3128                        .border_1()
3129                        .when(failed_or_canceled, |this| this.border_dashed())
3130                        .border_color(self.tool_card_border_color(cx))
3131                        .bg(cx.theme().colors().editor_background)
3132                        .overflow_hidden()
3133                } else {
3134                    this.my_1()
3135                }
3136            })
3137            .map(|this| {
3138                if has_location && !use_card_layout {
3139                    this.ml_4()
3140                } else {
3141                    this.ml_5()
3142                }
3143            })
3144            .mr_5()
3145            .map(|this| {
3146                if is_terminal_tool {
3147                    let label_source = tool_call.label.read(cx).source();
3148                    this.child(self.render_collapsible_command(true, label_source, &tool_call.id, cx))
3149                } else {
3150                    this.child(
3151                        h_flex()
3152                            .group(&card_header_id)
3153                            .relative()
3154                            .w_full()
3155                            .gap_1()
3156                            .justify_between()
3157                            .when(use_card_layout, |this| {
3158                                this.p_0p5()
3159                                    .rounded_t(rems_from_px(5.))
3160                                    .bg(self.tool_card_header_bg(cx))
3161                            })
3162                            .child(self.render_tool_call_label(
3163                                entry_ix,
3164                                tool_call,
3165                                is_edit,
3166                                is_cancelled_edit,
3167                                has_revealed_diff,
3168                                use_card_layout,
3169                                window,
3170                                cx,
3171                            ))
3172                            .when(is_collapsible || failed_or_canceled, |this| {
3173                                let diff_for_discard =
3174                                    if has_revealed_diff && is_cancelled_edit && cx.has_flag::<AgentV2FeatureFlag>() {
3175                                        tool_call.diffs().next().cloned()
3176                                    } else {
3177                                        None
3178                                    };
3179                                this.child(
3180                                    h_flex()
3181                                        .px_1()
3182                                        .when_some(diff_for_discard.clone(), |this, _| this.pr_0p5())
3183                                        .gap_1()
3184                                        .when(is_collapsible, |this| {
3185                                            this.child(
3186                                            Disclosure::new(("expand-output", entry_ix), is_open)
3187                                                .opened_icon(IconName::ChevronUp)
3188                                                .closed_icon(IconName::ChevronDown)
3189                                                .visible_on_hover(&card_header_id)
3190                                                .on_click(cx.listener({
3191                                                    let id = tool_call.id.clone();
3192                                                    move |this: &mut Self, _, _, cx: &mut Context<Self>| {
3193                                                        if is_open {
3194                                                            this.expanded_tool_calls.remove(&id);
3195                                                        } else {
3196                                                            this.expanded_tool_calls.insert(id.clone());
3197                                                        }
3198                                                        cx.notify();
3199                                                    }
3200                                                })),
3201                                        )
3202                                        })
3203                                        .when(failed_or_canceled, |this| {
3204                                            if is_cancelled_edit && !has_revealed_diff {
3205                                                this.child(
3206                                                    div()
3207                                                        .id(entry_ix)
3208                                                        .tooltip(Tooltip::text(
3209                                                            "Interrupted Edit",
3210                                                        ))
3211                                                        .child(
3212                                                            Icon::new(IconName::XCircle)
3213                                                                .color(Color::Muted)
3214                                                                .size(IconSize::Small),
3215                                                        ),
3216                                                )
3217                                            } else if is_cancelled_edit {
3218                                                this
3219                                            } else {
3220                                                this.child(
3221                                                    Icon::new(IconName::Close)
3222                                                        .color(Color::Error)
3223                                                        .size(IconSize::Small),
3224                                                )
3225                                            }
3226                                        })
3227                                        .when_some(diff_for_discard, |this, diff| {
3228                                            let tool_call_id = tool_call.id.clone();
3229                                            let is_discarded = self.discarded_partial_edits.contains(&tool_call_id);
3230                                            this.when(!is_discarded, |this| {
3231                                                this.child(
3232                                                    IconButton::new(
3233                                                        ("discard-partial-edit", entry_ix),
3234                                                        IconName::Trash,
3235                                                    )
3236                                                    .icon_size(IconSize::Small)
3237                                                    .tooltip(move |_, cx| Tooltip::with_meta(
3238                                                        "Discard Interrupted Edit",
3239                                                        None,
3240                                                        "You can discard this interrupted partial edit and restore the original file content.",
3241                                                        cx
3242                                                    ))
3243                                                    .on_click(cx.listener({
3244                                                        let tool_call_id = tool_call_id.clone();
3245                                                        move |this, _, _window, cx| {
3246                                                            let diff_data = diff.read(cx);
3247                                                            let base_text = diff_data.base_text().clone();
3248                                                            let buffer = diff_data.buffer().clone();
3249                                                            buffer.update(cx, |buffer, cx| {
3250                                                                buffer.set_text(base_text.as_ref(), cx);
3251                                                            });
3252                                                            this.discarded_partial_edits.insert(tool_call_id.clone());
3253                                                            cx.notify();
3254                                                        }
3255                                                    })),
3256                                                )
3257                                            })
3258                                        })
3259
3260                                )
3261                            }),
3262                    )
3263                }
3264            })
3265            .children(tool_output_display)
3266    }
3267
3268    fn render_tool_call_label(
3269        &self,
3270        entry_ix: usize,
3271        tool_call: &ToolCall,
3272        is_edit: bool,
3273        has_failed: bool,
3274        has_revealed_diff: bool,
3275        use_card_layout: bool,
3276        window: &Window,
3277        cx: &Context<Self>,
3278    ) -> Div {
3279        let has_location = tool_call.locations.len() == 1;
3280        let is_file = tool_call.kind == acp::ToolKind::Edit && has_location;
3281
3282        let file_icon = if has_location {
3283            FileIcons::get_icon(&tool_call.locations[0].path, cx)
3284                .map(Icon::from_path)
3285                .unwrap_or(Icon::new(IconName::ToolPencil))
3286        } else {
3287            Icon::new(IconName::ToolPencil)
3288        };
3289
3290        let tool_icon = if is_file && has_failed && has_revealed_diff {
3291            div()
3292                .id(entry_ix)
3293                .tooltip(Tooltip::text("Interrupted Edit"))
3294                .child(DecoratedIcon::new(
3295                    file_icon,
3296                    Some(
3297                        IconDecoration::new(
3298                            IconDecorationKind::Triangle,
3299                            self.tool_card_header_bg(cx),
3300                            cx,
3301                        )
3302                        .color(cx.theme().status().warning)
3303                        .position(gpui::Point {
3304                            x: px(-2.),
3305                            y: px(-2.),
3306                        }),
3307                    ),
3308                ))
3309                .into_any_element()
3310        } else if is_file {
3311            div().child(file_icon).into_any_element()
3312        } else {
3313            div()
3314                .child(
3315                    Icon::new(match tool_call.kind {
3316                        acp::ToolKind::Read => IconName::ToolSearch,
3317                        acp::ToolKind::Edit => IconName::ToolPencil,
3318                        acp::ToolKind::Delete => IconName::ToolDeleteFile,
3319                        acp::ToolKind::Move => IconName::ArrowRightLeft,
3320                        acp::ToolKind::Search => IconName::ToolSearch,
3321                        acp::ToolKind::Execute => IconName::ToolTerminal,
3322                        acp::ToolKind::Think => IconName::ToolThink,
3323                        acp::ToolKind::Fetch => IconName::ToolWeb,
3324                        acp::ToolKind::SwitchMode => IconName::ArrowRightLeft,
3325                        acp::ToolKind::Other | _ => IconName::ToolHammer,
3326                    })
3327                    .size(IconSize::Small)
3328                    .color(Color::Muted),
3329                )
3330                .into_any_element()
3331        };
3332
3333        let gradient_overlay = {
3334            div()
3335                .absolute()
3336                .top_0()
3337                .right_0()
3338                .w_12()
3339                .h_full()
3340                .map(|this| {
3341                    if use_card_layout {
3342                        this.bg(linear_gradient(
3343                            90.,
3344                            linear_color_stop(self.tool_card_header_bg(cx), 1.),
3345                            linear_color_stop(self.tool_card_header_bg(cx).opacity(0.2), 0.),
3346                        ))
3347                    } else {
3348                        this.bg(linear_gradient(
3349                            90.,
3350                            linear_color_stop(cx.theme().colors().panel_background, 1.),
3351                            linear_color_stop(
3352                                cx.theme().colors().panel_background.opacity(0.2),
3353                                0.,
3354                            ),
3355                        ))
3356                    }
3357                })
3358        };
3359
3360        h_flex()
3361            .relative()
3362            .w_full()
3363            .h(window.line_height() - px(2.))
3364            .text_size(self.tool_name_font_size())
3365            .gap_1p5()
3366            .when(has_location || use_card_layout, |this| this.px_1())
3367            .when(has_location, |this| {
3368                this.cursor(CursorStyle::PointingHand)
3369                    .rounded(rems_from_px(3.)) // Concentric border radius
3370                    .hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.5)))
3371            })
3372            .overflow_hidden()
3373            .child(tool_icon)
3374            .child(if has_location {
3375                h_flex()
3376                    .id(("open-tool-call-location", entry_ix))
3377                    .w_full()
3378                    .map(|this| {
3379                        if use_card_layout {
3380                            this.text_color(cx.theme().colors().text)
3381                        } else {
3382                            this.text_color(cx.theme().colors().text_muted)
3383                        }
3384                    })
3385                    .child(self.render_markdown(
3386                        tool_call.label.clone(),
3387                        MarkdownStyle {
3388                            prevent_mouse_interaction: true,
3389                            ..default_markdown_style(false, true, window, cx)
3390                        },
3391                    ))
3392                    .tooltip(Tooltip::text("Go to File"))
3393                    .on_click(cx.listener(move |this, _, window, cx| {
3394                        this.open_tool_call_location(entry_ix, 0, window, cx);
3395                    }))
3396                    .into_any_element()
3397            } else {
3398                h_flex()
3399                    .w_full()
3400                    .child(self.render_markdown(
3401                        tool_call.label.clone(),
3402                        default_markdown_style(false, true, window, cx),
3403                    ))
3404                    .into_any()
3405            })
3406            .when(!is_edit, |this| this.child(gradient_overlay))
3407    }
3408
3409    fn render_tool_call_content(
3410        &self,
3411        entry_ix: usize,
3412        content: &ToolCallContent,
3413        context_ix: usize,
3414        tool_call: &ToolCall,
3415        card_layout: bool,
3416        is_image_tool_call: bool,
3417        has_failed: bool,
3418        window: &Window,
3419        cx: &Context<Self>,
3420    ) -> AnyElement {
3421        match content {
3422            ToolCallContent::ContentBlock(content) => {
3423                if let Some(resource_link) = content.resource_link() {
3424                    self.render_resource_link(resource_link, cx)
3425                } else if let Some(markdown) = content.markdown() {
3426                    self.render_markdown_output(
3427                        markdown.clone(),
3428                        tool_call.id.clone(),
3429                        context_ix,
3430                        card_layout,
3431                        window,
3432                        cx,
3433                    )
3434                } else if let Some(image) = content.image() {
3435                    let location = tool_call.locations.first().cloned();
3436                    self.render_image_output(
3437                        entry_ix,
3438                        image.clone(),
3439                        location,
3440                        card_layout,
3441                        is_image_tool_call,
3442                        cx,
3443                    )
3444                } else {
3445                    Empty.into_any_element()
3446                }
3447            }
3448            ToolCallContent::Diff(diff) => {
3449                self.render_diff_editor(entry_ix, diff, tool_call, has_failed, cx)
3450            }
3451            ToolCallContent::Terminal(terminal) => {
3452                self.render_terminal_tool_call(entry_ix, terminal, tool_call, window, cx)
3453            }
3454            ToolCallContent::SubagentThread(_thread) => {
3455                // The subagent's AcpThread entity stores the subagent's conversation
3456                // (messages, tool calls, etc.) but we don't render it here. The entity
3457                // is used for serialization (e.g., to_markdown) and data storage, not display.
3458                Empty.into_any_element()
3459            }
3460        }
3461    }
3462
3463    fn render_markdown_output(
3464        &self,
3465        markdown: Entity<Markdown>,
3466        tool_call_id: acp::ToolCallId,
3467        context_ix: usize,
3468        card_layout: bool,
3469        window: &Window,
3470        cx: &Context<Self>,
3471    ) -> AnyElement {
3472        let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id));
3473
3474        v_flex()
3475            .gap_2()
3476            .map(|this| {
3477                if card_layout {
3478                    this.when(context_ix > 0, |this| {
3479                        this.pt_2()
3480                            .border_t_1()
3481                            .border_color(self.tool_card_border_color(cx))
3482                    })
3483                } else {
3484                    this.ml(rems(0.4))
3485                        .px_3p5()
3486                        .border_l_1()
3487                        .border_color(self.tool_card_border_color(cx))
3488                }
3489            })
3490            .text_xs()
3491            .text_color(cx.theme().colors().text_muted)
3492            .child(self.render_markdown(markdown, default_markdown_style(false, false, window, cx)))
3493            .when(!card_layout, |this| {
3494                this.child(
3495                    IconButton::new(button_id, IconName::ChevronUp)
3496                        .full_width()
3497                        .style(ButtonStyle::Outlined)
3498                        .icon_color(Color::Muted)
3499                        .on_click(cx.listener({
3500                            move |this: &mut Self, _, _, cx: &mut Context<Self>| {
3501                                this.expanded_tool_calls.remove(&tool_call_id);
3502                                cx.notify();
3503                            }
3504                        })),
3505                )
3506            })
3507            .into_any_element()
3508    }
3509
3510    fn render_image_output(
3511        &self,
3512        entry_ix: usize,
3513        image: Arc<gpui::Image>,
3514        location: Option<acp::ToolCallLocation>,
3515        card_layout: bool,
3516        show_dimensions: bool,
3517        cx: &Context<Self>,
3518    ) -> AnyElement {
3519        let dimensions_label = if show_dimensions {
3520            let format_name = match image.format() {
3521                gpui::ImageFormat::Png => "PNG",
3522                gpui::ImageFormat::Jpeg => "JPEG",
3523                gpui::ImageFormat::Webp => "WebP",
3524                gpui::ImageFormat::Gif => "GIF",
3525                gpui::ImageFormat::Svg => "SVG",
3526                gpui::ImageFormat::Bmp => "BMP",
3527                gpui::ImageFormat::Tiff => "TIFF",
3528                gpui::ImageFormat::Ico => "ICO",
3529            };
3530            let dimensions = image::ImageReader::new(std::io::Cursor::new(image.bytes()))
3531                .with_guessed_format()
3532                .ok()
3533                .and_then(|reader| reader.into_dimensions().ok());
3534            dimensions.map(|(w, h)| format!("{}×{} {}", w, h, format_name))
3535        } else {
3536            None
3537        };
3538
3539        v_flex()
3540            .gap_2()
3541            .map(|this| {
3542                if card_layout {
3543                    this
3544                } else {
3545                    this.ml(rems(0.4))
3546                        .px_3p5()
3547                        .border_l_1()
3548                        .border_color(self.tool_card_border_color(cx))
3549                }
3550            })
3551            .when(dimensions_label.is_some() || location.is_some(), |this| {
3552                this.child(
3553                    h_flex()
3554                        .w_full()
3555                        .justify_between()
3556                        .items_center()
3557                        .children(dimensions_label.map(|label| {
3558                            Label::new(label)
3559                                .size(LabelSize::XSmall)
3560                                .color(Color::Muted)
3561                                .buffer_font(cx)
3562                        }))
3563                        .when_some(location, |this, _loc| {
3564                            this.child(
3565                                Button::new(("go-to-file", entry_ix), "Go to File")
3566                                    .label_size(LabelSize::Small)
3567                                    .on_click(cx.listener(move |this, _, window, cx| {
3568                                        this.open_tool_call_location(entry_ix, 0, window, cx);
3569                                    })),
3570                            )
3571                        }),
3572                )
3573            })
3574            .child(
3575                img(image)
3576                    .max_w_96()
3577                    .max_h_96()
3578                    .object_fit(ObjectFit::ScaleDown),
3579            )
3580            .into_any_element()
3581    }
3582
3583    fn render_resource_link(
3584        &self,
3585        resource_link: &acp::ResourceLink,
3586        cx: &Context<Self>,
3587    ) -> AnyElement {
3588        let uri: SharedString = resource_link.uri.clone().into();
3589        let is_file = resource_link.uri.strip_prefix("file://");
3590
3591        let label: SharedString = if let Some(abs_path) = is_file {
3592            if let Some(project_path) = self
3593                .project
3594                .read(cx)
3595                .project_path_for_absolute_path(&Path::new(abs_path), cx)
3596                && let Some(worktree) = self
3597                    .project
3598                    .read(cx)
3599                    .worktree_for_id(project_path.worktree_id, cx)
3600            {
3601                worktree
3602                    .read(cx)
3603                    .full_path(&project_path.path)
3604                    .to_string_lossy()
3605                    .to_string()
3606                    .into()
3607            } else {
3608                abs_path.to_string().into()
3609            }
3610        } else {
3611            uri.clone()
3612        };
3613
3614        let button_id = SharedString::from(format!("item-{}", uri));
3615
3616        div()
3617            .ml(rems(0.4))
3618            .pl_2p5()
3619            .border_l_1()
3620            .border_color(self.tool_card_border_color(cx))
3621            .overflow_hidden()
3622            .child(
3623                Button::new(button_id, label)
3624                    .label_size(LabelSize::Small)
3625                    .color(Color::Muted)
3626                    .truncate(true)
3627                    .when(is_file.is_none(), |this| {
3628                        this.icon(IconName::ArrowUpRight)
3629                            .icon_size(IconSize::XSmall)
3630                            .icon_color(Color::Muted)
3631                    })
3632                    .on_click(cx.listener({
3633                        let workspace = self.workspace.clone();
3634                        move |_, _, window, cx: &mut Context<Self>| {
3635                            Self::open_link(uri.clone(), &workspace, window, cx);
3636                        }
3637                    })),
3638            )
3639            .into_any_element()
3640    }
3641
3642    fn render_permission_buttons(
3643        &self,
3644        kind: acp::ToolKind,
3645        options: &[acp::PermissionOption],
3646        entry_ix: usize,
3647        tool_call_id: acp::ToolCallId,
3648        cx: &Context<Self>,
3649    ) -> Div {
3650        let is_first = self.thread().is_some_and(|thread| {
3651            thread
3652                .read(cx)
3653                .first_tool_awaiting_confirmation()
3654                .is_some_and(|call| call.id == tool_call_id)
3655        });
3656        let mut seen_kinds: ArrayVec<acp::PermissionOptionKind, 3> = ArrayVec::new();
3657
3658        div()
3659            .p_1()
3660            .border_t_1()
3661            .border_color(self.tool_card_border_color(cx))
3662            .w_full()
3663            .map(|this| {
3664                if kind == acp::ToolKind::SwitchMode {
3665                    this.v_flex()
3666                } else {
3667                    this.h_flex().justify_end().flex_wrap()
3668                }
3669            })
3670            .gap_0p5()
3671            .children(options.iter().map(move |option| {
3672                let option_id = SharedString::from(option.option_id.0.clone());
3673                Button::new((option_id, entry_ix), option.name.clone())
3674                    .map(|this| {
3675                        let (this, action) = match option.kind {
3676                            acp::PermissionOptionKind::AllowOnce => (
3677                                this.icon(IconName::Check).icon_color(Color::Success),
3678                                Some(&AllowOnce as &dyn Action),
3679                            ),
3680                            acp::PermissionOptionKind::AllowAlways => (
3681                                this.icon(IconName::CheckDouble).icon_color(Color::Success),
3682                                Some(&AllowAlways as &dyn Action),
3683                            ),
3684                            acp::PermissionOptionKind::RejectOnce => (
3685                                this.icon(IconName::Close).icon_color(Color::Error),
3686                                Some(&RejectOnce as &dyn Action),
3687                            ),
3688                            acp::PermissionOptionKind::RejectAlways | _ => {
3689                                (this.icon(IconName::Close).icon_color(Color::Error), None)
3690                            }
3691                        };
3692
3693                        let Some(action) = action else {
3694                            return this;
3695                        };
3696
3697                        if !is_first || seen_kinds.contains(&option.kind) {
3698                            return this;
3699                        }
3700
3701                        seen_kinds.push(option.kind);
3702
3703                        this.key_binding(
3704                            KeyBinding::for_action_in(action, &self.focus_handle, cx)
3705                                .map(|kb| kb.size(rems_from_px(10.))),
3706                        )
3707                    })
3708                    .icon_position(IconPosition::Start)
3709                    .icon_size(IconSize::XSmall)
3710                    .label_size(LabelSize::Small)
3711                    .on_click(cx.listener({
3712                        let tool_call_id = tool_call_id.clone();
3713                        let option_id = option.option_id.clone();
3714                        let option_kind = option.kind;
3715                        move |this, _, window, cx| {
3716                            this.authorize_tool_call(
3717                                tool_call_id.clone(),
3718                                option_id.clone(),
3719                                option_kind,
3720                                window,
3721                                cx,
3722                            );
3723                        }
3724                    }))
3725            }))
3726    }
3727
3728    fn render_diff_loading(&self, cx: &Context<Self>) -> AnyElement {
3729        let bar = |n: u64, width_class: &str| {
3730            let bg_color = cx.theme().colors().element_active;
3731            let base = h_flex().h_1().rounded_full();
3732
3733            let modified = match width_class {
3734                "w_4_5" => base.w_3_4(),
3735                "w_1_4" => base.w_1_4(),
3736                "w_2_4" => base.w_2_4(),
3737                "w_3_5" => base.w_3_5(),
3738                "w_2_5" => base.w_2_5(),
3739                _ => base.w_1_2(),
3740            };
3741
3742            modified.with_animation(
3743                ElementId::Integer(n),
3744                Animation::new(Duration::from_secs(2)).repeat(),
3745                move |tab, delta| {
3746                    let delta = (delta - 0.15 * n as f32) / 0.7;
3747                    let delta = 1.0 - (0.5 - delta).abs() * 2.;
3748                    let delta = ease_in_out(delta.clamp(0., 1.));
3749                    let delta = 0.1 + 0.9 * delta;
3750
3751                    tab.bg(bg_color.opacity(delta))
3752                },
3753            )
3754        };
3755
3756        v_flex()
3757            .p_3()
3758            .gap_1()
3759            .rounded_b_md()
3760            .bg(cx.theme().colors().editor_background)
3761            .child(bar(0, "w_4_5"))
3762            .child(bar(1, "w_1_4"))
3763            .child(bar(2, "w_2_4"))
3764            .child(bar(3, "w_3_5"))
3765            .child(bar(4, "w_2_5"))
3766            .into_any_element()
3767    }
3768
3769    fn render_diff_editor(
3770        &self,
3771        entry_ix: usize,
3772        diff: &Entity<acp_thread::Diff>,
3773        tool_call: &ToolCall,
3774        has_failed: bool,
3775        cx: &Context<Self>,
3776    ) -> AnyElement {
3777        let tool_progress = matches!(
3778            &tool_call.status,
3779            ToolCallStatus::InProgress | ToolCallStatus::Pending
3780        );
3781
3782        let revealed_diff_editor = if let Some(entry) =
3783            self.entry_view_state.read(cx).entry(entry_ix)
3784            && let Some(editor) = entry.editor_for_diff(diff)
3785            && diff.read(cx).has_revealed_range(cx)
3786        {
3787            Some(editor)
3788        } else {
3789            None
3790        };
3791
3792        let show_top_border = !has_failed || revealed_diff_editor.is_some();
3793
3794        v_flex()
3795            .h_full()
3796            .when(show_top_border, |this| {
3797                this.border_t_1()
3798                    .when(has_failed, |this| this.border_dashed())
3799                    .border_color(self.tool_card_border_color(cx))
3800            })
3801            .child(if let Some(editor) = revealed_diff_editor {
3802                editor.into_any_element()
3803            } else if tool_progress && self.as_native_connection(cx).is_some() {
3804                self.render_diff_loading(cx)
3805            } else {
3806                Empty.into_any()
3807            })
3808            .into_any()
3809    }
3810
3811    /// Renders command lines with an optional expand/collapse button depending
3812    /// on the number of lines in `command_source`.
3813    fn render_collapsible_command(
3814        &self,
3815        is_preview: bool,
3816        command_source: &str,
3817        tool_call_id: &acp::ToolCallId,
3818        cx: &Context<Self>,
3819    ) -> Div {
3820        let expand_button_bg = self.tool_card_header_bg(cx);
3821        let expanded = self.expanded_terminal_commands.contains(tool_call_id);
3822
3823        let lines: Vec<&str> = command_source.lines().collect();
3824        let line_count = lines.len();
3825        let extra_lines = line_count.saturating_sub(MAX_COLLAPSED_LINES);
3826
3827        let show_expand_button = extra_lines > 0;
3828
3829        let max_lines = if expanded || !show_expand_button {
3830            usize::MAX
3831        } else {
3832            MAX_COLLAPSED_LINES
3833        };
3834
3835        let display_lines = lines.into_iter().take(max_lines);
3836
3837        let command_group =
3838            SharedString::from(format!("collapsible-command-group-{}", tool_call_id));
3839
3840        v_flex()
3841            .group(command_group.clone())
3842            .bg(self.tool_card_header_bg(cx))
3843            .child(
3844                v_flex()
3845                    .p_1p5()
3846                    .when(is_preview, |this| {
3847                        this.pt_1().child(
3848                            // Wrapping this label on a container with 24px height to avoid
3849                            // layout shift when it changes from being a preview label
3850                            // to the actual path where the command will run in
3851                            h_flex().h_6().child(
3852                                Label::new("Run Command")
3853                                    .buffer_font(cx)
3854                                    .size(LabelSize::XSmall)
3855                                    .color(Color::Muted),
3856                            ),
3857                        )
3858                    })
3859                    .children(display_lines.map(|line| {
3860                        let text: SharedString = if line.is_empty() {
3861                            " ".into()
3862                        } else {
3863                            line.to_string().into()
3864                        };
3865
3866                        Label::new(text).buffer_font(cx).size(LabelSize::Small)
3867                    }))
3868                    .child(
3869                        div().absolute().top_1().right_1().child(
3870                            CopyButton::new(command_source.to_string())
3871                                .tooltip_label("Copy Command")
3872                                .visible_on_hover(command_group),
3873                        ),
3874                    ),
3875            )
3876            .when(show_expand_button, |this| {
3877                let expand_icon = if expanded {
3878                    IconName::ChevronUp
3879                } else {
3880                    IconName::ChevronDown
3881                };
3882
3883                this.child(
3884                    h_flex()
3885                        .id(format!("expand-command-btn-{}", tool_call_id))
3886                        .cursor_pointer()
3887                        .when(!expanded, |s| s.absolute().bottom_0())
3888                        .when(expanded, |s| s.mt_1())
3889                        .w_full()
3890                        .h_6()
3891                        .gap_1()
3892                        .justify_center()
3893                        .border_t_1()
3894                        .border_color(self.tool_card_border_color(cx))
3895                        .bg(expand_button_bg.opacity(0.95))
3896                        .hover(|s| s.bg(cx.theme().colors().element_hover))
3897                        .when(!expanded, |this| {
3898                            let label = match extra_lines {
3899                                1 => "1 more line".to_string(),
3900                                _ => format!("{} more lines", extra_lines),
3901                            };
3902
3903                            this.child(Label::new(label).size(LabelSize::Small).color(Color::Muted))
3904                        })
3905                        .child(
3906                            Icon::new(expand_icon)
3907                                .size(IconSize::Small)
3908                                .color(Color::Muted),
3909                        )
3910                        .on_click(cx.listener({
3911                            let tool_call_id = tool_call_id.clone();
3912                            move |this, _event, _window, cx| {
3913                                if expanded {
3914                                    this.expanded_terminal_commands.remove(&tool_call_id);
3915                                } else {
3916                                    this.expanded_terminal_commands.insert(tool_call_id.clone());
3917                                }
3918                                cx.notify();
3919                            }
3920                        })),
3921                )
3922            })
3923    }
3924
3925    fn render_terminal_tool_call(
3926        &self,
3927        entry_ix: usize,
3928        terminal: &Entity<acp_thread::Terminal>,
3929        tool_call: &ToolCall,
3930        window: &Window,
3931        cx: &Context<Self>,
3932    ) -> AnyElement {
3933        let terminal_data = terminal.read(cx);
3934        let working_dir = terminal_data.working_dir();
3935        let command = terminal_data.command();
3936        let started_at = terminal_data.started_at();
3937
3938        let tool_failed = matches!(
3939            &tool_call.status,
3940            ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed
3941        );
3942
3943        let output = terminal_data.output();
3944        let command_finished = output.is_some();
3945        let truncated_output =
3946            output.is_some_and(|output| output.original_content_len > output.content.len());
3947        let output_line_count = output.map(|output| output.content_line_count).unwrap_or(0);
3948
3949        let command_failed = command_finished
3950            && output.is_some_and(|o| o.exit_status.is_some_and(|status| !status.success()));
3951
3952        let time_elapsed = if let Some(output) = output {
3953            output.ended_at.duration_since(started_at)
3954        } else {
3955            started_at.elapsed()
3956        };
3957
3958        let header_id =
3959            SharedString::from(format!("terminal-tool-header-{}", terminal.entity_id()));
3960        let header_group = SharedString::from(format!(
3961            "terminal-tool-header-group-{}",
3962            terminal.entity_id()
3963        ));
3964        let header_bg = cx
3965            .theme()
3966            .colors()
3967            .element_background
3968            .blend(cx.theme().colors().editor_foreground.opacity(0.025));
3969        let border_color = cx.theme().colors().border.opacity(0.6);
3970
3971        let working_dir = working_dir
3972            .as_ref()
3973            .map(|path| path.display().to_string())
3974            .unwrap_or_else(|| "current directory".to_string());
3975
3976        // Since the command's source is wrapped in a markdown code block
3977        // (```\n...\n```), we need to strip that so we're left with only the
3978        // command's content.
3979        let command_source = command.read(cx).source();
3980        let command_content = command_source
3981            .strip_prefix("```\n")
3982            .and_then(|s| s.strip_suffix("\n```"))
3983            .unwrap_or(&command_source);
3984
3985        let command_element =
3986            self.render_collapsible_command(false, command_content, &tool_call.id, cx);
3987
3988        let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
3989
3990        let header = h_flex()
3991            .id(header_id)
3992            .px_1p5()
3993            .pt_1()
3994            .flex_none()
3995            .gap_1()
3996            .justify_between()
3997            .rounded_t_md()
3998            .child(
3999                div()
4000                    .id(("command-target-path", terminal.entity_id()))
4001                    .w_full()
4002                    .max_w_full()
4003                    .overflow_x_scroll()
4004                    .child(
4005                        Label::new(working_dir)
4006                            .buffer_font(cx)
4007                            .size(LabelSize::XSmall)
4008                            .color(Color::Muted),
4009                    ),
4010            )
4011            .when(!command_finished, |header| {
4012                header
4013                    .gap_1p5()
4014                    .child(
4015                        Button::new(
4016                            SharedString::from(format!("stop-terminal-{}", terminal.entity_id())),
4017                            "Stop",
4018                        )
4019                        .icon(IconName::Stop)
4020                        .icon_position(IconPosition::Start)
4021                        .icon_size(IconSize::Small)
4022                        .icon_color(Color::Error)
4023                        .label_size(LabelSize::Small)
4024                        .tooltip(move |_window, cx| {
4025                            Tooltip::with_meta(
4026                                "Stop This Command",
4027                                None,
4028                                "Also possible by placing your cursor inside the terminal and using regular terminal bindings.",
4029                                cx,
4030                            )
4031                        })
4032                        .on_click({
4033                            let terminal = terminal.clone();
4034                            cx.listener(move |this, _event, _window, cx| {
4035                                terminal.update(cx, |terminal, cx| {
4036                                    terminal.stop_by_user(cx);
4037                                });
4038                                this.cancel_generation(cx);
4039                            })
4040                        }),
4041                    )
4042                    .child(Divider::vertical())
4043                    .child(
4044                        Icon::new(IconName::ArrowCircle)
4045                            .size(IconSize::XSmall)
4046                            .color(Color::Info)
4047                            .with_rotate_animation(2)
4048                    )
4049            })
4050            .when(truncated_output, |header| {
4051                let tooltip = if let Some(output) = output {
4052                    if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
4053                       format!("Output exceeded terminal max lines and was \
4054                            truncated, the model received the first {}.", format_file_size(output.content.len() as u64, true))
4055                    } else {
4056                        format!(
4057                            "Output is {} long, and to avoid unexpected token usage, \
4058                                only {} was sent back to the agent.",
4059                            format_file_size(output.original_content_len as u64, true),
4060                             format_file_size(output.content.len() as u64, true)
4061                        )
4062                    }
4063                } else {
4064                    "Output was truncated".to_string()
4065                };
4066
4067                header.child(
4068                    h_flex()
4069                        .id(("terminal-tool-truncated-label", terminal.entity_id()))
4070                        .gap_1()
4071                        .child(
4072                            Icon::new(IconName::Info)
4073                                .size(IconSize::XSmall)
4074                                .color(Color::Ignored),
4075                        )
4076                        .child(
4077                            Label::new("Truncated")
4078                                .color(Color::Muted)
4079                                .size(LabelSize::XSmall),
4080                        )
4081                        .tooltip(Tooltip::text(tooltip)),
4082                )
4083            })
4084            .when(time_elapsed > Duration::from_secs(10), |header| {
4085                header.child(
4086                    Label::new(format!("({})", duration_alt_display(time_elapsed)))
4087                        .buffer_font(cx)
4088                        .color(Color::Muted)
4089                        .size(LabelSize::XSmall),
4090                )
4091            })
4092            .when(tool_failed || command_failed, |header| {
4093                header.child(
4094                    div()
4095                        .id(("terminal-tool-error-code-indicator", terminal.entity_id()))
4096                        .child(
4097                            Icon::new(IconName::Close)
4098                                .size(IconSize::Small)
4099                                .color(Color::Error),
4100                        )
4101                        .when_some(output.and_then(|o| o.exit_status), |this, status| {
4102                            this.tooltip(Tooltip::text(format!(
4103                                "Exited with code {}",
4104                                status.code().unwrap_or(-1),
4105                            )))
4106                        }),
4107                )
4108            })
4109            .child(
4110                Disclosure::new(
4111                    SharedString::from(format!(
4112                        "terminal-tool-disclosure-{}",
4113                        terminal.entity_id()
4114                    )),
4115                    is_expanded,
4116                )
4117                .opened_icon(IconName::ChevronUp)
4118                .closed_icon(IconName::ChevronDown)
4119                .visible_on_hover(&header_group)
4120                .on_click(cx.listener({
4121                    let id = tool_call.id.clone();
4122                    move |this, _event, _window, _cx| {
4123                        if is_expanded {
4124                            this.expanded_tool_calls.remove(&id);
4125                        } else {
4126                            this.expanded_tool_calls.insert(id.clone());
4127                        }
4128                    }
4129                })),
4130            );
4131
4132        let terminal_view = self
4133            .entry_view_state
4134            .read(cx)
4135            .entry(entry_ix)
4136            .and_then(|entry| entry.terminal(terminal));
4137
4138        v_flex()
4139            .my_1p5()
4140            .mx_5()
4141            .border_1()
4142            .when(tool_failed || command_failed, |card| card.border_dashed())
4143            .border_color(border_color)
4144            .rounded_md()
4145            .overflow_hidden()
4146            .child(
4147                v_flex()
4148                    .group(&header_group)
4149                    .bg(header_bg)
4150                    .text_xs()
4151                    .child(header)
4152                    .child(command_element),
4153            )
4154            .when(is_expanded && terminal_view.is_some(), |this| {
4155                this.child(
4156                    div()
4157                        .pt_2()
4158                        .border_t_1()
4159                        .when(tool_failed || command_failed, |card| card.border_dashed())
4160                        .border_color(border_color)
4161                        .bg(cx.theme().colors().editor_background)
4162                        .rounded_b_md()
4163                        .text_ui_sm(cx)
4164                        .h_full()
4165                        .children(terminal_view.map(|terminal_view| {
4166                            let element = if terminal_view
4167                                .read(cx)
4168                                .content_mode(window, cx)
4169                                .is_scrollable()
4170                            {
4171                                div().h_72().child(terminal_view).into_any_element()
4172                            } else {
4173                                terminal_view.into_any_element()
4174                            };
4175
4176                            div()
4177                                .on_action(cx.listener(|_this, _: &NewTerminal, window, cx| {
4178                                    window.dispatch_action(NewThread.boxed_clone(), cx);
4179                                    cx.stop_propagation();
4180                                }))
4181                                .child(element)
4182                                .into_any_element()
4183                        })),
4184                )
4185            })
4186            .into_any()
4187    }
4188
4189    fn render_rules_item(&self, cx: &Context<Self>) -> Option<AnyElement> {
4190        let project_context = self
4191            .as_native_thread(cx)?
4192            .read(cx)
4193            .project_context()
4194            .read(cx);
4195
4196        let user_rules_text = if project_context.user_rules.is_empty() {
4197            None
4198        } else if project_context.user_rules.len() == 1 {
4199            let user_rules = &project_context.user_rules[0];
4200
4201            match user_rules.title.as_ref() {
4202                Some(title) => Some(format!("Using \"{title}\" user rule")),
4203                None => Some("Using user rule".into()),
4204            }
4205        } else {
4206            Some(format!(
4207                "Using {} user rules",
4208                project_context.user_rules.len()
4209            ))
4210        };
4211
4212        let first_user_rules_id = project_context
4213            .user_rules
4214            .first()
4215            .map(|user_rules| user_rules.uuid.0);
4216
4217        let rules_files = project_context
4218            .worktrees
4219            .iter()
4220            .filter_map(|worktree| worktree.rules_file.as_ref())
4221            .collect::<Vec<_>>();
4222
4223        let rules_file_text = match rules_files.as_slice() {
4224            &[] => None,
4225            &[rules_file] => Some(format!(
4226                "Using project {:?} file",
4227                rules_file.path_in_worktree
4228            )),
4229            rules_files => Some(format!("Using {} project rules files", rules_files.len())),
4230        };
4231
4232        if user_rules_text.is_none() && rules_file_text.is_none() {
4233            return None;
4234        }
4235
4236        let has_both = user_rules_text.is_some() && rules_file_text.is_some();
4237
4238        Some(
4239            h_flex()
4240                .px_2p5()
4241                .child(
4242                    Icon::new(IconName::Attach)
4243                        .size(IconSize::XSmall)
4244                        .color(Color::Disabled),
4245                )
4246                .when_some(user_rules_text, |parent, user_rules_text| {
4247                    parent.child(
4248                        h_flex()
4249                            .id("user-rules")
4250                            .ml_1()
4251                            .mr_1p5()
4252                            .child(
4253                                Label::new(user_rules_text)
4254                                    .size(LabelSize::XSmall)
4255                                    .color(Color::Muted)
4256                                    .truncate(),
4257                            )
4258                            .hover(|s| s.bg(cx.theme().colors().element_hover))
4259                            .tooltip(Tooltip::text("View User Rules"))
4260                            .on_click(move |_event, window, cx| {
4261                                window.dispatch_action(
4262                                    Box::new(OpenRulesLibrary {
4263                                        prompt_to_select: first_user_rules_id,
4264                                    }),
4265                                    cx,
4266                                )
4267                            }),
4268                    )
4269                })
4270                .when(has_both, |this| {
4271                    this.child(
4272                        Label::new("")
4273                            .size(LabelSize::XSmall)
4274                            .color(Color::Disabled),
4275                    )
4276                })
4277                .when_some(rules_file_text, |parent, rules_file_text| {
4278                    parent.child(
4279                        h_flex()
4280                            .id("project-rules")
4281                            .ml_1p5()
4282                            .child(
4283                                Label::new(rules_file_text)
4284                                    .size(LabelSize::XSmall)
4285                                    .color(Color::Muted),
4286                            )
4287                            .hover(|s| s.bg(cx.theme().colors().element_hover))
4288                            .tooltip(Tooltip::text("View Project Rules"))
4289                            .on_click(cx.listener(Self::handle_open_rules)),
4290                    )
4291                })
4292                .into_any(),
4293        )
4294    }
4295
4296    fn render_empty_state_section_header(
4297        &self,
4298        label: impl Into<SharedString>,
4299        action_slot: Option<AnyElement>,
4300        cx: &mut Context<Self>,
4301    ) -> impl IntoElement {
4302        div().pl_1().pr_1p5().child(
4303            h_flex()
4304                .mt_2()
4305                .pl_1p5()
4306                .pb_1()
4307                .w_full()
4308                .justify_between()
4309                .border_b_1()
4310                .border_color(cx.theme().colors().border_variant)
4311                .child(
4312                    Label::new(label.into())
4313                        .size(LabelSize::Small)
4314                        .color(Color::Muted),
4315                )
4316                .children(action_slot),
4317        )
4318    }
4319
4320    fn update_recent_history_from_cache(
4321        &mut self,
4322        history: &Entity<AcpThreadHistory>,
4323        cx: &mut Context<Self>,
4324    ) {
4325        self.recent_history_entries = history.read(cx).get_recent_sessions(3);
4326        self.hovered_recent_history_item = None;
4327        cx.notify();
4328    }
4329
4330    fn render_recent_history(&self, cx: &mut Context<Self>) -> AnyElement {
4331        let render_history = !self.recent_history_entries.is_empty();
4332
4333        v_flex()
4334            .size_full()
4335            .when(render_history, |this| {
4336                let recent_history = self.recent_history_entries.clone();
4337                this.justify_end().child(
4338                    v_flex()
4339                        .child(
4340                            self.render_empty_state_section_header(
4341                                "Recent",
4342                                Some(
4343                                    Button::new("view-history", "View All")
4344                                        .style(ButtonStyle::Subtle)
4345                                        .label_size(LabelSize::Small)
4346                                        .key_binding(
4347                                            KeyBinding::for_action_in(
4348                                                &OpenHistory,
4349                                                &self.focus_handle(cx),
4350                                                cx,
4351                                            )
4352                                            .map(|kb| kb.size(rems_from_px(12.))),
4353                                        )
4354                                        .on_click(move |_event, window, cx| {
4355                                            window.dispatch_action(OpenHistory.boxed_clone(), cx);
4356                                        })
4357                                        .into_any_element(),
4358                                ),
4359                                cx,
4360                            ),
4361                        )
4362                        .child(v_flex().p_1().pr_1p5().gap_1().children({
4363                            let supports_delete = self.history.read(cx).supports_delete();
4364                            recent_history
4365                                .into_iter()
4366                                .enumerate()
4367                                .map(move |(index, entry)| {
4368                                    // TODO: Add keyboard navigation.
4369                                    let is_hovered =
4370                                        self.hovered_recent_history_item == Some(index);
4371                                    crate::acp::thread_history::AcpHistoryEntryElement::new(
4372                                        entry,
4373                                        cx.entity().downgrade(),
4374                                    )
4375                                    .hovered(is_hovered)
4376                                    .supports_delete(supports_delete)
4377                                    .on_hover(cx.listener(move |this, is_hovered, _window, cx| {
4378                                        if *is_hovered {
4379                                            this.hovered_recent_history_item = Some(index);
4380                                        } else if this.hovered_recent_history_item == Some(index) {
4381                                            this.hovered_recent_history_item = None;
4382                                        }
4383                                        cx.notify();
4384                                    }))
4385                                    .into_any_element()
4386                                })
4387                        })),
4388                )
4389            })
4390            .into_any()
4391    }
4392
4393    fn render_auth_required_state(
4394        &self,
4395        connection: &Rc<dyn AgentConnection>,
4396        description: Option<&Entity<Markdown>>,
4397        configuration_view: Option<&AnyView>,
4398        pending_auth_method: Option<&acp::AuthMethodId>,
4399        window: &mut Window,
4400        cx: &Context<Self>,
4401    ) -> impl IntoElement {
4402        let auth_methods = connection.auth_methods();
4403
4404        let agent_display_name = self
4405            .agent_server_store
4406            .read(cx)
4407            .agent_display_name(&ExternalAgentServerName(self.agent.name()))
4408            .unwrap_or_else(|| self.agent.name());
4409
4410        let show_fallback_description = auth_methods.len() > 1
4411            && configuration_view.is_none()
4412            && description.is_none()
4413            && pending_auth_method.is_none();
4414
4415        let auth_buttons = || {
4416            h_flex().justify_end().flex_wrap().gap_1().children(
4417                connection
4418                    .auth_methods()
4419                    .iter()
4420                    .enumerate()
4421                    .rev()
4422                    .map(|(ix, method)| {
4423                        let (method_id, name) = if self.project.read(cx).is_via_remote_server()
4424                            && method.id.0.as_ref() == "oauth-personal"
4425                            && method.name == "Log in with Google"
4426                        {
4427                            ("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
4428                        } else {
4429                            (method.id.0.clone(), method.name.clone())
4430                        };
4431
4432                        let agent_telemetry_id = connection.telemetry_id();
4433
4434                        Button::new(method_id.clone(), name)
4435                            .label_size(LabelSize::Small)
4436                            .map(|this| {
4437                                if ix == 0 {
4438                                    this.style(ButtonStyle::Tinted(TintColor::Accent))
4439                                } else {
4440                                    this.style(ButtonStyle::Outlined)
4441                                }
4442                            })
4443                            .when_some(method.description.clone(), |this, description| {
4444                                this.tooltip(Tooltip::text(description))
4445                            })
4446                            .on_click({
4447                                cx.listener(move |this, _, window, cx| {
4448                                    telemetry::event!(
4449                                        "Authenticate Agent Started",
4450                                        agent = agent_telemetry_id,
4451                                        method = method_id
4452                                    );
4453
4454                                    this.authenticate(
4455                                        acp::AuthMethodId::new(method_id.clone()),
4456                                        window,
4457                                        cx,
4458                                    )
4459                                })
4460                            })
4461                    }),
4462            )
4463        };
4464
4465        if pending_auth_method.is_some() {
4466            return Callout::new()
4467                .icon(IconName::Info)
4468                .title(format!("Authenticating to {}", agent_display_name))
4469                .actions_slot(
4470                    Icon::new(IconName::ArrowCircle)
4471                        .size(IconSize::Small)
4472                        .color(Color::Muted)
4473                        .with_rotate_animation(2)
4474                        .into_any_element(),
4475                )
4476                .into_any_element();
4477        }
4478
4479        Callout::new()
4480            .icon(IconName::Info)
4481            .title(format!("Authenticate to {}", agent_display_name))
4482            .when(auth_methods.len() == 1, |this| {
4483                this.actions_slot(auth_buttons())
4484            })
4485            .description_slot(
4486                v_flex()
4487                    .text_ui(cx)
4488                    .map(|this| {
4489                        if show_fallback_description {
4490                            this.child(
4491                                Label::new("Choose one of the following authentication options:")
4492                                    .size(LabelSize::Small)
4493                                    .color(Color::Muted),
4494                            )
4495                        } else {
4496                            this.children(
4497                                configuration_view
4498                                    .cloned()
4499                                    .map(|view| div().w_full().child(view)),
4500                            )
4501                            .children(description.map(|desc| {
4502                                self.render_markdown(
4503                                    desc.clone(),
4504                                    default_markdown_style(false, false, window, cx),
4505                                )
4506                            }))
4507                        }
4508                    })
4509                    .when(auth_methods.len() > 1, |this| {
4510                        this.gap_1().child(auth_buttons())
4511                    }),
4512            )
4513            .into_any_element()
4514    }
4515
4516    fn render_load_error(
4517        &self,
4518        e: &LoadError,
4519        window: &mut Window,
4520        cx: &mut Context<Self>,
4521    ) -> AnyElement {
4522        let (title, message, action_slot): (_, SharedString, _) = match e {
4523            LoadError::Unsupported {
4524                command: path,
4525                current_version,
4526                minimum_version,
4527            } => {
4528                return self.render_unsupported(path, current_version, minimum_version, window, cx);
4529            }
4530            LoadError::FailedToInstall(msg) => (
4531                "Failed to Install",
4532                msg.into(),
4533                Some(self.create_copy_button(msg.to_string()).into_any_element()),
4534            ),
4535            LoadError::Exited { status } => (
4536                "Failed to Launch",
4537                format!("Server exited with status {status}").into(),
4538                None,
4539            ),
4540            LoadError::Other(msg) => (
4541                "Failed to Launch",
4542                msg.into(),
4543                Some(self.create_copy_button(msg.to_string()).into_any_element()),
4544            ),
4545        };
4546
4547        Callout::new()
4548            .severity(Severity::Error)
4549            .icon(IconName::XCircleFilled)
4550            .title(title)
4551            .description(message)
4552            .actions_slot(div().children(action_slot))
4553            .into_any_element()
4554    }
4555
4556    fn render_unsupported(
4557        &self,
4558        path: &SharedString,
4559        version: &SharedString,
4560        minimum_version: &SharedString,
4561        _window: &mut Window,
4562        cx: &mut Context<Self>,
4563    ) -> AnyElement {
4564        let (heading_label, description_label) = (
4565            format!("Upgrade {} to work with Zed", self.agent.name()),
4566            if version.is_empty() {
4567                format!(
4568                    "Currently using {}, which does not report a valid --version",
4569                    path,
4570                )
4571            } else {
4572                format!(
4573                    "Currently using {}, which is only version {} (need at least {minimum_version})",
4574                    path, version
4575                )
4576            },
4577        );
4578
4579        v_flex()
4580            .w_full()
4581            .p_3p5()
4582            .gap_2p5()
4583            .border_t_1()
4584            .border_color(cx.theme().colors().border)
4585            .bg(linear_gradient(
4586                180.,
4587                linear_color_stop(cx.theme().colors().editor_background.opacity(0.4), 4.),
4588                linear_color_stop(cx.theme().status().info_background.opacity(0.), 0.),
4589            ))
4590            .child(
4591                v_flex().gap_0p5().child(Label::new(heading_label)).child(
4592                    Label::new(description_label)
4593                        .size(LabelSize::Small)
4594                        .color(Color::Muted),
4595                ),
4596            )
4597            .into_any_element()
4598    }
4599
4600    fn activity_bar_bg(&self, cx: &Context<Self>) -> Hsla {
4601        let editor_bg_color = cx.theme().colors().editor_background;
4602        let active_color = cx.theme().colors().element_selected;
4603        editor_bg_color.blend(active_color.opacity(0.3))
4604    }
4605
4606    fn render_activity_bar(
4607        &self,
4608        thread_entity: &Entity<AcpThread>,
4609        window: &mut Window,
4610        cx: &Context<Self>,
4611    ) -> Option<AnyElement> {
4612        let thread = thread_entity.read(cx);
4613        let action_log = thread.action_log();
4614        let telemetry = ActionLogTelemetry::from(thread);
4615        let changed_buffers = action_log.read(cx).changed_buffers(cx);
4616        let plan = thread.plan();
4617
4618        if changed_buffers.is_empty() && plan.is_empty() && self.message_queue.is_empty() {
4619            return None;
4620        }
4621
4622        // Temporarily always enable ACP edit controls. This is temporary, to lessen the
4623        // impact of a nasty bug that causes them to sometimes be disabled when they shouldn't
4624        // be, which blocks you from being able to accept or reject edits. This switches the
4625        // bug to be that sometimes it's enabled when it shouldn't be, which at least doesn't
4626        // block you from using the panel.
4627        let pending_edits = false;
4628
4629        let use_keep_reject_buttons = !cx.has_flag::<AgentV2FeatureFlag>();
4630
4631        v_flex()
4632            .mt_1()
4633            .mx_2()
4634            .bg(self.activity_bar_bg(cx))
4635            .border_1()
4636            .border_b_0()
4637            .border_color(cx.theme().colors().border)
4638            .rounded_t_md()
4639            .shadow(vec![gpui::BoxShadow {
4640                color: gpui::black().opacity(0.15),
4641                offset: point(px(1.), px(-1.)),
4642                blur_radius: px(3.),
4643                spread_radius: px(0.),
4644            }])
4645            .when(!plan.is_empty(), |this| {
4646                this.child(self.render_plan_summary(plan, window, cx))
4647                    .when(self.plan_expanded, |parent| {
4648                        parent.child(self.render_plan_entries(plan, window, cx))
4649                    })
4650            })
4651            .when(!plan.is_empty() && !changed_buffers.is_empty(), |this| {
4652                this.child(Divider::horizontal().color(DividerColor::Border))
4653            })
4654            .when(!changed_buffers.is_empty(), |this| {
4655                this.child(self.render_edits_summary(
4656                    &changed_buffers,
4657                    self.edits_expanded,
4658                    pending_edits,
4659                    use_keep_reject_buttons,
4660                    cx,
4661                ))
4662                .when(self.edits_expanded, |parent| {
4663                    parent.child(self.render_edited_files(
4664                        action_log,
4665                        telemetry.clone(),
4666                        &changed_buffers,
4667                        pending_edits,
4668                        use_keep_reject_buttons,
4669                        cx,
4670                    ))
4671                })
4672            })
4673            .when(!self.message_queue.is_empty(), |this| {
4674                this.when(!plan.is_empty() || !changed_buffers.is_empty(), |this| {
4675                    this.child(Divider::horizontal().color(DividerColor::Border))
4676                })
4677                .child(self.render_message_queue_summary(window, cx))
4678                .when(self.queue_expanded, |parent| {
4679                    parent.child(self.render_message_queue_entries(window, cx))
4680                })
4681            })
4682            .into_any()
4683            .into()
4684    }
4685
4686    fn render_plan_summary(
4687        &self,
4688        plan: &Plan,
4689        window: &mut Window,
4690        cx: &Context<Self>,
4691    ) -> impl IntoElement {
4692        let stats = plan.stats();
4693
4694        let title = if let Some(entry) = stats.in_progress_entry
4695            && !self.plan_expanded
4696        {
4697            h_flex()
4698                .cursor_default()
4699                .relative()
4700                .w_full()
4701                .gap_1()
4702                .truncate()
4703                .child(
4704                    Label::new("Current:")
4705                        .size(LabelSize::Small)
4706                        .color(Color::Muted),
4707                )
4708                .child(
4709                    div()
4710                        .text_xs()
4711                        .text_color(cx.theme().colors().text_muted)
4712                        .line_clamp(1)
4713                        .child(MarkdownElement::new(
4714                            entry.content.clone(),
4715                            plan_label_markdown_style(&entry.status, window, cx),
4716                        )),
4717                )
4718                .when(stats.pending > 0, |this| {
4719                    this.child(
4720                        h_flex()
4721                            .absolute()
4722                            .top_0()
4723                            .right_0()
4724                            .h_full()
4725                            .child(div().min_w_8().h_full().bg(linear_gradient(
4726                                90.,
4727                                linear_color_stop(self.activity_bar_bg(cx), 1.),
4728                                linear_color_stop(self.activity_bar_bg(cx).opacity(0.2), 0.),
4729                            )))
4730                            .child(
4731                                div().pr_0p5().bg(self.activity_bar_bg(cx)).child(
4732                                    Label::new(format!("{} left", stats.pending))
4733                                        .size(LabelSize::Small)
4734                                        .color(Color::Muted),
4735                                ),
4736                            ),
4737                    )
4738                })
4739        } else {
4740            let status_label = if stats.pending == 0 {
4741                "All Done".to_string()
4742            } else if stats.completed == 0 {
4743                format!("{} Tasks", plan.entries.len())
4744            } else {
4745                format!("{}/{}", stats.completed, plan.entries.len())
4746            };
4747
4748            h_flex()
4749                .w_full()
4750                .gap_1()
4751                .justify_between()
4752                .child(
4753                    Label::new("Plan")
4754                        .size(LabelSize::Small)
4755                        .color(Color::Muted),
4756                )
4757                .child(
4758                    Label::new(status_label)
4759                        .size(LabelSize::Small)
4760                        .color(Color::Muted)
4761                        .mr_1(),
4762                )
4763        };
4764
4765        h_flex()
4766            .id("plan_summary")
4767            .p_1()
4768            .w_full()
4769            .gap_1()
4770            .when(self.plan_expanded, |this| {
4771                this.border_b_1().border_color(cx.theme().colors().border)
4772            })
4773            .child(Disclosure::new("plan_disclosure", self.plan_expanded))
4774            .child(title)
4775            .on_click(cx.listener(|this, _, _, cx| {
4776                this.plan_expanded = !this.plan_expanded;
4777                cx.notify();
4778            }))
4779    }
4780
4781    fn render_plan_entries(
4782        &self,
4783        plan: &Plan,
4784        window: &mut Window,
4785        cx: &Context<Self>,
4786    ) -> impl IntoElement {
4787        v_flex()
4788            .id("plan_items_list")
4789            .max_h_40()
4790            .overflow_y_scroll()
4791            .children(plan.entries.iter().enumerate().flat_map(|(index, entry)| {
4792                let element = h_flex()
4793                    .py_1()
4794                    .px_2()
4795                    .gap_2()
4796                    .justify_between()
4797                    .bg(cx.theme().colors().editor_background)
4798                    .when(index < plan.entries.len() - 1, |parent| {
4799                        parent.border_color(cx.theme().colors().border).border_b_1()
4800                    })
4801                    .child(
4802                        h_flex()
4803                            .id(("plan_entry", index))
4804                            .gap_1p5()
4805                            .max_w_full()
4806                            .overflow_x_scroll()
4807                            .text_xs()
4808                            .text_color(cx.theme().colors().text_muted)
4809                            .child(match entry.status {
4810                                acp::PlanEntryStatus::InProgress => {
4811                                    Icon::new(IconName::TodoProgress)
4812                                        .size(IconSize::Small)
4813                                        .color(Color::Accent)
4814                                        .with_rotate_animation(2)
4815                                        .into_any_element()
4816                                }
4817                                acp::PlanEntryStatus::Completed => {
4818                                    Icon::new(IconName::TodoComplete)
4819                                        .size(IconSize::Small)
4820                                        .color(Color::Success)
4821                                        .into_any_element()
4822                                }
4823                                acp::PlanEntryStatus::Pending | _ => {
4824                                    Icon::new(IconName::TodoPending)
4825                                        .size(IconSize::Small)
4826                                        .color(Color::Muted)
4827                                        .into_any_element()
4828                                }
4829                            })
4830                            .child(MarkdownElement::new(
4831                                entry.content.clone(),
4832                                plan_label_markdown_style(&entry.status, window, cx),
4833                            )),
4834                    );
4835
4836                Some(element)
4837            }))
4838            .into_any_element()
4839    }
4840
4841    fn render_edits_summary(
4842        &self,
4843        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
4844        expanded: bool,
4845        pending_edits: bool,
4846        use_keep_reject_buttons: bool,
4847        cx: &Context<Self>,
4848    ) -> Div {
4849        const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete.";
4850
4851        let focus_handle = self.focus_handle(cx);
4852
4853        h_flex()
4854            .p_1()
4855            .justify_between()
4856            .flex_wrap()
4857            .when(expanded, |this| {
4858                this.border_b_1().border_color(cx.theme().colors().border)
4859            })
4860            .child(
4861                h_flex()
4862                    .id("edits-container")
4863                    .cursor_pointer()
4864                    .gap_1()
4865                    .child(Disclosure::new("edits-disclosure", expanded))
4866                    .map(|this| {
4867                        if pending_edits {
4868                            this.child(
4869                                Label::new(format!(
4870                                    "Editing {} {}",
4871                                    changed_buffers.len(),
4872                                    if changed_buffers.len() == 1 {
4873                                        "file"
4874                                    } else {
4875                                        "files"
4876                                    }
4877                                ))
4878                                .color(Color::Muted)
4879                                .size(LabelSize::Small)
4880                                .with_animation(
4881                                    "edit-label",
4882                                    Animation::new(Duration::from_secs(2))
4883                                        .repeat()
4884                                        .with_easing(pulsating_between(0.3, 0.7)),
4885                                    |label, delta| label.alpha(delta),
4886                                ),
4887                            )
4888                        } else {
4889                            let stats = DiffStats::all_files(changed_buffers, cx);
4890                            let dot_divider = || {
4891                                Label::new("")
4892                                    .size(LabelSize::XSmall)
4893                                    .color(Color::Disabled)
4894                            };
4895
4896                            this.child(
4897                                Label::new("Edits")
4898                                    .size(LabelSize::Small)
4899                                    .color(Color::Muted),
4900                            )
4901                            .child(dot_divider())
4902                            .child(
4903                                Label::new(format!(
4904                                    "{} {}",
4905                                    changed_buffers.len(),
4906                                    if changed_buffers.len() == 1 {
4907                                        "file"
4908                                    } else {
4909                                        "files"
4910                                    }
4911                                ))
4912                                .size(LabelSize::Small)
4913                                .color(Color::Muted),
4914                            )
4915                            .child(dot_divider())
4916                            .child(DiffStat::new(
4917                                "total",
4918                                stats.lines_added as usize,
4919                                stats.lines_removed as usize,
4920                            ))
4921                        }
4922                    })
4923                    .on_click(cx.listener(|this, _, _, cx| {
4924                        this.edits_expanded = !this.edits_expanded;
4925                        cx.notify();
4926                    })),
4927            )
4928            .when(use_keep_reject_buttons, |this| {
4929                this.child(
4930                    h_flex()
4931                        .gap_1()
4932                        .child(
4933                            IconButton::new("review-changes", IconName::ListTodo)
4934                                .icon_size(IconSize::Small)
4935                                .tooltip({
4936                                    let focus_handle = focus_handle.clone();
4937                                    move |_window, cx| {
4938                                        Tooltip::for_action_in(
4939                                            "Review Changes",
4940                                            &OpenAgentDiff,
4941                                            &focus_handle,
4942                                            cx,
4943                                        )
4944                                    }
4945                                })
4946                                .on_click(cx.listener(|_, _, window, cx| {
4947                                    window.dispatch_action(OpenAgentDiff.boxed_clone(), cx);
4948                                })),
4949                        )
4950                        .child(Divider::vertical().color(DividerColor::Border))
4951                        .child(
4952                            Button::new("reject-all-changes", "Reject All")
4953                                .label_size(LabelSize::Small)
4954                                .disabled(pending_edits)
4955                                .when(pending_edits, |this| {
4956                                    this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
4957                                })
4958                                .key_binding(
4959                                    KeyBinding::for_action_in(
4960                                        &RejectAll,
4961                                        &focus_handle.clone(),
4962                                        cx,
4963                                    )
4964                                    .map(|kb| kb.size(rems_from_px(10.))),
4965                                )
4966                                .on_click(cx.listener(move |this, _, window, cx| {
4967                                    this.reject_all(&RejectAll, window, cx);
4968                                })),
4969                        )
4970                        .child(
4971                            Button::new("keep-all-changes", "Keep All")
4972                                .label_size(LabelSize::Small)
4973                                .disabled(pending_edits)
4974                                .when(pending_edits, |this| {
4975                                    this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
4976                                })
4977                                .key_binding(
4978                                    KeyBinding::for_action_in(&KeepAll, &focus_handle, cx)
4979                                        .map(|kb| kb.size(rems_from_px(10.))),
4980                                )
4981                                .on_click(cx.listener(move |this, _, window, cx| {
4982                                    this.keep_all(&KeepAll, window, cx);
4983                                })),
4984                        ),
4985                )
4986            })
4987            .when(!use_keep_reject_buttons, |this| {
4988                this.child(
4989                    Button::new("review-changes", "Review Changes")
4990                        .label_size(LabelSize::Small)
4991                        .key_binding(
4992                            KeyBinding::for_action_in(
4993                                &git_ui::project_diff::Diff,
4994                                &focus_handle,
4995                                cx,
4996                            )
4997                            .map(|kb| kb.size(rems_from_px(10.))),
4998                        )
4999                        .on_click(cx.listener(move |_, _, window, cx| {
5000                            window.dispatch_action(git_ui::project_diff::Diff.boxed_clone(), cx);
5001                        })),
5002                )
5003            })
5004    }
5005
5006    fn render_edited_files(
5007        &self,
5008        action_log: &Entity<ActionLog>,
5009        telemetry: ActionLogTelemetry,
5010        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
5011        pending_edits: bool,
5012        use_keep_reject_buttons: bool,
5013        cx: &Context<Self>,
5014    ) -> impl IntoElement {
5015        let editor_bg_color = cx.theme().colors().editor_background;
5016
5017        v_flex()
5018            .id("edited_files_list")
5019            .max_h_40()
5020            .overflow_y_scroll()
5021            .children(
5022                changed_buffers
5023                    .iter()
5024                    .enumerate()
5025                    .flat_map(|(index, (buffer, diff))| {
5026                        let file = buffer.read(cx).file()?;
5027                        let path = file.path();
5028                        let path_style = file.path_style(cx);
5029                        let separator = file.path_style(cx).primary_separator();
5030
5031                        let file_path = path.parent().and_then(|parent| {
5032                            if parent.is_empty() {
5033                                None
5034                            } else {
5035                                Some(
5036                                    Label::new(format!(
5037                                        "{}{separator}",
5038                                        parent.display(path_style)
5039                                    ))
5040                                    .color(Color::Muted)
5041                                    .size(LabelSize::XSmall)
5042                                    .buffer_font(cx),
5043                                )
5044                            }
5045                        });
5046
5047                        let file_name = path.file_name().map(|name| {
5048                            Label::new(name.to_string())
5049                                .size(LabelSize::XSmall)
5050                                .buffer_font(cx)
5051                                .ml_1()
5052                        });
5053
5054                        let full_path = path.display(path_style).to_string();
5055
5056                        let file_icon = FileIcons::get_icon(path.as_std_path(), cx)
5057                            .map(Icon::from_path)
5058                            .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
5059                            .unwrap_or_else(|| {
5060                                Icon::new(IconName::File)
5061                                    .color(Color::Muted)
5062                                    .size(IconSize::Small)
5063                            });
5064
5065                        let overlay_gradient = linear_gradient(
5066                            90.,
5067                            linear_color_stop(editor_bg_color, 1.),
5068                            linear_color_stop(editor_bg_color.opacity(0.2), 0.),
5069                        );
5070
5071                        let file_stats = DiffStats::single_file(buffer.read(cx), diff.read(cx), cx);
5072
5073                        let element = h_flex()
5074                            .group("edited-code")
5075                            .id(("file-container", index))
5076                            .py_1()
5077                            .pl_2()
5078                            .pr_1()
5079                            .gap_2()
5080                            .justify_between()
5081                            .bg(editor_bg_color)
5082                            .when(index < changed_buffers.len() - 1, |parent| {
5083                                parent.border_color(cx.theme().colors().border).border_b_1()
5084                            })
5085                            .child(
5086                                h_flex()
5087                                    .id(("file-name-row", index))
5088                                    .relative()
5089                                    .pr_8()
5090                                    .w_full()
5091                                    .child(
5092                                        h_flex()
5093                                            .id(("file-name-path", index))
5094                                            .cursor_pointer()
5095                                            .pr_0p5()
5096                                            .gap_0p5()
5097                                            .hover(|s| s.bg(cx.theme().colors().element_hover))
5098                                            .rounded_xs()
5099                                            .child(file_icon)
5100                                            .children(file_name)
5101                                            .children(file_path)
5102                                            .child(
5103                                                DiffStat::new(
5104                                                    "file",
5105                                                    file_stats.lines_added as usize,
5106                                                    file_stats.lines_removed as usize,
5107                                                )
5108                                                .label_size(LabelSize::XSmall),
5109                                            )
5110                                            .tooltip(move |_, cx| {
5111                                                Tooltip::with_meta(
5112                                                    "Go to File",
5113                                                    None,
5114                                                    full_path.clone(),
5115                                                    cx,
5116                                                )
5117                                            })
5118                                            .on_click({
5119                                                let buffer = buffer.clone();
5120                                                cx.listener(move |this, _, window, cx| {
5121                                                    this.open_edited_buffer(&buffer, window, cx);
5122                                                })
5123                                            }),
5124                                    )
5125                                    .child(
5126                                        div()
5127                                            .absolute()
5128                                            .h_full()
5129                                            .w_12()
5130                                            .top_0()
5131                                            .bottom_0()
5132                                            .right_0()
5133                                            .bg(overlay_gradient),
5134                                    ),
5135                            )
5136                            .when(use_keep_reject_buttons, |parent| {
5137                                parent.child(
5138                                    h_flex()
5139                                        .gap_1()
5140                                        .visible_on_hover("edited-code")
5141                                        .child(
5142                                            Button::new("review", "Review")
5143                                                .label_size(LabelSize::Small)
5144                                                .on_click({
5145                                                    let buffer = buffer.clone();
5146                                                    let workspace = self.workspace.clone();
5147                                                    cx.listener(move |_, _, window, cx| {
5148                                                        let Some(workspace) = workspace.upgrade() else {
5149                                                            return;
5150                                                        };
5151                                                        let Some(file) = buffer.read(cx).file() else {
5152                                                            return;
5153                                                        };
5154                                                        let project_path = project::ProjectPath {
5155                                                            worktree_id: file.worktree_id(cx),
5156                                                            path: file.path().clone(),
5157                                                        };
5158                                                        workspace.update(cx, |workspace, cx| {
5159                                                            git_ui::project_diff::ProjectDiff::deploy_at_project_path(
5160                                                                workspace,
5161                                                                project_path,
5162                                                                window,
5163                                                                cx,
5164                                                            );
5165                                                        });
5166                                                    })
5167                                                }),
5168                                        )
5169                                        .child(Divider::vertical().color(DividerColor::BorderVariant))
5170                                        .child(
5171                                            Button::new("reject-file", "Reject")
5172                                                .label_size(LabelSize::Small)
5173                                                .disabled(pending_edits)
5174                                                .on_click({
5175                                                    let buffer = buffer.clone();
5176                                                    let action_log = action_log.clone();
5177                                                    let telemetry = telemetry.clone();
5178                                                    move |_, _, cx| {
5179                                                        action_log.update(cx, |action_log, cx| {
5180                                                            action_log
5181                                                        .reject_edits_in_ranges(
5182                                                            buffer.clone(),
5183                                                            vec![Anchor::min_max_range_for_buffer(
5184                                                                buffer.read(cx).remote_id(),
5185                                                            )],
5186                                                            Some(telemetry.clone()),
5187                                                            cx,
5188                                                        )
5189                                                        .detach_and_log_err(cx);
5190                                                        })
5191                                                    }
5192                                                }),
5193                                        )
5194                                        .child(
5195                                            Button::new("keep-file", "Keep")
5196                                                .label_size(LabelSize::Small)
5197                                                .disabled(pending_edits)
5198                                                .on_click({
5199                                                    let buffer = buffer.clone();
5200                                                    let action_log = action_log.clone();
5201                                                    let telemetry = telemetry.clone();
5202                                                    move |_, _, cx| {
5203                                                        action_log.update(cx, |action_log, cx| {
5204                                                            action_log.keep_edits_in_range(
5205                                                                buffer.clone(),
5206                                                                Anchor::min_max_range_for_buffer(
5207                                                                    buffer.read(cx).remote_id(),
5208                                                                ),
5209                                                                Some(telemetry.clone()),
5210                                                                cx,
5211                                                            );
5212                                                        })
5213                                                    }
5214                                                }),
5215                                        ),
5216                                )
5217                            })
5218                            .when(!use_keep_reject_buttons, |parent| {
5219                                parent.child(
5220                                    h_flex()
5221                                        .gap_1()
5222                                        .visible_on_hover("edited-code")
5223                                        .child(
5224                                            Button::new("review", "Review")
5225                                                .label_size(LabelSize::Small)
5226                                                .on_click({
5227                                                    let buffer = buffer.clone();
5228                                                    let workspace = self.workspace.clone();
5229                                                    cx.listener(move |_, _, window, cx| {
5230                                                        let Some(workspace) = workspace.upgrade() else {
5231                                                            return;
5232                                                        };
5233                                                        let Some(file) = buffer.read(cx).file() else {
5234                                                            return;
5235                                                        };
5236                                                        let project_path = project::ProjectPath {
5237                                                            worktree_id: file.worktree_id(cx),
5238                                                            path: file.path().clone(),
5239                                                        };
5240                                                        workspace.update(cx, |workspace, cx| {
5241                                                            git_ui::project_diff::ProjectDiff::deploy_at_project_path(
5242                                                                workspace,
5243                                                                project_path,
5244                                                                window,
5245                                                                cx,
5246                                                            );
5247                                                        });
5248                                                    })
5249                                                }),
5250                                        ),
5251                                )
5252                            });
5253
5254                        Some(element)
5255                    }),
5256            )
5257            .into_any_element()
5258    }
5259
5260    fn render_message_queue_summary(
5261        &self,
5262        _window: &mut Window,
5263        cx: &Context<Self>,
5264    ) -> impl IntoElement {
5265        let queue_count = self.message_queue.len();
5266        let title: SharedString = if queue_count == 1 {
5267            "1 Queued Message".into()
5268        } else {
5269            format!("{} Queued Messages", queue_count).into()
5270        };
5271
5272        h_flex()
5273            .p_1()
5274            .w_full()
5275            .gap_1()
5276            .justify_between()
5277            .when(self.queue_expanded, |this| {
5278                this.border_b_1().border_color(cx.theme().colors().border)
5279            })
5280            .child(
5281                h_flex()
5282                    .id("queue_summary")
5283                    .gap_1()
5284                    .child(Disclosure::new("queue_disclosure", self.queue_expanded))
5285                    .child(Label::new(title).size(LabelSize::Small).color(Color::Muted))
5286                    .on_click(cx.listener(|this, _, _, cx| {
5287                        this.queue_expanded = !this.queue_expanded;
5288                        cx.notify();
5289                    })),
5290            )
5291            .child(
5292                Button::new("clear_queue", "Clear All")
5293                    .label_size(LabelSize::Small)
5294                    .key_binding(KeyBinding::for_action(&ClearMessageQueue, cx))
5295                    .on_click(cx.listener(|this, _, _, cx| {
5296                        this.message_queue.clear();
5297                        cx.notify();
5298                    })),
5299            )
5300    }
5301
5302    fn render_message_queue_entries(
5303        &self,
5304        _window: &mut Window,
5305        cx: &Context<Self>,
5306    ) -> impl IntoElement {
5307        let message_editor = self.message_editor.read(cx);
5308        let focus_handle = message_editor.focus_handle(cx);
5309
5310        v_flex()
5311            .id("message_queue_list")
5312            .max_h_40()
5313            .overflow_y_scroll()
5314            .children(
5315                self.message_queue
5316                    .iter()
5317                    .enumerate()
5318                    .map(|(index, queued)| {
5319                        let is_next = index == 0;
5320                        let icon_color = if is_next { Color::Accent } else { Color::Muted };
5321                        let queue_len = self.message_queue.len();
5322
5323                        let preview = queued
5324                            .content
5325                            .iter()
5326                            .find_map(|block| match block {
5327                                acp::ContentBlock::Text(text) => {
5328                                    text.text.lines().next().map(str::to_owned)
5329                                }
5330                                _ => None,
5331                            })
5332                            .unwrap_or_default();
5333
5334                        h_flex()
5335                            .group("queue_entry")
5336                            .w_full()
5337                            .p_1()
5338                            .pl_2()
5339                            .gap_1()
5340                            .justify_between()
5341                            .bg(cx.theme().colors().editor_background)
5342                            .when(index < queue_len - 1, |parent| {
5343                                parent.border_color(cx.theme().colors().border).border_b_1()
5344                            })
5345                            .child(
5346                                h_flex()
5347                                    .id(("queued_prompt", index))
5348                                    .min_w_0()
5349                                    .w_full()
5350                                    .gap_1p5()
5351                                    .child(
5352                                        Icon::new(IconName::Circle)
5353                                            .size(IconSize::Small)
5354                                            .color(icon_color),
5355                                    )
5356                                    .child(
5357                                        Label::new(preview)
5358                                            .size(LabelSize::XSmall)
5359                                            .color(Color::Muted)
5360                                            .buffer_font(cx)
5361                                            .truncate(),
5362                                    )
5363                                    .when(is_next, |this| {
5364                                        this.tooltip(Tooltip::text("Next Prompt in the Queue"))
5365                                    }),
5366                            )
5367                            .child(
5368                                h_flex()
5369                                    .flex_none()
5370                                    .gap_1()
5371                                    .visible_on_hover("queue_entry")
5372                                    .child(
5373                                        Button::new(("delete", index), "Remove")
5374                                            .label_size(LabelSize::Small)
5375                                            .on_click(cx.listener(move |this, _, _, cx| {
5376                                                if index < this.message_queue.len() {
5377                                                    this.message_queue.remove(index);
5378                                                    cx.notify();
5379                                                }
5380                                            })),
5381                                    )
5382                                    .child(
5383                                        Button::new(("send_now", index), "Send Now")
5384                                            .style(ButtonStyle::Outlined)
5385                                            .label_size(LabelSize::Small)
5386                                            .when(is_next, |this| {
5387                                                this.key_binding(
5388                                                    KeyBinding::for_action_in(
5389                                                        &SendNextQueuedMessage,
5390                                                        &focus_handle.clone(),
5391                                                        cx,
5392                                                    )
5393                                                    .map(|kb| kb.size(rems_from_px(10.))),
5394                                                )
5395                                            })
5396                                            .on_click(cx.listener(move |this, _, window, cx| {
5397                                                this.send_queued_message_at_index(
5398                                                    index, true, window, cx,
5399                                                );
5400                                            })),
5401                                    ),
5402                            )
5403                    }),
5404            )
5405            .into_any_element()
5406    }
5407
5408    fn render_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
5409        let focus_handle = self.message_editor.focus_handle(cx);
5410        let editor_bg_color = cx.theme().colors().editor_background;
5411        let (expand_icon, expand_tooltip) = if self.editor_expanded {
5412            (IconName::Minimize, "Minimize Message Editor")
5413        } else {
5414            (IconName::Maximize, "Expand Message Editor")
5415        };
5416
5417        let backdrop = div()
5418            .size_full()
5419            .absolute()
5420            .inset_0()
5421            .bg(cx.theme().colors().panel_background)
5422            .opacity(0.8)
5423            .block_mouse_except_scroll();
5424
5425        let enable_editor = match self.thread_state {
5426            ThreadState::Ready { .. } => true,
5427            ThreadState::Loading { .. }
5428            | ThreadState::Unauthenticated { .. }
5429            | ThreadState::LoadError(..) => false,
5430        };
5431
5432        v_flex()
5433            .on_action(cx.listener(Self::expand_message_editor))
5434            .p_2()
5435            .gap_2()
5436            .border_t_1()
5437            .border_color(cx.theme().colors().border)
5438            .bg(editor_bg_color)
5439            .when(self.editor_expanded, |this| {
5440                this.h(vh(0.8, window)).size_full().justify_between()
5441            })
5442            .child(
5443                v_flex()
5444                    .relative()
5445                    .size_full()
5446                    .pt_1()
5447                    .pr_2p5()
5448                    .child(self.message_editor.clone())
5449                    .child(
5450                        h_flex()
5451                            .absolute()
5452                            .top_0()
5453                            .right_0()
5454                            .opacity(0.5)
5455                            .hover(|this| this.opacity(1.0))
5456                            .child(
5457                                IconButton::new("toggle-height", expand_icon)
5458                                    .icon_size(IconSize::Small)
5459                                    .icon_color(Color::Muted)
5460                                    .tooltip({
5461                                        move |_window, cx| {
5462                                            Tooltip::for_action_in(
5463                                                expand_tooltip,
5464                                                &ExpandMessageEditor,
5465                                                &focus_handle,
5466                                                cx,
5467                                            )
5468                                        }
5469                                    })
5470                                    .on_click(cx.listener(|this, _, window, cx| {
5471                                        this.expand_message_editor(
5472                                            &ExpandMessageEditor,
5473                                            window,
5474                                            cx,
5475                                        );
5476                                    })),
5477                            ),
5478                    ),
5479            )
5480            .child(
5481                h_flex()
5482                    .flex_none()
5483                    .flex_wrap()
5484                    .justify_between()
5485                    .child(
5486                        h_flex()
5487                            .gap_0p5()
5488                            .child(self.render_add_context_button(cx))
5489                            .child(self.render_follow_toggle(cx))
5490                            .children(self.render_burn_mode_toggle(cx)),
5491                    )
5492                    .child(
5493                        h_flex()
5494                            .gap_1()
5495                            .children(self.render_token_usage(cx))
5496                            .children(self.profile_selector.clone())
5497                            // Either config_options_view OR (mode_selector + model_selector)
5498                            .children(self.config_options_view.clone())
5499                            .when(self.config_options_view.is_none(), |this| {
5500                                this.children(self.mode_selector().cloned())
5501                                    .children(self.model_selector.clone())
5502                            })
5503                            .child(self.render_send_button(cx)),
5504                    ),
5505            )
5506            .when(!enable_editor, |this| this.child(backdrop))
5507            .into_any()
5508    }
5509
5510    pub(crate) fn as_native_connection(
5511        &self,
5512        cx: &App,
5513    ) -> Option<Rc<agent::NativeAgentConnection>> {
5514        let acp_thread = self.thread()?.read(cx);
5515        acp_thread.connection().clone().downcast()
5516    }
5517
5518    pub(crate) fn as_native_thread(&self, cx: &App) -> Option<Entity<agent::Thread>> {
5519        let acp_thread = self.thread()?.read(cx);
5520        self.as_native_connection(cx)?
5521            .thread(acp_thread.session_id(), cx)
5522    }
5523
5524    fn is_imported_thread(&self, cx: &App) -> bool {
5525        let Some(thread) = self.as_native_thread(cx) else {
5526            return false;
5527        };
5528        thread.read(cx).is_imported()
5529    }
5530
5531    fn is_using_zed_ai_models(&self, cx: &App) -> bool {
5532        self.as_native_thread(cx)
5533            .and_then(|thread| thread.read(cx).model())
5534            .is_some_and(|model| model.provider_id() == language_model::ZED_CLOUD_PROVIDER_ID)
5535    }
5536
5537    fn render_token_usage(&self, cx: &mut Context<Self>) -> Option<Div> {
5538        let thread = self.thread()?.read(cx);
5539        let usage = thread.token_usage()?;
5540        let is_generating = thread.status() != ThreadStatus::Idle;
5541
5542        let used = crate::text_thread_editor::humanize_token_count(usage.used_tokens);
5543        let max = crate::text_thread_editor::humanize_token_count(usage.max_tokens);
5544
5545        Some(
5546            h_flex()
5547                .flex_shrink_0()
5548                .gap_0p5()
5549                .mr_1p5()
5550                .child(
5551                    Label::new(used)
5552                        .size(LabelSize::Small)
5553                        .color(Color::Muted)
5554                        .map(|label| {
5555                            if is_generating {
5556                                label
5557                                    .with_animation(
5558                                        "used-tokens-label",
5559                                        Animation::new(Duration::from_secs(2))
5560                                            .repeat()
5561                                            .with_easing(pulsating_between(0.3, 0.8)),
5562                                        |label, delta| label.alpha(delta),
5563                                    )
5564                                    .into_any()
5565                            } else {
5566                                label.into_any_element()
5567                            }
5568                        }),
5569                )
5570                .child(
5571                    Label::new("/")
5572                        .size(LabelSize::Small)
5573                        .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.5))),
5574                )
5575                .child(Label::new(max).size(LabelSize::Small).color(Color::Muted)),
5576        )
5577    }
5578
5579    fn toggle_burn_mode(
5580        &mut self,
5581        _: &ToggleBurnMode,
5582        _window: &mut Window,
5583        cx: &mut Context<Self>,
5584    ) {
5585        let Some(thread) = self.as_native_thread(cx) else {
5586            return;
5587        };
5588
5589        thread.update(cx, |thread, cx| {
5590            let current_mode = thread.completion_mode();
5591            thread.set_completion_mode(
5592                match current_mode {
5593                    CompletionMode::Burn => CompletionMode::Normal,
5594                    CompletionMode::Normal => CompletionMode::Burn,
5595                },
5596                cx,
5597            );
5598        });
5599    }
5600
5601    fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context<Self>) {
5602        let Some(thread) = self.thread() else {
5603            return;
5604        };
5605        let telemetry = ActionLogTelemetry::from(thread.read(cx));
5606        let action_log = thread.read(cx).action_log().clone();
5607        action_log.update(cx, |action_log, cx| {
5608            action_log.keep_all_edits(Some(telemetry), cx)
5609        });
5610    }
5611
5612    fn reject_all(&mut self, _: &RejectAll, _window: &mut Window, cx: &mut Context<Self>) {
5613        let Some(thread) = self.thread() else {
5614            return;
5615        };
5616        let telemetry = ActionLogTelemetry::from(thread.read(cx));
5617        let action_log = thread.read(cx).action_log().clone();
5618        action_log
5619            .update(cx, |action_log, cx| {
5620                action_log.reject_all_edits(Some(telemetry), cx)
5621            })
5622            .detach();
5623    }
5624
5625    fn allow_always(&mut self, _: &AllowAlways, window: &mut Window, cx: &mut Context<Self>) {
5626        self.authorize_pending_tool_call(acp::PermissionOptionKind::AllowAlways, window, cx);
5627    }
5628
5629    fn allow_once(&mut self, _: &AllowOnce, window: &mut Window, cx: &mut Context<Self>) {
5630        self.authorize_pending_tool_call(acp::PermissionOptionKind::AllowOnce, window, cx);
5631    }
5632
5633    fn reject_once(&mut self, _: &RejectOnce, window: &mut Window, cx: &mut Context<Self>) {
5634        self.authorize_pending_tool_call(acp::PermissionOptionKind::RejectOnce, window, cx);
5635    }
5636
5637    fn authorize_pending_tool_call(
5638        &mut self,
5639        kind: acp::PermissionOptionKind,
5640        window: &mut Window,
5641        cx: &mut Context<Self>,
5642    ) -> Option<()> {
5643        let thread = self.thread()?.read(cx);
5644        let tool_call = thread.first_tool_awaiting_confirmation()?;
5645        let ToolCallStatus::WaitingForConfirmation { options, .. } = &tool_call.status else {
5646            return None;
5647        };
5648        let option = options.iter().find(|o| o.kind == kind)?;
5649
5650        self.authorize_tool_call(
5651            tool_call.id.clone(),
5652            option.option_id.clone(),
5653            option.kind,
5654            window,
5655            cx,
5656        );
5657
5658        Some(())
5659    }
5660
5661    fn render_burn_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
5662        let thread = self.as_native_thread(cx)?.read(cx);
5663
5664        if thread
5665            .model()
5666            .is_none_or(|model| !model.supports_burn_mode())
5667        {
5668            return None;
5669        }
5670
5671        let active_completion_mode = thread.completion_mode();
5672        let burn_mode_enabled = active_completion_mode == CompletionMode::Burn;
5673        let icon = if burn_mode_enabled {
5674            IconName::ZedBurnModeOn
5675        } else {
5676            IconName::ZedBurnMode
5677        };
5678
5679        Some(
5680            IconButton::new("burn-mode", icon)
5681                .icon_size(IconSize::Small)
5682                .icon_color(Color::Muted)
5683                .toggle_state(burn_mode_enabled)
5684                .selected_icon_color(Color::Error)
5685                .on_click(cx.listener(|this, _event, window, cx| {
5686                    this.toggle_burn_mode(&ToggleBurnMode, window, cx);
5687                }))
5688                .tooltip(move |_window, cx| {
5689                    cx.new(|_| BurnModeTooltip::new().selected(burn_mode_enabled))
5690                        .into()
5691                })
5692                .into_any_element(),
5693        )
5694    }
5695
5696    fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
5697        let message_editor = self.message_editor.read(cx);
5698        let is_editor_empty = message_editor.is_empty(cx);
5699        let focus_handle = message_editor.focus_handle(cx);
5700
5701        let is_generating = self
5702            .thread()
5703            .is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle);
5704
5705        if self.is_loading_contents {
5706            div()
5707                .id("loading-message-content")
5708                .px_1()
5709                .tooltip(Tooltip::text("Loading Added Context…"))
5710                .child(loading_contents_spinner(IconSize::default()))
5711                .into_any_element()
5712        } else if is_generating && is_editor_empty {
5713            IconButton::new("stop-generation", IconName::Stop)
5714                .icon_color(Color::Error)
5715                .style(ButtonStyle::Tinted(TintColor::Error))
5716                .tooltip(move |_window, cx| {
5717                    Tooltip::for_action("Stop Generation", &editor::actions::Cancel, cx)
5718                })
5719                .on_click(cx.listener(|this, _event, _, cx| this.cancel_generation(cx)))
5720                .into_any_element()
5721        } else {
5722            IconButton::new("send-message", IconName::Send)
5723                .style(ButtonStyle::Filled)
5724                .map(|this| {
5725                    if is_editor_empty && !is_generating {
5726                        this.disabled(true).icon_color(Color::Muted)
5727                    } else {
5728                        this.icon_color(Color::Accent)
5729                    }
5730                })
5731                .tooltip(move |_window, cx| {
5732                    if is_editor_empty && !is_generating {
5733                        Tooltip::for_action("Type to Send", &Chat, cx)
5734                    } else if is_generating {
5735                        let focus_handle = focus_handle.clone();
5736
5737                        Tooltip::element(move |_window, cx| {
5738                            v_flex()
5739                                .gap_1()
5740                                .child(
5741                                    h_flex()
5742                                        .gap_2()
5743                                        .justify_between()
5744                                        .child(Label::new("Queue and Send"))
5745                                        .child(KeyBinding::for_action_in(&Chat, &focus_handle, cx)),
5746                                )
5747                                .child(
5748                                    h_flex()
5749                                        .pt_1()
5750                                        .gap_2()
5751                                        .justify_between()
5752                                        .border_t_1()
5753                                        .border_color(cx.theme().colors().border_variant)
5754                                        .child(Label::new("Send Immediately"))
5755                                        .child(KeyBinding::for_action_in(
5756                                            &SendImmediately,
5757                                            &focus_handle,
5758                                            cx,
5759                                        )),
5760                                )
5761                                .into_any_element()
5762                        })(_window, cx)
5763                    } else {
5764                        Tooltip::for_action("Send Message", &Chat, cx)
5765                    }
5766                })
5767                .on_click(cx.listener(|this, _, window, cx| {
5768                    this.send(window, cx);
5769                }))
5770                .into_any_element()
5771        }
5772    }
5773
5774    fn is_following(&self, cx: &App) -> bool {
5775        match self.thread().map(|thread| thread.read(cx).status()) {
5776            Some(ThreadStatus::Generating) => self
5777                .workspace
5778                .read_with(cx, |workspace, _| {
5779                    workspace.is_being_followed(CollaboratorId::Agent)
5780                })
5781                .unwrap_or(false),
5782            _ => self.should_be_following,
5783        }
5784    }
5785
5786    fn toggle_following(&mut self, window: &mut Window, cx: &mut Context<Self>) {
5787        let following = self.is_following(cx);
5788
5789        self.should_be_following = !following;
5790        if self.thread().map(|thread| thread.read(cx).status()) == Some(ThreadStatus::Generating) {
5791            self.workspace
5792                .update(cx, |workspace, cx| {
5793                    if following {
5794                        workspace.unfollow(CollaboratorId::Agent, window, cx);
5795                    } else {
5796                        workspace.follow(CollaboratorId::Agent, window, cx);
5797                    }
5798                })
5799                .ok();
5800        }
5801
5802        telemetry::event!("Follow Agent Selected", following = !following);
5803    }
5804
5805    fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
5806        let following = self.is_following(cx);
5807
5808        let tooltip_label = if following {
5809            if self.agent.name() == "Zed Agent" {
5810                format!("Stop Following the {}", self.agent.name())
5811            } else {
5812                format!("Stop Following {}", self.agent.name())
5813            }
5814        } else {
5815            if self.agent.name() == "Zed Agent" {
5816                format!("Follow the {}", self.agent.name())
5817            } else {
5818                format!("Follow {}", self.agent.name())
5819            }
5820        };
5821
5822        IconButton::new("follow-agent", IconName::Crosshair)
5823            .icon_size(IconSize::Small)
5824            .icon_color(Color::Muted)
5825            .toggle_state(following)
5826            .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
5827            .tooltip(move |_window, cx| {
5828                if following {
5829                    Tooltip::for_action(tooltip_label.clone(), &Follow, cx)
5830                } else {
5831                    Tooltip::with_meta(
5832                        tooltip_label.clone(),
5833                        Some(&Follow),
5834                        "Track the agent's location as it reads and edits files.",
5835                        cx,
5836                    )
5837                }
5838            })
5839            .on_click(cx.listener(move |this, _, window, cx| {
5840                this.toggle_following(window, cx);
5841            }))
5842    }
5843
5844    fn render_add_context_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
5845        let message_editor = self.message_editor.clone();
5846        let menu_visible = message_editor.read(cx).is_completions_menu_visible(cx);
5847
5848        IconButton::new("add-context", IconName::AtSign)
5849            .icon_size(IconSize::Small)
5850            .icon_color(Color::Muted)
5851            .when(!menu_visible, |this| {
5852                this.tooltip(move |_window, cx| {
5853                    Tooltip::with_meta("Add Context", None, "Or type @ to include context", cx)
5854                })
5855            })
5856            .on_click(cx.listener(move |_this, _, window, cx| {
5857                let message_editor_clone = message_editor.clone();
5858
5859                window.defer(cx, move |window, cx| {
5860                    message_editor_clone.update(cx, |message_editor, cx| {
5861                        message_editor.trigger_completion_menu(window, cx);
5862                    });
5863                });
5864            }))
5865    }
5866
5867    fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
5868        let workspace = self.workspace.clone();
5869        MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
5870            Self::open_link(text, &workspace, window, cx);
5871        })
5872    }
5873
5874    fn open_link(
5875        url: SharedString,
5876        workspace: &WeakEntity<Workspace>,
5877        window: &mut Window,
5878        cx: &mut App,
5879    ) {
5880        let Some(workspace) = workspace.upgrade() else {
5881            cx.open_url(&url);
5882            return;
5883        };
5884
5885        if let Some(mention) = MentionUri::parse(&url, workspace.read(cx).path_style(cx)).log_err()
5886        {
5887            workspace.update(cx, |workspace, cx| match mention {
5888                MentionUri::File { abs_path } => {
5889                    let project = workspace.project();
5890                    let Some(path) =
5891                        project.update(cx, |project, cx| project.find_project_path(abs_path, cx))
5892                    else {
5893                        return;
5894                    };
5895
5896                    workspace
5897                        .open_path(path, None, true, window, cx)
5898                        .detach_and_log_err(cx);
5899                }
5900                MentionUri::PastedImage => {}
5901                MentionUri::Directory { abs_path } => {
5902                    let project = workspace.project();
5903                    let Some(entry_id) = project.update(cx, |project, cx| {
5904                        let path = project.find_project_path(abs_path, cx)?;
5905                        project.entry_for_path(&path, cx).map(|entry| entry.id)
5906                    }) else {
5907                        return;
5908                    };
5909
5910                    project.update(cx, |_, cx| {
5911                        cx.emit(project::Event::RevealInProjectPanel(entry_id));
5912                    });
5913                }
5914                MentionUri::Symbol {
5915                    abs_path: path,
5916                    line_range,
5917                    ..
5918                }
5919                | MentionUri::Selection {
5920                    abs_path: Some(path),
5921                    line_range,
5922                } => {
5923                    let project = workspace.project();
5924                    let Some(path) =
5925                        project.update(cx, |project, cx| project.find_project_path(path, cx))
5926                    else {
5927                        return;
5928                    };
5929
5930                    let item = workspace.open_path(path, None, true, window, cx);
5931                    window
5932                        .spawn(cx, async move |cx| {
5933                            let Some(editor) = item.await?.downcast::<Editor>() else {
5934                                return Ok(());
5935                            };
5936                            let range = Point::new(*line_range.start(), 0)
5937                                ..Point::new(*line_range.start(), 0);
5938                            editor
5939                                .update_in(cx, |editor, window, cx| {
5940                                    editor.change_selections(
5941                                        SelectionEffects::scroll(Autoscroll::center()),
5942                                        window,
5943                                        cx,
5944                                        |s| s.select_ranges(vec![range]),
5945                                    );
5946                                })
5947                                .ok();
5948                            anyhow::Ok(())
5949                        })
5950                        .detach_and_log_err(cx);
5951                }
5952                MentionUri::Selection { abs_path: None, .. } => {}
5953                MentionUri::Thread { id, name } => {
5954                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
5955                        panel.update(cx, |panel, cx| {
5956                            panel.load_agent_thread(
5957                                AgentSessionInfo {
5958                                    session_id: id,
5959                                    cwd: None,
5960                                    title: Some(name.into()),
5961                                    updated_at: None,
5962                                    meta: None,
5963                                },
5964                                window,
5965                                cx,
5966                            )
5967                        });
5968                    }
5969                }
5970                MentionUri::TextThread { path, .. } => {
5971                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
5972                        panel.update(cx, |panel, cx| {
5973                            panel
5974                                .open_saved_text_thread(path.as_path().into(), window, cx)
5975                                .detach_and_log_err(cx);
5976                        });
5977                    }
5978                }
5979                MentionUri::Rule { id, .. } => {
5980                    let PromptId::User { uuid } = id else {
5981                        return;
5982                    };
5983                    window.dispatch_action(
5984                        Box::new(OpenRulesLibrary {
5985                            prompt_to_select: Some(uuid.0),
5986                        }),
5987                        cx,
5988                    )
5989                }
5990                MentionUri::Fetch { url } => {
5991                    cx.open_url(url.as_str());
5992                }
5993            })
5994        } else {
5995            cx.open_url(&url);
5996        }
5997    }
5998
5999    fn open_tool_call_location(
6000        &self,
6001        entry_ix: usize,
6002        location_ix: usize,
6003        window: &mut Window,
6004        cx: &mut Context<Self>,
6005    ) -> Option<()> {
6006        let (tool_call_location, agent_location) = self
6007            .thread()?
6008            .read(cx)
6009            .entries()
6010            .get(entry_ix)?
6011            .location(location_ix)?;
6012
6013        let project_path = self
6014            .project
6015            .read(cx)
6016            .find_project_path(&tool_call_location.path, cx)?;
6017
6018        let open_task = self
6019            .workspace
6020            .update(cx, |workspace, cx| {
6021                workspace.open_path(project_path, None, true, window, cx)
6022            })
6023            .log_err()?;
6024        window
6025            .spawn(cx, async move |cx| {
6026                let item = open_task.await?;
6027
6028                let Some(active_editor) = item.downcast::<Editor>() else {
6029                    return anyhow::Ok(());
6030                };
6031
6032                active_editor.update_in(cx, |editor, window, cx| {
6033                    let multibuffer = editor.buffer().read(cx);
6034                    let buffer = multibuffer.as_singleton();
6035                    if agent_location.buffer.upgrade() == buffer {
6036                        let excerpt_id = multibuffer.excerpt_ids().first().cloned();
6037                        let anchor =
6038                            editor::Anchor::in_buffer(excerpt_id.unwrap(), agent_location.position);
6039                        editor.change_selections(Default::default(), window, cx, |selections| {
6040                            selections.select_anchor_ranges([anchor..anchor]);
6041                        })
6042                    } else {
6043                        let row = tool_call_location.line.unwrap_or_default();
6044                        editor.change_selections(Default::default(), window, cx, |selections| {
6045                            selections.select_ranges([Point::new(row, 0)..Point::new(row, 0)]);
6046                        })
6047                    }
6048                })?;
6049
6050                anyhow::Ok(())
6051            })
6052            .detach_and_log_err(cx);
6053
6054        None
6055    }
6056
6057    pub fn open_thread_as_markdown(
6058        &self,
6059        workspace: Entity<Workspace>,
6060        window: &mut Window,
6061        cx: &mut App,
6062    ) -> Task<Result<()>> {
6063        let markdown_language_task = workspace
6064            .read(cx)
6065            .app_state()
6066            .languages
6067            .language_for_name("Markdown");
6068
6069        let (thread_title, markdown) = if let Some(thread) = self.thread() {
6070            let thread = thread.read(cx);
6071            (thread.title().to_string(), thread.to_markdown(cx))
6072        } else {
6073            return Task::ready(Ok(()));
6074        };
6075
6076        let project = workspace.read(cx).project().clone();
6077        window.spawn(cx, async move |cx| {
6078            let markdown_language = markdown_language_task.await?;
6079
6080            let buffer = project
6081                .update(cx, |project, cx| project.create_buffer(false, cx))
6082                .await?;
6083
6084            buffer.update(cx, |buffer, cx| {
6085                buffer.set_text(markdown, cx);
6086                buffer.set_language(Some(markdown_language), cx);
6087                buffer.set_capability(language::Capability::ReadWrite, cx);
6088            });
6089
6090            workspace.update_in(cx, |workspace, window, cx| {
6091                let buffer = cx
6092                    .new(|cx| MultiBuffer::singleton(buffer, cx).with_title(thread_title.clone()));
6093
6094                workspace.add_item_to_active_pane(
6095                    Box::new(cx.new(|cx| {
6096                        let mut editor =
6097                            Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
6098                        editor.set_breadcrumb_header(thread_title);
6099                        editor
6100                    })),
6101                    None,
6102                    true,
6103                    window,
6104                    cx,
6105                );
6106            })?;
6107            anyhow::Ok(())
6108        })
6109    }
6110
6111    fn scroll_to_top(&mut self, cx: &mut Context<Self>) {
6112        self.list_state.scroll_to(ListOffset::default());
6113        cx.notify();
6114    }
6115
6116    fn scroll_to_most_recent_user_prompt(&mut self, cx: &mut Context<Self>) {
6117        let Some(thread) = self.thread() else {
6118            return;
6119        };
6120
6121        let entries = thread.read(cx).entries();
6122        if entries.is_empty() {
6123            return;
6124        }
6125
6126        // Find the most recent user message and scroll it to the top of the viewport.
6127        // (Fallback: if no user message exists, scroll to the bottom.)
6128        if let Some(ix) = entries
6129            .iter()
6130            .rposition(|entry| matches!(entry, AgentThreadEntry::UserMessage(_)))
6131        {
6132            self.list_state.scroll_to(ListOffset {
6133                item_ix: ix,
6134                offset_in_item: px(0.0),
6135            });
6136            cx.notify();
6137        } else {
6138            self.scroll_to_bottom(cx);
6139        }
6140    }
6141
6142    pub fn scroll_to_bottom(&mut self, cx: &mut Context<Self>) {
6143        if let Some(thread) = self.thread() {
6144            let entry_count = thread.read(cx).entries().len();
6145            self.list_state.reset(entry_count);
6146            cx.notify();
6147        }
6148    }
6149
6150    fn notify_with_sound(
6151        &mut self,
6152        caption: impl Into<SharedString>,
6153        icon: IconName,
6154        window: &mut Window,
6155        cx: &mut Context<Self>,
6156    ) {
6157        self.play_notification_sound(window, cx);
6158        self.show_notification(caption, icon, window, cx);
6159    }
6160
6161    fn play_notification_sound(&self, window: &Window, cx: &mut App) {
6162        let settings = AgentSettings::get_global(cx);
6163        if settings.play_sound_when_agent_done && !window.is_window_active() {
6164            Audio::play_sound(Sound::AgentDone, cx);
6165        }
6166    }
6167
6168    fn show_notification(
6169        &mut self,
6170        caption: impl Into<SharedString>,
6171        icon: IconName,
6172        window: &mut Window,
6173        cx: &mut Context<Self>,
6174    ) {
6175        if !self.notifications.is_empty() {
6176            return;
6177        }
6178
6179        let settings = AgentSettings::get_global(cx);
6180
6181        let window_is_inactive = !window.is_window_active();
6182        let panel_is_hidden = self
6183            .workspace
6184            .upgrade()
6185            .map(|workspace| AgentPanel::is_hidden(&workspace, cx))
6186            .unwrap_or(true);
6187
6188        let should_notify = window_is_inactive || panel_is_hidden;
6189
6190        if !should_notify {
6191            return;
6192        }
6193
6194        // TODO: Change this once we have title summarization for external agents.
6195        let title = self.agent.name();
6196
6197        match settings.notify_when_agent_waiting {
6198            NotifyWhenAgentWaiting::PrimaryScreen => {
6199                if let Some(primary) = cx.primary_display() {
6200                    self.pop_up(icon, caption.into(), title, window, primary, cx);
6201                }
6202            }
6203            NotifyWhenAgentWaiting::AllScreens => {
6204                let caption = caption.into();
6205                for screen in cx.displays() {
6206                    self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx);
6207                }
6208            }
6209            NotifyWhenAgentWaiting::Never => {
6210                // Don't show anything
6211            }
6212        }
6213    }
6214
6215    fn pop_up(
6216        &mut self,
6217        icon: IconName,
6218        caption: SharedString,
6219        title: SharedString,
6220        window: &mut Window,
6221        screen: Rc<dyn PlatformDisplay>,
6222        cx: &mut Context<Self>,
6223    ) {
6224        let options = AgentNotification::window_options(screen, cx);
6225
6226        let project_name = self.workspace.upgrade().and_then(|workspace| {
6227            workspace
6228                .read(cx)
6229                .project()
6230                .read(cx)
6231                .visible_worktrees(cx)
6232                .next()
6233                .map(|worktree| worktree.read(cx).root_name_str().to_string())
6234        });
6235
6236        if let Some(screen_window) = cx
6237            .open_window(options, |_window, cx| {
6238                cx.new(|_cx| {
6239                    AgentNotification::new(title.clone(), caption.clone(), icon, project_name)
6240                })
6241            })
6242            .log_err()
6243            && let Some(pop_up) = screen_window.entity(cx).log_err()
6244        {
6245            self.notification_subscriptions
6246                .entry(screen_window)
6247                .or_insert_with(Vec::new)
6248                .push(cx.subscribe_in(&pop_up, window, {
6249                    |this, _, event, window, cx| match event {
6250                        AgentNotificationEvent::Accepted => {
6251                            let handle = window.window_handle();
6252                            cx.activate(true);
6253
6254                            let workspace_handle = this.workspace.clone();
6255
6256                            // If there are multiple Zed windows, activate the correct one.
6257                            cx.defer(move |cx| {
6258                                handle
6259                                    .update(cx, |_view, window, _cx| {
6260                                        window.activate_window();
6261
6262                                        if let Some(workspace) = workspace_handle.upgrade() {
6263                                            workspace.update(_cx, |workspace, cx| {
6264                                                workspace.focus_panel::<AgentPanel>(window, cx);
6265                                            });
6266                                        }
6267                                    })
6268                                    .log_err();
6269                            });
6270
6271                            this.dismiss_notifications(cx);
6272                        }
6273                        AgentNotificationEvent::Dismissed => {
6274                            this.dismiss_notifications(cx);
6275                        }
6276                    }
6277                }));
6278
6279            self.notifications.push(screen_window);
6280
6281            // If the user manually refocuses the original window, dismiss the popup.
6282            self.notification_subscriptions
6283                .entry(screen_window)
6284                .or_insert_with(Vec::new)
6285                .push({
6286                    let pop_up_weak = pop_up.downgrade();
6287
6288                    cx.observe_window_activation(window, move |_, window, cx| {
6289                        if window.is_window_active()
6290                            && let Some(pop_up) = pop_up_weak.upgrade()
6291                        {
6292                            pop_up.update(cx, |_, cx| {
6293                                cx.emit(AgentNotificationEvent::Dismissed);
6294                            });
6295                        }
6296                    })
6297                });
6298        }
6299    }
6300
6301    fn dismiss_notifications(&mut self, cx: &mut Context<Self>) {
6302        for window in self.notifications.drain(..) {
6303            window
6304                .update(cx, |_, window, _| {
6305                    window.remove_window();
6306                })
6307                .ok();
6308
6309            self.notification_subscriptions.remove(&window);
6310        }
6311    }
6312
6313    fn render_generating(&self, confirmation: bool, cx: &App) -> impl IntoElement {
6314        let show_stats = AgentSettings::get_global(cx).show_turn_stats;
6315        let elapsed_label = show_stats
6316            .then(|| {
6317                self.turn_started_at.and_then(|started_at| {
6318                    let elapsed = started_at.elapsed();
6319                    (elapsed > STOPWATCH_THRESHOLD).then(|| duration_alt_display(elapsed))
6320                })
6321            })
6322            .flatten();
6323
6324        let is_waiting = confirmation
6325            || self
6326                .thread()
6327                .is_some_and(|thread| thread.read(cx).has_in_progress_tool_calls());
6328
6329        let turn_tokens_label = elapsed_label
6330            .is_some()
6331            .then(|| {
6332                self.turn_tokens
6333                    .filter(|&tokens| tokens > TOKEN_THRESHOLD)
6334                    .map(|tokens| crate::text_thread_editor::humanize_token_count(tokens))
6335            })
6336            .flatten();
6337
6338        let arrow_icon = if is_waiting {
6339            IconName::ArrowUp
6340        } else {
6341            IconName::ArrowDown
6342        };
6343
6344        h_flex()
6345            .id("generating-spinner")
6346            .py_2()
6347            .px(rems_from_px(22.))
6348            .gap_2()
6349            .map(|this| {
6350                if confirmation {
6351                    this.child(
6352                        h_flex()
6353                            .w_2()
6354                            .child(SpinnerLabel::sand().size(LabelSize::Small)),
6355                    )
6356                    .child(
6357                        div().min_w(rems(8.)).child(
6358                            LoadingLabel::new("Waiting Confirmation")
6359                                .size(LabelSize::Small)
6360                                .color(Color::Muted),
6361                        ),
6362                    )
6363                } else {
6364                    this.child(SpinnerLabel::new().size(LabelSize::Small))
6365                }
6366            })
6367            .when_some(elapsed_label, |this, elapsed| {
6368                this.child(
6369                    Label::new(elapsed)
6370                        .size(LabelSize::Small)
6371                        .color(Color::Muted),
6372                )
6373            })
6374            .when_some(turn_tokens_label, |this, tokens| {
6375                this.child(
6376                    h_flex()
6377                        .gap_0p5()
6378                        .child(
6379                            Icon::new(arrow_icon)
6380                                .size(IconSize::XSmall)
6381                                .color(Color::Muted),
6382                        )
6383                        .child(
6384                            Label::new(format!("{} tokens", tokens))
6385                                .size(LabelSize::Small)
6386                                .color(Color::Muted),
6387                        ),
6388                )
6389            })
6390            .into_any_element()
6391    }
6392
6393    fn render_thread_controls(
6394        &self,
6395        thread: &Entity<AcpThread>,
6396        cx: &Context<Self>,
6397    ) -> impl IntoElement {
6398        let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
6399        if is_generating {
6400            return self.render_generating(false, cx).into_any_element();
6401        }
6402
6403        let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
6404            .shape(ui::IconButtonShape::Square)
6405            .icon_size(IconSize::Small)
6406            .icon_color(Color::Ignored)
6407            .tooltip(Tooltip::text("Open Thread as Markdown"))
6408            .on_click(cx.listener(move |this, _, window, cx| {
6409                if let Some(workspace) = this.workspace.upgrade() {
6410                    this.open_thread_as_markdown(workspace, window, cx)
6411                        .detach_and_log_err(cx);
6412                }
6413            }));
6414
6415        let scroll_to_recent_user_prompt =
6416            IconButton::new("scroll_to_recent_user_prompt", IconName::ForwardArrow)
6417                .shape(ui::IconButtonShape::Square)
6418                .icon_size(IconSize::Small)
6419                .icon_color(Color::Ignored)
6420                .tooltip(Tooltip::text("Scroll To Most Recent User Prompt"))
6421                .on_click(cx.listener(move |this, _, _, cx| {
6422                    this.scroll_to_most_recent_user_prompt(cx);
6423                }));
6424
6425        let scroll_to_top = IconButton::new("scroll_to_top", IconName::ArrowUp)
6426            .shape(ui::IconButtonShape::Square)
6427            .icon_size(IconSize::Small)
6428            .icon_color(Color::Ignored)
6429            .tooltip(Tooltip::text("Scroll To Top"))
6430            .on_click(cx.listener(move |this, _, _, cx| {
6431                this.scroll_to_top(cx);
6432            }));
6433
6434        let show_stats = AgentSettings::get_global(cx).show_turn_stats;
6435        let last_turn_clock = show_stats
6436            .then(|| {
6437                self.last_turn_duration
6438                    .filter(|&duration| duration > STOPWATCH_THRESHOLD)
6439                    .map(|duration| {
6440                        Label::new(duration_alt_display(duration))
6441                            .size(LabelSize::Small)
6442                            .color(Color::Muted)
6443                    })
6444            })
6445            .flatten();
6446
6447        let last_turn_tokens = last_turn_clock
6448            .is_some()
6449            .then(|| {
6450                self.last_turn_tokens
6451                    .filter(|&tokens| tokens > TOKEN_THRESHOLD)
6452                    .map(|tokens| {
6453                        Label::new(format!(
6454                            "{} tokens",
6455                            crate::text_thread_editor::humanize_token_count(tokens)
6456                        ))
6457                        .size(LabelSize::Small)
6458                        .color(Color::Muted)
6459                    })
6460            })
6461            .flatten();
6462
6463        let mut container = h_flex()
6464            .w_full()
6465            .py_2()
6466            .px_5()
6467            .gap_px()
6468            .opacity(0.6)
6469            .hover(|s| s.opacity(1.))
6470            .justify_end()
6471            .when(
6472                last_turn_tokens.is_some() || last_turn_clock.is_some(),
6473                |this| {
6474                    this.child(
6475                        h_flex()
6476                            .gap_1()
6477                            .px_1()
6478                            .when_some(last_turn_tokens, |this, label| this.child(label))
6479                            .when_some(last_turn_clock, |this, label| this.child(label)),
6480                    )
6481                },
6482            );
6483
6484        if AgentSettings::get_global(cx).enable_feedback
6485            && self
6486                .thread()
6487                .is_some_and(|thread| thread.read(cx).connection().telemetry().is_some())
6488        {
6489            let feedback = self.thread_feedback.feedback;
6490
6491            let tooltip_meta = || {
6492                SharedString::new(
6493                    "Rating the thread sends all of your current conversation to the Zed team.",
6494                )
6495            };
6496
6497            container = container
6498                .child(
6499                    IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
6500                        .shape(ui::IconButtonShape::Square)
6501                        .icon_size(IconSize::Small)
6502                        .icon_color(match feedback {
6503                            Some(ThreadFeedback::Positive) => Color::Accent,
6504                            _ => Color::Ignored,
6505                        })
6506                        .tooltip(move |window, cx| match feedback {
6507                            Some(ThreadFeedback::Positive) => {
6508                                Tooltip::text("Thanks for your feedback!")(window, cx)
6509                            }
6510                            _ => Tooltip::with_meta("Helpful Response", None, tooltip_meta(), cx),
6511                        })
6512                        .on_click(cx.listener(move |this, _, window, cx| {
6513                            this.handle_feedback_click(ThreadFeedback::Positive, window, cx);
6514                        })),
6515                )
6516                .child(
6517                    IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
6518                        .shape(ui::IconButtonShape::Square)
6519                        .icon_size(IconSize::Small)
6520                        .icon_color(match feedback {
6521                            Some(ThreadFeedback::Negative) => Color::Accent,
6522                            _ => Color::Ignored,
6523                        })
6524                        .tooltip(move |window, cx| match feedback {
6525                            Some(ThreadFeedback::Negative) => {
6526                                Tooltip::text(
6527                                    "We appreciate your feedback and will use it to improve in the future.",
6528                                )(window, cx)
6529                            }
6530                            _ => {
6531                                Tooltip::with_meta("Not Helpful Response", None, tooltip_meta(), cx)
6532                            }
6533                        })
6534                        .on_click(cx.listener(move |this, _, window, cx| {
6535                            this.handle_feedback_click(ThreadFeedback::Negative, window, cx);
6536                        })),
6537                );
6538        }
6539
6540        if cx.has_flag::<AgentSharingFeatureFlag>()
6541            && self.is_imported_thread(cx)
6542            && self
6543                .project
6544                .read(cx)
6545                .client()
6546                .status()
6547                .borrow()
6548                .is_connected()
6549        {
6550            let sync_button = IconButton::new("sync-thread", IconName::ArrowCircle)
6551                .shape(ui::IconButtonShape::Square)
6552                .icon_size(IconSize::Small)
6553                .icon_color(Color::Ignored)
6554                .tooltip(Tooltip::text("Sync with source thread"))
6555                .on_click(cx.listener(move |this, _, window, cx| {
6556                    this.sync_thread(window, cx);
6557                }));
6558
6559            container = container.child(sync_button);
6560        }
6561
6562        if cx.has_flag::<AgentSharingFeatureFlag>() && !self.is_imported_thread(cx) {
6563            let share_button = IconButton::new("share-thread", IconName::ArrowUpRight)
6564                .shape(ui::IconButtonShape::Square)
6565                .icon_size(IconSize::Small)
6566                .icon_color(Color::Ignored)
6567                .tooltip(Tooltip::text("Share Thread"))
6568                .on_click(cx.listener(move |this, _, window, cx| {
6569                    this.share_thread(window, cx);
6570                }));
6571
6572            container = container.child(share_button);
6573        }
6574
6575        container
6576            .child(open_as_markdown)
6577            .child(scroll_to_recent_user_prompt)
6578            .child(scroll_to_top)
6579            .into_any_element()
6580    }
6581
6582    fn render_feedback_feedback_editor(editor: Entity<Editor>, cx: &Context<Self>) -> Div {
6583        h_flex()
6584            .key_context("AgentFeedbackMessageEditor")
6585            .on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| {
6586                this.thread_feedback.dismiss_comments();
6587                cx.notify();
6588            }))
6589            .on_action(cx.listener(move |this, _: &menu::Confirm, _window, cx| {
6590                this.submit_feedback_message(cx);
6591            }))
6592            .p_2()
6593            .mb_2()
6594            .mx_5()
6595            .gap_1()
6596            .rounded_md()
6597            .border_1()
6598            .border_color(cx.theme().colors().border)
6599            .bg(cx.theme().colors().editor_background)
6600            .child(div().w_full().child(editor))
6601            .child(
6602                h_flex()
6603                    .child(
6604                        IconButton::new("dismiss-feedback-message", IconName::Close)
6605                            .icon_color(Color::Error)
6606                            .icon_size(IconSize::XSmall)
6607                            .shape(ui::IconButtonShape::Square)
6608                            .on_click(cx.listener(move |this, _, _window, cx| {
6609                                this.thread_feedback.dismiss_comments();
6610                                cx.notify();
6611                            })),
6612                    )
6613                    .child(
6614                        IconButton::new("submit-feedback-message", IconName::Return)
6615                            .icon_size(IconSize::XSmall)
6616                            .shape(ui::IconButtonShape::Square)
6617                            .on_click(cx.listener(move |this, _, _window, cx| {
6618                                this.submit_feedback_message(cx);
6619                            })),
6620                    ),
6621            )
6622    }
6623
6624    fn handle_feedback_click(
6625        &mut self,
6626        feedback: ThreadFeedback,
6627        window: &mut Window,
6628        cx: &mut Context<Self>,
6629    ) {
6630        let Some(thread) = self.thread().cloned() else {
6631            return;
6632        };
6633
6634        self.thread_feedback.submit(thread, feedback, window, cx);
6635        cx.notify();
6636    }
6637
6638    fn submit_feedback_message(&mut self, cx: &mut Context<Self>) {
6639        let Some(thread) = self.thread().cloned() else {
6640            return;
6641        };
6642
6643        self.thread_feedback.submit_comments(thread, cx);
6644        cx.notify();
6645    }
6646
6647    fn render_token_limit_callout(&self, cx: &mut Context<Self>) -> Option<Callout> {
6648        if self.token_limit_callout_dismissed {
6649            return None;
6650        }
6651
6652        let token_usage = self.thread()?.read(cx).token_usage()?;
6653        let ratio = token_usage.ratio();
6654
6655        let (severity, icon, title) = match ratio {
6656            acp_thread::TokenUsageRatio::Normal => return None,
6657            acp_thread::TokenUsageRatio::Warning => (
6658                Severity::Warning,
6659                IconName::Warning,
6660                "Thread reaching the token limit soon",
6661            ),
6662            acp_thread::TokenUsageRatio::Exceeded => (
6663                Severity::Error,
6664                IconName::XCircle,
6665                "Thread reached the token limit",
6666            ),
6667        };
6668
6669        let burn_mode_available = self.as_native_thread(cx).is_some_and(|thread| {
6670            thread.read(cx).completion_mode() == CompletionMode::Normal
6671                && thread
6672                    .read(cx)
6673                    .model()
6674                    .is_some_and(|model| model.supports_burn_mode())
6675        });
6676
6677        let description = if burn_mode_available {
6678            "To continue, start a new thread from a summary or turn Burn Mode on."
6679        } else {
6680            "To continue, start a new thread from a summary."
6681        };
6682
6683        Some(
6684            Callout::new()
6685                .severity(severity)
6686                .icon(icon)
6687                .title(title)
6688                .description(description)
6689                .actions_slot(
6690                    h_flex()
6691                        .gap_0p5()
6692                        .child(
6693                            Button::new("start-new-thread", "Start New Thread")
6694                                .label_size(LabelSize::Small)
6695                                .on_click(cx.listener(|this, _, window, cx| {
6696                                    let Some(thread) = this.thread() else {
6697                                        return;
6698                                    };
6699                                    let session_id = thread.read(cx).session_id().clone();
6700                                    window.dispatch_action(
6701                                        crate::NewNativeAgentThreadFromSummary {
6702                                            from_session_id: session_id,
6703                                        }
6704                                        .boxed_clone(),
6705                                        cx,
6706                                    );
6707                                })),
6708                        )
6709                        .when(burn_mode_available, |this| {
6710                            this.child(
6711                                IconButton::new("burn-mode-callout", IconName::ZedBurnMode)
6712                                    .icon_size(IconSize::XSmall)
6713                                    .on_click(cx.listener(|this, _event, window, cx| {
6714                                        this.toggle_burn_mode(&ToggleBurnMode, window, cx);
6715                                    })),
6716                            )
6717                        }),
6718                )
6719                .dismiss_action(self.dismiss_error_button(cx)),
6720        )
6721    }
6722
6723    fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context<Self>) -> Option<Div> {
6724        if !self.is_using_zed_ai_models(cx) {
6725            return None;
6726        }
6727
6728        let user_store = self.project.read(cx).user_store().read(cx);
6729        if user_store.is_usage_based_billing_enabled() {
6730            return None;
6731        }
6732
6733        let plan = user_store
6734            .plan()
6735            .unwrap_or(cloud_llm_client::Plan::V1(PlanV1::ZedFree));
6736
6737        let usage = user_store.model_request_usage()?;
6738
6739        Some(
6740            div()
6741                .child(UsageCallout::new(plan, usage))
6742                .line_height(line_height),
6743        )
6744    }
6745
6746    fn agent_ui_font_size_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
6747        self.entry_view_state.update(cx, |entry_view_state, cx| {
6748            entry_view_state.agent_ui_font_size_changed(cx);
6749        });
6750    }
6751
6752    pub(crate) fn insert_dragged_files(
6753        &self,
6754        paths: Vec<project::ProjectPath>,
6755        added_worktrees: Vec<Entity<project::Worktree>>,
6756        window: &mut Window,
6757        cx: &mut Context<Self>,
6758    ) {
6759        self.message_editor.update(cx, |message_editor, cx| {
6760            message_editor.insert_dragged_files(paths, added_worktrees, window, cx);
6761        })
6762    }
6763
6764    /// Inserts the selected text into the message editor or the message being
6765    /// edited, if any.
6766    pub(crate) fn insert_selections(&self, window: &mut Window, cx: &mut Context<Self>) {
6767        self.active_editor(cx).update(cx, |editor, cx| {
6768            editor.insert_selections(window, cx);
6769        });
6770    }
6771
6772    fn render_thread_retry_status_callout(
6773        &self,
6774        _window: &mut Window,
6775        _cx: &mut Context<Self>,
6776    ) -> Option<Callout> {
6777        let state = self.thread_retry_status.as_ref()?;
6778
6779        let next_attempt_in = state
6780            .duration
6781            .saturating_sub(Instant::now().saturating_duration_since(state.started_at));
6782        if next_attempt_in.is_zero() {
6783            return None;
6784        }
6785
6786        let next_attempt_in_secs = next_attempt_in.as_secs() + 1;
6787
6788        let retry_message = if state.max_attempts == 1 {
6789            if next_attempt_in_secs == 1 {
6790                "Retrying. Next attempt in 1 second.".to_string()
6791            } else {
6792                format!("Retrying. Next attempt in {next_attempt_in_secs} seconds.")
6793            }
6794        } else if next_attempt_in_secs == 1 {
6795            format!(
6796                "Retrying. Next attempt in 1 second (Attempt {} of {}).",
6797                state.attempt, state.max_attempts,
6798            )
6799        } else {
6800            format!(
6801                "Retrying. Next attempt in {next_attempt_in_secs} seconds (Attempt {} of {}).",
6802                state.attempt, state.max_attempts,
6803            )
6804        };
6805
6806        Some(
6807            Callout::new()
6808                .severity(Severity::Warning)
6809                .title(state.last_error.clone())
6810                .description(retry_message),
6811        )
6812    }
6813
6814    fn render_codex_windows_warning(&self, cx: &mut Context<Self>) -> Callout {
6815        Callout::new()
6816            .icon(IconName::Warning)
6817            .severity(Severity::Warning)
6818            .title("Codex on Windows")
6819            .description("For best performance, run Codex in Windows Subsystem for Linux (WSL2)")
6820            .actions_slot(
6821                Button::new("open-wsl-modal", "Open in WSL")
6822                    .icon_size(IconSize::Small)
6823                    .icon_color(Color::Muted)
6824                    .on_click(cx.listener({
6825                        move |_, _, _window, cx| {
6826                            #[cfg(windows)]
6827                            _window.dispatch_action(
6828                                zed_actions::wsl_actions::OpenWsl::default().boxed_clone(),
6829                                cx,
6830                            );
6831                            cx.notify();
6832                        }
6833                    })),
6834            )
6835            .dismiss_action(
6836                IconButton::new("dismiss", IconName::Close)
6837                    .icon_size(IconSize::Small)
6838                    .icon_color(Color::Muted)
6839                    .tooltip(Tooltip::text("Dismiss Warning"))
6840                    .on_click(cx.listener({
6841                        move |this, _, _, cx| {
6842                            this.show_codex_windows_warning = false;
6843                            cx.notify();
6844                        }
6845                    })),
6846            )
6847    }
6848
6849    fn render_thread_error(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
6850        let content = match self.thread_error.as_ref()? {
6851            ThreadError::Other(error) => self.render_any_thread_error(error.clone(), window, cx),
6852            ThreadError::Refusal => self.render_refusal_error(cx),
6853            ThreadError::AuthenticationRequired(error) => {
6854                self.render_authentication_required_error(error.clone(), cx)
6855            }
6856            ThreadError::PaymentRequired => self.render_payment_required_error(cx),
6857            ThreadError::ModelRequestLimitReached(plan) => {
6858                self.render_model_request_limit_reached_error(*plan, cx)
6859            }
6860            ThreadError::ToolUseLimitReached => self.render_tool_use_limit_reached_error(cx)?,
6861        };
6862
6863        Some(div().child(content))
6864    }
6865
6866    fn render_new_version_callout(&self, version: &SharedString, cx: &mut Context<Self>) -> Div {
6867        v_flex().w_full().justify_end().child(
6868            h_flex()
6869                .p_2()
6870                .pr_3()
6871                .w_full()
6872                .gap_1p5()
6873                .border_t_1()
6874                .border_color(cx.theme().colors().border)
6875                .bg(cx.theme().colors().element_background)
6876                .child(
6877                    h_flex()
6878                        .flex_1()
6879                        .gap_1p5()
6880                        .child(
6881                            Icon::new(IconName::Download)
6882                                .color(Color::Accent)
6883                                .size(IconSize::Small),
6884                        )
6885                        .child(Label::new("New version available").size(LabelSize::Small)),
6886                )
6887                .child(
6888                    Button::new("update-button", format!("Update to v{}", version))
6889                        .label_size(LabelSize::Small)
6890                        .style(ButtonStyle::Tinted(TintColor::Accent))
6891                        .on_click(cx.listener(|this, _, window, cx| {
6892                            this.reset(window, cx);
6893                        })),
6894                ),
6895        )
6896    }
6897
6898    fn current_mode_id(&self, cx: &App) -> Option<Arc<str>> {
6899        if let Some(thread) = self.as_native_thread(cx) {
6900            Some(thread.read(cx).profile().0.clone())
6901        } else if let Some(mode_selector) = self.mode_selector() {
6902            Some(mode_selector.read(cx).mode().0)
6903        } else {
6904            None
6905        }
6906    }
6907
6908    fn current_model_id(&self, cx: &App) -> Option<String> {
6909        self.model_selector
6910            .as_ref()
6911            .and_then(|selector| selector.read(cx).active_model(cx).map(|m| m.id.to_string()))
6912    }
6913
6914    fn current_model_name(&self, cx: &App) -> SharedString {
6915        // For native agent (Zed Agent), use the specific model name (e.g., "Claude 3.5 Sonnet")
6916        // For ACP agents, use the agent name (e.g., "Claude Code", "Gemini CLI")
6917        // This provides better clarity about what refused the request
6918        if self.as_native_connection(cx).is_some() {
6919            self.model_selector
6920                .as_ref()
6921                .and_then(|selector| selector.read(cx).active_model(cx))
6922                .map(|model| model.name.clone())
6923                .unwrap_or_else(|| SharedString::from("The model"))
6924        } else {
6925            // ACP agent - use the agent name (e.g., "Claude Code", "Gemini CLI")
6926            self.agent.name()
6927        }
6928    }
6929
6930    fn render_refusal_error(&self, cx: &mut Context<'_, Self>) -> Callout {
6931        let model_or_agent_name = self.current_model_name(cx);
6932        let refusal_message = format!(
6933            "{} 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.",
6934            model_or_agent_name
6935        );
6936
6937        Callout::new()
6938            .severity(Severity::Error)
6939            .title("Request Refused")
6940            .icon(IconName::XCircle)
6941            .description(refusal_message.clone())
6942            .actions_slot(self.create_copy_button(&refusal_message))
6943            .dismiss_action(self.dismiss_error_button(cx))
6944    }
6945
6946    fn render_any_thread_error(
6947        &mut self,
6948        error: SharedString,
6949        window: &mut Window,
6950        cx: &mut Context<'_, Self>,
6951    ) -> Callout {
6952        let can_resume = self
6953            .thread()
6954            .map_or(false, |thread| thread.read(cx).can_resume(cx));
6955
6956        let can_enable_burn_mode = self.as_native_thread(cx).map_or(false, |thread| {
6957            let thread = thread.read(cx);
6958            let supports_burn_mode = thread
6959                .model()
6960                .map_or(false, |model| model.supports_burn_mode());
6961            supports_burn_mode && thread.completion_mode() == CompletionMode::Normal
6962        });
6963
6964        let markdown = if let Some(markdown) = &self.thread_error_markdown {
6965            markdown.clone()
6966        } else {
6967            let markdown = cx.new(|cx| Markdown::new(error.clone(), None, None, cx));
6968            self.thread_error_markdown = Some(markdown.clone());
6969            markdown
6970        };
6971
6972        let markdown_style = default_markdown_style(false, true, window, cx);
6973        let description = self
6974            .render_markdown(markdown, markdown_style)
6975            .into_any_element();
6976
6977        Callout::new()
6978            .severity(Severity::Error)
6979            .icon(IconName::XCircle)
6980            .title("An Error Happened")
6981            .description_slot(description)
6982            .actions_slot(
6983                h_flex()
6984                    .gap_0p5()
6985                    .when(can_resume && can_enable_burn_mode, |this| {
6986                        this.child(
6987                            Button::new("enable-burn-mode-and-retry", "Enable Burn Mode and Retry")
6988                                .icon(IconName::ZedBurnMode)
6989                                .icon_position(IconPosition::Start)
6990                                .icon_size(IconSize::Small)
6991                                .label_size(LabelSize::Small)
6992                                .on_click(cx.listener(|this, _, window, cx| {
6993                                    this.toggle_burn_mode(&ToggleBurnMode, window, cx);
6994                                    this.resume_chat(cx);
6995                                })),
6996                        )
6997                    })
6998                    .when(can_resume, |this| {
6999                        this.child(
7000                            IconButton::new("retry", IconName::RotateCw)
7001                                .icon_size(IconSize::Small)
7002                                .tooltip(Tooltip::text("Retry Generation"))
7003                                .on_click(cx.listener(|this, _, _window, cx| {
7004                                    this.resume_chat(cx);
7005                                })),
7006                        )
7007                    })
7008                    .child(self.create_copy_button(error.to_string())),
7009            )
7010            .dismiss_action(self.dismiss_error_button(cx))
7011    }
7012
7013    fn render_payment_required_error(&self, cx: &mut Context<Self>) -> Callout {
7014        const ERROR_MESSAGE: &str =
7015            "You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
7016
7017        Callout::new()
7018            .severity(Severity::Error)
7019            .icon(IconName::XCircle)
7020            .title("Free Usage Exceeded")
7021            .description(ERROR_MESSAGE)
7022            .actions_slot(
7023                h_flex()
7024                    .gap_0p5()
7025                    .child(self.upgrade_button(cx))
7026                    .child(self.create_copy_button(ERROR_MESSAGE)),
7027            )
7028            .dismiss_action(self.dismiss_error_button(cx))
7029    }
7030
7031    fn render_authentication_required_error(
7032        &self,
7033        error: SharedString,
7034        cx: &mut Context<Self>,
7035    ) -> Callout {
7036        Callout::new()
7037            .severity(Severity::Error)
7038            .title("Authentication Required")
7039            .icon(IconName::XCircle)
7040            .description(error.clone())
7041            .actions_slot(
7042                h_flex()
7043                    .gap_0p5()
7044                    .child(self.authenticate_button(cx))
7045                    .child(self.create_copy_button(error)),
7046            )
7047            .dismiss_action(self.dismiss_error_button(cx))
7048    }
7049
7050    fn render_model_request_limit_reached_error(
7051        &self,
7052        plan: cloud_llm_client::Plan,
7053        cx: &mut Context<Self>,
7054    ) -> Callout {
7055        let error_message = match plan {
7056            cloud_llm_client::Plan::V1(PlanV1::ZedPro) => {
7057                "Upgrade to usage-based billing for more prompts."
7058            }
7059            cloud_llm_client::Plan::V1(PlanV1::ZedProTrial)
7060            | cloud_llm_client::Plan::V1(PlanV1::ZedFree) => "Upgrade to Zed Pro for more prompts.",
7061            cloud_llm_client::Plan::V2(_) => "",
7062        };
7063
7064        Callout::new()
7065            .severity(Severity::Error)
7066            .title("Model Prompt Limit Reached")
7067            .icon(IconName::XCircle)
7068            .description(error_message)
7069            .actions_slot(
7070                h_flex()
7071                    .gap_0p5()
7072                    .child(self.upgrade_button(cx))
7073                    .child(self.create_copy_button(error_message)),
7074            )
7075            .dismiss_action(self.dismiss_error_button(cx))
7076    }
7077
7078    fn render_tool_use_limit_reached_error(&self, cx: &mut Context<Self>) -> Option<Callout> {
7079        let thread = self.as_native_thread(cx)?;
7080        let supports_burn_mode = thread
7081            .read(cx)
7082            .model()
7083            .is_some_and(|model| model.supports_burn_mode());
7084
7085        let focus_handle = self.focus_handle(cx);
7086
7087        Some(
7088            Callout::new()
7089                .icon(IconName::Info)
7090                .title("Consecutive tool use limit reached.")
7091                .actions_slot(
7092                    h_flex()
7093                        .gap_0p5()
7094                        .when(supports_burn_mode, |this| {
7095                            this.child(
7096                                Button::new("continue-burn-mode", "Continue with Burn Mode")
7097                                    .style(ButtonStyle::Filled)
7098                                    .style(ButtonStyle::Tinted(ui::TintColor::Accent))
7099                                    .layer(ElevationIndex::ModalSurface)
7100                                    .label_size(LabelSize::Small)
7101                                    .key_binding(
7102                                        KeyBinding::for_action_in(
7103                                            &ContinueWithBurnMode,
7104                                            &focus_handle,
7105                                            cx,
7106                                        )
7107                                        .map(|kb| kb.size(rems_from_px(10.))),
7108                                    )
7109                                    .tooltip(Tooltip::text(
7110                                        "Enable Burn Mode for unlimited tool use.",
7111                                    ))
7112                                    .on_click({
7113                                        cx.listener(move |this, _, _window, cx| {
7114                                            thread.update(cx, |thread, cx| {
7115                                                thread
7116                                                    .set_completion_mode(CompletionMode::Burn, cx);
7117                                            });
7118                                            this.resume_chat(cx);
7119                                        })
7120                                    }),
7121                            )
7122                        })
7123                        .child(
7124                            Button::new("continue-conversation", "Continue")
7125                                .layer(ElevationIndex::ModalSurface)
7126                                .label_size(LabelSize::Small)
7127                                .key_binding(
7128                                    KeyBinding::for_action_in(&ContinueThread, &focus_handle, cx)
7129                                        .map(|kb| kb.size(rems_from_px(10.))),
7130                                )
7131                                .on_click(cx.listener(|this, _, _window, cx| {
7132                                    this.resume_chat(cx);
7133                                })),
7134                        ),
7135                ),
7136        )
7137    }
7138
7139    fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
7140        let message = message.into();
7141
7142        CopyButton::new(message).tooltip_label("Copy Error Message")
7143    }
7144
7145    fn dismiss_error_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
7146        IconButton::new("dismiss", IconName::Close)
7147            .icon_size(IconSize::Small)
7148            .tooltip(Tooltip::text("Dismiss"))
7149            .on_click(cx.listener({
7150                move |this, _, _, cx| {
7151                    this.clear_thread_error(cx);
7152                    cx.notify();
7153                }
7154            }))
7155    }
7156
7157    fn authenticate_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
7158        Button::new("authenticate", "Authenticate")
7159            .label_size(LabelSize::Small)
7160            .style(ButtonStyle::Filled)
7161            .on_click(cx.listener({
7162                move |this, _, window, cx| {
7163                    let agent = this.agent.clone();
7164                    let ThreadState::Ready { thread, .. } = &this.thread_state else {
7165                        return;
7166                    };
7167
7168                    let connection = thread.read(cx).connection().clone();
7169                    this.clear_thread_error(cx);
7170                    if let Some(message) = this.in_flight_prompt.take() {
7171                        this.message_editor.update(cx, |editor, cx| {
7172                            editor.set_message(message, window, cx);
7173                        });
7174                    }
7175                    let this = cx.weak_entity();
7176                    window.defer(cx, |window, cx| {
7177                        Self::handle_auth_required(
7178                            this,
7179                            AuthRequired::new(),
7180                            agent,
7181                            connection,
7182                            window,
7183                            cx,
7184                        );
7185                    })
7186                }
7187            }))
7188    }
7189
7190    pub(crate) fn reauthenticate(&mut self, window: &mut Window, cx: &mut Context<Self>) {
7191        let agent = self.agent.clone();
7192        let ThreadState::Ready { thread, .. } = &self.thread_state else {
7193            return;
7194        };
7195
7196        let connection = thread.read(cx).connection().clone();
7197        self.clear_thread_error(cx);
7198        let this = cx.weak_entity();
7199        window.defer(cx, |window, cx| {
7200            Self::handle_auth_required(this, AuthRequired::new(), agent, connection, window, cx);
7201        })
7202    }
7203
7204    fn upgrade_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
7205        Button::new("upgrade", "Upgrade")
7206            .label_size(LabelSize::Small)
7207            .style(ButtonStyle::Tinted(ui::TintColor::Accent))
7208            .on_click(cx.listener({
7209                move |this, _, _, cx| {
7210                    this.clear_thread_error(cx);
7211                    cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx));
7212                }
7213            }))
7214    }
7215
7216    pub fn delete_history_entry(&mut self, entry: AgentSessionInfo, cx: &mut Context<Self>) {
7217        let task = self.history.update(cx, |history, cx| {
7218            history.delete_session(&entry.session_id, cx)
7219        });
7220        task.detach_and_log_err(cx);
7221    }
7222
7223    /// Returns the currently active editor, either for a message that is being
7224    /// edited or the editor for a new message.
7225    fn active_editor(&self, cx: &App) -> Entity<MessageEditor> {
7226        if let Some(index) = self.editing_message
7227            && let Some(editor) = self
7228                .entry_view_state
7229                .read(cx)
7230                .entry(index)
7231                .and_then(|e| e.message_editor())
7232                .cloned()
7233        {
7234            editor
7235        } else {
7236            self.message_editor.clone()
7237        }
7238    }
7239}
7240
7241fn loading_contents_spinner(size: IconSize) -> AnyElement {
7242    Icon::new(IconName::LoadCircle)
7243        .size(size)
7244        .color(Color::Accent)
7245        .with_rotate_animation(3)
7246        .into_any_element()
7247}
7248
7249fn placeholder_text(agent_name: &str, has_commands: bool) -> String {
7250    if agent_name == "Zed Agent" {
7251        format!("Message the {} — @ to include context", agent_name)
7252    } else if has_commands {
7253        format!(
7254            "Message {} — @ to include context, / for commands",
7255            agent_name
7256        )
7257    } else {
7258        format!("Message {} — @ to include context", agent_name)
7259    }
7260}
7261
7262impl Focusable for AcpThreadView {
7263    fn focus_handle(&self, cx: &App) -> FocusHandle {
7264        match self.thread_state {
7265            ThreadState::Ready { .. } => self.active_editor(cx).focus_handle(cx),
7266            ThreadState::Loading { .. }
7267            | ThreadState::LoadError(_)
7268            | ThreadState::Unauthenticated { .. } => self.focus_handle.clone(),
7269        }
7270    }
7271}
7272
7273#[cfg(any(test, feature = "test-support"))]
7274impl AcpThreadView {
7275    /// Expands a tool call so its content is visible.
7276    /// This is primarily useful for visual testing.
7277    pub fn expand_tool_call(&mut self, tool_call_id: acp::ToolCallId, cx: &mut Context<Self>) {
7278        self.expanded_tool_calls.insert(tool_call_id);
7279        cx.notify();
7280    }
7281}
7282
7283impl Render for AcpThreadView {
7284    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
7285        let has_messages = self.list_state.item_count() > 0;
7286        let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5;
7287
7288        v_flex()
7289            .size_full()
7290            .key_context("AcpThread")
7291            .on_action(cx.listener(|this, _: &menu::Cancel, _, cx| {
7292                this.cancel_generation(cx);
7293            }))
7294            .on_action(cx.listener(Self::toggle_burn_mode))
7295            .on_action(cx.listener(Self::keep_all))
7296            .on_action(cx.listener(Self::reject_all))
7297            .on_action(cx.listener(Self::allow_always))
7298            .on_action(cx.listener(Self::allow_once))
7299            .on_action(cx.listener(Self::reject_once))
7300            .on_action(cx.listener(|this, _: &SendNextQueuedMessage, window, cx| {
7301                this.send_queued_message_at_index(0, true, window, cx);
7302            }))
7303            .on_action(cx.listener(|this, _: &ClearMessageQueue, _, cx| {
7304                this.message_queue.clear();
7305                cx.notify();
7306            }))
7307            .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
7308                if let Some(config_options_view) = this.config_options_view.as_ref() {
7309                    let handled = config_options_view.update(cx, |view, cx| {
7310                        view.toggle_category_picker(
7311                            acp::SessionConfigOptionCategory::Mode,
7312                            window,
7313                            cx,
7314                        )
7315                    });
7316                    if handled {
7317                        return;
7318                    }
7319                }
7320
7321                if let Some(profile_selector) = this.profile_selector.as_ref() {
7322                    profile_selector.read(cx).menu_handle().toggle(window, cx);
7323                } else if let Some(mode_selector) = this.mode_selector() {
7324                    mode_selector.read(cx).menu_handle().toggle(window, cx);
7325                }
7326            }))
7327            .on_action(cx.listener(|this, _: &CycleModeSelector, window, cx| {
7328                if let Some(config_options_view) = this.config_options_view.as_ref() {
7329                    let handled = config_options_view.update(cx, |view, cx| {
7330                        view.cycle_category_option(
7331                            acp::SessionConfigOptionCategory::Mode,
7332                            false,
7333                            cx,
7334                        )
7335                    });
7336                    if handled {
7337                        return;
7338                    }
7339                }
7340
7341                if let Some(profile_selector) = this.profile_selector.as_ref() {
7342                    profile_selector.update(cx, |profile_selector, cx| {
7343                        profile_selector.cycle_profile(cx);
7344                    });
7345                } else if let Some(mode_selector) = this.mode_selector() {
7346                    mode_selector.update(cx, |mode_selector, cx| {
7347                        mode_selector.cycle_mode(window, cx);
7348                    });
7349                }
7350            }))
7351            .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
7352                if let Some(config_options_view) = this.config_options_view.as_ref() {
7353                    let handled = config_options_view.update(cx, |view, cx| {
7354                        view.toggle_category_picker(
7355                            acp::SessionConfigOptionCategory::Model,
7356                            window,
7357                            cx,
7358                        )
7359                    });
7360                    if handled {
7361                        return;
7362                    }
7363                }
7364
7365                if let Some(model_selector) = this.model_selector.as_ref() {
7366                    model_selector
7367                        .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
7368                }
7369            }))
7370            .on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
7371                if let Some(config_options_view) = this.config_options_view.as_ref() {
7372                    let handled = config_options_view.update(cx, |view, cx| {
7373                        view.cycle_category_option(
7374                            acp::SessionConfigOptionCategory::Model,
7375                            true,
7376                            cx,
7377                        )
7378                    });
7379                    if handled {
7380                        return;
7381                    }
7382                }
7383
7384                if let Some(model_selector) = this.model_selector.as_ref() {
7385                    model_selector.update(cx, |model_selector, cx| {
7386                        model_selector.cycle_favorite_models(window, cx);
7387                    });
7388                }
7389            }))
7390            .track_focus(&self.focus_handle)
7391            .bg(cx.theme().colors().panel_background)
7392            .child(match &self.thread_state {
7393                ThreadState::Unauthenticated {
7394                    connection,
7395                    description,
7396                    configuration_view,
7397                    pending_auth_method,
7398                    ..
7399                } => v_flex()
7400                    .flex_1()
7401                    .size_full()
7402                    .justify_end()
7403                    .child(self.render_auth_required_state(
7404                        connection,
7405                        description.as_ref(),
7406                        configuration_view.as_ref(),
7407                        pending_auth_method.as_ref(),
7408                        window,
7409                        cx,
7410                    ))
7411                    .into_any_element(),
7412                ThreadState::Loading { .. } => v_flex()
7413                    .flex_1()
7414                    .child(self.render_recent_history(cx))
7415                    .into_any(),
7416                ThreadState::LoadError(e) => v_flex()
7417                    .flex_1()
7418                    .size_full()
7419                    .items_center()
7420                    .justify_end()
7421                    .child(self.render_load_error(e, window, cx))
7422                    .into_any(),
7423                ThreadState::Ready { .. } => v_flex().flex_1().map(|this| {
7424                    if has_messages {
7425                        this.child(
7426                            list(
7427                                self.list_state.clone(),
7428                                cx.processor(|this, index: usize, window, cx| {
7429                                    let Some((entry, len)) = this.thread().and_then(|thread| {
7430                                        let entries = &thread.read(cx).entries();
7431                                        Some((entries.get(index)?, entries.len()))
7432                                    }) else {
7433                                        return Empty.into_any();
7434                                    };
7435                                    this.render_entry(index, len, entry, window, cx)
7436                                }),
7437                            )
7438                            .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
7439                            .flex_grow()
7440                            .into_any(),
7441                        )
7442                        .vertical_scrollbar_for(&self.list_state, window, cx)
7443                        .into_any()
7444                    } else {
7445                        this.child(self.render_recent_history(cx)).into_any()
7446                    }
7447                }),
7448            })
7449            // The activity bar is intentionally rendered outside of the ThreadState::Ready match
7450            // above so that the scrollbar doesn't render behind it. The current setup allows
7451            // the scrollbar to stop exactly at the activity bar start.
7452            .when(has_messages, |this| match &self.thread_state {
7453                ThreadState::Ready { thread, .. } => {
7454                    this.children(self.render_activity_bar(thread, window, cx))
7455                }
7456                _ => this,
7457            })
7458            .children(self.render_thread_retry_status_callout(window, cx))
7459            .when(self.show_codex_windows_warning, |this| {
7460                this.child(self.render_codex_windows_warning(cx))
7461            })
7462            .children(self.render_thread_error(window, cx))
7463            .when_some(
7464                self.new_server_version_available.as_ref().filter(|_| {
7465                    !has_messages || !matches!(self.thread_state, ThreadState::Ready { .. })
7466                }),
7467                |this, version| this.child(self.render_new_version_callout(&version, cx)),
7468            )
7469            .children(
7470                if let Some(usage_callout) = self.render_usage_callout(line_height, cx) {
7471                    Some(usage_callout.into_any_element())
7472                } else {
7473                    self.render_token_limit_callout(cx)
7474                        .map(|token_limit_callout| token_limit_callout.into_any_element())
7475                },
7476            )
7477            .child(self.render_message_editor(window, cx))
7478    }
7479}
7480
7481fn default_markdown_style(
7482    buffer_font: bool,
7483    muted_text: bool,
7484    window: &Window,
7485    cx: &App,
7486) -> MarkdownStyle {
7487    let theme_settings = ThemeSettings::get_global(cx);
7488    let colors = cx.theme().colors();
7489
7490    let buffer_font_size = theme_settings.agent_buffer_font_size(cx);
7491
7492    let mut text_style = window.text_style();
7493    let line_height = buffer_font_size * 1.75;
7494
7495    let font_family = if buffer_font {
7496        theme_settings.buffer_font.family.clone()
7497    } else {
7498        theme_settings.ui_font.family.clone()
7499    };
7500
7501    let font_size = if buffer_font {
7502        theme_settings.agent_buffer_font_size(cx)
7503    } else {
7504        theme_settings.agent_ui_font_size(cx)
7505    };
7506
7507    let text_color = if muted_text {
7508        colors.text_muted
7509    } else {
7510        colors.text
7511    };
7512
7513    text_style.refine(&TextStyleRefinement {
7514        font_family: Some(font_family),
7515        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
7516        font_features: Some(theme_settings.ui_font.features.clone()),
7517        font_size: Some(font_size.into()),
7518        line_height: Some(line_height.into()),
7519        color: Some(text_color),
7520        ..Default::default()
7521    });
7522
7523    MarkdownStyle {
7524        base_text_style: text_style.clone(),
7525        syntax: cx.theme().syntax().clone(),
7526        selection_background_color: colors.element_selection_background,
7527        code_block_overflow_x_scroll: true,
7528        heading_level_styles: Some(HeadingLevelStyles {
7529            h1: Some(TextStyleRefinement {
7530                font_size: Some(rems(1.15).into()),
7531                ..Default::default()
7532            }),
7533            h2: Some(TextStyleRefinement {
7534                font_size: Some(rems(1.1).into()),
7535                ..Default::default()
7536            }),
7537            h3: Some(TextStyleRefinement {
7538                font_size: Some(rems(1.05).into()),
7539                ..Default::default()
7540            }),
7541            h4: Some(TextStyleRefinement {
7542                font_size: Some(rems(1.).into()),
7543                ..Default::default()
7544            }),
7545            h5: Some(TextStyleRefinement {
7546                font_size: Some(rems(0.95).into()),
7547                ..Default::default()
7548            }),
7549            h6: Some(TextStyleRefinement {
7550                font_size: Some(rems(0.875).into()),
7551                ..Default::default()
7552            }),
7553        }),
7554        code_block: StyleRefinement {
7555            padding: EdgesRefinement {
7556                top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
7557                left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
7558                right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
7559                bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
7560            },
7561            margin: EdgesRefinement {
7562                top: Some(Length::Definite(px(8.).into())),
7563                left: Some(Length::Definite(px(0.).into())),
7564                right: Some(Length::Definite(px(0.).into())),
7565                bottom: Some(Length::Definite(px(12.).into())),
7566            },
7567            border_style: Some(BorderStyle::Solid),
7568            border_widths: EdgesRefinement {
7569                top: Some(AbsoluteLength::Pixels(px(1.))),
7570                left: Some(AbsoluteLength::Pixels(px(1.))),
7571                right: Some(AbsoluteLength::Pixels(px(1.))),
7572                bottom: Some(AbsoluteLength::Pixels(px(1.))),
7573            },
7574            border_color: Some(colors.border_variant),
7575            background: Some(colors.editor_background.into()),
7576            text: TextStyleRefinement {
7577                font_family: Some(theme_settings.buffer_font.family.clone()),
7578                font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
7579                font_features: Some(theme_settings.buffer_font.features.clone()),
7580                font_size: Some(buffer_font_size.into()),
7581                ..Default::default()
7582            },
7583            ..Default::default()
7584        },
7585        inline_code: TextStyleRefinement {
7586            font_family: Some(theme_settings.buffer_font.family.clone()),
7587            font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
7588            font_features: Some(theme_settings.buffer_font.features.clone()),
7589            font_size: Some(buffer_font_size.into()),
7590            background_color: Some(colors.editor_foreground.opacity(0.08)),
7591            ..Default::default()
7592        },
7593        link: TextStyleRefinement {
7594            background_color: Some(colors.editor_foreground.opacity(0.025)),
7595            color: Some(colors.text_accent),
7596            underline: Some(UnderlineStyle {
7597                color: Some(colors.text_accent.opacity(0.5)),
7598                thickness: px(1.),
7599                ..Default::default()
7600            }),
7601            ..Default::default()
7602        },
7603        ..Default::default()
7604    }
7605}
7606
7607fn plan_label_markdown_style(
7608    status: &acp::PlanEntryStatus,
7609    window: &Window,
7610    cx: &App,
7611) -> MarkdownStyle {
7612    let default_md_style = default_markdown_style(false, false, window, cx);
7613
7614    MarkdownStyle {
7615        base_text_style: TextStyle {
7616            color: cx.theme().colors().text_muted,
7617            strikethrough: if matches!(status, acp::PlanEntryStatus::Completed) {
7618                Some(gpui::StrikethroughStyle {
7619                    thickness: px(1.),
7620                    color: Some(cx.theme().colors().text_muted.opacity(0.8)),
7621                })
7622            } else {
7623                None
7624            },
7625            ..default_md_style.base_text_style
7626        },
7627        ..default_md_style
7628    }
7629}
7630
7631#[cfg(test)]
7632pub(crate) mod tests {
7633    use acp_thread::{
7634        AgentSessionList, AgentSessionListRequest, AgentSessionListResponse, StubAgentConnection,
7635    };
7636    use action_log::ActionLog;
7637    use agent_client_protocol::SessionId;
7638    use editor::MultiBufferOffset;
7639    use fs::FakeFs;
7640    use gpui::{EventEmitter, TestAppContext, VisualTestContext};
7641    use project::Project;
7642    use serde_json::json;
7643    use settings::SettingsStore;
7644    use std::any::Any;
7645    use std::path::Path;
7646    use std::rc::Rc;
7647    use workspace::Item;
7648
7649    use super::*;
7650
7651    #[gpui::test]
7652    async fn test_drop(cx: &mut TestAppContext) {
7653        init_test(cx);
7654
7655        let (thread_view, _cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
7656        let weak_view = thread_view.downgrade();
7657        drop(thread_view);
7658        assert!(!weak_view.is_upgradable());
7659    }
7660
7661    #[gpui::test]
7662    async fn test_notification_for_stop_event(cx: &mut TestAppContext) {
7663        init_test(cx);
7664
7665        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
7666
7667        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
7668        message_editor.update_in(cx, |editor, window, cx| {
7669            editor.set_text("Hello", window, cx);
7670        });
7671
7672        cx.deactivate_window();
7673
7674        thread_view.update_in(cx, |thread_view, window, cx| {
7675            thread_view.send(window, cx);
7676        });
7677
7678        cx.run_until_parked();
7679
7680        assert!(
7681            cx.windows()
7682                .iter()
7683                .any(|window| window.downcast::<AgentNotification>().is_some())
7684        );
7685    }
7686
7687    #[gpui::test]
7688    async fn test_notification_for_error(cx: &mut TestAppContext) {
7689        init_test(cx);
7690
7691        let (thread_view, cx) =
7692            setup_thread_view(StubAgentServer::new(SaboteurAgentConnection), cx).await;
7693
7694        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
7695        message_editor.update_in(cx, |editor, window, cx| {
7696            editor.set_text("Hello", window, cx);
7697        });
7698
7699        cx.deactivate_window();
7700
7701        thread_view.update_in(cx, |thread_view, window, cx| {
7702            thread_view.send(window, cx);
7703        });
7704
7705        cx.run_until_parked();
7706
7707        assert!(
7708            cx.windows()
7709                .iter()
7710                .any(|window| window.downcast::<AgentNotification>().is_some())
7711        );
7712    }
7713
7714    #[gpui::test]
7715    async fn test_recent_history_refreshes_when_history_cache_updated(cx: &mut TestAppContext) {
7716        init_test(cx);
7717
7718        let session_a = AgentSessionInfo::new(SessionId::new("session-a"));
7719        let session_b = AgentSessionInfo::new(SessionId::new("session-b"));
7720
7721        let fs = FakeFs::new(cx.executor());
7722        let project = Project::test(fs, [], cx).await;
7723        let (workspace, cx) =
7724            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7725
7726        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
7727        // Create history without an initial session list - it will be set after connection
7728        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
7729
7730        let thread_view = cx.update(|window, cx| {
7731            cx.new(|cx| {
7732                AcpThreadView::new(
7733                    Rc::new(StubAgentServer::default_response()),
7734                    None,
7735                    None,
7736                    workspace.downgrade(),
7737                    project,
7738                    Some(thread_store),
7739                    None,
7740                    history.clone(),
7741                    false,
7742                    window,
7743                    cx,
7744                )
7745            })
7746        });
7747
7748        // Wait for connection to establish
7749        cx.run_until_parked();
7750
7751        // Initially empty because StubAgentConnection.session_list() returns None
7752        thread_view.read_with(cx, |view, _cx| {
7753            assert_eq!(view.recent_history_entries.len(), 0);
7754        });
7755
7756        // Now set the session list - this simulates external agents providing their history
7757        let list_a: Rc<dyn AgentSessionList> =
7758            Rc::new(StubSessionList::new(vec![session_a.clone()]));
7759        history.update(cx, |history, cx| {
7760            history.set_session_list(Some(list_a), cx);
7761        });
7762        cx.run_until_parked();
7763
7764        thread_view.read_with(cx, |view, _cx| {
7765            assert_eq!(view.recent_history_entries.len(), 1);
7766            assert_eq!(
7767                view.recent_history_entries[0].session_id,
7768                session_a.session_id
7769            );
7770        });
7771
7772        // Update to a different session list
7773        let list_b: Rc<dyn AgentSessionList> =
7774            Rc::new(StubSessionList::new(vec![session_b.clone()]));
7775        history.update(cx, |history, cx| {
7776            history.set_session_list(Some(list_b), cx);
7777        });
7778        cx.run_until_parked();
7779
7780        thread_view.read_with(cx, |view, _cx| {
7781            assert_eq!(view.recent_history_entries.len(), 1);
7782            assert_eq!(
7783                view.recent_history_entries[0].session_id,
7784                session_b.session_id
7785            );
7786        });
7787    }
7788
7789    #[gpui::test]
7790    async fn test_refusal_handling(cx: &mut TestAppContext) {
7791        init_test(cx);
7792
7793        let (thread_view, cx) =
7794            setup_thread_view(StubAgentServer::new(RefusalAgentConnection), cx).await;
7795
7796        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
7797        message_editor.update_in(cx, |editor, window, cx| {
7798            editor.set_text("Do something harmful", window, cx);
7799        });
7800
7801        thread_view.update_in(cx, |thread_view, window, cx| {
7802            thread_view.send(window, cx);
7803        });
7804
7805        cx.run_until_parked();
7806
7807        // Check that the refusal error is set
7808        thread_view.read_with(cx, |thread_view, _cx| {
7809            assert!(
7810                matches!(thread_view.thread_error, Some(ThreadError::Refusal)),
7811                "Expected refusal error to be set"
7812            );
7813        });
7814    }
7815
7816    #[gpui::test]
7817    async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) {
7818        init_test(cx);
7819
7820        let tool_call_id = acp::ToolCallId::new("1");
7821        let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Label")
7822            .kind(acp::ToolKind::Edit)
7823            .content(vec!["hi".into()]);
7824        let connection =
7825            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
7826                tool_call_id,
7827                vec![acp::PermissionOption::new(
7828                    "1",
7829                    "Allow",
7830                    acp::PermissionOptionKind::AllowOnce,
7831                )],
7832            )]));
7833
7834        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
7835
7836        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
7837
7838        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
7839        message_editor.update_in(cx, |editor, window, cx| {
7840            editor.set_text("Hello", window, cx);
7841        });
7842
7843        cx.deactivate_window();
7844
7845        thread_view.update_in(cx, |thread_view, window, cx| {
7846            thread_view.send(window, cx);
7847        });
7848
7849        cx.run_until_parked();
7850
7851        assert!(
7852            cx.windows()
7853                .iter()
7854                .any(|window| window.downcast::<AgentNotification>().is_some())
7855        );
7856    }
7857
7858    #[gpui::test]
7859    async fn test_notification_when_panel_hidden(cx: &mut TestAppContext) {
7860        init_test(cx);
7861
7862        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
7863
7864        add_to_workspace(thread_view.clone(), cx);
7865
7866        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
7867
7868        message_editor.update_in(cx, |editor, window, cx| {
7869            editor.set_text("Hello", window, cx);
7870        });
7871
7872        // Window is active (don't deactivate), but panel will be hidden
7873        // Note: In the test environment, the panel is not actually added to the dock,
7874        // so is_agent_panel_hidden will return true
7875
7876        thread_view.update_in(cx, |thread_view, window, cx| {
7877            thread_view.send(window, cx);
7878        });
7879
7880        cx.run_until_parked();
7881
7882        // Should show notification because window is active but panel is hidden
7883        assert!(
7884            cx.windows()
7885                .iter()
7886                .any(|window| window.downcast::<AgentNotification>().is_some()),
7887            "Expected notification when panel is hidden"
7888        );
7889    }
7890
7891    #[gpui::test]
7892    async fn test_notification_still_works_when_window_inactive(cx: &mut TestAppContext) {
7893        init_test(cx);
7894
7895        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
7896
7897        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
7898        message_editor.update_in(cx, |editor, window, cx| {
7899            editor.set_text("Hello", window, cx);
7900        });
7901
7902        // Deactivate window - should show notification regardless of setting
7903        cx.deactivate_window();
7904
7905        thread_view.update_in(cx, |thread_view, window, cx| {
7906            thread_view.send(window, cx);
7907        });
7908
7909        cx.run_until_parked();
7910
7911        // Should still show notification when window is inactive (existing behavior)
7912        assert!(
7913            cx.windows()
7914                .iter()
7915                .any(|window| window.downcast::<AgentNotification>().is_some()),
7916            "Expected notification when window is inactive"
7917        );
7918    }
7919
7920    #[gpui::test]
7921    async fn test_notification_respects_never_setting(cx: &mut TestAppContext) {
7922        init_test(cx);
7923
7924        // Set notify_when_agent_waiting to Never
7925        cx.update(|cx| {
7926            AgentSettings::override_global(
7927                AgentSettings {
7928                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
7929                    ..AgentSettings::get_global(cx).clone()
7930                },
7931                cx,
7932            );
7933        });
7934
7935        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
7936
7937        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
7938        message_editor.update_in(cx, |editor, window, cx| {
7939            editor.set_text("Hello", window, cx);
7940        });
7941
7942        // Window is active
7943
7944        thread_view.update_in(cx, |thread_view, window, cx| {
7945            thread_view.send(window, cx);
7946        });
7947
7948        cx.run_until_parked();
7949
7950        // Should NOT show notification because notify_when_agent_waiting is Never
7951        assert!(
7952            !cx.windows()
7953                .iter()
7954                .any(|window| window.downcast::<AgentNotification>().is_some()),
7955            "Expected no notification when notify_when_agent_waiting is Never"
7956        );
7957    }
7958
7959    #[gpui::test]
7960    async fn test_notification_closed_when_thread_view_dropped(cx: &mut TestAppContext) {
7961        init_test(cx);
7962
7963        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
7964
7965        let weak_view = thread_view.downgrade();
7966
7967        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
7968        message_editor.update_in(cx, |editor, window, cx| {
7969            editor.set_text("Hello", window, cx);
7970        });
7971
7972        cx.deactivate_window();
7973
7974        thread_view.update_in(cx, |thread_view, window, cx| {
7975            thread_view.send(window, cx);
7976        });
7977
7978        cx.run_until_parked();
7979
7980        // Verify notification is shown
7981        assert!(
7982            cx.windows()
7983                .iter()
7984                .any(|window| window.downcast::<AgentNotification>().is_some()),
7985            "Expected notification to be shown"
7986        );
7987
7988        // Drop the thread view (simulating navigation to a new thread)
7989        drop(thread_view);
7990        drop(message_editor);
7991        // Trigger an update to flush effects, which will call release_dropped_entities
7992        cx.update(|_window, _cx| {});
7993        cx.run_until_parked();
7994
7995        // Verify the entity was actually released
7996        assert!(
7997            !weak_view.is_upgradable(),
7998            "Thread view entity should be released after dropping"
7999        );
8000
8001        // The notification should be automatically closed via on_release
8002        assert!(
8003            !cx.windows()
8004                .iter()
8005                .any(|window| window.downcast::<AgentNotification>().is_some()),
8006            "Notification should be closed when thread view is dropped"
8007        );
8008    }
8009
8010    async fn setup_thread_view(
8011        agent: impl AgentServer + 'static,
8012        cx: &mut TestAppContext,
8013    ) -> (Entity<AcpThreadView>, &mut VisualTestContext) {
8014        let fs = FakeFs::new(cx.executor());
8015        let project = Project::test(fs, [], cx).await;
8016        let (workspace, cx) =
8017            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8018
8019        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
8020        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
8021
8022        let thread_view = cx.update(|window, cx| {
8023            cx.new(|cx| {
8024                AcpThreadView::new(
8025                    Rc::new(agent),
8026                    None,
8027                    None,
8028                    workspace.downgrade(),
8029                    project,
8030                    Some(thread_store),
8031                    None,
8032                    history,
8033                    false,
8034                    window,
8035                    cx,
8036                )
8037            })
8038        });
8039        cx.run_until_parked();
8040        (thread_view, cx)
8041    }
8042
8043    fn add_to_workspace(thread_view: Entity<AcpThreadView>, cx: &mut VisualTestContext) {
8044        let workspace = thread_view.read_with(cx, |thread_view, _cx| thread_view.workspace.clone());
8045
8046        workspace
8047            .update_in(cx, |workspace, window, cx| {
8048                workspace.add_item_to_active_pane(
8049                    Box::new(cx.new(|_| ThreadViewItem(thread_view.clone()))),
8050                    None,
8051                    true,
8052                    window,
8053                    cx,
8054                );
8055            })
8056            .unwrap();
8057    }
8058
8059    struct ThreadViewItem(Entity<AcpThreadView>);
8060
8061    impl Item for ThreadViewItem {
8062        type Event = ();
8063
8064        fn include_in_nav_history() -> bool {
8065            false
8066        }
8067
8068        fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
8069            "Test".into()
8070        }
8071    }
8072
8073    impl EventEmitter<()> for ThreadViewItem {}
8074
8075    impl Focusable for ThreadViewItem {
8076        fn focus_handle(&self, cx: &App) -> FocusHandle {
8077            self.0.read(cx).focus_handle(cx)
8078        }
8079    }
8080
8081    impl Render for ThreadViewItem {
8082        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
8083            self.0.clone().into_any_element()
8084        }
8085    }
8086
8087    struct StubAgentServer<C> {
8088        connection: C,
8089    }
8090
8091    impl<C> StubAgentServer<C> {
8092        fn new(connection: C) -> Self {
8093            Self { connection }
8094        }
8095    }
8096
8097    impl StubAgentServer<StubAgentConnection> {
8098        fn default_response() -> Self {
8099            let conn = StubAgentConnection::new();
8100            conn.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
8101                acp::ContentChunk::new("Default response".into()),
8102            )]);
8103            Self::new(conn)
8104        }
8105    }
8106
8107    #[derive(Clone)]
8108    struct StubSessionList {
8109        sessions: Vec<AgentSessionInfo>,
8110    }
8111
8112    impl StubSessionList {
8113        fn new(sessions: Vec<AgentSessionInfo>) -> Self {
8114            Self { sessions }
8115        }
8116    }
8117
8118    impl AgentSessionList for StubSessionList {
8119        fn list_sessions(
8120            &self,
8121            _request: AgentSessionListRequest,
8122            _cx: &mut App,
8123        ) -> Task<anyhow::Result<AgentSessionListResponse>> {
8124            Task::ready(Ok(AgentSessionListResponse::new(self.sessions.clone())))
8125        }
8126        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
8127            self
8128        }
8129    }
8130
8131    impl<C> AgentServer for StubAgentServer<C>
8132    where
8133        C: 'static + AgentConnection + Send + Clone,
8134    {
8135        fn logo(&self) -> ui::IconName {
8136            ui::IconName::Ai
8137        }
8138
8139        fn name(&self) -> SharedString {
8140            "Test".into()
8141        }
8142
8143        fn connect(
8144            &self,
8145            _root_dir: Option<&Path>,
8146            _delegate: AgentServerDelegate,
8147            _cx: &mut App,
8148        ) -> Task<gpui::Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
8149            Task::ready(Ok((Rc::new(self.connection.clone()), None)))
8150        }
8151
8152        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
8153            self
8154        }
8155    }
8156
8157    #[derive(Clone)]
8158    struct SaboteurAgentConnection;
8159
8160    impl AgentConnection for SaboteurAgentConnection {
8161        fn telemetry_id(&self) -> SharedString {
8162            "saboteur".into()
8163        }
8164
8165        fn new_thread(
8166            self: Rc<Self>,
8167            project: Entity<Project>,
8168            _cwd: &Path,
8169            cx: &mut gpui::App,
8170        ) -> Task<gpui::Result<Entity<AcpThread>>> {
8171            Task::ready(Ok(cx.new(|cx| {
8172                let action_log = cx.new(|_| ActionLog::new(project.clone()));
8173                AcpThread::new(
8174                    "SaboteurAgentConnection",
8175                    self,
8176                    project,
8177                    action_log,
8178                    SessionId::new("test"),
8179                    watch::Receiver::constant(
8180                        acp::PromptCapabilities::new()
8181                            .image(true)
8182                            .audio(true)
8183                            .embedded_context(true),
8184                    ),
8185                    cx,
8186                )
8187            })))
8188        }
8189
8190        fn auth_methods(&self) -> &[acp::AuthMethod] {
8191            &[]
8192        }
8193
8194        fn authenticate(
8195            &self,
8196            _method_id: acp::AuthMethodId,
8197            _cx: &mut App,
8198        ) -> Task<gpui::Result<()>> {
8199            unimplemented!()
8200        }
8201
8202        fn prompt(
8203            &self,
8204            _id: Option<acp_thread::UserMessageId>,
8205            _params: acp::PromptRequest,
8206            _cx: &mut App,
8207        ) -> Task<gpui::Result<acp::PromptResponse>> {
8208            Task::ready(Err(anyhow::anyhow!("Error prompting")))
8209        }
8210
8211        fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
8212            unimplemented!()
8213        }
8214
8215        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
8216            self
8217        }
8218    }
8219
8220    /// Simulates a model which always returns a refusal response
8221    #[derive(Clone)]
8222    struct RefusalAgentConnection;
8223
8224    impl AgentConnection for RefusalAgentConnection {
8225        fn telemetry_id(&self) -> SharedString {
8226            "refusal".into()
8227        }
8228
8229        fn new_thread(
8230            self: Rc<Self>,
8231            project: Entity<Project>,
8232            _cwd: &Path,
8233            cx: &mut gpui::App,
8234        ) -> Task<gpui::Result<Entity<AcpThread>>> {
8235            Task::ready(Ok(cx.new(|cx| {
8236                let action_log = cx.new(|_| ActionLog::new(project.clone()));
8237                AcpThread::new(
8238                    "RefusalAgentConnection",
8239                    self,
8240                    project,
8241                    action_log,
8242                    SessionId::new("test"),
8243                    watch::Receiver::constant(
8244                        acp::PromptCapabilities::new()
8245                            .image(true)
8246                            .audio(true)
8247                            .embedded_context(true),
8248                    ),
8249                    cx,
8250                )
8251            })))
8252        }
8253
8254        fn auth_methods(&self) -> &[acp::AuthMethod] {
8255            &[]
8256        }
8257
8258        fn authenticate(
8259            &self,
8260            _method_id: acp::AuthMethodId,
8261            _cx: &mut App,
8262        ) -> Task<gpui::Result<()>> {
8263            unimplemented!()
8264        }
8265
8266        fn prompt(
8267            &self,
8268            _id: Option<acp_thread::UserMessageId>,
8269            _params: acp::PromptRequest,
8270            _cx: &mut App,
8271        ) -> Task<gpui::Result<acp::PromptResponse>> {
8272            Task::ready(Ok(acp::PromptResponse::new(acp::StopReason::Refusal)))
8273        }
8274
8275        fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
8276            unimplemented!()
8277        }
8278
8279        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
8280            self
8281        }
8282    }
8283
8284    pub(crate) fn init_test(cx: &mut TestAppContext) {
8285        cx.update(|cx| {
8286            let settings_store = SettingsStore::test(cx);
8287            cx.set_global(settings_store);
8288            theme::init(theme::LoadThemes::JustBase, cx);
8289            release_channel::init(semver::Version::new(0, 0, 0), cx);
8290            prompt_store::init(cx)
8291        });
8292    }
8293
8294    #[gpui::test]
8295    async fn test_rewind_views(cx: &mut TestAppContext) {
8296        init_test(cx);
8297
8298        let fs = FakeFs::new(cx.executor());
8299        fs.insert_tree(
8300            "/project",
8301            json!({
8302                "test1.txt": "old content 1",
8303                "test2.txt": "old content 2"
8304            }),
8305        )
8306        .await;
8307        let project = Project::test(fs, [Path::new("/project")], cx).await;
8308        let (workspace, cx) =
8309            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8310
8311        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
8312        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
8313
8314        let connection = Rc::new(StubAgentConnection::new());
8315        let thread_view = cx.update(|window, cx| {
8316            cx.new(|cx| {
8317                AcpThreadView::new(
8318                    Rc::new(StubAgentServer::new(connection.as_ref().clone())),
8319                    None,
8320                    None,
8321                    workspace.downgrade(),
8322                    project.clone(),
8323                    Some(thread_store.clone()),
8324                    None,
8325                    history,
8326                    false,
8327                    window,
8328                    cx,
8329                )
8330            })
8331        });
8332
8333        cx.run_until_parked();
8334
8335        let thread = thread_view
8336            .read_with(cx, |view, _| view.thread().cloned())
8337            .unwrap();
8338
8339        // First user message
8340        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(
8341            acp::ToolCall::new("tool1", "Edit file 1")
8342                .kind(acp::ToolKind::Edit)
8343                .status(acp::ToolCallStatus::Completed)
8344                .content(vec![acp::ToolCallContent::Diff(
8345                    acp::Diff::new("/project/test1.txt", "new content 1").old_text("old content 1"),
8346                )]),
8347        )]);
8348
8349        thread
8350            .update(cx, |thread, cx| thread.send_raw("Give me a diff", cx))
8351            .await
8352            .unwrap();
8353        cx.run_until_parked();
8354
8355        thread.read_with(cx, |thread, _| {
8356            assert_eq!(thread.entries().len(), 2);
8357        });
8358
8359        thread_view.read_with(cx, |view, cx| {
8360            view.entry_view_state.read_with(cx, |entry_view_state, _| {
8361                assert!(
8362                    entry_view_state
8363                        .entry(0)
8364                        .unwrap()
8365                        .message_editor()
8366                        .is_some()
8367                );
8368                assert!(entry_view_state.entry(1).unwrap().has_content());
8369            });
8370        });
8371
8372        // Second user message
8373        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(
8374            acp::ToolCall::new("tool2", "Edit file 2")
8375                .kind(acp::ToolKind::Edit)
8376                .status(acp::ToolCallStatus::Completed)
8377                .content(vec![acp::ToolCallContent::Diff(
8378                    acp::Diff::new("/project/test2.txt", "new content 2").old_text("old content 2"),
8379                )]),
8380        )]);
8381
8382        thread
8383            .update(cx, |thread, cx| thread.send_raw("Another one", cx))
8384            .await
8385            .unwrap();
8386        cx.run_until_parked();
8387
8388        let second_user_message_id = thread.read_with(cx, |thread, _| {
8389            assert_eq!(thread.entries().len(), 4);
8390            let AgentThreadEntry::UserMessage(user_message) = &thread.entries()[2] else {
8391                panic!();
8392            };
8393            user_message.id.clone().unwrap()
8394        });
8395
8396        thread_view.read_with(cx, |view, cx| {
8397            view.entry_view_state.read_with(cx, |entry_view_state, _| {
8398                assert!(
8399                    entry_view_state
8400                        .entry(0)
8401                        .unwrap()
8402                        .message_editor()
8403                        .is_some()
8404                );
8405                assert!(entry_view_state.entry(1).unwrap().has_content());
8406                assert!(
8407                    entry_view_state
8408                        .entry(2)
8409                        .unwrap()
8410                        .message_editor()
8411                        .is_some()
8412                );
8413                assert!(entry_view_state.entry(3).unwrap().has_content());
8414            });
8415        });
8416
8417        // Rewind to first message
8418        thread
8419            .update(cx, |thread, cx| thread.rewind(second_user_message_id, cx))
8420            .await
8421            .unwrap();
8422
8423        cx.run_until_parked();
8424
8425        thread.read_with(cx, |thread, _| {
8426            assert_eq!(thread.entries().len(), 2);
8427        });
8428
8429        thread_view.read_with(cx, |view, cx| {
8430            view.entry_view_state.read_with(cx, |entry_view_state, _| {
8431                assert!(
8432                    entry_view_state
8433                        .entry(0)
8434                        .unwrap()
8435                        .message_editor()
8436                        .is_some()
8437                );
8438                assert!(entry_view_state.entry(1).unwrap().has_content());
8439
8440                // Old views should be dropped
8441                assert!(entry_view_state.entry(2).is_none());
8442                assert!(entry_view_state.entry(3).is_none());
8443            });
8444        });
8445    }
8446
8447    #[gpui::test]
8448    async fn test_scroll_to_most_recent_user_prompt(cx: &mut TestAppContext) {
8449        init_test(cx);
8450
8451        let connection = StubAgentConnection::new();
8452
8453        // Each user prompt will result in a user message entry plus an agent message entry.
8454        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
8455            acp::ContentChunk::new("Response 1".into()),
8456        )]);
8457
8458        let (thread_view, cx) =
8459            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
8460
8461        let thread = thread_view
8462            .read_with(cx, |view, _| view.thread().cloned())
8463            .unwrap();
8464
8465        thread
8466            .update(cx, |thread, cx| thread.send_raw("Prompt 1", cx))
8467            .await
8468            .unwrap();
8469        cx.run_until_parked();
8470
8471        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
8472            acp::ContentChunk::new("Response 2".into()),
8473        )]);
8474
8475        thread
8476            .update(cx, |thread, cx| thread.send_raw("Prompt 2", cx))
8477            .await
8478            .unwrap();
8479        cx.run_until_parked();
8480
8481        // Move somewhere else first so we're not trivially already on the last user prompt.
8482        thread_view.update(cx, |view, cx| {
8483            view.scroll_to_top(cx);
8484        });
8485        cx.run_until_parked();
8486
8487        thread_view.update(cx, |view, cx| {
8488            view.scroll_to_most_recent_user_prompt(cx);
8489            let scroll_top = view.list_state.logical_scroll_top();
8490            // Entries layout is: [User1, Assistant1, User2, Assistant2]
8491            assert_eq!(scroll_top.item_ix, 2);
8492        });
8493    }
8494
8495    #[gpui::test]
8496    async fn test_scroll_to_most_recent_user_prompt_falls_back_to_bottom_without_user_messages(
8497        cx: &mut TestAppContext,
8498    ) {
8499        init_test(cx);
8500
8501        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
8502
8503        // With no entries, scrolling should be a no-op and must not panic.
8504        thread_view.update(cx, |view, cx| {
8505            view.scroll_to_most_recent_user_prompt(cx);
8506            let scroll_top = view.list_state.logical_scroll_top();
8507            assert_eq!(scroll_top.item_ix, 0);
8508        });
8509    }
8510
8511    #[gpui::test]
8512    async fn test_message_editing_cancel(cx: &mut TestAppContext) {
8513        init_test(cx);
8514
8515        let connection = StubAgentConnection::new();
8516
8517        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
8518            acp::ContentChunk::new("Response".into()),
8519        )]);
8520
8521        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
8522        add_to_workspace(thread_view.clone(), cx);
8523
8524        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
8525        message_editor.update_in(cx, |editor, window, cx| {
8526            editor.set_text("Original message to edit", window, cx);
8527        });
8528        thread_view.update_in(cx, |thread_view, window, cx| {
8529            thread_view.send(window, cx);
8530        });
8531
8532        cx.run_until_parked();
8533
8534        let user_message_editor = thread_view.read_with(cx, |view, cx| {
8535            assert_eq!(view.editing_message, None);
8536
8537            view.entry_view_state
8538                .read(cx)
8539                .entry(0)
8540                .unwrap()
8541                .message_editor()
8542                .unwrap()
8543                .clone()
8544        });
8545
8546        // Focus
8547        cx.focus(&user_message_editor);
8548        thread_view.read_with(cx, |view, _cx| {
8549            assert_eq!(view.editing_message, Some(0));
8550        });
8551
8552        // Edit
8553        user_message_editor.update_in(cx, |editor, window, cx| {
8554            editor.set_text("Edited message content", window, cx);
8555        });
8556
8557        // Cancel
8558        user_message_editor.update_in(cx, |_editor, window, cx| {
8559            window.dispatch_action(Box::new(editor::actions::Cancel), cx);
8560        });
8561
8562        thread_view.read_with(cx, |view, _cx| {
8563            assert_eq!(view.editing_message, None);
8564        });
8565
8566        user_message_editor.read_with(cx, |editor, cx| {
8567            assert_eq!(editor.text(cx), "Original message to edit");
8568        });
8569    }
8570
8571    #[gpui::test]
8572    async fn test_message_doesnt_send_if_empty(cx: &mut TestAppContext) {
8573        init_test(cx);
8574
8575        let connection = StubAgentConnection::new();
8576
8577        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
8578        add_to_workspace(thread_view.clone(), cx);
8579
8580        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
8581        let mut events = cx.events(&message_editor);
8582        message_editor.update_in(cx, |editor, window, cx| {
8583            editor.set_text("", window, cx);
8584        });
8585
8586        message_editor.update_in(cx, |_editor, window, cx| {
8587            window.dispatch_action(Box::new(Chat), cx);
8588        });
8589        cx.run_until_parked();
8590        // We shouldn't have received any messages
8591        assert!(matches!(
8592            events.try_next(),
8593            Err(futures::channel::mpsc::TryRecvError { .. })
8594        ));
8595    }
8596
8597    #[gpui::test]
8598    async fn test_message_editing_regenerate(cx: &mut TestAppContext) {
8599        init_test(cx);
8600
8601        let connection = StubAgentConnection::new();
8602
8603        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
8604            acp::ContentChunk::new("Response".into()),
8605        )]);
8606
8607        let (thread_view, cx) =
8608            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
8609        add_to_workspace(thread_view.clone(), cx);
8610
8611        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
8612        message_editor.update_in(cx, |editor, window, cx| {
8613            editor.set_text("Original message to edit", window, cx);
8614        });
8615        thread_view.update_in(cx, |thread_view, window, cx| {
8616            thread_view.send(window, cx);
8617        });
8618
8619        cx.run_until_parked();
8620
8621        let user_message_editor = thread_view.read_with(cx, |view, cx| {
8622            assert_eq!(view.editing_message, None);
8623            assert_eq!(view.thread().unwrap().read(cx).entries().len(), 2);
8624
8625            view.entry_view_state
8626                .read(cx)
8627                .entry(0)
8628                .unwrap()
8629                .message_editor()
8630                .unwrap()
8631                .clone()
8632        });
8633
8634        // Focus
8635        cx.focus(&user_message_editor);
8636
8637        // Edit
8638        user_message_editor.update_in(cx, |editor, window, cx| {
8639            editor.set_text("Edited message content", window, cx);
8640        });
8641
8642        // Send
8643        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
8644            acp::ContentChunk::new("New Response".into()),
8645        )]);
8646
8647        user_message_editor.update_in(cx, |_editor, window, cx| {
8648            window.dispatch_action(Box::new(Chat), cx);
8649        });
8650
8651        cx.run_until_parked();
8652
8653        thread_view.read_with(cx, |view, cx| {
8654            assert_eq!(view.editing_message, None);
8655
8656            let entries = view.thread().unwrap().read(cx).entries();
8657            assert_eq!(entries.len(), 2);
8658            assert_eq!(
8659                entries[0].to_markdown(cx),
8660                "## User\n\nEdited message content\n\n"
8661            );
8662            assert_eq!(
8663                entries[1].to_markdown(cx),
8664                "## Assistant\n\nNew Response\n\n"
8665            );
8666
8667            let new_editor = view.entry_view_state.read_with(cx, |state, _cx| {
8668                assert!(!state.entry(1).unwrap().has_content());
8669                state.entry(0).unwrap().message_editor().unwrap().clone()
8670            });
8671
8672            assert_eq!(new_editor.read(cx).text(cx), "Edited message content");
8673        })
8674    }
8675
8676    #[gpui::test]
8677    async fn test_message_editing_while_generating(cx: &mut TestAppContext) {
8678        init_test(cx);
8679
8680        let connection = StubAgentConnection::new();
8681
8682        let (thread_view, cx) =
8683            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
8684        add_to_workspace(thread_view.clone(), cx);
8685
8686        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
8687        message_editor.update_in(cx, |editor, window, cx| {
8688            editor.set_text("Original message to edit", window, cx);
8689        });
8690        thread_view.update_in(cx, |thread_view, window, cx| {
8691            thread_view.send(window, cx);
8692        });
8693
8694        cx.run_until_parked();
8695
8696        let (user_message_editor, session_id) = thread_view.read_with(cx, |view, cx| {
8697            let thread = view.thread().unwrap().read(cx);
8698            assert_eq!(thread.entries().len(), 1);
8699
8700            let editor = view
8701                .entry_view_state
8702                .read(cx)
8703                .entry(0)
8704                .unwrap()
8705                .message_editor()
8706                .unwrap()
8707                .clone();
8708
8709            (editor, thread.session_id().clone())
8710        });
8711
8712        // Focus
8713        cx.focus(&user_message_editor);
8714
8715        thread_view.read_with(cx, |view, _cx| {
8716            assert_eq!(view.editing_message, Some(0));
8717        });
8718
8719        // Edit
8720        user_message_editor.update_in(cx, |editor, window, cx| {
8721            editor.set_text("Edited message content", window, cx);
8722        });
8723
8724        thread_view.read_with(cx, |view, _cx| {
8725            assert_eq!(view.editing_message, Some(0));
8726        });
8727
8728        // Finish streaming response
8729        cx.update(|_, cx| {
8730            connection.send_update(
8731                session_id.clone(),
8732                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("Response".into())),
8733                cx,
8734            );
8735            connection.end_turn(session_id, acp::StopReason::EndTurn);
8736        });
8737
8738        thread_view.read_with(cx, |view, _cx| {
8739            assert_eq!(view.editing_message, Some(0));
8740        });
8741
8742        cx.run_until_parked();
8743
8744        // Should still be editing
8745        cx.update(|window, cx| {
8746            assert!(user_message_editor.focus_handle(cx).is_focused(window));
8747            assert_eq!(thread_view.read(cx).editing_message, Some(0));
8748            assert_eq!(
8749                user_message_editor.read(cx).text(cx),
8750                "Edited message content"
8751            );
8752        });
8753    }
8754
8755    struct GeneratingThreadSetup {
8756        thread_view: Entity<AcpThreadView>,
8757        thread: Entity<AcpThread>,
8758        message_editor: Entity<MessageEditor>,
8759    }
8760
8761    async fn setup_generating_thread(
8762        cx: &mut TestAppContext,
8763    ) -> (GeneratingThreadSetup, &mut VisualTestContext) {
8764        let connection = StubAgentConnection::new();
8765
8766        let (thread_view, cx) =
8767            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
8768        add_to_workspace(thread_view.clone(), cx);
8769
8770        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
8771        message_editor.update_in(cx, |editor, window, cx| {
8772            editor.set_text("Hello", window, cx);
8773        });
8774        thread_view.update_in(cx, |thread_view, window, cx| {
8775            thread_view.send(window, cx);
8776        });
8777
8778        let (thread, session_id) = thread_view.read_with(cx, |view, cx| {
8779            let thread = view.thread().unwrap();
8780            (thread.clone(), thread.read(cx).session_id().clone())
8781        });
8782
8783        cx.run_until_parked();
8784
8785        cx.update(|_, cx| {
8786            connection.send_update(
8787                session_id.clone(),
8788                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
8789                    "Response chunk".into(),
8790                )),
8791                cx,
8792            );
8793        });
8794
8795        cx.run_until_parked();
8796
8797        thread.read_with(cx, |thread, _cx| {
8798            assert_eq!(thread.status(), ThreadStatus::Generating);
8799        });
8800
8801        (
8802            GeneratingThreadSetup {
8803                thread_view,
8804                thread,
8805                message_editor,
8806            },
8807            cx,
8808        )
8809    }
8810
8811    #[gpui::test]
8812    async fn test_escape_cancels_generation_from_conversation_focus(cx: &mut TestAppContext) {
8813        init_test(cx);
8814
8815        let (setup, cx) = setup_generating_thread(cx).await;
8816
8817        let focus_handle = setup
8818            .thread_view
8819            .read_with(cx, |view, _cx| view.focus_handle.clone());
8820        cx.update(|window, cx| {
8821            window.focus(&focus_handle, cx);
8822        });
8823
8824        setup.thread_view.update_in(cx, |_, window, cx| {
8825            window.dispatch_action(menu::Cancel.boxed_clone(), cx);
8826        });
8827
8828        cx.run_until_parked();
8829
8830        setup.thread.read_with(cx, |thread, _cx| {
8831            assert_eq!(thread.status(), ThreadStatus::Idle);
8832        });
8833    }
8834
8835    #[gpui::test]
8836    async fn test_escape_cancels_generation_from_editor_focus(cx: &mut TestAppContext) {
8837        init_test(cx);
8838
8839        let (setup, cx) = setup_generating_thread(cx).await;
8840
8841        let editor_focus_handle = setup
8842            .message_editor
8843            .read_with(cx, |editor, cx| editor.focus_handle(cx));
8844        cx.update(|window, cx| {
8845            window.focus(&editor_focus_handle, cx);
8846        });
8847
8848        setup.message_editor.update_in(cx, |_, window, cx| {
8849            window.dispatch_action(editor::actions::Cancel.boxed_clone(), cx);
8850        });
8851
8852        cx.run_until_parked();
8853
8854        setup.thread.read_with(cx, |thread, _cx| {
8855            assert_eq!(thread.status(), ThreadStatus::Idle);
8856        });
8857    }
8858
8859    #[gpui::test]
8860    async fn test_escape_when_idle_is_noop(cx: &mut TestAppContext) {
8861        init_test(cx);
8862
8863        let (thread_view, cx) =
8864            setup_thread_view(StubAgentServer::new(StubAgentConnection::new()), cx).await;
8865        add_to_workspace(thread_view.clone(), cx);
8866
8867        let thread = thread_view.read_with(cx, |view, _cx| view.thread().unwrap().clone());
8868
8869        thread.read_with(cx, |thread, _cx| {
8870            assert_eq!(thread.status(), ThreadStatus::Idle);
8871        });
8872
8873        let focus_handle = thread_view.read_with(cx, |view, _cx| view.focus_handle.clone());
8874        cx.update(|window, cx| {
8875            window.focus(&focus_handle, cx);
8876        });
8877
8878        thread_view.update_in(cx, |_, window, cx| {
8879            window.dispatch_action(menu::Cancel.boxed_clone(), cx);
8880        });
8881
8882        cx.run_until_parked();
8883
8884        thread.read_with(cx, |thread, _cx| {
8885            assert_eq!(thread.status(), ThreadStatus::Idle);
8886        });
8887    }
8888
8889    #[gpui::test]
8890    async fn test_interrupt(cx: &mut TestAppContext) {
8891        init_test(cx);
8892
8893        let connection = StubAgentConnection::new();
8894
8895        let (thread_view, cx) =
8896            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
8897        add_to_workspace(thread_view.clone(), cx);
8898
8899        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
8900        message_editor.update_in(cx, |editor, window, cx| {
8901            editor.set_text("Message 1", window, cx);
8902        });
8903        thread_view.update_in(cx, |thread_view, window, cx| {
8904            thread_view.send(window, cx);
8905        });
8906
8907        let (thread, session_id) = thread_view.read_with(cx, |view, cx| {
8908            let thread = view.thread().unwrap();
8909
8910            (thread.clone(), thread.read(cx).session_id().clone())
8911        });
8912
8913        cx.run_until_parked();
8914
8915        cx.update(|_, cx| {
8916            connection.send_update(
8917                session_id.clone(),
8918                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
8919                    "Message 1 resp".into(),
8920                )),
8921                cx,
8922            );
8923        });
8924
8925        cx.run_until_parked();
8926
8927        thread.read_with(cx, |thread, cx| {
8928            assert_eq!(
8929                thread.to_markdown(cx),
8930                indoc::indoc! {"
8931                    ## User
8932
8933                    Message 1
8934
8935                    ## Assistant
8936
8937                    Message 1 resp
8938
8939                "}
8940            )
8941        });
8942
8943        message_editor.update_in(cx, |editor, window, cx| {
8944            editor.set_text("Message 2", window, cx);
8945        });
8946        thread_view.update_in(cx, |thread_view, window, cx| {
8947            thread_view.interrupt_and_send(window, cx);
8948        });
8949
8950        cx.update(|_, cx| {
8951            // Simulate a response sent after beginning to cancel
8952            connection.send_update(
8953                session_id.clone(),
8954                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("onse".into())),
8955                cx,
8956            );
8957        });
8958
8959        cx.run_until_parked();
8960
8961        // Last Message 1 response should appear before Message 2
8962        thread.read_with(cx, |thread, cx| {
8963            assert_eq!(
8964                thread.to_markdown(cx),
8965                indoc::indoc! {"
8966                    ## User
8967
8968                    Message 1
8969
8970                    ## Assistant
8971
8972                    Message 1 response
8973
8974                    ## User
8975
8976                    Message 2
8977
8978                "}
8979            )
8980        });
8981
8982        cx.update(|_, cx| {
8983            connection.send_update(
8984                session_id.clone(),
8985                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
8986                    "Message 2 response".into(),
8987                )),
8988                cx,
8989            );
8990            connection.end_turn(session_id.clone(), acp::StopReason::EndTurn);
8991        });
8992
8993        cx.run_until_parked();
8994
8995        thread.read_with(cx, |thread, cx| {
8996            assert_eq!(
8997                thread.to_markdown(cx),
8998                indoc::indoc! {"
8999                    ## User
9000
9001                    Message 1
9002
9003                    ## Assistant
9004
9005                    Message 1 response
9006
9007                    ## User
9008
9009                    Message 2
9010
9011                    ## Assistant
9012
9013                    Message 2 response
9014
9015                "}
9016            )
9017        });
9018    }
9019
9020    #[gpui::test]
9021    async fn test_message_editing_insert_selections(cx: &mut TestAppContext) {
9022        init_test(cx);
9023
9024        let connection = StubAgentConnection::new();
9025        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
9026            acp::ContentChunk::new("Response".into()),
9027        )]);
9028
9029        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
9030        add_to_workspace(thread_view.clone(), cx);
9031
9032        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
9033        message_editor.update_in(cx, |editor, window, cx| {
9034            editor.set_text("Original message to edit", window, cx)
9035        });
9036        thread_view.update_in(cx, |thread_view, window, cx| thread_view.send(window, cx));
9037        cx.run_until_parked();
9038
9039        let user_message_editor = thread_view.read_with(cx, |thread_view, cx| {
9040            thread_view
9041                .entry_view_state
9042                .read(cx)
9043                .entry(0)
9044                .expect("Should have at least one entry")
9045                .message_editor()
9046                .expect("Should have message editor")
9047                .clone()
9048        });
9049
9050        cx.focus(&user_message_editor);
9051        thread_view.read_with(cx, |thread_view, _cx| {
9052            assert_eq!(thread_view.editing_message, Some(0));
9053        });
9054
9055        // Ensure to edit the focused message before proceeding otherwise, since
9056        // its content is not different from what was sent, focus will be lost.
9057        user_message_editor.update_in(cx, |editor, window, cx| {
9058            editor.set_text("Original message to edit with ", window, cx)
9059        });
9060
9061        // Create a simple buffer with some text so we can create a selection
9062        // that will then be added to the message being edited.
9063        let (workspace, project) = thread_view.read_with(cx, |thread_view, _cx| {
9064            (thread_view.workspace.clone(), thread_view.project.clone())
9065        });
9066        let buffer = project.update(cx, |project, cx| {
9067            project.create_local_buffer("let a = 10 + 10;", None, false, cx)
9068        });
9069
9070        workspace
9071            .update_in(cx, |workspace, window, cx| {
9072                let editor = cx.new(|cx| {
9073                    let mut editor =
9074                        Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx);
9075
9076                    editor.change_selections(Default::default(), window, cx, |selections| {
9077                        selections.select_ranges([MultiBufferOffset(8)..MultiBufferOffset(15)]);
9078                    });
9079
9080                    editor
9081                });
9082                workspace.add_item_to_active_pane(Box::new(editor), None, false, window, cx);
9083            })
9084            .unwrap();
9085
9086        thread_view.update_in(cx, |thread_view, window, cx| {
9087            assert_eq!(thread_view.editing_message, Some(0));
9088            thread_view.insert_selections(window, cx);
9089        });
9090
9091        user_message_editor.read_with(cx, |editor, cx| {
9092            let text = editor.editor().read(cx).text(cx);
9093            let expected_text = String::from("Original message to edit with selection ");
9094
9095            assert_eq!(text, expected_text);
9096        });
9097    }
9098
9099    #[gpui::test]
9100    async fn test_insert_selections(cx: &mut TestAppContext) {
9101        init_test(cx);
9102
9103        let connection = StubAgentConnection::new();
9104        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
9105            acp::ContentChunk::new("Response".into()),
9106        )]);
9107
9108        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
9109        add_to_workspace(thread_view.clone(), cx);
9110
9111        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
9112        message_editor.update_in(cx, |editor, window, cx| {
9113            editor.set_text("Can you review this snippet ", window, cx)
9114        });
9115
9116        // Create a simple buffer with some text so we can create a selection
9117        // that will then be added to the message being edited.
9118        let (workspace, project) = thread_view.read_with(cx, |thread_view, _cx| {
9119            (thread_view.workspace.clone(), thread_view.project.clone())
9120        });
9121        let buffer = project.update(cx, |project, cx| {
9122            project.create_local_buffer("let a = 10 + 10;", None, false, cx)
9123        });
9124
9125        workspace
9126            .update_in(cx, |workspace, window, cx| {
9127                let editor = cx.new(|cx| {
9128                    let mut editor =
9129                        Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx);
9130
9131                    editor.change_selections(Default::default(), window, cx, |selections| {
9132                        selections.select_ranges([MultiBufferOffset(8)..MultiBufferOffset(15)]);
9133                    });
9134
9135                    editor
9136                });
9137                workspace.add_item_to_active_pane(Box::new(editor), None, false, window, cx);
9138            })
9139            .unwrap();
9140
9141        thread_view.update_in(cx, |thread_view, window, cx| {
9142            assert_eq!(thread_view.editing_message, None);
9143            thread_view.insert_selections(window, cx);
9144        });
9145
9146        thread_view.read_with(cx, |thread_view, cx| {
9147            let text = thread_view.message_editor.read(cx).text(cx);
9148            let expected_txt = String::from("Can you review this snippet selection ");
9149
9150            assert_eq!(text, expected_txt);
9151        })
9152    }
9153}