thread_view.rs

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