thread_view.rs

    1use acp_thread::{
    2    AcpThread, AcpThreadEvent, AgentSessionInfo, AgentThreadEntry, AssistantMessage,
    3    AssistantMessageChunk, AuthRequired, LoadError, MentionUri, PermissionOptionChoice,
    4    PermissionOptions, RetryStatus, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus,
    5    UserMessageId,
    6};
    7use acp_thread::{AgentConnection, Plan};
    8use action_log::{ActionLog, ActionLogTelemetry};
    9use agent::{NativeAgentServer, NativeAgentSessionList, SharedThread, ThreadStore};
   10use agent_client_protocol::{self as acp, PromptCapabilities};
   11use agent_servers::{AgentServer, AgentServerDelegate};
   12use agent_settings::{AgentProfileId, AgentSettings};
   13use anyhow::{Result, anyhow};
   14use arrayvec::ArrayVec;
   15use audio::{Audio, Sound};
   16use buffer_diff::BufferDiff;
   17use client::zed_urls;
   18use collections::{HashMap, HashSet};
   19use editor::scroll::Autoscroll;
   20use editor::{
   21    Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects, SizingBehavior,
   22};
   23use feature_flags::{
   24    AgentSharingFeatureFlag, AgentV2FeatureFlag, CloudThinkingToggleFeatureFlag,
   25    FeatureFlagAppExt as _, UserSlashCommandsFeatureFlag,
   26};
   27use file_icons::FileIcons;
   28use fs::Fs;
   29use futures::FutureExt as _;
   30use gpui::{
   31    Action, Animation, AnimationExt, AnyView, App, ClickEvent, ClipboardItem, CursorStyle,
   32    ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, ListOffset, ListState, ObjectFit,
   33    PlatformDisplay, ScrollHandle, SharedString, Subscription, Task, TextStyle, WeakEntity, Window,
   34    WindowHandle, div, ease_in_out, img, linear_color_stop, linear_gradient, list, point,
   35    pulsating_between,
   36};
   37use language::Buffer;
   38use language_model::LanguageModelRegistry;
   39use markdown::{Markdown, MarkdownElement, MarkdownFont, MarkdownStyle};
   40use project::{AgentServerStore, ExternalAgentServerName, Project, ProjectEntryId};
   41use prompt_store::{PromptId, PromptStore};
   42use rope::Point;
   43use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore};
   44use std::cell::RefCell;
   45use std::path::Path;
   46use std::sync::Arc;
   47use std::time::Instant;
   48use std::{collections::BTreeMap, rc::Rc, time::Duration};
   49use terminal_view::terminal_panel::TerminalPanel;
   50use text::{Anchor, ToPoint as _};
   51use theme::AgentFontSize;
   52use ui::{
   53    Callout, CommonAnimationExt, ContextMenu, ContextMenuEntry, CopyButton, DecoratedIcon,
   54    DiffStat, Disclosure, Divider, DividerColor, IconButtonShape, IconDecoration,
   55    IconDecorationKind, KeyBinding, PopoverMenu, PopoverMenuHandle, SpinnerLabel, TintColor,
   56    Tooltip, WithScrollbar, prelude::*, right_click_menu,
   57};
   58use util::defer;
   59use util::{ResultExt, size::format_file_size, time::duration_alt_display};
   60use workspace::{
   61    CollaboratorId, NewTerminal, OpenOptions, Toast, Workspace, notifications::NotificationId,
   62};
   63use zed_actions::agent::{Chat, ToggleModelSelector};
   64use zed_actions::assistant::OpenRulesLibrary;
   65
   66use super::config_options::ConfigOptionsView;
   67use super::entry_view_state::EntryViewState;
   68use super::thread_history::AcpThreadHistory;
   69use crate::acp::AcpModelSelectorPopover;
   70use crate::acp::ModeSelector;
   71use crate::acp::entry_view_state::{EntryViewEvent, ViewEvent};
   72use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
   73use crate::agent_diff::AgentDiff;
   74use crate::profile_selector::{ProfileProvider, ProfileSelector};
   75use crate::ui::{AgentNotification, AgentNotificationEvent};
   76use crate::user_slash_command::{
   77    self, CommandLoadError, SlashCommandRegistry, SlashCommandRegistryEvent, UserSlashCommand,
   78};
   79use crate::{
   80    AgentDiffPane, AgentPanel, AllowAlways, AllowOnce, AuthorizeToolCall, ClearMessageQueue,
   81    CycleFavoriteModels, CycleModeSelector, EditFirstQueuedMessage, ExpandMessageEditor,
   82    ExternalAgentInitialContent, Follow, KeepAll, NewThread, OpenAddContextMenu, OpenAgentDiff,
   83    OpenHistory, RejectAll, RejectOnce, RemoveFirstQueuedMessage, SelectPermissionGranularity,
   84    SendImmediately, SendNextQueuedMessage, ToggleProfileSelector, ToggleThinkingMode,
   85};
   86
   87const STOPWATCH_THRESHOLD: Duration = Duration::from_secs(30);
   88const TOKEN_THRESHOLD: u64 = 250;
   89
   90mod active_thread;
   91pub use active_thread::*;
   92
   93pub struct QueuedMessage {
   94    pub content: Vec<acp::ContentBlock>,
   95    pub tracked_buffers: Vec<Entity<Buffer>>,
   96}
   97
   98#[derive(Copy, Clone, Debug, PartialEq, Eq)]
   99enum ThreadFeedback {
  100    Positive,
  101    Negative,
  102}
  103
  104#[derive(Debug)]
  105enum ThreadError {
  106    PaymentRequired,
  107    Refusal,
  108    AuthenticationRequired(SharedString),
  109    Other {
  110        message: SharedString,
  111        acp_error_code: Option<SharedString>,
  112    },
  113}
  114
  115impl ThreadError {
  116    fn from_err(error: anyhow::Error, agent: &Rc<dyn AgentServer>) -> Self {
  117        if error.is::<language_model::PaymentRequiredError>() {
  118            Self::PaymentRequired
  119        } else if let Some(acp_error) = error.downcast_ref::<acp::Error>()
  120            && acp_error.code == acp::ErrorCode::AuthRequired
  121        {
  122            Self::AuthenticationRequired(acp_error.message.clone().into())
  123        } else {
  124            let message: SharedString = format!("{:#}", error).into();
  125
  126            // Extract ACP error code if available
  127            let acp_error_code = error
  128                .downcast_ref::<acp::Error>()
  129                .map(|acp_error| SharedString::from(acp_error.code.to_string()));
  130
  131            // TODO: we should have Gemini return better errors here.
  132            if agent.clone().downcast::<agent_servers::Gemini>().is_some()
  133                && message.contains("Could not load the default credentials")
  134                || message.contains("API key not valid")
  135                || message.contains("Request had invalid authentication credentials")
  136            {
  137                Self::AuthenticationRequired(message)
  138            } else {
  139                Self::Other {
  140                    message,
  141                    acp_error_code,
  142                }
  143            }
  144        }
  145    }
  146}
  147
  148impl ProfileProvider for Entity<agent::Thread> {
  149    fn profile_id(&self, cx: &App) -> AgentProfileId {
  150        self.read(cx).profile().clone()
  151    }
  152
  153    fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App) {
  154        self.update(cx, |thread, cx| {
  155            // Apply the profile and let the thread swap to its default model.
  156            thread.set_profile(profile_id, cx);
  157        });
  158    }
  159
  160    fn profiles_supported(&self, cx: &App) -> bool {
  161        self.read(cx)
  162            .model()
  163            .is_some_and(|model| model.supports_tools())
  164    }
  165}
  166
  167#[derive(Default)]
  168struct ThreadFeedbackState {
  169    feedback: Option<ThreadFeedback>,
  170    comments_editor: Option<Entity<Editor>>,
  171}
  172
  173impl ThreadFeedbackState {
  174    pub fn submit(
  175        &mut self,
  176        thread: Entity<AcpThread>,
  177        feedback: ThreadFeedback,
  178        window: &mut Window,
  179        cx: &mut App,
  180    ) {
  181        let Some(telemetry) = thread.read(cx).connection().telemetry() else {
  182            return;
  183        };
  184
  185        if self.feedback == Some(feedback) {
  186            return;
  187        }
  188
  189        self.feedback = Some(feedback);
  190        match feedback {
  191            ThreadFeedback::Positive => {
  192                self.comments_editor = None;
  193            }
  194            ThreadFeedback::Negative => {
  195                self.comments_editor = Some(Self::build_feedback_comments_editor(window, cx));
  196            }
  197        }
  198        let session_id = thread.read(cx).session_id().clone();
  199        let agent_telemetry_id = thread.read(cx).connection().telemetry_id();
  200        let task = telemetry.thread_data(&session_id, cx);
  201        let rating = match feedback {
  202            ThreadFeedback::Positive => "positive",
  203            ThreadFeedback::Negative => "negative",
  204        };
  205        cx.background_spawn(async move {
  206            let thread = task.await?;
  207            telemetry::event!(
  208                "Agent Thread Rated",
  209                agent = agent_telemetry_id,
  210                session_id = session_id,
  211                rating = rating,
  212                thread = thread
  213            );
  214            anyhow::Ok(())
  215        })
  216        .detach_and_log_err(cx);
  217    }
  218
  219    pub fn submit_comments(&mut self, thread: Entity<AcpThread>, cx: &mut App) {
  220        let Some(telemetry) = thread.read(cx).connection().telemetry() else {
  221            return;
  222        };
  223
  224        let Some(comments) = self
  225            .comments_editor
  226            .as_ref()
  227            .map(|editor| editor.read(cx).text(cx))
  228            .filter(|text| !text.trim().is_empty())
  229        else {
  230            return;
  231        };
  232
  233        self.comments_editor.take();
  234
  235        let session_id = thread.read(cx).session_id().clone();
  236        let agent_telemetry_id = thread.read(cx).connection().telemetry_id();
  237        let task = telemetry.thread_data(&session_id, cx);
  238        cx.background_spawn(async move {
  239            let thread = task.await?;
  240            telemetry::event!(
  241                "Agent Thread Feedback Comments",
  242                agent = agent_telemetry_id,
  243                session_id = session_id,
  244                comments = comments,
  245                thread = thread
  246            );
  247            anyhow::Ok(())
  248        })
  249        .detach_and_log_err(cx);
  250    }
  251
  252    pub fn clear(&mut self) {
  253        *self = Self::default()
  254    }
  255
  256    pub fn dismiss_comments(&mut self) {
  257        self.comments_editor.take();
  258    }
  259
  260    fn build_feedback_comments_editor(window: &mut Window, cx: &mut App) -> Entity<Editor> {
  261        let buffer = cx.new(|cx| {
  262            let empty_string = String::new();
  263            MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx)
  264        });
  265
  266        let editor = cx.new(|cx| {
  267            let mut editor = Editor::new(
  268                editor::EditorMode::AutoHeight {
  269                    min_lines: 1,
  270                    max_lines: Some(4),
  271                },
  272                buffer,
  273                None,
  274                window,
  275                cx,
  276            );
  277            editor.set_placeholder_text(
  278                "What went wrong? Share your feedback so we can improve.",
  279                window,
  280                cx,
  281            );
  282            editor
  283        });
  284
  285        editor.read(cx).focus_handle(cx).focus(window, cx);
  286        editor
  287    }
  288}
  289
  290#[derive(Default, Clone, Copy)]
  291struct DiffStats {
  292    lines_added: u32,
  293    lines_removed: u32,
  294}
  295
  296impl DiffStats {
  297    fn single_file(buffer: &Buffer, diff: &BufferDiff, cx: &App) -> Self {
  298        let mut stats = DiffStats::default();
  299        let diff_snapshot = diff.snapshot(cx);
  300        let buffer_snapshot = buffer.snapshot();
  301        let base_text = diff_snapshot.base_text();
  302
  303        for hunk in diff_snapshot.hunks(&buffer_snapshot) {
  304            let added_rows = hunk.range.end.row.saturating_sub(hunk.range.start.row);
  305            stats.lines_added += added_rows;
  306
  307            let base_start = hunk.diff_base_byte_range.start.to_point(base_text).row;
  308            let base_end = hunk.diff_base_byte_range.end.to_point(base_text).row;
  309            let removed_rows = base_end.saturating_sub(base_start);
  310            stats.lines_removed += removed_rows;
  311        }
  312
  313        stats
  314    }
  315
  316    fn all_files(changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>, cx: &App) -> Self {
  317        let mut total = DiffStats::default();
  318        for (buffer, diff) in changed_buffers {
  319            let stats = DiffStats::single_file(buffer.read(cx), diff.read(cx), cx);
  320            total.lines_added += stats.lines_added;
  321            total.lines_removed += stats.lines_removed;
  322        }
  323        total
  324    }
  325}
  326
  327pub struct AcpServerView {
  328    agent: Rc<dyn AgentServer>,
  329    agent_server_store: Entity<AgentServerStore>,
  330    workspace: WeakEntity<Workspace>,
  331    project: Entity<Project>,
  332    thread_store: Option<Entity<ThreadStore>>,
  333    prompt_store: Option<Entity<PromptStore>>,
  334    server_state: ServerState,
  335    login: Option<task::SpawnInTerminal>, // is some <=> Active | Unauthenticated
  336    recent_history_entries: Vec<AgentSessionInfo>,
  337    history: Entity<AcpThreadHistory>,
  338    _history_subscription: Subscription,
  339    hovered_recent_history_item: Option<usize>,
  340    message_editor: Entity<MessageEditor>,
  341    focus_handle: FocusHandle,
  342    notifications: Vec<WindowHandle<AgentNotification>>,
  343    notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
  344    slash_command_registry: Option<Entity<SlashCommandRegistry>>,
  345    auth_task: Option<Task<()>>,
  346    _subscriptions: Vec<Subscription>,
  347    show_codex_windows_warning: bool,
  348    in_flight_prompt: Option<Vec<acp::ContentBlock>>,
  349    add_context_menu_handle: PopoverMenuHandle<ContextMenu>,
  350}
  351
  352impl AcpServerView {
  353    pub fn as_active_thread(&self) -> Option<&AcpThreadView> {
  354        match &self.server_state {
  355            ServerState::Connected(connected) => connected.current.as_ref(),
  356            _ => None,
  357        }
  358    }
  359
  360    pub fn as_active_thread_mut(&mut self) -> Option<&mut AcpThreadView> {
  361        match &mut self.server_state {
  362            ServerState::Connected(connected) => connected.current.as_mut(),
  363            _ => None,
  364        }
  365    }
  366
  367    pub fn as_connected(&self) -> Option<&ConnectedServerState> {
  368        match &self.server_state {
  369            ServerState::Connected(connected) => Some(connected),
  370            _ => None,
  371        }
  372    }
  373
  374    pub fn as_connected_mut(&mut self) -> Option<&mut ConnectedServerState> {
  375        match &mut self.server_state {
  376            ServerState::Connected(connected) => Some(connected),
  377            _ => None,
  378        }
  379    }
  380}
  381
  382enum ServerState {
  383    Loading(Entity<LoadingView>),
  384    LoadError(LoadError),
  385    Connected(ConnectedServerState),
  386}
  387
  388// current -> Entity
  389// hashmap of threads, current becomes session_id
  390pub struct ConnectedServerState {
  391    auth_state: AuthState,
  392    current: Option<AcpThreadView>,
  393    connection: Rc<dyn AgentConnection>,
  394}
  395
  396enum AuthState {
  397    Ok,
  398    Unauthenticated {
  399        description: Option<Entity<Markdown>>,
  400        configuration_view: Option<AnyView>,
  401        pending_auth_method: Option<acp::AuthMethodId>,
  402        _subscription: Option<Subscription>,
  403    },
  404}
  405
  406impl AuthState {
  407    pub fn is_ok(&self) -> bool {
  408        matches!(self, Self::Ok)
  409    }
  410}
  411
  412struct LoadingView {
  413    title: SharedString,
  414    _load_task: Task<()>,
  415    _update_title_task: Task<anyhow::Result<()>>,
  416}
  417
  418impl ConnectedServerState {
  419    pub fn has_thread_error(&self) -> bool {
  420        self.current
  421            .as_ref()
  422            .map_or(false, |current| current.thread_error.is_some())
  423    }
  424}
  425
  426impl AcpServerView {
  427    pub fn new(
  428        agent: Rc<dyn AgentServer>,
  429        resume_thread: Option<AgentSessionInfo>,
  430        initial_content: Option<ExternalAgentInitialContent>,
  431        workspace: WeakEntity<Workspace>,
  432        project: Entity<Project>,
  433        thread_store: Option<Entity<ThreadStore>>,
  434        prompt_store: Option<Entity<PromptStore>>,
  435        history: Entity<AcpThreadHistory>,
  436        window: &mut Window,
  437        cx: &mut Context<Self>,
  438    ) -> Self {
  439        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
  440        let available_commands = Rc::new(RefCell::new(vec![]));
  441        let cached_user_commands = Rc::new(RefCell::new(collections::HashMap::default()));
  442        let cached_user_command_errors = Rc::new(RefCell::new(Vec::new()));
  443
  444        let agent_server_store = project.read(cx).agent_server_store().clone();
  445        let agent_display_name = agent_server_store
  446            .read(cx)
  447            .agent_display_name(&ExternalAgentServerName(agent.name()))
  448            .unwrap_or_else(|| agent.name());
  449
  450        let placeholder = placeholder_text(agent_display_name.as_ref(), false);
  451
  452        let message_editor = cx.new(|cx| {
  453            let mut editor = MessageEditor::new_with_cache(
  454                workspace.clone(),
  455                project.downgrade(),
  456                thread_store.clone(),
  457                history.downgrade(),
  458                prompt_store.clone(),
  459                prompt_capabilities.clone(),
  460                available_commands.clone(),
  461                cached_user_commands.clone(),
  462                cached_user_command_errors.clone(),
  463                agent.name(),
  464                &placeholder,
  465                editor::EditorMode::AutoHeight {
  466                    min_lines: AgentSettings::get_global(cx).message_editor_min_lines,
  467                    max_lines: Some(AgentSettings::get_global(cx).set_message_editor_max_lines()),
  468                },
  469                window,
  470                cx,
  471            );
  472            if let Some(content) = initial_content {
  473                match content {
  474                    ExternalAgentInitialContent::ThreadSummary(entry) => {
  475                        editor.insert_thread_summary(entry, window, cx);
  476                    }
  477                    ExternalAgentInitialContent::Text(prompt) => {
  478                        editor.set_message(
  479                            vec![acp::ContentBlock::Text(acp::TextContent::new(prompt))],
  480                            window,
  481                            cx,
  482                        );
  483                    }
  484                }
  485            }
  486            editor
  487        });
  488
  489        let subscriptions = vec![
  490            cx.observe_global_in::<SettingsStore>(window, Self::agent_ui_font_size_changed),
  491            cx.observe_global_in::<AgentFontSize>(window, Self::agent_ui_font_size_changed),
  492            cx.subscribe_in(&message_editor, window, Self::handle_message_editor_event),
  493            cx.subscribe_in(
  494                &agent_server_store,
  495                window,
  496                Self::handle_agent_servers_updated,
  497            ),
  498        ];
  499
  500        cx.on_release(|this, cx| {
  501            for window in this.notifications.drain(..) {
  502                window
  503                    .update(cx, |_, window, _| {
  504                        window.remove_window();
  505                    })
  506                    .ok();
  507            }
  508        })
  509        .detach();
  510
  511        let show_codex_windows_warning = cfg!(windows)
  512            && project.read(cx).is_local()
  513            && agent.clone().downcast::<agent_servers::Codex>().is_some();
  514
  515        // Create SlashCommandRegistry to cache user-defined slash commands and watch for changes
  516        let slash_command_registry = if cx.has_flag::<UserSlashCommandsFeatureFlag>() {
  517            let fs = project.read(cx).fs().clone();
  518            let worktree_roots: Vec<std::path::PathBuf> = project
  519                .read(cx)
  520                .visible_worktrees(cx)
  521                .map(|worktree| worktree.read(cx).abs_path().to_path_buf())
  522                .collect();
  523            let registry = cx.new(|cx| SlashCommandRegistry::new(fs, worktree_roots, cx));
  524
  525            // Subscribe to registry changes to update error display and cached commands
  526            cx.subscribe(&registry, move |this, registry, event, cx| match event {
  527                SlashCommandRegistryEvent::CommandsChanged => {
  528                    this.refresh_cached_user_commands_from_registry(&registry, cx);
  529                }
  530            })
  531            .detach();
  532
  533            // Initialize cached commands and errors from registry
  534            let mut commands = registry.read(cx).commands().clone();
  535            let mut errors = registry.read(cx).errors().to_vec();
  536            let server_command_names = available_commands
  537                .borrow()
  538                .iter()
  539                .map(|command| command.name.clone())
  540                .collect::<HashSet<_>>();
  541            user_slash_command::apply_server_command_conflicts_to_map(
  542                &mut commands,
  543                &mut errors,
  544                &server_command_names,
  545            );
  546            *cached_user_commands.borrow_mut() = commands;
  547            *cached_user_command_errors.borrow_mut() = errors;
  548
  549            Some(registry)
  550        } else {
  551            None
  552        };
  553
  554        let recent_history_entries = history.read(cx).get_recent_sessions(3);
  555        let history_subscription = cx.observe(&history, |this, history, cx| {
  556            this.update_recent_history_from_cache(&history, cx);
  557        });
  558
  559        Self {
  560            agent: agent.clone(),
  561            agent_server_store,
  562            workspace: workspace.clone(),
  563            project: project.clone(),
  564            thread_store,
  565            prompt_store,
  566            server_state: Self::initial_state(
  567                agent.clone(),
  568                resume_thread,
  569                workspace.clone(),
  570                project.clone(),
  571                prompt_capabilities,
  572                available_commands,
  573                cached_user_commands,
  574                cached_user_command_errors,
  575                window,
  576                cx,
  577            ),
  578            login: None,
  579            message_editor,
  580            notifications: Vec::new(),
  581            notification_subscriptions: HashMap::default(),
  582            slash_command_registry,
  583            auth_task: None,
  584            recent_history_entries,
  585            history,
  586            _history_subscription: history_subscription,
  587            hovered_recent_history_item: None,
  588            _subscriptions: subscriptions,
  589            focus_handle: cx.focus_handle(),
  590            show_codex_windows_warning,
  591            in_flight_prompt: None,
  592            add_context_menu_handle: PopoverMenuHandle::default(),
  593        }
  594    }
  595
  596    fn reset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
  597        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
  598        let available_commands = Rc::new(RefCell::new(vec![]));
  599        let cached_user_commands = Rc::new(RefCell::new(collections::HashMap::default()));
  600        let cached_user_command_errors = Rc::new(RefCell::new(Vec::new()));
  601
  602        let resume_thread_metadata = self
  603            .as_active_thread()
  604            .and_then(|thread| thread.resume_thread_metadata.clone());
  605
  606        self.message_editor.update(cx, |editor, cx| {
  607            editor.set_command_state(
  608                prompt_capabilities.clone(),
  609                available_commands.clone(),
  610                cached_user_commands.clone(),
  611                cached_user_command_errors.clone(),
  612                cx,
  613            );
  614        });
  615
  616        self.server_state = Self::initial_state(
  617            self.agent.clone(),
  618            resume_thread_metadata,
  619            self.workspace.clone(),
  620            self.project.clone(),
  621            prompt_capabilities,
  622            available_commands,
  623            cached_user_commands,
  624            cached_user_command_errors,
  625            window,
  626            cx,
  627        );
  628        self.refresh_cached_user_commands(cx);
  629        self.recent_history_entries.clear();
  630        cx.notify();
  631    }
  632
  633    fn initial_state(
  634        agent: Rc<dyn AgentServer>,
  635        resume_thread: Option<AgentSessionInfo>,
  636        workspace: WeakEntity<Workspace>,
  637        project: Entity<Project>,
  638        prompt_capabilities: Rc<RefCell<PromptCapabilities>>,
  639        available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
  640        cached_user_commands: Rc<RefCell<HashMap<String, UserSlashCommand>>>,
  641        cached_user_command_errors: Rc<RefCell<Vec<CommandLoadError>>>,
  642        window: &mut Window,
  643        cx: &mut Context<Self>,
  644    ) -> ServerState {
  645        if project.read(cx).is_via_collab()
  646            && agent.clone().downcast::<NativeAgentServer>().is_none()
  647        {
  648            return ServerState::LoadError(LoadError::Other(
  649                "External agents are not yet supported in shared projects.".into(),
  650            ));
  651        }
  652        let mut worktrees = project.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
  653        // Pick the first non-single-file worktree for the root directory if there are any,
  654        // and otherwise the parent of a single-file worktree, falling back to $HOME if there are no visible worktrees.
  655        worktrees.sort_by(|l, r| {
  656            l.read(cx)
  657                .is_single_file()
  658                .cmp(&r.read(cx).is_single_file())
  659        });
  660        let root_dir = worktrees
  661            .into_iter()
  662            .filter_map(|worktree| {
  663                if worktree.read(cx).is_single_file() {
  664                    Some(worktree.read(cx).abs_path().parent()?.into())
  665                } else {
  666                    Some(worktree.read(cx).abs_path())
  667                }
  668            })
  669            .next();
  670        let fallback_cwd = root_dir
  671            .clone()
  672            .unwrap_or_else(|| paths::home_dir().as_path().into());
  673        let (status_tx, mut status_rx) = watch::channel("Loading…".into());
  674        let (new_version_available_tx, mut new_version_available_rx) = watch::channel(None);
  675        let delegate = AgentServerDelegate::new(
  676            project.read(cx).agent_server_store().clone(),
  677            project.clone(),
  678            Some(status_tx),
  679            Some(new_version_available_tx),
  680        );
  681
  682        let connect_task = agent.connect(root_dir.as_deref(), delegate, cx);
  683        let load_task = cx.spawn_in(window, async move |this, cx| {
  684            let connection = match connect_task.await {
  685                Ok((connection, login)) => {
  686                    this.update(cx, |this, _| this.login = login).ok();
  687                    connection
  688                }
  689                Err(err) => {
  690                    this.update_in(cx, |this, window, cx| {
  691                        if err.downcast_ref::<LoadError>().is_some() {
  692                            this.handle_load_error(err, window, cx);
  693                        } else {
  694                            this.handle_thread_error(err, cx);
  695                        }
  696                        cx.notify();
  697                    })
  698                    .log_err();
  699                    return;
  700                }
  701            };
  702
  703            telemetry::event!("Agent Thread Started", agent = connection.telemetry_id());
  704
  705            let mut resumed_without_history = false;
  706            let result = if let Some(resume) = resume_thread.clone() {
  707                cx.update(|_, cx| {
  708                    let session_cwd = resume
  709                        .cwd
  710                        .clone()
  711                        .unwrap_or_else(|| fallback_cwd.as_ref().to_path_buf());
  712                    if connection.supports_load_session(cx) {
  713                        connection.clone().load_session(
  714                            resume,
  715                            project.clone(),
  716                            session_cwd.as_path(),
  717                            cx,
  718                        )
  719                    } else if connection.supports_resume_session(cx) {
  720                        resumed_without_history = true;
  721                        connection.clone().resume_session(
  722                            resume,
  723                            project.clone(),
  724                            session_cwd.as_path(),
  725                            cx,
  726                        )
  727                    } else {
  728                        Task::ready(Err(anyhow!(LoadError::Other(
  729                            "Loading or resuming sessions is not supported by this agent.".into()
  730                        ))))
  731                    }
  732                })
  733                .log_err()
  734            } else {
  735                cx.update(|_, cx| {
  736                    connection
  737                        .clone()
  738                        .new_thread(project.clone(), fallback_cwd.as_ref(), cx)
  739                })
  740                .log_err()
  741            };
  742
  743            let Some(result) = result else {
  744                return;
  745            };
  746
  747            let result = match result.await {
  748                Err(e) => match e.downcast::<acp_thread::AuthRequired>() {
  749                    Ok(err) => {
  750                        cx.update(|window, cx| {
  751                            Self::handle_auth_required(
  752                                this,
  753                                err,
  754                                agent.name(),
  755                                connection.clone(),
  756                                window,
  757                                cx,
  758                            )
  759                        })
  760                        .log_err();
  761                        return;
  762                    }
  763                    Err(err) => Err(err),
  764                },
  765                Ok(thread) => Ok(thread),
  766            };
  767
  768            this.update_in(cx, |this, window, cx| {
  769                match result {
  770                    Ok(thread) => {
  771                        let action_log = thread.read(cx).action_log().clone();
  772
  773                        prompt_capabilities.replace(thread.read(cx).prompt_capabilities());
  774
  775                        let entry_view_state = cx.new(|_| {
  776                            EntryViewState::new(
  777                                this.workspace.clone(),
  778                                this.project.downgrade(),
  779                                this.thread_store.clone(),
  780                                this.history.downgrade(),
  781                                this.prompt_store.clone(),
  782                                prompt_capabilities.clone(),
  783                                available_commands.clone(),
  784                                cached_user_commands.clone(),
  785                                cached_user_command_errors.clone(),
  786                                this.agent.name(),
  787                            )
  788                        });
  789
  790                        let count = thread.read(cx).entries().len();
  791                        let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0));
  792                        entry_view_state.update(cx, |view_state, cx| {
  793                            for ix in 0..count {
  794                                view_state.sync_entry(ix, &thread, window, cx);
  795                            }
  796                            list_state.splice_focusable(
  797                                0..0,
  798                                (0..count).map(|ix| view_state.entry(ix)?.focus_handle(cx)),
  799                            );
  800                        });
  801
  802                        AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
  803
  804                        let connection = thread.read(cx).connection().clone();
  805                        let session_id = thread.read(cx).session_id().clone();
  806                        let session_list = if connection.supports_session_history(cx) {
  807                            connection.session_list(cx)
  808                        } else {
  809                            None
  810                        };
  811                        this.history.update(cx, |history, cx| {
  812                            history.set_session_list(session_list, cx);
  813                        });
  814
  815                        // Check for config options first
  816                        // Config options take precedence over legacy mode/model selectors
  817                        // (feature flag gating happens at the data layer)
  818                        let config_options_provider =
  819                            connection.session_config_options(&session_id, cx);
  820
  821                        let config_options_view;
  822                        let mode_selector;
  823                        let model_selector;
  824                        if let Some(config_options) = config_options_provider {
  825                            // Use config options - don't create mode_selector or model_selector
  826                            let agent_server = this.agent.clone();
  827                            let fs = this.project.read(cx).fs().clone();
  828                            config_options_view = Some(cx.new(|cx| {
  829                                ConfigOptionsView::new(config_options, agent_server, fs, window, cx)
  830                            }));
  831                            model_selector = None;
  832                            mode_selector = None;
  833                        } else {
  834                            // Fall back to legacy mode/model selectors
  835                            config_options_view = None;
  836                            model_selector =
  837                                connection.model_selector(&session_id).map(|selector| {
  838                                    let agent_server = this.agent.clone();
  839                                    let fs = this.project.read(cx).fs().clone();
  840                                    cx.new(|cx| {
  841                                        AcpModelSelectorPopover::new(
  842                                            selector,
  843                                            agent_server,
  844                                            fs,
  845                                            PopoverMenuHandle::default(),
  846                                            this.focus_handle(cx),
  847                                            window,
  848                                            cx,
  849                                        )
  850                                    })
  851                                });
  852
  853                            mode_selector =
  854                                connection
  855                                    .session_modes(&session_id, cx)
  856                                    .map(|session_modes| {
  857                                        let fs = this.project.read(cx).fs().clone();
  858                                        let focus_handle = this.focus_handle(cx);
  859                                        cx.new(|_cx| {
  860                                            ModeSelector::new(
  861                                                session_modes,
  862                                                this.agent.clone(),
  863                                                fs,
  864                                                focus_handle,
  865                                            )
  866                                        })
  867                                    });
  868                        }
  869
  870                        let mut subscriptions = vec![
  871                            cx.subscribe_in(&thread, window, Self::handle_thread_event),
  872                            cx.observe(&action_log, |_, _, cx| cx.notify()),
  873                            cx.subscribe_in(
  874                                &entry_view_state,
  875                                window,
  876                                Self::handle_entry_view_event,
  877                            ),
  878                        ];
  879
  880                        let title_editor =
  881                            if thread.update(cx, |thread, cx| thread.can_set_title(cx)) {
  882                                let editor = cx.new(|cx| {
  883                                    let mut editor = Editor::single_line(window, cx);
  884                                    editor.set_text(thread.read(cx).title(), window, cx);
  885                                    editor
  886                                });
  887                                subscriptions.push(cx.subscribe_in(
  888                                    &editor,
  889                                    window,
  890                                    Self::handle_title_editor_event,
  891                                ));
  892                                Some(editor)
  893                            } else {
  894                                None
  895                            };
  896
  897                        let profile_selector: Option<Rc<agent::NativeAgentConnection>> =
  898                            connection.clone().downcast();
  899                        let profile_selector = profile_selector
  900                            .and_then(|native_connection| native_connection.thread(&session_id, cx))
  901                            .map(|native_thread| {
  902                                cx.new(|cx| {
  903                                    ProfileSelector::new(
  904                                        <dyn Fs>::global(cx),
  905                                        Arc::new(native_thread),
  906                                        this.focus_handle(cx),
  907                                        cx,
  908                                    )
  909                                })
  910                            });
  911
  912                        this.server_state = ServerState::Connected(ConnectedServerState {
  913                            connection,
  914                            auth_state: AuthState::Ok,
  915                            current: Some(AcpThreadView::new(
  916                                thread,
  917                                workspace.clone(),
  918                                entry_view_state,
  919                                title_editor,
  920                                config_options_view,
  921                                mode_selector,
  922                                model_selector,
  923                                profile_selector,
  924                                list_state,
  925                                prompt_capabilities,
  926                                available_commands,
  927                                cached_user_commands,
  928                                cached_user_command_errors,
  929                                resumed_without_history,
  930                                resume_thread.clone(),
  931                                subscriptions,
  932                                cx,
  933                            )),
  934                        });
  935
  936                        if this.focus_handle.contains_focused(window, cx) {
  937                            this.message_editor.focus_handle(cx).focus(window, cx);
  938                        }
  939
  940                        cx.notify();
  941                    }
  942                    Err(err) => {
  943                        this.handle_load_error(err, window, cx);
  944                    }
  945                };
  946            })
  947            .log_err();
  948        });
  949
  950        cx.spawn(async move |this, cx| {
  951            while let Ok(new_version) = new_version_available_rx.recv().await {
  952                if let Some(new_version) = new_version {
  953                    this.update(cx, |this, cx| {
  954                        if let Some(thread) = this.as_active_thread_mut() {
  955                            thread.new_server_version_available = Some(new_version.into());
  956                        }
  957                        cx.notify();
  958                    })
  959                    .ok();
  960                }
  961            }
  962        })
  963        .detach();
  964
  965        let loading_view = cx.new(|cx| {
  966            let update_title_task = cx.spawn(async move |this, cx| {
  967                loop {
  968                    let status = status_rx.recv().await?;
  969                    this.update(cx, |this: &mut LoadingView, cx| {
  970                        this.title = status;
  971                        cx.notify();
  972                    })?;
  973                }
  974            });
  975
  976            LoadingView {
  977                title: "Loading…".into(),
  978                _load_task: load_task,
  979                _update_title_task: update_title_task,
  980            }
  981        });
  982
  983        ServerState::Loading(loading_view)
  984    }
  985
  986    fn handle_auth_required(
  987        this: WeakEntity<Self>,
  988        err: AuthRequired,
  989        agent_name: SharedString,
  990        connection: Rc<dyn AgentConnection>,
  991        window: &mut Window,
  992        cx: &mut App,
  993    ) {
  994        let (configuration_view, subscription) = if let Some(provider_id) = &err.provider_id {
  995            let registry = LanguageModelRegistry::global(cx);
  996
  997            let sub = window.subscribe(&registry, cx, {
  998                let provider_id = provider_id.clone();
  999                let this = this.clone();
 1000                move |_, ev, window, cx| {
 1001                    if let language_model::Event::ProviderStateChanged(updated_provider_id) = &ev
 1002                        && &provider_id == updated_provider_id
 1003                        && LanguageModelRegistry::global(cx)
 1004                            .read(cx)
 1005                            .provider(&provider_id)
 1006                            .map_or(false, |provider| provider.is_authenticated(cx))
 1007                    {
 1008                        this.update(cx, |this, cx| {
 1009                            this.reset(window, cx);
 1010                        })
 1011                        .ok();
 1012                    }
 1013                }
 1014            });
 1015
 1016            let view = registry.read(cx).provider(&provider_id).map(|provider| {
 1017                provider.configuration_view(
 1018                    language_model::ConfigurationViewTargetAgent::Other(agent_name),
 1019                    window,
 1020                    cx,
 1021                )
 1022            });
 1023
 1024            (view, Some(sub))
 1025        } else {
 1026            (None, None)
 1027        };
 1028
 1029        this.update(cx, |this, cx| {
 1030            let description = err
 1031                .description
 1032                .map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx)));
 1033
 1034            let auth_state = AuthState::Unauthenticated {
 1035                pending_auth_method: None,
 1036                configuration_view,
 1037                description,
 1038                _subscription: subscription,
 1039            };
 1040
 1041            if let Some(connected) = this.as_connected_mut() {
 1042                connected.auth_state = auth_state;
 1043            } else {
 1044                this.server_state = ServerState::Connected(ConnectedServerState {
 1045                    auth_state,
 1046                    current: None,
 1047                    connection,
 1048                })
 1049            }
 1050            if this.message_editor.focus_handle(cx).is_focused(window) {
 1051                this.focus_handle.focus(window, cx)
 1052            }
 1053            cx.notify();
 1054        })
 1055        .ok();
 1056    }
 1057
 1058    fn handle_load_error(
 1059        &mut self,
 1060        err: anyhow::Error,
 1061        window: &mut Window,
 1062        cx: &mut Context<Self>,
 1063    ) {
 1064        if let Some(load_err) = err.downcast_ref::<LoadError>() {
 1065            self.server_state = ServerState::LoadError(load_err.clone());
 1066        } else {
 1067            self.server_state =
 1068                ServerState::LoadError(LoadError::Other(format!("{:#}", err).into()))
 1069        }
 1070        if self.message_editor.focus_handle(cx).is_focused(window) {
 1071            self.focus_handle.focus(window, cx)
 1072        }
 1073        cx.notify();
 1074    }
 1075
 1076    fn handle_agent_servers_updated(
 1077        &mut self,
 1078        _agent_server_store: &Entity<project::AgentServerStore>,
 1079        _event: &project::AgentServersUpdated,
 1080        window: &mut Window,
 1081        cx: &mut Context<Self>,
 1082    ) {
 1083        // If we're in a LoadError state OR have a thread_error set (which can happen
 1084        // when agent.connect() fails during loading), retry loading the thread.
 1085        // This handles the case where a thread is restored before authentication completes.
 1086        let should_retry = match &self.server_state {
 1087            ServerState::Loading(_) => false,
 1088            ServerState::LoadError(_) => true,
 1089            ServerState::Connected(connected) => {
 1090                connected.auth_state.is_ok() && connected.has_thread_error()
 1091            }
 1092        };
 1093
 1094        if should_retry {
 1095            if let Some(active) = self.as_active_thread_mut() {
 1096                active.thread_error = None;
 1097                active.thread_error_markdown = None;
 1098            }
 1099            self.reset(window, cx);
 1100        }
 1101    }
 1102
 1103    pub fn workspace(&self) -> &WeakEntity<Workspace> {
 1104        &self.workspace
 1105    }
 1106
 1107    pub fn title(&self, cx: &App) -> SharedString {
 1108        match &self.server_state {
 1109            ServerState::Connected(_) => "New Thread".into(),
 1110            ServerState::Loading(loading_view) => loading_view.read(cx).title.clone(),
 1111            ServerState::LoadError(error) => match error {
 1112                LoadError::Unsupported { .. } => format!("Upgrade {}", self.agent.name()).into(),
 1113                LoadError::FailedToInstall(_) => {
 1114                    format!("Failed to Install {}", self.agent.name()).into()
 1115                }
 1116                LoadError::Exited { .. } => format!("{} Exited", self.agent.name()).into(),
 1117                LoadError::Other(_) => format!("Error Loading {}", self.agent.name()).into(),
 1118            },
 1119        }
 1120    }
 1121
 1122    pub fn cancel_generation(&mut self, cx: &mut Context<Self>) {
 1123        if let Some(active) = self.as_active_thread_mut() {
 1124            active.cancel_generation(cx);
 1125        }
 1126    }
 1127
 1128    fn share_thread(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
 1129        let Some(thread) = self.as_native_thread(cx) else {
 1130            return;
 1131        };
 1132
 1133        let client = self.project.read(cx).client();
 1134        let workspace = self.workspace.clone();
 1135        let session_id = thread.read(cx).id().to_string();
 1136
 1137        let load_task = thread.read(cx).to_db(cx);
 1138
 1139        cx.spawn(async move |_this, cx| {
 1140            let db_thread = load_task.await;
 1141
 1142            let shared_thread = SharedThread::from_db_thread(&db_thread);
 1143            let thread_data = shared_thread.to_bytes()?;
 1144            let title = shared_thread.title.to_string();
 1145
 1146            client
 1147                .request(proto::ShareAgentThread {
 1148                    session_id: session_id.clone(),
 1149                    title,
 1150                    thread_data,
 1151                })
 1152                .await?;
 1153
 1154            let share_url = client::zed_urls::shared_agent_thread_url(&session_id);
 1155
 1156            cx.update(|cx| {
 1157                if let Some(workspace) = workspace.upgrade() {
 1158                    workspace.update(cx, |workspace, cx| {
 1159                        struct ThreadSharedToast;
 1160                        workspace.show_toast(
 1161                            Toast::new(
 1162                                NotificationId::unique::<ThreadSharedToast>(),
 1163                                "Thread shared!",
 1164                            )
 1165                            .on_click(
 1166                                "Copy URL",
 1167                                move |_window, cx| {
 1168                                    cx.write_to_clipboard(ClipboardItem::new_string(
 1169                                        share_url.clone(),
 1170                                    ));
 1171                                },
 1172                            ),
 1173                            cx,
 1174                        );
 1175                    });
 1176                }
 1177            });
 1178
 1179            anyhow::Ok(())
 1180        })
 1181        .detach_and_log_err(cx);
 1182    }
 1183
 1184    fn sync_thread(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 1185        if !self.is_imported_thread(cx) {
 1186            return;
 1187        }
 1188
 1189        let Some(active) = self.as_active_thread() else {
 1190            return;
 1191        };
 1192
 1193        let Some(session_list) = self
 1194            .as_native_connection(cx)
 1195            .and_then(|connection| connection.session_list(cx))
 1196            .and_then(|list| list.downcast::<NativeAgentSessionList>())
 1197        else {
 1198            return;
 1199        };
 1200        let thread_store = session_list.thread_store().clone();
 1201
 1202        let client = self.project.read(cx).client();
 1203        let session_id = active.thread.read(cx).session_id().clone();
 1204
 1205        cx.spawn_in(window, async move |this, cx| {
 1206            let response = client
 1207                .request(proto::GetSharedAgentThread {
 1208                    session_id: session_id.to_string(),
 1209                })
 1210                .await?;
 1211
 1212            let shared_thread = SharedThread::from_bytes(&response.thread_data)?;
 1213
 1214            let db_thread = shared_thread.to_db_thread();
 1215
 1216            thread_store
 1217                .update(&mut cx.clone(), |store, cx| {
 1218                    store.save_thread(session_id.clone(), db_thread, cx)
 1219                })
 1220                .await?;
 1221
 1222            let thread_metadata = AgentSessionInfo {
 1223                session_id,
 1224                cwd: None,
 1225                title: Some(format!("🔗 {}", response.title).into()),
 1226                updated_at: Some(chrono::Utc::now()),
 1227                meta: None,
 1228            };
 1229
 1230            this.update_in(cx, |this, window, cx| {
 1231                if let Some(active) = this.as_active_thread_mut() {
 1232                    active.resume_thread_metadata = Some(thread_metadata);
 1233                }
 1234                this.reset(window, cx);
 1235            })?;
 1236
 1237            this.update_in(cx, |this, _window, cx| {
 1238                if let Some(workspace) = this.workspace.upgrade() {
 1239                    workspace.update(cx, |workspace, cx| {
 1240                        struct ThreadSyncedToast;
 1241                        workspace.show_toast(
 1242                            Toast::new(
 1243                                NotificationId::unique::<ThreadSyncedToast>(),
 1244                                "Thread synced with latest version",
 1245                            )
 1246                            .autohide(),
 1247                            cx,
 1248                        );
 1249                    });
 1250                }
 1251            })?;
 1252
 1253            anyhow::Ok(())
 1254        })
 1255        .detach_and_log_err(cx);
 1256    }
 1257
 1258    pub fn expand_message_editor(
 1259        &mut self,
 1260        _: &ExpandMessageEditor,
 1261        _window: &mut Window,
 1262        cx: &mut Context<Self>,
 1263    ) {
 1264        let editor = self.message_editor.clone();
 1265        if let Some(active) = self.as_active_thread_mut() {
 1266            active.expand_message_editor(editor, cx);
 1267        }
 1268    }
 1269
 1270    fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {
 1271        let editor = self.message_editor.clone();
 1272        if let Some(active) = self.as_active_thread_mut() {
 1273            active.set_editor_is_expanded(is_expanded, editor, cx);
 1274        }
 1275    }
 1276
 1277    pub fn handle_title_editor_event(
 1278        &mut self,
 1279        title_editor: &Entity<Editor>,
 1280        event: &EditorEvent,
 1281        window: &mut Window,
 1282        cx: &mut Context<Self>,
 1283    ) {
 1284        if let Some(active) = self.as_active_thread_mut() {
 1285            active.handle_title_editor_event(title_editor, event, window, cx);
 1286        }
 1287    }
 1288
 1289    pub fn handle_message_editor_event(
 1290        &mut self,
 1291        _: &Entity<MessageEditor>,
 1292        event: &MessageEditorEvent,
 1293        window: &mut Window,
 1294        cx: &mut Context<Self>,
 1295    ) {
 1296        match event {
 1297            MessageEditorEvent::Send => self.send(window, cx),
 1298            MessageEditorEvent::SendImmediately => self.interrupt_and_send(window, cx),
 1299            MessageEditorEvent::Cancel => self.cancel_generation(cx),
 1300            MessageEditorEvent::Focus => {
 1301                self.cancel_editing(&Default::default(), window, cx);
 1302            }
 1303            MessageEditorEvent::LostFocus => {}
 1304        }
 1305    }
 1306
 1307    pub fn handle_entry_view_event(
 1308        &mut self,
 1309        _: &Entity<EntryViewState>,
 1310        event: &EntryViewEvent,
 1311        window: &mut Window,
 1312        cx: &mut Context<Self>,
 1313    ) {
 1314        match &event.view_event {
 1315            ViewEvent::NewDiff(tool_call_id) => {
 1316                if AgentSettings::get_global(cx).expand_edit_card {
 1317                    if let Some(active) = self.as_active_thread_mut() {
 1318                        active.expanded_tool_calls.insert(tool_call_id.clone());
 1319                    }
 1320                }
 1321            }
 1322            ViewEvent::NewTerminal(tool_call_id) => {
 1323                if AgentSettings::get_global(cx).expand_terminal_card {
 1324                    if let Some(active) = self.as_active_thread_mut() {
 1325                        active.expanded_tool_calls.insert(tool_call_id.clone());
 1326                    }
 1327                }
 1328            }
 1329            ViewEvent::TerminalMovedToBackground(tool_call_id) => {
 1330                if let Some(active) = self.as_active_thread_mut() {
 1331                    active.expanded_tool_calls.remove(tool_call_id);
 1332                }
 1333            }
 1334            ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => {
 1335                if let Some(active) = self.as_active_thread()
 1336                    && let Some(AgentThreadEntry::UserMessage(user_message)) =
 1337                        active.thread.read(cx).entries().get(event.entry_index)
 1338                    && user_message.id.is_some()
 1339                {
 1340                    if let Some(active) = self.as_active_thread_mut() {
 1341                        active.editing_message = Some(event.entry_index);
 1342                    }
 1343                    cx.notify();
 1344                }
 1345            }
 1346            ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::LostFocus) => {
 1347                if let Some(active) = self.as_active_thread()
 1348                    && let Some(AgentThreadEntry::UserMessage(user_message)) =
 1349                        active.thread.read(cx).entries().get(event.entry_index)
 1350                    && user_message.id.is_some()
 1351                {
 1352                    if editor.read(cx).text(cx).as_str() == user_message.content.to_markdown(cx) {
 1353                        if let Some(active) = self.as_active_thread_mut() {
 1354                            active.editing_message = None;
 1355                        }
 1356                        cx.notify();
 1357                    }
 1358                }
 1359            }
 1360            ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::SendImmediately) => {}
 1361            ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => {
 1362                self.regenerate(event.entry_index, editor.clone(), window, cx);
 1363            }
 1364            ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Cancel) => {
 1365                self.cancel_editing(&Default::default(), window, cx);
 1366            }
 1367        }
 1368    }
 1369
 1370    pub fn is_loading(&self) -> bool {
 1371        matches!(self.server_state, ServerState::Loading { .. })
 1372    }
 1373
 1374    fn retry_generation(&mut self, cx: &mut Context<Self>) {
 1375        if let Some(active) = self.as_active_thread_mut() {
 1376            active.retry_generation(cx);
 1377        };
 1378    }
 1379
 1380    fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 1381        let message_editor = self.message_editor.clone();
 1382        let login = self.login.clone();
 1383        let agent_name = self.agent.name();
 1384
 1385        if let Some(active) = self.as_active_thread_mut() {
 1386            active.send(message_editor, agent_name, login, window, cx);
 1387        }
 1388    }
 1389
 1390    fn interrupt_and_send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 1391        let message_editor = self.message_editor.clone();
 1392        if let Some(active) = self.as_active_thread_mut() {
 1393            active.interrupt_and_send(message_editor, window, cx);
 1394        };
 1395    }
 1396
 1397    fn start_turn(&mut self, cx: &mut Context<Self>) -> usize {
 1398        self.as_active_thread_mut()
 1399            .map(|active| active.start_turn(cx))
 1400            .unwrap_or(0)
 1401    }
 1402
 1403    fn stop_turn(&mut self, generation: usize) {
 1404        if let Some(active) = self.as_active_thread_mut() {
 1405            active.stop_turn(generation);
 1406        }
 1407    }
 1408
 1409    fn update_turn_tokens(&mut self, cx: &App) {
 1410        if let Some(active) = self.as_active_thread_mut() {
 1411            active.update_turn_tokens(cx);
 1412        }
 1413    }
 1414
 1415    fn send_impl(
 1416        &mut self,
 1417        message_editor: Entity<MessageEditor>,
 1418        window: &mut Window,
 1419        cx: &mut Context<Self>,
 1420    ) {
 1421        let full_mention_content = self.as_native_thread(cx).is_some_and(|thread| {
 1422            // Include full contents when using minimal profile
 1423            let thread = thread.read(cx);
 1424            AgentSettings::get_global(cx)
 1425                .profiles
 1426                .get(thread.profile())
 1427                .is_some_and(|profile| profile.tools.is_empty())
 1428        });
 1429
 1430        let cached_commands = self.cached_slash_commands(cx);
 1431        let cached_errors = self.cached_slash_command_errors(cx);
 1432        let contents = message_editor.update(cx, |message_editor, cx| {
 1433            message_editor.contents_with_cache(
 1434                full_mention_content,
 1435                Some(cached_commands),
 1436                Some(cached_errors),
 1437                cx,
 1438            )
 1439        });
 1440
 1441        if let Some(thread) = self.as_active_thread_mut() {
 1442            thread.thread_error.take();
 1443            thread.thread_feedback.clear();
 1444            thread.editing_message.take();
 1445
 1446            if thread.should_be_following {
 1447                let _ = self.workspace.update(cx, |workspace, cx| {
 1448                    workspace.follow(CollaboratorId::Agent, window, cx);
 1449                });
 1450            }
 1451        }
 1452
 1453        let contents_task = cx.spawn_in(window, async move |this, cx| {
 1454            let (contents, tracked_buffers) = contents.await?;
 1455
 1456            if contents.is_empty() {
 1457                return Ok(None);
 1458            }
 1459
 1460            this.update_in(cx, |this, window, cx| {
 1461                this.message_editor.update(cx, |message_editor, cx| {
 1462                    message_editor.clear(window, cx);
 1463                });
 1464            })?;
 1465
 1466            Ok(Some((contents, tracked_buffers)))
 1467        });
 1468
 1469        self.send_content(contents_task, window, cx);
 1470    }
 1471
 1472    fn send_content(
 1473        &mut self,
 1474        contents_task: Task<anyhow::Result<Option<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>>>,
 1475        window: &mut Window,
 1476        cx: &mut Context<Self>,
 1477    ) {
 1478        if let Some(active) = self.as_active_thread_mut() {
 1479            active.send_content(contents_task, window, cx);
 1480        };
 1481    }
 1482
 1483    fn send_queued_message_at_index(
 1484        &mut self,
 1485        index: usize,
 1486        is_send_now: bool,
 1487        window: &mut Window,
 1488        cx: &mut Context<Self>,
 1489    ) {
 1490        if let Some(active) = self.as_active_thread_mut() {
 1491            active.send_queued_message_at_index(index, is_send_now, window, cx);
 1492        }
 1493    }
 1494
 1495    fn cancel_editing(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
 1496        let focus_handle = self.focus_handle(cx);
 1497        if let Some(active) = self.as_active_thread_mut() {
 1498            active.cancel_editing(focus_handle, window, cx);
 1499        }
 1500    }
 1501
 1502    fn regenerate(
 1503        &mut self,
 1504        entry_ix: usize,
 1505        message_editor: Entity<MessageEditor>,
 1506        window: &mut Window,
 1507        cx: &mut Context<Self>,
 1508    ) {
 1509        if let Some(active) = self.as_active_thread_mut() {
 1510            active.regenerate(entry_ix, message_editor, window, cx);
 1511        }
 1512    }
 1513
 1514    fn open_edited_buffer(
 1515        &mut self,
 1516        buffer: &Entity<Buffer>,
 1517        window: &mut Window,
 1518        cx: &mut Context<Self>,
 1519    ) {
 1520        if let Some(active) = self.as_active_thread_mut() {
 1521            active.open_edited_buffer(buffer, window, cx);
 1522        };
 1523    }
 1524
 1525    fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
 1526        if let Some(active) = self.as_active_thread_mut() {
 1527            active.handle_open_rules(window, cx);
 1528        }
 1529    }
 1530
 1531    fn handle_thread_error(&mut self, error: anyhow::Error, cx: &mut Context<Self>) {
 1532        let error = ThreadError::from_err(error, &self.agent);
 1533        self.emit_thread_error_telemetry(&error, cx);
 1534        if let Some(thread) = self.as_active_thread_mut() {
 1535            thread.thread_error = Some(error);
 1536        }
 1537        cx.notify();
 1538    }
 1539
 1540    fn emit_thread_error_telemetry(&self, error: &ThreadError, cx: &mut Context<Self>) {
 1541        let (error_kind, acp_error_code, message): (&str, Option<SharedString>, SharedString) =
 1542            match error {
 1543                ThreadError::PaymentRequired => (
 1544                    "payment_required",
 1545                    None,
 1546                    "You reached your free usage limit. Upgrade to Zed Pro for more prompts."
 1547                        .into(),
 1548                ),
 1549                ThreadError::Refusal => {
 1550                    let model_or_agent_name = self.current_model_name(cx);
 1551                    let message = format!(
 1552                        "{} 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.",
 1553                        model_or_agent_name
 1554                    );
 1555                    ("refusal", None, message.into())
 1556                }
 1557                ThreadError::AuthenticationRequired(message) => {
 1558                    ("authentication_required", None, message.clone())
 1559                }
 1560                ThreadError::Other {
 1561                    acp_error_code,
 1562                    message,
 1563                } => ("other", acp_error_code.clone(), message.clone()),
 1564            };
 1565
 1566        let (agent_telemetry_id, session_id) = self
 1567            .as_active_thread()
 1568            .map(|r| {
 1569                let thread = r.thread.read(cx);
 1570                (
 1571                    thread.connection().telemetry_id(),
 1572                    thread.session_id().clone(),
 1573                )
 1574            })
 1575            .unzip();
 1576
 1577        telemetry::event!(
 1578            "Agent Panel Error Shown",
 1579            agent = agent_telemetry_id,
 1580            session_id = session_id,
 1581            kind = error_kind,
 1582            acp_error_code = acp_error_code,
 1583            message = message,
 1584        );
 1585    }
 1586
 1587    fn clear_thread_error(&mut self, cx: &mut Context<Self>) {
 1588        if let Some(active) = self.as_active_thread_mut() {
 1589            active.clear_thread_error(cx);
 1590        }
 1591    }
 1592
 1593    fn handle_thread_event(
 1594        &mut self,
 1595        thread: &Entity<AcpThread>,
 1596        event: &AcpThreadEvent,
 1597        window: &mut Window,
 1598        cx: &mut Context<Self>,
 1599    ) {
 1600        match event {
 1601            AcpThreadEvent::NewEntry => {
 1602                let len = thread.read(cx).entries().len();
 1603                let index = len - 1;
 1604                if let Some(active) = self.as_active_thread_mut() {
 1605                    active.entry_view_state.update(cx, |view_state, cx| {
 1606                        view_state.sync_entry(index, thread, window, cx);
 1607                        active.list_state.splice_focusable(
 1608                            index..index,
 1609                            [view_state
 1610                                .entry(index)
 1611                                .and_then(|entry| entry.focus_handle(cx))],
 1612                        );
 1613                    });
 1614                }
 1615            }
 1616            AcpThreadEvent::EntryUpdated(index) => {
 1617                if let Some(entry_view_state) = self
 1618                    .as_active_thread()
 1619                    .map(|active| &active.entry_view_state)
 1620                    .cloned()
 1621                {
 1622                    entry_view_state.update(cx, |view_state, cx| {
 1623                        view_state.sync_entry(*index, thread, window, cx)
 1624                    });
 1625                }
 1626            }
 1627            AcpThreadEvent::EntriesRemoved(range) => {
 1628                if let Some(active) = self.as_active_thread_mut() {
 1629                    active
 1630                        .entry_view_state
 1631                        .update(cx, |view_state, _cx| view_state.remove(range.clone()));
 1632                    active.list_state.splice(range.clone(), 0);
 1633                }
 1634            }
 1635            AcpThreadEvent::ToolAuthorizationRequired => {
 1636                self.notify_with_sound("Waiting for tool confirmation", IconName::Info, window, cx);
 1637            }
 1638            AcpThreadEvent::Retry(retry) => {
 1639                if let Some(active) = self.as_active_thread_mut() {
 1640                    active.thread_retry_status = Some(retry.clone());
 1641                }
 1642            }
 1643            AcpThreadEvent::Stopped => {
 1644                if let Some(active) = self.as_active_thread_mut() {
 1645                    active.thread_retry_status.take();
 1646                }
 1647                let used_tools = thread.read(cx).used_tools_since_last_user_message();
 1648                self.notify_with_sound(
 1649                    if used_tools {
 1650                        "Finished running tools"
 1651                    } else {
 1652                        "New message"
 1653                    },
 1654                    IconName::ZedAssistant,
 1655                    window,
 1656                    cx,
 1657                );
 1658
 1659                let should_send_queued = if let Some(active) = self.as_active_thread_mut() {
 1660                    if active.skip_queue_processing_count > 0 {
 1661                        active.skip_queue_processing_count -= 1;
 1662                        false
 1663                    } else if active.user_interrupted_generation {
 1664                        // Manual interruption: don't auto-process queue.
 1665                        // Reset the flag so future completions can process normally.
 1666                        active.user_interrupted_generation = false;
 1667                        false
 1668                    } else {
 1669                        let has_queued = !active.local_queued_messages.is_empty();
 1670                        // Don't auto-send if the first message editor is currently focused
 1671                        let is_first_editor_focused = active
 1672                            .queued_message_editors
 1673                            .first()
 1674                            .is_some_and(|editor| editor.focus_handle(cx).is_focused(window));
 1675                        has_queued && !is_first_editor_focused
 1676                    }
 1677                } else {
 1678                    false
 1679                };
 1680                if should_send_queued {
 1681                    self.send_queued_message_at_index(0, false, window, cx);
 1682                }
 1683
 1684                self.history.update(cx, |history, cx| history.refresh(cx));
 1685            }
 1686            AcpThreadEvent::Refusal => {
 1687                let error = ThreadError::Refusal;
 1688                self.emit_thread_error_telemetry(&error, cx);
 1689
 1690                if let Some(active) = self.as_active_thread_mut() {
 1691                    active.thread_retry_status.take();
 1692                    active.thread_error = Some(error);
 1693                }
 1694                let model_or_agent_name = self.current_model_name(cx);
 1695                let notification_message =
 1696                    format!("{} refused to respond to this request", model_or_agent_name);
 1697                self.notify_with_sound(&notification_message, IconName::Warning, window, cx);
 1698            }
 1699            AcpThreadEvent::Error => {
 1700                if let Some(active) = self.as_active_thread_mut() {
 1701                    active.thread_retry_status.take();
 1702                }
 1703                self.notify_with_sound(
 1704                    "Agent stopped due to an error",
 1705                    IconName::Warning,
 1706                    window,
 1707                    cx,
 1708                );
 1709            }
 1710            AcpThreadEvent::LoadError(error) => {
 1711                self.server_state = ServerState::LoadError(error.clone());
 1712                if self.message_editor.focus_handle(cx).is_focused(window) {
 1713                    self.focus_handle.focus(window, cx)
 1714                }
 1715            }
 1716            AcpThreadEvent::TitleUpdated => {
 1717                let title = thread.read(cx).title();
 1718                if let Some(title_editor) = self
 1719                    .as_active_thread()
 1720                    .and_then(|active| active.title_editor.as_ref())
 1721                {
 1722                    title_editor.update(cx, |editor, cx| {
 1723                        if editor.text(cx) != title {
 1724                            editor.set_text(title, window, cx);
 1725                        }
 1726                    });
 1727                }
 1728                self.history.update(cx, |history, cx| history.refresh(cx));
 1729            }
 1730            AcpThreadEvent::PromptCapabilitiesUpdated => {
 1731                if let Some(active) = self.as_active_thread_mut() {
 1732                    active
 1733                        .prompt_capabilities
 1734                        .replace(thread.read(cx).prompt_capabilities());
 1735                }
 1736            }
 1737            AcpThreadEvent::TokenUsageUpdated => {
 1738                self.update_turn_tokens(cx);
 1739            }
 1740            AcpThreadEvent::AvailableCommandsUpdated(available_commands) => {
 1741                let mut available_commands = available_commands.clone();
 1742
 1743                if thread
 1744                    .read(cx)
 1745                    .connection()
 1746                    .auth_methods()
 1747                    .iter()
 1748                    .any(|method| method.id.0.as_ref() == "claude-login")
 1749                {
 1750                    available_commands.push(acp::AvailableCommand::new("login", "Authenticate"));
 1751                    available_commands.push(acp::AvailableCommand::new("logout", "Authenticate"));
 1752                }
 1753
 1754                let has_commands = !available_commands.is_empty();
 1755                if let Some(active) = self.as_active_thread_mut() {
 1756                    active.available_commands.replace(available_commands);
 1757                }
 1758                self.refresh_cached_user_commands(cx);
 1759
 1760                let agent_display_name = self
 1761                    .agent_server_store
 1762                    .read(cx)
 1763                    .agent_display_name(&ExternalAgentServerName(self.agent.name()))
 1764                    .unwrap_or_else(|| self.agent.name());
 1765
 1766                let new_placeholder = placeholder_text(agent_display_name.as_ref(), has_commands);
 1767
 1768                self.message_editor.update(cx, |editor, cx| {
 1769                    editor.set_placeholder_text(&new_placeholder, window, cx);
 1770                });
 1771            }
 1772            AcpThreadEvent::ModeUpdated(_mode) => {
 1773                // The connection keeps track of the mode
 1774                cx.notify();
 1775            }
 1776            AcpThreadEvent::ConfigOptionsUpdated(_) => {
 1777                // The watch task in ConfigOptionsView handles rebuilding selectors
 1778                cx.notify();
 1779            }
 1780        }
 1781        cx.notify();
 1782    }
 1783
 1784    fn authenticate(
 1785        &mut self,
 1786        method: acp::AuthMethodId,
 1787        window: &mut Window,
 1788        cx: &mut Context<Self>,
 1789    ) {
 1790        let Some(connected) = self.as_connected_mut() else {
 1791            return;
 1792        };
 1793        let connection = connected.connection.clone();
 1794
 1795        let AuthState::Unauthenticated {
 1796            configuration_view,
 1797            pending_auth_method,
 1798            ..
 1799        } = &mut connected.auth_state
 1800        else {
 1801            return;
 1802        };
 1803
 1804        let agent_telemetry_id = connection.telemetry_id();
 1805
 1806        // Check for the experimental "terminal-auth" _meta field
 1807        let auth_method = connection.auth_methods().iter().find(|m| m.id == method);
 1808
 1809        if let Some(terminal_auth) = auth_method
 1810            .and_then(|a| a.meta.as_ref())
 1811            .and_then(|m| m.get("terminal-auth"))
 1812        {
 1813            // Extract terminal auth details from meta
 1814            if let (Some(command), Some(label)) = (
 1815                terminal_auth.get("command").and_then(|v| v.as_str()),
 1816                terminal_auth.get("label").and_then(|v| v.as_str()),
 1817            ) {
 1818                let args = terminal_auth
 1819                    .get("args")
 1820                    .and_then(|v| v.as_array())
 1821                    .map(|arr| {
 1822                        arr.iter()
 1823                            .filter_map(|v| v.as_str().map(String::from))
 1824                            .collect()
 1825                    })
 1826                    .unwrap_or_default();
 1827
 1828                let env = terminal_auth
 1829                    .get("env")
 1830                    .and_then(|v| v.as_object())
 1831                    .map(|obj| {
 1832                        obj.iter()
 1833                            .filter_map(|(k, v)| v.as_str().map(|val| (k.clone(), val.to_string())))
 1834                            .collect::<HashMap<String, String>>()
 1835                    })
 1836                    .unwrap_or_default();
 1837
 1838                // Run SpawnInTerminal in the same dir as the ACP server
 1839                let cwd = connected
 1840                    .connection
 1841                    .clone()
 1842                    .downcast::<agent_servers::AcpConnection>()
 1843                    .map(|acp_conn| acp_conn.root_dir().to_path_buf());
 1844
 1845                // Build SpawnInTerminal from _meta
 1846                let login = task::SpawnInTerminal {
 1847                    id: task::TaskId(format!("external-agent-{}-login", label)),
 1848                    full_label: label.to_string(),
 1849                    label: label.to_string(),
 1850                    command: Some(command.to_string()),
 1851                    args,
 1852                    command_label: label.to_string(),
 1853                    cwd,
 1854                    env,
 1855                    use_new_terminal: true,
 1856                    allow_concurrent_runs: true,
 1857                    hide: task::HideStrategy::Always,
 1858                    ..Default::default()
 1859                };
 1860
 1861                configuration_view.take();
 1862                pending_auth_method.replace(method.clone());
 1863
 1864                if let Some(workspace) = self.workspace.upgrade() {
 1865                    let project = self.project.clone();
 1866                    let authenticate = Self::spawn_external_agent_login(
 1867                        login,
 1868                        workspace,
 1869                        project,
 1870                        method.clone(),
 1871                        false,
 1872                        window,
 1873                        cx,
 1874                    );
 1875                    cx.notify();
 1876                    self.auth_task = Some(cx.spawn_in(window, {
 1877                        async move |this, cx| {
 1878                            let result = authenticate.await;
 1879
 1880                            match &result {
 1881                                Ok(_) => telemetry::event!(
 1882                                    "Authenticate Agent Succeeded",
 1883                                    agent = agent_telemetry_id
 1884                                ),
 1885                                Err(_) => {
 1886                                    telemetry::event!(
 1887                                        "Authenticate Agent Failed",
 1888                                        agent = agent_telemetry_id,
 1889                                    )
 1890                                }
 1891                            }
 1892
 1893                            this.update_in(cx, |this, window, cx| {
 1894                                if let Err(err) = result {
 1895                                    if let Some(ConnectedServerState {
 1896                                        auth_state:
 1897                                            AuthState::Unauthenticated {
 1898                                                pending_auth_method,
 1899                                                ..
 1900                                            },
 1901                                        ..
 1902                                    }) = this.as_connected_mut()
 1903                                    {
 1904                                        pending_auth_method.take();
 1905                                    }
 1906                                    this.handle_thread_error(err, cx);
 1907                                } else {
 1908                                    this.reset(window, cx);
 1909                                }
 1910                                this.auth_task.take()
 1911                            })
 1912                            .ok();
 1913                        }
 1914                    }));
 1915                }
 1916                return;
 1917            }
 1918        }
 1919
 1920        if method.0.as_ref() == "gemini-api-key" {
 1921            let registry = LanguageModelRegistry::global(cx);
 1922            let provider = registry
 1923                .read(cx)
 1924                .provider(&language_model::GOOGLE_PROVIDER_ID)
 1925                .unwrap();
 1926            if !provider.is_authenticated(cx) {
 1927                let this = cx.weak_entity();
 1928                let agent_name = self.agent.name();
 1929                window.defer(cx, |window, cx| {
 1930                    Self::handle_auth_required(
 1931                        this,
 1932                        AuthRequired {
 1933                            description: Some("GEMINI_API_KEY must be set".to_owned()),
 1934                            provider_id: Some(language_model::GOOGLE_PROVIDER_ID),
 1935                        },
 1936                        agent_name,
 1937                        connection,
 1938                        window,
 1939                        cx,
 1940                    );
 1941                });
 1942                return;
 1943            }
 1944        } else if method.0.as_ref() == "vertex-ai"
 1945            && std::env::var("GOOGLE_API_KEY").is_err()
 1946            && (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()
 1947                || (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()))
 1948        {
 1949            let this = cx.weak_entity();
 1950            let agent_name = self.agent.name();
 1951
 1952            window.defer(cx, |window, cx| {
 1953                    Self::handle_auth_required(
 1954                        this,
 1955                        AuthRequired {
 1956                            description: Some(
 1957                                "GOOGLE_API_KEY must be set in the environment to use Vertex AI authentication for Gemini CLI. Please export it and restart Zed."
 1958                                    .to_owned(),
 1959                            ),
 1960                            provider_id: None,
 1961                        },
 1962                        agent_name,
 1963                        connection,
 1964                        window,
 1965                        cx,
 1966                    )
 1967                });
 1968            return;
 1969        }
 1970
 1971        configuration_view.take();
 1972        pending_auth_method.replace(method.clone());
 1973        let authenticate = if let Some(login) = self.login.clone() {
 1974            if let Some(workspace) = self.workspace.upgrade() {
 1975                let project = self.project.clone();
 1976                Self::spawn_external_agent_login(
 1977                    login,
 1978                    workspace,
 1979                    project,
 1980                    method.clone(),
 1981                    false,
 1982                    window,
 1983                    cx,
 1984                )
 1985            } else {
 1986                Task::ready(Ok(()))
 1987            }
 1988        } else {
 1989            connection.authenticate(method, cx)
 1990        };
 1991        cx.notify();
 1992        self.auth_task = Some(cx.spawn_in(window, {
 1993            async move |this, cx| {
 1994                let result = authenticate.await;
 1995
 1996                match &result {
 1997                    Ok(_) => telemetry::event!(
 1998                        "Authenticate Agent Succeeded",
 1999                        agent = agent_telemetry_id
 2000                    ),
 2001                    Err(_) => {
 2002                        telemetry::event!("Authenticate Agent Failed", agent = agent_telemetry_id,)
 2003                    }
 2004                }
 2005
 2006                this.update_in(cx, |this, window, cx| {
 2007                    if let Err(err) = result {
 2008                        if let Some(ConnectedServerState {
 2009                            auth_state:
 2010                                AuthState::Unauthenticated {
 2011                                    pending_auth_method,
 2012                                    ..
 2013                                },
 2014                            ..
 2015                        }) = this.as_connected_mut()
 2016                        {
 2017                            pending_auth_method.take();
 2018                        }
 2019                        this.handle_thread_error(err, cx);
 2020                    } else {
 2021                        this.reset(window, cx);
 2022                    }
 2023                    this.auth_task.take()
 2024                })
 2025                .ok();
 2026            }
 2027        }));
 2028    }
 2029
 2030    fn spawn_external_agent_login(
 2031        login: task::SpawnInTerminal,
 2032        workspace: Entity<Workspace>,
 2033        project: Entity<Project>,
 2034        method: acp::AuthMethodId,
 2035        previous_attempt: bool,
 2036        window: &mut Window,
 2037        cx: &mut App,
 2038    ) -> Task<Result<()>> {
 2039        let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
 2040            return Task::ready(Ok(()));
 2041        };
 2042
 2043        window.spawn(cx, async move |cx| {
 2044            let mut task = login.clone();
 2045            if let Some(cmd) = &task.command {
 2046                // Have "node" command use Zed's managed Node runtime by default
 2047                if cmd == "node" {
 2048                    let resolved_node_runtime = project
 2049                        .update(cx, |project, cx| {
 2050                            let agent_server_store = project.agent_server_store().clone();
 2051                            agent_server_store.update(cx, |store, cx| {
 2052                                store.node_runtime().map(|node_runtime| {
 2053                                    cx.background_spawn(async move {
 2054                                        node_runtime.binary_path().await
 2055                                    })
 2056                                })
 2057                            })
 2058                        });
 2059
 2060                    if let Some(resolve_task) = resolved_node_runtime {
 2061                        if let Ok(node_path) = resolve_task.await {
 2062                            task.command = Some(node_path.to_string_lossy().to_string());
 2063                        }
 2064                    }
 2065                }
 2066            }
 2067            task.shell = task::Shell::WithArguments {
 2068                program: task.command.take().expect("login command should be set"),
 2069                args: std::mem::take(&mut task.args),
 2070                title_override: None
 2071            };
 2072            task.full_label = task.label.clone();
 2073            task.id = task::TaskId(format!("external-agent-{}-login", task.label));
 2074            task.command_label = task.label.clone();
 2075            task.use_new_terminal = true;
 2076            task.allow_concurrent_runs = true;
 2077            task.hide = task::HideStrategy::Always;
 2078
 2079            let terminal = terminal_panel
 2080                .update_in(cx, |terminal_panel, window, cx| {
 2081                    terminal_panel.spawn_task(&task, window, cx)
 2082                })?
 2083                .await?;
 2084
 2085            let success_patterns = match method.0.as_ref() {
 2086                "claude-login" | "spawn-gemini-cli" => vec![
 2087                    "Login successful".to_string(),
 2088                    "Type your message".to_string(),
 2089                ],
 2090                _ => Vec::new(),
 2091            };
 2092            if success_patterns.is_empty() {
 2093                // No success patterns specified: wait for the process to exit and check exit code
 2094                let exit_status = terminal
 2095                    .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
 2096                    .await;
 2097
 2098                match exit_status {
 2099                    Some(status) if status.success() => Ok(()),
 2100                    Some(status) => Err(anyhow!(
 2101                        "Login command failed with exit code: {:?}",
 2102                        status.code()
 2103                    )),
 2104                    None => Err(anyhow!("Login command terminated without exit status")),
 2105                }
 2106            } else {
 2107                // Look for specific output patterns to detect successful login
 2108                let mut exit_status = terminal
 2109                    .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
 2110                    .fuse();
 2111
 2112                let logged_in = cx
 2113                    .spawn({
 2114                        let terminal = terminal.clone();
 2115                        async move |cx| {
 2116                            loop {
 2117                                cx.background_executor().timer(Duration::from_secs(1)).await;
 2118                                let content =
 2119                                    terminal.update(cx, |terminal, _cx| terminal.get_content())?;
 2120                                if success_patterns.iter().any(|pattern| content.contains(pattern))
 2121                                {
 2122                                    return anyhow::Ok(());
 2123                                }
 2124                            }
 2125                        }
 2126                    })
 2127                    .fuse();
 2128                futures::pin_mut!(logged_in);
 2129                futures::select_biased! {
 2130                    result = logged_in => {
 2131                        if let Err(e) = result {
 2132                            log::error!("{e}");
 2133                            return Err(anyhow!("exited before logging in"));
 2134                        }
 2135                    }
 2136                    _ = exit_status => {
 2137                        if !previous_attempt && project.read_with(cx, |project, _| project.is_via_remote_server()) && login.label.contains("gemini") {
 2138                            return cx.update(|window, cx| Self::spawn_external_agent_login(login, workspace, project.clone(), method, true, window, cx))?.await
 2139                        }
 2140                        return Err(anyhow!("exited before logging in"));
 2141                    }
 2142                }
 2143                terminal.update(cx, |terminal, _| terminal.kill_active_task())?;
 2144                Ok(())
 2145            }
 2146        })
 2147    }
 2148
 2149    pub fn has_user_submitted_prompt(&self, cx: &App) -> bool {
 2150        self.as_active_thread().is_some_and(|active| {
 2151            active.thread.read(cx).entries().iter().any(|entry| {
 2152                matches!(
 2153                    entry,
 2154                    AgentThreadEntry::UserMessage(user_message) if user_message.id.is_some()
 2155                )
 2156            })
 2157        })
 2158    }
 2159
 2160    fn authorize_tool_call(
 2161        &mut self,
 2162        tool_call_id: acp::ToolCallId,
 2163        option_id: acp::PermissionOptionId,
 2164        option_kind: acp::PermissionOptionKind,
 2165        window: &mut Window,
 2166        cx: &mut Context<Self>,
 2167    ) {
 2168        if let Some(active) = self.as_active_thread_mut() {
 2169            active.authorize_tool_call(tool_call_id, option_id, option_kind, window, cx);
 2170        };
 2171    }
 2172
 2173    fn authorize_subagent_tool_call(
 2174        &mut self,
 2175        subagent_thread: Entity<AcpThread>,
 2176        tool_call_id: acp::ToolCallId,
 2177        option_id: acp::PermissionOptionId,
 2178        option_kind: acp::PermissionOptionKind,
 2179        _window: &mut Window,
 2180        cx: &mut Context<Self>,
 2181    ) {
 2182        subagent_thread.update(cx, |thread, cx| {
 2183            thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx);
 2184        });
 2185    }
 2186
 2187    fn restore_checkpoint(&mut self, message_id: &UserMessageId, cx: &mut Context<Self>) {
 2188        if let Some(active) = self.as_active_thread_mut() {
 2189            active.restore_checkpoint(message_id, cx);
 2190        };
 2191    }
 2192
 2193    fn render_entry(
 2194        &self,
 2195        entry_ix: usize,
 2196        total_entries: usize,
 2197        entry: &AgentThreadEntry,
 2198        window: &mut Window,
 2199        cx: &Context<Self>,
 2200    ) -> AnyElement {
 2201        let is_indented = entry.is_indented();
 2202        let is_first_indented = is_indented
 2203            && self.as_active_thread().is_some_and(|active| {
 2204                active
 2205                    .thread
 2206                    .read(cx)
 2207                    .entries()
 2208                    .get(entry_ix.saturating_sub(1))
 2209                    .is_none_or(|entry| !entry.is_indented())
 2210            });
 2211
 2212        let primary = match &entry {
 2213            AgentThreadEntry::UserMessage(message) => {
 2214                let Some(entry_view_state) = self
 2215                    .as_active_thread()
 2216                    .map(|active| &active.entry_view_state)
 2217                else {
 2218                    return Empty.into_any_element();
 2219                };
 2220                let Some(editor) = entry_view_state
 2221                    .read(cx)
 2222                    .entry(entry_ix)
 2223                    .and_then(|entry| entry.message_editor())
 2224                    .cloned()
 2225                else {
 2226                    return Empty.into_any_element();
 2227                };
 2228
 2229                let editing = self
 2230                    .as_active_thread()
 2231                    .and_then(|active| active.editing_message)
 2232                    == Some(entry_ix);
 2233                let editor_focus = editor.focus_handle(cx).is_focused(window);
 2234                let focus_border = cx.theme().colors().border_focused;
 2235
 2236                let rules_item = if entry_ix == 0 {
 2237                    self.render_rules_item(cx)
 2238                } else {
 2239                    None
 2240                };
 2241
 2242                let has_checkpoint_button = message
 2243                    .checkpoint
 2244                    .as_ref()
 2245                    .is_some_and(|checkpoint| checkpoint.show);
 2246
 2247                let agent_name = self.agent.name();
 2248
 2249                v_flex()
 2250                    .id(("user_message", entry_ix))
 2251                    .map(|this| {
 2252                        if is_first_indented {
 2253                            this.pt_0p5()
 2254                        } else if entry_ix == 0 && !has_checkpoint_button && rules_item.is_none()  {
 2255                            this.pt(rems_from_px(18.))
 2256                        } else if rules_item.is_some() {
 2257                            this.pt_3()
 2258                        } else {
 2259                            this.pt_2()
 2260                        }
 2261                    })
 2262                    .pb_3()
 2263                    .px_2()
 2264                    .gap_1p5()
 2265                    .w_full()
 2266                    .children(rules_item)
 2267                    .children(message.id.clone().and_then(|message_id| {
 2268                        message.checkpoint.as_ref()?.show.then(|| {
 2269                            h_flex()
 2270                                .px_3()
 2271                                .gap_2()
 2272                                .child(Divider::horizontal())
 2273                                .child(
 2274                                    Button::new("restore-checkpoint", "Restore Checkpoint")
 2275                                        .icon(IconName::Undo)
 2276                                        .icon_size(IconSize::XSmall)
 2277                                        .icon_position(IconPosition::Start)
 2278                                        .label_size(LabelSize::XSmall)
 2279                                        .icon_color(Color::Muted)
 2280                                        .color(Color::Muted)
 2281                                        .tooltip(Tooltip::text("Restores all files in the project to the content they had at this point in the conversation."))
 2282                                        .on_click(cx.listener(move |this, _, _window, cx| {
 2283                                            this.restore_checkpoint(&message_id, cx);
 2284                                        }))
 2285                                )
 2286                                .child(Divider::horizontal())
 2287                        })
 2288                    }))
 2289                    .child(
 2290                        div()
 2291                            .relative()
 2292                            .child(
 2293                                div()
 2294                                    .py_3()
 2295                                    .px_2()
 2296                                    .rounded_md()
 2297                                    .shadow_md()
 2298                                    .bg(cx.theme().colors().editor_background)
 2299                                    .border_1()
 2300                                    .when(is_indented, |this| {
 2301                                        this.py_2().px_2().shadow_sm()
 2302                                    })
 2303                                    .when(editing && !editor_focus, |this| this.border_dashed())
 2304                                    .border_color(cx.theme().colors().border)
 2305                                    .map(|this|{
 2306                                        if editing && editor_focus {
 2307                                            this.border_color(focus_border)
 2308                                        } else if message.id.is_some() {
 2309                                            this.hover(|s| s.border_color(focus_border.opacity(0.8)))
 2310                                        } else {
 2311                                            this
 2312                                        }
 2313                                    })
 2314                                    .text_xs()
 2315                                    .child(editor.clone().into_any_element())
 2316                            )
 2317                            .when(editor_focus, |this| {
 2318                                let base_container = h_flex()
 2319                                    .absolute()
 2320                                    .top_neg_3p5()
 2321                                    .right_3()
 2322                                    .gap_1()
 2323                                    .rounded_sm()
 2324                                    .border_1()
 2325                                    .border_color(cx.theme().colors().border)
 2326                                    .bg(cx.theme().colors().editor_background)
 2327                                    .overflow_hidden();
 2328
 2329                                let is_loading_contents = matches!(&self.server_state, ServerState::Connected(ConnectedServerState { current: Some(AcpThreadView { is_loading_contents: true, .. }), ..}));
 2330                                if message.id.is_some() {
 2331                                    this.child(
 2332                                        base_container
 2333                                            .child(
 2334                                                IconButton::new("cancel", IconName::Close)
 2335                                                    .disabled(is_loading_contents)
 2336                                                    .icon_color(Color::Error)
 2337                                                    .icon_size(IconSize::XSmall)
 2338                                                    .on_click(cx.listener(Self::cancel_editing))
 2339                                            )
 2340                                            .child(
 2341                                                if is_loading_contents {
 2342                                                    div()
 2343                                                        .id("loading-edited-message-content")
 2344                                                        .tooltip(Tooltip::text("Loading Added Context…"))
 2345                                                        .child(loading_contents_spinner(IconSize::XSmall))
 2346                                                        .into_any_element()
 2347                                                } else {
 2348                                                    IconButton::new("regenerate", IconName::Return)
 2349                                                        .icon_color(Color::Muted)
 2350                                                        .icon_size(IconSize::XSmall)
 2351                                                        .tooltip(Tooltip::text(
 2352                                                            "Editing will restart the thread from this point."
 2353                                                        ))
 2354                                                        .on_click(cx.listener({
 2355                                                            let editor = editor.clone();
 2356                                                            move |this, _, window, cx| {
 2357                                                                this.regenerate(
 2358                                                                    entry_ix, editor.clone(), window, cx,
 2359                                                                );
 2360                                                            }
 2361                                                        })).into_any_element()
 2362                                                }
 2363                                            )
 2364                                    )
 2365                                } else {
 2366                                    this.child(
 2367                                        base_container
 2368                                            .border_dashed()
 2369                                            .child(
 2370                                                IconButton::new("editing_unavailable", IconName::PencilUnavailable)
 2371                                                    .icon_size(IconSize::Small)
 2372                                                    .icon_color(Color::Muted)
 2373                                                    .style(ButtonStyle::Transparent)
 2374                                                    .tooltip(Tooltip::element({
 2375                                                        move |_, _| {
 2376                                                            v_flex()
 2377                                                                .gap_1()
 2378                                                                .child(Label::new("Unavailable Editing")).child(
 2379                                                                    div().max_w_64().child(
 2380                                                                        Label::new(format!(
 2381                                                                            "Editing previous messages is not available for {} yet.",
 2382                                                                            agent_name.clone()
 2383                                                                        ))
 2384                                                                        .size(LabelSize::Small)
 2385                                                                        .color(Color::Muted),
 2386                                                                    ),
 2387                                                                )
 2388                                                                .into_any_element()
 2389                                                        }
 2390                                                    }))
 2391                                            )
 2392                                    )
 2393                                }
 2394                            }),
 2395                    )
 2396                    .into_any()
 2397            }
 2398            AgentThreadEntry::AssistantMessage(AssistantMessage {
 2399                chunks,
 2400                indented: _,
 2401            }) => {
 2402                let mut is_blank = true;
 2403                let is_last = entry_ix + 1 == total_entries;
 2404
 2405                let style = MarkdownStyle::themed(MarkdownFont::Agent, window, cx);
 2406                let message_body = v_flex()
 2407                    .w_full()
 2408                    .gap_3()
 2409                    .children(chunks.iter().enumerate().filter_map(
 2410                        |(chunk_ix, chunk)| match chunk {
 2411                            AssistantMessageChunk::Message { block } => {
 2412                                block.markdown().and_then(|md| {
 2413                                    let this_is_blank = md.read(cx).source().trim().is_empty();
 2414                                    is_blank = is_blank && this_is_blank;
 2415                                    if this_is_blank {
 2416                                        return None;
 2417                                    }
 2418
 2419                                    Some(
 2420                                        self.render_markdown(md.clone(), style.clone())
 2421                                            .into_any_element(),
 2422                                    )
 2423                                })
 2424                            }
 2425                            AssistantMessageChunk::Thought { block } => {
 2426                                block.markdown().and_then(|md| {
 2427                                    let this_is_blank = md.read(cx).source().trim().is_empty();
 2428                                    is_blank = is_blank && this_is_blank;
 2429                                    if this_is_blank {
 2430                                        return None;
 2431                                    }
 2432                                    Some(
 2433                                        self.render_thinking_block(
 2434                                            entry_ix,
 2435                                            chunk_ix,
 2436                                            md.clone(),
 2437                                            window,
 2438                                            cx,
 2439                                        )
 2440                                        .into_any_element(),
 2441                                    )
 2442                                })
 2443                            }
 2444                        },
 2445                    ))
 2446                    .into_any();
 2447
 2448                if is_blank {
 2449                    Empty.into_any()
 2450                } else {
 2451                    v_flex()
 2452                        .px_5()
 2453                        .py_1p5()
 2454                        .when(is_last, |this| this.pb_4())
 2455                        .w_full()
 2456                        .text_ui(cx)
 2457                        .child(self.render_message_context_menu(entry_ix, message_body, cx))
 2458                        .into_any()
 2459                }
 2460            }
 2461            AgentThreadEntry::ToolCall(tool_call) => {
 2462                let has_terminals = tool_call.terminals().next().is_some();
 2463
 2464                div()
 2465                    .w_full()
 2466                    .map(|this| {
 2467                        if has_terminals {
 2468                            this.children(tool_call.terminals().map(|terminal| {
 2469                                self.render_terminal_tool_call(
 2470                                    entry_ix, terminal, tool_call, window, cx,
 2471                                )
 2472                            }))
 2473                        } else {
 2474                            this.child(self.render_tool_call(entry_ix, tool_call, window, cx))
 2475                        }
 2476                    })
 2477                    .into_any()
 2478            }
 2479        };
 2480
 2481        let primary = if is_indented {
 2482            let line_top = if is_first_indented {
 2483                rems_from_px(-12.0)
 2484            } else {
 2485                rems_from_px(0.0)
 2486            };
 2487
 2488            div()
 2489                .relative()
 2490                .w_full()
 2491                .pl_5()
 2492                .bg(cx.theme().colors().panel_background.opacity(0.2))
 2493                .child(
 2494                    div()
 2495                        .absolute()
 2496                        .left(rems_from_px(18.0))
 2497                        .top(line_top)
 2498                        .bottom_0()
 2499                        .w_px()
 2500                        .bg(cx.theme().colors().border.opacity(0.6)),
 2501                )
 2502                .child(primary)
 2503                .into_any_element()
 2504        } else {
 2505            primary
 2506        };
 2507
 2508        let needs_confirmation = if let AgentThreadEntry::ToolCall(tool_call) = entry {
 2509            matches!(
 2510                tool_call.status,
 2511                ToolCallStatus::WaitingForConfirmation { .. }
 2512            )
 2513        } else {
 2514            false
 2515        };
 2516
 2517        let Some(active) = self.as_active_thread() else {
 2518            return primary;
 2519        };
 2520
 2521        let primary = if entry_ix == total_entries - 1 {
 2522            v_flex()
 2523                .w_full()
 2524                .child(primary)
 2525                .map(|this| {
 2526                    if needs_confirmation {
 2527                        this.child(self.render_generating(true, cx))
 2528                    } else {
 2529                        this.child(self.render_thread_controls(&active.thread, cx))
 2530                    }
 2531                })
 2532                .when_some(
 2533                    active.thread_feedback.comments_editor.clone(),
 2534                    |this, editor| this.child(Self::render_feedback_feedback_editor(editor, cx)),
 2535                )
 2536                .into_any_element()
 2537        } else {
 2538            primary
 2539        };
 2540
 2541        if let Some(editing_index) = self
 2542            .as_active_thread()
 2543            .and_then(|active| active.editing_message)
 2544            && editing_index < entry_ix
 2545        {
 2546            let backdrop = div()
 2547                .id(("backdrop", entry_ix))
 2548                .size_full()
 2549                .absolute()
 2550                .inset_0()
 2551                .bg(cx.theme().colors().panel_background)
 2552                .opacity(0.8)
 2553                .block_mouse_except_scroll()
 2554                .on_click(cx.listener(Self::cancel_editing));
 2555
 2556            div()
 2557                .relative()
 2558                .child(primary)
 2559                .child(backdrop)
 2560                .into_any_element()
 2561        } else {
 2562            primary
 2563        }
 2564    }
 2565
 2566    fn render_message_context_menu(
 2567        &self,
 2568        entry_ix: usize,
 2569        message_body: AnyElement,
 2570        cx: &Context<Self>,
 2571    ) -> AnyElement {
 2572        let entity = cx.entity();
 2573        let workspace = self.workspace.clone();
 2574
 2575        right_click_menu(format!("agent_context_menu-{}", entry_ix))
 2576            .trigger(move |_, _, _| message_body)
 2577            .menu(move |window, cx| {
 2578                let focus = window.focused(cx);
 2579                let entity = entity.clone();
 2580                let workspace = workspace.clone();
 2581
 2582                ContextMenu::build(window, cx, move |menu, _, cx| {
 2583                    let active_thread = entity.read(cx).as_active_thread();
 2584                    let is_at_top = active_thread
 2585                        .map(|active| &active.list_state)
 2586                        .map_or(true, |state| state.logical_scroll_top().item_ix == 0);
 2587
 2588                    let has_selection = active_thread
 2589                        .and_then(|active| active.thread.read(cx).entries().get(entry_ix))
 2590                        .and_then(|entry| match entry {
 2591                            AgentThreadEntry::AssistantMessage(msg) => Some(&msg.chunks),
 2592                            _ => None,
 2593                        })
 2594                        .map(|chunks| {
 2595                            chunks.iter().any(|chunk| {
 2596                                let md = match chunk {
 2597                                    AssistantMessageChunk::Message { block } => block.markdown(),
 2598                                    AssistantMessageChunk::Thought { block } => block.markdown(),
 2599                                };
 2600                                md.map_or(false, |m| m.read(cx).selected_text().is_some())
 2601                            })
 2602                        })
 2603                        .unwrap_or(false);
 2604
 2605                    let copy_this_agent_response =
 2606                        ContextMenuEntry::new("Copy This Agent Response").handler({
 2607                            let entity = entity.clone();
 2608                            move |_, cx| {
 2609                                entity.update(cx, |this, cx| {
 2610                                    if let Some(active) = this.as_active_thread() {
 2611                                        let entries = active.thread.read(cx).entries();
 2612                                        if let Some(text) =
 2613                                            Self::get_agent_message_content(entries, entry_ix, cx)
 2614                                        {
 2615                                            cx.write_to_clipboard(ClipboardItem::new_string(text));
 2616                                        }
 2617                                    }
 2618                                });
 2619                            }
 2620                        });
 2621
 2622                    let scroll_item = if is_at_top {
 2623                        ContextMenuEntry::new("Scroll to Bottom").handler({
 2624                            let entity = entity.clone();
 2625                            move |_, cx| {
 2626                                entity.update(cx, |this, cx| {
 2627                                    this.scroll_to_bottom(cx);
 2628                                });
 2629                            }
 2630                        })
 2631                    } else {
 2632                        ContextMenuEntry::new("Scroll to Top").handler({
 2633                            let entity = entity.clone();
 2634                            move |_, cx| {
 2635                                entity.update(cx, |this, cx| {
 2636                                    this.scroll_to_top(cx);
 2637                                });
 2638                            }
 2639                        })
 2640                    };
 2641
 2642                    let open_thread_as_markdown = ContextMenuEntry::new("Open Thread as Markdown")
 2643                        .handler({
 2644                            let entity = entity.clone();
 2645                            let workspace = workspace.clone();
 2646                            move |window, cx| {
 2647                                if let Some(workspace) = workspace.upgrade() {
 2648                                    entity
 2649                                        .update(cx, |this, cx| {
 2650                                            this.open_thread_as_markdown(workspace, window, cx)
 2651                                        })
 2652                                        .detach_and_log_err(cx);
 2653                                }
 2654                            }
 2655                        });
 2656
 2657                    menu.when_some(focus, |menu, focus| menu.context(focus))
 2658                        .action_disabled_when(
 2659                            !has_selection,
 2660                            "Copy Selection",
 2661                            Box::new(markdown::CopyAsMarkdown),
 2662                        )
 2663                        .item(copy_this_agent_response)
 2664                        .separator()
 2665                        .item(scroll_item)
 2666                        .item(open_thread_as_markdown)
 2667                })
 2668            })
 2669            .into_any_element()
 2670    }
 2671
 2672    fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
 2673        cx.theme()
 2674            .colors()
 2675            .element_background
 2676            .blend(cx.theme().colors().editor_foreground.opacity(0.025))
 2677    }
 2678
 2679    fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
 2680        cx.theme().colors().border.opacity(0.8)
 2681    }
 2682
 2683    fn tool_name_font_size(&self) -> Rems {
 2684        rems_from_px(13.)
 2685    }
 2686
 2687    fn render_thinking_block(
 2688        &self,
 2689        entry_ix: usize,
 2690        chunk_ix: usize,
 2691        chunk: Entity<Markdown>,
 2692        window: &Window,
 2693        cx: &Context<Self>,
 2694    ) -> AnyElement {
 2695        let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix));
 2696        let card_header_id = SharedString::from("inner-card-header");
 2697
 2698        let key = (entry_ix, chunk_ix);
 2699
 2700        let is_open = matches!(&self.server_state, ServerState::Connected(ConnectedServerState {current: Some(AcpThreadView { expanded_thinking_blocks, .. }), ..}) if expanded_thinking_blocks.contains(&key));
 2701
 2702        let scroll_handle = self
 2703            .as_active_thread()
 2704            .map(|active| &active.entry_view_state)
 2705            .and_then(|entry_view_state| {
 2706                entry_view_state
 2707                    .read(cx)
 2708                    .entry(entry_ix)
 2709                    .and_then(|entry| entry.scroll_handle_for_assistant_message_chunk(chunk_ix))
 2710            });
 2711
 2712        let thinking_content = {
 2713            div()
 2714                .id(("thinking-content", chunk_ix))
 2715                .when_some(scroll_handle, |this, scroll_handle| {
 2716                    this.track_scroll(&scroll_handle)
 2717                })
 2718                .text_ui_sm(cx)
 2719                .overflow_hidden()
 2720                .child(self.render_markdown(
 2721                    chunk,
 2722                    MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
 2723                ))
 2724        };
 2725
 2726        v_flex()
 2727            .gap_1()
 2728            .child(
 2729                h_flex()
 2730                    .id(header_id)
 2731                    .group(&card_header_id)
 2732                    .relative()
 2733                    .w_full()
 2734                    .pr_1()
 2735                    .justify_between()
 2736                    .child(
 2737                        h_flex()
 2738                            .h(window.line_height() - px(2.))
 2739                            .gap_1p5()
 2740                            .overflow_hidden()
 2741                            .child(
 2742                                Icon::new(IconName::ToolThink)
 2743                                    .size(IconSize::Small)
 2744                                    .color(Color::Muted),
 2745                            )
 2746                            .child(
 2747                                div()
 2748                                    .text_size(self.tool_name_font_size())
 2749                                    .text_color(cx.theme().colors().text_muted)
 2750                                    .child("Thinking"),
 2751                            ),
 2752                    )
 2753                    .child(
 2754                        Disclosure::new(("expand", entry_ix), is_open)
 2755                            .opened_icon(IconName::ChevronUp)
 2756                            .closed_icon(IconName::ChevronDown)
 2757                            .visible_on_hover(&card_header_id)
 2758                            .on_click(cx.listener({
 2759                                move |this, _event, _window, cx| {
 2760                                    if let Some(active) = this.as_active_thread_mut() {
 2761                                        if is_open {
 2762                                            active.expanded_thinking_blocks.remove(&key);
 2763                                        } else {
 2764                                            active.expanded_thinking_blocks.insert(key);
 2765                                        }
 2766                                        cx.notify();
 2767                                    }
 2768                                }
 2769                            })),
 2770                    )
 2771                    .on_click(cx.listener({
 2772                        move |this, _event, _window, cx| {
 2773                            if let Some(active) = this.as_active_thread_mut() {
 2774                                if is_open {
 2775                                    active.expanded_thinking_blocks.remove(&key);
 2776                                } else {
 2777                                    active.expanded_thinking_blocks.insert(key);
 2778                                }
 2779                                cx.notify();
 2780                            }
 2781                        }
 2782                    })),
 2783            )
 2784            .when(is_open, |this| {
 2785                this.child(
 2786                    div()
 2787                        .ml_1p5()
 2788                        .pl_3p5()
 2789                        .border_l_1()
 2790                        .border_color(self.tool_card_border_color(cx))
 2791                        .child(thinking_content),
 2792                )
 2793            })
 2794            .into_any_element()
 2795    }
 2796
 2797    fn render_tool_call(
 2798        &self,
 2799        entry_ix: usize,
 2800        tool_call: &ToolCall,
 2801        window: &Window,
 2802        cx: &Context<Self>,
 2803    ) -> Div {
 2804        let has_location = tool_call.locations.len() == 1;
 2805        let card_header_id = SharedString::from("inner-tool-call-header");
 2806
 2807        let failed_or_canceled = match &tool_call.status {
 2808            ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => true,
 2809            _ => false,
 2810        };
 2811
 2812        let needs_confirmation = matches!(
 2813            tool_call.status,
 2814            ToolCallStatus::WaitingForConfirmation { .. }
 2815        );
 2816        let is_terminal_tool = matches!(tool_call.kind, acp::ToolKind::Execute);
 2817
 2818        let is_edit =
 2819            matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some();
 2820        let is_subagent = tool_call.is_subagent();
 2821
 2822        // For subagent tool calls, render the subagent cards directly without wrapper
 2823        if is_subagent {
 2824            return self.render_subagent_tool_call(entry_ix, tool_call, window, cx);
 2825        }
 2826
 2827        let is_cancelled_edit = is_edit && matches!(tool_call.status, ToolCallStatus::Canceled);
 2828        let has_revealed_diff = tool_call.diffs().next().is_some_and(|diff| {
 2829            self.as_active_thread()
 2830                .and_then(|active| {
 2831                    active
 2832                        .entry_view_state
 2833                        .read(cx)
 2834                        .entry(entry_ix)
 2835                        .and_then(|entry| entry.editor_for_diff(diff))
 2836                })
 2837                .is_some()
 2838                && diff.read(cx).has_revealed_range(cx)
 2839        });
 2840
 2841        let use_card_layout = needs_confirmation || is_edit || is_terminal_tool;
 2842
 2843        let has_image_content = tool_call.content.iter().any(|c| c.image().is_some());
 2844        let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
 2845        let mut is_open = match &self.server_state {
 2846            ServerState::Connected(ConnectedServerState {
 2847                current: Some(current),
 2848                ..
 2849            }) => current.expanded_tool_calls.contains(&tool_call.id),
 2850            _ => false,
 2851        };
 2852
 2853        is_open |= needs_confirmation;
 2854
 2855        let should_show_raw_input = !is_terminal_tool && !is_edit && !has_image_content;
 2856
 2857        let input_output_header = |label: SharedString| {
 2858            Label::new(label)
 2859                .size(LabelSize::XSmall)
 2860                .color(Color::Muted)
 2861                .buffer_font(cx)
 2862        };
 2863
 2864        let tool_output_display = if is_open {
 2865            match &tool_call.status {
 2866                ToolCallStatus::WaitingForConfirmation { options, .. } => v_flex()
 2867                    .w_full()
 2868                    .children(
 2869                        tool_call
 2870                            .content
 2871                            .iter()
 2872                            .enumerate()
 2873                            .map(|(content_ix, content)| {
 2874                                div()
 2875                                    .child(self.render_tool_call_content(
 2876                                        entry_ix,
 2877                                        content,
 2878                                        content_ix,
 2879                                        tool_call,
 2880                                        use_card_layout,
 2881                                        has_image_content,
 2882                                        failed_or_canceled,
 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                            matches!(&self.server_state, ServerState::Connected(ConnectedServerState {current: Some(AcpThreadView { expanded_tool_call_raw_inputs, .. }), ..}) if 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 let Some(active) = this.as_active_thread_mut() {
 2927                                                    if active.expanded_tool_call_raw_inputs.contains(&id) {
 2928                                                        active.expanded_tool_call_raw_inputs.remove(&id);
 2929                                                    } else {
 2930                                                        active.expanded_tool_call_raw_inputs.insert(id.clone());
 2931                                                    }
 2932                                                    cx.notify();
 2933                                                }
 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                                                MarkdownStyle::themed(
 2943                                                    MarkdownFont::Agent,
 2944                                                    window,
 2945                                                    cx,
 2946                                                ),
 2947                                            )
 2948                                        },
 2949                                    ))
 2950                                }),
 2951                        )
 2952                    })
 2953                    .child(self.render_permission_buttons(
 2954                        options,
 2955                        entry_ix,
 2956                        tool_call.id.clone(),
 2957                        cx,
 2958                    ))
 2959                    .into_any(),
 2960                ToolCallStatus::Pending | ToolCallStatus::InProgress
 2961                    if is_edit
 2962                        && tool_call.content.is_empty()
 2963                        && self.as_native_connection(cx).is_some() =>
 2964                {
 2965                    self.render_diff_loading(cx).into_any()
 2966                }
 2967                ToolCallStatus::Pending
 2968                | ToolCallStatus::InProgress
 2969                | ToolCallStatus::Completed
 2970                | ToolCallStatus::Failed
 2971                | ToolCallStatus::Canceled => v_flex()
 2972                    .when(should_show_raw_input, |this| {
 2973                        this.mt_1p5().w_full().child(
 2974                            v_flex()
 2975                                .ml(rems(0.4))
 2976                                .px_3p5()
 2977                                .pb_1()
 2978                                .gap_1()
 2979                                .border_l_1()
 2980                                .border_color(self.tool_card_border_color(cx))
 2981                                .child(input_output_header("Raw Input:".into()))
 2982                                .children(tool_call.raw_input_markdown.clone().map(|input| {
 2983                                    div().id(("tool-call-raw-input-markdown", entry_ix)).child(
 2984                                        self.render_markdown(
 2985                                            input,
 2986                                            MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
 2987                                        ),
 2988                                    )
 2989                                }))
 2990                                .child(input_output_header("Output:".into())),
 2991                        )
 2992                    })
 2993                    .children(
 2994                        tool_call
 2995                            .content
 2996                            .iter()
 2997                            .enumerate()
 2998                            .map(|(content_ix, content)| {
 2999                                div().id(("tool-call-output", entry_ix)).child(
 3000                                    self.render_tool_call_content(
 3001                                        entry_ix,
 3002                                        content,
 3003                                        content_ix,
 3004                                        tool_call,
 3005                                        use_card_layout,
 3006                                        has_image_content,
 3007                                        failed_or_canceled,
 3008                                        window,
 3009                                        cx,
 3010                                    ),
 3011                                )
 3012                            }),
 3013                    )
 3014                    .into_any(),
 3015                ToolCallStatus::Rejected => Empty.into_any(),
 3016            }
 3017            .into()
 3018        } else {
 3019            None
 3020        };
 3021
 3022        v_flex()
 3023            .map(|this| {
 3024                if use_card_layout {
 3025                    this.my_1p5()
 3026                        .rounded_md()
 3027                        .border_1()
 3028                        .when(failed_or_canceled, |this| this.border_dashed())
 3029                        .border_color(self.tool_card_border_color(cx))
 3030                        .bg(cx.theme().colors().editor_background)
 3031                        .overflow_hidden()
 3032                } else {
 3033                    this.my_1()
 3034                }
 3035            })
 3036            .map(|this| {
 3037                if has_location && !use_card_layout {
 3038                    this.ml_4()
 3039                } else {
 3040                    this.ml_5()
 3041                }
 3042            })
 3043            .mr_5()
 3044            .map(|this| {
 3045                if is_terminal_tool {
 3046                    let label_source = tool_call.label.read(cx).source();
 3047                    this.child(self.render_collapsible_command(true, label_source, &tool_call.id, cx))
 3048                } else {
 3049                    this.child(
 3050                        h_flex()
 3051                            .group(&card_header_id)
 3052                            .relative()
 3053                            .w_full()
 3054                            .gap_1()
 3055                            .justify_between()
 3056                            .when(use_card_layout, |this| {
 3057                                this.p_0p5()
 3058                                    .rounded_t(rems_from_px(5.))
 3059                                    .bg(self.tool_card_header_bg(cx))
 3060                            })
 3061                            .child(self.render_tool_call_label(
 3062                                entry_ix,
 3063                                tool_call,
 3064                                is_edit,
 3065                                is_cancelled_edit,
 3066                                has_revealed_diff,
 3067                                use_card_layout,
 3068                                window,
 3069                                cx,
 3070                            ))
 3071                            .when(is_collapsible || failed_or_canceled, |this| {
 3072                                let diff_for_discard =
 3073                                    if has_revealed_diff && is_cancelled_edit && cx.has_flag::<AgentV2FeatureFlag>() {
 3074                                        tool_call.diffs().next().cloned()
 3075                                    } else {
 3076                                        None
 3077                                    };
 3078                                this.child(
 3079                                    h_flex()
 3080                                        .px_1()
 3081                                        .when_some(diff_for_discard.clone(), |this, _| this.pr_0p5())
 3082                                        .gap_1()
 3083                                        .when(is_collapsible, |this| {
 3084                                            this.child(
 3085                                            Disclosure::new(("expand-output", entry_ix), is_open)
 3086                                                .opened_icon(IconName::ChevronUp)
 3087                                                .closed_icon(IconName::ChevronDown)
 3088                                                .visible_on_hover(&card_header_id)
 3089                                                .on_click(cx.listener({
 3090                                                    let id = tool_call.id.clone();
 3091                                                    move |this: &mut Self, _, _, cx: &mut Context<Self>| {
 3092                                                        if let Some(active) = this.as_active_thread_mut() {
 3093                                                            if is_open {
 3094                                                                active.expanded_tool_calls.remove(&id);
 3095                                                            } else {
 3096                                                                active.expanded_tool_calls.insert(id.clone());
 3097                                                            }
 3098                                                            cx.notify();
 3099                                                        }
 3100                                                    }
 3101                                                })),
 3102                                        )
 3103                                        })
 3104                                        .when(failed_or_canceled, |this| {
 3105                                            if is_cancelled_edit && !has_revealed_diff {
 3106                                                this.child(
 3107                                                    div()
 3108                                                        .id(entry_ix)
 3109                                                        .tooltip(Tooltip::text(
 3110                                                            "Interrupted Edit",
 3111                                                        ))
 3112                                                        .child(
 3113                                                            Icon::new(IconName::XCircle)
 3114                                                                .color(Color::Muted)
 3115                                                                .size(IconSize::Small),
 3116                                                        ),
 3117                                                )
 3118                                            } else if is_cancelled_edit {
 3119                                                this
 3120                                            } else {
 3121                                                this.child(
 3122                                                    Icon::new(IconName::Close)
 3123                                                        .color(Color::Error)
 3124                                                        .size(IconSize::Small),
 3125                                                )
 3126                                            }
 3127                                        })
 3128                                        .when_some(diff_for_discard, |this, diff| {
 3129                                            let tool_call_id = tool_call.id.clone();
 3130                                            let is_discarded = matches!(&self.server_state, ServerState::Connected(ConnectedServerState{current: Some(AcpThreadView { discarded_partial_edits, .. }), ..}) if discarded_partial_edits.contains(&tool_call_id));
 3131                                            this.when(!is_discarded, |this| {
 3132                                                this.child(
 3133                                                    IconButton::new(
 3134                                                        ("discard-partial-edit", entry_ix),
 3135                                                        IconName::Undo,
 3136                                                    )
 3137                                                    .icon_size(IconSize::Small)
 3138                                                    .tooltip(move |_, cx| Tooltip::with_meta(
 3139                                                        "Discard Interrupted Edit",
 3140                                                        None,
 3141                                                        "You can discard this interrupted partial edit and restore the original file content.",
 3142                                                        cx
 3143                                                    ))
 3144                                                    .on_click(cx.listener({
 3145                                                        let tool_call_id = tool_call_id.clone();
 3146                                                        move |this, _, _window, cx| {
 3147                                                            let diff_data = diff.read(cx);
 3148                                                            let base_text = diff_data.base_text().clone();
 3149                                                            let buffer = diff_data.buffer().clone();
 3150                                                            buffer.update(cx, |buffer, cx| {
 3151                                                                buffer.set_text(base_text.as_ref(), cx);
 3152                                                            });
 3153                                                            if let Some(active) = this.as_active_thread_mut() {
 3154                                                                active.discarded_partial_edits.insert(tool_call_id.clone());
 3155                                                            }
 3156                                                            cx.notify();
 3157                                                        }
 3158                                                    })),
 3159                                                )
 3160                                            })
 3161                                        })
 3162
 3163                                )
 3164                            }),
 3165                    )
 3166                }
 3167            })
 3168            .children(tool_output_display)
 3169    }
 3170
 3171    fn render_tool_call_label(
 3172        &self,
 3173        entry_ix: usize,
 3174        tool_call: &ToolCall,
 3175        is_edit: bool,
 3176        has_failed: bool,
 3177        has_revealed_diff: bool,
 3178        use_card_layout: bool,
 3179        window: &Window,
 3180        cx: &Context<Self>,
 3181    ) -> Div {
 3182        let has_location = tool_call.locations.len() == 1;
 3183        let is_file = tool_call.kind == acp::ToolKind::Edit && has_location;
 3184
 3185        let file_icon = if has_location {
 3186            FileIcons::get_icon(&tool_call.locations[0].path, cx)
 3187                .map(Icon::from_path)
 3188                .unwrap_or(Icon::new(IconName::ToolPencil))
 3189        } else {
 3190            Icon::new(IconName::ToolPencil)
 3191        };
 3192
 3193        let tool_icon = if is_file && has_failed && has_revealed_diff {
 3194            div()
 3195                .id(entry_ix)
 3196                .tooltip(Tooltip::text("Interrupted Edit"))
 3197                .child(DecoratedIcon::new(
 3198                    file_icon,
 3199                    Some(
 3200                        IconDecoration::new(
 3201                            IconDecorationKind::Triangle,
 3202                            self.tool_card_header_bg(cx),
 3203                            cx,
 3204                        )
 3205                        .color(cx.theme().status().warning)
 3206                        .position(gpui::Point {
 3207                            x: px(-2.),
 3208                            y: px(-2.),
 3209                        }),
 3210                    ),
 3211                ))
 3212                .into_any_element()
 3213        } else if is_file {
 3214            div().child(file_icon).into_any_element()
 3215        } else {
 3216            div()
 3217                .child(
 3218                    Icon::new(match tool_call.kind {
 3219                        acp::ToolKind::Read => IconName::ToolSearch,
 3220                        acp::ToolKind::Edit => IconName::ToolPencil,
 3221                        acp::ToolKind::Delete => IconName::ToolDeleteFile,
 3222                        acp::ToolKind::Move => IconName::ArrowRightLeft,
 3223                        acp::ToolKind::Search => IconName::ToolSearch,
 3224                        acp::ToolKind::Execute => IconName::ToolTerminal,
 3225                        acp::ToolKind::Think => IconName::ToolThink,
 3226                        acp::ToolKind::Fetch => IconName::ToolWeb,
 3227                        acp::ToolKind::SwitchMode => IconName::ArrowRightLeft,
 3228                        acp::ToolKind::Other | _ => IconName::ToolHammer,
 3229                    })
 3230                    .size(IconSize::Small)
 3231                    .color(Color::Muted),
 3232                )
 3233                .into_any_element()
 3234        };
 3235
 3236        let gradient_overlay = {
 3237            div()
 3238                .absolute()
 3239                .top_0()
 3240                .right_0()
 3241                .w_12()
 3242                .h_full()
 3243                .map(|this| {
 3244                    if use_card_layout {
 3245                        this.bg(linear_gradient(
 3246                            90.,
 3247                            linear_color_stop(self.tool_card_header_bg(cx), 1.),
 3248                            linear_color_stop(self.tool_card_header_bg(cx).opacity(0.2), 0.),
 3249                        ))
 3250                    } else {
 3251                        this.bg(linear_gradient(
 3252                            90.,
 3253                            linear_color_stop(cx.theme().colors().panel_background, 1.),
 3254                            linear_color_stop(
 3255                                cx.theme().colors().panel_background.opacity(0.2),
 3256                                0.,
 3257                            ),
 3258                        ))
 3259                    }
 3260                })
 3261        };
 3262
 3263        h_flex()
 3264            .relative()
 3265            .w_full()
 3266            .h(window.line_height() - px(2.))
 3267            .text_size(self.tool_name_font_size())
 3268            .gap_1p5()
 3269            .when(has_location || use_card_layout, |this| this.px_1())
 3270            .when(has_location, |this| {
 3271                this.cursor(CursorStyle::PointingHand)
 3272                    .rounded(rems_from_px(3.)) // Concentric border radius
 3273                    .hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.5)))
 3274            })
 3275            .overflow_hidden()
 3276            .child(tool_icon)
 3277            .child(if has_location {
 3278                h_flex()
 3279                    .id(("open-tool-call-location", entry_ix))
 3280                    .w_full()
 3281                    .map(|this| {
 3282                        if use_card_layout {
 3283                            this.text_color(cx.theme().colors().text)
 3284                        } else {
 3285                            this.text_color(cx.theme().colors().text_muted)
 3286                        }
 3287                    })
 3288                    .child(
 3289                        self.render_markdown(
 3290                            tool_call.label.clone(),
 3291                            MarkdownStyle {
 3292                                prevent_mouse_interaction: true,
 3293                                ..MarkdownStyle::themed(MarkdownFont::Agent, window, cx)
 3294                                    .with_muted_text(cx)
 3295                            },
 3296                        ),
 3297                    )
 3298                    .tooltip(Tooltip::text("Go to File"))
 3299                    .on_click(cx.listener(move |this, _, window, cx| {
 3300                        this.open_tool_call_location(entry_ix, 0, window, cx);
 3301                    }))
 3302                    .into_any_element()
 3303            } else {
 3304                h_flex()
 3305                    .w_full()
 3306                    .child(self.render_markdown(
 3307                        tool_call.label.clone(),
 3308                        MarkdownStyle::themed(MarkdownFont::Agent, window, cx).with_muted_text(cx),
 3309                    ))
 3310                    .into_any()
 3311            })
 3312            .when(!is_edit, |this| this.child(gradient_overlay))
 3313    }
 3314
 3315    fn render_tool_call_content(
 3316        &self,
 3317        entry_ix: usize,
 3318        content: &ToolCallContent,
 3319        context_ix: usize,
 3320        tool_call: &ToolCall,
 3321        card_layout: bool,
 3322        is_image_tool_call: bool,
 3323        has_failed: bool,
 3324        window: &Window,
 3325        cx: &Context<Self>,
 3326    ) -> AnyElement {
 3327        match content {
 3328            ToolCallContent::ContentBlock(content) => {
 3329                if let Some(resource_link) = content.resource_link() {
 3330                    self.render_resource_link(resource_link, cx)
 3331                } else if let Some(markdown) = content.markdown() {
 3332                    self.render_markdown_output(
 3333                        markdown.clone(),
 3334                        tool_call.id.clone(),
 3335                        context_ix,
 3336                        card_layout,
 3337                        window,
 3338                        cx,
 3339                    )
 3340                } else if let Some(image) = content.image() {
 3341                    let location = tool_call.locations.first().cloned();
 3342                    self.render_image_output(
 3343                        entry_ix,
 3344                        image.clone(),
 3345                        location,
 3346                        card_layout,
 3347                        is_image_tool_call,
 3348                        cx,
 3349                    )
 3350                } else {
 3351                    Empty.into_any_element()
 3352                }
 3353            }
 3354            ToolCallContent::Diff(diff) => {
 3355                self.render_diff_editor(entry_ix, diff, tool_call, has_failed, cx)
 3356            }
 3357            ToolCallContent::Terminal(terminal) => {
 3358                self.render_terminal_tool_call(entry_ix, terminal, tool_call, window, cx)
 3359            }
 3360            ToolCallContent::SubagentThread(_thread) => {
 3361                // Subagent threads are rendered by render_subagent_tool_call, not here
 3362                Empty.into_any_element()
 3363            }
 3364        }
 3365    }
 3366
 3367    fn render_subagent_tool_call(
 3368        &self,
 3369        entry_ix: usize,
 3370        tool_call: &ToolCall,
 3371        window: &Window,
 3372        cx: &Context<Self>,
 3373    ) -> Div {
 3374        let subagent_threads: Vec<_> = tool_call
 3375            .content
 3376            .iter()
 3377            .filter_map(|c| c.subagent_thread().cloned())
 3378            .collect();
 3379
 3380        let tool_call_status = &tool_call.status;
 3381
 3382        v_flex()
 3383            .mx_5()
 3384            .my_1p5()
 3385            .gap_3()
 3386            .children(
 3387                subagent_threads
 3388                    .into_iter()
 3389                    .enumerate()
 3390                    .map(|(context_ix, thread)| {
 3391                        self.render_subagent_card(
 3392                            entry_ix,
 3393                            context_ix,
 3394                            &thread,
 3395                            tool_call_status,
 3396                            window,
 3397                            cx,
 3398                        )
 3399                    }),
 3400            )
 3401    }
 3402
 3403    fn render_subagent_card(
 3404        &self,
 3405        entry_ix: usize,
 3406        context_ix: usize,
 3407        thread: &Entity<AcpThread>,
 3408        tool_call_status: &ToolCallStatus,
 3409        window: &Window,
 3410        cx: &Context<Self>,
 3411    ) -> AnyElement {
 3412        let thread_read = thread.read(cx);
 3413        let session_id = thread_read.session_id().clone();
 3414        let title = thread_read.title();
 3415        let action_log = thread_read.action_log();
 3416        let changed_buffers = action_log.read(cx).changed_buffers(cx);
 3417
 3418        let is_expanded = if let Some(active) = self.as_active_thread() {
 3419            active.expanded_subagents.contains(&session_id)
 3420        } else {
 3421            false
 3422        };
 3423        let files_changed = changed_buffers.len();
 3424        let diff_stats = DiffStats::all_files(&changed_buffers, cx);
 3425
 3426        let is_running = matches!(
 3427            tool_call_status,
 3428            ToolCallStatus::Pending | ToolCallStatus::InProgress
 3429        );
 3430        let is_canceled_or_failed = matches!(
 3431            tool_call_status,
 3432            ToolCallStatus::Canceled | ToolCallStatus::Failed | ToolCallStatus::Rejected
 3433        );
 3434
 3435        let card_header_id =
 3436            SharedString::from(format!("subagent-header-{}-{}", entry_ix, context_ix));
 3437        let diff_stat_id = SharedString::from(format!("subagent-diff-{}-{}", entry_ix, context_ix));
 3438
 3439        let icon = h_flex().w_4().justify_center().child(if is_running {
 3440            SpinnerLabel::new()
 3441                .size(LabelSize::Small)
 3442                .into_any_element()
 3443        } else if is_canceled_or_failed {
 3444            Icon::new(IconName::Close)
 3445                .size(IconSize::Small)
 3446                .color(Color::Error)
 3447                .into_any_element()
 3448        } else {
 3449            Icon::new(IconName::Check)
 3450                .size(IconSize::Small)
 3451                .color(Color::Success)
 3452                .into_any_element()
 3453        });
 3454
 3455        let has_expandable_content = thread_read.entries().iter().rev().any(|entry| {
 3456            if let AgentThreadEntry::AssistantMessage(msg) = entry {
 3457                msg.chunks.iter().any(|chunk| match chunk {
 3458                    AssistantMessageChunk::Message { block } => block.markdown().is_some(),
 3459                    AssistantMessageChunk::Thought { block } => block.markdown().is_some(),
 3460                })
 3461            } else {
 3462                false
 3463            }
 3464        });
 3465
 3466        v_flex()
 3467            .w_full()
 3468            .rounded_md()
 3469            .border_1()
 3470            .border_color(self.tool_card_border_color(cx))
 3471            .overflow_hidden()
 3472            .child(
 3473                h_flex()
 3474                    .group(&card_header_id)
 3475                    .py_1()
 3476                    .px_1p5()
 3477                    .w_full()
 3478                    .gap_1()
 3479                    .justify_between()
 3480                    .bg(self.tool_card_header_bg(cx))
 3481                    .child(
 3482                        h_flex()
 3483                            .gap_1p5()
 3484                            .child(icon)
 3485                            .child(
 3486                                Label::new(title.to_string())
 3487                                    .size(LabelSize::Small)
 3488                                    .color(Color::Default),
 3489                            )
 3490                            .when(files_changed > 0, |this| {
 3491                                this.child(
 3492                                    h_flex()
 3493                                        .gap_1()
 3494                                        .child(
 3495                                            Label::new(format!(
 3496                                                "{} {} changed",
 3497                                                files_changed,
 3498                                                if files_changed == 1 { "file" } else { "files" }
 3499                                            ))
 3500                                            .size(LabelSize::Small)
 3501                                            .color(Color::Muted),
 3502                                        )
 3503                                        .child(DiffStat::new(
 3504                                            diff_stat_id.clone(),
 3505                                            diff_stats.lines_added as usize,
 3506                                            diff_stats.lines_removed as usize,
 3507                                        )),
 3508                                )
 3509                            }),
 3510                    )
 3511                    .child(
 3512                        h_flex()
 3513                            .gap_1p5()
 3514                            .when(is_running, |buttons| {
 3515                                buttons.child(
 3516                                    Button::new(
 3517                                        SharedString::from(format!(
 3518                                            "stop-subagent-{}-{}",
 3519                                            entry_ix, context_ix
 3520                                        )),
 3521                                        "Stop",
 3522                                    )
 3523                                    .icon(IconName::Stop)
 3524                                    .icon_position(IconPosition::Start)
 3525                                    .icon_size(IconSize::Small)
 3526                                    .icon_color(Color::Error)
 3527                                    .label_size(LabelSize::Small)
 3528                                    .tooltip(Tooltip::text("Stop this subagent"))
 3529                                    .on_click({
 3530                                        let thread = thread.clone();
 3531                                        cx.listener(move |_this, _event, _window, cx| {
 3532                                            thread.update(cx, |thread, _cx| {
 3533                                                thread.stop_by_user();
 3534                                            });
 3535                                        })
 3536                                    }),
 3537                                )
 3538                            })
 3539                            .child(
 3540                                IconButton::new(
 3541                                    SharedString::from(format!(
 3542                                        "subagent-disclosure-{}-{}",
 3543                                        entry_ix, context_ix
 3544                                    )),
 3545                                    if is_expanded {
 3546                                        IconName::ChevronUp
 3547                                    } else {
 3548                                        IconName::ChevronDown
 3549                                    },
 3550                                )
 3551                                .shape(IconButtonShape::Square)
 3552                                .icon_color(Color::Muted)
 3553                                .icon_size(IconSize::Small)
 3554                                .disabled(!has_expandable_content)
 3555                                .when(has_expandable_content, |button| {
 3556                                    button.on_click(cx.listener({
 3557                                        move |this, _, _, cx| {
 3558                                            if let Some(active) = this.as_active_thread_mut() {
 3559                                                if active.expanded_subagents.contains(&session_id) {
 3560                                                    active.expanded_subagents.remove(&session_id);
 3561                                                } else {
 3562                                                    active
 3563                                                        .expanded_subagents
 3564                                                        .insert(session_id.clone());
 3565                                                }
 3566                                            }
 3567                                            cx.notify();
 3568                                        }
 3569                                    }))
 3570                                })
 3571                                .when(
 3572                                    !has_expandable_content,
 3573                                    |button| {
 3574                                        button.tooltip(Tooltip::text("Waiting for content..."))
 3575                                    },
 3576                                ),
 3577                            ),
 3578                    ),
 3579            )
 3580            .when(is_expanded, |this| {
 3581                this.child(
 3582                    self.render_subagent_expanded_content(entry_ix, context_ix, thread, window, cx),
 3583                )
 3584            })
 3585            .children(
 3586                thread_read
 3587                    .first_tool_awaiting_confirmation()
 3588                    .and_then(|tc| {
 3589                        if let ToolCallStatus::WaitingForConfirmation { options, .. } = &tc.status {
 3590                            Some(self.render_subagent_pending_tool_call(
 3591                                entry_ix,
 3592                                context_ix,
 3593                                thread.clone(),
 3594                                tc,
 3595                                options,
 3596                                window,
 3597                                cx,
 3598                            ))
 3599                        } else {
 3600                            None
 3601                        }
 3602                    }),
 3603            )
 3604            .into_any_element()
 3605    }
 3606
 3607    fn render_subagent_expanded_content(
 3608        &self,
 3609        _entry_ix: usize,
 3610        _context_ix: usize,
 3611        thread: &Entity<AcpThread>,
 3612        window: &Window,
 3613        cx: &Context<Self>,
 3614    ) -> impl IntoElement {
 3615        let thread_read = thread.read(cx);
 3616        let session_id = thread_read.session_id().clone();
 3617        let entries = thread_read.entries();
 3618
 3619        // Find the most recent agent message with any content (message or thought)
 3620        let last_assistant_markdown = entries.iter().rev().find_map(|entry| {
 3621            if let AgentThreadEntry::AssistantMessage(msg) = entry {
 3622                msg.chunks.iter().find_map(|chunk| match chunk {
 3623                    AssistantMessageChunk::Message { block } => block.markdown().cloned(),
 3624                    AssistantMessageChunk::Thought { block } => block.markdown().cloned(),
 3625                })
 3626            } else {
 3627                None
 3628            }
 3629        });
 3630
 3631        let scroll_handle = self
 3632            .as_active_thread()
 3633            .map(|state| {
 3634                state
 3635                    .subagent_scroll_handles
 3636                    .borrow_mut()
 3637                    .entry(session_id.clone())
 3638                    .or_default()
 3639                    .clone()
 3640            })
 3641            .unwrap_or_default();
 3642
 3643        scroll_handle.scroll_to_bottom();
 3644        let editor_bg = cx.theme().colors().editor_background;
 3645
 3646        let gradient_overlay = {
 3647            div().absolute().inset_0().bg(linear_gradient(
 3648                180.,
 3649                linear_color_stop(editor_bg, 0.),
 3650                linear_color_stop(editor_bg.opacity(0.), 0.15),
 3651            ))
 3652        };
 3653
 3654        div()
 3655            .relative()
 3656            .w_full()
 3657            .max_h_56()
 3658            .p_2p5()
 3659            .text_ui(cx)
 3660            .border_t_1()
 3661            .border_color(self.tool_card_border_color(cx))
 3662            .bg(editor_bg.opacity(0.4))
 3663            .overflow_hidden()
 3664            .child(
 3665                div()
 3666                    .id(format!("subagent-content-{}", session_id))
 3667                    .size_full()
 3668                    .track_scroll(&scroll_handle)
 3669                    .when_some(last_assistant_markdown, |this, markdown| {
 3670                        this.child(self.render_markdown(
 3671                            markdown,
 3672                            MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
 3673                        ))
 3674                    }),
 3675            )
 3676            .child(gradient_overlay)
 3677    }
 3678
 3679    fn render_markdown_output(
 3680        &self,
 3681        markdown: Entity<Markdown>,
 3682        tool_call_id: acp::ToolCallId,
 3683        context_ix: usize,
 3684        card_layout: bool,
 3685        window: &Window,
 3686        cx: &Context<Self>,
 3687    ) -> AnyElement {
 3688        let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id));
 3689
 3690        v_flex()
 3691            .gap_2()
 3692            .map(|this| {
 3693                if card_layout {
 3694                    this.when(context_ix > 0, |this| {
 3695                        this.pt_2()
 3696                            .border_t_1()
 3697                            .border_color(self.tool_card_border_color(cx))
 3698                    })
 3699                } else {
 3700                    this.ml(rems(0.4))
 3701                        .px_3p5()
 3702                        .border_l_1()
 3703                        .border_color(self.tool_card_border_color(cx))
 3704                }
 3705            })
 3706            .text_xs()
 3707            .text_color(cx.theme().colors().text_muted)
 3708            .child(self.render_markdown(
 3709                markdown,
 3710                MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
 3711            ))
 3712            .when(!card_layout, |this| {
 3713                this.child(
 3714                    IconButton::new(button_id, IconName::ChevronUp)
 3715                        .full_width()
 3716                        .style(ButtonStyle::Outlined)
 3717                        .icon_color(Color::Muted)
 3718                        .on_click(cx.listener({
 3719                            move |this: &mut Self, _, _, cx: &mut Context<Self>| {
 3720                                if let Some(active) = this.as_active_thread_mut() {
 3721                                    active.expanded_tool_calls.remove(&tool_call_id);
 3722                                    cx.notify();
 3723                                }
 3724                            }
 3725                        })),
 3726                )
 3727            })
 3728            .into_any_element()
 3729    }
 3730
 3731    fn render_image_output(
 3732        &self,
 3733        entry_ix: usize,
 3734        image: Arc<gpui::Image>,
 3735        location: Option<acp::ToolCallLocation>,
 3736        card_layout: bool,
 3737        show_dimensions: bool,
 3738        cx: &Context<Self>,
 3739    ) -> AnyElement {
 3740        let dimensions_label = if show_dimensions {
 3741            let format_name = match image.format() {
 3742                gpui::ImageFormat::Png => "PNG",
 3743                gpui::ImageFormat::Jpeg => "JPEG",
 3744                gpui::ImageFormat::Webp => "WebP",
 3745                gpui::ImageFormat::Gif => "GIF",
 3746                gpui::ImageFormat::Svg => "SVG",
 3747                gpui::ImageFormat::Bmp => "BMP",
 3748                gpui::ImageFormat::Tiff => "TIFF",
 3749                gpui::ImageFormat::Ico => "ICO",
 3750            };
 3751            let dimensions = image::ImageReader::new(std::io::Cursor::new(image.bytes()))
 3752                .with_guessed_format()
 3753                .ok()
 3754                .and_then(|reader| reader.into_dimensions().ok());
 3755            dimensions.map(|(w, h)| format!("{}×{} {}", w, h, format_name))
 3756        } else {
 3757            None
 3758        };
 3759
 3760        v_flex()
 3761            .gap_2()
 3762            .map(|this| {
 3763                if card_layout {
 3764                    this
 3765                } else {
 3766                    this.ml(rems(0.4))
 3767                        .px_3p5()
 3768                        .border_l_1()
 3769                        .border_color(self.tool_card_border_color(cx))
 3770                }
 3771            })
 3772            .when(dimensions_label.is_some() || location.is_some(), |this| {
 3773                this.child(
 3774                    h_flex()
 3775                        .w_full()
 3776                        .justify_between()
 3777                        .items_center()
 3778                        .children(dimensions_label.map(|label| {
 3779                            Label::new(label)
 3780                                .size(LabelSize::XSmall)
 3781                                .color(Color::Muted)
 3782                                .buffer_font(cx)
 3783                        }))
 3784                        .when_some(location, |this, _loc| {
 3785                            this.child(
 3786                                Button::new(("go-to-file", entry_ix), "Go to File")
 3787                                    .label_size(LabelSize::Small)
 3788                                    .on_click(cx.listener(move |this, _, window, cx| {
 3789                                        this.open_tool_call_location(entry_ix, 0, window, cx);
 3790                                    })),
 3791                            )
 3792                        }),
 3793                )
 3794            })
 3795            .child(
 3796                img(image)
 3797                    .max_w_96()
 3798                    .max_h_96()
 3799                    .object_fit(ObjectFit::ScaleDown),
 3800            )
 3801            .into_any_element()
 3802    }
 3803
 3804    fn render_resource_link(
 3805        &self,
 3806        resource_link: &acp::ResourceLink,
 3807        cx: &Context<Self>,
 3808    ) -> AnyElement {
 3809        let uri: SharedString = resource_link.uri.clone().into();
 3810        let is_file = resource_link.uri.strip_prefix("file://");
 3811
 3812        let label: SharedString = if let Some(abs_path) = is_file {
 3813            if let Some(project_path) = self
 3814                .project
 3815                .read(cx)
 3816                .project_path_for_absolute_path(&Path::new(abs_path), cx)
 3817                && let Some(worktree) = self
 3818                    .project
 3819                    .read(cx)
 3820                    .worktree_for_id(project_path.worktree_id, cx)
 3821            {
 3822                worktree
 3823                    .read(cx)
 3824                    .full_path(&project_path.path)
 3825                    .to_string_lossy()
 3826                    .to_string()
 3827                    .into()
 3828            } else {
 3829                abs_path.to_string().into()
 3830            }
 3831        } else {
 3832            uri.clone()
 3833        };
 3834
 3835        let button_id = SharedString::from(format!("item-{}", uri));
 3836
 3837        div()
 3838            .ml(rems(0.4))
 3839            .pl_2p5()
 3840            .border_l_1()
 3841            .border_color(self.tool_card_border_color(cx))
 3842            .overflow_hidden()
 3843            .child(
 3844                Button::new(button_id, label)
 3845                    .label_size(LabelSize::Small)
 3846                    .color(Color::Muted)
 3847                    .truncate(true)
 3848                    .when(is_file.is_none(), |this| {
 3849                        this.icon(IconName::ArrowUpRight)
 3850                            .icon_size(IconSize::XSmall)
 3851                            .icon_color(Color::Muted)
 3852                    })
 3853                    .on_click(cx.listener({
 3854                        let workspace = self.workspace.clone();
 3855                        move |_, _, window, cx: &mut Context<Self>| {
 3856                            Self::open_link(uri.clone(), &workspace, window, cx);
 3857                        }
 3858                    })),
 3859            )
 3860            .into_any_element()
 3861    }
 3862
 3863    fn render_permission_buttons(
 3864        &self,
 3865        options: &PermissionOptions,
 3866        entry_ix: usize,
 3867        tool_call_id: acp::ToolCallId,
 3868        cx: &Context<Self>,
 3869    ) -> Div {
 3870        match options {
 3871            PermissionOptions::Flat(options) => {
 3872                self.render_permission_buttons_flat(options, entry_ix, tool_call_id, cx)
 3873            }
 3874            PermissionOptions::Dropdown(options) => {
 3875                self.render_permission_buttons_dropdown(options, entry_ix, tool_call_id, cx)
 3876            }
 3877        }
 3878    }
 3879
 3880    fn render_permission_buttons_dropdown(
 3881        &self,
 3882        choices: &[PermissionOptionChoice],
 3883        entry_ix: usize,
 3884        tool_call_id: acp::ToolCallId,
 3885        cx: &Context<Self>,
 3886    ) -> Div {
 3887        let is_first = self.as_active_thread().is_some_and(|active| {
 3888            active
 3889                .thread
 3890                .read(cx)
 3891                .first_tool_awaiting_confirmation()
 3892                .is_some_and(|call| call.id == tool_call_id)
 3893        });
 3894
 3895        // Get the selected granularity index, defaulting to the last option ("Only this time")
 3896        let selected_index = if let Some(active) = self.as_active_thread() {
 3897            active
 3898                .selected_permission_granularity
 3899                .get(&tool_call_id)
 3900                .copied()
 3901                .unwrap_or_else(|| choices.len().saturating_sub(1))
 3902        } else {
 3903            choices.len().saturating_sub(1)
 3904        };
 3905
 3906        let selected_choice = choices.get(selected_index).or(choices.last());
 3907
 3908        let dropdown_label: SharedString = selected_choice
 3909            .map(|choice| choice.label())
 3910            .unwrap_or_else(|| "Only this time".into());
 3911
 3912        let (allow_option_id, allow_option_kind, deny_option_id, deny_option_kind) =
 3913            if let Some(choice) = selected_choice {
 3914                (
 3915                    choice.allow.option_id.clone(),
 3916                    choice.allow.kind,
 3917                    choice.deny.option_id.clone(),
 3918                    choice.deny.kind,
 3919                )
 3920            } else {
 3921                (
 3922                    acp::PermissionOptionId::new("allow"),
 3923                    acp::PermissionOptionKind::AllowOnce,
 3924                    acp::PermissionOptionId::new("deny"),
 3925                    acp::PermissionOptionKind::RejectOnce,
 3926                )
 3927            };
 3928
 3929        h_flex()
 3930            .w_full()
 3931            .p_1()
 3932            .gap_2()
 3933            .justify_between()
 3934            .border_t_1()
 3935            .border_color(self.tool_card_border_color(cx))
 3936            .child(
 3937                h_flex()
 3938                    .gap_0p5()
 3939                    .child(
 3940                        Button::new(("allow-btn", entry_ix), "Allow")
 3941                            .icon(IconName::Check)
 3942                            .icon_color(Color::Success)
 3943                            .icon_position(IconPosition::Start)
 3944                            .icon_size(IconSize::XSmall)
 3945                            .label_size(LabelSize::Small)
 3946                            .when(is_first, |this| {
 3947                                this.key_binding(
 3948                                    KeyBinding::for_action_in(
 3949                                        &AllowOnce as &dyn Action,
 3950                                        &self.focus_handle,
 3951                                        cx,
 3952                                    )
 3953                                    .map(|kb| kb.size(rems_from_px(10.))),
 3954                                )
 3955                            })
 3956                            .on_click(cx.listener({
 3957                                let tool_call_id = tool_call_id.clone();
 3958                                let option_id = allow_option_id;
 3959                                let option_kind = allow_option_kind;
 3960                                move |this, _, window, cx| {
 3961                                    this.authorize_tool_call(
 3962                                        tool_call_id.clone(),
 3963                                        option_id.clone(),
 3964                                        option_kind,
 3965                                        window,
 3966                                        cx,
 3967                                    );
 3968                                }
 3969                            })),
 3970                    )
 3971                    .child(
 3972                        Button::new(("deny-btn", entry_ix), "Deny")
 3973                            .icon(IconName::Close)
 3974                            .icon_color(Color::Error)
 3975                            .icon_position(IconPosition::Start)
 3976                            .icon_size(IconSize::XSmall)
 3977                            .label_size(LabelSize::Small)
 3978                            .when(is_first, |this| {
 3979                                this.key_binding(
 3980                                    KeyBinding::for_action_in(
 3981                                        &RejectOnce as &dyn Action,
 3982                                        &self.focus_handle,
 3983                                        cx,
 3984                                    )
 3985                                    .map(|kb| kb.size(rems_from_px(10.))),
 3986                                )
 3987                            })
 3988                            .on_click(cx.listener({
 3989                                let tool_call_id = tool_call_id.clone();
 3990                                let option_id = deny_option_id;
 3991                                let option_kind = deny_option_kind;
 3992                                move |this, _, window, cx| {
 3993                                    this.authorize_tool_call(
 3994                                        tool_call_id.clone(),
 3995                                        option_id.clone(),
 3996                                        option_kind,
 3997                                        window,
 3998                                        cx,
 3999                                    );
 4000                                }
 4001                            })),
 4002                    ),
 4003            )
 4004            .child(self.render_permission_granularity_dropdown(
 4005                choices,
 4006                dropdown_label,
 4007                entry_ix,
 4008                tool_call_id,
 4009                selected_index,
 4010                is_first,
 4011                cx,
 4012            ))
 4013    }
 4014
 4015    fn render_permission_granularity_dropdown(
 4016        &self,
 4017        choices: &[PermissionOptionChoice],
 4018        current_label: SharedString,
 4019        entry_ix: usize,
 4020        tool_call_id: acp::ToolCallId,
 4021        selected_index: usize,
 4022        is_first: bool,
 4023        cx: &Context<Self>,
 4024    ) -> AnyElement {
 4025        let menu_options: Vec<(usize, SharedString)> = choices
 4026            .iter()
 4027            .enumerate()
 4028            .map(|(i, choice)| (i, choice.label()))
 4029            .collect();
 4030
 4031        let permission_dropdown_handle = match self.as_active_thread() {
 4032            Some(thread) => thread.permission_dropdown_handle.clone(),
 4033            None => return div().into_any_element(),
 4034        };
 4035
 4036        PopoverMenu::new(("permission-granularity", entry_ix))
 4037            .with_handle(permission_dropdown_handle)
 4038            .trigger(
 4039                Button::new(("granularity-trigger", entry_ix), current_label)
 4040                    .icon(IconName::ChevronDown)
 4041                    .icon_size(IconSize::XSmall)
 4042                    .icon_color(Color::Muted)
 4043                    .label_size(LabelSize::Small)
 4044                    .when(is_first, |this| {
 4045                        this.key_binding(
 4046                            KeyBinding::for_action_in(
 4047                                &crate::OpenPermissionDropdown as &dyn Action,
 4048                                &self.focus_handle,
 4049                                cx,
 4050                            )
 4051                            .map(|kb| kb.size(rems_from_px(10.))),
 4052                        )
 4053                    }),
 4054            )
 4055            .menu(move |window, cx| {
 4056                let tool_call_id = tool_call_id.clone();
 4057                let options = menu_options.clone();
 4058
 4059                Some(ContextMenu::build(window, cx, move |mut menu, _, _| {
 4060                    for (index, display_name) in options.iter() {
 4061                        let display_name = display_name.clone();
 4062                        let index = *index;
 4063                        let tool_call_id_for_entry = tool_call_id.clone();
 4064                        let is_selected = index == selected_index;
 4065
 4066                        menu = menu.toggleable_entry(
 4067                            display_name,
 4068                            is_selected,
 4069                            IconPosition::End,
 4070                            None,
 4071                            move |window, cx| {
 4072                                window.dispatch_action(
 4073                                    SelectPermissionGranularity {
 4074                                        tool_call_id: tool_call_id_for_entry.0.to_string(),
 4075                                        index,
 4076                                    }
 4077                                    .boxed_clone(),
 4078                                    cx,
 4079                                );
 4080                            },
 4081                        );
 4082                    }
 4083
 4084                    menu
 4085                }))
 4086            })
 4087            .into_any_element()
 4088    }
 4089
 4090    fn render_permission_buttons_flat(
 4091        &self,
 4092        options: &[acp::PermissionOption],
 4093        entry_ix: usize,
 4094        tool_call_id: acp::ToolCallId,
 4095        cx: &Context<Self>,
 4096    ) -> Div {
 4097        let is_first = self.as_active_thread().is_some_and(|active| {
 4098            active
 4099                .thread
 4100                .read(cx)
 4101                .first_tool_awaiting_confirmation()
 4102                .is_some_and(|call| call.id == tool_call_id)
 4103        });
 4104        let mut seen_kinds: ArrayVec<acp::PermissionOptionKind, 3> = ArrayVec::new();
 4105
 4106        div()
 4107            .p_1()
 4108            .border_t_1()
 4109            .border_color(self.tool_card_border_color(cx))
 4110            .w_full()
 4111            .v_flex()
 4112            .gap_0p5()
 4113            .children(options.iter().map(move |option| {
 4114                let option_id = SharedString::from(option.option_id.0.clone());
 4115                Button::new((option_id, entry_ix), option.name.clone())
 4116                    .map(|this| {
 4117                        let (this, action) = match option.kind {
 4118                            acp::PermissionOptionKind::AllowOnce => (
 4119                                this.icon(IconName::Check).icon_color(Color::Success),
 4120                                Some(&AllowOnce as &dyn Action),
 4121                            ),
 4122                            acp::PermissionOptionKind::AllowAlways => (
 4123                                this.icon(IconName::CheckDouble).icon_color(Color::Success),
 4124                                Some(&AllowAlways as &dyn Action),
 4125                            ),
 4126                            acp::PermissionOptionKind::RejectOnce => (
 4127                                this.icon(IconName::Close).icon_color(Color::Error),
 4128                                Some(&RejectOnce as &dyn Action),
 4129                            ),
 4130                            acp::PermissionOptionKind::RejectAlways | _ => {
 4131                                (this.icon(IconName::Close).icon_color(Color::Error), None)
 4132                            }
 4133                        };
 4134
 4135                        let Some(action) = action else {
 4136                            return this;
 4137                        };
 4138
 4139                        if !is_first || seen_kinds.contains(&option.kind) {
 4140                            return this;
 4141                        }
 4142
 4143                        seen_kinds.push(option.kind);
 4144
 4145                        this.key_binding(
 4146                            KeyBinding::for_action_in(action, &self.focus_handle, cx)
 4147                                .map(|kb| kb.size(rems_from_px(10.))),
 4148                        )
 4149                    })
 4150                    .icon_position(IconPosition::Start)
 4151                    .icon_size(IconSize::XSmall)
 4152                    .label_size(LabelSize::Small)
 4153                    .on_click(cx.listener({
 4154                        let tool_call_id = tool_call_id.clone();
 4155                        let option_id = option.option_id.clone();
 4156                        let option_kind = option.kind;
 4157                        move |this, _, window, cx| {
 4158                            this.authorize_tool_call(
 4159                                tool_call_id.clone(),
 4160                                option_id.clone(),
 4161                                option_kind,
 4162                                window,
 4163                                cx,
 4164                            );
 4165                        }
 4166                    }))
 4167            }))
 4168    }
 4169
 4170    fn render_subagent_pending_tool_call(
 4171        &self,
 4172        entry_ix: usize,
 4173        context_ix: usize,
 4174        subagent_thread: Entity<AcpThread>,
 4175        tool_call: &ToolCall,
 4176        options: &PermissionOptions,
 4177        window: &Window,
 4178        cx: &Context<Self>,
 4179    ) -> Div {
 4180        let tool_call_id = tool_call.id.clone();
 4181        let is_edit =
 4182            matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some();
 4183        let has_image_content = tool_call.content.iter().any(|c| c.image().is_some());
 4184
 4185        v_flex()
 4186            .w_full()
 4187            .border_t_1()
 4188            .border_color(self.tool_card_border_color(cx))
 4189            .child(
 4190                self.render_tool_call_label(
 4191                    entry_ix, tool_call, is_edit, false, // has_failed
 4192                    false, // has_revealed_diff
 4193                    true,  // use_card_layout
 4194                    window, cx,
 4195                )
 4196                .py_1(),
 4197            )
 4198            .children(
 4199                tool_call
 4200                    .content
 4201                    .iter()
 4202                    .enumerate()
 4203                    .map(|(content_ix, content)| {
 4204                        self.render_tool_call_content(
 4205                            entry_ix,
 4206                            content,
 4207                            content_ix,
 4208                            tool_call,
 4209                            true, // card_layout
 4210                            has_image_content,
 4211                            false, // has_failed
 4212                            window,
 4213                            cx,
 4214                        )
 4215                    }),
 4216            )
 4217            .child(self.render_subagent_permission_buttons(
 4218                entry_ix,
 4219                context_ix,
 4220                subagent_thread,
 4221                tool_call_id,
 4222                options,
 4223                cx,
 4224            ))
 4225    }
 4226
 4227    fn render_subagent_permission_buttons(
 4228        &self,
 4229        entry_ix: usize,
 4230        context_ix: usize,
 4231        subagent_thread: Entity<AcpThread>,
 4232        tool_call_id: acp::ToolCallId,
 4233        options: &PermissionOptions,
 4234        cx: &Context<Self>,
 4235    ) -> Div {
 4236        match options {
 4237            PermissionOptions::Flat(options) => self.render_subagent_permission_buttons_flat(
 4238                entry_ix,
 4239                context_ix,
 4240                subagent_thread,
 4241                tool_call_id,
 4242                options,
 4243                cx,
 4244            ),
 4245            PermissionOptions::Dropdown(options) => self
 4246                .render_subagent_permission_buttons_dropdown(
 4247                    entry_ix,
 4248                    context_ix,
 4249                    subagent_thread,
 4250                    tool_call_id,
 4251                    options,
 4252                    cx,
 4253                ),
 4254        }
 4255    }
 4256
 4257    fn render_subagent_permission_buttons_flat(
 4258        &self,
 4259        entry_ix: usize,
 4260        context_ix: usize,
 4261        subagent_thread: Entity<AcpThread>,
 4262        tool_call_id: acp::ToolCallId,
 4263        options: &[acp::PermissionOption],
 4264        cx: &Context<Self>,
 4265    ) -> Div {
 4266        div()
 4267            .p_1()
 4268            .border_t_1()
 4269            .border_color(self.tool_card_border_color(cx))
 4270            .w_full()
 4271            .v_flex()
 4272            .gap_0p5()
 4273            .children(options.iter().map(move |option| {
 4274                let option_id = SharedString::from(format!(
 4275                    "subagent-{}-{}-{}",
 4276                    entry_ix, context_ix, option.option_id.0
 4277                ));
 4278                Button::new((option_id, entry_ix), option.name.clone())
 4279                    .map(|this| match option.kind {
 4280                        acp::PermissionOptionKind::AllowOnce => {
 4281                            this.icon(IconName::Check).icon_color(Color::Success)
 4282                        }
 4283                        acp::PermissionOptionKind::AllowAlways => {
 4284                            this.icon(IconName::CheckDouble).icon_color(Color::Success)
 4285                        }
 4286                        acp::PermissionOptionKind::RejectOnce
 4287                        | acp::PermissionOptionKind::RejectAlways
 4288                        | _ => this.icon(IconName::Close).icon_color(Color::Error),
 4289                    })
 4290                    .icon_position(IconPosition::Start)
 4291                    .icon_size(IconSize::XSmall)
 4292                    .label_size(LabelSize::Small)
 4293                    .on_click(cx.listener({
 4294                        let subagent_thread = subagent_thread.clone();
 4295                        let tool_call_id = tool_call_id.clone();
 4296                        let option_id = option.option_id.clone();
 4297                        let option_kind = option.kind;
 4298                        move |this, _, window, cx| {
 4299                            this.authorize_subagent_tool_call(
 4300                                subagent_thread.clone(),
 4301                                tool_call_id.clone(),
 4302                                option_id.clone(),
 4303                                option_kind,
 4304                                window,
 4305                                cx,
 4306                            );
 4307                        }
 4308                    }))
 4309            }))
 4310    }
 4311
 4312    fn render_subagent_permission_buttons_dropdown(
 4313        &self,
 4314        entry_ix: usize,
 4315        context_ix: usize,
 4316        subagent_thread: Entity<AcpThread>,
 4317        tool_call_id: acp::ToolCallId,
 4318        choices: &[PermissionOptionChoice],
 4319        cx: &Context<Self>,
 4320    ) -> Div {
 4321        let selected_index = if let Some(active) = self.as_active_thread() {
 4322            active
 4323                .selected_permission_granularity
 4324                .get(&tool_call_id)
 4325                .copied()
 4326                .unwrap_or_else(|| choices.len().saturating_sub(1))
 4327        } else {
 4328            choices.len().saturating_sub(1)
 4329        };
 4330
 4331        let selected_choice = choices.get(selected_index).or(choices.last());
 4332
 4333        let dropdown_label: SharedString = selected_choice
 4334            .map(|choice| choice.label())
 4335            .unwrap_or_else(|| "Only this time".into());
 4336
 4337        let (allow_option_id, allow_option_kind, deny_option_id, deny_option_kind) =
 4338            if let Some(choice) = selected_choice {
 4339                (
 4340                    choice.allow.option_id.clone(),
 4341                    choice.allow.kind,
 4342                    choice.deny.option_id.clone(),
 4343                    choice.deny.kind,
 4344                )
 4345            } else {
 4346                (
 4347                    acp::PermissionOptionId::new("allow"),
 4348                    acp::PermissionOptionKind::AllowOnce,
 4349                    acp::PermissionOptionId::new("deny"),
 4350                    acp::PermissionOptionKind::RejectOnce,
 4351                )
 4352            };
 4353
 4354        h_flex()
 4355            .w_full()
 4356            .p_1()
 4357            .gap_2()
 4358            .justify_between()
 4359            .border_t_1()
 4360            .border_color(self.tool_card_border_color(cx))
 4361            .child(
 4362                h_flex()
 4363                    .gap_0p5()
 4364                    .child(
 4365                        Button::new(
 4366                            (
 4367                                SharedString::from(format!(
 4368                                    "subagent-allow-btn-{}-{}",
 4369                                    entry_ix, context_ix
 4370                                )),
 4371                                entry_ix,
 4372                            ),
 4373                            "Allow",
 4374                        )
 4375                        .icon(IconName::Check)
 4376                        .icon_color(Color::Success)
 4377                        .icon_position(IconPosition::Start)
 4378                        .icon_size(IconSize::XSmall)
 4379                        .label_size(LabelSize::Small)
 4380                        .on_click(cx.listener({
 4381                            let subagent_thread = subagent_thread.clone();
 4382                            let tool_call_id = tool_call_id.clone();
 4383                            let option_id = allow_option_id;
 4384                            let option_kind = allow_option_kind;
 4385                            move |this, _, window, cx| {
 4386                                this.authorize_subagent_tool_call(
 4387                                    subagent_thread.clone(),
 4388                                    tool_call_id.clone(),
 4389                                    option_id.clone(),
 4390                                    option_kind,
 4391                                    window,
 4392                                    cx,
 4393                                );
 4394                            }
 4395                        })),
 4396                    )
 4397                    .child(
 4398                        Button::new(
 4399                            (
 4400                                SharedString::from(format!(
 4401                                    "subagent-deny-btn-{}-{}",
 4402                                    entry_ix, context_ix
 4403                                )),
 4404                                entry_ix,
 4405                            ),
 4406                            "Deny",
 4407                        )
 4408                        .icon(IconName::Close)
 4409                        .icon_color(Color::Error)
 4410                        .icon_position(IconPosition::Start)
 4411                        .icon_size(IconSize::XSmall)
 4412                        .label_size(LabelSize::Small)
 4413                        .on_click(cx.listener({
 4414                            let tool_call_id = tool_call_id.clone();
 4415                            let option_id = deny_option_id;
 4416                            let option_kind = deny_option_kind;
 4417                            move |this, _, window, cx| {
 4418                                this.authorize_subagent_tool_call(
 4419                                    subagent_thread.clone(),
 4420                                    tool_call_id.clone(),
 4421                                    option_id.clone(),
 4422                                    option_kind,
 4423                                    window,
 4424                                    cx,
 4425                                );
 4426                            }
 4427                        })),
 4428                    ),
 4429            )
 4430            .child(self.render_subagent_permission_granularity_dropdown(
 4431                choices,
 4432                dropdown_label,
 4433                entry_ix,
 4434                context_ix,
 4435                tool_call_id,
 4436                selected_index,
 4437                cx,
 4438            ))
 4439    }
 4440
 4441    fn render_subagent_permission_granularity_dropdown(
 4442        &self,
 4443        choices: &[PermissionOptionChoice],
 4444        current_label: SharedString,
 4445        entry_ix: usize,
 4446        context_ix: usize,
 4447        tool_call_id: acp::ToolCallId,
 4448        selected_index: usize,
 4449        _cx: &Context<Self>,
 4450    ) -> AnyElement {
 4451        let menu_options: Vec<(usize, SharedString)> = choices
 4452            .iter()
 4453            .enumerate()
 4454            .map(|(i, choice)| (i, choice.label()))
 4455            .collect();
 4456
 4457        let permission_dropdown_handle = match self.as_active_thread() {
 4458            Some(thread) => thread.permission_dropdown_handle.clone(),
 4459            _ => return div().into_any_element(),
 4460        };
 4461
 4462        PopoverMenu::new((
 4463            SharedString::from(format!(
 4464                "subagent-permission-granularity-{}-{}",
 4465                entry_ix, context_ix
 4466            )),
 4467            entry_ix,
 4468        ))
 4469        .with_handle(permission_dropdown_handle)
 4470        .trigger(
 4471            Button::new(
 4472                (
 4473                    SharedString::from(format!(
 4474                        "subagent-granularity-trigger-{}-{}",
 4475                        entry_ix, context_ix
 4476                    )),
 4477                    entry_ix,
 4478                ),
 4479                current_label,
 4480            )
 4481            .icon(IconName::ChevronDown)
 4482            .icon_size(IconSize::XSmall)
 4483            .icon_color(Color::Muted)
 4484            .label_size(LabelSize::Small),
 4485        )
 4486        .menu(move |window, cx| {
 4487            let tool_call_id = tool_call_id.clone();
 4488            let options = menu_options.clone();
 4489
 4490            Some(ContextMenu::build(window, cx, move |mut menu, _, _| {
 4491                for (index, display_name) in options.iter() {
 4492                    let display_name = display_name.clone();
 4493                    let index = *index;
 4494                    let tool_call_id_for_entry = tool_call_id.clone();
 4495                    let is_selected = index == selected_index;
 4496
 4497                    menu = menu.toggleable_entry(
 4498                        display_name,
 4499                        is_selected,
 4500                        IconPosition::End,
 4501                        None,
 4502                        move |window, cx| {
 4503                            window.dispatch_action(
 4504                                SelectPermissionGranularity {
 4505                                    tool_call_id: tool_call_id_for_entry.0.to_string(),
 4506                                    index,
 4507                                }
 4508                                .boxed_clone(),
 4509                                cx,
 4510                            );
 4511                        },
 4512                    );
 4513                }
 4514
 4515                menu
 4516            }))
 4517        })
 4518        .into_any_element()
 4519    }
 4520
 4521    fn render_diff_loading(&self, cx: &Context<Self>) -> AnyElement {
 4522        let bar = |n: u64, width_class: &str| {
 4523            let bg_color = cx.theme().colors().element_active;
 4524            let base = h_flex().h_1().rounded_full();
 4525
 4526            let modified = match width_class {
 4527                "w_4_5" => base.w_3_4(),
 4528                "w_1_4" => base.w_1_4(),
 4529                "w_2_4" => base.w_2_4(),
 4530                "w_3_5" => base.w_3_5(),
 4531                "w_2_5" => base.w_2_5(),
 4532                _ => base.w_1_2(),
 4533            };
 4534
 4535            modified.with_animation(
 4536                ElementId::Integer(n),
 4537                Animation::new(Duration::from_secs(2)).repeat(),
 4538                move |tab, delta| {
 4539                    let delta = (delta - 0.15 * n as f32) / 0.7;
 4540                    let delta = 1.0 - (0.5 - delta).abs() * 2.;
 4541                    let delta = ease_in_out(delta.clamp(0., 1.));
 4542                    let delta = 0.1 + 0.9 * delta;
 4543
 4544                    tab.bg(bg_color.opacity(delta))
 4545                },
 4546            )
 4547        };
 4548
 4549        v_flex()
 4550            .p_3()
 4551            .gap_1()
 4552            .rounded_b_md()
 4553            .bg(cx.theme().colors().editor_background)
 4554            .child(bar(0, "w_4_5"))
 4555            .child(bar(1, "w_1_4"))
 4556            .child(bar(2, "w_2_4"))
 4557            .child(bar(3, "w_3_5"))
 4558            .child(bar(4, "w_2_5"))
 4559            .into_any_element()
 4560    }
 4561
 4562    fn render_diff_editor(
 4563        &self,
 4564        entry_ix: usize,
 4565        diff: &Entity<acp_thread::Diff>,
 4566        tool_call: &ToolCall,
 4567        has_failed: bool,
 4568        cx: &Context<Self>,
 4569    ) -> AnyElement {
 4570        let tool_progress = matches!(
 4571            &tool_call.status,
 4572            ToolCallStatus::InProgress | ToolCallStatus::Pending
 4573        );
 4574
 4575        let revealed_diff_editor = if let Some(entry_view_state) = self
 4576            .as_active_thread()
 4577            .map(|active| &active.entry_view_state)
 4578            && let Some(entry) = entry_view_state.read(cx).entry(entry_ix)
 4579            && let Some(editor) = entry.editor_for_diff(diff)
 4580            && diff.read(cx).has_revealed_range(cx)
 4581        {
 4582            Some(editor)
 4583        } else {
 4584            None
 4585        };
 4586
 4587        let show_top_border = !has_failed || revealed_diff_editor.is_some();
 4588
 4589        v_flex()
 4590            .h_full()
 4591            .when(show_top_border, |this| {
 4592                this.border_t_1()
 4593                    .when(has_failed, |this| this.border_dashed())
 4594                    .border_color(self.tool_card_border_color(cx))
 4595            })
 4596            .child(if let Some(editor) = revealed_diff_editor {
 4597                editor.into_any_element()
 4598            } else if tool_progress && self.as_native_connection(cx).is_some() {
 4599                self.render_diff_loading(cx)
 4600            } else {
 4601                Empty.into_any()
 4602            })
 4603            .into_any()
 4604    }
 4605
 4606    fn render_collapsible_command(
 4607        &self,
 4608        is_preview: bool,
 4609        command_source: &str,
 4610        tool_call_id: &acp::ToolCallId,
 4611        cx: &Context<Self>,
 4612    ) -> Div {
 4613        let command_group =
 4614            SharedString::from(format!("collapsible-command-group-{}", tool_call_id));
 4615
 4616        v_flex()
 4617            .group(command_group.clone())
 4618            .bg(self.tool_card_header_bg(cx))
 4619            .child(
 4620                v_flex()
 4621                    .p_1p5()
 4622                    .when(is_preview, |this| {
 4623                        this.pt_1().child(
 4624                            // Wrapping this label on a container with 24px height to avoid
 4625                            // layout shift when it changes from being a preview label
 4626                            // to the actual path where the command will run in
 4627                            h_flex().h_6().child(
 4628                                Label::new("Run Command")
 4629                                    .buffer_font(cx)
 4630                                    .size(LabelSize::XSmall)
 4631                                    .color(Color::Muted),
 4632                            ),
 4633                        )
 4634                    })
 4635                    .children(command_source.lines().map(|line| {
 4636                        let text: SharedString = if line.is_empty() {
 4637                            " ".into()
 4638                        } else {
 4639                            line.to_string().into()
 4640                        };
 4641
 4642                        Label::new(text).buffer_font(cx).size(LabelSize::Small)
 4643                    }))
 4644                    .child(
 4645                        div().absolute().top_1().right_1().child(
 4646                            CopyButton::new("copy-command", command_source.to_string())
 4647                                .tooltip_label("Copy Command")
 4648                                .visible_on_hover(command_group),
 4649                        ),
 4650                    ),
 4651            )
 4652    }
 4653
 4654    fn render_terminal_tool_call(
 4655        &self,
 4656        entry_ix: usize,
 4657        terminal: &Entity<acp_thread::Terminal>,
 4658        tool_call: &ToolCall,
 4659        window: &Window,
 4660        cx: &Context<Self>,
 4661    ) -> AnyElement {
 4662        let terminal_data = terminal.read(cx);
 4663        let working_dir = terminal_data.working_dir();
 4664        let command = terminal_data.command();
 4665        let started_at = terminal_data.started_at();
 4666
 4667        let tool_failed = matches!(
 4668            &tool_call.status,
 4669            ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed
 4670        );
 4671
 4672        let output = terminal_data.output();
 4673        let command_finished = output.is_some();
 4674        let truncated_output =
 4675            output.is_some_and(|output| output.original_content_len > output.content.len());
 4676        let output_line_count = output.map(|output| output.content_line_count).unwrap_or(0);
 4677
 4678        let command_failed = command_finished
 4679            && output.is_some_and(|o| o.exit_status.is_some_and(|status| !status.success()));
 4680
 4681        let time_elapsed = if let Some(output) = output {
 4682            output.ended_at.duration_since(started_at)
 4683        } else {
 4684            started_at.elapsed()
 4685        };
 4686
 4687        let header_id =
 4688            SharedString::from(format!("terminal-tool-header-{}", terminal.entity_id()));
 4689        let header_group = SharedString::from(format!(
 4690            "terminal-tool-header-group-{}",
 4691            terminal.entity_id()
 4692        ));
 4693        let header_bg = cx
 4694            .theme()
 4695            .colors()
 4696            .element_background
 4697            .blend(cx.theme().colors().editor_foreground.opacity(0.025));
 4698        let border_color = cx.theme().colors().border.opacity(0.6);
 4699
 4700        let working_dir = working_dir
 4701            .as_ref()
 4702            .map(|path| path.display().to_string())
 4703            .unwrap_or_else(|| "current directory".to_string());
 4704
 4705        // Since the command's source is wrapped in a markdown code block
 4706        // (```\n...\n```), we need to strip that so we're left with only the
 4707        // command's content.
 4708        let command_source = command.read(cx).source();
 4709        let command_content = command_source
 4710            .strip_prefix("```\n")
 4711            .and_then(|s| s.strip_suffix("\n```"))
 4712            .unwrap_or(&command_source);
 4713
 4714        let command_element =
 4715            self.render_collapsible_command(false, command_content, &tool_call.id, cx);
 4716
 4717        let is_expanded = self.as_connected().is_some_and(|c| {
 4718            c.current.as_ref().map_or(false, |view| {
 4719                view.expanded_tool_calls.contains(&tool_call.id)
 4720            })
 4721        });
 4722
 4723        let header = h_flex()
 4724            .id(header_id)
 4725            .px_1p5()
 4726            .pt_1()
 4727            .flex_none()
 4728            .gap_1()
 4729            .justify_between()
 4730            .rounded_t_md()
 4731            .child(
 4732                div()
 4733                    .id(("command-target-path", terminal.entity_id()))
 4734                    .w_full()
 4735                    .max_w_full()
 4736                    .overflow_x_scroll()
 4737                    .child(
 4738                        Label::new(working_dir)
 4739                            .buffer_font(cx)
 4740                            .size(LabelSize::XSmall)
 4741                            .color(Color::Muted),
 4742                    ),
 4743            )
 4744            .when(!command_finished, |header| {
 4745                header
 4746                    .gap_1p5()
 4747                    .child(
 4748                        Button::new(
 4749                            SharedString::from(format!("stop-terminal-{}", terminal.entity_id())),
 4750                            "Stop",
 4751                        )
 4752                        .icon(IconName::Stop)
 4753                        .icon_position(IconPosition::Start)
 4754                        .icon_size(IconSize::Small)
 4755                        .icon_color(Color::Error)
 4756                        .label_size(LabelSize::Small)
 4757                        .tooltip(move |_window, cx| {
 4758                            Tooltip::with_meta(
 4759                                "Stop This Command",
 4760                                None,
 4761                                "Also possible by placing your cursor inside the terminal and using regular terminal bindings.",
 4762                                cx,
 4763                            )
 4764                        })
 4765                        .on_click({
 4766                            let terminal = terminal.clone();
 4767                            cx.listener(move |this, _event, _window, cx| {
 4768                                terminal.update(cx, |terminal, cx| {
 4769                                    terminal.stop_by_user(cx);
 4770                                });
 4771                                if AgentSettings::get_global(cx).cancel_generation_on_terminal_stop {
 4772                                    this.cancel_generation(cx);
 4773                                }
 4774                            })
 4775                        }),
 4776                    )
 4777                    .child(Divider::vertical())
 4778                    .child(
 4779                        Icon::new(IconName::ArrowCircle)
 4780                            .size(IconSize::XSmall)
 4781                            .color(Color::Info)
 4782                            .with_rotate_animation(2)
 4783                    )
 4784            })
 4785            .when(truncated_output, |header| {
 4786                let tooltip = if let Some(output) = output {
 4787                    if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
 4788                       format!("Output exceeded terminal max lines and was \
 4789                            truncated, the model received the first {}.", format_file_size(output.content.len() as u64, true))
 4790                    } else {
 4791                        format!(
 4792                            "Output is {} long, and to avoid unexpected token usage, \
 4793                                only {} was sent back to the agent.",
 4794                            format_file_size(output.original_content_len as u64, true),
 4795                             format_file_size(output.content.len() as u64, true)
 4796                        )
 4797                    }
 4798                } else {
 4799                    "Output was truncated".to_string()
 4800                };
 4801
 4802                header.child(
 4803                    h_flex()
 4804                        .id(("terminal-tool-truncated-label", terminal.entity_id()))
 4805                        .gap_1()
 4806                        .child(
 4807                            Icon::new(IconName::Info)
 4808                                .size(IconSize::XSmall)
 4809                                .color(Color::Ignored),
 4810                        )
 4811                        .child(
 4812                            Label::new("Truncated")
 4813                                .color(Color::Muted)
 4814                                .size(LabelSize::XSmall),
 4815                        )
 4816                        .tooltip(Tooltip::text(tooltip)),
 4817                )
 4818            })
 4819            .when(time_elapsed > Duration::from_secs(10), |header| {
 4820                header.child(
 4821                    Label::new(format!("({})", duration_alt_display(time_elapsed)))
 4822                        .buffer_font(cx)
 4823                        .color(Color::Muted)
 4824                        .size(LabelSize::XSmall),
 4825                )
 4826            })
 4827            .when(tool_failed || command_failed, |header| {
 4828                header.child(
 4829                    div()
 4830                        .id(("terminal-tool-error-code-indicator", terminal.entity_id()))
 4831                        .child(
 4832                            Icon::new(IconName::Close)
 4833                                .size(IconSize::Small)
 4834                                .color(Color::Error),
 4835                        )
 4836                        .when_some(output.and_then(|o| o.exit_status), |this, status| {
 4837                            this.tooltip(Tooltip::text(format!(
 4838                                "Exited with code {}",
 4839                                status.code().unwrap_or(-1),
 4840                            )))
 4841                        }),
 4842                )
 4843            })
 4844            .child(
 4845                Disclosure::new(
 4846                    SharedString::from(format!(
 4847                        "terminal-tool-disclosure-{}",
 4848                        terminal.entity_id()
 4849                    )),
 4850                    is_expanded,
 4851                )
 4852                .opened_icon(IconName::ChevronUp)
 4853                .closed_icon(IconName::ChevronDown)
 4854                .visible_on_hover(&header_group)
 4855                .on_click(cx.listener({
 4856                    let id = tool_call.id.clone();
 4857                    move |this, _event, _window, _cx| {
 4858                        if let Some(active) = this.as_active_thread_mut() {
 4859                            if is_expanded {
 4860                                active.expanded_tool_calls.remove(&id);
 4861                            } else {
 4862                                active.expanded_tool_calls.insert(id.clone());
 4863                            }
 4864                        }
 4865                    }
 4866                })),
 4867            );
 4868
 4869        let terminal_view = self
 4870            .as_active_thread()
 4871            .map(|active| &active.entry_view_state)
 4872            .and_then(|entry_view_state| {
 4873                entry_view_state
 4874                    .read(cx)
 4875                    .entry(entry_ix)
 4876                    .and_then(|entry| entry.terminal(terminal))
 4877            });
 4878
 4879        v_flex()
 4880            .my_1p5()
 4881            .mx_5()
 4882            .border_1()
 4883            .when(tool_failed || command_failed, |card| card.border_dashed())
 4884            .border_color(border_color)
 4885            .rounded_md()
 4886            .overflow_hidden()
 4887            .child(
 4888                v_flex()
 4889                    .group(&header_group)
 4890                    .bg(header_bg)
 4891                    .text_xs()
 4892                    .child(header)
 4893                    .child(command_element),
 4894            )
 4895            .when(is_expanded && terminal_view.is_some(), |this| {
 4896                this.child(
 4897                    div()
 4898                        .pt_2()
 4899                        .border_t_1()
 4900                        .when(tool_failed || command_failed, |card| card.border_dashed())
 4901                        .border_color(border_color)
 4902                        .bg(cx.theme().colors().editor_background)
 4903                        .rounded_b_md()
 4904                        .text_ui_sm(cx)
 4905                        .h_full()
 4906                        .children(terminal_view.map(|terminal_view| {
 4907                            let element = if terminal_view
 4908                                .read(cx)
 4909                                .content_mode(window, cx)
 4910                                .is_scrollable()
 4911                            {
 4912                                div().h_72().child(terminal_view).into_any_element()
 4913                            } else {
 4914                                terminal_view.into_any_element()
 4915                            };
 4916
 4917                            div()
 4918                                .on_action(cx.listener(|_this, _: &NewTerminal, window, cx| {
 4919                                    window.dispatch_action(NewThread.boxed_clone(), cx);
 4920                                    cx.stop_propagation();
 4921                                }))
 4922                                .child(element)
 4923                                .into_any_element()
 4924                        })),
 4925                )
 4926            })
 4927            .into_any()
 4928    }
 4929
 4930    fn render_rules_item(&self, cx: &Context<Self>) -> Option<AnyElement> {
 4931        let project_context = self
 4932            .as_native_thread(cx)?
 4933            .read(cx)
 4934            .project_context()
 4935            .read(cx);
 4936
 4937        let user_rules_text = if project_context.user_rules.is_empty() {
 4938            None
 4939        } else if project_context.user_rules.len() == 1 {
 4940            let user_rules = &project_context.user_rules[0];
 4941
 4942            match user_rules.title.as_ref() {
 4943                Some(title) => Some(format!("Using \"{title}\" user rule")),
 4944                None => Some("Using user rule".into()),
 4945            }
 4946        } else {
 4947            Some(format!(
 4948                "Using {} user rules",
 4949                project_context.user_rules.len()
 4950            ))
 4951        };
 4952
 4953        let first_user_rules_id = project_context
 4954            .user_rules
 4955            .first()
 4956            .map(|user_rules| user_rules.uuid.0);
 4957
 4958        let rules_files = project_context
 4959            .worktrees
 4960            .iter()
 4961            .filter_map(|worktree| worktree.rules_file.as_ref())
 4962            .collect::<Vec<_>>();
 4963
 4964        let rules_file_text = match rules_files.as_slice() {
 4965            &[] => None,
 4966            &[rules_file] => Some(format!(
 4967                "Using project {:?} file",
 4968                rules_file.path_in_worktree
 4969            )),
 4970            rules_files => Some(format!("Using {} project rules files", rules_files.len())),
 4971        };
 4972
 4973        if user_rules_text.is_none() && rules_file_text.is_none() {
 4974            return None;
 4975        }
 4976
 4977        let has_both = user_rules_text.is_some() && rules_file_text.is_some();
 4978
 4979        Some(
 4980            h_flex()
 4981                .px_2p5()
 4982                .child(
 4983                    Icon::new(IconName::Attach)
 4984                        .size(IconSize::XSmall)
 4985                        .color(Color::Disabled),
 4986                )
 4987                .when_some(user_rules_text, |parent, user_rules_text| {
 4988                    parent.child(
 4989                        h_flex()
 4990                            .id("user-rules")
 4991                            .ml_1()
 4992                            .mr_1p5()
 4993                            .child(
 4994                                Label::new(user_rules_text)
 4995                                    .size(LabelSize::XSmall)
 4996                                    .color(Color::Muted)
 4997                                    .truncate(),
 4998                            )
 4999                            .hover(|s| s.bg(cx.theme().colors().element_hover))
 5000                            .tooltip(Tooltip::text("View User Rules"))
 5001                            .on_click(move |_event, window, cx| {
 5002                                window.dispatch_action(
 5003                                    Box::new(OpenRulesLibrary {
 5004                                        prompt_to_select: first_user_rules_id,
 5005                                    }),
 5006                                    cx,
 5007                                )
 5008                            }),
 5009                    )
 5010                })
 5011                .when(has_both, |this| {
 5012                    this.child(
 5013                        Label::new("")
 5014                            .size(LabelSize::XSmall)
 5015                            .color(Color::Disabled),
 5016                    )
 5017                })
 5018                .when_some(rules_file_text, |parent, rules_file_text| {
 5019                    parent.child(
 5020                        h_flex()
 5021                            .id("project-rules")
 5022                            .ml_1p5()
 5023                            .child(
 5024                                Label::new(rules_file_text)
 5025                                    .size(LabelSize::XSmall)
 5026                                    .color(Color::Muted),
 5027                            )
 5028                            .hover(|s| s.bg(cx.theme().colors().element_hover))
 5029                            .tooltip(Tooltip::text("View Project Rules"))
 5030                            .on_click(cx.listener(Self::handle_open_rules)),
 5031                    )
 5032                })
 5033                .into_any(),
 5034        )
 5035    }
 5036
 5037    fn render_empty_state_section_header(
 5038        &self,
 5039        label: impl Into<SharedString>,
 5040        action_slot: Option<AnyElement>,
 5041        cx: &mut Context<Self>,
 5042    ) -> impl IntoElement {
 5043        div().pl_1().pr_1p5().child(
 5044            h_flex()
 5045                .mt_2()
 5046                .pl_1p5()
 5047                .pb_1()
 5048                .w_full()
 5049                .justify_between()
 5050                .border_b_1()
 5051                .border_color(cx.theme().colors().border_variant)
 5052                .child(
 5053                    Label::new(label.into())
 5054                        .size(LabelSize::Small)
 5055                        .color(Color::Muted),
 5056                )
 5057                .children(action_slot),
 5058        )
 5059    }
 5060
 5061    fn render_resume_notice(&self, _cx: &Context<Self>) -> AnyElement {
 5062        let description = "This agent does not support viewing previous messages. However, your session will still continue from where you last left off.";
 5063
 5064        div()
 5065            .px_2()
 5066            .pt_2()
 5067            .pb_3()
 5068            .w_full()
 5069            .child(
 5070                Callout::new()
 5071                    .severity(Severity::Info)
 5072                    .icon(IconName::Info)
 5073                    .title("Resumed Session")
 5074                    .description(description),
 5075            )
 5076            .into_any_element()
 5077    }
 5078
 5079    fn update_recent_history_from_cache(
 5080        &mut self,
 5081        history: &Entity<AcpThreadHistory>,
 5082        cx: &mut Context<Self>,
 5083    ) {
 5084        self.recent_history_entries = history.read(cx).get_recent_sessions(3);
 5085        self.hovered_recent_history_item = None;
 5086        cx.notify();
 5087    }
 5088
 5089    fn render_recent_history(&self, cx: &mut Context<Self>) -> AnyElement {
 5090        let render_history = !self.recent_history_entries.is_empty();
 5091
 5092        v_flex()
 5093            .size_full()
 5094            .when(render_history, |this| {
 5095                let recent_history = self.recent_history_entries.clone();
 5096                this.justify_end().child(
 5097                    v_flex()
 5098                        .child(
 5099                            self.render_empty_state_section_header(
 5100                                "Recent",
 5101                                Some(
 5102                                    Button::new("view-history", "View All")
 5103                                        .style(ButtonStyle::Subtle)
 5104                                        .label_size(LabelSize::Small)
 5105                                        .key_binding(
 5106                                            KeyBinding::for_action_in(
 5107                                                &OpenHistory,
 5108                                                &self.focus_handle(cx),
 5109                                                cx,
 5110                                            )
 5111                                            .map(|kb| kb.size(rems_from_px(12.))),
 5112                                        )
 5113                                        .on_click(move |_event, window, cx| {
 5114                                            window.dispatch_action(OpenHistory.boxed_clone(), cx);
 5115                                        })
 5116                                        .into_any_element(),
 5117                                ),
 5118                                cx,
 5119                            ),
 5120                        )
 5121                        .child(v_flex().p_1().pr_1p5().gap_1().children({
 5122                            let supports_delete = self.history.read(cx).supports_delete();
 5123                            recent_history
 5124                                .into_iter()
 5125                                .enumerate()
 5126                                .map(move |(index, entry)| {
 5127                                    // TODO: Add keyboard navigation.
 5128                                    let is_hovered =
 5129                                        self.hovered_recent_history_item == Some(index);
 5130                                    crate::acp::thread_history::AcpHistoryEntryElement::new(
 5131                                        entry,
 5132                                        cx.entity().downgrade(),
 5133                                    )
 5134                                    .hovered(is_hovered)
 5135                                    .supports_delete(supports_delete)
 5136                                    .on_hover(cx.listener(move |this, is_hovered, _window, cx| {
 5137                                        if *is_hovered {
 5138                                            this.hovered_recent_history_item = Some(index);
 5139                                        } else if this.hovered_recent_history_item == Some(index) {
 5140                                            this.hovered_recent_history_item = None;
 5141                                        }
 5142                                        cx.notify();
 5143                                    }))
 5144                                    .into_any_element()
 5145                                })
 5146                        })),
 5147                )
 5148            })
 5149            .into_any()
 5150    }
 5151
 5152    fn render_auth_required_state(
 5153        &self,
 5154        connection: &Rc<dyn AgentConnection>,
 5155        description: Option<&Entity<Markdown>>,
 5156        configuration_view: Option<&AnyView>,
 5157        pending_auth_method: Option<&acp::AuthMethodId>,
 5158        window: &mut Window,
 5159        cx: &Context<Self>,
 5160    ) -> impl IntoElement {
 5161        let auth_methods = connection.auth_methods();
 5162
 5163        let agent_display_name = self
 5164            .agent_server_store
 5165            .read(cx)
 5166            .agent_display_name(&ExternalAgentServerName(self.agent.name()))
 5167            .unwrap_or_else(|| self.agent.name());
 5168
 5169        let show_fallback_description = auth_methods.len() > 1
 5170            && configuration_view.is_none()
 5171            && description.is_none()
 5172            && pending_auth_method.is_none();
 5173
 5174        let auth_buttons = || {
 5175            h_flex().justify_end().flex_wrap().gap_1().children(
 5176                connection
 5177                    .auth_methods()
 5178                    .iter()
 5179                    .enumerate()
 5180                    .rev()
 5181                    .map(|(ix, method)| {
 5182                        let (method_id, name) = if self.project.read(cx).is_via_remote_server()
 5183                            && method.id.0.as_ref() == "oauth-personal"
 5184                            && method.name == "Log in with Google"
 5185                        {
 5186                            ("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
 5187                        } else {
 5188                            (method.id.0.clone(), method.name.clone())
 5189                        };
 5190
 5191                        let agent_telemetry_id = connection.telemetry_id();
 5192
 5193                        Button::new(method_id.clone(), name)
 5194                            .label_size(LabelSize::Small)
 5195                            .map(|this| {
 5196                                if ix == 0 {
 5197                                    this.style(ButtonStyle::Tinted(TintColor::Accent))
 5198                                } else {
 5199                                    this.style(ButtonStyle::Outlined)
 5200                                }
 5201                            })
 5202                            .when_some(method.description.clone(), |this, description| {
 5203                                this.tooltip(Tooltip::text(description))
 5204                            })
 5205                            .on_click({
 5206                                cx.listener(move |this, _, window, cx| {
 5207                                    telemetry::event!(
 5208                                        "Authenticate Agent Started",
 5209                                        agent = agent_telemetry_id,
 5210                                        method = method_id
 5211                                    );
 5212
 5213                                    this.authenticate(
 5214                                        acp::AuthMethodId::new(method_id.clone()),
 5215                                        window,
 5216                                        cx,
 5217                                    )
 5218                                })
 5219                            })
 5220                    }),
 5221            )
 5222        };
 5223
 5224        if pending_auth_method.is_some() {
 5225            return Callout::new()
 5226                .icon(IconName::Info)
 5227                .title(format!("Authenticating to {}", agent_display_name))
 5228                .actions_slot(
 5229                    Icon::new(IconName::ArrowCircle)
 5230                        .size(IconSize::Small)
 5231                        .color(Color::Muted)
 5232                        .with_rotate_animation(2)
 5233                        .into_any_element(),
 5234                )
 5235                .into_any_element();
 5236        }
 5237
 5238        Callout::new()
 5239            .icon(IconName::Info)
 5240            .title(format!("Authenticate to {}", agent_display_name))
 5241            .when(auth_methods.len() == 1, |this| {
 5242                this.actions_slot(auth_buttons())
 5243            })
 5244            .description_slot(
 5245                v_flex()
 5246                    .text_ui(cx)
 5247                    .map(|this| {
 5248                        if show_fallback_description {
 5249                            this.child(
 5250                                Label::new("Choose one of the following authentication options:")
 5251                                    .size(LabelSize::Small)
 5252                                    .color(Color::Muted),
 5253                            )
 5254                        } else {
 5255                            this.children(
 5256                                configuration_view
 5257                                    .cloned()
 5258                                    .map(|view| div().w_full().child(view)),
 5259                            )
 5260                            .children(description.map(|desc| {
 5261                                self.render_markdown(
 5262                                    desc.clone(),
 5263                                    MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
 5264                                )
 5265                            }))
 5266                        }
 5267                    })
 5268                    .when(auth_methods.len() > 1, |this| {
 5269                        this.gap_1().child(auth_buttons())
 5270                    }),
 5271            )
 5272            .into_any_element()
 5273    }
 5274
 5275    fn render_load_error(
 5276        &self,
 5277        e: &LoadError,
 5278        window: &mut Window,
 5279        cx: &mut Context<Self>,
 5280    ) -> AnyElement {
 5281        let (title, message, action_slot): (_, SharedString, _) = match e {
 5282            LoadError::Unsupported {
 5283                command: path,
 5284                current_version,
 5285                minimum_version,
 5286            } => {
 5287                return self.render_unsupported(path, current_version, minimum_version, window, cx);
 5288            }
 5289            LoadError::FailedToInstall(msg) => (
 5290                "Failed to Install",
 5291                msg.into(),
 5292                Some(self.create_copy_button(msg.to_string()).into_any_element()),
 5293            ),
 5294            LoadError::Exited { status } => (
 5295                "Failed to Launch",
 5296                format!("Server exited with status {status}").into(),
 5297                None,
 5298            ),
 5299            LoadError::Other(msg) => (
 5300                "Failed to Launch",
 5301                msg.into(),
 5302                Some(self.create_copy_button(msg.to_string()).into_any_element()),
 5303            ),
 5304        };
 5305
 5306        Callout::new()
 5307            .severity(Severity::Error)
 5308            .icon(IconName::XCircleFilled)
 5309            .title(title)
 5310            .description(message)
 5311            .actions_slot(div().children(action_slot))
 5312            .into_any_element()
 5313    }
 5314
 5315    fn render_unsupported(
 5316        &self,
 5317        path: &SharedString,
 5318        version: &SharedString,
 5319        minimum_version: &SharedString,
 5320        _window: &mut Window,
 5321        cx: &mut Context<Self>,
 5322    ) -> AnyElement {
 5323        let (heading_label, description_label) = (
 5324            format!("Upgrade {} to work with Zed", self.agent.name()),
 5325            if version.is_empty() {
 5326                format!(
 5327                    "Currently using {}, which does not report a valid --version",
 5328                    path,
 5329                )
 5330            } else {
 5331                format!(
 5332                    "Currently using {}, which is only version {} (need at least {minimum_version})",
 5333                    path, version
 5334                )
 5335            },
 5336        );
 5337
 5338        v_flex()
 5339            .w_full()
 5340            .p_3p5()
 5341            .gap_2p5()
 5342            .border_t_1()
 5343            .border_color(cx.theme().colors().border)
 5344            .bg(linear_gradient(
 5345                180.,
 5346                linear_color_stop(cx.theme().colors().editor_background.opacity(0.4), 4.),
 5347                linear_color_stop(cx.theme().status().info_background.opacity(0.), 0.),
 5348            ))
 5349            .child(
 5350                v_flex().gap_0p5().child(Label::new(heading_label)).child(
 5351                    Label::new(description_label)
 5352                        .size(LabelSize::Small)
 5353                        .color(Color::Muted),
 5354                ),
 5355            )
 5356            .into_any_element()
 5357    }
 5358
 5359    fn activity_bar_bg(&self, cx: &Context<Self>) -> Hsla {
 5360        let editor_bg_color = cx.theme().colors().editor_background;
 5361        let active_color = cx.theme().colors().element_selected;
 5362        editor_bg_color.blend(active_color.opacity(0.3))
 5363    }
 5364
 5365    fn render_activity_bar(
 5366        &self,
 5367        thread_entity: &Entity<AcpThread>,
 5368        window: &mut Window,
 5369        cx: &Context<Self>,
 5370    ) -> Option<AnyElement> {
 5371        let thread = thread_entity.read(cx);
 5372        let action_log = thread.action_log();
 5373        let telemetry = ActionLogTelemetry::from(thread);
 5374        let changed_buffers = action_log.read(cx).changed_buffers(cx);
 5375        let plan = thread.plan();
 5376        let queue_is_empty = !self.has_queued_messages();
 5377
 5378        if changed_buffers.is_empty() && plan.is_empty() && queue_is_empty {
 5379            return None;
 5380        }
 5381
 5382        // Temporarily always enable ACP edit controls. This is temporary, to lessen the
 5383        // impact of a nasty bug that causes them to sometimes be disabled when they shouldn't
 5384        // be, which blocks you from being able to accept or reject edits. This switches the
 5385        // bug to be that sometimes it's enabled when it shouldn't be, which at least doesn't
 5386        // block you from using the panel.
 5387        let pending_edits = false;
 5388
 5389        let Some(active) = self.as_active_thread() else {
 5390            return None;
 5391        };
 5392
 5393        v_flex()
 5394            .mt_1()
 5395            .mx_2()
 5396            .bg(self.activity_bar_bg(cx))
 5397            .border_1()
 5398            .border_b_0()
 5399            .border_color(cx.theme().colors().border)
 5400            .rounded_t_md()
 5401            .shadow(vec![gpui::BoxShadow {
 5402                color: gpui::black().opacity(0.15),
 5403                offset: point(px(1.), px(-1.)),
 5404                blur_radius: px(3.),
 5405                spread_radius: px(0.),
 5406            }])
 5407            .when(!plan.is_empty(), |this| {
 5408                this.child(self.render_plan_summary(plan, window, cx))
 5409                    .when(active.plan_expanded, |parent| {
 5410                        parent.child(self.render_plan_entries(plan, window, cx))
 5411                    })
 5412            })
 5413            .when(!plan.is_empty() && !changed_buffers.is_empty(), |this| {
 5414                this.child(Divider::horizontal().color(DividerColor::Border))
 5415            })
 5416            .when(!changed_buffers.is_empty(), |this| {
 5417                this.child(self.render_edits_summary(
 5418                    &changed_buffers,
 5419                    active.edits_expanded,
 5420                    pending_edits,
 5421                    cx,
 5422                ))
 5423                .when(active.edits_expanded, |parent| {
 5424                    parent.child(self.render_edited_files(
 5425                        action_log,
 5426                        telemetry.clone(),
 5427                        &changed_buffers,
 5428                        pending_edits,
 5429                        cx,
 5430                    ))
 5431                })
 5432            })
 5433            .when(!queue_is_empty, |this| {
 5434                this.when(!plan.is_empty() || !changed_buffers.is_empty(), |this| {
 5435                    this.child(Divider::horizontal().color(DividerColor::Border))
 5436                })
 5437                .child(self.render_message_queue_summary(window, cx))
 5438                .when(active.queue_expanded, |parent| {
 5439                    parent.child(self.render_message_queue_entries(window, cx))
 5440                })
 5441            })
 5442            .into_any()
 5443            .into()
 5444    }
 5445
 5446    fn render_plan_summary(
 5447        &self,
 5448        plan: &Plan,
 5449        window: &mut Window,
 5450        cx: &Context<Self>,
 5451    ) -> impl IntoElement {
 5452        let Some(active) = self.as_active_thread() else {
 5453            return Empty.into_any_element();
 5454        };
 5455        let stats = plan.stats();
 5456
 5457        let title = if let Some(entry) = stats.in_progress_entry
 5458            && !active.plan_expanded
 5459        {
 5460            h_flex()
 5461                .cursor_default()
 5462                .relative()
 5463                .w_full()
 5464                .gap_1()
 5465                .truncate()
 5466                .child(
 5467                    Label::new("Current:")
 5468                        .size(LabelSize::Small)
 5469                        .color(Color::Muted),
 5470                )
 5471                .child(
 5472                    div()
 5473                        .text_xs()
 5474                        .text_color(cx.theme().colors().text_muted)
 5475                        .line_clamp(1)
 5476                        .child(MarkdownElement::new(
 5477                            entry.content.clone(),
 5478                            plan_label_markdown_style(&entry.status, window, cx),
 5479                        )),
 5480                )
 5481                .when(stats.pending > 0, |this| {
 5482                    this.child(
 5483                        h_flex()
 5484                            .absolute()
 5485                            .top_0()
 5486                            .right_0()
 5487                            .h_full()
 5488                            .child(div().min_w_8().h_full().bg(linear_gradient(
 5489                                90.,
 5490                                linear_color_stop(self.activity_bar_bg(cx), 1.),
 5491                                linear_color_stop(self.activity_bar_bg(cx).opacity(0.2), 0.),
 5492                            )))
 5493                            .child(
 5494                                div().pr_0p5().bg(self.activity_bar_bg(cx)).child(
 5495                                    Label::new(format!("{} left", stats.pending))
 5496                                        .size(LabelSize::Small)
 5497                                        .color(Color::Muted),
 5498                                ),
 5499                            ),
 5500                    )
 5501                })
 5502        } else {
 5503            let status_label = if stats.pending == 0 {
 5504                "All Done".to_string()
 5505            } else if stats.completed == 0 {
 5506                format!("{} Tasks", plan.entries.len())
 5507            } else {
 5508                format!("{}/{}", stats.completed, plan.entries.len())
 5509            };
 5510
 5511            h_flex()
 5512                .w_full()
 5513                .gap_1()
 5514                .justify_between()
 5515                .child(
 5516                    Label::new("Plan")
 5517                        .size(LabelSize::Small)
 5518                        .color(Color::Muted),
 5519                )
 5520                .child(
 5521                    Label::new(status_label)
 5522                        .size(LabelSize::Small)
 5523                        .color(Color::Muted)
 5524                        .mr_1(),
 5525                )
 5526        };
 5527
 5528        h_flex()
 5529            .id("plan_summary")
 5530            .p_1()
 5531            .w_full()
 5532            .gap_1()
 5533            .when(active.plan_expanded, |this| {
 5534                this.border_b_1().border_color(cx.theme().colors().border)
 5535            })
 5536            .child(Disclosure::new("plan_disclosure", active.plan_expanded))
 5537            .child(title)
 5538            .on_click(cx.listener(|this, _, _, cx| {
 5539                let Some(active) = this.as_active_thread_mut() else {
 5540                    return;
 5541                };
 5542                active.plan_expanded = !active.plan_expanded;
 5543                cx.notify();
 5544            }))
 5545            .into_any_element()
 5546    }
 5547
 5548    fn render_plan_entries(
 5549        &self,
 5550        plan: &Plan,
 5551        window: &mut Window,
 5552        cx: &Context<Self>,
 5553    ) -> impl IntoElement {
 5554        v_flex()
 5555            .id("plan_items_list")
 5556            .max_h_40()
 5557            .overflow_y_scroll()
 5558            .children(plan.entries.iter().enumerate().flat_map(|(index, entry)| {
 5559                let element = h_flex()
 5560                    .py_1()
 5561                    .px_2()
 5562                    .gap_2()
 5563                    .justify_between()
 5564                    .bg(cx.theme().colors().editor_background)
 5565                    .when(index < plan.entries.len() - 1, |parent| {
 5566                        parent.border_color(cx.theme().colors().border).border_b_1()
 5567                    })
 5568                    .child(
 5569                        h_flex()
 5570                            .id(("plan_entry", index))
 5571                            .gap_1p5()
 5572                            .max_w_full()
 5573                            .overflow_x_scroll()
 5574                            .text_xs()
 5575                            .text_color(cx.theme().colors().text_muted)
 5576                            .child(match entry.status {
 5577                                acp::PlanEntryStatus::InProgress => {
 5578                                    Icon::new(IconName::TodoProgress)
 5579                                        .size(IconSize::Small)
 5580                                        .color(Color::Accent)
 5581                                        .with_rotate_animation(2)
 5582                                        .into_any_element()
 5583                                }
 5584                                acp::PlanEntryStatus::Completed => {
 5585                                    Icon::new(IconName::TodoComplete)
 5586                                        .size(IconSize::Small)
 5587                                        .color(Color::Success)
 5588                                        .into_any_element()
 5589                                }
 5590                                acp::PlanEntryStatus::Pending | _ => {
 5591                                    Icon::new(IconName::TodoPending)
 5592                                        .size(IconSize::Small)
 5593                                        .color(Color::Muted)
 5594                                        .into_any_element()
 5595                                }
 5596                            })
 5597                            .child(MarkdownElement::new(
 5598                                entry.content.clone(),
 5599                                plan_label_markdown_style(&entry.status, window, cx),
 5600                            )),
 5601                    );
 5602
 5603                Some(element)
 5604            }))
 5605            .into_any_element()
 5606    }
 5607
 5608    fn render_edits_summary(
 5609        &self,
 5610        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
 5611        expanded: bool,
 5612        pending_edits: bool,
 5613        cx: &Context<Self>,
 5614    ) -> Div {
 5615        const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete.";
 5616
 5617        let focus_handle = self.focus_handle(cx);
 5618
 5619        h_flex()
 5620            .p_1()
 5621            .justify_between()
 5622            .flex_wrap()
 5623            .when(expanded, |this| {
 5624                this.border_b_1().border_color(cx.theme().colors().border)
 5625            })
 5626            .child(
 5627                h_flex()
 5628                    .id("edits-container")
 5629                    .cursor_pointer()
 5630                    .gap_1()
 5631                    .child(Disclosure::new("edits-disclosure", expanded))
 5632                    .map(|this| {
 5633                        if pending_edits {
 5634                            this.child(
 5635                                Label::new(format!(
 5636                                    "Editing {} {}",
 5637                                    changed_buffers.len(),
 5638                                    if changed_buffers.len() == 1 {
 5639                                        "file"
 5640                                    } else {
 5641                                        "files"
 5642                                    }
 5643                                ))
 5644                                .color(Color::Muted)
 5645                                .size(LabelSize::Small)
 5646                                .with_animation(
 5647                                    "edit-label",
 5648                                    Animation::new(Duration::from_secs(2))
 5649                                        .repeat()
 5650                                        .with_easing(pulsating_between(0.3, 0.7)),
 5651                                    |label, delta| label.alpha(delta),
 5652                                ),
 5653                            )
 5654                        } else {
 5655                            let stats = DiffStats::all_files(changed_buffers, cx);
 5656                            let dot_divider = || {
 5657                                Label::new("")
 5658                                    .size(LabelSize::XSmall)
 5659                                    .color(Color::Disabled)
 5660                            };
 5661
 5662                            this.child(
 5663                                Label::new("Edits")
 5664                                    .size(LabelSize::Small)
 5665                                    .color(Color::Muted),
 5666                            )
 5667                            .child(dot_divider())
 5668                            .child(
 5669                                Label::new(format!(
 5670                                    "{} {}",
 5671                                    changed_buffers.len(),
 5672                                    if changed_buffers.len() == 1 {
 5673                                        "file"
 5674                                    } else {
 5675                                        "files"
 5676                                    }
 5677                                ))
 5678                                .size(LabelSize::Small)
 5679                                .color(Color::Muted),
 5680                            )
 5681                            .child(dot_divider())
 5682                            .child(DiffStat::new(
 5683                                "total",
 5684                                stats.lines_added as usize,
 5685                                stats.lines_removed as usize,
 5686                            ))
 5687                        }
 5688                    })
 5689                    .on_click(cx.listener(|this, _, _, cx| {
 5690                        let Some(active) = this.as_active_thread_mut() else {
 5691                            return;
 5692                        };
 5693                        active.edits_expanded = !active.edits_expanded;
 5694                        cx.notify();
 5695                    })),
 5696            )
 5697            .child(
 5698                h_flex()
 5699                    .gap_1()
 5700                    .child(
 5701                        IconButton::new("review-changes", IconName::ListTodo)
 5702                            .icon_size(IconSize::Small)
 5703                            .tooltip({
 5704                                let focus_handle = focus_handle.clone();
 5705                                move |_window, cx| {
 5706                                    Tooltip::for_action_in(
 5707                                        "Review Changes",
 5708                                        &OpenAgentDiff,
 5709                                        &focus_handle,
 5710                                        cx,
 5711                                    )
 5712                                }
 5713                            })
 5714                            .on_click(cx.listener(|_, _, window, cx| {
 5715                                window.dispatch_action(OpenAgentDiff.boxed_clone(), cx);
 5716                            })),
 5717                    )
 5718                    .child(Divider::vertical().color(DividerColor::Border))
 5719                    .child(
 5720                        Button::new("reject-all-changes", "Reject All")
 5721                            .label_size(LabelSize::Small)
 5722                            .disabled(pending_edits)
 5723                            .when(pending_edits, |this| {
 5724                                this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
 5725                            })
 5726                            .key_binding(
 5727                                KeyBinding::for_action_in(&RejectAll, &focus_handle.clone(), cx)
 5728                                    .map(|kb| kb.size(rems_from_px(10.))),
 5729                            )
 5730                            .on_click(cx.listener(move |this, _, window, cx| {
 5731                                this.reject_all(&RejectAll, window, cx);
 5732                            })),
 5733                    )
 5734                    .child(
 5735                        Button::new("keep-all-changes", "Keep All")
 5736                            .label_size(LabelSize::Small)
 5737                            .disabled(pending_edits)
 5738                            .when(pending_edits, |this| {
 5739                                this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
 5740                            })
 5741                            .key_binding(
 5742                                KeyBinding::for_action_in(&KeepAll, &focus_handle, cx)
 5743                                    .map(|kb| kb.size(rems_from_px(10.))),
 5744                            )
 5745                            .on_click(cx.listener(move |this, _, window, cx| {
 5746                                this.keep_all(&KeepAll, window, cx);
 5747                            })),
 5748                    ),
 5749            )
 5750    }
 5751
 5752    fn render_edited_files_buttons(
 5753        &self,
 5754        index: usize,
 5755        buffer: &Entity<Buffer>,
 5756        action_log: &Entity<ActionLog>,
 5757        telemetry: &ActionLogTelemetry,
 5758        pending_edits: bool,
 5759        editor_bg_color: Hsla,
 5760        cx: &Context<Self>,
 5761    ) -> impl IntoElement {
 5762        h_flex()
 5763            .id("edited-buttons-container")
 5764            .visible_on_hover("edited-code")
 5765            .absolute()
 5766            .right_0()
 5767            .px_1()
 5768            .gap_1()
 5769            .bg(editor_bg_color)
 5770            .on_hover(cx.listener(move |this, is_hovered, _window, cx| {
 5771                if let Some(active) = this.as_active_thread_mut() {
 5772                    if *is_hovered {
 5773                        active.hovered_edited_file_buttons = Some(index);
 5774                    } else if active.hovered_edited_file_buttons == Some(index) {
 5775                        active.hovered_edited_file_buttons = None;
 5776                    }
 5777                }
 5778                cx.notify();
 5779            }))
 5780            .child(
 5781                Button::new("review", "Review")
 5782                    .label_size(LabelSize::Small)
 5783                    .on_click({
 5784                        let buffer = buffer.clone();
 5785                        cx.listener(move |this, _, window, cx| {
 5786                            this.open_edited_buffer(&buffer, window, cx);
 5787                        })
 5788                    }),
 5789            )
 5790            .child(
 5791                Button::new(("reject-file", index), "Reject")
 5792                    .label_size(LabelSize::Small)
 5793                    .disabled(pending_edits)
 5794                    .on_click({
 5795                        let buffer = buffer.clone();
 5796                        let action_log = action_log.clone();
 5797                        let telemetry = telemetry.clone();
 5798                        move |_, _, cx| {
 5799                            action_log.update(cx, |action_log, cx| {
 5800                                action_log
 5801                                    .reject_edits_in_ranges(
 5802                                        buffer.clone(),
 5803                                        vec![Anchor::min_max_range_for_buffer(
 5804                                            buffer.read(cx).remote_id(),
 5805                                        )],
 5806                                        Some(telemetry.clone()),
 5807                                        cx,
 5808                                    )
 5809                                    .detach_and_log_err(cx);
 5810                            })
 5811                        }
 5812                    }),
 5813            )
 5814            .child(
 5815                Button::new(("keep-file", index), "Keep")
 5816                    .label_size(LabelSize::Small)
 5817                    .disabled(pending_edits)
 5818                    .on_click({
 5819                        let buffer = buffer.clone();
 5820                        let action_log = action_log.clone();
 5821                        let telemetry = telemetry.clone();
 5822                        move |_, _, cx| {
 5823                            action_log.update(cx, |action_log, cx| {
 5824                                action_log.keep_edits_in_range(
 5825                                    buffer.clone(),
 5826                                    Anchor::min_max_range_for_buffer(buffer.read(cx).remote_id()),
 5827                                    Some(telemetry.clone()),
 5828                                    cx,
 5829                                );
 5830                            })
 5831                        }
 5832                    }),
 5833            )
 5834    }
 5835
 5836    fn render_edited_files(
 5837        &self,
 5838        action_log: &Entity<ActionLog>,
 5839        telemetry: ActionLogTelemetry,
 5840        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
 5841        pending_edits: bool,
 5842        cx: &Context<Self>,
 5843    ) -> impl IntoElement {
 5844        let editor_bg_color = cx.theme().colors().editor_background;
 5845
 5846        // Sort edited files alphabetically for consistency with Git diff view
 5847        let mut sorted_buffers: Vec<_> = changed_buffers.iter().collect();
 5848        sorted_buffers.sort_by(|(buffer_a, _), (buffer_b, _)| {
 5849            let path_a = buffer_a.read(cx).file().map(|f| f.path().clone());
 5850            let path_b = buffer_b.read(cx).file().map(|f| f.path().clone());
 5851            path_a.cmp(&path_b)
 5852        });
 5853
 5854        v_flex()
 5855            .id("edited_files_list")
 5856            .max_h_40()
 5857            .overflow_y_scroll()
 5858            .children(
 5859                sorted_buffers
 5860                    .into_iter()
 5861                    .enumerate()
 5862                    .flat_map(|(index, (buffer, diff))| {
 5863                        let file = buffer.read(cx).file()?;
 5864                        let path = file.path();
 5865                        let path_style = file.path_style(cx);
 5866                        let separator = file.path_style(cx).primary_separator();
 5867
 5868                        let file_path = path.parent().and_then(|parent| {
 5869                            if parent.is_empty() {
 5870                                None
 5871                            } else {
 5872                                Some(
 5873                                    Label::new(format!(
 5874                                        "{}{separator}",
 5875                                        parent.display(path_style)
 5876                                    ))
 5877                                    .color(Color::Muted)
 5878                                    .size(LabelSize::XSmall)
 5879                                    .buffer_font(cx),
 5880                                )
 5881                            }
 5882                        });
 5883
 5884                        let file_name = path.file_name().map(|name| {
 5885                            Label::new(name.to_string())
 5886                                .size(LabelSize::XSmall)
 5887                                .buffer_font(cx)
 5888                                .ml_1()
 5889                        });
 5890
 5891                        let full_path = path.display(path_style).to_string();
 5892
 5893                        let file_icon = FileIcons::get_icon(path.as_std_path(), cx)
 5894                            .map(Icon::from_path)
 5895                            .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
 5896                            .unwrap_or_else(|| {
 5897                                Icon::new(IconName::File)
 5898                                    .color(Color::Muted)
 5899                                    .size(IconSize::Small)
 5900                            });
 5901
 5902                        let file_stats = DiffStats::single_file(buffer.read(cx), diff.read(cx), cx);
 5903
 5904                        let buttons = self.render_edited_files_buttons(
 5905                            index,
 5906                            buffer,
 5907                            action_log,
 5908                            &telemetry,
 5909                            pending_edits,
 5910                            editor_bg_color,
 5911                            cx,
 5912                        );
 5913
 5914                        let element = h_flex()
 5915                            .group("edited-code")
 5916                            .id(("file-container", index))
 5917                            .relative()
 5918                            .min_w_0()
 5919                            .p_1p5()
 5920                            .gap_2()
 5921                            .justify_between()
 5922                            .bg(editor_bg_color)
 5923                            .when(index < changed_buffers.len() - 1, |parent| {
 5924                                parent.border_color(cx.theme().colors().border).border_b_1()
 5925                            })
 5926                            .child(
 5927                                h_flex()
 5928                                    .id(("file-name-path", index))
 5929                                    .cursor_pointer()
 5930                                    .pr_0p5()
 5931                                    .gap_0p5()
 5932                                    .rounded_xs()
 5933                                    .child(file_icon)
 5934                                    .children(file_name)
 5935                                    .children(file_path)
 5936                                    .child(
 5937                                        DiffStat::new(
 5938                                            "file",
 5939                                            file_stats.lines_added as usize,
 5940                                            file_stats.lines_removed as usize,
 5941                                        )
 5942                                        .label_size(LabelSize::XSmall),
 5943                                    )
 5944                                    .hover(|s| s.bg(cx.theme().colors().element_hover))
 5945                                    .tooltip({
 5946                                        move |_, cx| {
 5947                                            Tooltip::with_meta(
 5948                                                "Go to File",
 5949                                                None,
 5950                                                full_path.clone(),
 5951                                                cx,
 5952                                            )
 5953                                        }
 5954                                    })
 5955                                    .on_click({
 5956                                        let buffer = buffer.clone();
 5957                                        cx.listener(move |this, _, window, cx| {
 5958                                            this.open_edited_buffer(&buffer, window, cx);
 5959                                        })
 5960                                    }),
 5961                            )
 5962                            .child(buttons);
 5963
 5964                        Some(element)
 5965                    }),
 5966            )
 5967            .into_any_element()
 5968    }
 5969
 5970    fn render_message_queue_summary(
 5971        &self,
 5972        _window: &mut Window,
 5973        cx: &Context<Self>,
 5974    ) -> impl IntoElement {
 5975        let queue_count = self.queued_messages_len();
 5976        let title: SharedString = if queue_count == 1 {
 5977            "1 Queued Message".into()
 5978        } else {
 5979            format!("{} Queued Messages", queue_count).into()
 5980        };
 5981
 5982        let Some(active) = self.as_active_thread() else {
 5983            return Empty.into_any_element();
 5984        };
 5985
 5986        h_flex()
 5987            .p_1()
 5988            .w_full()
 5989            .gap_1()
 5990            .justify_between()
 5991            .when(active.queue_expanded, |this| {
 5992                this.border_b_1().border_color(cx.theme().colors().border)
 5993            })
 5994            .child(
 5995                h_flex()
 5996                    .id("queue_summary")
 5997                    .gap_1()
 5998                    .child(Disclosure::new("queue_disclosure", active.queue_expanded))
 5999                    .child(Label::new(title).size(LabelSize::Small).color(Color::Muted))
 6000                    .on_click(cx.listener(|this, _, _, cx| {
 6001                        let Some(active) = this.as_active_thread_mut() else {
 6002                            return;
 6003                        };
 6004                        active.queue_expanded = !active.queue_expanded;
 6005                        cx.notify();
 6006                    })),
 6007            )
 6008            .child(
 6009                Button::new("clear_queue", "Clear All")
 6010                    .label_size(LabelSize::Small)
 6011                    .key_binding(KeyBinding::for_action(&ClearMessageQueue, cx))
 6012                    .on_click(cx.listener(|this, _, _, cx| {
 6013                        this.clear_queue(cx);
 6014                        if let Some(active) = this.as_active_thread_mut() {
 6015                            active.can_fast_track_queue = false;
 6016                        }
 6017                        cx.notify();
 6018                    })),
 6019            )
 6020            .into_any_element()
 6021    }
 6022
 6023    fn render_message_queue_entries(
 6024        &self,
 6025        _window: &mut Window,
 6026        cx: &Context<Self>,
 6027    ) -> impl IntoElement {
 6028        let message_editor = self.message_editor.read(cx);
 6029        let focus_handle = message_editor.focus_handle(cx);
 6030
 6031        let queued_message_editors = self
 6032            .as_connected()
 6033            .and_then(|c| c.current.as_ref())
 6034            .map(|current| current.queued_message_editors.as_slice())
 6035            .unwrap_or(&[]);
 6036
 6037        let queue_len = queued_message_editors.len();
 6038        let can_fast_track = if let Some(active) = self.as_active_thread() {
 6039            active.can_fast_track_queue && queue_len > 0
 6040        } else {
 6041            false
 6042        };
 6043
 6044        v_flex()
 6045            .id("message_queue_list")
 6046            .max_h_40()
 6047            .overflow_y_scroll()
 6048            .children(
 6049                queued_message_editors
 6050                    .iter()
 6051                    .enumerate()
 6052                    .map(|(index, editor)| {
 6053                        let is_next = index == 0;
 6054                        let (icon_color, tooltip_text) = if is_next {
 6055                            (Color::Accent, "Next in Queue")
 6056                        } else {
 6057                            (Color::Muted, "In Queue")
 6058                        };
 6059
 6060                        let editor_focused = editor.focus_handle(cx).is_focused(_window);
 6061                        let keybinding_size = rems_from_px(12.);
 6062
 6063                        h_flex()
 6064                            .group("queue_entry")
 6065                            .w_full()
 6066                            .p_1p5()
 6067                            .gap_1()
 6068                            .bg(cx.theme().colors().editor_background)
 6069                            .when(index < queue_len - 1, |this| {
 6070                                this.border_b_1()
 6071                                    .border_color(cx.theme().colors().border_variant)
 6072                            })
 6073                            .child(
 6074                                div()
 6075                                    .id("next_in_queue")
 6076                                    .child(
 6077                                        Icon::new(IconName::Circle)
 6078                                            .size(IconSize::Small)
 6079                                            .color(icon_color),
 6080                                    )
 6081                                    .tooltip(Tooltip::text(tooltip_text)),
 6082                            )
 6083                            .child(editor.clone())
 6084                            .child(if editor_focused {
 6085                                h_flex()
 6086                                    .gap_1()
 6087                                    .min_w_40()
 6088                                    .child(
 6089                                        IconButton::new(("cancel_edit", index), IconName::Close)
 6090                                            .icon_size(IconSize::Small)
 6091                                            .icon_color(Color::Error)
 6092                                            .tooltip({
 6093                                                let focus_handle = editor.focus_handle(cx);
 6094                                                move |_window, cx| {
 6095                                                    Tooltip::for_action_in(
 6096                                                        "Cancel Edit",
 6097                                                        &editor::actions::Cancel,
 6098                                                        &focus_handle,
 6099                                                        cx,
 6100                                                    )
 6101                                                }
 6102                                            })
 6103                                            .on_click({
 6104                                                let main_editor = self.message_editor.clone();
 6105                                                cx.listener(move |_, _, window, cx| {
 6106                                                    window.focus(&main_editor.focus_handle(cx), cx);
 6107                                                })
 6108                                            }),
 6109                                    )
 6110                                    .child(
 6111                                        IconButton::new(("save_edit", index), IconName::Check)
 6112                                            .icon_size(IconSize::Small)
 6113                                            .icon_color(Color::Success)
 6114                                            .tooltip({
 6115                                                let focus_handle = editor.focus_handle(cx);
 6116                                                move |_window, cx| {
 6117                                                    Tooltip::for_action_in(
 6118                                                        "Save Edit",
 6119                                                        &Chat,
 6120                                                        &focus_handle,
 6121                                                        cx,
 6122                                                    )
 6123                                                }
 6124                                            })
 6125                                            .on_click({
 6126                                                let main_editor = self.message_editor.clone();
 6127                                                cx.listener(move |_, _, window, cx| {
 6128                                                    window.focus(&main_editor.focus_handle(cx), cx);
 6129                                                })
 6130                                            }),
 6131                                    )
 6132                                    .child(
 6133                                        Button::new(("send_now_focused", index), "Send Now")
 6134                                            .label_size(LabelSize::Small)
 6135                                            .style(ButtonStyle::Outlined)
 6136                                            .key_binding(
 6137                                                KeyBinding::for_action_in(
 6138                                                    &SendImmediately,
 6139                                                    &editor.focus_handle(cx),
 6140                                                    cx,
 6141                                                )
 6142                                                .map(|kb| kb.size(keybinding_size)),
 6143                                            )
 6144                                            .on_click(cx.listener(move |this, _, window, cx| {
 6145                                                this.send_queued_message_at_index(
 6146                                                    index, true, window, cx,
 6147                                                );
 6148                                            })),
 6149                                    )
 6150                            } else {
 6151                                h_flex()
 6152                                    .gap_1()
 6153                                    .when(!is_next, |this| this.visible_on_hover("queue_entry"))
 6154                                    .child(
 6155                                        IconButton::new(("edit", index), IconName::Pencil)
 6156                                            .icon_size(IconSize::Small)
 6157                                            .tooltip({
 6158                                                let focus_handle = focus_handle.clone();
 6159                                                move |_window, cx| {
 6160                                                    if is_next {
 6161                                                        Tooltip::for_action_in(
 6162                                                            "Edit",
 6163                                                            &EditFirstQueuedMessage,
 6164                                                            &focus_handle,
 6165                                                            cx,
 6166                                                        )
 6167                                                    } else {
 6168                                                        Tooltip::simple("Edit", cx)
 6169                                                    }
 6170                                                }
 6171                                            })
 6172                                            .on_click({
 6173                                                let editor = editor.clone();
 6174                                                cx.listener(move |_, _, window, cx| {
 6175                                                    window.focus(&editor.focus_handle(cx), cx);
 6176                                                })
 6177                                            }),
 6178                                    )
 6179                                    .child(
 6180                                        IconButton::new(("delete", index), IconName::Trash)
 6181                                            .icon_size(IconSize::Small)
 6182                                            .tooltip({
 6183                                                let focus_handle = focus_handle.clone();
 6184                                                move |_window, cx| {
 6185                                                    if is_next {
 6186                                                        Tooltip::for_action_in(
 6187                                                            "Remove Message from Queue",
 6188                                                            &RemoveFirstQueuedMessage,
 6189                                                            &focus_handle,
 6190                                                            cx,
 6191                                                        )
 6192                                                    } else {
 6193                                                        Tooltip::simple(
 6194                                                            "Remove Message from Queue",
 6195                                                            cx,
 6196                                                        )
 6197                                                    }
 6198                                                }
 6199                                            })
 6200                                            .on_click(cx.listener(move |this, _, _, cx| {
 6201                                                this.remove_from_queue(index, cx);
 6202                                                cx.notify();
 6203                                            })),
 6204                                    )
 6205                                    .child(
 6206                                        Button::new(("send_now", index), "Send Now")
 6207                                            .label_size(LabelSize::Small)
 6208                                            .when(is_next && message_editor.is_empty(cx), |this| {
 6209                                                let action: Box<dyn gpui::Action> =
 6210                                                    if can_fast_track {
 6211                                                        Box::new(Chat)
 6212                                                    } else {
 6213                                                        Box::new(SendNextQueuedMessage)
 6214                                                    };
 6215
 6216                                                this.style(ButtonStyle::Outlined).key_binding(
 6217                                                    KeyBinding::for_action_in(
 6218                                                        action.as_ref(),
 6219                                                        &focus_handle.clone(),
 6220                                                        cx,
 6221                                                    )
 6222                                                    .map(|kb| kb.size(keybinding_size)),
 6223                                                )
 6224                                            })
 6225                                            .when(is_next && !message_editor.is_empty(cx), |this| {
 6226                                                this.style(ButtonStyle::Outlined)
 6227                                            })
 6228                                            .on_click(cx.listener(move |this, _, window, cx| {
 6229                                                this.send_queued_message_at_index(
 6230                                                    index, true, window, cx,
 6231                                                );
 6232                                            })),
 6233                                    )
 6234                            })
 6235                    }),
 6236            )
 6237            .into_any_element()
 6238    }
 6239
 6240    fn render_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
 6241        let focus_handle = self.message_editor.focus_handle(cx);
 6242        let editor_bg_color = cx.theme().colors().editor_background;
 6243        let editor_expanded = self
 6244            .as_active_thread()
 6245            .is_some_and(|active| active.editor_expanded);
 6246        let (expand_icon, expand_tooltip) = if editor_expanded {
 6247            (IconName::Minimize, "Minimize Message Editor")
 6248        } else {
 6249            (IconName::Maximize, "Expand Message Editor")
 6250        };
 6251
 6252        let backdrop = div()
 6253            .size_full()
 6254            .absolute()
 6255            .inset_0()
 6256            .bg(cx.theme().colors().panel_background)
 6257            .opacity(0.8)
 6258            .block_mouse_except_scroll();
 6259
 6260        let enable_editor = self
 6261            .as_connected()
 6262            .is_some_and(|conn| conn.auth_state.is_ok());
 6263
 6264        v_flex()
 6265            .on_action(cx.listener(Self::expand_message_editor))
 6266            .p_2()
 6267            .gap_2()
 6268            .border_t_1()
 6269            .border_color(cx.theme().colors().border)
 6270            .bg(editor_bg_color)
 6271            .when(editor_expanded, |this| {
 6272                this.h(vh(0.8, window)).size_full().justify_between()
 6273            })
 6274            .child(
 6275                v_flex()
 6276                    .relative()
 6277                    .size_full()
 6278                    .pt_1()
 6279                    .pr_2p5()
 6280                    .child(self.message_editor.clone())
 6281                    .child(
 6282                        h_flex()
 6283                            .absolute()
 6284                            .top_0()
 6285                            .right_0()
 6286                            .opacity(0.5)
 6287                            .hover(|this| this.opacity(1.0))
 6288                            .child(
 6289                                IconButton::new("toggle-height", expand_icon)
 6290                                    .icon_size(IconSize::Small)
 6291                                    .icon_color(Color::Muted)
 6292                                    .tooltip({
 6293                                        move |_window, cx| {
 6294                                            Tooltip::for_action_in(
 6295                                                expand_tooltip,
 6296                                                &ExpandMessageEditor,
 6297                                                &focus_handle,
 6298                                                cx,
 6299                                            )
 6300                                        }
 6301                                    })
 6302                                    .on_click(cx.listener(|this, _, window, cx| {
 6303                                        this.expand_message_editor(
 6304                                            &ExpandMessageEditor,
 6305                                            window,
 6306                                            cx,
 6307                                        );
 6308                                    })),
 6309                            ),
 6310                    ),
 6311            )
 6312            .child(
 6313                h_flex()
 6314                    .flex_none()
 6315                    .flex_wrap()
 6316                    .justify_between()
 6317                    .child(
 6318                        h_flex()
 6319                            .gap_0p5()
 6320                            .child(self.render_add_context_button(cx))
 6321                            .child(self.render_follow_toggle(cx))
 6322                            .children(self.render_thinking_toggle(cx)),
 6323                    )
 6324                    .child(
 6325                        h_flex()
 6326                            .gap_1()
 6327                            .children(self.render_token_usage(cx))
 6328                            .when_some(self.as_active_thread(), |this, active| {
 6329                                this.children(active.profile_selector.clone()).map(|this| {
 6330                                    // Either config_options_view OR (mode_selector + model_selector)
 6331                                    match active.config_options_view.clone() {
 6332                                        Some(config_view) => this.child(config_view),
 6333                                        None => this
 6334                                            .children(active.mode_selector.clone())
 6335                                            .children(active.model_selector.clone()),
 6336                                    }
 6337                                })
 6338                            })
 6339                            .child(self.render_send_button(cx)),
 6340                    ),
 6341            )
 6342            .when(!enable_editor, |this| this.child(backdrop))
 6343            .into_any()
 6344    }
 6345
 6346    pub(crate) fn as_native_connection(
 6347        &self,
 6348        cx: &App,
 6349    ) -> Option<Rc<agent::NativeAgentConnection>> {
 6350        let acp_thread = self.as_active_thread()?.thread.read(cx);
 6351        acp_thread.connection().clone().downcast()
 6352    }
 6353
 6354    pub(crate) fn as_native_thread(&self, cx: &App) -> Option<Entity<agent::Thread>> {
 6355        let acp_thread = self.as_active_thread()?.thread.read(cx);
 6356        self.as_native_connection(cx)?
 6357            .thread(acp_thread.session_id(), cx)
 6358    }
 6359
 6360    fn queued_messages_len(&self) -> usize {
 6361        self.as_active_thread()
 6362            .map(|thread| thread.local_queued_messages.len())
 6363            .unwrap_or_default()
 6364    }
 6365
 6366    fn has_queued_messages(&self) -> bool {
 6367        self.as_active_thread()
 6368            .map(|active| active.has_queued_messages())
 6369            .unwrap_or(false)
 6370    }
 6371
 6372    /// Syncs the has_queued_message flag to the native thread (if applicable).
 6373    /// This flag tells the native thread to end its turn at the next message boundary.
 6374    fn sync_queue_flag_to_native_thread(&self, cx: &mut Context<Self>) {
 6375        if let Some(active) = self.as_active_thread() {
 6376            active.sync_queue_flag_to_native_thread(cx);
 6377        }
 6378    }
 6379
 6380    fn add_to_queue(
 6381        &mut self,
 6382        content: Vec<acp::ContentBlock>,
 6383        tracked_buffers: Vec<Entity<Buffer>>,
 6384        cx: &mut Context<Self>,
 6385    ) {
 6386        if let Some(active) = self.as_active_thread_mut() {
 6387            active.local_queued_messages.push(QueuedMessage {
 6388                content,
 6389                tracked_buffers,
 6390            });
 6391        }
 6392        self.sync_queue_flag_to_native_thread(cx);
 6393    }
 6394
 6395    fn remove_from_queue(&mut self, index: usize, cx: &mut Context<Self>) -> Option<QueuedMessage> {
 6396        self.as_active_thread_mut()
 6397            .and_then(|active| active.remove_from_queue(index, cx))
 6398    }
 6399
 6400    fn update_queued_message(
 6401        &mut self,
 6402        index: usize,
 6403        content: Vec<acp::ContentBlock>,
 6404        tracked_buffers: Vec<Entity<Buffer>>,
 6405        _cx: &mut Context<Self>,
 6406    ) -> bool {
 6407        match self.as_active_thread_mut() {
 6408            Some(thread) if index < thread.local_queued_messages.len() => {
 6409                thread.local_queued_messages[index] = QueuedMessage {
 6410                    content,
 6411                    tracked_buffers,
 6412                };
 6413                true
 6414            }
 6415            Some(_) | None => false,
 6416        }
 6417    }
 6418
 6419    fn clear_queue(&mut self, cx: &mut Context<Self>) {
 6420        if let Some(active) = self.as_active_thread_mut() {
 6421            active.local_queued_messages.clear();
 6422        }
 6423        self.sync_queue_flag_to_native_thread(cx);
 6424    }
 6425
 6426    fn queued_message_contents(&self) -> Vec<Vec<acp::ContentBlock>> {
 6427        match self.as_active_thread() {
 6428            None => Vec::new(),
 6429            Some(thread) => thread
 6430                .local_queued_messages
 6431                .iter()
 6432                .map(|q| q.content.clone())
 6433                .collect(),
 6434        }
 6435    }
 6436
 6437    fn save_queued_message_at_index(&mut self, index: usize, cx: &mut Context<Self>) {
 6438        let editor = match self.as_active_thread() {
 6439            Some(thread) => thread.queued_message_editors.get(index).cloned(),
 6440            None => None,
 6441        };
 6442        let Some(editor) = editor else {
 6443            return;
 6444        };
 6445
 6446        let contents_task = editor.update(cx, |editor, cx| editor.contents(false, cx));
 6447
 6448        cx.spawn(async move |this, cx| {
 6449            let Ok((content, tracked_buffers)) = contents_task.await else {
 6450                return Ok::<(), anyhow::Error>(());
 6451            };
 6452
 6453            this.update(cx, |this, cx| {
 6454                this.update_queued_message(index, content, tracked_buffers, cx);
 6455                cx.notify();
 6456            })?;
 6457
 6458            Ok(())
 6459        })
 6460        .detach_and_log_err(cx);
 6461    }
 6462
 6463    fn sync_queued_message_editors(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 6464        let needed_count = self.queued_messages_len();
 6465        let queued_messages = self.queued_message_contents();
 6466
 6467        let agent_name = self.agent.name();
 6468        let workspace = self.workspace.clone();
 6469        let project = self.project.downgrade();
 6470        let history = self.history.downgrade();
 6471        let message_editor = self.message_editor.clone();
 6472
 6473        let Some(thread) = self.as_active_thread_mut() else {
 6474            return;
 6475        };
 6476        let prompt_capabilities = thread.prompt_capabilities.clone();
 6477        let available_commands = thread.available_commands.clone();
 6478
 6479        let current_count = thread.queued_message_editors.len();
 6480
 6481        if current_count == needed_count && needed_count == thread.last_synced_queue_length {
 6482            return;
 6483        }
 6484
 6485        if current_count > needed_count {
 6486            thread.queued_message_editors.truncate(needed_count);
 6487            thread
 6488                .queued_message_editor_subscriptions
 6489                .truncate(needed_count);
 6490
 6491            for (index, editor) in thread.queued_message_editors.iter().enumerate() {
 6492                if let Some(content) = queued_messages.get(index) {
 6493                    editor.update(cx, |editor, cx| {
 6494                        editor.set_message(content.clone(), window, cx);
 6495                    });
 6496                }
 6497            }
 6498        }
 6499
 6500        while thread.queued_message_editors.len() < needed_count {
 6501            let index = thread.queued_message_editors.len();
 6502            let content = queued_messages.get(index).cloned().unwrap_or_default();
 6503
 6504            let editor = cx.new(|cx| {
 6505                let mut editor = MessageEditor::new(
 6506                    workspace.clone(),
 6507                    project.clone(),
 6508                    None,
 6509                    history.clone(),
 6510                    None,
 6511                    prompt_capabilities.clone(),
 6512                    available_commands.clone(),
 6513                    agent_name.clone(),
 6514                    "",
 6515                    EditorMode::AutoHeight {
 6516                        min_lines: 1,
 6517                        max_lines: Some(10),
 6518                    },
 6519                    window,
 6520                    cx,
 6521                );
 6522                editor.set_message(content, window, cx);
 6523                editor
 6524            });
 6525
 6526            let message_editor = message_editor.clone();
 6527            let subscription = cx.subscribe_in(
 6528                &editor,
 6529                window,
 6530                move |this, _editor, event, window, cx| match event {
 6531                    MessageEditorEvent::LostFocus => {
 6532                        this.save_queued_message_at_index(index, cx);
 6533                    }
 6534                    MessageEditorEvent::Cancel => {
 6535                        window.focus(&message_editor.focus_handle(cx), cx);
 6536                    }
 6537                    MessageEditorEvent::Send => {
 6538                        window.focus(&message_editor.focus_handle(cx), cx);
 6539                    }
 6540                    MessageEditorEvent::SendImmediately => {
 6541                        this.send_queued_message_at_index(index, true, window, cx);
 6542                    }
 6543                    _ => {}
 6544                },
 6545            );
 6546
 6547            thread.queued_message_editors.push(editor);
 6548            thread
 6549                .queued_message_editor_subscriptions
 6550                .push(subscription);
 6551        }
 6552
 6553        if let Some(active) = self.as_active_thread_mut() {
 6554            active.last_synced_queue_length = needed_count;
 6555        }
 6556    }
 6557
 6558    fn is_imported_thread(&self, cx: &App) -> bool {
 6559        if let Some(active) = self.as_active_thread() {
 6560            active.is_imported_thread(cx)
 6561        } else {
 6562            false
 6563        }
 6564    }
 6565
 6566    fn supports_split_token_display(&self, cx: &App) -> bool {
 6567        self.as_native_thread(cx)
 6568            .and_then(|thread| thread.read(cx).model())
 6569            .is_some_and(|model| model.supports_split_token_display())
 6570    }
 6571
 6572    fn render_token_usage(&self, cx: &mut Context<Self>) -> Option<Div> {
 6573        let active = self.as_active_thread()?;
 6574        let thread = active.thread.read(cx);
 6575        let usage = thread.token_usage()?;
 6576        let is_generating = thread.status() != ThreadStatus::Idle;
 6577        let show_split = self.supports_split_token_display(cx);
 6578
 6579        let separator_color = Color::Custom(cx.theme().colors().text_muted.opacity(0.5));
 6580        let token_label = |text: String, animation_id: &'static str| {
 6581            Label::new(text)
 6582                .size(LabelSize::Small)
 6583                .color(Color::Muted)
 6584                .map(|label| {
 6585                    if is_generating {
 6586                        label
 6587                            .with_animation(
 6588                                animation_id,
 6589                                Animation::new(Duration::from_secs(2))
 6590                                    .repeat()
 6591                                    .with_easing(pulsating_between(0.3, 0.8)),
 6592                                |label, delta| label.alpha(delta),
 6593                            )
 6594                            .into_any()
 6595                    } else {
 6596                        label.into_any_element()
 6597                    }
 6598                })
 6599        };
 6600
 6601        if show_split {
 6602            let max_output_tokens = self
 6603                .as_native_thread(cx)
 6604                .and_then(|thread| thread.read(cx).model())
 6605                .and_then(|model| model.max_output_tokens())
 6606                .unwrap_or(0);
 6607
 6608            let input = crate::text_thread_editor::humanize_token_count(usage.input_tokens);
 6609            let input_max = crate::text_thread_editor::humanize_token_count(
 6610                usage.max_tokens.saturating_sub(max_output_tokens),
 6611            );
 6612            let output = crate::text_thread_editor::humanize_token_count(usage.output_tokens);
 6613            let output_max = crate::text_thread_editor::humanize_token_count(max_output_tokens);
 6614
 6615            Some(
 6616                h_flex()
 6617                    .flex_shrink_0()
 6618                    .gap_1()
 6619                    .mr_1p5()
 6620                    .child(
 6621                        h_flex()
 6622                            .gap_0p5()
 6623                            .child(
 6624                                Icon::new(IconName::ArrowUp)
 6625                                    .size(IconSize::XSmall)
 6626                                    .color(Color::Muted),
 6627                            )
 6628                            .child(token_label(input, "input-tokens-label"))
 6629                            .child(
 6630                                Label::new("/")
 6631                                    .size(LabelSize::Small)
 6632                                    .color(separator_color),
 6633                            )
 6634                            .child(
 6635                                Label::new(input_max)
 6636                                    .size(LabelSize::Small)
 6637                                    .color(Color::Muted),
 6638                            ),
 6639                    )
 6640                    .child(
 6641                        h_flex()
 6642                            .gap_0p5()
 6643                            .child(
 6644                                Icon::new(IconName::ArrowDown)
 6645                                    .size(IconSize::XSmall)
 6646                                    .color(Color::Muted),
 6647                            )
 6648                            .child(token_label(output, "output-tokens-label"))
 6649                            .child(
 6650                                Label::new("/")
 6651                                    .size(LabelSize::Small)
 6652                                    .color(separator_color),
 6653                            )
 6654                            .child(
 6655                                Label::new(output_max)
 6656                                    .size(LabelSize::Small)
 6657                                    .color(Color::Muted),
 6658                            ),
 6659                    ),
 6660            )
 6661        } else {
 6662            let used = crate::text_thread_editor::humanize_token_count(usage.used_tokens);
 6663            let max = crate::text_thread_editor::humanize_token_count(usage.max_tokens);
 6664
 6665            Some(
 6666                h_flex()
 6667                    .flex_shrink_0()
 6668                    .gap_0p5()
 6669                    .mr_1p5()
 6670                    .child(token_label(used, "used-tokens-label"))
 6671                    .child(
 6672                        Label::new("/")
 6673                            .size(LabelSize::Small)
 6674                            .color(separator_color),
 6675                    )
 6676                    .child(Label::new(max).size(LabelSize::Small).color(Color::Muted)),
 6677            )
 6678        }
 6679    }
 6680
 6681    fn render_thinking_toggle(&self, cx: &mut Context<Self>) -> Option<IconButton> {
 6682        if !cx.has_flag::<CloudThinkingToggleFeatureFlag>() {
 6683            return None;
 6684        }
 6685
 6686        let thread = self.as_native_thread(cx)?.read(cx);
 6687
 6688        let supports_thinking = thread.model()?.supports_thinking();
 6689        if !supports_thinking {
 6690            return None;
 6691        }
 6692
 6693        let thinking = thread.thinking_enabled();
 6694
 6695        let (tooltip_label, icon) = if thinking {
 6696            ("Disable Thinking Mode", IconName::ThinkingMode)
 6697        } else {
 6698            ("Enable Thinking Mode", IconName::ToolThink)
 6699        };
 6700
 6701        let focus_handle = self.message_editor.focus_handle(cx);
 6702
 6703        Some(
 6704            IconButton::new("thinking-mode", icon)
 6705                .icon_size(IconSize::Small)
 6706                .icon_color(Color::Muted)
 6707                .toggle_state(thinking)
 6708                .tooltip(move |_, cx| {
 6709                    Tooltip::for_action_in(tooltip_label, &ToggleThinkingMode, &focus_handle, cx)
 6710                })
 6711                .on_click(cx.listener(move |this, _, _window, cx| {
 6712                    if let Some(thread) = this.as_native_thread(cx) {
 6713                        thread.update(cx, |thread, cx| {
 6714                            thread.set_thinking_enabled(!thread.thinking_enabled(), cx);
 6715                        });
 6716                    }
 6717                })),
 6718        )
 6719    }
 6720
 6721    fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context<Self>) {
 6722        if let Some(active) = self.as_active_thread_mut() {
 6723            active.keep_all(cx);
 6724        };
 6725    }
 6726
 6727    fn reject_all(&mut self, _: &RejectAll, _window: &mut Window, cx: &mut Context<Self>) {
 6728        if let Some(active) = self.as_active_thread_mut() {
 6729            active.reject_all(cx);
 6730        };
 6731    }
 6732
 6733    fn allow_always(&mut self, _: &AllowAlways, window: &mut Window, cx: &mut Context<Self>) {
 6734        self.authorize_pending_tool_call(acp::PermissionOptionKind::AllowAlways, window, cx);
 6735    }
 6736
 6737    fn allow_once(&mut self, _: &AllowOnce, window: &mut Window, cx: &mut Context<Self>) {
 6738        self.authorize_pending_with_granularity(true, window, cx);
 6739    }
 6740
 6741    fn reject_once(&mut self, _: &RejectOnce, window: &mut Window, cx: &mut Context<Self>) {
 6742        self.authorize_pending_with_granularity(false, window, cx);
 6743    }
 6744
 6745    fn authorize_pending_with_granularity(
 6746        &mut self,
 6747        is_allow: bool,
 6748        window: &mut Window,
 6749        cx: &mut Context<Self>,
 6750    ) -> Option<()> {
 6751        let active = self.as_active_thread()?;
 6752        let thread = active.thread.read(cx);
 6753        let tool_call = thread.first_tool_awaiting_confirmation()?;
 6754        let ToolCallStatus::WaitingForConfirmation { options, .. } = &tool_call.status else {
 6755            return None;
 6756        };
 6757        let tool_call_id = tool_call.id.clone();
 6758
 6759        let PermissionOptions::Dropdown(choices) = options else {
 6760            let kind = if is_allow {
 6761                acp::PermissionOptionKind::AllowOnce
 6762            } else {
 6763                acp::PermissionOptionKind::RejectOnce
 6764            };
 6765            return self.authorize_pending_tool_call(kind, window, cx);
 6766        };
 6767
 6768        // Get selected index, defaulting to last option ("Only this time")
 6769        let selected_index = if let Some(active) = self.as_active_thread() {
 6770            active
 6771                .selected_permission_granularity
 6772                .get(&tool_call_id)
 6773                .copied()
 6774                .unwrap_or_else(|| choices.len().saturating_sub(1))
 6775        } else {
 6776            choices.len().saturating_sub(1)
 6777        };
 6778
 6779        let selected_choice = choices.get(selected_index).or(choices.last())?;
 6780
 6781        let selected_option = if is_allow {
 6782            &selected_choice.allow
 6783        } else {
 6784            &selected_choice.deny
 6785        };
 6786
 6787        self.authorize_tool_call(
 6788            tool_call_id,
 6789            selected_option.option_id.clone(),
 6790            selected_option.kind,
 6791            window,
 6792            cx,
 6793        );
 6794
 6795        Some(())
 6796    }
 6797
 6798    fn open_permission_dropdown(
 6799        &mut self,
 6800        _: &crate::OpenPermissionDropdown,
 6801        window: &mut Window,
 6802        cx: &mut Context<Self>,
 6803    ) {
 6804        if let Some(active) = self.as_active_thread() {
 6805            active.permission_dropdown_handle.toggle(window, cx);
 6806        }
 6807    }
 6808
 6809    fn handle_select_permission_granularity(
 6810        &mut self,
 6811        action: &SelectPermissionGranularity,
 6812        _window: &mut Window,
 6813        cx: &mut Context<Self>,
 6814    ) {
 6815        if let Some(active) = self.as_active_thread_mut() {
 6816            active.handle_select_permission_granularity(action, cx);
 6817        }
 6818    }
 6819
 6820    fn handle_authorize_tool_call(
 6821        &mut self,
 6822        action: &AuthorizeToolCall,
 6823        window: &mut Window,
 6824        cx: &mut Context<Self>,
 6825    ) {
 6826        let tool_call_id = acp::ToolCallId::new(action.tool_call_id.clone());
 6827        let option_id = acp::PermissionOptionId::new(action.option_id.clone());
 6828        let option_kind = match action.option_kind.as_str() {
 6829            "AllowOnce" => acp::PermissionOptionKind::AllowOnce,
 6830            "AllowAlways" => acp::PermissionOptionKind::AllowAlways,
 6831            "RejectOnce" => acp::PermissionOptionKind::RejectOnce,
 6832            "RejectAlways" => acp::PermissionOptionKind::RejectAlways,
 6833            _ => acp::PermissionOptionKind::AllowOnce,
 6834        };
 6835
 6836        self.authorize_tool_call(tool_call_id, option_id, option_kind, window, cx);
 6837    }
 6838
 6839    fn authorize_pending_tool_call(
 6840        &mut self,
 6841        kind: acp::PermissionOptionKind,
 6842        window: &mut Window,
 6843        cx: &mut Context<Self>,
 6844    ) -> Option<()> {
 6845        self.as_active_thread_mut()?
 6846            .authorize_pending_tool_call(kind, window, cx)
 6847    }
 6848
 6849    fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
 6850        let message_editor = self.message_editor.read(cx);
 6851        let is_editor_empty = message_editor.is_empty(cx);
 6852        let focus_handle = message_editor.focus_handle(cx);
 6853
 6854        let is_generating = self
 6855            .as_active_thread()
 6856            .is_some_and(|active| active.thread.read(cx).status() != ThreadStatus::Idle);
 6857
 6858        if self
 6859            .as_active_thread()
 6860            .is_some_and(|thread| thread.is_loading_contents)
 6861        {
 6862            div()
 6863                .id("loading-message-content")
 6864                .px_1()
 6865                .tooltip(Tooltip::text("Loading Added Context…"))
 6866                .child(loading_contents_spinner(IconSize::default()))
 6867                .into_any_element()
 6868        } else if is_generating && is_editor_empty {
 6869            IconButton::new("stop-generation", IconName::Stop)
 6870                .icon_color(Color::Error)
 6871                .style(ButtonStyle::Tinted(TintColor::Error))
 6872                .tooltip(move |_window, cx| {
 6873                    Tooltip::for_action("Stop Generation", &editor::actions::Cancel, cx)
 6874                })
 6875                .on_click(cx.listener(|this, _event, _, cx| this.cancel_generation(cx)))
 6876                .into_any_element()
 6877        } else {
 6878            IconButton::new("send-message", IconName::Send)
 6879                .style(ButtonStyle::Filled)
 6880                .map(|this| {
 6881                    if is_editor_empty && !is_generating {
 6882                        this.disabled(true).icon_color(Color::Muted)
 6883                    } else {
 6884                        this.icon_color(Color::Accent)
 6885                    }
 6886                })
 6887                .tooltip(move |_window, cx| {
 6888                    if is_editor_empty && !is_generating {
 6889                        Tooltip::for_action("Type to Send", &Chat, cx)
 6890                    } else if is_generating {
 6891                        let focus_handle = focus_handle.clone();
 6892
 6893                        Tooltip::element(move |_window, cx| {
 6894                            v_flex()
 6895                                .gap_1()
 6896                                .child(
 6897                                    h_flex()
 6898                                        .gap_2()
 6899                                        .justify_between()
 6900                                        .child(Label::new("Queue and Send"))
 6901                                        .child(KeyBinding::for_action_in(&Chat, &focus_handle, cx)),
 6902                                )
 6903                                .child(
 6904                                    h_flex()
 6905                                        .pt_1()
 6906                                        .gap_2()
 6907                                        .justify_between()
 6908                                        .border_t_1()
 6909                                        .border_color(cx.theme().colors().border_variant)
 6910                                        .child(Label::new("Send Immediately"))
 6911                                        .child(KeyBinding::for_action_in(
 6912                                            &SendImmediately,
 6913                                            &focus_handle,
 6914                                            cx,
 6915                                        )),
 6916                                )
 6917                                .into_any_element()
 6918                        })(_window, cx)
 6919                    } else {
 6920                        Tooltip::for_action("Send Message", &Chat, cx)
 6921                    }
 6922                })
 6923                .on_click(cx.listener(|this, _, window, cx| {
 6924                    this.send(window, cx);
 6925                }))
 6926                .into_any_element()
 6927        }
 6928    }
 6929
 6930    fn is_following(&self, cx: &App) -> bool {
 6931        match self
 6932            .as_active_thread()
 6933            .map(|active| active.thread.read(cx).status())
 6934        {
 6935            Some(ThreadStatus::Generating) => self
 6936                .workspace
 6937                .read_with(cx, |workspace, _| {
 6938                    workspace.is_being_followed(CollaboratorId::Agent)
 6939                })
 6940                .unwrap_or(false),
 6941            _ => self
 6942                .as_active_thread()
 6943                .is_some_and(|thread| thread.should_be_following),
 6944        }
 6945    }
 6946
 6947    fn toggle_following(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 6948        let following = self.is_following(cx);
 6949
 6950        if let Some(active) = self.as_active_thread_mut() {
 6951            active.should_be_following = !following;
 6952        }
 6953        if self
 6954            .as_active_thread()
 6955            .map(|active| active.thread.read(cx).status())
 6956            == Some(ThreadStatus::Generating)
 6957        {
 6958            self.workspace
 6959                .update(cx, |workspace, cx| {
 6960                    if following {
 6961                        workspace.unfollow(CollaboratorId::Agent, window, cx);
 6962                    } else {
 6963                        workspace.follow(CollaboratorId::Agent, window, cx);
 6964                    }
 6965                })
 6966                .ok();
 6967        }
 6968
 6969        telemetry::event!("Follow Agent Selected", following = !following);
 6970    }
 6971
 6972    fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
 6973        let following = self.is_following(cx);
 6974
 6975        let tooltip_label = if following {
 6976            if self.agent.name() == "Zed Agent" {
 6977                format!("Stop Following the {}", self.agent.name())
 6978            } else {
 6979                format!("Stop Following {}", self.agent.name())
 6980            }
 6981        } else {
 6982            if self.agent.name() == "Zed Agent" {
 6983                format!("Follow the {}", self.agent.name())
 6984            } else {
 6985                format!("Follow {}", self.agent.name())
 6986            }
 6987        };
 6988
 6989        IconButton::new("follow-agent", IconName::Crosshair)
 6990            .icon_size(IconSize::Small)
 6991            .icon_color(Color::Muted)
 6992            .toggle_state(following)
 6993            .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
 6994            .tooltip(move |_window, cx| {
 6995                if following {
 6996                    Tooltip::for_action(tooltip_label.clone(), &Follow, cx)
 6997                } else {
 6998                    Tooltip::with_meta(
 6999                        tooltip_label.clone(),
 7000                        Some(&Follow),
 7001                        "Track the agent's location as it reads and edits files.",
 7002                        cx,
 7003                    )
 7004                }
 7005            })
 7006            .on_click(cx.listener(move |this, _, window, cx| {
 7007                this.toggle_following(window, cx);
 7008            }))
 7009    }
 7010
 7011    fn render_add_context_button(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
 7012        let focus_handle = self.message_editor.focus_handle(cx);
 7013        let weak_self = cx.weak_entity();
 7014
 7015        PopoverMenu::new("add-context-menu")
 7016            .trigger_with_tooltip(
 7017                IconButton::new("add-context", IconName::Plus)
 7018                    .icon_size(IconSize::Small)
 7019                    .icon_color(Color::Muted),
 7020                {
 7021                    move |_window, cx| {
 7022                        Tooltip::for_action_in(
 7023                            "Add Context",
 7024                            &OpenAddContextMenu,
 7025                            &focus_handle,
 7026                            cx,
 7027                        )
 7028                    }
 7029                },
 7030            )
 7031            .anchor(gpui::Corner::BottomLeft)
 7032            .with_handle(self.add_context_menu_handle.clone())
 7033            .offset(gpui::Point {
 7034                x: px(0.0),
 7035                y: px(-2.0),
 7036            })
 7037            .menu(move |window, cx| {
 7038                weak_self
 7039                    .update(cx, |this, cx| this.build_add_context_menu(window, cx))
 7040                    .ok()
 7041            })
 7042    }
 7043
 7044    fn build_add_context_menu(
 7045        &self,
 7046        window: &mut Window,
 7047        cx: &mut Context<Self>,
 7048    ) -> Entity<ContextMenu> {
 7049        let message_editor = self.message_editor.clone();
 7050        let workspace = self.workspace.clone();
 7051        let supports_images = self
 7052            .as_active_thread()
 7053            .map(|active| active.prompt_capabilities.borrow().image)
 7054            .unwrap_or_default();
 7055
 7056        let has_editor_selection = workspace
 7057            .upgrade()
 7058            .and_then(|ws| {
 7059                ws.read(cx)
 7060                    .active_item(cx)
 7061                    .and_then(|item| item.downcast::<Editor>())
 7062            })
 7063            .is_some_and(|editor| {
 7064                editor.update(cx, |editor, cx| {
 7065                    editor.has_non_empty_selection(&editor.display_snapshot(cx))
 7066                })
 7067            });
 7068
 7069        let has_terminal_selection = workspace
 7070            .upgrade()
 7071            .and_then(|ws| ws.read(cx).panel::<TerminalPanel>(cx))
 7072            .is_some_and(|panel| !panel.read(cx).terminal_selections(cx).is_empty());
 7073
 7074        let has_selection = has_editor_selection || has_terminal_selection;
 7075
 7076        ContextMenu::build(window, cx, move |menu, _window, _cx| {
 7077            menu.key_context("AddContextMenu")
 7078                .header("Context")
 7079                .item(
 7080                    ContextMenuEntry::new("Files & Directories")
 7081                        .icon(IconName::File)
 7082                        .icon_color(Color::Muted)
 7083                        .icon_size(IconSize::XSmall)
 7084                        .handler({
 7085                            let message_editor = message_editor.clone();
 7086                            move |window, cx| {
 7087                                message_editor.focus_handle(cx).focus(window, cx);
 7088                                message_editor.update(cx, |editor, cx| {
 7089                                    editor.insert_context_type("file", window, cx);
 7090                                });
 7091                            }
 7092                        }),
 7093                )
 7094                .item(
 7095                    ContextMenuEntry::new("Symbols")
 7096                        .icon(IconName::Code)
 7097                        .icon_color(Color::Muted)
 7098                        .icon_size(IconSize::XSmall)
 7099                        .handler({
 7100                            let message_editor = message_editor.clone();
 7101                            move |window, cx| {
 7102                                message_editor.focus_handle(cx).focus(window, cx);
 7103                                message_editor.update(cx, |editor, cx| {
 7104                                    editor.insert_context_type("symbol", window, cx);
 7105                                });
 7106                            }
 7107                        }),
 7108                )
 7109                .item(
 7110                    ContextMenuEntry::new("Threads")
 7111                        .icon(IconName::Thread)
 7112                        .icon_color(Color::Muted)
 7113                        .icon_size(IconSize::XSmall)
 7114                        .handler({
 7115                            let message_editor = message_editor.clone();
 7116                            move |window, cx| {
 7117                                message_editor.focus_handle(cx).focus(window, cx);
 7118                                message_editor.update(cx, |editor, cx| {
 7119                                    editor.insert_context_type("thread", window, cx);
 7120                                });
 7121                            }
 7122                        }),
 7123                )
 7124                .item(
 7125                    ContextMenuEntry::new("Rules")
 7126                        .icon(IconName::Reader)
 7127                        .icon_color(Color::Muted)
 7128                        .icon_size(IconSize::XSmall)
 7129                        .handler({
 7130                            let message_editor = message_editor.clone();
 7131                            move |window, cx| {
 7132                                message_editor.focus_handle(cx).focus(window, cx);
 7133                                message_editor.update(cx, |editor, cx| {
 7134                                    editor.insert_context_type("rule", window, cx);
 7135                                });
 7136                            }
 7137                        }),
 7138                )
 7139                .item(
 7140                    ContextMenuEntry::new("Image")
 7141                        .icon(IconName::Image)
 7142                        .icon_color(Color::Muted)
 7143                        .icon_size(IconSize::XSmall)
 7144                        .disabled(!supports_images)
 7145                        .handler({
 7146                            let message_editor = message_editor.clone();
 7147                            move |window, cx| {
 7148                                message_editor.focus_handle(cx).focus(window, cx);
 7149                                message_editor.update(cx, |editor, cx| {
 7150                                    editor.add_images_from_picker(window, cx);
 7151                                });
 7152                            }
 7153                        }),
 7154                )
 7155                .item(
 7156                    ContextMenuEntry::new("Selection")
 7157                        .icon(IconName::CursorIBeam)
 7158                        .icon_color(Color::Muted)
 7159                        .icon_size(IconSize::XSmall)
 7160                        .disabled(!has_selection)
 7161                        .handler({
 7162                            move |window, cx| {
 7163                                window.dispatch_action(
 7164                                    zed_actions::agent::AddSelectionToThread.boxed_clone(),
 7165                                    cx,
 7166                                );
 7167                            }
 7168                        }),
 7169                )
 7170        })
 7171    }
 7172
 7173    fn open_add_context_menu(
 7174        &mut self,
 7175        _action: &OpenAddContextMenu,
 7176        window: &mut Window,
 7177        cx: &mut Context<Self>,
 7178    ) {
 7179        let menu_handle = self.add_context_menu_handle.clone();
 7180        window.defer(cx, move |window, cx| {
 7181            menu_handle.toggle(window, cx);
 7182        });
 7183    }
 7184
 7185    fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
 7186        let workspace = self.workspace.clone();
 7187        MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
 7188            Self::open_link(text, &workspace, window, cx);
 7189        })
 7190    }
 7191
 7192    fn open_link(
 7193        url: SharedString,
 7194        workspace: &WeakEntity<Workspace>,
 7195        window: &mut Window,
 7196        cx: &mut App,
 7197    ) {
 7198        let Some(workspace) = workspace.upgrade() else {
 7199            cx.open_url(&url);
 7200            return;
 7201        };
 7202
 7203        if let Some(mention) = MentionUri::parse(&url, workspace.read(cx).path_style(cx)).log_err()
 7204        {
 7205            workspace.update(cx, |workspace, cx| match mention {
 7206                MentionUri::File { abs_path } => {
 7207                    let project = workspace.project();
 7208                    let Some(path) =
 7209                        project.update(cx, |project, cx| project.find_project_path(abs_path, cx))
 7210                    else {
 7211                        return;
 7212                    };
 7213
 7214                    workspace
 7215                        .open_path(path, None, true, window, cx)
 7216                        .detach_and_log_err(cx);
 7217                }
 7218                MentionUri::PastedImage => {}
 7219                MentionUri::Directory { abs_path } => {
 7220                    let project = workspace.project();
 7221                    let Some(entry_id) = project.update(cx, |project, cx| {
 7222                        let path = project.find_project_path(abs_path, cx)?;
 7223                        project.entry_for_path(&path, cx).map(|entry| entry.id)
 7224                    }) else {
 7225                        return;
 7226                    };
 7227
 7228                    project.update(cx, |_, cx| {
 7229                        cx.emit(project::Event::RevealInProjectPanel(entry_id));
 7230                    });
 7231                }
 7232                MentionUri::Symbol {
 7233                    abs_path: path,
 7234                    line_range,
 7235                    ..
 7236                }
 7237                | MentionUri::Selection {
 7238                    abs_path: Some(path),
 7239                    line_range,
 7240                } => {
 7241                    let project = workspace.project();
 7242                    let Some(path) =
 7243                        project.update(cx, |project, cx| project.find_project_path(path, cx))
 7244                    else {
 7245                        return;
 7246                    };
 7247
 7248                    let item = workspace.open_path(path, None, true, window, cx);
 7249                    window
 7250                        .spawn(cx, async move |cx| {
 7251                            let Some(editor) = item.await?.downcast::<Editor>() else {
 7252                                return Ok(());
 7253                            };
 7254                            let range = Point::new(*line_range.start(), 0)
 7255                                ..Point::new(*line_range.start(), 0);
 7256                            editor
 7257                                .update_in(cx, |editor, window, cx| {
 7258                                    editor.change_selections(
 7259                                        SelectionEffects::scroll(Autoscroll::center()),
 7260                                        window,
 7261                                        cx,
 7262                                        |s| s.select_ranges(vec![range]),
 7263                                    );
 7264                                })
 7265                                .ok();
 7266                            anyhow::Ok(())
 7267                        })
 7268                        .detach_and_log_err(cx);
 7269                }
 7270                MentionUri::Selection { abs_path: None, .. } => {}
 7271                MentionUri::Thread { id, name } => {
 7272                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 7273                        panel.update(cx, |panel, cx| {
 7274                            panel.open_thread(
 7275                                AgentSessionInfo {
 7276                                    session_id: id,
 7277                                    cwd: None,
 7278                                    title: Some(name.into()),
 7279                                    updated_at: None,
 7280                                    meta: None,
 7281                                },
 7282                                window,
 7283                                cx,
 7284                            )
 7285                        });
 7286                    }
 7287                }
 7288                MentionUri::TextThread { path, .. } => {
 7289                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 7290                        panel.update(cx, |panel, cx| {
 7291                            panel
 7292                                .open_saved_text_thread(path.as_path().into(), window, cx)
 7293                                .detach_and_log_err(cx);
 7294                        });
 7295                    }
 7296                }
 7297                MentionUri::Rule { id, .. } => {
 7298                    let PromptId::User { uuid } = id else {
 7299                        return;
 7300                    };
 7301                    window.dispatch_action(
 7302                        Box::new(OpenRulesLibrary {
 7303                            prompt_to_select: Some(uuid.0),
 7304                        }),
 7305                        cx,
 7306                    )
 7307                }
 7308                MentionUri::Fetch { url } => {
 7309                    cx.open_url(url.as_str());
 7310                }
 7311                MentionUri::Diagnostics { .. } => {}
 7312                MentionUri::TerminalSelection { .. } => {}
 7313            })
 7314        } else {
 7315            cx.open_url(&url);
 7316        }
 7317    }
 7318
 7319    fn open_tool_call_location(
 7320        &self,
 7321        entry_ix: usize,
 7322        location_ix: usize,
 7323        window: &mut Window,
 7324        cx: &mut Context<Self>,
 7325    ) -> Option<()> {
 7326        let (tool_call_location, agent_location) = self
 7327            .as_active_thread()?
 7328            .thread
 7329            .read(cx)
 7330            .entries()
 7331            .get(entry_ix)?
 7332            .location(location_ix)?;
 7333
 7334        let project_path = self
 7335            .project
 7336            .read(cx)
 7337            .find_project_path(&tool_call_location.path, cx)?;
 7338
 7339        let open_task = self
 7340            .workspace
 7341            .update(cx, |workspace, cx| {
 7342                workspace.open_path(project_path, None, true, window, cx)
 7343            })
 7344            .log_err()?;
 7345        window
 7346            .spawn(cx, async move |cx| {
 7347                let item = open_task.await?;
 7348
 7349                let Some(active_editor) = item.downcast::<Editor>() else {
 7350                    return anyhow::Ok(());
 7351                };
 7352
 7353                active_editor.update_in(cx, |editor, window, cx| {
 7354                    let multibuffer = editor.buffer().read(cx);
 7355                    let buffer = multibuffer.as_singleton();
 7356                    if agent_location.buffer.upgrade() == buffer {
 7357                        let excerpt_id = multibuffer.excerpt_ids().first().cloned();
 7358                        let anchor =
 7359                            editor::Anchor::in_buffer(excerpt_id.unwrap(), agent_location.position);
 7360                        editor.change_selections(Default::default(), window, cx, |selections| {
 7361                            selections.select_anchor_ranges([anchor..anchor]);
 7362                        })
 7363                    } else {
 7364                        let row = tool_call_location.line.unwrap_or_default();
 7365                        editor.change_selections(Default::default(), window, cx, |selections| {
 7366                            selections.select_ranges([Point::new(row, 0)..Point::new(row, 0)]);
 7367                        })
 7368                    }
 7369                })?;
 7370
 7371                anyhow::Ok(())
 7372            })
 7373            .detach_and_log_err(cx);
 7374
 7375        None
 7376    }
 7377
 7378    pub fn open_thread_as_markdown(
 7379        &self,
 7380        workspace: Entity<Workspace>,
 7381        window: &mut Window,
 7382        cx: &mut App,
 7383    ) -> Task<Result<()>> {
 7384        let markdown_language_task = workspace
 7385            .read(cx)
 7386            .app_state()
 7387            .languages
 7388            .language_for_name("Markdown");
 7389
 7390        let (thread_title, markdown) = if let Some(active) = self.as_active_thread() {
 7391            let thread = active.thread.read(cx);
 7392            (thread.title().to_string(), thread.to_markdown(cx))
 7393        } else {
 7394            return Task::ready(Ok(()));
 7395        };
 7396
 7397        let project = workspace.read(cx).project().clone();
 7398        window.spawn(cx, async move |cx| {
 7399            let markdown_language = markdown_language_task.await?;
 7400
 7401            let buffer = project
 7402                .update(cx, |project, cx| {
 7403                    project.create_buffer(Some(markdown_language), false, cx)
 7404                })
 7405                .await?;
 7406
 7407            buffer.update(cx, |buffer, cx| {
 7408                buffer.set_text(markdown, cx);
 7409                buffer.set_capability(language::Capability::ReadWrite, cx);
 7410            });
 7411
 7412            workspace.update_in(cx, |workspace, window, cx| {
 7413                let buffer = cx
 7414                    .new(|cx| MultiBuffer::singleton(buffer, cx).with_title(thread_title.clone()));
 7415
 7416                workspace.add_item_to_active_pane(
 7417                    Box::new(cx.new(|cx| {
 7418                        let mut editor =
 7419                            Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
 7420                        editor.set_breadcrumb_header(thread_title);
 7421                        editor
 7422                    })),
 7423                    None,
 7424                    true,
 7425                    window,
 7426                    cx,
 7427                );
 7428            })?;
 7429            anyhow::Ok(())
 7430        })
 7431    }
 7432
 7433    fn scroll_to_top(&mut self, cx: &mut Context<Self>) {
 7434        if let Some(list_state) = self
 7435            .as_active_thread_mut()
 7436            .map(|active| &mut active.list_state)
 7437        {
 7438            list_state.scroll_to(ListOffset::default());
 7439            cx.notify();
 7440        }
 7441    }
 7442
 7443    fn scroll_to_most_recent_user_prompt(&mut self, cx: &mut Context<Self>) {
 7444        let Some(active) = self.as_active_thread() else {
 7445            return;
 7446        };
 7447
 7448        let entries = active.thread.read(cx).entries();
 7449        if entries.is_empty() {
 7450            return;
 7451        }
 7452
 7453        // Find the most recent user message and scroll it to the top of the viewport.
 7454        // (Fallback: if no user message exists, scroll to the bottom.)
 7455        if let Some(ix) = entries
 7456            .iter()
 7457            .rposition(|entry| matches!(entry, AgentThreadEntry::UserMessage(_)))
 7458        {
 7459            if let Some(list_state) = self
 7460                .as_active_thread_mut()
 7461                .map(|active| &mut active.list_state)
 7462            {
 7463                list_state.scroll_to(ListOffset {
 7464                    item_ix: ix,
 7465                    offset_in_item: px(0.0),
 7466                });
 7467                cx.notify();
 7468            }
 7469        } else {
 7470            self.scroll_to_bottom(cx);
 7471        }
 7472    }
 7473
 7474    pub fn scroll_to_bottom(&mut self, cx: &mut Context<Self>) {
 7475        if let Some(active) = self.as_active_thread() {
 7476            let entry_count = active.thread.read(cx).entries().len();
 7477            active.list_state.reset(entry_count);
 7478            cx.notify();
 7479        }
 7480    }
 7481
 7482    fn notify_with_sound(
 7483        &mut self,
 7484        caption: impl Into<SharedString>,
 7485        icon: IconName,
 7486        window: &mut Window,
 7487        cx: &mut Context<Self>,
 7488    ) {
 7489        self.play_notification_sound(window, cx);
 7490        self.show_notification(caption, icon, window, cx);
 7491    }
 7492
 7493    fn play_notification_sound(&self, window: &Window, cx: &mut App) {
 7494        let settings = AgentSettings::get_global(cx);
 7495        if settings.play_sound_when_agent_done && !window.is_window_active() {
 7496            Audio::play_sound(Sound::AgentDone, cx);
 7497        }
 7498    }
 7499
 7500    fn show_notification(
 7501        &mut self,
 7502        caption: impl Into<SharedString>,
 7503        icon: IconName,
 7504        window: &mut Window,
 7505        cx: &mut Context<Self>,
 7506    ) {
 7507        if !self.notifications.is_empty() {
 7508            return;
 7509        }
 7510
 7511        let settings = AgentSettings::get_global(cx);
 7512
 7513        let window_is_inactive = !window.is_window_active();
 7514        let panel_is_hidden = self
 7515            .workspace
 7516            .upgrade()
 7517            .map(|workspace| AgentPanel::is_hidden(&workspace, cx))
 7518            .unwrap_or(true);
 7519
 7520        let should_notify = window_is_inactive || panel_is_hidden;
 7521
 7522        if !should_notify {
 7523            return;
 7524        }
 7525
 7526        // TODO: Change this once we have title summarization for external agents.
 7527        let title = self.agent.name();
 7528
 7529        match settings.notify_when_agent_waiting {
 7530            NotifyWhenAgentWaiting::PrimaryScreen => {
 7531                if let Some(primary) = cx.primary_display() {
 7532                    self.pop_up(icon, caption.into(), title, window, primary, cx);
 7533                }
 7534            }
 7535            NotifyWhenAgentWaiting::AllScreens => {
 7536                let caption = caption.into();
 7537                for screen in cx.displays() {
 7538                    self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx);
 7539                }
 7540            }
 7541            NotifyWhenAgentWaiting::Never => {
 7542                // Don't show anything
 7543            }
 7544        }
 7545    }
 7546
 7547    fn pop_up(
 7548        &mut self,
 7549        icon: IconName,
 7550        caption: SharedString,
 7551        title: SharedString,
 7552        window: &mut Window,
 7553        screen: Rc<dyn PlatformDisplay>,
 7554        cx: &mut Context<Self>,
 7555    ) {
 7556        let options = AgentNotification::window_options(screen, cx);
 7557
 7558        let project_name = self.workspace.upgrade().and_then(|workspace| {
 7559            workspace
 7560                .read(cx)
 7561                .project()
 7562                .read(cx)
 7563                .visible_worktrees(cx)
 7564                .next()
 7565                .map(|worktree| worktree.read(cx).root_name_str().to_string())
 7566        });
 7567
 7568        if let Some(screen_window) = cx
 7569            .open_window(options, |_window, cx| {
 7570                cx.new(|_cx| {
 7571                    AgentNotification::new(title.clone(), caption.clone(), icon, project_name)
 7572                })
 7573            })
 7574            .log_err()
 7575            && let Some(pop_up) = screen_window.entity(cx).log_err()
 7576        {
 7577            self.notification_subscriptions
 7578                .entry(screen_window)
 7579                .or_insert_with(Vec::new)
 7580                .push(cx.subscribe_in(&pop_up, window, {
 7581                    |this, _, event, window, cx| match event {
 7582                        AgentNotificationEvent::Accepted => {
 7583                            let handle = window.window_handle();
 7584                            cx.activate(true);
 7585
 7586                            let workspace_handle = this.workspace.clone();
 7587
 7588                            // If there are multiple Zed windows, activate the correct one.
 7589                            cx.defer(move |cx| {
 7590                                handle
 7591                                    .update(cx, |_view, window, _cx| {
 7592                                        window.activate_window();
 7593
 7594                                        if let Some(workspace) = workspace_handle.upgrade() {
 7595                                            workspace.update(_cx, |workspace, cx| {
 7596                                                workspace.focus_panel::<AgentPanel>(window, cx);
 7597                                            });
 7598                                        }
 7599                                    })
 7600                                    .log_err();
 7601                            });
 7602
 7603                            this.dismiss_notifications(cx);
 7604                        }
 7605                        AgentNotificationEvent::Dismissed => {
 7606                            this.dismiss_notifications(cx);
 7607                        }
 7608                    }
 7609                }));
 7610
 7611            self.notifications.push(screen_window);
 7612
 7613            // If the user manually refocuses the original window, dismiss the popup.
 7614            self.notification_subscriptions
 7615                .entry(screen_window)
 7616                .or_insert_with(Vec::new)
 7617                .push({
 7618                    let pop_up_weak = pop_up.downgrade();
 7619
 7620                    cx.observe_window_activation(window, move |_, window, cx| {
 7621                        if window.is_window_active()
 7622                            && let Some(pop_up) = pop_up_weak.upgrade()
 7623                        {
 7624                            pop_up.update(cx, |_, cx| {
 7625                                cx.emit(AgentNotificationEvent::Dismissed);
 7626                            });
 7627                        }
 7628                    })
 7629                });
 7630        }
 7631    }
 7632
 7633    fn dismiss_notifications(&mut self, cx: &mut Context<Self>) {
 7634        for window in self.notifications.drain(..) {
 7635            window
 7636                .update(cx, |_, window, _| {
 7637                    window.remove_window();
 7638                })
 7639                .ok();
 7640
 7641            self.notification_subscriptions.remove(&window);
 7642        }
 7643    }
 7644
 7645    fn render_generating(&self, confirmation: bool, cx: &App) -> impl IntoElement {
 7646        let Some(active) = self.as_active_thread() else {
 7647            return div().into_any_element();
 7648        };
 7649
 7650        let show_stats = AgentSettings::get_global(cx).show_turn_stats;
 7651        let elapsed_label = show_stats
 7652            .then(|| {
 7653                active.turn_fields.turn_started_at.and_then(|started_at| {
 7654                    let elapsed = started_at.elapsed();
 7655                    (elapsed > STOPWATCH_THRESHOLD).then(|| duration_alt_display(elapsed))
 7656                })
 7657            })
 7658            .flatten();
 7659
 7660        let is_waiting = confirmation || active.thread.read(cx).has_in_progress_tool_calls();
 7661
 7662        let turn_tokens_label = elapsed_label
 7663            .is_some()
 7664            .then(|| {
 7665                active
 7666                    .turn_fields
 7667                    .turn_tokens
 7668                    .filter(|&tokens| tokens > TOKEN_THRESHOLD)
 7669                    .map(|tokens| crate::text_thread_editor::humanize_token_count(tokens))
 7670            })
 7671            .flatten();
 7672
 7673        let arrow_icon = if is_waiting {
 7674            IconName::ArrowUp
 7675        } else {
 7676            IconName::ArrowDown
 7677        };
 7678
 7679        h_flex()
 7680            .id("generating-spinner")
 7681            .py_2()
 7682            .px(rems_from_px(22.))
 7683            .gap_2()
 7684            .map(|this| {
 7685                if confirmation {
 7686                    this.child(
 7687                        h_flex()
 7688                            .w_2()
 7689                            .child(SpinnerLabel::sand().size(LabelSize::Small)),
 7690                    )
 7691                    .child(
 7692                        div().min_w(rems(8.)).child(
 7693                            LoadingLabel::new("Awaiting Confirmation")
 7694                                .size(LabelSize::Small)
 7695                                .color(Color::Muted),
 7696                        ),
 7697                    )
 7698                } else {
 7699                    this.child(SpinnerLabel::new().size(LabelSize::Small))
 7700                }
 7701            })
 7702            .when_some(elapsed_label, |this, elapsed| {
 7703                this.child(
 7704                    Label::new(elapsed)
 7705                        .size(LabelSize::Small)
 7706                        .color(Color::Muted),
 7707                )
 7708            })
 7709            .when_some(turn_tokens_label, |this, tokens| {
 7710                this.child(
 7711                    h_flex()
 7712                        .gap_0p5()
 7713                        .child(
 7714                            Icon::new(arrow_icon)
 7715                                .size(IconSize::XSmall)
 7716                                .color(Color::Muted),
 7717                        )
 7718                        .child(
 7719                            Label::new(format!("{} tokens", tokens))
 7720                                .size(LabelSize::Small)
 7721                                .color(Color::Muted),
 7722                        ),
 7723                )
 7724            })
 7725            .into_any_element()
 7726    }
 7727
 7728    fn render_thread_controls(
 7729        &self,
 7730        thread: &Entity<AcpThread>,
 7731        cx: &Context<Self>,
 7732    ) -> impl IntoElement {
 7733        let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
 7734        if is_generating {
 7735            return self.render_generating(false, cx).into_any_element();
 7736        }
 7737
 7738        let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
 7739            .shape(ui::IconButtonShape::Square)
 7740            .icon_size(IconSize::Small)
 7741            .icon_color(Color::Ignored)
 7742            .tooltip(Tooltip::text("Open Thread as Markdown"))
 7743            .on_click(cx.listener(move |this, _, window, cx| {
 7744                if let Some(workspace) = this.workspace.upgrade() {
 7745                    this.open_thread_as_markdown(workspace, window, cx)
 7746                        .detach_and_log_err(cx);
 7747                }
 7748            }));
 7749
 7750        let scroll_to_recent_user_prompt =
 7751            IconButton::new("scroll_to_recent_user_prompt", IconName::ForwardArrow)
 7752                .shape(ui::IconButtonShape::Square)
 7753                .icon_size(IconSize::Small)
 7754                .icon_color(Color::Ignored)
 7755                .tooltip(Tooltip::text("Scroll To Most Recent User Prompt"))
 7756                .on_click(cx.listener(move |this, _, _, cx| {
 7757                    this.scroll_to_most_recent_user_prompt(cx);
 7758                }));
 7759
 7760        let scroll_to_top = IconButton::new("scroll_to_top", IconName::ArrowUp)
 7761            .shape(ui::IconButtonShape::Square)
 7762            .icon_size(IconSize::Small)
 7763            .icon_color(Color::Ignored)
 7764            .tooltip(Tooltip::text("Scroll To Top"))
 7765            .on_click(cx.listener(move |this, _, _, cx| {
 7766                this.scroll_to_top(cx);
 7767            }));
 7768
 7769        let Some(active) = self.as_active_thread() else {
 7770            return div().into_any_element();
 7771        };
 7772
 7773        let show_stats = AgentSettings::get_global(cx).show_turn_stats;
 7774        let last_turn_clock = show_stats
 7775            .then(|| {
 7776                active
 7777                    .turn_fields
 7778                    .last_turn_duration
 7779                    .filter(|&duration| duration > STOPWATCH_THRESHOLD)
 7780                    .map(|duration| {
 7781                        Label::new(duration_alt_display(duration))
 7782                            .size(LabelSize::Small)
 7783                            .color(Color::Muted)
 7784                    })
 7785            })
 7786            .flatten();
 7787
 7788        let last_turn_tokens_label = last_turn_clock
 7789            .is_some()
 7790            .then(|| {
 7791                active
 7792                    .turn_fields
 7793                    .last_turn_tokens
 7794                    .filter(|&tokens| tokens > TOKEN_THRESHOLD)
 7795                    .map(|tokens| {
 7796                        Label::new(format!(
 7797                            "{} tokens",
 7798                            crate::text_thread_editor::humanize_token_count(tokens)
 7799                        ))
 7800                        .size(LabelSize::Small)
 7801                        .color(Color::Muted)
 7802                    })
 7803            })
 7804            .flatten();
 7805
 7806        let mut container = h_flex()
 7807            .w_full()
 7808            .py_2()
 7809            .px_5()
 7810            .gap_px()
 7811            .opacity(0.6)
 7812            .hover(|s| s.opacity(1.))
 7813            .justify_end()
 7814            .when(
 7815                last_turn_tokens_label.is_some() || last_turn_clock.is_some(),
 7816                |this| {
 7817                    this.child(
 7818                        h_flex()
 7819                            .gap_1()
 7820                            .px_1()
 7821                            .when_some(last_turn_tokens_label, |this, label| this.child(label))
 7822                            .when_some(last_turn_clock, |this, label| this.child(label)),
 7823                    )
 7824                },
 7825            );
 7826
 7827        if let Some(active) = self.as_active_thread() {
 7828            if AgentSettings::get_global(cx).enable_feedback
 7829                && active.thread.read(cx).connection().telemetry().is_some()
 7830            {
 7831                let feedback = active.thread_feedback.feedback;
 7832
 7833                let tooltip_meta = || {
 7834                    SharedString::new(
 7835                        "Rating the thread sends all of your current conversation to the Zed team.",
 7836                    )
 7837                };
 7838
 7839                container = container
 7840                    .child(
 7841                        IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
 7842                            .shape(ui::IconButtonShape::Square)
 7843                            .icon_size(IconSize::Small)
 7844                            .icon_color(match feedback {
 7845                                Some(ThreadFeedback::Positive) => Color::Accent,
 7846                                _ => Color::Ignored,
 7847                            })
 7848                            .tooltip(move |window, cx| match feedback {
 7849                                Some(ThreadFeedback::Positive) => {
 7850                                    Tooltip::text("Thanks for your feedback!")(window, cx)
 7851                                }
 7852                                _ => {
 7853                                    Tooltip::with_meta("Helpful Response", None, tooltip_meta(), cx)
 7854                                }
 7855                            })
 7856                            .on_click(cx.listener(move |this, _, window, cx| {
 7857                                this.handle_feedback_click(ThreadFeedback::Positive, window, cx);
 7858                            })),
 7859                    )
 7860                    .child(
 7861                        IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
 7862                            .shape(ui::IconButtonShape::Square)
 7863                            .icon_size(IconSize::Small)
 7864                            .icon_color(match feedback {
 7865                                Some(ThreadFeedback::Negative) => Color::Accent,
 7866                                _ => Color::Ignored,
 7867                            })
 7868                            .tooltip(move |window, cx| match feedback {
 7869                                Some(ThreadFeedback::Negative) => {
 7870                                    Tooltip::text(
 7871                                    "We appreciate your feedback and will use it to improve in the future.",
 7872                                )(window, cx)
 7873                                }
 7874                                _ => {
 7875                                    Tooltip::with_meta(
 7876                                        "Not Helpful Response",
 7877                                        None,
 7878                                        tooltip_meta(),
 7879                                        cx,
 7880                                    )
 7881                                }
 7882                            })
 7883                            .on_click(cx.listener(move |this, _, window, cx| {
 7884                                this.handle_feedback_click(ThreadFeedback::Negative, window, cx);
 7885                            })),
 7886                    );
 7887            }
 7888        }
 7889
 7890        if cx.has_flag::<AgentSharingFeatureFlag>()
 7891            && self.is_imported_thread(cx)
 7892            && self
 7893                .project
 7894                .read(cx)
 7895                .client()
 7896                .status()
 7897                .borrow()
 7898                .is_connected()
 7899        {
 7900            let sync_button = IconButton::new("sync-thread", IconName::ArrowCircle)
 7901                .shape(ui::IconButtonShape::Square)
 7902                .icon_size(IconSize::Small)
 7903                .icon_color(Color::Ignored)
 7904                .tooltip(Tooltip::text("Sync with source thread"))
 7905                .on_click(cx.listener(move |this, _, window, cx| {
 7906                    this.sync_thread(window, cx);
 7907                }));
 7908
 7909            container = container.child(sync_button);
 7910        }
 7911
 7912        if cx.has_flag::<AgentSharingFeatureFlag>() && !self.is_imported_thread(cx) {
 7913            let share_button = IconButton::new("share-thread", IconName::ArrowUpRight)
 7914                .shape(ui::IconButtonShape::Square)
 7915                .icon_size(IconSize::Small)
 7916                .icon_color(Color::Ignored)
 7917                .tooltip(Tooltip::text("Share Thread"))
 7918                .on_click(cx.listener(move |this, _, window, cx| {
 7919                    this.share_thread(window, cx);
 7920                }));
 7921
 7922            container = container.child(share_button);
 7923        }
 7924
 7925        container
 7926            .child(open_as_markdown)
 7927            .child(scroll_to_recent_user_prompt)
 7928            .child(scroll_to_top)
 7929            .into_any_element()
 7930    }
 7931
 7932    fn render_feedback_feedback_editor(editor: Entity<Editor>, cx: &Context<Self>) -> Div {
 7933        h_flex()
 7934            .key_context("AgentFeedbackMessageEditor")
 7935            .on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| {
 7936                if let Some(active) = this.as_active_thread_mut() {
 7937                    active.thread_feedback.dismiss_comments();
 7938                }
 7939                cx.notify();
 7940            }))
 7941            .on_action(cx.listener(move |this, _: &menu::Confirm, _window, cx| {
 7942                this.submit_feedback_message(cx);
 7943            }))
 7944            .p_2()
 7945            .mb_2()
 7946            .mx_5()
 7947            .gap_1()
 7948            .rounded_md()
 7949            .border_1()
 7950            .border_color(cx.theme().colors().border)
 7951            .bg(cx.theme().colors().editor_background)
 7952            .child(div().w_full().child(editor))
 7953            .child(
 7954                h_flex()
 7955                    .child(
 7956                        IconButton::new("dismiss-feedback-message", IconName::Close)
 7957                            .icon_color(Color::Error)
 7958                            .icon_size(IconSize::XSmall)
 7959                            .shape(ui::IconButtonShape::Square)
 7960                            .on_click(cx.listener(move |this, _, _window, cx| {
 7961                                if let Some(active) = this.as_active_thread_mut() {
 7962                                    active.thread_feedback.dismiss_comments();
 7963                                }
 7964                                cx.notify();
 7965                            })),
 7966                    )
 7967                    .child(
 7968                        IconButton::new("submit-feedback-message", IconName::Return)
 7969                            .icon_size(IconSize::XSmall)
 7970                            .shape(ui::IconButtonShape::Square)
 7971                            .on_click(cx.listener(move |this, _, _window, cx| {
 7972                                this.submit_feedback_message(cx);
 7973                            })),
 7974                    ),
 7975            )
 7976    }
 7977
 7978    fn handle_feedback_click(
 7979        &mut self,
 7980        feedback: ThreadFeedback,
 7981        window: &mut Window,
 7982        cx: &mut Context<Self>,
 7983    ) {
 7984        let Some(active) = self.as_active_thread_mut() else {
 7985            return;
 7986        };
 7987
 7988        active
 7989            .thread_feedback
 7990            .submit(active.thread.clone(), feedback, window, cx);
 7991        cx.notify();
 7992    }
 7993
 7994    fn submit_feedback_message(&mut self, cx: &mut Context<Self>) {
 7995        let Some(active) = self.as_active_thread_mut() else {
 7996            return;
 7997        };
 7998
 7999        active
 8000            .thread_feedback
 8001            .submit_comments(active.thread.clone(), cx);
 8002        cx.notify();
 8003    }
 8004
 8005    fn render_token_limit_callout(&self, cx: &mut Context<Self>) -> Option<Callout> {
 8006        let Some(active) = self.as_active_thread() else {
 8007            return None;
 8008        };
 8009
 8010        if active.token_limit_callout_dismissed {
 8011            return None;
 8012        }
 8013
 8014        let token_usage = active.thread.read(cx).token_usage()?;
 8015        let ratio = token_usage.ratio();
 8016
 8017        let (severity, icon, title) = match ratio {
 8018            acp_thread::TokenUsageRatio::Normal => return None,
 8019            acp_thread::TokenUsageRatio::Warning => (
 8020                Severity::Warning,
 8021                IconName::Warning,
 8022                "Thread reaching the token limit soon",
 8023            ),
 8024            acp_thread::TokenUsageRatio::Exceeded => (
 8025                Severity::Error,
 8026                IconName::XCircle,
 8027                "Thread reached the token limit",
 8028            ),
 8029        };
 8030
 8031        let description = "To continue, start a new thread from a summary.";
 8032
 8033        Some(
 8034            Callout::new()
 8035                .severity(severity)
 8036                .icon(icon)
 8037                .title(title)
 8038                .description(description)
 8039                .actions_slot(
 8040                    h_flex().gap_0p5().child(
 8041                        Button::new("start-new-thread", "Start New Thread")
 8042                            .label_size(LabelSize::Small)
 8043                            .on_click(cx.listener(|this, _, window, cx| {
 8044                                let Some(active) = this.as_active_thread() else {
 8045                                    return;
 8046                                };
 8047                                let session_id = active.thread.read(cx).session_id().clone();
 8048                                window.dispatch_action(
 8049                                    crate::NewNativeAgentThreadFromSummary {
 8050                                        from_session_id: session_id,
 8051                                    }
 8052                                    .boxed_clone(),
 8053                                    cx,
 8054                                );
 8055                            })),
 8056                    ),
 8057                )
 8058                .dismiss_action(self.dismiss_error_button(cx)),
 8059        )
 8060    }
 8061
 8062    fn agent_ui_font_size_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
 8063        if let Some(entry_view_state) = self
 8064            .as_active_thread()
 8065            .map(|active| &active.entry_view_state)
 8066            .cloned()
 8067        {
 8068            entry_view_state.update(cx, |entry_view_state, cx| {
 8069                entry_view_state.agent_ui_font_size_changed(cx);
 8070            });
 8071        }
 8072    }
 8073
 8074    pub(crate) fn insert_dragged_files(
 8075        &self,
 8076        paths: Vec<project::ProjectPath>,
 8077        added_worktrees: Vec<Entity<project::Worktree>>,
 8078        window: &mut Window,
 8079        cx: &mut Context<Self>,
 8080    ) {
 8081        self.message_editor.update(cx, |message_editor, cx| {
 8082            message_editor.insert_dragged_files(paths, added_worktrees, window, cx);
 8083        })
 8084    }
 8085
 8086    /// Inserts the selected text into the message editor or the message being
 8087    /// edited, if any.
 8088    pub(crate) fn insert_selections(&self, window: &mut Window, cx: &mut Context<Self>) {
 8089        self.active_editor(cx).update(cx, |editor, cx| {
 8090            editor.insert_selections(window, cx);
 8091        });
 8092    }
 8093
 8094    /// Inserts terminal text as a crease into the message editor.
 8095    pub(crate) fn insert_terminal_text(
 8096        &self,
 8097        text: String,
 8098        window: &mut Window,
 8099        cx: &mut Context<Self>,
 8100    ) {
 8101        self.message_editor.update(cx, |message_editor, cx| {
 8102            message_editor.insert_terminal_crease(text, window, cx);
 8103        });
 8104    }
 8105
 8106    /// Inserts code snippets as creases into the message editor.
 8107    pub(crate) fn insert_code_crease(
 8108        &self,
 8109        creases: Vec<(String, String)>,
 8110        window: &mut Window,
 8111        cx: &mut Context<Self>,
 8112    ) {
 8113        self.message_editor.update(cx, |message_editor, cx| {
 8114            message_editor.insert_code_creases(creases, window, cx);
 8115        });
 8116    }
 8117
 8118    fn render_codex_windows_warning(&self, cx: &mut Context<Self>) -> Callout {
 8119        Callout::new()
 8120            .icon(IconName::Warning)
 8121            .severity(Severity::Warning)
 8122            .title("Codex on Windows")
 8123            .description("For best performance, run Codex in Windows Subsystem for Linux (WSL2)")
 8124            .actions_slot(
 8125                Button::new("open-wsl-modal", "Open in WSL")
 8126                    .icon_size(IconSize::Small)
 8127                    .icon_color(Color::Muted)
 8128                    .on_click(cx.listener({
 8129                        move |_, _, _window, cx| {
 8130                            #[cfg(windows)]
 8131                            _window.dispatch_action(
 8132                                zed_actions::wsl_actions::OpenWsl::default().boxed_clone(),
 8133                                cx,
 8134                            );
 8135                            cx.notify();
 8136                        }
 8137                    })),
 8138            )
 8139            .dismiss_action(
 8140                IconButton::new("dismiss", IconName::Close)
 8141                    .icon_size(IconSize::Small)
 8142                    .icon_color(Color::Muted)
 8143                    .tooltip(Tooltip::text("Dismiss Warning"))
 8144                    .on_click(cx.listener({
 8145                        move |this, _, _, cx| {
 8146                            this.show_codex_windows_warning = false;
 8147                            cx.notify();
 8148                        }
 8149                    })),
 8150            )
 8151    }
 8152
 8153    fn clear_command_load_errors(&mut self, cx: &mut Context<Self>) {
 8154        if let Some(active) = self.as_active_thread_mut() {
 8155            active.command_load_errors_dismissed = true;
 8156        }
 8157        cx.notify();
 8158    }
 8159
 8160    fn refresh_cached_user_commands(&mut self, cx: &mut Context<Self>) {
 8161        let Some(registry) = self.slash_command_registry.clone() else {
 8162            return;
 8163        };
 8164        self.refresh_cached_user_commands_from_registry(&registry, cx);
 8165    }
 8166
 8167    fn refresh_cached_user_commands_from_registry(
 8168        &mut self,
 8169        registry: &Entity<SlashCommandRegistry>,
 8170        cx: &mut Context<Self>,
 8171    ) {
 8172        let Some(thread_state) = self.as_active_thread_mut() else {
 8173            return;
 8174        };
 8175        thread_state.refresh_cached_user_commands_from_registry(registry, cx);
 8176        cx.notify();
 8177    }
 8178
 8179    /// Returns the cached slash commands, if available.
 8180    pub fn cached_slash_commands(
 8181        &self,
 8182        _cx: &App,
 8183    ) -> collections::HashMap<String, UserSlashCommand> {
 8184        let Some(thread_state) = &self.as_active_thread() else {
 8185            return collections::HashMap::default();
 8186        };
 8187        thread_state.cached_user_commands.borrow().clone()
 8188    }
 8189
 8190    /// Returns the cached slash command errors, if available.
 8191    fn cached_slash_command_errors(&self, _cx: &App) -> Vec<CommandLoadError> {
 8192        let Some(thread_state) = &self.as_active_thread() else {
 8193            return Vec::new();
 8194        };
 8195        thread_state.cached_user_command_errors.borrow().clone()
 8196    }
 8197
 8198    fn render_thread_error(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
 8199        let view = self.as_active_thread()?;
 8200
 8201        let connection = view.thread.read(cx).connection().clone();
 8202
 8203        let content = match view.thread_error.as_ref()? {
 8204            ThreadError::Other { message, .. } => {
 8205                self.render_any_thread_error(message.clone(), window, cx)
 8206            }
 8207            ThreadError::Refusal => self.render_refusal_error(cx),
 8208            ThreadError::AuthenticationRequired(error) => {
 8209                self.render_authentication_required_error(error.clone(), connection, cx)
 8210            }
 8211            ThreadError::PaymentRequired => self.render_payment_required_error(cx),
 8212        };
 8213
 8214        Some(div().child(content))
 8215    }
 8216
 8217    fn render_new_version_callout(&self, version: &SharedString, cx: &mut Context<Self>) -> Div {
 8218        v_flex().w_full().justify_end().child(
 8219            h_flex()
 8220                .p_2()
 8221                .pr_3()
 8222                .w_full()
 8223                .gap_1p5()
 8224                .border_t_1()
 8225                .border_color(cx.theme().colors().border)
 8226                .bg(cx.theme().colors().element_background)
 8227                .child(
 8228                    h_flex()
 8229                        .flex_1()
 8230                        .gap_1p5()
 8231                        .child(
 8232                            Icon::new(IconName::Download)
 8233                                .color(Color::Accent)
 8234                                .size(IconSize::Small),
 8235                        )
 8236                        .child(Label::new("New version available").size(LabelSize::Small)),
 8237                )
 8238                .child(
 8239                    Button::new("update-button", format!("Update to v{}", version))
 8240                        .label_size(LabelSize::Small)
 8241                        .style(ButtonStyle::Tinted(TintColor::Accent))
 8242                        .on_click(cx.listener(|this, _, window, cx| {
 8243                            this.reset(window, cx);
 8244                        })),
 8245                ),
 8246        )
 8247    }
 8248
 8249    fn current_model_name(&self, cx: &App) -> SharedString {
 8250        // For native agent (Zed Agent), use the specific model name (e.g., "Claude 3.5 Sonnet")
 8251        // For ACP agents, use the agent name (e.g., "Claude Code", "Gemini CLI")
 8252        // This provides better clarity about what refused the request
 8253        if self.as_native_connection(cx).is_some() {
 8254            self.as_active_thread()
 8255                .and_then(|active| active.model_selector.as_ref())
 8256                .and_then(|selector| selector.read(cx).active_model(cx))
 8257                .map(|model| model.name.clone())
 8258                .unwrap_or_else(|| SharedString::from("The model"))
 8259        } else {
 8260            // ACP agent - use the agent name (e.g., "Claude Code", "Gemini CLI")
 8261            self.agent.name()
 8262        }
 8263    }
 8264
 8265    fn render_refusal_error(&self, cx: &mut Context<'_, Self>) -> Callout {
 8266        let model_or_agent_name = self.current_model_name(cx);
 8267        let refusal_message = format!(
 8268            "{} refused to respond to this prompt. \
 8269            This can happen when a model believes the prompt violates its content policy \
 8270            or safety guidelines, so rephrasing it can sometimes address the issue.",
 8271            model_or_agent_name
 8272        );
 8273
 8274        Callout::new()
 8275            .severity(Severity::Error)
 8276            .title("Request Refused")
 8277            .icon(IconName::XCircle)
 8278            .description(refusal_message.clone())
 8279            .actions_slot(self.create_copy_button(&refusal_message))
 8280            .dismiss_action(self.dismiss_error_button(cx))
 8281    }
 8282
 8283    fn set_can_fast_track_queue(&mut self, value: bool) {
 8284        if let Some(active) = self.as_active_thread_mut() {
 8285            active.can_fast_track_queue = value;
 8286        }
 8287    }
 8288
 8289    fn render_any_thread_error(
 8290        &mut self,
 8291        error: SharedString,
 8292        window: &mut Window,
 8293        cx: &mut Context<'_, Self>,
 8294    ) -> Callout {
 8295        let can_resume = self
 8296            .as_active_thread()
 8297            .map_or(false, |active| active.thread.read(cx).can_retry(cx));
 8298
 8299        let markdown = if let Some(thread_state) = self.as_active_thread()
 8300            && let Some(markdown) = &thread_state.thread_error_markdown
 8301        {
 8302            markdown.clone()
 8303        } else {
 8304            let markdown = cx.new(|cx| Markdown::new(error.clone(), None, None, cx));
 8305            if let Some(thread_state) = self.as_active_thread_mut() {
 8306                thread_state.thread_error_markdown = Some(markdown.clone());
 8307            }
 8308            markdown
 8309        };
 8310
 8311        let markdown_style =
 8312            MarkdownStyle::themed(MarkdownFont::Agent, window, cx).with_muted_text(cx);
 8313        let description = self
 8314            .render_markdown(markdown, markdown_style)
 8315            .into_any_element();
 8316
 8317        Callout::new()
 8318            .severity(Severity::Error)
 8319            .icon(IconName::XCircle)
 8320            .title("An Error Happened")
 8321            .description_slot(description)
 8322            .actions_slot(
 8323                h_flex()
 8324                    .gap_0p5()
 8325                    .when(can_resume, |this| {
 8326                        this.child(
 8327                            IconButton::new("retry", IconName::RotateCw)
 8328                                .icon_size(IconSize::Small)
 8329                                .tooltip(Tooltip::text("Retry Generation"))
 8330                                .on_click(cx.listener(|this, _, _window, cx| {
 8331                                    this.retry_generation(cx);
 8332                                })),
 8333                        )
 8334                    })
 8335                    .child(self.create_copy_button(error.to_string())),
 8336            )
 8337            .dismiss_action(self.dismiss_error_button(cx))
 8338    }
 8339
 8340    fn render_payment_required_error(&self, cx: &mut Context<Self>) -> Callout {
 8341        const ERROR_MESSAGE: &str =
 8342            "You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
 8343
 8344        Callout::new()
 8345            .severity(Severity::Error)
 8346            .icon(IconName::XCircle)
 8347            .title("Free Usage Exceeded")
 8348            .description(ERROR_MESSAGE)
 8349            .actions_slot(
 8350                h_flex()
 8351                    .gap_0p5()
 8352                    .child(self.upgrade_button(cx))
 8353                    .child(self.create_copy_button(ERROR_MESSAGE)),
 8354            )
 8355            .dismiss_action(self.dismiss_error_button(cx))
 8356    }
 8357
 8358    fn render_authentication_required_error(
 8359        &self,
 8360        error: SharedString,
 8361        connection: Rc<dyn AgentConnection>,
 8362        cx: &mut Context<Self>,
 8363    ) -> Callout {
 8364        Callout::new()
 8365            .severity(Severity::Error)
 8366            .title("Authentication Required")
 8367            .icon(IconName::XCircle)
 8368            .description(error.clone())
 8369            .actions_slot(
 8370                h_flex()
 8371                    .gap_0p5()
 8372                    .child(self.authenticate_button(connection, cx))
 8373                    .child(self.create_copy_button(error)),
 8374            )
 8375            .dismiss_action(self.dismiss_error_button(cx))
 8376    }
 8377
 8378    fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
 8379        let message = message.into();
 8380
 8381        CopyButton::new("copy-error-message", message).tooltip_label("Copy Error Message")
 8382    }
 8383
 8384    fn dismiss_error_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
 8385        IconButton::new("dismiss", IconName::Close)
 8386            .icon_size(IconSize::Small)
 8387            .tooltip(Tooltip::text("Dismiss"))
 8388            .on_click(cx.listener({
 8389                move |this, _, _, cx| {
 8390                    this.clear_thread_error(cx);
 8391                    cx.notify();
 8392                }
 8393            }))
 8394    }
 8395
 8396    fn authenticate_button(
 8397        &self,
 8398        connection: Rc<dyn AgentConnection>,
 8399        cx: &mut Context<Self>,
 8400    ) -> impl IntoElement {
 8401        Button::new("authenticate", "Authenticate")
 8402            .label_size(LabelSize::Small)
 8403            .style(ButtonStyle::Filled)
 8404            .on_click(cx.listener({
 8405                let connection = connection.clone();
 8406                move |this, _, window, cx| {
 8407                    let agent_name = this.agent.name();
 8408                    this.clear_thread_error(cx);
 8409                    if let Some(message) = this.in_flight_prompt.take() {
 8410                        this.message_editor.update(cx, |editor, cx| {
 8411                            editor.set_message(message, window, cx);
 8412                        });
 8413                    }
 8414                    let this = cx.weak_entity();
 8415                    window.defer(cx, {
 8416                        let connection = connection.clone();
 8417                        move |window, cx| {
 8418                            Self::handle_auth_required(
 8419                                this,
 8420                                AuthRequired::new(),
 8421                                agent_name,
 8422                                connection,
 8423                                window,
 8424                                cx,
 8425                            );
 8426                        }
 8427                    })
 8428                }
 8429            }))
 8430    }
 8431
 8432    pub(crate) fn reauthenticate(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 8433        let Some(view) = self.as_active_thread() else {
 8434            return;
 8435        };
 8436        let connection = view.thread.read(cx).connection().clone();
 8437        let agent_name = self.agent.name();
 8438        self.clear_thread_error(cx);
 8439        let this = cx.weak_entity();
 8440        window.defer(cx, |window, cx| {
 8441            Self::handle_auth_required(
 8442                this,
 8443                AuthRequired::new(),
 8444                agent_name,
 8445                connection,
 8446                window,
 8447                cx,
 8448            );
 8449        })
 8450    }
 8451
 8452    fn upgrade_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
 8453        Button::new("upgrade", "Upgrade")
 8454            .label_size(LabelSize::Small)
 8455            .style(ButtonStyle::Tinted(ui::TintColor::Accent))
 8456            .on_click(cx.listener({
 8457                move |this, _, _, cx| {
 8458                    this.clear_thread_error(cx);
 8459                    cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx));
 8460                }
 8461            }))
 8462    }
 8463
 8464    pub fn delete_history_entry(&mut self, entry: AgentSessionInfo, cx: &mut Context<Self>) {
 8465        let task = self.history.update(cx, |history, cx| {
 8466            history.delete_session(&entry.session_id, cx)
 8467        });
 8468        task.detach_and_log_err(cx);
 8469    }
 8470
 8471    /// Returns the currently active editor, either for a message that is being
 8472    /// edited or the editor for a new message.
 8473    fn active_editor(&self, cx: &App) -> Entity<MessageEditor> {
 8474        if let Some(thread_state) = self.as_active_thread()
 8475            && let Some(index) = thread_state.editing_message
 8476            && let Some(editor) = thread_state
 8477                .entry_view_state
 8478                .read(cx)
 8479                .entry(index)
 8480                .and_then(|entry| entry.message_editor())
 8481                .cloned()
 8482        {
 8483            editor
 8484        } else {
 8485            self.message_editor.clone()
 8486        }
 8487    }
 8488
 8489    fn get_agent_message_content(
 8490        entries: &[AgentThreadEntry],
 8491        entry_index: usize,
 8492        cx: &App,
 8493    ) -> Option<String> {
 8494        let entry = entries.get(entry_index)?;
 8495        if matches!(entry, AgentThreadEntry::UserMessage(_)) {
 8496            return None;
 8497        }
 8498
 8499        let start_index = (0..entry_index)
 8500            .rev()
 8501            .find(|&i| matches!(entries.get(i), Some(AgentThreadEntry::UserMessage(_))))
 8502            .map(|i| i + 1)
 8503            .unwrap_or(0);
 8504
 8505        let end_index = (entry_index + 1..entries.len())
 8506            .find(|&i| matches!(entries.get(i), Some(AgentThreadEntry::UserMessage(_))))
 8507            .map(|i| i - 1)
 8508            .unwrap_or(entries.len() - 1);
 8509
 8510        let parts: Vec<String> = (start_index..=end_index)
 8511            .filter_map(|i| entries.get(i))
 8512            .filter_map(|entry| {
 8513                if let AgentThreadEntry::AssistantMessage(message) = entry {
 8514                    let text: String = message
 8515                        .chunks
 8516                        .iter()
 8517                        .filter_map(|chunk| match chunk {
 8518                            AssistantMessageChunk::Message { block } => {
 8519                                let markdown = block.to_markdown(cx);
 8520                                if markdown.trim().is_empty() {
 8521                                    None
 8522                                } else {
 8523                                    Some(markdown.to_string())
 8524                                }
 8525                            }
 8526                            AssistantMessageChunk::Thought { .. } => None,
 8527                        })
 8528                        .collect::<Vec<_>>()
 8529                        .join("\n\n");
 8530
 8531                    if text.is_empty() { None } else { Some(text) }
 8532                } else {
 8533                    None
 8534                }
 8535            })
 8536            .collect();
 8537
 8538        let text = parts.join("\n\n");
 8539        if text.is_empty() { None } else { Some(text) }
 8540    }
 8541}
 8542
 8543fn loading_contents_spinner(size: IconSize) -> AnyElement {
 8544    Icon::new(IconName::LoadCircle)
 8545        .size(size)
 8546        .color(Color::Accent)
 8547        .with_rotate_animation(3)
 8548        .into_any_element()
 8549}
 8550
 8551fn placeholder_text(agent_name: &str, has_commands: bool) -> String {
 8552    if agent_name == "Zed Agent" {
 8553        format!("Message the {} — @ to include context", agent_name)
 8554    } else if has_commands {
 8555        format!(
 8556            "Message {} — @ to include context, / for commands",
 8557            agent_name
 8558        )
 8559    } else {
 8560        format!("Message {} — @ to include context", agent_name)
 8561    }
 8562}
 8563
 8564impl Focusable for AcpServerView {
 8565    fn focus_handle(&self, cx: &App) -> FocusHandle {
 8566        match self.as_active_thread() {
 8567            Some(_) => self.active_editor(cx).focus_handle(cx),
 8568            None => self.focus_handle.clone(),
 8569        }
 8570    }
 8571}
 8572
 8573#[cfg(any(test, feature = "test-support"))]
 8574impl AcpServerView {
 8575    /// Expands a tool call so its content is visible.
 8576    /// This is primarily useful for visual testing.
 8577    pub fn expand_tool_call(&mut self, tool_call_id: acp::ToolCallId, cx: &mut Context<Self>) {
 8578        if let Some(active) = self.as_active_thread_mut() {
 8579            active.expanded_tool_calls.insert(tool_call_id);
 8580            cx.notify();
 8581        }
 8582    }
 8583
 8584    /// Expands a subagent card so its content is visible.
 8585    /// This is primarily useful for visual testing.
 8586    pub fn expand_subagent(&mut self, session_id: acp::SessionId, cx: &mut Context<Self>) {
 8587        if let Some(active) = self.as_active_thread_mut() {
 8588            active.expanded_subagents.insert(session_id);
 8589            cx.notify();
 8590        }
 8591    }
 8592}
 8593
 8594impl Render for AcpServerView {
 8595    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 8596        self.sync_queued_message_editors(window, cx);
 8597
 8598        let has_messages = self
 8599            .as_active_thread()
 8600            .is_some_and(|active| active.list_state.item_count() > 0);
 8601
 8602        v_flex()
 8603            .size_full()
 8604            .key_context("AcpThread")
 8605            .on_action(cx.listener(|this, _: &menu::Cancel, _, cx| {
 8606                this.cancel_generation(cx);
 8607            }))
 8608            .on_action(cx.listener(Self::keep_all))
 8609            .on_action(cx.listener(Self::reject_all))
 8610            .on_action(cx.listener(Self::allow_always))
 8611            .on_action(cx.listener(Self::allow_once))
 8612            .on_action(cx.listener(Self::reject_once))
 8613            .on_action(cx.listener(Self::handle_authorize_tool_call))
 8614            .on_action(cx.listener(Self::handle_select_permission_granularity))
 8615            .on_action(cx.listener(Self::open_permission_dropdown))
 8616            .on_action(cx.listener(Self::open_add_context_menu))
 8617            .on_action(cx.listener(|this, _: &ToggleThinkingMode, _window, cx| {
 8618                if let Some(thread) = this.as_native_thread(cx) {
 8619                    thread.update(cx, |thread, cx| {
 8620                        thread.set_thinking_enabled(!thread.thinking_enabled(), cx);
 8621                    });
 8622                }
 8623            }))
 8624            .on_action(cx.listener(|this, _: &SendNextQueuedMessage, window, cx| {
 8625                this.send_queued_message_at_index(0, true, window, cx);
 8626            }))
 8627            .on_action(cx.listener(|this, _: &RemoveFirstQueuedMessage, _, cx| {
 8628                this.remove_from_queue(0, cx);
 8629                cx.notify();
 8630            }))
 8631            .on_action(cx.listener(|this, _: &EditFirstQueuedMessage, window, cx| {
 8632                if let Some(active) = this.as_active_thread()
 8633                    && let Some(editor) = active.queued_message_editors.first()
 8634                {
 8635                    window.focus(&editor.focus_handle(cx), cx);
 8636                }
 8637            }))
 8638            .on_action(cx.listener(|this, _: &ClearMessageQueue, _, cx| {
 8639                this.clear_queue(cx);
 8640                if let Some(state) = this.as_active_thread_mut() {
 8641                    state.can_fast_track_queue = false;
 8642                }
 8643                cx.notify();
 8644            }))
 8645            .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
 8646                if let Some(config_options_view) = this
 8647                    .as_active_thread()
 8648                    .and_then(|active| active.config_options_view.as_ref())
 8649                {
 8650                    let handled = config_options_view.update(cx, |view, cx| {
 8651                        view.toggle_category_picker(
 8652                            acp::SessionConfigOptionCategory::Mode,
 8653                            window,
 8654                            cx,
 8655                        )
 8656                    });
 8657                    if handled {
 8658                        return;
 8659                    }
 8660                }
 8661
 8662                if let Some(profile_selector) = this
 8663                    .as_active_thread()
 8664                    .and_then(|active| active.profile_selector.as_ref())
 8665                {
 8666                    profile_selector.read(cx).menu_handle().toggle(window, cx);
 8667                } else if let Some(mode_selector) = this
 8668                    .as_active_thread()
 8669                    .and_then(|active| active.mode_selector.as_ref())
 8670                {
 8671                    mode_selector.read(cx).menu_handle().toggle(window, cx);
 8672                }
 8673            }))
 8674            .on_action(cx.listener(|this, _: &CycleModeSelector, window, cx| {
 8675                if let Some(config_options_view) = this
 8676                    .as_active_thread()
 8677                    .and_then(|active| active.config_options_view.as_ref())
 8678                {
 8679                    let handled = config_options_view.update(cx, |view, cx| {
 8680                        view.cycle_category_option(
 8681                            acp::SessionConfigOptionCategory::Mode,
 8682                            false,
 8683                            cx,
 8684                        )
 8685                    });
 8686                    if handled {
 8687                        return;
 8688                    }
 8689                }
 8690
 8691                if let Some(profile_selector) = this
 8692                    .as_active_thread()
 8693                    .and_then(|active| active.profile_selector.as_ref())
 8694                {
 8695                    profile_selector.update(cx, |profile_selector, cx| {
 8696                        profile_selector.cycle_profile(cx);
 8697                    });
 8698                } else if let Some(mode_selector) = this
 8699                    .as_active_thread()
 8700                    .and_then(|active| active.mode_selector.as_ref())
 8701                {
 8702                    mode_selector.update(cx, |mode_selector, cx| {
 8703                        mode_selector.cycle_mode(window, cx);
 8704                    });
 8705                }
 8706            }))
 8707            .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
 8708                if let Some(config_options_view) = this
 8709                    .as_active_thread()
 8710                    .and_then(|active| active.config_options_view.as_ref())
 8711                {
 8712                    let handled = config_options_view.update(cx, |view, cx| {
 8713                        view.toggle_category_picker(
 8714                            acp::SessionConfigOptionCategory::Model,
 8715                            window,
 8716                            cx,
 8717                        )
 8718                    });
 8719                    if handled {
 8720                        return;
 8721                    }
 8722                }
 8723
 8724                if let Some(model_selector) = this
 8725                    .as_active_thread()
 8726                    .and_then(|active| active.model_selector.as_ref())
 8727                {
 8728                    model_selector
 8729                        .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
 8730                }
 8731            }))
 8732            .on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
 8733                if let Some(config_options_view) = this
 8734                    .as_active_thread()
 8735                    .and_then(|active| active.config_options_view.as_ref())
 8736                {
 8737                    let handled = config_options_view.update(cx, |view, cx| {
 8738                        view.cycle_category_option(
 8739                            acp::SessionConfigOptionCategory::Model,
 8740                            true,
 8741                            cx,
 8742                        )
 8743                    });
 8744                    if handled {
 8745                        return;
 8746                    }
 8747                }
 8748
 8749                if let Some(model_selector) = this
 8750                    .as_active_thread()
 8751                    .and_then(|active| active.model_selector.as_ref())
 8752                {
 8753                    model_selector.update(cx, |model_selector, cx| {
 8754                        model_selector.cycle_favorite_models(window, cx);
 8755                    });
 8756                }
 8757            }))
 8758            .track_focus(&self.focus_handle)
 8759            .bg(cx.theme().colors().panel_background)
 8760            .child(match &self.server_state {
 8761                ServerState::Loading { .. } => v_flex()
 8762                    .flex_1()
 8763                    .child(self.render_recent_history(cx))
 8764                    .into_any(),
 8765                ServerState::LoadError(e) => v_flex()
 8766                    .flex_1()
 8767                    .size_full()
 8768                    .items_center()
 8769                    .justify_end()
 8770                    .child(self.render_load_error(e, window, cx))
 8771                    .into_any(),
 8772                ServerState::Connected(ConnectedServerState {
 8773                    connection,
 8774                    auth_state:
 8775                        AuthState::Unauthenticated {
 8776                            description,
 8777                            configuration_view,
 8778                            pending_auth_method,
 8779                            _subscription,
 8780                        },
 8781                    ..
 8782                }) => v_flex()
 8783                    .flex_1()
 8784                    .size_full()
 8785                    .justify_end()
 8786                    .child(self.render_auth_required_state(
 8787                        connection,
 8788                        description.as_ref(),
 8789                        configuration_view.as_ref(),
 8790                        pending_auth_method.as_ref(),
 8791                        window,
 8792                        cx,
 8793                    ))
 8794                    .into_any_element(),
 8795                ServerState::Connected(connected) => {
 8796                    if let Some(current) = connected.current.as_ref() {
 8797                        v_flex()
 8798                            .flex_1()
 8799                            .map(|this| {
 8800                                let this = this.when(current.resumed_without_history, |this| {
 8801                                    this.child(self.render_resume_notice(cx))
 8802                                });
 8803                                if has_messages {
 8804                                    this.child(
 8805                                        list(
 8806                                            current.list_state.clone(),
 8807                                            cx.processor(|this, index: usize, window, cx| {
 8808                                                let Some((entry, len)) =
 8809                                                    this.as_active_thread().and_then(|active| {
 8810                                                        let entries =
 8811                                                            &active.thread.read(cx).entries();
 8812                                                        Some((entries.get(index)?, entries.len()))
 8813                                                    })
 8814                                                else {
 8815                                                    return Empty.into_any();
 8816                                                };
 8817                                                this.render_entry(index, len, entry, window, cx)
 8818                                            }),
 8819                                        )
 8820                                        .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
 8821                                        .flex_grow()
 8822                                        .into_any(),
 8823                                    )
 8824                                    .vertical_scrollbar_for(&current.list_state, window, cx)
 8825                                    .into_any()
 8826                                } else {
 8827                                    this.child(self.render_recent_history(cx)).into_any()
 8828                                }
 8829                            })
 8830                            .into_any_element()
 8831                    } else {
 8832                        div().into_any_element()
 8833                    }
 8834                }
 8835            })
 8836            // The activity bar is intentionally rendered outside of the ThreadState::Active match
 8837            // above so that the scrollbar doesn't render behind it. The current setup allows
 8838            // the scrollbar to stop exactly at the activity bar start.
 8839            .when(has_messages, |this| match self.as_active_thread() {
 8840                Some(thread) => this.children(self.render_activity_bar(&thread.thread, window, cx)),
 8841                _ => this,
 8842            })
 8843            .when(self.show_codex_windows_warning, |this| {
 8844                this.child(self.render_codex_windows_warning(cx))
 8845            })
 8846            .when_some(self.as_active_thread(), |this, thread_state| {
 8847                this.children(thread_state.render_thread_retry_status_callout())
 8848                    .children(thread_state.render_command_load_errors(cx))
 8849            })
 8850            .children(self.render_thread_error(window, cx))
 8851            .when_some(
 8852                match has_messages {
 8853                    true => None,
 8854                    false => self
 8855                        .as_active_thread()
 8856                        .and_then(|active| active.new_server_version_available.as_ref()),
 8857                },
 8858                |this, version| this.child(self.render_new_version_callout(version, cx)),
 8859            )
 8860            .children(self.render_token_limit_callout(cx))
 8861            .child(self.render_message_editor(window, cx))
 8862    }
 8863}
 8864
 8865fn plan_label_markdown_style(
 8866    status: &acp::PlanEntryStatus,
 8867    window: &Window,
 8868    cx: &App,
 8869) -> MarkdownStyle {
 8870    let default_md_style = MarkdownStyle::themed(MarkdownFont::Agent, window, cx);
 8871
 8872    MarkdownStyle {
 8873        base_text_style: TextStyle {
 8874            color: cx.theme().colors().text_muted,
 8875            strikethrough: if matches!(status, acp::PlanEntryStatus::Completed) {
 8876                Some(gpui::StrikethroughStyle {
 8877                    thickness: px(1.),
 8878                    color: Some(cx.theme().colors().text_muted.opacity(0.8)),
 8879                })
 8880            } else {
 8881                None
 8882            },
 8883            ..default_md_style.base_text_style
 8884        },
 8885        ..default_md_style
 8886    }
 8887}
 8888
 8889#[cfg(test)]
 8890pub(crate) mod tests {
 8891    use acp_thread::{
 8892        AgentSessionList, AgentSessionListRequest, AgentSessionListResponse, StubAgentConnection,
 8893    };
 8894    use action_log::ActionLog;
 8895    use agent::ToolPermissionContext;
 8896    use agent_client_protocol::SessionId;
 8897    use editor::MultiBufferOffset;
 8898    use fs::FakeFs;
 8899    use gpui::{EventEmitter, TestAppContext, VisualTestContext};
 8900    use project::Project;
 8901    use serde_json::json;
 8902    use settings::SettingsStore;
 8903    use std::any::Any;
 8904    use std::path::Path;
 8905    use std::rc::Rc;
 8906    use workspace::Item;
 8907
 8908    use super::*;
 8909
 8910    #[gpui::test]
 8911    async fn test_drop(cx: &mut TestAppContext) {
 8912        init_test(cx);
 8913
 8914        let (thread_view, _cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
 8915        let weak_view = thread_view.downgrade();
 8916        drop(thread_view);
 8917        assert!(!weak_view.is_upgradable());
 8918    }
 8919
 8920    #[gpui::test]
 8921    async fn test_notification_for_stop_event(cx: &mut TestAppContext) {
 8922        init_test(cx);
 8923
 8924        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
 8925
 8926        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
 8927        message_editor.update_in(cx, |editor, window, cx| {
 8928            editor.set_text("Hello", window, cx);
 8929        });
 8930
 8931        cx.deactivate_window();
 8932
 8933        thread_view.update_in(cx, |thread_view, window, cx| {
 8934            thread_view.send(window, cx);
 8935        });
 8936
 8937        cx.run_until_parked();
 8938
 8939        assert!(
 8940            cx.windows()
 8941                .iter()
 8942                .any(|window| window.downcast::<AgentNotification>().is_some())
 8943        );
 8944    }
 8945
 8946    #[gpui::test]
 8947    async fn test_notification_for_error(cx: &mut TestAppContext) {
 8948        init_test(cx);
 8949
 8950        let (thread_view, cx) =
 8951            setup_thread_view(StubAgentServer::new(SaboteurAgentConnection), cx).await;
 8952
 8953        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
 8954        message_editor.update_in(cx, |editor, window, cx| {
 8955            editor.set_text("Hello", window, cx);
 8956        });
 8957
 8958        cx.deactivate_window();
 8959
 8960        thread_view.update_in(cx, |thread_view, window, cx| {
 8961            thread_view.send(window, cx);
 8962        });
 8963
 8964        cx.run_until_parked();
 8965
 8966        assert!(
 8967            cx.windows()
 8968                .iter()
 8969                .any(|window| window.downcast::<AgentNotification>().is_some())
 8970        );
 8971    }
 8972
 8973    #[gpui::test]
 8974    async fn test_recent_history_refreshes_when_history_cache_updated(cx: &mut TestAppContext) {
 8975        init_test(cx);
 8976
 8977        let session_a = AgentSessionInfo::new(SessionId::new("session-a"));
 8978        let session_b = AgentSessionInfo::new(SessionId::new("session-b"));
 8979
 8980        let fs = FakeFs::new(cx.executor());
 8981        let project = Project::test(fs, [], cx).await;
 8982        let (workspace, cx) =
 8983            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 8984
 8985        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
 8986        // Create history without an initial session list - it will be set after connection
 8987        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
 8988
 8989        let thread_view = cx.update(|window, cx| {
 8990            cx.new(|cx| {
 8991                AcpServerView::new(
 8992                    Rc::new(StubAgentServer::default_response()),
 8993                    None,
 8994                    None,
 8995                    workspace.downgrade(),
 8996                    project,
 8997                    Some(thread_store),
 8998                    None,
 8999                    history.clone(),
 9000                    window,
 9001                    cx,
 9002                )
 9003            })
 9004        });
 9005
 9006        // Wait for connection to establish
 9007        cx.run_until_parked();
 9008
 9009        // Initially empty because StubAgentConnection.session_list() returns None
 9010        thread_view.read_with(cx, |view, _cx| {
 9011            assert_eq!(view.recent_history_entries.len(), 0);
 9012        });
 9013
 9014        // Now set the session list - this simulates external agents providing their history
 9015        let list_a: Rc<dyn AgentSessionList> =
 9016            Rc::new(StubSessionList::new(vec![session_a.clone()]));
 9017        history.update(cx, |history, cx| {
 9018            history.set_session_list(Some(list_a), cx);
 9019        });
 9020        cx.run_until_parked();
 9021
 9022        thread_view.read_with(cx, |view, _cx| {
 9023            assert_eq!(view.recent_history_entries.len(), 1);
 9024            assert_eq!(
 9025                view.recent_history_entries[0].session_id,
 9026                session_a.session_id
 9027            );
 9028        });
 9029
 9030        // Update to a different session list
 9031        let list_b: Rc<dyn AgentSessionList> =
 9032            Rc::new(StubSessionList::new(vec![session_b.clone()]));
 9033        history.update(cx, |history, cx| {
 9034            history.set_session_list(Some(list_b), cx);
 9035        });
 9036        cx.run_until_parked();
 9037
 9038        thread_view.read_with(cx, |view, _cx| {
 9039            assert_eq!(view.recent_history_entries.len(), 1);
 9040            assert_eq!(
 9041                view.recent_history_entries[0].session_id,
 9042                session_b.session_id
 9043            );
 9044        });
 9045    }
 9046
 9047    #[gpui::test]
 9048    async fn test_resume_without_history_adds_notice(cx: &mut TestAppContext) {
 9049        init_test(cx);
 9050
 9051        let session = AgentSessionInfo::new(SessionId::new("resume-session"));
 9052        let fs = FakeFs::new(cx.executor());
 9053        let project = Project::test(fs, [], cx).await;
 9054        let (workspace, cx) =
 9055            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 9056
 9057        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
 9058        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
 9059
 9060        let thread_view = cx.update(|window, cx| {
 9061            cx.new(|cx| {
 9062                AcpServerView::new(
 9063                    Rc::new(StubAgentServer::new(ResumeOnlyAgentConnection)),
 9064                    Some(session),
 9065                    None,
 9066                    workspace.downgrade(),
 9067                    project,
 9068                    Some(thread_store),
 9069                    None,
 9070                    history,
 9071                    window,
 9072                    cx,
 9073                )
 9074            })
 9075        });
 9076
 9077        cx.run_until_parked();
 9078
 9079        thread_view.read_with(cx, |view, _cx| {
 9080            let state = view.as_active_thread().unwrap();
 9081            assert!(state.resumed_without_history);
 9082            assert_eq!(state.list_state.item_count(), 0);
 9083        });
 9084    }
 9085
 9086    #[gpui::test]
 9087    async fn test_refusal_handling(cx: &mut TestAppContext) {
 9088        init_test(cx);
 9089
 9090        let (thread_view, cx) =
 9091            setup_thread_view(StubAgentServer::new(RefusalAgentConnection), cx).await;
 9092
 9093        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
 9094        message_editor.update_in(cx, |editor, window, cx| {
 9095            editor.set_text("Do something harmful", window, cx);
 9096        });
 9097
 9098        thread_view.update_in(cx, |thread_view, window, cx| {
 9099            thread_view.send(window, cx);
 9100        });
 9101
 9102        cx.run_until_parked();
 9103
 9104        // Check that the refusal error is set
 9105        thread_view.read_with(cx, |thread_view, _cx| {
 9106            let state = thread_view.as_active_thread().unwrap();
 9107            assert!(
 9108                matches!(state.thread_error, Some(ThreadError::Refusal)),
 9109                "Expected refusal error to be set"
 9110            );
 9111        });
 9112    }
 9113
 9114    #[gpui::test]
 9115    async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) {
 9116        init_test(cx);
 9117
 9118        let tool_call_id = acp::ToolCallId::new("1");
 9119        let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Label")
 9120            .kind(acp::ToolKind::Edit)
 9121            .content(vec!["hi".into()]);
 9122        let connection =
 9123            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
 9124                tool_call_id,
 9125                PermissionOptions::Flat(vec![acp::PermissionOption::new(
 9126                    "1",
 9127                    "Allow",
 9128                    acp::PermissionOptionKind::AllowOnce,
 9129                )]),
 9130            )]));
 9131
 9132        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
 9133
 9134        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
 9135
 9136        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
 9137        message_editor.update_in(cx, |editor, window, cx| {
 9138            editor.set_text("Hello", window, cx);
 9139        });
 9140
 9141        cx.deactivate_window();
 9142
 9143        thread_view.update_in(cx, |thread_view, window, cx| {
 9144            thread_view.send(window, cx);
 9145        });
 9146
 9147        cx.run_until_parked();
 9148
 9149        assert!(
 9150            cx.windows()
 9151                .iter()
 9152                .any(|window| window.downcast::<AgentNotification>().is_some())
 9153        );
 9154    }
 9155
 9156    #[gpui::test]
 9157    async fn test_notification_when_panel_hidden(cx: &mut TestAppContext) {
 9158        init_test(cx);
 9159
 9160        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
 9161
 9162        add_to_workspace(thread_view.clone(), cx);
 9163
 9164        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
 9165
 9166        message_editor.update_in(cx, |editor, window, cx| {
 9167            editor.set_text("Hello", window, cx);
 9168        });
 9169
 9170        // Window is active (don't deactivate), but panel will be hidden
 9171        // Note: In the test environment, the panel is not actually added to the dock,
 9172        // so is_agent_panel_hidden will return true
 9173
 9174        thread_view.update_in(cx, |thread_view, window, cx| {
 9175            thread_view.send(window, cx);
 9176        });
 9177
 9178        cx.run_until_parked();
 9179
 9180        // Should show notification because window is active but panel is hidden
 9181        assert!(
 9182            cx.windows()
 9183                .iter()
 9184                .any(|window| window.downcast::<AgentNotification>().is_some()),
 9185            "Expected notification when panel is hidden"
 9186        );
 9187    }
 9188
 9189    #[gpui::test]
 9190    async fn test_notification_still_works_when_window_inactive(cx: &mut TestAppContext) {
 9191        init_test(cx);
 9192
 9193        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
 9194
 9195        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
 9196        message_editor.update_in(cx, |editor, window, cx| {
 9197            editor.set_text("Hello", window, cx);
 9198        });
 9199
 9200        // Deactivate window - should show notification regardless of setting
 9201        cx.deactivate_window();
 9202
 9203        thread_view.update_in(cx, |thread_view, window, cx| {
 9204            thread_view.send(window, cx);
 9205        });
 9206
 9207        cx.run_until_parked();
 9208
 9209        // Should still show notification when window is inactive (existing behavior)
 9210        assert!(
 9211            cx.windows()
 9212                .iter()
 9213                .any(|window| window.downcast::<AgentNotification>().is_some()),
 9214            "Expected notification when window is inactive"
 9215        );
 9216    }
 9217
 9218    #[gpui::test]
 9219    async fn test_notification_respects_never_setting(cx: &mut TestAppContext) {
 9220        init_test(cx);
 9221
 9222        // Set notify_when_agent_waiting to Never
 9223        cx.update(|cx| {
 9224            AgentSettings::override_global(
 9225                AgentSettings {
 9226                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
 9227                    ..AgentSettings::get_global(cx).clone()
 9228                },
 9229                cx,
 9230            );
 9231        });
 9232
 9233        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
 9234
 9235        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
 9236        message_editor.update_in(cx, |editor, window, cx| {
 9237            editor.set_text("Hello", window, cx);
 9238        });
 9239
 9240        // Window is active
 9241
 9242        thread_view.update_in(cx, |thread_view, window, cx| {
 9243            thread_view.send(window, cx);
 9244        });
 9245
 9246        cx.run_until_parked();
 9247
 9248        // Should NOT show notification because notify_when_agent_waiting is Never
 9249        assert!(
 9250            !cx.windows()
 9251                .iter()
 9252                .any(|window| window.downcast::<AgentNotification>().is_some()),
 9253            "Expected no notification when notify_when_agent_waiting is Never"
 9254        );
 9255    }
 9256
 9257    #[gpui::test]
 9258    async fn test_notification_closed_when_thread_view_dropped(cx: &mut TestAppContext) {
 9259        init_test(cx);
 9260
 9261        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
 9262
 9263        let weak_view = thread_view.downgrade();
 9264
 9265        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
 9266        message_editor.update_in(cx, |editor, window, cx| {
 9267            editor.set_text("Hello", window, cx);
 9268        });
 9269
 9270        cx.deactivate_window();
 9271
 9272        thread_view.update_in(cx, |thread_view, window, cx| {
 9273            thread_view.send(window, cx);
 9274        });
 9275
 9276        cx.run_until_parked();
 9277
 9278        // Verify notification is shown
 9279        assert!(
 9280            cx.windows()
 9281                .iter()
 9282                .any(|window| window.downcast::<AgentNotification>().is_some()),
 9283            "Expected notification to be shown"
 9284        );
 9285
 9286        // Drop the thread view (simulating navigation to a new thread)
 9287        drop(thread_view);
 9288        drop(message_editor);
 9289        // Trigger an update to flush effects, which will call release_dropped_entities
 9290        cx.update(|_window, _cx| {});
 9291        cx.run_until_parked();
 9292
 9293        // Verify the entity was actually released
 9294        assert!(
 9295            !weak_view.is_upgradable(),
 9296            "Thread view entity should be released after dropping"
 9297        );
 9298
 9299        // The notification should be automatically closed via on_release
 9300        assert!(
 9301            !cx.windows()
 9302                .iter()
 9303                .any(|window| window.downcast::<AgentNotification>().is_some()),
 9304            "Notification should be closed when thread view is dropped"
 9305        );
 9306    }
 9307
 9308    async fn setup_thread_view(
 9309        agent: impl AgentServer + 'static,
 9310        cx: &mut TestAppContext,
 9311    ) -> (Entity<AcpServerView>, &mut VisualTestContext) {
 9312        let fs = FakeFs::new(cx.executor());
 9313        let project = Project::test(fs, [], cx).await;
 9314        let (workspace, cx) =
 9315            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 9316
 9317        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
 9318        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
 9319
 9320        let thread_view = cx.update(|window, cx| {
 9321            cx.new(|cx| {
 9322                AcpServerView::new(
 9323                    Rc::new(agent),
 9324                    None,
 9325                    None,
 9326                    workspace.downgrade(),
 9327                    project,
 9328                    Some(thread_store),
 9329                    None,
 9330                    history,
 9331                    window,
 9332                    cx,
 9333                )
 9334            })
 9335        });
 9336        cx.run_until_parked();
 9337        (thread_view, cx)
 9338    }
 9339
 9340    fn add_to_workspace(thread_view: Entity<AcpServerView>, cx: &mut VisualTestContext) {
 9341        let workspace = thread_view.read_with(cx, |thread_view, _cx| thread_view.workspace.clone());
 9342
 9343        workspace
 9344            .update_in(cx, |workspace, window, cx| {
 9345                workspace.add_item_to_active_pane(
 9346                    Box::new(cx.new(|_| ThreadViewItem(thread_view.clone()))),
 9347                    None,
 9348                    true,
 9349                    window,
 9350                    cx,
 9351                );
 9352            })
 9353            .unwrap();
 9354    }
 9355
 9356    struct ThreadViewItem(Entity<AcpServerView>);
 9357
 9358    impl Item for ThreadViewItem {
 9359        type Event = ();
 9360
 9361        fn include_in_nav_history() -> bool {
 9362            false
 9363        }
 9364
 9365        fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
 9366            "Test".into()
 9367        }
 9368    }
 9369
 9370    impl EventEmitter<()> for ThreadViewItem {}
 9371
 9372    impl Focusable for ThreadViewItem {
 9373        fn focus_handle(&self, cx: &App) -> FocusHandle {
 9374            self.0.read(cx).focus_handle(cx)
 9375        }
 9376    }
 9377
 9378    impl Render for ThreadViewItem {
 9379        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
 9380            self.0.clone().into_any_element()
 9381        }
 9382    }
 9383
 9384    struct StubAgentServer<C> {
 9385        connection: C,
 9386    }
 9387
 9388    impl<C> StubAgentServer<C> {
 9389        fn new(connection: C) -> Self {
 9390            Self { connection }
 9391        }
 9392    }
 9393
 9394    impl StubAgentServer<StubAgentConnection> {
 9395        fn default_response() -> Self {
 9396            let conn = StubAgentConnection::new();
 9397            conn.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
 9398                acp::ContentChunk::new("Default response".into()),
 9399            )]);
 9400            Self::new(conn)
 9401        }
 9402    }
 9403
 9404    #[derive(Clone)]
 9405    struct StubSessionList {
 9406        sessions: Vec<AgentSessionInfo>,
 9407    }
 9408
 9409    impl StubSessionList {
 9410        fn new(sessions: Vec<AgentSessionInfo>) -> Self {
 9411            Self { sessions }
 9412        }
 9413    }
 9414
 9415    impl AgentSessionList for StubSessionList {
 9416        fn list_sessions(
 9417            &self,
 9418            _request: AgentSessionListRequest,
 9419            _cx: &mut App,
 9420        ) -> Task<anyhow::Result<AgentSessionListResponse>> {
 9421            Task::ready(Ok(AgentSessionListResponse::new(self.sessions.clone())))
 9422        }
 9423        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
 9424            self
 9425        }
 9426    }
 9427
 9428    #[derive(Clone)]
 9429    struct ResumeOnlyAgentConnection;
 9430
 9431    impl AgentConnection for ResumeOnlyAgentConnection {
 9432        fn telemetry_id(&self) -> SharedString {
 9433            "resume-only".into()
 9434        }
 9435
 9436        fn new_thread(
 9437            self: Rc<Self>,
 9438            project: Entity<Project>,
 9439            _cwd: &Path,
 9440            cx: &mut gpui::App,
 9441        ) -> Task<gpui::Result<Entity<AcpThread>>> {
 9442            let action_log = cx.new(|_| ActionLog::new(project.clone()));
 9443            let thread = cx.new(|cx| {
 9444                AcpThread::new(
 9445                    "ResumeOnlyAgentConnection",
 9446                    self.clone(),
 9447                    project,
 9448                    action_log,
 9449                    SessionId::new("new-session"),
 9450                    watch::Receiver::constant(
 9451                        acp::PromptCapabilities::new()
 9452                            .image(true)
 9453                            .audio(true)
 9454                            .embedded_context(true),
 9455                    ),
 9456                    cx,
 9457                )
 9458            });
 9459            Task::ready(Ok(thread))
 9460        }
 9461
 9462        fn supports_resume_session(&self, _cx: &App) -> bool {
 9463            true
 9464        }
 9465
 9466        fn resume_session(
 9467            self: Rc<Self>,
 9468            session: AgentSessionInfo,
 9469            project: Entity<Project>,
 9470            _cwd: &Path,
 9471            cx: &mut App,
 9472        ) -> Task<gpui::Result<Entity<AcpThread>>> {
 9473            let action_log = cx.new(|_| ActionLog::new(project.clone()));
 9474            let thread = cx.new(|cx| {
 9475                AcpThread::new(
 9476                    "ResumeOnlyAgentConnection",
 9477                    self.clone(),
 9478                    project,
 9479                    action_log,
 9480                    session.session_id,
 9481                    watch::Receiver::constant(
 9482                        acp::PromptCapabilities::new()
 9483                            .image(true)
 9484                            .audio(true)
 9485                            .embedded_context(true),
 9486                    ),
 9487                    cx,
 9488                )
 9489            });
 9490            Task::ready(Ok(thread))
 9491        }
 9492
 9493        fn auth_methods(&self) -> &[acp::AuthMethod] {
 9494            &[]
 9495        }
 9496
 9497        fn authenticate(
 9498            &self,
 9499            _method_id: acp::AuthMethodId,
 9500            _cx: &mut App,
 9501        ) -> Task<gpui::Result<()>> {
 9502            Task::ready(Ok(()))
 9503        }
 9504
 9505        fn prompt(
 9506            &self,
 9507            _id: Option<acp_thread::UserMessageId>,
 9508            _params: acp::PromptRequest,
 9509            _cx: &mut App,
 9510        ) -> Task<gpui::Result<acp::PromptResponse>> {
 9511            Task::ready(Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)))
 9512        }
 9513
 9514        fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {}
 9515
 9516        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
 9517            self
 9518        }
 9519    }
 9520
 9521    impl<C> AgentServer for StubAgentServer<C>
 9522    where
 9523        C: 'static + AgentConnection + Send + Clone,
 9524    {
 9525        fn logo(&self) -> ui::IconName {
 9526            ui::IconName::Ai
 9527        }
 9528
 9529        fn name(&self) -> SharedString {
 9530            "Test".into()
 9531        }
 9532
 9533        fn connect(
 9534            &self,
 9535            _root_dir: Option<&Path>,
 9536            _delegate: AgentServerDelegate,
 9537            _cx: &mut App,
 9538        ) -> Task<gpui::Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
 9539            Task::ready(Ok((Rc::new(self.connection.clone()), None)))
 9540        }
 9541
 9542        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
 9543            self
 9544        }
 9545    }
 9546
 9547    #[derive(Clone)]
 9548    struct SaboteurAgentConnection;
 9549
 9550    impl AgentConnection for SaboteurAgentConnection {
 9551        fn telemetry_id(&self) -> SharedString {
 9552            "saboteur".into()
 9553        }
 9554
 9555        fn new_thread(
 9556            self: Rc<Self>,
 9557            project: Entity<Project>,
 9558            _cwd: &Path,
 9559            cx: &mut gpui::App,
 9560        ) -> Task<gpui::Result<Entity<AcpThread>>> {
 9561            Task::ready(Ok(cx.new(|cx| {
 9562                let action_log = cx.new(|_| ActionLog::new(project.clone()));
 9563                AcpThread::new(
 9564                    "SaboteurAgentConnection",
 9565                    self,
 9566                    project,
 9567                    action_log,
 9568                    SessionId::new("test"),
 9569                    watch::Receiver::constant(
 9570                        acp::PromptCapabilities::new()
 9571                            .image(true)
 9572                            .audio(true)
 9573                            .embedded_context(true),
 9574                    ),
 9575                    cx,
 9576                )
 9577            })))
 9578        }
 9579
 9580        fn auth_methods(&self) -> &[acp::AuthMethod] {
 9581            &[]
 9582        }
 9583
 9584        fn authenticate(
 9585            &self,
 9586            _method_id: acp::AuthMethodId,
 9587            _cx: &mut App,
 9588        ) -> Task<gpui::Result<()>> {
 9589            unimplemented!()
 9590        }
 9591
 9592        fn prompt(
 9593            &self,
 9594            _id: Option<acp_thread::UserMessageId>,
 9595            _params: acp::PromptRequest,
 9596            _cx: &mut App,
 9597        ) -> Task<gpui::Result<acp::PromptResponse>> {
 9598            Task::ready(Err(anyhow::anyhow!("Error prompting")))
 9599        }
 9600
 9601        fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
 9602            unimplemented!()
 9603        }
 9604
 9605        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
 9606            self
 9607        }
 9608    }
 9609
 9610    /// Simulates a model which always returns a refusal response
 9611    #[derive(Clone)]
 9612    struct RefusalAgentConnection;
 9613
 9614    impl AgentConnection for RefusalAgentConnection {
 9615        fn telemetry_id(&self) -> SharedString {
 9616            "refusal".into()
 9617        }
 9618
 9619        fn new_thread(
 9620            self: Rc<Self>,
 9621            project: Entity<Project>,
 9622            _cwd: &Path,
 9623            cx: &mut gpui::App,
 9624        ) -> Task<gpui::Result<Entity<AcpThread>>> {
 9625            Task::ready(Ok(cx.new(|cx| {
 9626                let action_log = cx.new(|_| ActionLog::new(project.clone()));
 9627                AcpThread::new(
 9628                    "RefusalAgentConnection",
 9629                    self,
 9630                    project,
 9631                    action_log,
 9632                    SessionId::new("test"),
 9633                    watch::Receiver::constant(
 9634                        acp::PromptCapabilities::new()
 9635                            .image(true)
 9636                            .audio(true)
 9637                            .embedded_context(true),
 9638                    ),
 9639                    cx,
 9640                )
 9641            })))
 9642        }
 9643
 9644        fn auth_methods(&self) -> &[acp::AuthMethod] {
 9645            &[]
 9646        }
 9647
 9648        fn authenticate(
 9649            &self,
 9650            _method_id: acp::AuthMethodId,
 9651            _cx: &mut App,
 9652        ) -> Task<gpui::Result<()>> {
 9653            unimplemented!()
 9654        }
 9655
 9656        fn prompt(
 9657            &self,
 9658            _id: Option<acp_thread::UserMessageId>,
 9659            _params: acp::PromptRequest,
 9660            _cx: &mut App,
 9661        ) -> Task<gpui::Result<acp::PromptResponse>> {
 9662            Task::ready(Ok(acp::PromptResponse::new(acp::StopReason::Refusal)))
 9663        }
 9664
 9665        fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
 9666            unimplemented!()
 9667        }
 9668
 9669        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
 9670            self
 9671        }
 9672    }
 9673
 9674    pub(crate) fn init_test(cx: &mut TestAppContext) {
 9675        cx.update(|cx| {
 9676            let settings_store = SettingsStore::test(cx);
 9677            cx.set_global(settings_store);
 9678            theme::init(theme::LoadThemes::JustBase, cx);
 9679            editor::init(cx);
 9680            release_channel::init(semver::Version::new(0, 0, 0), cx);
 9681            prompt_store::init(cx)
 9682        });
 9683    }
 9684
 9685    #[gpui::test]
 9686    async fn test_rewind_views(cx: &mut TestAppContext) {
 9687        init_test(cx);
 9688
 9689        let fs = FakeFs::new(cx.executor());
 9690        fs.insert_tree(
 9691            "/project",
 9692            json!({
 9693                "test1.txt": "old content 1",
 9694                "test2.txt": "old content 2"
 9695            }),
 9696        )
 9697        .await;
 9698        let project = Project::test(fs, [Path::new("/project")], cx).await;
 9699        let (workspace, cx) =
 9700            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 9701
 9702        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
 9703        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
 9704
 9705        let connection = Rc::new(StubAgentConnection::new());
 9706        let thread_view = cx.update(|window, cx| {
 9707            cx.new(|cx| {
 9708                AcpServerView::new(
 9709                    Rc::new(StubAgentServer::new(connection.as_ref().clone())),
 9710                    None,
 9711                    None,
 9712                    workspace.downgrade(),
 9713                    project.clone(),
 9714                    Some(thread_store.clone()),
 9715                    None,
 9716                    history,
 9717                    window,
 9718                    cx,
 9719                )
 9720            })
 9721        });
 9722
 9723        cx.run_until_parked();
 9724
 9725        let thread = thread_view
 9726            .read_with(cx, |view, _| {
 9727                view.as_active_thread().map(|r| r.thread.clone())
 9728            })
 9729            .unwrap();
 9730
 9731        // First user message
 9732        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(
 9733            acp::ToolCall::new("tool1", "Edit file 1")
 9734                .kind(acp::ToolKind::Edit)
 9735                .status(acp::ToolCallStatus::Completed)
 9736                .content(vec![acp::ToolCallContent::Diff(
 9737                    acp::Diff::new("/project/test1.txt", "new content 1").old_text("old content 1"),
 9738                )]),
 9739        )]);
 9740
 9741        thread
 9742            .update(cx, |thread, cx| thread.send_raw("Give me a diff", cx))
 9743            .await
 9744            .unwrap();
 9745        cx.run_until_parked();
 9746
 9747        thread.read_with(cx, |thread, _| {
 9748            assert_eq!(thread.entries().len(), 2);
 9749        });
 9750
 9751        thread_view.read_with(cx, |view, cx| {
 9752            let entry_view_state = view
 9753                .as_active_thread()
 9754                .map(|active| &active.entry_view_state)
 9755                .unwrap();
 9756            entry_view_state.read_with(cx, |entry_view_state, _| {
 9757                assert!(
 9758                    entry_view_state
 9759                        .entry(0)
 9760                        .unwrap()
 9761                        .message_editor()
 9762                        .is_some()
 9763                );
 9764                assert!(entry_view_state.entry(1).unwrap().has_content());
 9765            });
 9766        });
 9767
 9768        // Second user message
 9769        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(
 9770            acp::ToolCall::new("tool2", "Edit file 2")
 9771                .kind(acp::ToolKind::Edit)
 9772                .status(acp::ToolCallStatus::Completed)
 9773                .content(vec![acp::ToolCallContent::Diff(
 9774                    acp::Diff::new("/project/test2.txt", "new content 2").old_text("old content 2"),
 9775                )]),
 9776        )]);
 9777
 9778        thread
 9779            .update(cx, |thread, cx| thread.send_raw("Another one", cx))
 9780            .await
 9781            .unwrap();
 9782        cx.run_until_parked();
 9783
 9784        let second_user_message_id = thread.read_with(cx, |thread, _| {
 9785            assert_eq!(thread.entries().len(), 4);
 9786            let AgentThreadEntry::UserMessage(user_message) = &thread.entries()[2] else {
 9787                panic!();
 9788            };
 9789            user_message.id.clone().unwrap()
 9790        });
 9791
 9792        thread_view.read_with(cx, |view, cx| {
 9793            let entry_view_state = &view.as_active_thread().unwrap().entry_view_state;
 9794            entry_view_state.read_with(cx, |entry_view_state, _| {
 9795                assert!(
 9796                    entry_view_state
 9797                        .entry(0)
 9798                        .unwrap()
 9799                        .message_editor()
 9800                        .is_some()
 9801                );
 9802                assert!(entry_view_state.entry(1).unwrap().has_content());
 9803                assert!(
 9804                    entry_view_state
 9805                        .entry(2)
 9806                        .unwrap()
 9807                        .message_editor()
 9808                        .is_some()
 9809                );
 9810                assert!(entry_view_state.entry(3).unwrap().has_content());
 9811            });
 9812        });
 9813
 9814        // Rewind to first message
 9815        thread
 9816            .update(cx, |thread, cx| thread.rewind(second_user_message_id, cx))
 9817            .await
 9818            .unwrap();
 9819
 9820        cx.run_until_parked();
 9821
 9822        thread.read_with(cx, |thread, _| {
 9823            assert_eq!(thread.entries().len(), 2);
 9824        });
 9825
 9826        thread_view.read_with(cx, |view, cx| {
 9827            let active = view.as_active_thread().unwrap();
 9828            active
 9829                .entry_view_state
 9830                .read_with(cx, |entry_view_state, _| {
 9831                    assert!(
 9832                        entry_view_state
 9833                            .entry(0)
 9834                            .unwrap()
 9835                            .message_editor()
 9836                            .is_some()
 9837                    );
 9838                    assert!(entry_view_state.entry(1).unwrap().has_content());
 9839
 9840                    // Old views should be dropped
 9841                    assert!(entry_view_state.entry(2).is_none());
 9842                    assert!(entry_view_state.entry(3).is_none());
 9843                });
 9844        });
 9845    }
 9846
 9847    #[gpui::test]
 9848    async fn test_scroll_to_most_recent_user_prompt(cx: &mut TestAppContext) {
 9849        init_test(cx);
 9850
 9851        let connection = StubAgentConnection::new();
 9852
 9853        // Each user prompt will result in a user message entry plus an agent message entry.
 9854        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
 9855            acp::ContentChunk::new("Response 1".into()),
 9856        )]);
 9857
 9858        let (thread_view, cx) =
 9859            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
 9860
 9861        let thread = thread_view
 9862            .read_with(cx, |view, _| {
 9863                view.as_active_thread().map(|r| r.thread.clone())
 9864            })
 9865            .unwrap();
 9866
 9867        thread
 9868            .update(cx, |thread, cx| thread.send_raw("Prompt 1", cx))
 9869            .await
 9870            .unwrap();
 9871        cx.run_until_parked();
 9872
 9873        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
 9874            acp::ContentChunk::new("Response 2".into()),
 9875        )]);
 9876
 9877        thread
 9878            .update(cx, |thread, cx| thread.send_raw("Prompt 2", cx))
 9879            .await
 9880            .unwrap();
 9881        cx.run_until_parked();
 9882
 9883        // Move somewhere else first so we're not trivially already on the last user prompt.
 9884        thread_view.update(cx, |view, cx| {
 9885            view.scroll_to_top(cx);
 9886        });
 9887        cx.run_until_parked();
 9888
 9889        thread_view.update(cx, |view, cx| {
 9890            view.scroll_to_most_recent_user_prompt(cx);
 9891            let scroll_top = view
 9892                .as_active_thread()
 9893                .map(|active| &active.list_state)
 9894                .unwrap()
 9895                .logical_scroll_top();
 9896            // Entries layout is: [User1, Assistant1, User2, Assistant2]
 9897            assert_eq!(scroll_top.item_ix, 2);
 9898        });
 9899    }
 9900
 9901    #[gpui::test]
 9902    async fn test_scroll_to_most_recent_user_prompt_falls_back_to_bottom_without_user_messages(
 9903        cx: &mut TestAppContext,
 9904    ) {
 9905        init_test(cx);
 9906
 9907        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
 9908
 9909        // With no entries, scrolling should be a no-op and must not panic.
 9910        thread_view.update(cx, |view, cx| {
 9911            view.scroll_to_most_recent_user_prompt(cx);
 9912            let scroll_top = view
 9913                .as_active_thread()
 9914                .map(|active| &active.list_state)
 9915                .unwrap()
 9916                .logical_scroll_top();
 9917            assert_eq!(scroll_top.item_ix, 0);
 9918        });
 9919    }
 9920
 9921    #[gpui::test]
 9922    async fn test_message_editing_cancel(cx: &mut TestAppContext) {
 9923        init_test(cx);
 9924
 9925        let connection = StubAgentConnection::new();
 9926
 9927        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
 9928            acp::ContentChunk::new("Response".into()),
 9929        )]);
 9930
 9931        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
 9932        add_to_workspace(thread_view.clone(), cx);
 9933
 9934        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
 9935        message_editor.update_in(cx, |editor, window, cx| {
 9936            editor.set_text("Original message to edit", window, cx);
 9937        });
 9938        thread_view.update_in(cx, |thread_view, window, cx| {
 9939            thread_view.send(window, cx);
 9940        });
 9941
 9942        cx.run_until_parked();
 9943
 9944        let user_message_editor = thread_view.read_with(cx, |view, cx| {
 9945            assert_eq!(
 9946                view.as_active_thread()
 9947                    .and_then(|active| active.editing_message),
 9948                None
 9949            );
 9950
 9951            view.as_active_thread()
 9952                .map(|active| &active.entry_view_state)
 9953                .as_ref()
 9954                .unwrap()
 9955                .read(cx)
 9956                .entry(0)
 9957                .unwrap()
 9958                .message_editor()
 9959                .unwrap()
 9960                .clone()
 9961        });
 9962
 9963        // Focus
 9964        cx.focus(&user_message_editor);
 9965        thread_view.read_with(cx, |view, _cx| {
 9966            assert_eq!(
 9967                view.as_active_thread()
 9968                    .and_then(|active| active.editing_message),
 9969                Some(0)
 9970            );
 9971        });
 9972
 9973        // Edit
 9974        user_message_editor.update_in(cx, |editor, window, cx| {
 9975            editor.set_text("Edited message content", window, cx);
 9976        });
 9977
 9978        // Cancel
 9979        user_message_editor.update_in(cx, |_editor, window, cx| {
 9980            window.dispatch_action(Box::new(editor::actions::Cancel), cx);
 9981        });
 9982
 9983        thread_view.read_with(cx, |view, _cx| {
 9984            assert_eq!(
 9985                view.as_active_thread()
 9986                    .and_then(|active| active.editing_message),
 9987                None
 9988            );
 9989        });
 9990
 9991        user_message_editor.read_with(cx, |editor, cx| {
 9992            assert_eq!(editor.text(cx), "Original message to edit");
 9993        });
 9994    }
 9995
 9996    #[gpui::test]
 9997    async fn test_message_doesnt_send_if_empty(cx: &mut TestAppContext) {
 9998        init_test(cx);
 9999
10000        let connection = StubAgentConnection::new();
10001
10002        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
10003        add_to_workspace(thread_view.clone(), cx);
10004
10005        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
10006        message_editor.update_in(cx, |editor, window, cx| {
10007            editor.set_text("", window, cx);
10008        });
10009
10010        let thread = cx.read(|cx| {
10011            thread_view
10012                .read(cx)
10013                .as_active_thread()
10014                .unwrap()
10015                .thread
10016                .clone()
10017        });
10018        let entries_before = cx.read(|cx| thread.read(cx).entries().len());
10019
10020        thread_view.update_in(cx, |view, window, cx| {
10021            view.send(window, cx);
10022        });
10023        cx.run_until_parked();
10024
10025        let entries_after = cx.read(|cx| thread.read(cx).entries().len());
10026        assert_eq!(
10027            entries_before, entries_after,
10028            "No message should be sent when editor is empty"
10029        );
10030    }
10031
10032    #[gpui::test]
10033    async fn test_message_editing_regenerate(cx: &mut TestAppContext) {
10034        init_test(cx);
10035
10036        let connection = StubAgentConnection::new();
10037
10038        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
10039            acp::ContentChunk::new("Response".into()),
10040        )]);
10041
10042        let (thread_view, cx) =
10043            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
10044        add_to_workspace(thread_view.clone(), cx);
10045
10046        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
10047        message_editor.update_in(cx, |editor, window, cx| {
10048            editor.set_text("Original message to edit", window, cx);
10049        });
10050        thread_view.update_in(cx, |thread_view, window, cx| {
10051            thread_view.send(window, cx);
10052        });
10053
10054        cx.run_until_parked();
10055
10056        let user_message_editor = thread_view.read_with(cx, |view, cx| {
10057            assert_eq!(
10058                view.as_active_thread()
10059                    .and_then(|active| active.editing_message),
10060                None
10061            );
10062            assert_eq!(
10063                view.as_active_thread()
10064                    .unwrap()
10065                    .thread
10066                    .read(cx)
10067                    .entries()
10068                    .len(),
10069                2
10070            );
10071
10072            view.as_active_thread()
10073                .map(|active| &active.entry_view_state)
10074                .as_ref()
10075                .unwrap()
10076                .read(cx)
10077                .entry(0)
10078                .unwrap()
10079                .message_editor()
10080                .unwrap()
10081                .clone()
10082        });
10083
10084        // Focus
10085        cx.focus(&user_message_editor);
10086
10087        // Edit
10088        user_message_editor.update_in(cx, |editor, window, cx| {
10089            editor.set_text("Edited message content", window, cx);
10090        });
10091
10092        // Send
10093        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
10094            acp::ContentChunk::new("New Response".into()),
10095        )]);
10096
10097        user_message_editor.update_in(cx, |_editor, window, cx| {
10098            window.dispatch_action(Box::new(Chat), cx);
10099        });
10100
10101        cx.run_until_parked();
10102
10103        thread_view.read_with(cx, |view, cx| {
10104            assert_eq!(
10105                view.as_active_thread()
10106                    .and_then(|active| active.editing_message),
10107                None
10108            );
10109
10110            let entries = view.as_active_thread().unwrap().thread.read(cx).entries();
10111            assert_eq!(entries.len(), 2);
10112            assert_eq!(
10113                entries[0].to_markdown(cx),
10114                "## User\n\nEdited message content\n\n"
10115            );
10116            assert_eq!(
10117                entries[1].to_markdown(cx),
10118                "## Assistant\n\nNew Response\n\n"
10119            );
10120
10121            let entry_view_state = view
10122                .as_active_thread()
10123                .map(|active| &active.entry_view_state)
10124                .unwrap();
10125            let new_editor = entry_view_state.read_with(cx, |state, _cx| {
10126                assert!(!state.entry(1).unwrap().has_content());
10127                state.entry(0).unwrap().message_editor().unwrap().clone()
10128            });
10129
10130            assert_eq!(new_editor.read(cx).text(cx), "Edited message content");
10131        })
10132    }
10133
10134    #[gpui::test]
10135    async fn test_message_editing_while_generating(cx: &mut TestAppContext) {
10136        init_test(cx);
10137
10138        let connection = StubAgentConnection::new();
10139
10140        let (thread_view, cx) =
10141            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
10142        add_to_workspace(thread_view.clone(), cx);
10143
10144        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
10145        message_editor.update_in(cx, |editor, window, cx| {
10146            editor.set_text("Original message to edit", window, cx);
10147        });
10148        thread_view.update_in(cx, |thread_view, window, cx| {
10149            thread_view.send(window, cx);
10150        });
10151
10152        cx.run_until_parked();
10153
10154        let (user_message_editor, session_id) = thread_view.read_with(cx, |view, cx| {
10155            let thread = view.as_active_thread().unwrap().thread.read(cx);
10156            assert_eq!(thread.entries().len(), 1);
10157
10158            let editor = view
10159                .as_active_thread()
10160                .map(|active| &active.entry_view_state)
10161                .as_ref()
10162                .unwrap()
10163                .read(cx)
10164                .entry(0)
10165                .unwrap()
10166                .message_editor()
10167                .unwrap()
10168                .clone();
10169
10170            (editor, thread.session_id().clone())
10171        });
10172
10173        // Focus
10174        cx.focus(&user_message_editor);
10175
10176        thread_view.read_with(cx, |view, _cx| {
10177            assert_eq!(
10178                view.as_active_thread()
10179                    .and_then(|active| active.editing_message),
10180                Some(0)
10181            );
10182        });
10183
10184        // Edit
10185        user_message_editor.update_in(cx, |editor, window, cx| {
10186            editor.set_text("Edited message content", window, cx);
10187        });
10188
10189        thread_view.read_with(cx, |view, _cx| {
10190            assert_eq!(
10191                view.as_active_thread()
10192                    .and_then(|active| active.editing_message),
10193                Some(0)
10194            );
10195        });
10196
10197        // Finish streaming response
10198        cx.update(|_, cx| {
10199            connection.send_update(
10200                session_id.clone(),
10201                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("Response".into())),
10202                cx,
10203            );
10204            connection.end_turn(session_id, acp::StopReason::EndTurn);
10205        });
10206
10207        thread_view.read_with(cx, |view, _cx| {
10208            assert_eq!(
10209                view.as_active_thread()
10210                    .and_then(|active| active.editing_message),
10211                Some(0)
10212            );
10213        });
10214
10215        cx.run_until_parked();
10216
10217        // Should still be editing
10218        cx.update(|window, cx| {
10219            assert!(user_message_editor.focus_handle(cx).is_focused(window));
10220            assert_eq!(
10221                thread_view
10222                    .read(cx)
10223                    .as_active_thread()
10224                    .and_then(|active| active.editing_message),
10225                Some(0)
10226            );
10227            assert_eq!(
10228                user_message_editor.read(cx).text(cx),
10229                "Edited message content"
10230            );
10231        });
10232    }
10233
10234    struct GeneratingThreadSetup {
10235        thread_view: Entity<AcpServerView>,
10236        thread: Entity<AcpThread>,
10237        message_editor: Entity<MessageEditor>,
10238    }
10239
10240    async fn setup_generating_thread(
10241        cx: &mut TestAppContext,
10242    ) -> (GeneratingThreadSetup, &mut VisualTestContext) {
10243        let connection = StubAgentConnection::new();
10244
10245        let (thread_view, cx) =
10246            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
10247        add_to_workspace(thread_view.clone(), cx);
10248
10249        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
10250        message_editor.update_in(cx, |editor, window, cx| {
10251            editor.set_text("Hello", window, cx);
10252        });
10253        thread_view.update_in(cx, |thread_view, window, cx| {
10254            thread_view.send(window, cx);
10255        });
10256
10257        let (thread, session_id) = thread_view.read_with(cx, |view, cx| {
10258            let thread = view.as_active_thread().as_ref().unwrap().thread.clone();
10259            (thread.clone(), thread.read(cx).session_id().clone())
10260        });
10261
10262        cx.run_until_parked();
10263
10264        cx.update(|_, cx| {
10265            connection.send_update(
10266                session_id.clone(),
10267                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
10268                    "Response chunk".into(),
10269                )),
10270                cx,
10271            );
10272        });
10273
10274        cx.run_until_parked();
10275
10276        thread.read_with(cx, |thread, _cx| {
10277            assert_eq!(thread.status(), ThreadStatus::Generating);
10278        });
10279
10280        (
10281            GeneratingThreadSetup {
10282                thread_view,
10283                thread,
10284                message_editor,
10285            },
10286            cx,
10287        )
10288    }
10289
10290    #[gpui::test]
10291    async fn test_escape_cancels_generation_from_conversation_focus(cx: &mut TestAppContext) {
10292        init_test(cx);
10293
10294        let (setup, cx) = setup_generating_thread(cx).await;
10295
10296        let focus_handle = setup
10297            .thread_view
10298            .read_with(cx, |view, _cx| view.focus_handle.clone());
10299        cx.update(|window, cx| {
10300            window.focus(&focus_handle, cx);
10301        });
10302
10303        setup.thread_view.update_in(cx, |_, window, cx| {
10304            window.dispatch_action(menu::Cancel.boxed_clone(), cx);
10305        });
10306
10307        cx.run_until_parked();
10308
10309        setup.thread.read_with(cx, |thread, _cx| {
10310            assert_eq!(thread.status(), ThreadStatus::Idle);
10311        });
10312    }
10313
10314    #[gpui::test]
10315    async fn test_escape_cancels_generation_from_editor_focus(cx: &mut TestAppContext) {
10316        init_test(cx);
10317
10318        let (setup, cx) = setup_generating_thread(cx).await;
10319
10320        let editor_focus_handle = setup
10321            .message_editor
10322            .read_with(cx, |editor, cx| editor.focus_handle(cx));
10323        cx.update(|window, cx| {
10324            window.focus(&editor_focus_handle, cx);
10325        });
10326
10327        setup.message_editor.update_in(cx, |_, window, cx| {
10328            window.dispatch_action(editor::actions::Cancel.boxed_clone(), cx);
10329        });
10330
10331        cx.run_until_parked();
10332
10333        setup.thread.read_with(cx, |thread, _cx| {
10334            assert_eq!(thread.status(), ThreadStatus::Idle);
10335        });
10336    }
10337
10338    #[gpui::test]
10339    async fn test_escape_when_idle_is_noop(cx: &mut TestAppContext) {
10340        init_test(cx);
10341
10342        let (thread_view, cx) =
10343            setup_thread_view(StubAgentServer::new(StubAgentConnection::new()), cx).await;
10344        add_to_workspace(thread_view.clone(), cx);
10345
10346        let thread = thread_view.read_with(cx, |view, _cx| {
10347            view.as_active_thread().unwrap().thread.clone()
10348        });
10349
10350        thread.read_with(cx, |thread, _cx| {
10351            assert_eq!(thread.status(), ThreadStatus::Idle);
10352        });
10353
10354        let focus_handle = thread_view.read_with(cx, |view, _cx| view.focus_handle.clone());
10355        cx.update(|window, cx| {
10356            window.focus(&focus_handle, cx);
10357        });
10358
10359        thread_view.update_in(cx, |_, window, cx| {
10360            window.dispatch_action(menu::Cancel.boxed_clone(), cx);
10361        });
10362
10363        cx.run_until_parked();
10364
10365        thread.read_with(cx, |thread, _cx| {
10366            assert_eq!(thread.status(), ThreadStatus::Idle);
10367        });
10368    }
10369
10370    #[gpui::test]
10371    async fn test_interrupt(cx: &mut TestAppContext) {
10372        init_test(cx);
10373
10374        let connection = StubAgentConnection::new();
10375
10376        let (thread_view, cx) =
10377            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
10378        add_to_workspace(thread_view.clone(), cx);
10379
10380        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
10381        message_editor.update_in(cx, |editor, window, cx| {
10382            editor.set_text("Message 1", window, cx);
10383        });
10384        thread_view.update_in(cx, |thread_view, window, cx| {
10385            thread_view.send(window, cx);
10386        });
10387
10388        let (thread, session_id) = thread_view.read_with(cx, |view, cx| {
10389            let thread = view.as_active_thread().unwrap().thread.clone();
10390
10391            (thread.clone(), thread.read(cx).session_id().clone())
10392        });
10393
10394        cx.run_until_parked();
10395
10396        cx.update(|_, cx| {
10397            connection.send_update(
10398                session_id.clone(),
10399                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
10400                    "Message 1 resp".into(),
10401                )),
10402                cx,
10403            );
10404        });
10405
10406        cx.run_until_parked();
10407
10408        thread.read_with(cx, |thread, cx| {
10409            assert_eq!(
10410                thread.to_markdown(cx),
10411                indoc::indoc! {"
10412                    ## User
10413
10414                    Message 1
10415
10416                    ## Assistant
10417
10418                    Message 1 resp
10419
10420                "}
10421            )
10422        });
10423
10424        message_editor.update_in(cx, |editor, window, cx| {
10425            editor.set_text("Message 2", window, cx);
10426        });
10427        thread_view.update_in(cx, |thread_view, window, cx| {
10428            thread_view.interrupt_and_send(window, cx);
10429        });
10430
10431        cx.update(|_, cx| {
10432            // Simulate a response sent after beginning to cancel
10433            connection.send_update(
10434                session_id.clone(),
10435                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("onse".into())),
10436                cx,
10437            );
10438        });
10439
10440        cx.run_until_parked();
10441
10442        // Last Message 1 response should appear before Message 2
10443        thread.read_with(cx, |thread, cx| {
10444            assert_eq!(
10445                thread.to_markdown(cx),
10446                indoc::indoc! {"
10447                    ## User
10448
10449                    Message 1
10450
10451                    ## Assistant
10452
10453                    Message 1 response
10454
10455                    ## User
10456
10457                    Message 2
10458
10459                "}
10460            )
10461        });
10462
10463        cx.update(|_, cx| {
10464            connection.send_update(
10465                session_id.clone(),
10466                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
10467                    "Message 2 response".into(),
10468                )),
10469                cx,
10470            );
10471            connection.end_turn(session_id.clone(), acp::StopReason::EndTurn);
10472        });
10473
10474        cx.run_until_parked();
10475
10476        thread.read_with(cx, |thread, cx| {
10477            assert_eq!(
10478                thread.to_markdown(cx),
10479                indoc::indoc! {"
10480                    ## User
10481
10482                    Message 1
10483
10484                    ## Assistant
10485
10486                    Message 1 response
10487
10488                    ## User
10489
10490                    Message 2
10491
10492                    ## Assistant
10493
10494                    Message 2 response
10495
10496                "}
10497            )
10498        });
10499    }
10500
10501    #[gpui::test]
10502    async fn test_message_editing_insert_selections(cx: &mut TestAppContext) {
10503        init_test(cx);
10504
10505        let connection = StubAgentConnection::new();
10506        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
10507            acp::ContentChunk::new("Response".into()),
10508        )]);
10509
10510        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
10511        add_to_workspace(thread_view.clone(), cx);
10512
10513        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
10514        message_editor.update_in(cx, |editor, window, cx| {
10515            editor.set_text("Original message to edit", window, cx)
10516        });
10517        thread_view.update_in(cx, |thread_view, window, cx| thread_view.send(window, cx));
10518        cx.run_until_parked();
10519
10520        let user_message_editor = thread_view.read_with(cx, |thread_view, cx| {
10521            thread_view
10522                .as_active_thread()
10523                .map(|active| &active.entry_view_state)
10524                .as_ref()
10525                .unwrap()
10526                .read(cx)
10527                .entry(0)
10528                .expect("Should have at least one entry")
10529                .message_editor()
10530                .expect("Should have message editor")
10531                .clone()
10532        });
10533
10534        cx.focus(&user_message_editor);
10535        thread_view.read_with(cx, |view, _cx| {
10536            assert_eq!(
10537                view.as_active_thread()
10538                    .and_then(|active| active.editing_message),
10539                Some(0)
10540            );
10541        });
10542
10543        // Ensure to edit the focused message before proceeding otherwise, since
10544        // its content is not different from what was sent, focus will be lost.
10545        user_message_editor.update_in(cx, |editor, window, cx| {
10546            editor.set_text("Original message to edit with ", window, cx)
10547        });
10548
10549        // Create a simple buffer with some text so we can create a selection
10550        // that will then be added to the message being edited.
10551        let (workspace, project) = thread_view.read_with(cx, |thread_view, _cx| {
10552            (thread_view.workspace.clone(), thread_view.project.clone())
10553        });
10554        let buffer = project.update(cx, |project, cx| {
10555            project.create_local_buffer("let a = 10 + 10;", None, false, cx)
10556        });
10557
10558        workspace
10559            .update_in(cx, |workspace, window, cx| {
10560                let editor = cx.new(|cx| {
10561                    let mut editor =
10562                        Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx);
10563
10564                    editor.change_selections(Default::default(), window, cx, |selections| {
10565                        selections.select_ranges([MultiBufferOffset(8)..MultiBufferOffset(15)]);
10566                    });
10567
10568                    editor
10569                });
10570                workspace.add_item_to_active_pane(Box::new(editor), None, false, window, cx);
10571            })
10572            .unwrap();
10573
10574        thread_view.update_in(cx, |view, window, cx| {
10575            assert_eq!(
10576                view.as_active_thread()
10577                    .and_then(|active| active.editing_message),
10578                Some(0)
10579            );
10580            view.insert_selections(window, cx);
10581        });
10582
10583        user_message_editor.read_with(cx, |editor, cx| {
10584            let text = editor.editor().read(cx).text(cx);
10585            let expected_text = String::from("Original message to edit with selection ");
10586
10587            assert_eq!(text, expected_text);
10588        });
10589    }
10590
10591    #[gpui::test]
10592    async fn test_insert_selections(cx: &mut TestAppContext) {
10593        init_test(cx);
10594
10595        let connection = StubAgentConnection::new();
10596        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
10597            acp::ContentChunk::new("Response".into()),
10598        )]);
10599
10600        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
10601        add_to_workspace(thread_view.clone(), cx);
10602
10603        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
10604        message_editor.update_in(cx, |editor, window, cx| {
10605            editor.set_text("Can you review this snippet ", window, cx)
10606        });
10607
10608        // Create a simple buffer with some text so we can create a selection
10609        // that will then be added to the message being edited.
10610        let (workspace, project) = thread_view.read_with(cx, |thread_view, _cx| {
10611            (thread_view.workspace.clone(), thread_view.project.clone())
10612        });
10613        let buffer = project.update(cx, |project, cx| {
10614            project.create_local_buffer("let a = 10 + 10;", None, false, cx)
10615        });
10616
10617        workspace
10618            .update_in(cx, |workspace, window, cx| {
10619                let editor = cx.new(|cx| {
10620                    let mut editor =
10621                        Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx);
10622
10623                    editor.change_selections(Default::default(), window, cx, |selections| {
10624                        selections.select_ranges([MultiBufferOffset(8)..MultiBufferOffset(15)]);
10625                    });
10626
10627                    editor
10628                });
10629                workspace.add_item_to_active_pane(Box::new(editor), None, false, window, cx);
10630            })
10631            .unwrap();
10632
10633        thread_view.update_in(cx, |view, window, cx| {
10634            assert_eq!(
10635                view.as_active_thread()
10636                    .and_then(|active| active.editing_message),
10637                None
10638            );
10639            view.insert_selections(window, cx);
10640        });
10641
10642        thread_view.read_with(cx, |thread_view, cx| {
10643            let text = thread_view.message_editor.read(cx).text(cx);
10644            let expected_txt = String::from("Can you review this snippet selection ");
10645
10646            assert_eq!(text, expected_txt);
10647        })
10648    }
10649
10650    #[gpui::test]
10651    async fn test_tool_permission_buttons_terminal_with_pattern(cx: &mut TestAppContext) {
10652        init_test(cx);
10653
10654        let tool_call_id = acp::ToolCallId::new("terminal-1");
10655        let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Run `cargo build --release`")
10656            .kind(acp::ToolKind::Edit);
10657
10658        let permission_options = ToolPermissionContext::new("terminal", "cargo build --release")
10659            .build_permission_options();
10660
10661        let connection =
10662            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
10663                tool_call_id.clone(),
10664                permission_options,
10665            )]));
10666
10667        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
10668
10669        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
10670
10671        // Disable notifications to avoid popup windows
10672        cx.update(|_window, cx| {
10673            AgentSettings::override_global(
10674                AgentSettings {
10675                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
10676                    ..AgentSettings::get_global(cx).clone()
10677                },
10678                cx,
10679            );
10680        });
10681
10682        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
10683        message_editor.update_in(cx, |editor, window, cx| {
10684            editor.set_text("Run cargo build", window, cx);
10685        });
10686
10687        thread_view.update_in(cx, |thread_view, window, cx| {
10688            thread_view.send(window, cx);
10689        });
10690
10691        cx.run_until_parked();
10692
10693        // Verify the tool call is in WaitingForConfirmation state with the expected options
10694        thread_view.read_with(cx, |thread_view, cx| {
10695            let thread = thread_view
10696                .as_active_thread()
10697                .expect("Thread should exist")
10698                .thread
10699                .clone();
10700            let thread = thread.read(cx);
10701
10702            let tool_call = thread.entries().iter().find_map(|entry| {
10703                if let acp_thread::AgentThreadEntry::ToolCall(call) = entry {
10704                    Some(call)
10705                } else {
10706                    None
10707                }
10708            });
10709
10710            assert!(tool_call.is_some(), "Expected a tool call entry");
10711            let tool_call = tool_call.unwrap();
10712
10713            // Verify it's waiting for confirmation
10714            assert!(
10715                matches!(
10716                    tool_call.status,
10717                    acp_thread::ToolCallStatus::WaitingForConfirmation { .. }
10718                ),
10719                "Expected WaitingForConfirmation status, got {:?}",
10720                tool_call.status
10721            );
10722
10723            // Verify the options count (granularity options only, no separate Deny option)
10724            if let acp_thread::ToolCallStatus::WaitingForConfirmation { options, .. } =
10725                &tool_call.status
10726            {
10727                let PermissionOptions::Dropdown(choices) = options else {
10728                    panic!("Expected dropdown permission options");
10729                };
10730
10731                assert_eq!(
10732                    choices.len(),
10733                    3,
10734                    "Expected 3 permission options (granularity only)"
10735                );
10736
10737                // Verify specific button labels (now using neutral names)
10738                let labels: Vec<&str> = choices
10739                    .iter()
10740                    .map(|choice| choice.allow.name.as_ref())
10741                    .collect();
10742                assert!(
10743                    labels.contains(&"Always for terminal"),
10744                    "Missing 'Always for terminal' option"
10745                );
10746                assert!(
10747                    labels.contains(&"Always for `cargo` commands"),
10748                    "Missing pattern option"
10749                );
10750                assert!(
10751                    labels.contains(&"Only this time"),
10752                    "Missing 'Only this time' option"
10753                );
10754            }
10755        });
10756    }
10757
10758    #[gpui::test]
10759    async fn test_tool_permission_buttons_edit_file_with_path_pattern(cx: &mut TestAppContext) {
10760        init_test(cx);
10761
10762        let tool_call_id = acp::ToolCallId::new("edit-file-1");
10763        let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Edit `src/main.rs`")
10764            .kind(acp::ToolKind::Edit);
10765
10766        let permission_options =
10767            ToolPermissionContext::new("edit_file", "src/main.rs").build_permission_options();
10768
10769        let connection =
10770            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
10771                tool_call_id.clone(),
10772                permission_options,
10773            )]));
10774
10775        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
10776
10777        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
10778
10779        // Disable notifications
10780        cx.update(|_window, cx| {
10781            AgentSettings::override_global(
10782                AgentSettings {
10783                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
10784                    ..AgentSettings::get_global(cx).clone()
10785                },
10786                cx,
10787            );
10788        });
10789
10790        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
10791        message_editor.update_in(cx, |editor, window, cx| {
10792            editor.set_text("Edit the main file", window, cx);
10793        });
10794
10795        thread_view.update_in(cx, |thread_view, window, cx| {
10796            thread_view.send(window, cx);
10797        });
10798
10799        cx.run_until_parked();
10800
10801        // Verify the options
10802        thread_view.read_with(cx, |thread_view, cx| {
10803            let thread = thread_view
10804                .as_active_thread()
10805                .expect("Thread should exist")
10806                .thread
10807                .clone();
10808            let thread = thread.read(cx);
10809
10810            let tool_call = thread.entries().iter().find_map(|entry| {
10811                if let acp_thread::AgentThreadEntry::ToolCall(call) = entry {
10812                    Some(call)
10813                } else {
10814                    None
10815                }
10816            });
10817
10818            assert!(tool_call.is_some(), "Expected a tool call entry");
10819            let tool_call = tool_call.unwrap();
10820
10821            if let acp_thread::ToolCallStatus::WaitingForConfirmation { options, .. } =
10822                &tool_call.status
10823            {
10824                let PermissionOptions::Dropdown(choices) = options else {
10825                    panic!("Expected dropdown permission options");
10826                };
10827
10828                let labels: Vec<&str> = choices
10829                    .iter()
10830                    .map(|choice| choice.allow.name.as_ref())
10831                    .collect();
10832                assert!(
10833                    labels.contains(&"Always for edit file"),
10834                    "Missing 'Always for edit file' option"
10835                );
10836                assert!(
10837                    labels.contains(&"Always for `src/`"),
10838                    "Missing path pattern option"
10839                );
10840            } else {
10841                panic!("Expected WaitingForConfirmation status");
10842            }
10843        });
10844    }
10845
10846    #[gpui::test]
10847    async fn test_tool_permission_buttons_fetch_with_domain_pattern(cx: &mut TestAppContext) {
10848        init_test(cx);
10849
10850        let tool_call_id = acp::ToolCallId::new("fetch-1");
10851        let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Fetch `https://docs.rs/gpui`")
10852            .kind(acp::ToolKind::Fetch);
10853
10854        let permission_options =
10855            ToolPermissionContext::new("fetch", "https://docs.rs/gpui").build_permission_options();
10856
10857        let connection =
10858            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
10859                tool_call_id.clone(),
10860                permission_options,
10861            )]));
10862
10863        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
10864
10865        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
10866
10867        // Disable notifications
10868        cx.update(|_window, cx| {
10869            AgentSettings::override_global(
10870                AgentSettings {
10871                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
10872                    ..AgentSettings::get_global(cx).clone()
10873                },
10874                cx,
10875            );
10876        });
10877
10878        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
10879        message_editor.update_in(cx, |editor, window, cx| {
10880            editor.set_text("Fetch the docs", window, cx);
10881        });
10882
10883        thread_view.update_in(cx, |thread_view, window, cx| {
10884            thread_view.send(window, cx);
10885        });
10886
10887        cx.run_until_parked();
10888
10889        // Verify the options
10890        thread_view.read_with(cx, |thread_view, cx| {
10891            let thread = thread_view
10892                .as_active_thread()
10893                .expect("Thread should exist")
10894                .thread
10895                .clone();
10896            let thread = thread.read(cx);
10897
10898            let tool_call = thread.entries().iter().find_map(|entry| {
10899                if let acp_thread::AgentThreadEntry::ToolCall(call) = entry {
10900                    Some(call)
10901                } else {
10902                    None
10903                }
10904            });
10905
10906            assert!(tool_call.is_some(), "Expected a tool call entry");
10907            let tool_call = tool_call.unwrap();
10908
10909            if let acp_thread::ToolCallStatus::WaitingForConfirmation { options, .. } =
10910                &tool_call.status
10911            {
10912                let PermissionOptions::Dropdown(choices) = options else {
10913                    panic!("Expected dropdown permission options");
10914                };
10915
10916                let labels: Vec<&str> = choices
10917                    .iter()
10918                    .map(|choice| choice.allow.name.as_ref())
10919                    .collect();
10920                assert!(
10921                    labels.contains(&"Always for fetch"),
10922                    "Missing 'Always for fetch' option"
10923                );
10924                assert!(
10925                    labels.contains(&"Always for `docs.rs`"),
10926                    "Missing domain pattern option"
10927                );
10928            } else {
10929                panic!("Expected WaitingForConfirmation status");
10930            }
10931        });
10932    }
10933
10934    #[gpui::test]
10935    async fn test_tool_permission_buttons_without_pattern(cx: &mut TestAppContext) {
10936        init_test(cx);
10937
10938        let tool_call_id = acp::ToolCallId::new("terminal-no-pattern-1");
10939        let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Run `./deploy.sh --production`")
10940            .kind(acp::ToolKind::Edit);
10941
10942        // No pattern button since ./deploy.sh doesn't match the alphanumeric pattern
10943        let permission_options = ToolPermissionContext::new("terminal", "./deploy.sh --production")
10944            .build_permission_options();
10945
10946        let connection =
10947            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
10948                tool_call_id.clone(),
10949                permission_options,
10950            )]));
10951
10952        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
10953
10954        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
10955
10956        // Disable notifications
10957        cx.update(|_window, cx| {
10958            AgentSettings::override_global(
10959                AgentSettings {
10960                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
10961                    ..AgentSettings::get_global(cx).clone()
10962                },
10963                cx,
10964            );
10965        });
10966
10967        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
10968        message_editor.update_in(cx, |editor, window, cx| {
10969            editor.set_text("Run the deploy script", window, cx);
10970        });
10971
10972        thread_view.update_in(cx, |thread_view, window, cx| {
10973            thread_view.send(window, cx);
10974        });
10975
10976        cx.run_until_parked();
10977
10978        // Verify only 2 options (no pattern button when command doesn't match pattern)
10979        thread_view.read_with(cx, |thread_view, cx| {
10980            let thread = thread_view
10981                .as_active_thread()
10982                .expect("Thread should exist")
10983                .thread
10984                .clone();
10985            let thread = thread.read(cx);
10986
10987            let tool_call = thread.entries().iter().find_map(|entry| {
10988                if let acp_thread::AgentThreadEntry::ToolCall(call) = entry {
10989                    Some(call)
10990                } else {
10991                    None
10992                }
10993            });
10994
10995            assert!(tool_call.is_some(), "Expected a tool call entry");
10996            let tool_call = tool_call.unwrap();
10997
10998            if let acp_thread::ToolCallStatus::WaitingForConfirmation { options, .. } =
10999                &tool_call.status
11000            {
11001                let PermissionOptions::Dropdown(choices) = options else {
11002                    panic!("Expected dropdown permission options");
11003                };
11004
11005                assert_eq!(
11006                    choices.len(),
11007                    2,
11008                    "Expected 2 permission options (no pattern option)"
11009                );
11010
11011                let labels: Vec<&str> = choices
11012                    .iter()
11013                    .map(|choice| choice.allow.name.as_ref())
11014                    .collect();
11015                assert!(
11016                    labels.contains(&"Always for terminal"),
11017                    "Missing 'Always for terminal' option"
11018                );
11019                assert!(
11020                    labels.contains(&"Only this time"),
11021                    "Missing 'Only this time' option"
11022                );
11023                // Should NOT contain a pattern option
11024                assert!(
11025                    !labels.iter().any(|l| l.contains("commands")),
11026                    "Should not have pattern option"
11027                );
11028            } else {
11029                panic!("Expected WaitingForConfirmation status");
11030            }
11031        });
11032    }
11033
11034    #[gpui::test]
11035    async fn test_authorize_tool_call_action_triggers_authorization(cx: &mut TestAppContext) {
11036        init_test(cx);
11037
11038        let tool_call_id = acp::ToolCallId::new("action-test-1");
11039        let tool_call =
11040            acp::ToolCall::new(tool_call_id.clone(), "Run `cargo test`").kind(acp::ToolKind::Edit);
11041
11042        let permission_options =
11043            ToolPermissionContext::new("terminal", "cargo test").build_permission_options();
11044
11045        let connection =
11046            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
11047                tool_call_id.clone(),
11048                permission_options,
11049            )]));
11050
11051        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
11052
11053        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
11054        add_to_workspace(thread_view.clone(), cx);
11055
11056        cx.update(|_window, cx| {
11057            AgentSettings::override_global(
11058                AgentSettings {
11059                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
11060                    ..AgentSettings::get_global(cx).clone()
11061                },
11062                cx,
11063            );
11064        });
11065
11066        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
11067        message_editor.update_in(cx, |editor, window, cx| {
11068            editor.set_text("Run tests", window, cx);
11069        });
11070
11071        thread_view.update_in(cx, |thread_view, window, cx| {
11072            thread_view.send(window, cx);
11073        });
11074
11075        cx.run_until_parked();
11076
11077        // Verify tool call is waiting for confirmation
11078        thread_view.read_with(cx, |thread_view, cx| {
11079            let thread = thread_view
11080                .as_active_thread()
11081                .expect("Thread should exist")
11082                .thread
11083                .clone();
11084            let thread = thread.read(cx);
11085            let tool_call = thread.first_tool_awaiting_confirmation();
11086            assert!(
11087                tool_call.is_some(),
11088                "Expected a tool call waiting for confirmation"
11089            );
11090        });
11091
11092        // Dispatch the AuthorizeToolCall action (simulating dropdown menu selection)
11093        thread_view.update_in(cx, |_, window, cx| {
11094            window.dispatch_action(
11095                crate::AuthorizeToolCall {
11096                    tool_call_id: "action-test-1".to_string(),
11097                    option_id: "allow".to_string(),
11098                    option_kind: "AllowOnce".to_string(),
11099                }
11100                .boxed_clone(),
11101                cx,
11102            );
11103        });
11104
11105        cx.run_until_parked();
11106
11107        // Verify tool call is no longer waiting for confirmation (was authorized)
11108        thread_view.read_with(cx, |thread_view, cx| {
11109            let thread = thread_view.as_active_thread().expect("Thread should exist").thread.clone();
11110            let thread = thread.read(cx);
11111            let tool_call = thread.first_tool_awaiting_confirmation();
11112            assert!(
11113                tool_call.is_none(),
11114                "Tool call should no longer be waiting for confirmation after AuthorizeToolCall action"
11115            );
11116        });
11117    }
11118
11119    #[gpui::test]
11120    async fn test_authorize_tool_call_action_with_pattern_option(cx: &mut TestAppContext) {
11121        init_test(cx);
11122
11123        let tool_call_id = acp::ToolCallId::new("pattern-action-test-1");
11124        let tool_call =
11125            acp::ToolCall::new(tool_call_id.clone(), "Run `npm install`").kind(acp::ToolKind::Edit);
11126
11127        let permission_options =
11128            ToolPermissionContext::new("terminal", "npm install").build_permission_options();
11129
11130        let connection =
11131            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
11132                tool_call_id.clone(),
11133                permission_options.clone(),
11134            )]));
11135
11136        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
11137
11138        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
11139        add_to_workspace(thread_view.clone(), cx);
11140
11141        cx.update(|_window, cx| {
11142            AgentSettings::override_global(
11143                AgentSettings {
11144                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
11145                    ..AgentSettings::get_global(cx).clone()
11146                },
11147                cx,
11148            );
11149        });
11150
11151        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
11152        message_editor.update_in(cx, |editor, window, cx| {
11153            editor.set_text("Install dependencies", window, cx);
11154        });
11155
11156        thread_view.update_in(cx, |thread_view, window, cx| {
11157            thread_view.send(window, cx);
11158        });
11159
11160        cx.run_until_parked();
11161
11162        // Find the pattern option ID
11163        let pattern_option = match &permission_options {
11164            PermissionOptions::Dropdown(choices) => choices
11165                .iter()
11166                .find(|choice| {
11167                    choice
11168                        .allow
11169                        .option_id
11170                        .0
11171                        .starts_with("always_allow_pattern:")
11172                })
11173                .map(|choice| &choice.allow)
11174                .expect("Should have a pattern option for npm command"),
11175            _ => panic!("Expected dropdown permission options"),
11176        };
11177
11178        // Dispatch action with the pattern option (simulating "Always allow `npm` commands")
11179        thread_view.update_in(cx, |_, window, cx| {
11180            window.dispatch_action(
11181                crate::AuthorizeToolCall {
11182                    tool_call_id: "pattern-action-test-1".to_string(),
11183                    option_id: pattern_option.option_id.0.to_string(),
11184                    option_kind: "AllowAlways".to_string(),
11185                }
11186                .boxed_clone(),
11187                cx,
11188            );
11189        });
11190
11191        cx.run_until_parked();
11192
11193        // Verify tool call was authorized
11194        thread_view.read_with(cx, |thread_view, cx| {
11195            let thread = thread_view
11196                .as_active_thread()
11197                .expect("Thread should exist")
11198                .thread
11199                .clone();
11200            let thread = thread.read(cx);
11201            let tool_call = thread.first_tool_awaiting_confirmation();
11202            assert!(
11203                tool_call.is_none(),
11204                "Tool call should be authorized after selecting pattern option"
11205            );
11206        });
11207    }
11208
11209    #[gpui::test]
11210    async fn test_granularity_selection_updates_state(cx: &mut TestAppContext) {
11211        init_test(cx);
11212
11213        let tool_call_id = acp::ToolCallId::new("granularity-test-1");
11214        let tool_call =
11215            acp::ToolCall::new(tool_call_id.clone(), "Run `cargo build`").kind(acp::ToolKind::Edit);
11216
11217        let permission_options =
11218            ToolPermissionContext::new("terminal", "cargo build").build_permission_options();
11219
11220        let connection =
11221            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
11222                tool_call_id.clone(),
11223                permission_options.clone(),
11224            )]));
11225
11226        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
11227
11228        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
11229        add_to_workspace(thread_view.clone(), cx);
11230
11231        cx.update(|_window, cx| {
11232            AgentSettings::override_global(
11233                AgentSettings {
11234                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
11235                    ..AgentSettings::get_global(cx).clone()
11236                },
11237                cx,
11238            );
11239        });
11240
11241        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
11242        message_editor.update_in(cx, |editor, window, cx| {
11243            editor.set_text("Build the project", window, cx);
11244        });
11245
11246        thread_view.update_in(cx, |thread_view, window, cx| {
11247            thread_view.send(window, cx);
11248        });
11249
11250        cx.run_until_parked();
11251
11252        // Verify default granularity is the last option (index 2 = "Only this time")
11253        thread_view.read_with(cx, |thread_view, _cx| {
11254            let state = thread_view.as_active_thread().unwrap();
11255            let selected = state.selected_permission_granularity.get(&tool_call_id);
11256            assert!(
11257                selected.is_none(),
11258                "Should have no selection initially (defaults to last)"
11259            );
11260        });
11261
11262        // Select the first option (index 0 = "Always for terminal")
11263        thread_view.update_in(cx, |_, window, cx| {
11264            window.dispatch_action(
11265                crate::SelectPermissionGranularity {
11266                    tool_call_id: "granularity-test-1".to_string(),
11267                    index: 0,
11268                }
11269                .boxed_clone(),
11270                cx,
11271            );
11272        });
11273
11274        cx.run_until_parked();
11275
11276        // Verify the selection was updated
11277        thread_view.read_with(cx, |thread_view, _cx| {
11278            let state = thread_view.as_active_thread().unwrap();
11279            let selected = state.selected_permission_granularity.get(&tool_call_id);
11280            assert_eq!(selected, Some(&0), "Should have selected index 0");
11281        });
11282    }
11283
11284    #[gpui::test]
11285    async fn test_allow_button_uses_selected_granularity(cx: &mut TestAppContext) {
11286        init_test(cx);
11287
11288        let tool_call_id = acp::ToolCallId::new("allow-granularity-test-1");
11289        let tool_call =
11290            acp::ToolCall::new(tool_call_id.clone(), "Run `npm install`").kind(acp::ToolKind::Edit);
11291
11292        let permission_options =
11293            ToolPermissionContext::new("terminal", "npm install").build_permission_options();
11294
11295        // Verify we have the expected options
11296        let PermissionOptions::Dropdown(choices) = &permission_options else {
11297            panic!("Expected dropdown permission options");
11298        };
11299
11300        assert_eq!(choices.len(), 3);
11301        assert!(
11302            choices[0]
11303                .allow
11304                .option_id
11305                .0
11306                .contains("always_allow:terminal")
11307        );
11308        assert!(
11309            choices[1]
11310                .allow
11311                .option_id
11312                .0
11313                .contains("always_allow_pattern:terminal")
11314        );
11315        assert_eq!(choices[2].allow.option_id.0.as_ref(), "allow");
11316
11317        let connection =
11318            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
11319                tool_call_id.clone(),
11320                permission_options.clone(),
11321            )]));
11322
11323        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
11324
11325        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
11326        add_to_workspace(thread_view.clone(), cx);
11327
11328        cx.update(|_window, cx| {
11329            AgentSettings::override_global(
11330                AgentSettings {
11331                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
11332                    ..AgentSettings::get_global(cx).clone()
11333                },
11334                cx,
11335            );
11336        });
11337
11338        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
11339        message_editor.update_in(cx, |editor, window, cx| {
11340            editor.set_text("Install dependencies", window, cx);
11341        });
11342
11343        thread_view.update_in(cx, |thread_view, window, cx| {
11344            thread_view.send(window, cx);
11345        });
11346
11347        cx.run_until_parked();
11348
11349        // Select the pattern option (index 1 = "Always for `npm` commands")
11350        thread_view.update_in(cx, |_, window, cx| {
11351            window.dispatch_action(
11352                crate::SelectPermissionGranularity {
11353                    tool_call_id: "allow-granularity-test-1".to_string(),
11354                    index: 1,
11355                }
11356                .boxed_clone(),
11357                cx,
11358            );
11359        });
11360
11361        cx.run_until_parked();
11362
11363        // Simulate clicking the Allow button by dispatching AllowOnce action
11364        // which should use the selected granularity
11365        thread_view.update_in(cx, |thread_view, window, cx| {
11366            thread_view.allow_once(&AllowOnce, window, cx);
11367        });
11368
11369        cx.run_until_parked();
11370
11371        // Verify tool call was authorized
11372        thread_view.read_with(cx, |thread_view, cx| {
11373            let thread = thread_view
11374                .as_active_thread()
11375                .expect("Thread should exist")
11376                .thread
11377                .clone();
11378            let thread = thread.read(cx);
11379            let tool_call = thread.first_tool_awaiting_confirmation();
11380            assert!(
11381                tool_call.is_none(),
11382                "Tool call should be authorized after Allow with pattern granularity"
11383            );
11384        });
11385    }
11386
11387    #[gpui::test]
11388    async fn test_deny_button_uses_selected_granularity(cx: &mut TestAppContext) {
11389        init_test(cx);
11390
11391        let tool_call_id = acp::ToolCallId::new("deny-granularity-test-1");
11392        let tool_call =
11393            acp::ToolCall::new(tool_call_id.clone(), "Run `git push`").kind(acp::ToolKind::Edit);
11394
11395        let permission_options =
11396            ToolPermissionContext::new("terminal", "git push").build_permission_options();
11397
11398        let connection =
11399            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
11400                tool_call_id.clone(),
11401                permission_options.clone(),
11402            )]));
11403
11404        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
11405
11406        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
11407        add_to_workspace(thread_view.clone(), cx);
11408
11409        cx.update(|_window, cx| {
11410            AgentSettings::override_global(
11411                AgentSettings {
11412                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
11413                    ..AgentSettings::get_global(cx).clone()
11414                },
11415                cx,
11416            );
11417        });
11418
11419        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
11420        message_editor.update_in(cx, |editor, window, cx| {
11421            editor.set_text("Push changes", window, cx);
11422        });
11423
11424        thread_view.update_in(cx, |thread_view, window, cx| {
11425            thread_view.send(window, cx);
11426        });
11427
11428        cx.run_until_parked();
11429
11430        // Use default granularity (last option = "Only this time")
11431        // Simulate clicking the Deny button
11432        thread_view.update_in(cx, |thread_view, window, cx| {
11433            thread_view.reject_once(&RejectOnce, window, cx);
11434        });
11435
11436        cx.run_until_parked();
11437
11438        // Verify tool call was rejected (no longer waiting for confirmation)
11439        thread_view.read_with(cx, |thread_view, cx| {
11440            let thread = thread_view
11441                .as_active_thread()
11442                .expect("Thread should exist")
11443                .thread
11444                .clone();
11445            let thread = thread.read(cx);
11446            let tool_call = thread.first_tool_awaiting_confirmation();
11447            assert!(
11448                tool_call.is_none(),
11449                "Tool call should be rejected after Deny"
11450            );
11451        });
11452    }
11453
11454    #[gpui::test]
11455    async fn test_option_id_transformation_for_allow() {
11456        let permission_options = ToolPermissionContext::new("terminal", "cargo build --release")
11457            .build_permission_options();
11458
11459        let PermissionOptions::Dropdown(choices) = permission_options else {
11460            panic!("Expected dropdown permission options");
11461        };
11462
11463        let allow_ids: Vec<String> = choices
11464            .iter()
11465            .map(|choice| choice.allow.option_id.0.to_string())
11466            .collect();
11467
11468        assert!(allow_ids.contains(&"always_allow:terminal".to_string()));
11469        assert!(allow_ids.contains(&"allow".to_string()));
11470        assert!(
11471            allow_ids
11472                .iter()
11473                .any(|id| id.starts_with("always_allow_pattern:terminal:")),
11474            "Missing allow pattern option"
11475        );
11476    }
11477
11478    #[gpui::test]
11479    async fn test_option_id_transformation_for_deny() {
11480        let permission_options = ToolPermissionContext::new("terminal", "cargo build --release")
11481            .build_permission_options();
11482
11483        let PermissionOptions::Dropdown(choices) = permission_options else {
11484            panic!("Expected dropdown permission options");
11485        };
11486
11487        let deny_ids: Vec<String> = choices
11488            .iter()
11489            .map(|choice| choice.deny.option_id.0.to_string())
11490            .collect();
11491
11492        assert!(deny_ids.contains(&"always_deny:terminal".to_string()));
11493        assert!(deny_ids.contains(&"deny".to_string()));
11494        assert!(
11495            deny_ids
11496                .iter()
11497                .any(|id| id.starts_with("always_deny_pattern:terminal:")),
11498            "Missing deny pattern option"
11499        );
11500    }
11501}