thread_view.rs

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