thread_view.rs

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