thread_view.rs

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