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