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 confirmation_options = match &tool_call.status {
 4673            ToolCallStatus::WaitingForConfirmation { options, .. } => Some(options),
 4674            _ => None,
 4675        };
 4676        let needs_confirmation = confirmation_options.is_some();
 4677
 4678        let output = terminal_data.output();
 4679        let command_finished = output.is_some();
 4680        let truncated_output =
 4681            output.is_some_and(|output| output.original_content_len > output.content.len());
 4682        let output_line_count = output.map(|output| output.content_line_count).unwrap_or(0);
 4683
 4684        let command_failed = command_finished
 4685            && output.is_some_and(|o| o.exit_status.is_some_and(|status| !status.success()));
 4686
 4687        let time_elapsed = if let Some(output) = output {
 4688            output.ended_at.duration_since(started_at)
 4689        } else {
 4690            started_at.elapsed()
 4691        };
 4692
 4693        let header_id =
 4694            SharedString::from(format!("terminal-tool-header-{}", terminal.entity_id()));
 4695        let header_group = SharedString::from(format!(
 4696            "terminal-tool-header-group-{}",
 4697            terminal.entity_id()
 4698        ));
 4699        let header_bg = cx
 4700            .theme()
 4701            .colors()
 4702            .element_background
 4703            .blend(cx.theme().colors().editor_foreground.opacity(0.025));
 4704        let border_color = cx.theme().colors().border.opacity(0.6);
 4705
 4706        let working_dir = working_dir
 4707            .as_ref()
 4708            .map(|path| path.display().to_string())
 4709            .unwrap_or_else(|| "current directory".to_string());
 4710
 4711        // Since the command's source is wrapped in a markdown code block
 4712        // (```\n...\n```), we need to strip that so we're left with only the
 4713        // command's content.
 4714        let command_source = command.read(cx).source();
 4715        let command_content = command_source
 4716            .strip_prefix("```\n")
 4717            .and_then(|s| s.strip_suffix("\n```"))
 4718            .unwrap_or(&command_source);
 4719
 4720        let command_element =
 4721            self.render_collapsible_command(false, command_content, &tool_call.id, cx);
 4722
 4723        let is_expanded = self.as_connected().is_some_and(|c| {
 4724            c.current.as_ref().map_or(false, |view| {
 4725                view.expanded_tool_calls.contains(&tool_call.id)
 4726            })
 4727        });
 4728
 4729        let header = h_flex()
 4730            .id(header_id)
 4731            .px_1p5()
 4732            .pt_1()
 4733            .flex_none()
 4734            .gap_1()
 4735            .justify_between()
 4736            .rounded_t_md()
 4737            .child(
 4738                div()
 4739                    .id(("command-target-path", terminal.entity_id()))
 4740                    .w_full()
 4741                    .max_w_full()
 4742                    .overflow_x_scroll()
 4743                    .child(
 4744                        Label::new(working_dir)
 4745                            .buffer_font(cx)
 4746                            .size(LabelSize::XSmall)
 4747                            .color(Color::Muted),
 4748                    ),
 4749            )
 4750            .when(!command_finished && !needs_confirmation, |header| {
 4751                header
 4752                    .gap_1p5()
 4753                    .child(
 4754                        Button::new(
 4755                            SharedString::from(format!("stop-terminal-{}", terminal.entity_id())),
 4756                            "Stop",
 4757                        )
 4758                        .icon(IconName::Stop)
 4759                        .icon_position(IconPosition::Start)
 4760                        .icon_size(IconSize::Small)
 4761                        .icon_color(Color::Error)
 4762                        .label_size(LabelSize::Small)
 4763                        .tooltip(move |_window, cx| {
 4764                            Tooltip::with_meta(
 4765                                "Stop This Command",
 4766                                None,
 4767                                "Also possible by placing your cursor inside the terminal and using regular terminal bindings.",
 4768                                cx,
 4769                            )
 4770                        })
 4771                        .on_click({
 4772                            let terminal = terminal.clone();
 4773                            cx.listener(move |this, _event, _window, cx| {
 4774                                terminal.update(cx, |terminal, cx| {
 4775                                    terminal.stop_by_user(cx);
 4776                                });
 4777                                if AgentSettings::get_global(cx).cancel_generation_on_terminal_stop {
 4778                                    this.cancel_generation(cx);
 4779                                }
 4780                            })
 4781                        }),
 4782                    )
 4783                    .child(Divider::vertical())
 4784                    .child(
 4785                        Icon::new(IconName::ArrowCircle)
 4786                            .size(IconSize::XSmall)
 4787                            .color(Color::Info)
 4788                            .with_rotate_animation(2)
 4789                    )
 4790            })
 4791            .when(truncated_output, |header| {
 4792                let tooltip = if let Some(output) = output {
 4793                    if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
 4794                       format!("Output exceeded terminal max lines and was \
 4795                            truncated, the model received the first {}.", format_file_size(output.content.len() as u64, true))
 4796                    } else {
 4797                        format!(
 4798                            "Output is {} long, and to avoid unexpected token usage, \
 4799                                only {} was sent back to the agent.",
 4800                            format_file_size(output.original_content_len as u64, true),
 4801                             format_file_size(output.content.len() as u64, true)
 4802                        )
 4803                    }
 4804                } else {
 4805                    "Output was truncated".to_string()
 4806                };
 4807
 4808                header.child(
 4809                    h_flex()
 4810                        .id(("terminal-tool-truncated-label", terminal.entity_id()))
 4811                        .gap_1()
 4812                        .child(
 4813                            Icon::new(IconName::Info)
 4814                                .size(IconSize::XSmall)
 4815                                .color(Color::Ignored),
 4816                        )
 4817                        .child(
 4818                            Label::new("Truncated")
 4819                                .color(Color::Muted)
 4820                                .size(LabelSize::XSmall),
 4821                        )
 4822                        .tooltip(Tooltip::text(tooltip)),
 4823                )
 4824            })
 4825            .when(time_elapsed > Duration::from_secs(10), |header| {
 4826                header.child(
 4827                    Label::new(format!("({})", duration_alt_display(time_elapsed)))
 4828                        .buffer_font(cx)
 4829                        .color(Color::Muted)
 4830                        .size(LabelSize::XSmall),
 4831                )
 4832            })
 4833            .when(tool_failed || command_failed, |header| {
 4834                header.child(
 4835                    div()
 4836                        .id(("terminal-tool-error-code-indicator", terminal.entity_id()))
 4837                        .child(
 4838                            Icon::new(IconName::Close)
 4839                                .size(IconSize::Small)
 4840                                .color(Color::Error),
 4841                        )
 4842                        .when_some(output.and_then(|o| o.exit_status), |this, status| {
 4843                            this.tooltip(Tooltip::text(format!(
 4844                                "Exited with code {}",
 4845                                status.code().unwrap_or(-1),
 4846                            )))
 4847                        }),
 4848                )
 4849            })
 4850            .child(
 4851                Disclosure::new(
 4852                    SharedString::from(format!(
 4853                        "terminal-tool-disclosure-{}",
 4854                        terminal.entity_id()
 4855                    )),
 4856                    is_expanded,
 4857                )
 4858                .opened_icon(IconName::ChevronUp)
 4859                .closed_icon(IconName::ChevronDown)
 4860                .visible_on_hover(&header_group)
 4861                .on_click(cx.listener({
 4862                    let id = tool_call.id.clone();
 4863                    move |this, _event, _window, _cx| {
 4864                        if let Some(active) = this.as_active_thread_mut() {
 4865                            if is_expanded {
 4866                                active.expanded_tool_calls.remove(&id);
 4867                            } else {
 4868                                active.expanded_tool_calls.insert(id.clone());
 4869                            }
 4870                        }
 4871                    }
 4872                })),
 4873            );
 4874
 4875        let terminal_view = self
 4876            .as_active_thread()
 4877            .map(|active| &active.entry_view_state)
 4878            .and_then(|entry_view_state| {
 4879                entry_view_state
 4880                    .read(cx)
 4881                    .entry(entry_ix)
 4882                    .and_then(|entry| entry.terminal(terminal))
 4883            });
 4884
 4885        v_flex()
 4886            .my_1p5()
 4887            .mx_5()
 4888            .border_1()
 4889            .when(tool_failed || command_failed, |card| card.border_dashed())
 4890            .border_color(border_color)
 4891            .rounded_md()
 4892            .overflow_hidden()
 4893            .child(
 4894                v_flex()
 4895                    .group(&header_group)
 4896                    .bg(header_bg)
 4897                    .text_xs()
 4898                    .child(header)
 4899                    .child(command_element),
 4900            )
 4901            .when(is_expanded && terminal_view.is_some(), |this| {
 4902                this.child(
 4903                    div()
 4904                        .pt_2()
 4905                        .border_t_1()
 4906                        .when(tool_failed || command_failed, |card| card.border_dashed())
 4907                        .border_color(border_color)
 4908                        .bg(cx.theme().colors().editor_background)
 4909                        .rounded_b_md()
 4910                        .text_ui_sm(cx)
 4911                        .h_full()
 4912                        .children(terminal_view.map(|terminal_view| {
 4913                            let element = if terminal_view
 4914                                .read(cx)
 4915                                .content_mode(window, cx)
 4916                                .is_scrollable()
 4917                            {
 4918                                div().h_72().child(terminal_view).into_any_element()
 4919                            } else {
 4920                                terminal_view.into_any_element()
 4921                            };
 4922
 4923                            div()
 4924                                .on_action(cx.listener(|_this, _: &NewTerminal, window, cx| {
 4925                                    window.dispatch_action(NewThread.boxed_clone(), cx);
 4926                                    cx.stop_propagation();
 4927                                }))
 4928                                .child(element)
 4929                                .into_any_element()
 4930                        })),
 4931                )
 4932            })
 4933            .when_some(confirmation_options, |this, options| {
 4934                this.child(self.render_permission_buttons(
 4935                    options,
 4936                    entry_ix,
 4937                    tool_call.id.clone(),
 4938                    cx,
 4939                ))
 4940            })
 4941            .into_any()
 4942    }
 4943
 4944    fn render_rules_item(&self, cx: &Context<Self>) -> Option<AnyElement> {
 4945        let project_context = self
 4946            .as_native_thread(cx)?
 4947            .read(cx)
 4948            .project_context()
 4949            .read(cx);
 4950
 4951        let user_rules_text = if project_context.user_rules.is_empty() {
 4952            None
 4953        } else if project_context.user_rules.len() == 1 {
 4954            let user_rules = &project_context.user_rules[0];
 4955
 4956            match user_rules.title.as_ref() {
 4957                Some(title) => Some(format!("Using \"{title}\" user rule")),
 4958                None => Some("Using user rule".into()),
 4959            }
 4960        } else {
 4961            Some(format!(
 4962                "Using {} user rules",
 4963                project_context.user_rules.len()
 4964            ))
 4965        };
 4966
 4967        let first_user_rules_id = project_context
 4968            .user_rules
 4969            .first()
 4970            .map(|user_rules| user_rules.uuid.0);
 4971
 4972        let rules_files = project_context
 4973            .worktrees
 4974            .iter()
 4975            .filter_map(|worktree| worktree.rules_file.as_ref())
 4976            .collect::<Vec<_>>();
 4977
 4978        let rules_file_text = match rules_files.as_slice() {
 4979            &[] => None,
 4980            &[rules_file] => Some(format!(
 4981                "Using project {:?} file",
 4982                rules_file.path_in_worktree
 4983            )),
 4984            rules_files => Some(format!("Using {} project rules files", rules_files.len())),
 4985        };
 4986
 4987        if user_rules_text.is_none() && rules_file_text.is_none() {
 4988            return None;
 4989        }
 4990
 4991        let has_both = user_rules_text.is_some() && rules_file_text.is_some();
 4992
 4993        Some(
 4994            h_flex()
 4995                .px_2p5()
 4996                .child(
 4997                    Icon::new(IconName::Attach)
 4998                        .size(IconSize::XSmall)
 4999                        .color(Color::Disabled),
 5000                )
 5001                .when_some(user_rules_text, |parent, user_rules_text| {
 5002                    parent.child(
 5003                        h_flex()
 5004                            .id("user-rules")
 5005                            .ml_1()
 5006                            .mr_1p5()
 5007                            .child(
 5008                                Label::new(user_rules_text)
 5009                                    .size(LabelSize::XSmall)
 5010                                    .color(Color::Muted)
 5011                                    .truncate(),
 5012                            )
 5013                            .hover(|s| s.bg(cx.theme().colors().element_hover))
 5014                            .tooltip(Tooltip::text("View User Rules"))
 5015                            .on_click(move |_event, window, cx| {
 5016                                window.dispatch_action(
 5017                                    Box::new(OpenRulesLibrary {
 5018                                        prompt_to_select: first_user_rules_id,
 5019                                    }),
 5020                                    cx,
 5021                                )
 5022                            }),
 5023                    )
 5024                })
 5025                .when(has_both, |this| {
 5026                    this.child(
 5027                        Label::new("")
 5028                            .size(LabelSize::XSmall)
 5029                            .color(Color::Disabled),
 5030                    )
 5031                })
 5032                .when_some(rules_file_text, |parent, rules_file_text| {
 5033                    parent.child(
 5034                        h_flex()
 5035                            .id("project-rules")
 5036                            .ml_1p5()
 5037                            .child(
 5038                                Label::new(rules_file_text)
 5039                                    .size(LabelSize::XSmall)
 5040                                    .color(Color::Muted),
 5041                            )
 5042                            .hover(|s| s.bg(cx.theme().colors().element_hover))
 5043                            .tooltip(Tooltip::text("View Project Rules"))
 5044                            .on_click(cx.listener(Self::handle_open_rules)),
 5045                    )
 5046                })
 5047                .into_any(),
 5048        )
 5049    }
 5050
 5051    fn render_empty_state_section_header(
 5052        &self,
 5053        label: impl Into<SharedString>,
 5054        action_slot: Option<AnyElement>,
 5055        cx: &mut Context<Self>,
 5056    ) -> impl IntoElement {
 5057        div().pl_1().pr_1p5().child(
 5058            h_flex()
 5059                .mt_2()
 5060                .pl_1p5()
 5061                .pb_1()
 5062                .w_full()
 5063                .justify_between()
 5064                .border_b_1()
 5065                .border_color(cx.theme().colors().border_variant)
 5066                .child(
 5067                    Label::new(label.into())
 5068                        .size(LabelSize::Small)
 5069                        .color(Color::Muted),
 5070                )
 5071                .children(action_slot),
 5072        )
 5073    }
 5074
 5075    fn render_resume_notice(&self, _cx: &Context<Self>) -> AnyElement {
 5076        let description = "This agent does not support viewing previous messages. However, your session will still continue from where you last left off.";
 5077
 5078        div()
 5079            .px_2()
 5080            .pt_2()
 5081            .pb_3()
 5082            .w_full()
 5083            .child(
 5084                Callout::new()
 5085                    .severity(Severity::Info)
 5086                    .icon(IconName::Info)
 5087                    .title("Resumed Session")
 5088                    .description(description),
 5089            )
 5090            .into_any_element()
 5091    }
 5092
 5093    fn update_recent_history_from_cache(
 5094        &mut self,
 5095        history: &Entity<AcpThreadHistory>,
 5096        cx: &mut Context<Self>,
 5097    ) {
 5098        self.recent_history_entries = history.read(cx).get_recent_sessions(3);
 5099        self.hovered_recent_history_item = None;
 5100        cx.notify();
 5101    }
 5102
 5103    fn render_recent_history(&self, cx: &mut Context<Self>) -> AnyElement {
 5104        let render_history = !self.recent_history_entries.is_empty();
 5105
 5106        v_flex()
 5107            .size_full()
 5108            .when(render_history, |this| {
 5109                let recent_history = self.recent_history_entries.clone();
 5110                this.justify_end().child(
 5111                    v_flex()
 5112                        .child(
 5113                            self.render_empty_state_section_header(
 5114                                "Recent",
 5115                                Some(
 5116                                    Button::new("view-history", "View All")
 5117                                        .style(ButtonStyle::Subtle)
 5118                                        .label_size(LabelSize::Small)
 5119                                        .key_binding(
 5120                                            KeyBinding::for_action_in(
 5121                                                &OpenHistory,
 5122                                                &self.focus_handle(cx),
 5123                                                cx,
 5124                                            )
 5125                                            .map(|kb| kb.size(rems_from_px(12.))),
 5126                                        )
 5127                                        .on_click(move |_event, window, cx| {
 5128                                            window.dispatch_action(OpenHistory.boxed_clone(), cx);
 5129                                        })
 5130                                        .into_any_element(),
 5131                                ),
 5132                                cx,
 5133                            ),
 5134                        )
 5135                        .child(v_flex().p_1().pr_1p5().gap_1().children({
 5136                            let supports_delete = self.history.read(cx).supports_delete();
 5137                            recent_history
 5138                                .into_iter()
 5139                                .enumerate()
 5140                                .map(move |(index, entry)| {
 5141                                    // TODO: Add keyboard navigation.
 5142                                    let is_hovered =
 5143                                        self.hovered_recent_history_item == Some(index);
 5144                                    crate::acp::thread_history::AcpHistoryEntryElement::new(
 5145                                        entry,
 5146                                        cx.entity().downgrade(),
 5147                                    )
 5148                                    .hovered(is_hovered)
 5149                                    .supports_delete(supports_delete)
 5150                                    .on_hover(cx.listener(move |this, is_hovered, _window, cx| {
 5151                                        if *is_hovered {
 5152                                            this.hovered_recent_history_item = Some(index);
 5153                                        } else if this.hovered_recent_history_item == Some(index) {
 5154                                            this.hovered_recent_history_item = None;
 5155                                        }
 5156                                        cx.notify();
 5157                                    }))
 5158                                    .into_any_element()
 5159                                })
 5160                        })),
 5161                )
 5162            })
 5163            .into_any()
 5164    }
 5165
 5166    fn render_auth_required_state(
 5167        &self,
 5168        connection: &Rc<dyn AgentConnection>,
 5169        description: Option<&Entity<Markdown>>,
 5170        configuration_view: Option<&AnyView>,
 5171        pending_auth_method: Option<&acp::AuthMethodId>,
 5172        window: &mut Window,
 5173        cx: &Context<Self>,
 5174    ) -> impl IntoElement {
 5175        let auth_methods = connection.auth_methods();
 5176
 5177        let agent_display_name = self
 5178            .agent_server_store
 5179            .read(cx)
 5180            .agent_display_name(&ExternalAgentServerName(self.agent.name()))
 5181            .unwrap_or_else(|| self.agent.name());
 5182
 5183        let show_fallback_description = auth_methods.len() > 1
 5184            && configuration_view.is_none()
 5185            && description.is_none()
 5186            && pending_auth_method.is_none();
 5187
 5188        let auth_buttons = || {
 5189            h_flex().justify_end().flex_wrap().gap_1().children(
 5190                connection
 5191                    .auth_methods()
 5192                    .iter()
 5193                    .enumerate()
 5194                    .rev()
 5195                    .map(|(ix, method)| {
 5196                        let (method_id, name) = if self.project.read(cx).is_via_remote_server()
 5197                            && method.id.0.as_ref() == "oauth-personal"
 5198                            && method.name == "Log in with Google"
 5199                        {
 5200                            ("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
 5201                        } else {
 5202                            (method.id.0.clone(), method.name.clone())
 5203                        };
 5204
 5205                        let agent_telemetry_id = connection.telemetry_id();
 5206
 5207                        Button::new(method_id.clone(), name)
 5208                            .label_size(LabelSize::Small)
 5209                            .map(|this| {
 5210                                if ix == 0 {
 5211                                    this.style(ButtonStyle::Tinted(TintColor::Accent))
 5212                                } else {
 5213                                    this.style(ButtonStyle::Outlined)
 5214                                }
 5215                            })
 5216                            .when_some(method.description.clone(), |this, description| {
 5217                                this.tooltip(Tooltip::text(description))
 5218                            })
 5219                            .on_click({
 5220                                cx.listener(move |this, _, window, cx| {
 5221                                    telemetry::event!(
 5222                                        "Authenticate Agent Started",
 5223                                        agent = agent_telemetry_id,
 5224                                        method = method_id
 5225                                    );
 5226
 5227                                    this.authenticate(
 5228                                        acp::AuthMethodId::new(method_id.clone()),
 5229                                        window,
 5230                                        cx,
 5231                                    )
 5232                                })
 5233                            })
 5234                    }),
 5235            )
 5236        };
 5237
 5238        if pending_auth_method.is_some() {
 5239            return Callout::new()
 5240                .icon(IconName::Info)
 5241                .title(format!("Authenticating to {}", agent_display_name))
 5242                .actions_slot(
 5243                    Icon::new(IconName::ArrowCircle)
 5244                        .size(IconSize::Small)
 5245                        .color(Color::Muted)
 5246                        .with_rotate_animation(2)
 5247                        .into_any_element(),
 5248                )
 5249                .into_any_element();
 5250        }
 5251
 5252        Callout::new()
 5253            .icon(IconName::Info)
 5254            .title(format!("Authenticate to {}", agent_display_name))
 5255            .when(auth_methods.len() == 1, |this| {
 5256                this.actions_slot(auth_buttons())
 5257            })
 5258            .description_slot(
 5259                v_flex()
 5260                    .text_ui(cx)
 5261                    .map(|this| {
 5262                        if show_fallback_description {
 5263                            this.child(
 5264                                Label::new("Choose one of the following authentication options:")
 5265                                    .size(LabelSize::Small)
 5266                                    .color(Color::Muted),
 5267                            )
 5268                        } else {
 5269                            this.children(
 5270                                configuration_view
 5271                                    .cloned()
 5272                                    .map(|view| div().w_full().child(view)),
 5273                            )
 5274                            .children(description.map(|desc| {
 5275                                self.render_markdown(
 5276                                    desc.clone(),
 5277                                    MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
 5278                                )
 5279                            }))
 5280                        }
 5281                    })
 5282                    .when(auth_methods.len() > 1, |this| {
 5283                        this.gap_1().child(auth_buttons())
 5284                    }),
 5285            )
 5286            .into_any_element()
 5287    }
 5288
 5289    fn render_load_error(
 5290        &self,
 5291        e: &LoadError,
 5292        window: &mut Window,
 5293        cx: &mut Context<Self>,
 5294    ) -> AnyElement {
 5295        let (title, message, action_slot): (_, SharedString, _) = match e {
 5296            LoadError::Unsupported {
 5297                command: path,
 5298                current_version,
 5299                minimum_version,
 5300            } => {
 5301                return self.render_unsupported(path, current_version, minimum_version, window, cx);
 5302            }
 5303            LoadError::FailedToInstall(msg) => (
 5304                "Failed to Install",
 5305                msg.into(),
 5306                Some(self.create_copy_button(msg.to_string()).into_any_element()),
 5307            ),
 5308            LoadError::Exited { status } => (
 5309                "Failed to Launch",
 5310                format!("Server exited with status {status}").into(),
 5311                None,
 5312            ),
 5313            LoadError::Other(msg) => (
 5314                "Failed to Launch",
 5315                msg.into(),
 5316                Some(self.create_copy_button(msg.to_string()).into_any_element()),
 5317            ),
 5318        };
 5319
 5320        Callout::new()
 5321            .severity(Severity::Error)
 5322            .icon(IconName::XCircleFilled)
 5323            .title(title)
 5324            .description(message)
 5325            .actions_slot(div().children(action_slot))
 5326            .into_any_element()
 5327    }
 5328
 5329    fn render_unsupported(
 5330        &self,
 5331        path: &SharedString,
 5332        version: &SharedString,
 5333        minimum_version: &SharedString,
 5334        _window: &mut Window,
 5335        cx: &mut Context<Self>,
 5336    ) -> AnyElement {
 5337        let (heading_label, description_label) = (
 5338            format!("Upgrade {} to work with Zed", self.agent.name()),
 5339            if version.is_empty() {
 5340                format!(
 5341                    "Currently using {}, which does not report a valid --version",
 5342                    path,
 5343                )
 5344            } else {
 5345                format!(
 5346                    "Currently using {}, which is only version {} (need at least {minimum_version})",
 5347                    path, version
 5348                )
 5349            },
 5350        );
 5351
 5352        v_flex()
 5353            .w_full()
 5354            .p_3p5()
 5355            .gap_2p5()
 5356            .border_t_1()
 5357            .border_color(cx.theme().colors().border)
 5358            .bg(linear_gradient(
 5359                180.,
 5360                linear_color_stop(cx.theme().colors().editor_background.opacity(0.4), 4.),
 5361                linear_color_stop(cx.theme().status().info_background.opacity(0.), 0.),
 5362            ))
 5363            .child(
 5364                v_flex().gap_0p5().child(Label::new(heading_label)).child(
 5365                    Label::new(description_label)
 5366                        .size(LabelSize::Small)
 5367                        .color(Color::Muted),
 5368                ),
 5369            )
 5370            .into_any_element()
 5371    }
 5372
 5373    fn activity_bar_bg(&self, cx: &Context<Self>) -> Hsla {
 5374        let editor_bg_color = cx.theme().colors().editor_background;
 5375        let active_color = cx.theme().colors().element_selected;
 5376        editor_bg_color.blend(active_color.opacity(0.3))
 5377    }
 5378
 5379    fn render_activity_bar(
 5380        &self,
 5381        thread_entity: &Entity<AcpThread>,
 5382        window: &mut Window,
 5383        cx: &Context<Self>,
 5384    ) -> Option<AnyElement> {
 5385        let thread = thread_entity.read(cx);
 5386        let action_log = thread.action_log();
 5387        let telemetry = ActionLogTelemetry::from(thread);
 5388        let changed_buffers = action_log.read(cx).changed_buffers(cx);
 5389        let plan = thread.plan();
 5390        let queue_is_empty = !self.has_queued_messages();
 5391
 5392        if changed_buffers.is_empty() && plan.is_empty() && queue_is_empty {
 5393            return None;
 5394        }
 5395
 5396        // Temporarily always enable ACP edit controls. This is temporary, to lessen the
 5397        // impact of a nasty bug that causes them to sometimes be disabled when they shouldn't
 5398        // be, which blocks you from being able to accept or reject edits. This switches the
 5399        // bug to be that sometimes it's enabled when it shouldn't be, which at least doesn't
 5400        // block you from using the panel.
 5401        let pending_edits = false;
 5402
 5403        let Some(active) = self.as_active_thread() else {
 5404            return None;
 5405        };
 5406
 5407        v_flex()
 5408            .mt_1()
 5409            .mx_2()
 5410            .bg(self.activity_bar_bg(cx))
 5411            .border_1()
 5412            .border_b_0()
 5413            .border_color(cx.theme().colors().border)
 5414            .rounded_t_md()
 5415            .shadow(vec![gpui::BoxShadow {
 5416                color: gpui::black().opacity(0.15),
 5417                offset: point(px(1.), px(-1.)),
 5418                blur_radius: px(3.),
 5419                spread_radius: px(0.),
 5420            }])
 5421            .when(!plan.is_empty(), |this| {
 5422                this.child(self.render_plan_summary(plan, window, cx))
 5423                    .when(active.plan_expanded, |parent| {
 5424                        parent.child(self.render_plan_entries(plan, window, cx))
 5425                    })
 5426            })
 5427            .when(!plan.is_empty() && !changed_buffers.is_empty(), |this| {
 5428                this.child(Divider::horizontal().color(DividerColor::Border))
 5429            })
 5430            .when(!changed_buffers.is_empty(), |this| {
 5431                this.child(self.render_edits_summary(
 5432                    &changed_buffers,
 5433                    active.edits_expanded,
 5434                    pending_edits,
 5435                    cx,
 5436                ))
 5437                .when(active.edits_expanded, |parent| {
 5438                    parent.child(self.render_edited_files(
 5439                        action_log,
 5440                        telemetry.clone(),
 5441                        &changed_buffers,
 5442                        pending_edits,
 5443                        cx,
 5444                    ))
 5445                })
 5446            })
 5447            .when(!queue_is_empty, |this| {
 5448                this.when(!plan.is_empty() || !changed_buffers.is_empty(), |this| {
 5449                    this.child(Divider::horizontal().color(DividerColor::Border))
 5450                })
 5451                .child(self.render_message_queue_summary(window, cx))
 5452                .when(active.queue_expanded, |parent| {
 5453                    parent.child(self.render_message_queue_entries(window, cx))
 5454                })
 5455            })
 5456            .into_any()
 5457            .into()
 5458    }
 5459
 5460    fn render_plan_summary(
 5461        &self,
 5462        plan: &Plan,
 5463        window: &mut Window,
 5464        cx: &Context<Self>,
 5465    ) -> impl IntoElement {
 5466        let Some(active) = self.as_active_thread() else {
 5467            return Empty.into_any_element();
 5468        };
 5469        let stats = plan.stats();
 5470
 5471        let title = if let Some(entry) = stats.in_progress_entry
 5472            && !active.plan_expanded
 5473        {
 5474            h_flex()
 5475                .cursor_default()
 5476                .relative()
 5477                .w_full()
 5478                .gap_1()
 5479                .truncate()
 5480                .child(
 5481                    Label::new("Current:")
 5482                        .size(LabelSize::Small)
 5483                        .color(Color::Muted),
 5484                )
 5485                .child(
 5486                    div()
 5487                        .text_xs()
 5488                        .text_color(cx.theme().colors().text_muted)
 5489                        .line_clamp(1)
 5490                        .child(MarkdownElement::new(
 5491                            entry.content.clone(),
 5492                            plan_label_markdown_style(&entry.status, window, cx),
 5493                        )),
 5494                )
 5495                .when(stats.pending > 0, |this| {
 5496                    this.child(
 5497                        h_flex()
 5498                            .absolute()
 5499                            .top_0()
 5500                            .right_0()
 5501                            .h_full()
 5502                            .child(div().min_w_8().h_full().bg(linear_gradient(
 5503                                90.,
 5504                                linear_color_stop(self.activity_bar_bg(cx), 1.),
 5505                                linear_color_stop(self.activity_bar_bg(cx).opacity(0.2), 0.),
 5506                            )))
 5507                            .child(
 5508                                div().pr_0p5().bg(self.activity_bar_bg(cx)).child(
 5509                                    Label::new(format!("{} left", stats.pending))
 5510                                        .size(LabelSize::Small)
 5511                                        .color(Color::Muted),
 5512                                ),
 5513                            ),
 5514                    )
 5515                })
 5516        } else {
 5517            let status_label = if stats.pending == 0 {
 5518                "All Done".to_string()
 5519            } else if stats.completed == 0 {
 5520                format!("{} Tasks", plan.entries.len())
 5521            } else {
 5522                format!("{}/{}", stats.completed, plan.entries.len())
 5523            };
 5524
 5525            h_flex()
 5526                .w_full()
 5527                .gap_1()
 5528                .justify_between()
 5529                .child(
 5530                    Label::new("Plan")
 5531                        .size(LabelSize::Small)
 5532                        .color(Color::Muted),
 5533                )
 5534                .child(
 5535                    Label::new(status_label)
 5536                        .size(LabelSize::Small)
 5537                        .color(Color::Muted)
 5538                        .mr_1(),
 5539                )
 5540        };
 5541
 5542        h_flex()
 5543            .id("plan_summary")
 5544            .p_1()
 5545            .w_full()
 5546            .gap_1()
 5547            .when(active.plan_expanded, |this| {
 5548                this.border_b_1().border_color(cx.theme().colors().border)
 5549            })
 5550            .child(Disclosure::new("plan_disclosure", active.plan_expanded))
 5551            .child(title)
 5552            .on_click(cx.listener(|this, _, _, cx| {
 5553                let Some(active) = this.as_active_thread_mut() else {
 5554                    return;
 5555                };
 5556                active.plan_expanded = !active.plan_expanded;
 5557                cx.notify();
 5558            }))
 5559            .into_any_element()
 5560    }
 5561
 5562    fn render_plan_entries(
 5563        &self,
 5564        plan: &Plan,
 5565        window: &mut Window,
 5566        cx: &Context<Self>,
 5567    ) -> impl IntoElement {
 5568        v_flex()
 5569            .id("plan_items_list")
 5570            .max_h_40()
 5571            .overflow_y_scroll()
 5572            .children(plan.entries.iter().enumerate().flat_map(|(index, entry)| {
 5573                let element = h_flex()
 5574                    .py_1()
 5575                    .px_2()
 5576                    .gap_2()
 5577                    .justify_between()
 5578                    .bg(cx.theme().colors().editor_background)
 5579                    .when(index < plan.entries.len() - 1, |parent| {
 5580                        parent.border_color(cx.theme().colors().border).border_b_1()
 5581                    })
 5582                    .child(
 5583                        h_flex()
 5584                            .id(("plan_entry", index))
 5585                            .gap_1p5()
 5586                            .max_w_full()
 5587                            .overflow_x_scroll()
 5588                            .text_xs()
 5589                            .text_color(cx.theme().colors().text_muted)
 5590                            .child(match entry.status {
 5591                                acp::PlanEntryStatus::InProgress => {
 5592                                    Icon::new(IconName::TodoProgress)
 5593                                        .size(IconSize::Small)
 5594                                        .color(Color::Accent)
 5595                                        .with_rotate_animation(2)
 5596                                        .into_any_element()
 5597                                }
 5598                                acp::PlanEntryStatus::Completed => {
 5599                                    Icon::new(IconName::TodoComplete)
 5600                                        .size(IconSize::Small)
 5601                                        .color(Color::Success)
 5602                                        .into_any_element()
 5603                                }
 5604                                acp::PlanEntryStatus::Pending | _ => {
 5605                                    Icon::new(IconName::TodoPending)
 5606                                        .size(IconSize::Small)
 5607                                        .color(Color::Muted)
 5608                                        .into_any_element()
 5609                                }
 5610                            })
 5611                            .child(MarkdownElement::new(
 5612                                entry.content.clone(),
 5613                                plan_label_markdown_style(&entry.status, window, cx),
 5614                            )),
 5615                    );
 5616
 5617                Some(element)
 5618            }))
 5619            .into_any_element()
 5620    }
 5621
 5622    fn render_edits_summary(
 5623        &self,
 5624        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
 5625        expanded: bool,
 5626        pending_edits: bool,
 5627        cx: &Context<Self>,
 5628    ) -> Div {
 5629        const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete.";
 5630
 5631        let focus_handle = self.focus_handle(cx);
 5632
 5633        h_flex()
 5634            .p_1()
 5635            .justify_between()
 5636            .flex_wrap()
 5637            .when(expanded, |this| {
 5638                this.border_b_1().border_color(cx.theme().colors().border)
 5639            })
 5640            .child(
 5641                h_flex()
 5642                    .id("edits-container")
 5643                    .cursor_pointer()
 5644                    .gap_1()
 5645                    .child(Disclosure::new("edits-disclosure", expanded))
 5646                    .map(|this| {
 5647                        if pending_edits {
 5648                            this.child(
 5649                                Label::new(format!(
 5650                                    "Editing {} {}",
 5651                                    changed_buffers.len(),
 5652                                    if changed_buffers.len() == 1 {
 5653                                        "file"
 5654                                    } else {
 5655                                        "files"
 5656                                    }
 5657                                ))
 5658                                .color(Color::Muted)
 5659                                .size(LabelSize::Small)
 5660                                .with_animation(
 5661                                    "edit-label",
 5662                                    Animation::new(Duration::from_secs(2))
 5663                                        .repeat()
 5664                                        .with_easing(pulsating_between(0.3, 0.7)),
 5665                                    |label, delta| label.alpha(delta),
 5666                                ),
 5667                            )
 5668                        } else {
 5669                            let stats = DiffStats::all_files(changed_buffers, cx);
 5670                            let dot_divider = || {
 5671                                Label::new("")
 5672                                    .size(LabelSize::XSmall)
 5673                                    .color(Color::Disabled)
 5674                            };
 5675
 5676                            this.child(
 5677                                Label::new("Edits")
 5678                                    .size(LabelSize::Small)
 5679                                    .color(Color::Muted),
 5680                            )
 5681                            .child(dot_divider())
 5682                            .child(
 5683                                Label::new(format!(
 5684                                    "{} {}",
 5685                                    changed_buffers.len(),
 5686                                    if changed_buffers.len() == 1 {
 5687                                        "file"
 5688                                    } else {
 5689                                        "files"
 5690                                    }
 5691                                ))
 5692                                .size(LabelSize::Small)
 5693                                .color(Color::Muted),
 5694                            )
 5695                            .child(dot_divider())
 5696                            .child(DiffStat::new(
 5697                                "total",
 5698                                stats.lines_added as usize,
 5699                                stats.lines_removed as usize,
 5700                            ))
 5701                        }
 5702                    })
 5703                    .on_click(cx.listener(|this, _, _, cx| {
 5704                        let Some(active) = this.as_active_thread_mut() else {
 5705                            return;
 5706                        };
 5707                        active.edits_expanded = !active.edits_expanded;
 5708                        cx.notify();
 5709                    })),
 5710            )
 5711            .child(
 5712                h_flex()
 5713                    .gap_1()
 5714                    .child(
 5715                        IconButton::new("review-changes", IconName::ListTodo)
 5716                            .icon_size(IconSize::Small)
 5717                            .tooltip({
 5718                                let focus_handle = focus_handle.clone();
 5719                                move |_window, cx| {
 5720                                    Tooltip::for_action_in(
 5721                                        "Review Changes",
 5722                                        &OpenAgentDiff,
 5723                                        &focus_handle,
 5724                                        cx,
 5725                                    )
 5726                                }
 5727                            })
 5728                            .on_click(cx.listener(|_, _, window, cx| {
 5729                                window.dispatch_action(OpenAgentDiff.boxed_clone(), cx);
 5730                            })),
 5731                    )
 5732                    .child(Divider::vertical().color(DividerColor::Border))
 5733                    .child(
 5734                        Button::new("reject-all-changes", "Reject All")
 5735                            .label_size(LabelSize::Small)
 5736                            .disabled(pending_edits)
 5737                            .when(pending_edits, |this| {
 5738                                this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
 5739                            })
 5740                            .key_binding(
 5741                                KeyBinding::for_action_in(&RejectAll, &focus_handle.clone(), cx)
 5742                                    .map(|kb| kb.size(rems_from_px(10.))),
 5743                            )
 5744                            .on_click(cx.listener(move |this, _, window, cx| {
 5745                                this.reject_all(&RejectAll, window, cx);
 5746                            })),
 5747                    )
 5748                    .child(
 5749                        Button::new("keep-all-changes", "Keep All")
 5750                            .label_size(LabelSize::Small)
 5751                            .disabled(pending_edits)
 5752                            .when(pending_edits, |this| {
 5753                                this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
 5754                            })
 5755                            .key_binding(
 5756                                KeyBinding::for_action_in(&KeepAll, &focus_handle, cx)
 5757                                    .map(|kb| kb.size(rems_from_px(10.))),
 5758                            )
 5759                            .on_click(cx.listener(move |this, _, window, cx| {
 5760                                this.keep_all(&KeepAll, window, cx);
 5761                            })),
 5762                    ),
 5763            )
 5764    }
 5765
 5766    fn render_edited_files_buttons(
 5767        &self,
 5768        index: usize,
 5769        buffer: &Entity<Buffer>,
 5770        action_log: &Entity<ActionLog>,
 5771        telemetry: &ActionLogTelemetry,
 5772        pending_edits: bool,
 5773        editor_bg_color: Hsla,
 5774        cx: &Context<Self>,
 5775    ) -> impl IntoElement {
 5776        h_flex()
 5777            .id("edited-buttons-container")
 5778            .visible_on_hover("edited-code")
 5779            .absolute()
 5780            .right_0()
 5781            .px_1()
 5782            .gap_1()
 5783            .bg(editor_bg_color)
 5784            .on_hover(cx.listener(move |this, is_hovered, _window, cx| {
 5785                if let Some(active) = this.as_active_thread_mut() {
 5786                    if *is_hovered {
 5787                        active.hovered_edited_file_buttons = Some(index);
 5788                    } else if active.hovered_edited_file_buttons == Some(index) {
 5789                        active.hovered_edited_file_buttons = None;
 5790                    }
 5791                }
 5792                cx.notify();
 5793            }))
 5794            .child(
 5795                Button::new("review", "Review")
 5796                    .label_size(LabelSize::Small)
 5797                    .on_click({
 5798                        let buffer = buffer.clone();
 5799                        cx.listener(move |this, _, window, cx| {
 5800                            this.open_edited_buffer(&buffer, window, cx);
 5801                        })
 5802                    }),
 5803            )
 5804            .child(
 5805                Button::new(("reject-file", index), "Reject")
 5806                    .label_size(LabelSize::Small)
 5807                    .disabled(pending_edits)
 5808                    .on_click({
 5809                        let buffer = buffer.clone();
 5810                        let action_log = action_log.clone();
 5811                        let telemetry = telemetry.clone();
 5812                        move |_, _, cx| {
 5813                            action_log.update(cx, |action_log, cx| {
 5814                                action_log
 5815                                    .reject_edits_in_ranges(
 5816                                        buffer.clone(),
 5817                                        vec![Anchor::min_max_range_for_buffer(
 5818                                            buffer.read(cx).remote_id(),
 5819                                        )],
 5820                                        Some(telemetry.clone()),
 5821                                        cx,
 5822                                    )
 5823                                    .detach_and_log_err(cx);
 5824                            })
 5825                        }
 5826                    }),
 5827            )
 5828            .child(
 5829                Button::new(("keep-file", index), "Keep")
 5830                    .label_size(LabelSize::Small)
 5831                    .disabled(pending_edits)
 5832                    .on_click({
 5833                        let buffer = buffer.clone();
 5834                        let action_log = action_log.clone();
 5835                        let telemetry = telemetry.clone();
 5836                        move |_, _, cx| {
 5837                            action_log.update(cx, |action_log, cx| {
 5838                                action_log.keep_edits_in_range(
 5839                                    buffer.clone(),
 5840                                    Anchor::min_max_range_for_buffer(buffer.read(cx).remote_id()),
 5841                                    Some(telemetry.clone()),
 5842                                    cx,
 5843                                );
 5844                            })
 5845                        }
 5846                    }),
 5847            )
 5848    }
 5849
 5850    fn render_edited_files(
 5851        &self,
 5852        action_log: &Entity<ActionLog>,
 5853        telemetry: ActionLogTelemetry,
 5854        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
 5855        pending_edits: bool,
 5856        cx: &Context<Self>,
 5857    ) -> impl IntoElement {
 5858        let editor_bg_color = cx.theme().colors().editor_background;
 5859
 5860        // Sort edited files alphabetically for consistency with Git diff view
 5861        let mut sorted_buffers: Vec<_> = changed_buffers.iter().collect();
 5862        sorted_buffers.sort_by(|(buffer_a, _), (buffer_b, _)| {
 5863            let path_a = buffer_a.read(cx).file().map(|f| f.path().clone());
 5864            let path_b = buffer_b.read(cx).file().map(|f| f.path().clone());
 5865            path_a.cmp(&path_b)
 5866        });
 5867
 5868        v_flex()
 5869            .id("edited_files_list")
 5870            .max_h_40()
 5871            .overflow_y_scroll()
 5872            .children(
 5873                sorted_buffers
 5874                    .into_iter()
 5875                    .enumerate()
 5876                    .flat_map(|(index, (buffer, diff))| {
 5877                        let file = buffer.read(cx).file()?;
 5878                        let path = file.path();
 5879                        let path_style = file.path_style(cx);
 5880                        let separator = file.path_style(cx).primary_separator();
 5881
 5882                        let file_path = path.parent().and_then(|parent| {
 5883                            if parent.is_empty() {
 5884                                None
 5885                            } else {
 5886                                Some(
 5887                                    Label::new(format!(
 5888                                        "{}{separator}",
 5889                                        parent.display(path_style)
 5890                                    ))
 5891                                    .color(Color::Muted)
 5892                                    .size(LabelSize::XSmall)
 5893                                    .buffer_font(cx),
 5894                                )
 5895                            }
 5896                        });
 5897
 5898                        let file_name = path.file_name().map(|name| {
 5899                            Label::new(name.to_string())
 5900                                .size(LabelSize::XSmall)
 5901                                .buffer_font(cx)
 5902                                .ml_1()
 5903                        });
 5904
 5905                        let full_path = path.display(path_style).to_string();
 5906
 5907                        let file_icon = FileIcons::get_icon(path.as_std_path(), cx)
 5908                            .map(Icon::from_path)
 5909                            .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
 5910                            .unwrap_or_else(|| {
 5911                                Icon::new(IconName::File)
 5912                                    .color(Color::Muted)
 5913                                    .size(IconSize::Small)
 5914                            });
 5915
 5916                        let file_stats = DiffStats::single_file(buffer.read(cx), diff.read(cx), cx);
 5917
 5918                        let buttons = self.render_edited_files_buttons(
 5919                            index,
 5920                            buffer,
 5921                            action_log,
 5922                            &telemetry,
 5923                            pending_edits,
 5924                            editor_bg_color,
 5925                            cx,
 5926                        );
 5927
 5928                        let element = h_flex()
 5929                            .group("edited-code")
 5930                            .id(("file-container", index))
 5931                            .relative()
 5932                            .min_w_0()
 5933                            .p_1p5()
 5934                            .gap_2()
 5935                            .justify_between()
 5936                            .bg(editor_bg_color)
 5937                            .when(index < changed_buffers.len() - 1, |parent| {
 5938                                parent.border_color(cx.theme().colors().border).border_b_1()
 5939                            })
 5940                            .child(
 5941                                h_flex()
 5942                                    .id(("file-name-path", index))
 5943                                    .cursor_pointer()
 5944                                    .pr_0p5()
 5945                                    .gap_0p5()
 5946                                    .rounded_xs()
 5947                                    .child(file_icon)
 5948                                    .children(file_name)
 5949                                    .children(file_path)
 5950                                    .child(
 5951                                        DiffStat::new(
 5952                                            "file",
 5953                                            file_stats.lines_added as usize,
 5954                                            file_stats.lines_removed as usize,
 5955                                        )
 5956                                        .label_size(LabelSize::XSmall),
 5957                                    )
 5958                                    .hover(|s| s.bg(cx.theme().colors().element_hover))
 5959                                    .tooltip({
 5960                                        move |_, cx| {
 5961                                            Tooltip::with_meta(
 5962                                                "Go to File",
 5963                                                None,
 5964                                                full_path.clone(),
 5965                                                cx,
 5966                                            )
 5967                                        }
 5968                                    })
 5969                                    .on_click({
 5970                                        let buffer = buffer.clone();
 5971                                        cx.listener(move |this, _, window, cx| {
 5972                                            this.open_edited_buffer(&buffer, window, cx);
 5973                                        })
 5974                                    }),
 5975                            )
 5976                            .child(buttons);
 5977
 5978                        Some(element)
 5979                    }),
 5980            )
 5981            .into_any_element()
 5982    }
 5983
 5984    fn render_message_queue_summary(
 5985        &self,
 5986        _window: &mut Window,
 5987        cx: &Context<Self>,
 5988    ) -> impl IntoElement {
 5989        let queue_count = self.queued_messages_len();
 5990        let title: SharedString = if queue_count == 1 {
 5991            "1 Queued Message".into()
 5992        } else {
 5993            format!("{} Queued Messages", queue_count).into()
 5994        };
 5995
 5996        let Some(active) = self.as_active_thread() else {
 5997            return Empty.into_any_element();
 5998        };
 5999
 6000        h_flex()
 6001            .p_1()
 6002            .w_full()
 6003            .gap_1()
 6004            .justify_between()
 6005            .when(active.queue_expanded, |this| {
 6006                this.border_b_1().border_color(cx.theme().colors().border)
 6007            })
 6008            .child(
 6009                h_flex()
 6010                    .id("queue_summary")
 6011                    .gap_1()
 6012                    .child(Disclosure::new("queue_disclosure", active.queue_expanded))
 6013                    .child(Label::new(title).size(LabelSize::Small).color(Color::Muted))
 6014                    .on_click(cx.listener(|this, _, _, cx| {
 6015                        let Some(active) = this.as_active_thread_mut() else {
 6016                            return;
 6017                        };
 6018                        active.queue_expanded = !active.queue_expanded;
 6019                        cx.notify();
 6020                    })),
 6021            )
 6022            .child(
 6023                Button::new("clear_queue", "Clear All")
 6024                    .label_size(LabelSize::Small)
 6025                    .key_binding(KeyBinding::for_action(&ClearMessageQueue, cx))
 6026                    .on_click(cx.listener(|this, _, _, cx| {
 6027                        this.clear_queue(cx);
 6028                        if let Some(active) = this.as_active_thread_mut() {
 6029                            active.can_fast_track_queue = false;
 6030                        }
 6031                        cx.notify();
 6032                    })),
 6033            )
 6034            .into_any_element()
 6035    }
 6036
 6037    fn render_message_queue_entries(
 6038        &self,
 6039        _window: &mut Window,
 6040        cx: &Context<Self>,
 6041    ) -> impl IntoElement {
 6042        let message_editor = self.message_editor.read(cx);
 6043        let focus_handle = message_editor.focus_handle(cx);
 6044
 6045        let queued_message_editors = self
 6046            .as_connected()
 6047            .and_then(|c| c.current.as_ref())
 6048            .map(|current| current.queued_message_editors.as_slice())
 6049            .unwrap_or(&[]);
 6050
 6051        let queue_len = queued_message_editors.len();
 6052        let can_fast_track = if let Some(active) = self.as_active_thread() {
 6053            active.can_fast_track_queue && queue_len > 0
 6054        } else {
 6055            false
 6056        };
 6057
 6058        v_flex()
 6059            .id("message_queue_list")
 6060            .max_h_40()
 6061            .overflow_y_scroll()
 6062            .children(
 6063                queued_message_editors
 6064                    .iter()
 6065                    .enumerate()
 6066                    .map(|(index, editor)| {
 6067                        let is_next = index == 0;
 6068                        let (icon_color, tooltip_text) = if is_next {
 6069                            (Color::Accent, "Next in Queue")
 6070                        } else {
 6071                            (Color::Muted, "In Queue")
 6072                        };
 6073
 6074                        let editor_focused = editor.focus_handle(cx).is_focused(_window);
 6075                        let keybinding_size = rems_from_px(12.);
 6076
 6077                        h_flex()
 6078                            .group("queue_entry")
 6079                            .w_full()
 6080                            .p_1p5()
 6081                            .gap_1()
 6082                            .bg(cx.theme().colors().editor_background)
 6083                            .when(index < queue_len - 1, |this| {
 6084                                this.border_b_1()
 6085                                    .border_color(cx.theme().colors().border_variant)
 6086                            })
 6087                            .child(
 6088                                div()
 6089                                    .id("next_in_queue")
 6090                                    .child(
 6091                                        Icon::new(IconName::Circle)
 6092                                            .size(IconSize::Small)
 6093                                            .color(icon_color),
 6094                                    )
 6095                                    .tooltip(Tooltip::text(tooltip_text)),
 6096                            )
 6097                            .child(editor.clone())
 6098                            .child(if editor_focused {
 6099                                h_flex()
 6100                                    .gap_1()
 6101                                    .min_w_40()
 6102                                    .child(
 6103                                        IconButton::new(("cancel_edit", index), IconName::Close)
 6104                                            .icon_size(IconSize::Small)
 6105                                            .icon_color(Color::Error)
 6106                                            .tooltip({
 6107                                                let focus_handle = editor.focus_handle(cx);
 6108                                                move |_window, cx| {
 6109                                                    Tooltip::for_action_in(
 6110                                                        "Cancel Edit",
 6111                                                        &editor::actions::Cancel,
 6112                                                        &focus_handle,
 6113                                                        cx,
 6114                                                    )
 6115                                                }
 6116                                            })
 6117                                            .on_click({
 6118                                                let main_editor = self.message_editor.clone();
 6119                                                cx.listener(move |_, _, window, cx| {
 6120                                                    window.focus(&main_editor.focus_handle(cx), cx);
 6121                                                })
 6122                                            }),
 6123                                    )
 6124                                    .child(
 6125                                        IconButton::new(("save_edit", index), IconName::Check)
 6126                                            .icon_size(IconSize::Small)
 6127                                            .icon_color(Color::Success)
 6128                                            .tooltip({
 6129                                                let focus_handle = editor.focus_handle(cx);
 6130                                                move |_window, cx| {
 6131                                                    Tooltip::for_action_in(
 6132                                                        "Save Edit",
 6133                                                        &Chat,
 6134                                                        &focus_handle,
 6135                                                        cx,
 6136                                                    )
 6137                                                }
 6138                                            })
 6139                                            .on_click({
 6140                                                let main_editor = self.message_editor.clone();
 6141                                                cx.listener(move |_, _, window, cx| {
 6142                                                    window.focus(&main_editor.focus_handle(cx), cx);
 6143                                                })
 6144                                            }),
 6145                                    )
 6146                                    .child(
 6147                                        Button::new(("send_now_focused", index), "Send Now")
 6148                                            .label_size(LabelSize::Small)
 6149                                            .style(ButtonStyle::Outlined)
 6150                                            .key_binding(
 6151                                                KeyBinding::for_action_in(
 6152                                                    &SendImmediately,
 6153                                                    &editor.focus_handle(cx),
 6154                                                    cx,
 6155                                                )
 6156                                                .map(|kb| kb.size(keybinding_size)),
 6157                                            )
 6158                                            .on_click(cx.listener(move |this, _, window, cx| {
 6159                                                this.send_queued_message_at_index(
 6160                                                    index, true, window, cx,
 6161                                                );
 6162                                            })),
 6163                                    )
 6164                            } else {
 6165                                h_flex()
 6166                                    .gap_1()
 6167                                    .when(!is_next, |this| this.visible_on_hover("queue_entry"))
 6168                                    .child(
 6169                                        IconButton::new(("edit", index), IconName::Pencil)
 6170                                            .icon_size(IconSize::Small)
 6171                                            .tooltip({
 6172                                                let focus_handle = focus_handle.clone();
 6173                                                move |_window, cx| {
 6174                                                    if is_next {
 6175                                                        Tooltip::for_action_in(
 6176                                                            "Edit",
 6177                                                            &EditFirstQueuedMessage,
 6178                                                            &focus_handle,
 6179                                                            cx,
 6180                                                        )
 6181                                                    } else {
 6182                                                        Tooltip::simple("Edit", cx)
 6183                                                    }
 6184                                                }
 6185                                            })
 6186                                            .on_click({
 6187                                                let editor = editor.clone();
 6188                                                cx.listener(move |_, _, window, cx| {
 6189                                                    window.focus(&editor.focus_handle(cx), cx);
 6190                                                })
 6191                                            }),
 6192                                    )
 6193                                    .child(
 6194                                        IconButton::new(("delete", index), IconName::Trash)
 6195                                            .icon_size(IconSize::Small)
 6196                                            .tooltip({
 6197                                                let focus_handle = focus_handle.clone();
 6198                                                move |_window, cx| {
 6199                                                    if is_next {
 6200                                                        Tooltip::for_action_in(
 6201                                                            "Remove Message from Queue",
 6202                                                            &RemoveFirstQueuedMessage,
 6203                                                            &focus_handle,
 6204                                                            cx,
 6205                                                        )
 6206                                                    } else {
 6207                                                        Tooltip::simple(
 6208                                                            "Remove Message from Queue",
 6209                                                            cx,
 6210                                                        )
 6211                                                    }
 6212                                                }
 6213                                            })
 6214                                            .on_click(cx.listener(move |this, _, _, cx| {
 6215                                                this.remove_from_queue(index, cx);
 6216                                                cx.notify();
 6217                                            })),
 6218                                    )
 6219                                    .child(
 6220                                        Button::new(("send_now", index), "Send Now")
 6221                                            .label_size(LabelSize::Small)
 6222                                            .when(is_next && message_editor.is_empty(cx), |this| {
 6223                                                let action: Box<dyn gpui::Action> =
 6224                                                    if can_fast_track {
 6225                                                        Box::new(Chat)
 6226                                                    } else {
 6227                                                        Box::new(SendNextQueuedMessage)
 6228                                                    };
 6229
 6230                                                this.style(ButtonStyle::Outlined).key_binding(
 6231                                                    KeyBinding::for_action_in(
 6232                                                        action.as_ref(),
 6233                                                        &focus_handle.clone(),
 6234                                                        cx,
 6235                                                    )
 6236                                                    .map(|kb| kb.size(keybinding_size)),
 6237                                                )
 6238                                            })
 6239                                            .when(is_next && !message_editor.is_empty(cx), |this| {
 6240                                                this.style(ButtonStyle::Outlined)
 6241                                            })
 6242                                            .on_click(cx.listener(move |this, _, window, cx| {
 6243                                                this.send_queued_message_at_index(
 6244                                                    index, true, window, cx,
 6245                                                );
 6246                                            })),
 6247                                    )
 6248                            })
 6249                    }),
 6250            )
 6251            .into_any_element()
 6252    }
 6253
 6254    fn render_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
 6255        let focus_handle = self.message_editor.focus_handle(cx);
 6256        let editor_bg_color = cx.theme().colors().editor_background;
 6257        let editor_expanded = self
 6258            .as_active_thread()
 6259            .is_some_and(|active| active.editor_expanded);
 6260        let (expand_icon, expand_tooltip) = if editor_expanded {
 6261            (IconName::Minimize, "Minimize Message Editor")
 6262        } else {
 6263            (IconName::Maximize, "Expand Message Editor")
 6264        };
 6265
 6266        let backdrop = div()
 6267            .size_full()
 6268            .absolute()
 6269            .inset_0()
 6270            .bg(cx.theme().colors().panel_background)
 6271            .opacity(0.8)
 6272            .block_mouse_except_scroll();
 6273
 6274        let enable_editor = self
 6275            .as_connected()
 6276            .is_some_and(|conn| conn.auth_state.is_ok());
 6277
 6278        v_flex()
 6279            .on_action(cx.listener(Self::expand_message_editor))
 6280            .p_2()
 6281            .gap_2()
 6282            .border_t_1()
 6283            .border_color(cx.theme().colors().border)
 6284            .bg(editor_bg_color)
 6285            .when(editor_expanded, |this| {
 6286                this.h(vh(0.8, window)).size_full().justify_between()
 6287            })
 6288            .child(
 6289                v_flex()
 6290                    .relative()
 6291                    .size_full()
 6292                    .pt_1()
 6293                    .pr_2p5()
 6294                    .child(self.message_editor.clone())
 6295                    .child(
 6296                        h_flex()
 6297                            .absolute()
 6298                            .top_0()
 6299                            .right_0()
 6300                            .opacity(0.5)
 6301                            .hover(|this| this.opacity(1.0))
 6302                            .child(
 6303                                IconButton::new("toggle-height", expand_icon)
 6304                                    .icon_size(IconSize::Small)
 6305                                    .icon_color(Color::Muted)
 6306                                    .tooltip({
 6307                                        move |_window, cx| {
 6308                                            Tooltip::for_action_in(
 6309                                                expand_tooltip,
 6310                                                &ExpandMessageEditor,
 6311                                                &focus_handle,
 6312                                                cx,
 6313                                            )
 6314                                        }
 6315                                    })
 6316                                    .on_click(cx.listener(|this, _, window, cx| {
 6317                                        this.expand_message_editor(
 6318                                            &ExpandMessageEditor,
 6319                                            window,
 6320                                            cx,
 6321                                        );
 6322                                    })),
 6323                            ),
 6324                    ),
 6325            )
 6326            .child(
 6327                h_flex()
 6328                    .flex_none()
 6329                    .flex_wrap()
 6330                    .justify_between()
 6331                    .child(
 6332                        h_flex()
 6333                            .gap_0p5()
 6334                            .child(self.render_add_context_button(cx))
 6335                            .child(self.render_follow_toggle(cx))
 6336                            .children(self.render_thinking_toggle(cx)),
 6337                    )
 6338                    .child(
 6339                        h_flex()
 6340                            .gap_1()
 6341                            .children(self.render_token_usage(cx))
 6342                            .when_some(self.as_active_thread(), |this, active| {
 6343                                this.children(active.profile_selector.clone()).map(|this| {
 6344                                    // Either config_options_view OR (mode_selector + model_selector)
 6345                                    match active.config_options_view.clone() {
 6346                                        Some(config_view) => this.child(config_view),
 6347                                        None => this
 6348                                            .children(active.mode_selector.clone())
 6349                                            .children(active.model_selector.clone()),
 6350                                    }
 6351                                })
 6352                            })
 6353                            .child(self.render_send_button(cx)),
 6354                    ),
 6355            )
 6356            .when(!enable_editor, |this| this.child(backdrop))
 6357            .into_any()
 6358    }
 6359
 6360    pub(crate) fn as_native_connection(
 6361        &self,
 6362        cx: &App,
 6363    ) -> Option<Rc<agent::NativeAgentConnection>> {
 6364        let acp_thread = self.as_active_thread()?.thread.read(cx);
 6365        acp_thread.connection().clone().downcast()
 6366    }
 6367
 6368    pub(crate) fn as_native_thread(&self, cx: &App) -> Option<Entity<agent::Thread>> {
 6369        let acp_thread = self.as_active_thread()?.thread.read(cx);
 6370        self.as_native_connection(cx)?
 6371            .thread(acp_thread.session_id(), cx)
 6372    }
 6373
 6374    fn queued_messages_len(&self) -> usize {
 6375        self.as_active_thread()
 6376            .map(|thread| thread.local_queued_messages.len())
 6377            .unwrap_or_default()
 6378    }
 6379
 6380    fn has_queued_messages(&self) -> bool {
 6381        self.as_active_thread()
 6382            .map(|active| active.has_queued_messages())
 6383            .unwrap_or(false)
 6384    }
 6385
 6386    /// Syncs the has_queued_message flag to the native thread (if applicable).
 6387    /// This flag tells the native thread to end its turn at the next message boundary.
 6388    fn sync_queue_flag_to_native_thread(&self, cx: &mut Context<Self>) {
 6389        if let Some(active) = self.as_active_thread() {
 6390            active.sync_queue_flag_to_native_thread(cx);
 6391        }
 6392    }
 6393
 6394    fn add_to_queue(
 6395        &mut self,
 6396        content: Vec<acp::ContentBlock>,
 6397        tracked_buffers: Vec<Entity<Buffer>>,
 6398        cx: &mut Context<Self>,
 6399    ) {
 6400        if let Some(active) = self.as_active_thread_mut() {
 6401            active.local_queued_messages.push(QueuedMessage {
 6402                content,
 6403                tracked_buffers,
 6404            });
 6405        }
 6406        self.sync_queue_flag_to_native_thread(cx);
 6407    }
 6408
 6409    fn remove_from_queue(&mut self, index: usize, cx: &mut Context<Self>) -> Option<QueuedMessage> {
 6410        self.as_active_thread_mut()
 6411            .and_then(|active| active.remove_from_queue(index, cx))
 6412    }
 6413
 6414    fn update_queued_message(
 6415        &mut self,
 6416        index: usize,
 6417        content: Vec<acp::ContentBlock>,
 6418        tracked_buffers: Vec<Entity<Buffer>>,
 6419        _cx: &mut Context<Self>,
 6420    ) -> bool {
 6421        match self.as_active_thread_mut() {
 6422            Some(thread) if index < thread.local_queued_messages.len() => {
 6423                thread.local_queued_messages[index] = QueuedMessage {
 6424                    content,
 6425                    tracked_buffers,
 6426                };
 6427                true
 6428            }
 6429            Some(_) | None => false,
 6430        }
 6431    }
 6432
 6433    fn clear_queue(&mut self, cx: &mut Context<Self>) {
 6434        if let Some(active) = self.as_active_thread_mut() {
 6435            active.local_queued_messages.clear();
 6436        }
 6437        self.sync_queue_flag_to_native_thread(cx);
 6438    }
 6439
 6440    fn queued_message_contents(&self) -> Vec<Vec<acp::ContentBlock>> {
 6441        match self.as_active_thread() {
 6442            None => Vec::new(),
 6443            Some(thread) => thread
 6444                .local_queued_messages
 6445                .iter()
 6446                .map(|q| q.content.clone())
 6447                .collect(),
 6448        }
 6449    }
 6450
 6451    fn save_queued_message_at_index(&mut self, index: usize, cx: &mut Context<Self>) {
 6452        let editor = match self.as_active_thread() {
 6453            Some(thread) => thread.queued_message_editors.get(index).cloned(),
 6454            None => None,
 6455        };
 6456        let Some(editor) = editor else {
 6457            return;
 6458        };
 6459
 6460        let contents_task = editor.update(cx, |editor, cx| editor.contents(false, cx));
 6461
 6462        cx.spawn(async move |this, cx| {
 6463            let Ok((content, tracked_buffers)) = contents_task.await else {
 6464                return Ok::<(), anyhow::Error>(());
 6465            };
 6466
 6467            this.update(cx, |this, cx| {
 6468                this.update_queued_message(index, content, tracked_buffers, cx);
 6469                cx.notify();
 6470            })?;
 6471
 6472            Ok(())
 6473        })
 6474        .detach_and_log_err(cx);
 6475    }
 6476
 6477    fn sync_queued_message_editors(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 6478        let needed_count = self.queued_messages_len();
 6479        let queued_messages = self.queued_message_contents();
 6480
 6481        let agent_name = self.agent.name();
 6482        let workspace = self.workspace.clone();
 6483        let project = self.project.downgrade();
 6484        let history = self.history.downgrade();
 6485        let message_editor = self.message_editor.clone();
 6486
 6487        let Some(thread) = self.as_active_thread_mut() else {
 6488            return;
 6489        };
 6490        let prompt_capabilities = thread.prompt_capabilities.clone();
 6491        let available_commands = thread.available_commands.clone();
 6492
 6493        let current_count = thread.queued_message_editors.len();
 6494
 6495        if current_count == needed_count && needed_count == thread.last_synced_queue_length {
 6496            return;
 6497        }
 6498
 6499        if current_count > needed_count {
 6500            thread.queued_message_editors.truncate(needed_count);
 6501            thread
 6502                .queued_message_editor_subscriptions
 6503                .truncate(needed_count);
 6504
 6505            for (index, editor) in thread.queued_message_editors.iter().enumerate() {
 6506                if let Some(content) = queued_messages.get(index) {
 6507                    editor.update(cx, |editor, cx| {
 6508                        editor.set_message(content.clone(), window, cx);
 6509                    });
 6510                }
 6511            }
 6512        }
 6513
 6514        while thread.queued_message_editors.len() < needed_count {
 6515            let index = thread.queued_message_editors.len();
 6516            let content = queued_messages.get(index).cloned().unwrap_or_default();
 6517
 6518            let editor = cx.new(|cx| {
 6519                let mut editor = MessageEditor::new(
 6520                    workspace.clone(),
 6521                    project.clone(),
 6522                    None,
 6523                    history.clone(),
 6524                    None,
 6525                    prompt_capabilities.clone(),
 6526                    available_commands.clone(),
 6527                    agent_name.clone(),
 6528                    "",
 6529                    EditorMode::AutoHeight {
 6530                        min_lines: 1,
 6531                        max_lines: Some(10),
 6532                    },
 6533                    window,
 6534                    cx,
 6535                );
 6536                editor.set_message(content, window, cx);
 6537                editor
 6538            });
 6539
 6540            let message_editor = message_editor.clone();
 6541            let subscription = cx.subscribe_in(
 6542                &editor,
 6543                window,
 6544                move |this, _editor, event, window, cx| match event {
 6545                    MessageEditorEvent::LostFocus => {
 6546                        this.save_queued_message_at_index(index, cx);
 6547                    }
 6548                    MessageEditorEvent::Cancel => {
 6549                        window.focus(&message_editor.focus_handle(cx), cx);
 6550                    }
 6551                    MessageEditorEvent::Send => {
 6552                        window.focus(&message_editor.focus_handle(cx), cx);
 6553                    }
 6554                    MessageEditorEvent::SendImmediately => {
 6555                        this.send_queued_message_at_index(index, true, window, cx);
 6556                    }
 6557                    _ => {}
 6558                },
 6559            );
 6560
 6561            thread.queued_message_editors.push(editor);
 6562            thread
 6563                .queued_message_editor_subscriptions
 6564                .push(subscription);
 6565        }
 6566
 6567        if let Some(active) = self.as_active_thread_mut() {
 6568            active.last_synced_queue_length = needed_count;
 6569        }
 6570    }
 6571
 6572    fn is_imported_thread(&self, cx: &App) -> bool {
 6573        if let Some(active) = self.as_active_thread() {
 6574            active.is_imported_thread(cx)
 6575        } else {
 6576            false
 6577        }
 6578    }
 6579
 6580    fn supports_split_token_display(&self, cx: &App) -> bool {
 6581        self.as_native_thread(cx)
 6582            .and_then(|thread| thread.read(cx).model())
 6583            .is_some_and(|model| model.supports_split_token_display())
 6584    }
 6585
 6586    fn render_token_usage(&self, cx: &mut Context<Self>) -> Option<Div> {
 6587        let active = self.as_active_thread()?;
 6588        let thread = active.thread.read(cx);
 6589        let usage = thread.token_usage()?;
 6590        let is_generating = thread.status() != ThreadStatus::Idle;
 6591        let show_split = self.supports_split_token_display(cx);
 6592
 6593        let separator_color = Color::Custom(cx.theme().colors().text_muted.opacity(0.5));
 6594        let token_label = |text: String, animation_id: &'static str| {
 6595            Label::new(text)
 6596                .size(LabelSize::Small)
 6597                .color(Color::Muted)
 6598                .map(|label| {
 6599                    if is_generating {
 6600                        label
 6601                            .with_animation(
 6602                                animation_id,
 6603                                Animation::new(Duration::from_secs(2))
 6604                                    .repeat()
 6605                                    .with_easing(pulsating_between(0.3, 0.8)),
 6606                                |label, delta| label.alpha(delta),
 6607                            )
 6608                            .into_any()
 6609                    } else {
 6610                        label.into_any_element()
 6611                    }
 6612                })
 6613        };
 6614
 6615        if show_split {
 6616            let max_output_tokens = self
 6617                .as_native_thread(cx)
 6618                .and_then(|thread| thread.read(cx).model())
 6619                .and_then(|model| model.max_output_tokens())
 6620                .unwrap_or(0);
 6621
 6622            let input = crate::text_thread_editor::humanize_token_count(usage.input_tokens);
 6623            let input_max = crate::text_thread_editor::humanize_token_count(
 6624                usage.max_tokens.saturating_sub(max_output_tokens),
 6625            );
 6626            let output = crate::text_thread_editor::humanize_token_count(usage.output_tokens);
 6627            let output_max = crate::text_thread_editor::humanize_token_count(max_output_tokens);
 6628
 6629            Some(
 6630                h_flex()
 6631                    .flex_shrink_0()
 6632                    .gap_1()
 6633                    .mr_1p5()
 6634                    .child(
 6635                        h_flex()
 6636                            .gap_0p5()
 6637                            .child(
 6638                                Icon::new(IconName::ArrowUp)
 6639                                    .size(IconSize::XSmall)
 6640                                    .color(Color::Muted),
 6641                            )
 6642                            .child(token_label(input, "input-tokens-label"))
 6643                            .child(
 6644                                Label::new("/")
 6645                                    .size(LabelSize::Small)
 6646                                    .color(separator_color),
 6647                            )
 6648                            .child(
 6649                                Label::new(input_max)
 6650                                    .size(LabelSize::Small)
 6651                                    .color(Color::Muted),
 6652                            ),
 6653                    )
 6654                    .child(
 6655                        h_flex()
 6656                            .gap_0p5()
 6657                            .child(
 6658                                Icon::new(IconName::ArrowDown)
 6659                                    .size(IconSize::XSmall)
 6660                                    .color(Color::Muted),
 6661                            )
 6662                            .child(token_label(output, "output-tokens-label"))
 6663                            .child(
 6664                                Label::new("/")
 6665                                    .size(LabelSize::Small)
 6666                                    .color(separator_color),
 6667                            )
 6668                            .child(
 6669                                Label::new(output_max)
 6670                                    .size(LabelSize::Small)
 6671                                    .color(Color::Muted),
 6672                            ),
 6673                    ),
 6674            )
 6675        } else {
 6676            let used = crate::text_thread_editor::humanize_token_count(usage.used_tokens);
 6677            let max = crate::text_thread_editor::humanize_token_count(usage.max_tokens);
 6678
 6679            Some(
 6680                h_flex()
 6681                    .flex_shrink_0()
 6682                    .gap_0p5()
 6683                    .mr_1p5()
 6684                    .child(token_label(used, "used-tokens-label"))
 6685                    .child(
 6686                        Label::new("/")
 6687                            .size(LabelSize::Small)
 6688                            .color(separator_color),
 6689                    )
 6690                    .child(Label::new(max).size(LabelSize::Small).color(Color::Muted)),
 6691            )
 6692        }
 6693    }
 6694
 6695    fn render_thinking_toggle(&self, cx: &mut Context<Self>) -> Option<IconButton> {
 6696        if !cx.has_flag::<CloudThinkingToggleFeatureFlag>() {
 6697            return None;
 6698        }
 6699
 6700        let thread = self.as_native_thread(cx)?.read(cx);
 6701
 6702        let supports_thinking = thread.model()?.supports_thinking();
 6703        if !supports_thinking {
 6704            return None;
 6705        }
 6706
 6707        let thinking = thread.thinking_enabled();
 6708
 6709        let (tooltip_label, icon) = if thinking {
 6710            ("Disable Thinking Mode", IconName::ThinkingMode)
 6711        } else {
 6712            ("Enable Thinking Mode", IconName::ToolThink)
 6713        };
 6714
 6715        let focus_handle = self.message_editor.focus_handle(cx);
 6716
 6717        Some(
 6718            IconButton::new("thinking-mode", icon)
 6719                .icon_size(IconSize::Small)
 6720                .icon_color(Color::Muted)
 6721                .toggle_state(thinking)
 6722                .tooltip(move |_, cx| {
 6723                    Tooltip::for_action_in(tooltip_label, &ToggleThinkingMode, &focus_handle, cx)
 6724                })
 6725                .on_click(cx.listener(move |this, _, _window, cx| {
 6726                    if let Some(thread) = this.as_native_thread(cx) {
 6727                        thread.update(cx, |thread, cx| {
 6728                            thread.set_thinking_enabled(!thread.thinking_enabled(), cx);
 6729                        });
 6730                    }
 6731                })),
 6732        )
 6733    }
 6734
 6735    fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context<Self>) {
 6736        if let Some(active) = self.as_active_thread_mut() {
 6737            active.keep_all(cx);
 6738        };
 6739    }
 6740
 6741    fn reject_all(&mut self, _: &RejectAll, _window: &mut Window, cx: &mut Context<Self>) {
 6742        if let Some(active) = self.as_active_thread_mut() {
 6743            active.reject_all(cx);
 6744        };
 6745    }
 6746
 6747    fn allow_always(&mut self, _: &AllowAlways, window: &mut Window, cx: &mut Context<Self>) {
 6748        self.authorize_pending_tool_call(acp::PermissionOptionKind::AllowAlways, window, cx);
 6749    }
 6750
 6751    fn allow_once(&mut self, _: &AllowOnce, window: &mut Window, cx: &mut Context<Self>) {
 6752        self.authorize_pending_with_granularity(true, window, cx);
 6753    }
 6754
 6755    fn reject_once(&mut self, _: &RejectOnce, window: &mut Window, cx: &mut Context<Self>) {
 6756        self.authorize_pending_with_granularity(false, window, cx);
 6757    }
 6758
 6759    fn authorize_pending_with_granularity(
 6760        &mut self,
 6761        is_allow: bool,
 6762        window: &mut Window,
 6763        cx: &mut Context<Self>,
 6764    ) -> Option<()> {
 6765        let active = self.as_active_thread()?;
 6766        let thread = active.thread.read(cx);
 6767        let tool_call = thread.first_tool_awaiting_confirmation()?;
 6768        let ToolCallStatus::WaitingForConfirmation { options, .. } = &tool_call.status else {
 6769            return None;
 6770        };
 6771        let tool_call_id = tool_call.id.clone();
 6772
 6773        let PermissionOptions::Dropdown(choices) = options else {
 6774            let kind = if is_allow {
 6775                acp::PermissionOptionKind::AllowOnce
 6776            } else {
 6777                acp::PermissionOptionKind::RejectOnce
 6778            };
 6779            return self.authorize_pending_tool_call(kind, window, cx);
 6780        };
 6781
 6782        // Get selected index, defaulting to last option ("Only this time")
 6783        let selected_index = if let Some(active) = self.as_active_thread() {
 6784            active
 6785                .selected_permission_granularity
 6786                .get(&tool_call_id)
 6787                .copied()
 6788                .unwrap_or_else(|| choices.len().saturating_sub(1))
 6789        } else {
 6790            choices.len().saturating_sub(1)
 6791        };
 6792
 6793        let selected_choice = choices.get(selected_index).or(choices.last())?;
 6794
 6795        let selected_option = if is_allow {
 6796            &selected_choice.allow
 6797        } else {
 6798            &selected_choice.deny
 6799        };
 6800
 6801        self.authorize_tool_call(
 6802            tool_call_id,
 6803            selected_option.option_id.clone(),
 6804            selected_option.kind,
 6805            window,
 6806            cx,
 6807        );
 6808
 6809        Some(())
 6810    }
 6811
 6812    fn open_permission_dropdown(
 6813        &mut self,
 6814        _: &crate::OpenPermissionDropdown,
 6815        window: &mut Window,
 6816        cx: &mut Context<Self>,
 6817    ) {
 6818        if let Some(active) = self.as_active_thread() {
 6819            active.permission_dropdown_handle.toggle(window, cx);
 6820        }
 6821    }
 6822
 6823    fn handle_select_permission_granularity(
 6824        &mut self,
 6825        action: &SelectPermissionGranularity,
 6826        _window: &mut Window,
 6827        cx: &mut Context<Self>,
 6828    ) {
 6829        if let Some(active) = self.as_active_thread_mut() {
 6830            active.handle_select_permission_granularity(action, cx);
 6831        }
 6832    }
 6833
 6834    fn handle_authorize_tool_call(
 6835        &mut self,
 6836        action: &AuthorizeToolCall,
 6837        window: &mut Window,
 6838        cx: &mut Context<Self>,
 6839    ) {
 6840        let tool_call_id = acp::ToolCallId::new(action.tool_call_id.clone());
 6841        let option_id = acp::PermissionOptionId::new(action.option_id.clone());
 6842        let option_kind = match action.option_kind.as_str() {
 6843            "AllowOnce" => acp::PermissionOptionKind::AllowOnce,
 6844            "AllowAlways" => acp::PermissionOptionKind::AllowAlways,
 6845            "RejectOnce" => acp::PermissionOptionKind::RejectOnce,
 6846            "RejectAlways" => acp::PermissionOptionKind::RejectAlways,
 6847            _ => acp::PermissionOptionKind::AllowOnce,
 6848        };
 6849
 6850        self.authorize_tool_call(tool_call_id, option_id, option_kind, window, cx);
 6851    }
 6852
 6853    fn authorize_pending_tool_call(
 6854        &mut self,
 6855        kind: acp::PermissionOptionKind,
 6856        window: &mut Window,
 6857        cx: &mut Context<Self>,
 6858    ) -> Option<()> {
 6859        self.as_active_thread_mut()?
 6860            .authorize_pending_tool_call(kind, window, cx)
 6861    }
 6862
 6863    fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
 6864        let message_editor = self.message_editor.read(cx);
 6865        let is_editor_empty = message_editor.is_empty(cx);
 6866        let focus_handle = message_editor.focus_handle(cx);
 6867
 6868        let is_generating = self
 6869            .as_active_thread()
 6870            .is_some_and(|active| active.thread.read(cx).status() != ThreadStatus::Idle);
 6871
 6872        if self
 6873            .as_active_thread()
 6874            .is_some_and(|thread| thread.is_loading_contents)
 6875        {
 6876            div()
 6877                .id("loading-message-content")
 6878                .px_1()
 6879                .tooltip(Tooltip::text("Loading Added Context…"))
 6880                .child(loading_contents_spinner(IconSize::default()))
 6881                .into_any_element()
 6882        } else if is_generating && is_editor_empty {
 6883            IconButton::new("stop-generation", IconName::Stop)
 6884                .icon_color(Color::Error)
 6885                .style(ButtonStyle::Tinted(TintColor::Error))
 6886                .tooltip(move |_window, cx| {
 6887                    Tooltip::for_action("Stop Generation", &editor::actions::Cancel, cx)
 6888                })
 6889                .on_click(cx.listener(|this, _event, _, cx| this.cancel_generation(cx)))
 6890                .into_any_element()
 6891        } else {
 6892            IconButton::new("send-message", IconName::Send)
 6893                .style(ButtonStyle::Filled)
 6894                .map(|this| {
 6895                    if is_editor_empty && !is_generating {
 6896                        this.disabled(true).icon_color(Color::Muted)
 6897                    } else {
 6898                        this.icon_color(Color::Accent)
 6899                    }
 6900                })
 6901                .tooltip(move |_window, cx| {
 6902                    if is_editor_empty && !is_generating {
 6903                        Tooltip::for_action("Type to Send", &Chat, cx)
 6904                    } else if is_generating {
 6905                        let focus_handle = focus_handle.clone();
 6906
 6907                        Tooltip::element(move |_window, cx| {
 6908                            v_flex()
 6909                                .gap_1()
 6910                                .child(
 6911                                    h_flex()
 6912                                        .gap_2()
 6913                                        .justify_between()
 6914                                        .child(Label::new("Queue and Send"))
 6915                                        .child(KeyBinding::for_action_in(&Chat, &focus_handle, cx)),
 6916                                )
 6917                                .child(
 6918                                    h_flex()
 6919                                        .pt_1()
 6920                                        .gap_2()
 6921                                        .justify_between()
 6922                                        .border_t_1()
 6923                                        .border_color(cx.theme().colors().border_variant)
 6924                                        .child(Label::new("Send Immediately"))
 6925                                        .child(KeyBinding::for_action_in(
 6926                                            &SendImmediately,
 6927                                            &focus_handle,
 6928                                            cx,
 6929                                        )),
 6930                                )
 6931                                .into_any_element()
 6932                        })(_window, cx)
 6933                    } else {
 6934                        Tooltip::for_action("Send Message", &Chat, cx)
 6935                    }
 6936                })
 6937                .on_click(cx.listener(|this, _, window, cx| {
 6938                    this.send(window, cx);
 6939                }))
 6940                .into_any_element()
 6941        }
 6942    }
 6943
 6944    fn is_following(&self, cx: &App) -> bool {
 6945        match self
 6946            .as_active_thread()
 6947            .map(|active| active.thread.read(cx).status())
 6948        {
 6949            Some(ThreadStatus::Generating) => self
 6950                .workspace
 6951                .read_with(cx, |workspace, _| {
 6952                    workspace.is_being_followed(CollaboratorId::Agent)
 6953                })
 6954                .unwrap_or(false),
 6955            _ => self
 6956                .as_active_thread()
 6957                .is_some_and(|thread| thread.should_be_following),
 6958        }
 6959    }
 6960
 6961    fn toggle_following(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 6962        let following = self.is_following(cx);
 6963
 6964        if let Some(active) = self.as_active_thread_mut() {
 6965            active.should_be_following = !following;
 6966        }
 6967        if self
 6968            .as_active_thread()
 6969            .map(|active| active.thread.read(cx).status())
 6970            == Some(ThreadStatus::Generating)
 6971        {
 6972            self.workspace
 6973                .update(cx, |workspace, cx| {
 6974                    if following {
 6975                        workspace.unfollow(CollaboratorId::Agent, window, cx);
 6976                    } else {
 6977                        workspace.follow(CollaboratorId::Agent, window, cx);
 6978                    }
 6979                })
 6980                .ok();
 6981        }
 6982
 6983        telemetry::event!("Follow Agent Selected", following = !following);
 6984    }
 6985
 6986    fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
 6987        let following = self.is_following(cx);
 6988
 6989        let tooltip_label = if following {
 6990            if self.agent.name() == "Zed Agent" {
 6991                format!("Stop Following the {}", self.agent.name())
 6992            } else {
 6993                format!("Stop Following {}", self.agent.name())
 6994            }
 6995        } else {
 6996            if self.agent.name() == "Zed Agent" {
 6997                format!("Follow the {}", self.agent.name())
 6998            } else {
 6999                format!("Follow {}", self.agent.name())
 7000            }
 7001        };
 7002
 7003        IconButton::new("follow-agent", IconName::Crosshair)
 7004            .icon_size(IconSize::Small)
 7005            .icon_color(Color::Muted)
 7006            .toggle_state(following)
 7007            .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
 7008            .tooltip(move |_window, cx| {
 7009                if following {
 7010                    Tooltip::for_action(tooltip_label.clone(), &Follow, cx)
 7011                } else {
 7012                    Tooltip::with_meta(
 7013                        tooltip_label.clone(),
 7014                        Some(&Follow),
 7015                        "Track the agent's location as it reads and edits files.",
 7016                        cx,
 7017                    )
 7018                }
 7019            })
 7020            .on_click(cx.listener(move |this, _, window, cx| {
 7021                this.toggle_following(window, cx);
 7022            }))
 7023    }
 7024
 7025    fn render_add_context_button(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
 7026        let focus_handle = self.message_editor.focus_handle(cx);
 7027        let weak_self = cx.weak_entity();
 7028
 7029        PopoverMenu::new("add-context-menu")
 7030            .trigger_with_tooltip(
 7031                IconButton::new("add-context", IconName::Plus)
 7032                    .icon_size(IconSize::Small)
 7033                    .icon_color(Color::Muted),
 7034                {
 7035                    move |_window, cx| {
 7036                        Tooltip::for_action_in(
 7037                            "Add Context",
 7038                            &OpenAddContextMenu,
 7039                            &focus_handle,
 7040                            cx,
 7041                        )
 7042                    }
 7043                },
 7044            )
 7045            .anchor(gpui::Corner::BottomLeft)
 7046            .with_handle(self.add_context_menu_handle.clone())
 7047            .offset(gpui::Point {
 7048                x: px(0.0),
 7049                y: px(-2.0),
 7050            })
 7051            .menu(move |window, cx| {
 7052                weak_self
 7053                    .update(cx, |this, cx| this.build_add_context_menu(window, cx))
 7054                    .ok()
 7055            })
 7056    }
 7057
 7058    fn build_add_context_menu(
 7059        &self,
 7060        window: &mut Window,
 7061        cx: &mut Context<Self>,
 7062    ) -> Entity<ContextMenu> {
 7063        let message_editor = self.message_editor.clone();
 7064        let workspace = self.workspace.clone();
 7065        let supports_images = self
 7066            .as_active_thread()
 7067            .map(|active| active.prompt_capabilities.borrow().image)
 7068            .unwrap_or_default();
 7069
 7070        let has_editor_selection = workspace
 7071            .upgrade()
 7072            .and_then(|ws| {
 7073                ws.read(cx)
 7074                    .active_item(cx)
 7075                    .and_then(|item| item.downcast::<Editor>())
 7076            })
 7077            .is_some_and(|editor| {
 7078                editor.update(cx, |editor, cx| {
 7079                    editor.has_non_empty_selection(&editor.display_snapshot(cx))
 7080                })
 7081            });
 7082
 7083        let has_terminal_selection = workspace
 7084            .upgrade()
 7085            .and_then(|ws| ws.read(cx).panel::<TerminalPanel>(cx))
 7086            .is_some_and(|panel| !panel.read(cx).terminal_selections(cx).is_empty());
 7087
 7088        let has_selection = has_editor_selection || has_terminal_selection;
 7089
 7090        ContextMenu::build(window, cx, move |menu, _window, _cx| {
 7091            menu.key_context("AddContextMenu")
 7092                .header("Context")
 7093                .item(
 7094                    ContextMenuEntry::new("Files & Directories")
 7095                        .icon(IconName::File)
 7096                        .icon_color(Color::Muted)
 7097                        .icon_size(IconSize::XSmall)
 7098                        .handler({
 7099                            let message_editor = message_editor.clone();
 7100                            move |window, cx| {
 7101                                message_editor.focus_handle(cx).focus(window, cx);
 7102                                message_editor.update(cx, |editor, cx| {
 7103                                    editor.insert_context_type("file", window, cx);
 7104                                });
 7105                            }
 7106                        }),
 7107                )
 7108                .item(
 7109                    ContextMenuEntry::new("Symbols")
 7110                        .icon(IconName::Code)
 7111                        .icon_color(Color::Muted)
 7112                        .icon_size(IconSize::XSmall)
 7113                        .handler({
 7114                            let message_editor = message_editor.clone();
 7115                            move |window, cx| {
 7116                                message_editor.focus_handle(cx).focus(window, cx);
 7117                                message_editor.update(cx, |editor, cx| {
 7118                                    editor.insert_context_type("symbol", window, cx);
 7119                                });
 7120                            }
 7121                        }),
 7122                )
 7123                .item(
 7124                    ContextMenuEntry::new("Threads")
 7125                        .icon(IconName::Thread)
 7126                        .icon_color(Color::Muted)
 7127                        .icon_size(IconSize::XSmall)
 7128                        .handler({
 7129                            let message_editor = message_editor.clone();
 7130                            move |window, cx| {
 7131                                message_editor.focus_handle(cx).focus(window, cx);
 7132                                message_editor.update(cx, |editor, cx| {
 7133                                    editor.insert_context_type("thread", window, cx);
 7134                                });
 7135                            }
 7136                        }),
 7137                )
 7138                .item(
 7139                    ContextMenuEntry::new("Rules")
 7140                        .icon(IconName::Reader)
 7141                        .icon_color(Color::Muted)
 7142                        .icon_size(IconSize::XSmall)
 7143                        .handler({
 7144                            let message_editor = message_editor.clone();
 7145                            move |window, cx| {
 7146                                message_editor.focus_handle(cx).focus(window, cx);
 7147                                message_editor.update(cx, |editor, cx| {
 7148                                    editor.insert_context_type("rule", window, cx);
 7149                                });
 7150                            }
 7151                        }),
 7152                )
 7153                .item(
 7154                    ContextMenuEntry::new("Image")
 7155                        .icon(IconName::Image)
 7156                        .icon_color(Color::Muted)
 7157                        .icon_size(IconSize::XSmall)
 7158                        .disabled(!supports_images)
 7159                        .handler({
 7160                            let message_editor = message_editor.clone();
 7161                            move |window, cx| {
 7162                                message_editor.focus_handle(cx).focus(window, cx);
 7163                                message_editor.update(cx, |editor, cx| {
 7164                                    editor.add_images_from_picker(window, cx);
 7165                                });
 7166                            }
 7167                        }),
 7168                )
 7169                .item(
 7170                    ContextMenuEntry::new("Selection")
 7171                        .icon(IconName::CursorIBeam)
 7172                        .icon_color(Color::Muted)
 7173                        .icon_size(IconSize::XSmall)
 7174                        .disabled(!has_selection)
 7175                        .handler({
 7176                            move |window, cx| {
 7177                                window.dispatch_action(
 7178                                    zed_actions::agent::AddSelectionToThread.boxed_clone(),
 7179                                    cx,
 7180                                );
 7181                            }
 7182                        }),
 7183                )
 7184        })
 7185    }
 7186
 7187    fn open_add_context_menu(
 7188        &mut self,
 7189        _action: &OpenAddContextMenu,
 7190        window: &mut Window,
 7191        cx: &mut Context<Self>,
 7192    ) {
 7193        let menu_handle = self.add_context_menu_handle.clone();
 7194        window.defer(cx, move |window, cx| {
 7195            menu_handle.toggle(window, cx);
 7196        });
 7197    }
 7198
 7199    fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
 7200        let workspace = self.workspace.clone();
 7201        MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
 7202            Self::open_link(text, &workspace, window, cx);
 7203        })
 7204    }
 7205
 7206    fn open_link(
 7207        url: SharedString,
 7208        workspace: &WeakEntity<Workspace>,
 7209        window: &mut Window,
 7210        cx: &mut App,
 7211    ) {
 7212        let Some(workspace) = workspace.upgrade() else {
 7213            cx.open_url(&url);
 7214            return;
 7215        };
 7216
 7217        if let Some(mention) = MentionUri::parse(&url, workspace.read(cx).path_style(cx)).log_err()
 7218        {
 7219            workspace.update(cx, |workspace, cx| match mention {
 7220                MentionUri::File { abs_path } => {
 7221                    let project = workspace.project();
 7222                    let Some(path) =
 7223                        project.update(cx, |project, cx| project.find_project_path(abs_path, cx))
 7224                    else {
 7225                        return;
 7226                    };
 7227
 7228                    workspace
 7229                        .open_path(path, None, true, window, cx)
 7230                        .detach_and_log_err(cx);
 7231                }
 7232                MentionUri::PastedImage => {}
 7233                MentionUri::Directory { abs_path } => {
 7234                    let project = workspace.project();
 7235                    let Some(entry_id) = project.update(cx, |project, cx| {
 7236                        let path = project.find_project_path(abs_path, cx)?;
 7237                        project.entry_for_path(&path, cx).map(|entry| entry.id)
 7238                    }) else {
 7239                        return;
 7240                    };
 7241
 7242                    project.update(cx, |_, cx| {
 7243                        cx.emit(project::Event::RevealInProjectPanel(entry_id));
 7244                    });
 7245                }
 7246                MentionUri::Symbol {
 7247                    abs_path: path,
 7248                    line_range,
 7249                    ..
 7250                }
 7251                | MentionUri::Selection {
 7252                    abs_path: Some(path),
 7253                    line_range,
 7254                } => {
 7255                    let project = workspace.project();
 7256                    let Some(path) =
 7257                        project.update(cx, |project, cx| project.find_project_path(path, cx))
 7258                    else {
 7259                        return;
 7260                    };
 7261
 7262                    let item = workspace.open_path(path, None, true, window, cx);
 7263                    window
 7264                        .spawn(cx, async move |cx| {
 7265                            let Some(editor) = item.await?.downcast::<Editor>() else {
 7266                                return Ok(());
 7267                            };
 7268                            let range = Point::new(*line_range.start(), 0)
 7269                                ..Point::new(*line_range.start(), 0);
 7270                            editor
 7271                                .update_in(cx, |editor, window, cx| {
 7272                                    editor.change_selections(
 7273                                        SelectionEffects::scroll(Autoscroll::center()),
 7274                                        window,
 7275                                        cx,
 7276                                        |s| s.select_ranges(vec![range]),
 7277                                    );
 7278                                })
 7279                                .ok();
 7280                            anyhow::Ok(())
 7281                        })
 7282                        .detach_and_log_err(cx);
 7283                }
 7284                MentionUri::Selection { abs_path: None, .. } => {}
 7285                MentionUri::Thread { id, name } => {
 7286                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 7287                        panel.update(cx, |panel, cx| {
 7288                            panel.open_thread(
 7289                                AgentSessionInfo {
 7290                                    session_id: id,
 7291                                    cwd: None,
 7292                                    title: Some(name.into()),
 7293                                    updated_at: None,
 7294                                    meta: None,
 7295                                },
 7296                                window,
 7297                                cx,
 7298                            )
 7299                        });
 7300                    }
 7301                }
 7302                MentionUri::TextThread { path, .. } => {
 7303                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 7304                        panel.update(cx, |panel, cx| {
 7305                            panel
 7306                                .open_saved_text_thread(path.as_path().into(), window, cx)
 7307                                .detach_and_log_err(cx);
 7308                        });
 7309                    }
 7310                }
 7311                MentionUri::Rule { id, .. } => {
 7312                    let PromptId::User { uuid } = id else {
 7313                        return;
 7314                    };
 7315                    window.dispatch_action(
 7316                        Box::new(OpenRulesLibrary {
 7317                            prompt_to_select: Some(uuid.0),
 7318                        }),
 7319                        cx,
 7320                    )
 7321                }
 7322                MentionUri::Fetch { url } => {
 7323                    cx.open_url(url.as_str());
 7324                }
 7325                MentionUri::Diagnostics { .. } => {}
 7326                MentionUri::TerminalSelection { .. } => {}
 7327            })
 7328        } else {
 7329            cx.open_url(&url);
 7330        }
 7331    }
 7332
 7333    fn open_tool_call_location(
 7334        &self,
 7335        entry_ix: usize,
 7336        location_ix: usize,
 7337        window: &mut Window,
 7338        cx: &mut Context<Self>,
 7339    ) -> Option<()> {
 7340        let (tool_call_location, agent_location) = self
 7341            .as_active_thread()?
 7342            .thread
 7343            .read(cx)
 7344            .entries()
 7345            .get(entry_ix)?
 7346            .location(location_ix)?;
 7347
 7348        let project_path = self
 7349            .project
 7350            .read(cx)
 7351            .find_project_path(&tool_call_location.path, cx)?;
 7352
 7353        let open_task = self
 7354            .workspace
 7355            .update(cx, |workspace, cx| {
 7356                workspace.open_path(project_path, None, true, window, cx)
 7357            })
 7358            .log_err()?;
 7359        window
 7360            .spawn(cx, async move |cx| {
 7361                let item = open_task.await?;
 7362
 7363                let Some(active_editor) = item.downcast::<Editor>() else {
 7364                    return anyhow::Ok(());
 7365                };
 7366
 7367                active_editor.update_in(cx, |editor, window, cx| {
 7368                    let multibuffer = editor.buffer().read(cx);
 7369                    let buffer = multibuffer.as_singleton();
 7370                    if agent_location.buffer.upgrade() == buffer {
 7371                        let excerpt_id = multibuffer.excerpt_ids().first().cloned();
 7372                        let anchor =
 7373                            editor::Anchor::in_buffer(excerpt_id.unwrap(), agent_location.position);
 7374                        editor.change_selections(Default::default(), window, cx, |selections| {
 7375                            selections.select_anchor_ranges([anchor..anchor]);
 7376                        })
 7377                    } else {
 7378                        let row = tool_call_location.line.unwrap_or_default();
 7379                        editor.change_selections(Default::default(), window, cx, |selections| {
 7380                            selections.select_ranges([Point::new(row, 0)..Point::new(row, 0)]);
 7381                        })
 7382                    }
 7383                })?;
 7384
 7385                anyhow::Ok(())
 7386            })
 7387            .detach_and_log_err(cx);
 7388
 7389        None
 7390    }
 7391
 7392    pub fn open_thread_as_markdown(
 7393        &self,
 7394        workspace: Entity<Workspace>,
 7395        window: &mut Window,
 7396        cx: &mut App,
 7397    ) -> Task<Result<()>> {
 7398        let markdown_language_task = workspace
 7399            .read(cx)
 7400            .app_state()
 7401            .languages
 7402            .language_for_name("Markdown");
 7403
 7404        let (thread_title, markdown) = if let Some(active) = self.as_active_thread() {
 7405            let thread = active.thread.read(cx);
 7406            (thread.title().to_string(), thread.to_markdown(cx))
 7407        } else {
 7408            return Task::ready(Ok(()));
 7409        };
 7410
 7411        let project = workspace.read(cx).project().clone();
 7412        window.spawn(cx, async move |cx| {
 7413            let markdown_language = markdown_language_task.await?;
 7414
 7415            let buffer = project
 7416                .update(cx, |project, cx| {
 7417                    project.create_buffer(Some(markdown_language), false, cx)
 7418                })
 7419                .await?;
 7420
 7421            buffer.update(cx, |buffer, cx| {
 7422                buffer.set_text(markdown, cx);
 7423                buffer.set_capability(language::Capability::ReadWrite, cx);
 7424            });
 7425
 7426            workspace.update_in(cx, |workspace, window, cx| {
 7427                let buffer = cx
 7428                    .new(|cx| MultiBuffer::singleton(buffer, cx).with_title(thread_title.clone()));
 7429
 7430                workspace.add_item_to_active_pane(
 7431                    Box::new(cx.new(|cx| {
 7432                        let mut editor =
 7433                            Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
 7434                        editor.set_breadcrumb_header(thread_title);
 7435                        editor
 7436                    })),
 7437                    None,
 7438                    true,
 7439                    window,
 7440                    cx,
 7441                );
 7442            })?;
 7443            anyhow::Ok(())
 7444        })
 7445    }
 7446
 7447    fn scroll_to_top(&mut self, cx: &mut Context<Self>) {
 7448        if let Some(list_state) = self
 7449            .as_active_thread_mut()
 7450            .map(|active| &mut active.list_state)
 7451        {
 7452            list_state.scroll_to(ListOffset::default());
 7453            cx.notify();
 7454        }
 7455    }
 7456
 7457    fn scroll_to_most_recent_user_prompt(&mut self, cx: &mut Context<Self>) {
 7458        let Some(active) = self.as_active_thread() else {
 7459            return;
 7460        };
 7461
 7462        let entries = active.thread.read(cx).entries();
 7463        if entries.is_empty() {
 7464            return;
 7465        }
 7466
 7467        // Find the most recent user message and scroll it to the top of the viewport.
 7468        // (Fallback: if no user message exists, scroll to the bottom.)
 7469        if let Some(ix) = entries
 7470            .iter()
 7471            .rposition(|entry| matches!(entry, AgentThreadEntry::UserMessage(_)))
 7472        {
 7473            if let Some(list_state) = self
 7474                .as_active_thread_mut()
 7475                .map(|active| &mut active.list_state)
 7476            {
 7477                list_state.scroll_to(ListOffset {
 7478                    item_ix: ix,
 7479                    offset_in_item: px(0.0),
 7480                });
 7481                cx.notify();
 7482            }
 7483        } else {
 7484            self.scroll_to_bottom(cx);
 7485        }
 7486    }
 7487
 7488    pub fn scroll_to_bottom(&mut self, cx: &mut Context<Self>) {
 7489        if let Some(active) = self.as_active_thread() {
 7490            let entry_count = active.thread.read(cx).entries().len();
 7491            active.list_state.reset(entry_count);
 7492            cx.notify();
 7493        }
 7494    }
 7495
 7496    fn notify_with_sound(
 7497        &mut self,
 7498        caption: impl Into<SharedString>,
 7499        icon: IconName,
 7500        window: &mut Window,
 7501        cx: &mut Context<Self>,
 7502    ) {
 7503        self.play_notification_sound(window, cx);
 7504        self.show_notification(caption, icon, window, cx);
 7505    }
 7506
 7507    fn play_notification_sound(&self, window: &Window, cx: &mut App) {
 7508        let settings = AgentSettings::get_global(cx);
 7509        if settings.play_sound_when_agent_done && !window.is_window_active() {
 7510            Audio::play_sound(Sound::AgentDone, cx);
 7511        }
 7512    }
 7513
 7514    fn show_notification(
 7515        &mut self,
 7516        caption: impl Into<SharedString>,
 7517        icon: IconName,
 7518        window: &mut Window,
 7519        cx: &mut Context<Self>,
 7520    ) {
 7521        if !self.notifications.is_empty() {
 7522            return;
 7523        }
 7524
 7525        let settings = AgentSettings::get_global(cx);
 7526
 7527        let window_is_inactive = !window.is_window_active();
 7528        let panel_is_hidden = self
 7529            .workspace
 7530            .upgrade()
 7531            .map(|workspace| AgentPanel::is_hidden(&workspace, cx))
 7532            .unwrap_or(true);
 7533
 7534        let should_notify = window_is_inactive || panel_is_hidden;
 7535
 7536        if !should_notify {
 7537            return;
 7538        }
 7539
 7540        // TODO: Change this once we have title summarization for external agents.
 7541        let title = self.agent.name();
 7542
 7543        match settings.notify_when_agent_waiting {
 7544            NotifyWhenAgentWaiting::PrimaryScreen => {
 7545                if let Some(primary) = cx.primary_display() {
 7546                    self.pop_up(icon, caption.into(), title, window, primary, cx);
 7547                }
 7548            }
 7549            NotifyWhenAgentWaiting::AllScreens => {
 7550                let caption = caption.into();
 7551                for screen in cx.displays() {
 7552                    self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx);
 7553                }
 7554            }
 7555            NotifyWhenAgentWaiting::Never => {
 7556                // Don't show anything
 7557            }
 7558        }
 7559    }
 7560
 7561    fn pop_up(
 7562        &mut self,
 7563        icon: IconName,
 7564        caption: SharedString,
 7565        title: SharedString,
 7566        window: &mut Window,
 7567        screen: Rc<dyn PlatformDisplay>,
 7568        cx: &mut Context<Self>,
 7569    ) {
 7570        let options = AgentNotification::window_options(screen, cx);
 7571
 7572        let project_name = self.workspace.upgrade().and_then(|workspace| {
 7573            workspace
 7574                .read(cx)
 7575                .project()
 7576                .read(cx)
 7577                .visible_worktrees(cx)
 7578                .next()
 7579                .map(|worktree| worktree.read(cx).root_name_str().to_string())
 7580        });
 7581
 7582        if let Some(screen_window) = cx
 7583            .open_window(options, |_window, cx| {
 7584                cx.new(|_cx| {
 7585                    AgentNotification::new(title.clone(), caption.clone(), icon, project_name)
 7586                })
 7587            })
 7588            .log_err()
 7589            && let Some(pop_up) = screen_window.entity(cx).log_err()
 7590        {
 7591            self.notification_subscriptions
 7592                .entry(screen_window)
 7593                .or_insert_with(Vec::new)
 7594                .push(cx.subscribe_in(&pop_up, window, {
 7595                    |this, _, event, window, cx| match event {
 7596                        AgentNotificationEvent::Accepted => {
 7597                            let handle = window.window_handle();
 7598                            cx.activate(true);
 7599
 7600                            let workspace_handle = this.workspace.clone();
 7601
 7602                            // If there are multiple Zed windows, activate the correct one.
 7603                            cx.defer(move |cx| {
 7604                                handle
 7605                                    .update(cx, |_view, window, _cx| {
 7606                                        window.activate_window();
 7607
 7608                                        if let Some(workspace) = workspace_handle.upgrade() {
 7609                                            workspace.update(_cx, |workspace, cx| {
 7610                                                workspace.focus_panel::<AgentPanel>(window, cx);
 7611                                            });
 7612                                        }
 7613                                    })
 7614                                    .log_err();
 7615                            });
 7616
 7617                            this.dismiss_notifications(cx);
 7618                        }
 7619                        AgentNotificationEvent::Dismissed => {
 7620                            this.dismiss_notifications(cx);
 7621                        }
 7622                    }
 7623                }));
 7624
 7625            self.notifications.push(screen_window);
 7626
 7627            // If the user manually refocuses the original window, dismiss the popup.
 7628            self.notification_subscriptions
 7629                .entry(screen_window)
 7630                .or_insert_with(Vec::new)
 7631                .push({
 7632                    let pop_up_weak = pop_up.downgrade();
 7633
 7634                    cx.observe_window_activation(window, move |_, window, cx| {
 7635                        if window.is_window_active()
 7636                            && let Some(pop_up) = pop_up_weak.upgrade()
 7637                        {
 7638                            pop_up.update(cx, |_, cx| {
 7639                                cx.emit(AgentNotificationEvent::Dismissed);
 7640                            });
 7641                        }
 7642                    })
 7643                });
 7644        }
 7645    }
 7646
 7647    fn dismiss_notifications(&mut self, cx: &mut Context<Self>) {
 7648        for window in self.notifications.drain(..) {
 7649            window
 7650                .update(cx, |_, window, _| {
 7651                    window.remove_window();
 7652                })
 7653                .ok();
 7654
 7655            self.notification_subscriptions.remove(&window);
 7656        }
 7657    }
 7658
 7659    fn render_generating(&self, confirmation: bool, cx: &App) -> impl IntoElement {
 7660        let Some(active) = self.as_active_thread() else {
 7661            return div().into_any_element();
 7662        };
 7663
 7664        let show_stats = AgentSettings::get_global(cx).show_turn_stats;
 7665        let elapsed_label = show_stats
 7666            .then(|| {
 7667                active.turn_fields.turn_started_at.and_then(|started_at| {
 7668                    let elapsed = started_at.elapsed();
 7669                    (elapsed > STOPWATCH_THRESHOLD).then(|| duration_alt_display(elapsed))
 7670                })
 7671            })
 7672            .flatten();
 7673
 7674        let is_waiting = confirmation || active.thread.read(cx).has_in_progress_tool_calls();
 7675
 7676        let turn_tokens_label = elapsed_label
 7677            .is_some()
 7678            .then(|| {
 7679                active
 7680                    .turn_fields
 7681                    .turn_tokens
 7682                    .filter(|&tokens| tokens > TOKEN_THRESHOLD)
 7683                    .map(|tokens| crate::text_thread_editor::humanize_token_count(tokens))
 7684            })
 7685            .flatten();
 7686
 7687        let arrow_icon = if is_waiting {
 7688            IconName::ArrowUp
 7689        } else {
 7690            IconName::ArrowDown
 7691        };
 7692
 7693        h_flex()
 7694            .id("generating-spinner")
 7695            .py_2()
 7696            .px(rems_from_px(22.))
 7697            .gap_2()
 7698            .map(|this| {
 7699                if confirmation {
 7700                    this.child(
 7701                        h_flex()
 7702                            .w_2()
 7703                            .child(SpinnerLabel::sand().size(LabelSize::Small)),
 7704                    )
 7705                    .child(
 7706                        div().min_w(rems(8.)).child(
 7707                            LoadingLabel::new("Awaiting Confirmation")
 7708                                .size(LabelSize::Small)
 7709                                .color(Color::Muted),
 7710                        ),
 7711                    )
 7712                } else {
 7713                    this.child(SpinnerLabel::new().size(LabelSize::Small))
 7714                }
 7715            })
 7716            .when_some(elapsed_label, |this, elapsed| {
 7717                this.child(
 7718                    Label::new(elapsed)
 7719                        .size(LabelSize::Small)
 7720                        .color(Color::Muted),
 7721                )
 7722            })
 7723            .when_some(turn_tokens_label, |this, tokens| {
 7724                this.child(
 7725                    h_flex()
 7726                        .gap_0p5()
 7727                        .child(
 7728                            Icon::new(arrow_icon)
 7729                                .size(IconSize::XSmall)
 7730                                .color(Color::Muted),
 7731                        )
 7732                        .child(
 7733                            Label::new(format!("{} tokens", tokens))
 7734                                .size(LabelSize::Small)
 7735                                .color(Color::Muted),
 7736                        ),
 7737                )
 7738            })
 7739            .into_any_element()
 7740    }
 7741
 7742    fn render_thread_controls(
 7743        &self,
 7744        thread: &Entity<AcpThread>,
 7745        cx: &Context<Self>,
 7746    ) -> impl IntoElement {
 7747        let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
 7748        if is_generating {
 7749            return self.render_generating(false, cx).into_any_element();
 7750        }
 7751
 7752        let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
 7753            .shape(ui::IconButtonShape::Square)
 7754            .icon_size(IconSize::Small)
 7755            .icon_color(Color::Ignored)
 7756            .tooltip(Tooltip::text("Open Thread as Markdown"))
 7757            .on_click(cx.listener(move |this, _, window, cx| {
 7758                if let Some(workspace) = this.workspace.upgrade() {
 7759                    this.open_thread_as_markdown(workspace, window, cx)
 7760                        .detach_and_log_err(cx);
 7761                }
 7762            }));
 7763
 7764        let scroll_to_recent_user_prompt =
 7765            IconButton::new("scroll_to_recent_user_prompt", IconName::ForwardArrow)
 7766                .shape(ui::IconButtonShape::Square)
 7767                .icon_size(IconSize::Small)
 7768                .icon_color(Color::Ignored)
 7769                .tooltip(Tooltip::text("Scroll To Most Recent User Prompt"))
 7770                .on_click(cx.listener(move |this, _, _, cx| {
 7771                    this.scroll_to_most_recent_user_prompt(cx);
 7772                }));
 7773
 7774        let scroll_to_top = IconButton::new("scroll_to_top", IconName::ArrowUp)
 7775            .shape(ui::IconButtonShape::Square)
 7776            .icon_size(IconSize::Small)
 7777            .icon_color(Color::Ignored)
 7778            .tooltip(Tooltip::text("Scroll To Top"))
 7779            .on_click(cx.listener(move |this, _, _, cx| {
 7780                this.scroll_to_top(cx);
 7781            }));
 7782
 7783        let Some(active) = self.as_active_thread() else {
 7784            return div().into_any_element();
 7785        };
 7786
 7787        let show_stats = AgentSettings::get_global(cx).show_turn_stats;
 7788        let last_turn_clock = show_stats
 7789            .then(|| {
 7790                active
 7791                    .turn_fields
 7792                    .last_turn_duration
 7793                    .filter(|&duration| duration > STOPWATCH_THRESHOLD)
 7794                    .map(|duration| {
 7795                        Label::new(duration_alt_display(duration))
 7796                            .size(LabelSize::Small)
 7797                            .color(Color::Muted)
 7798                    })
 7799            })
 7800            .flatten();
 7801
 7802        let last_turn_tokens_label = last_turn_clock
 7803            .is_some()
 7804            .then(|| {
 7805                active
 7806                    .turn_fields
 7807                    .last_turn_tokens
 7808                    .filter(|&tokens| tokens > TOKEN_THRESHOLD)
 7809                    .map(|tokens| {
 7810                        Label::new(format!(
 7811                            "{} tokens",
 7812                            crate::text_thread_editor::humanize_token_count(tokens)
 7813                        ))
 7814                        .size(LabelSize::Small)
 7815                        .color(Color::Muted)
 7816                    })
 7817            })
 7818            .flatten();
 7819
 7820        let mut container = h_flex()
 7821            .w_full()
 7822            .py_2()
 7823            .px_5()
 7824            .gap_px()
 7825            .opacity(0.6)
 7826            .hover(|s| s.opacity(1.))
 7827            .justify_end()
 7828            .when(
 7829                last_turn_tokens_label.is_some() || last_turn_clock.is_some(),
 7830                |this| {
 7831                    this.child(
 7832                        h_flex()
 7833                            .gap_1()
 7834                            .px_1()
 7835                            .when_some(last_turn_tokens_label, |this, label| this.child(label))
 7836                            .when_some(last_turn_clock, |this, label| this.child(label)),
 7837                    )
 7838                },
 7839            );
 7840
 7841        if let Some(active) = self.as_active_thread() {
 7842            if AgentSettings::get_global(cx).enable_feedback
 7843                && active.thread.read(cx).connection().telemetry().is_some()
 7844            {
 7845                let feedback = active.thread_feedback.feedback;
 7846
 7847                let tooltip_meta = || {
 7848                    SharedString::new(
 7849                        "Rating the thread sends all of your current conversation to the Zed team.",
 7850                    )
 7851                };
 7852
 7853                container = container
 7854                    .child(
 7855                        IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
 7856                            .shape(ui::IconButtonShape::Square)
 7857                            .icon_size(IconSize::Small)
 7858                            .icon_color(match feedback {
 7859                                Some(ThreadFeedback::Positive) => Color::Accent,
 7860                                _ => Color::Ignored,
 7861                            })
 7862                            .tooltip(move |window, cx| match feedback {
 7863                                Some(ThreadFeedback::Positive) => {
 7864                                    Tooltip::text("Thanks for your feedback!")(window, cx)
 7865                                }
 7866                                _ => {
 7867                                    Tooltip::with_meta("Helpful Response", None, tooltip_meta(), cx)
 7868                                }
 7869                            })
 7870                            .on_click(cx.listener(move |this, _, window, cx| {
 7871                                this.handle_feedback_click(ThreadFeedback::Positive, window, cx);
 7872                            })),
 7873                    )
 7874                    .child(
 7875                        IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
 7876                            .shape(ui::IconButtonShape::Square)
 7877                            .icon_size(IconSize::Small)
 7878                            .icon_color(match feedback {
 7879                                Some(ThreadFeedback::Negative) => Color::Accent,
 7880                                _ => Color::Ignored,
 7881                            })
 7882                            .tooltip(move |window, cx| match feedback {
 7883                                Some(ThreadFeedback::Negative) => {
 7884                                    Tooltip::text(
 7885                                    "We appreciate your feedback and will use it to improve in the future.",
 7886                                )(window, cx)
 7887                                }
 7888                                _ => {
 7889                                    Tooltip::with_meta(
 7890                                        "Not Helpful Response",
 7891                                        None,
 7892                                        tooltip_meta(),
 7893                                        cx,
 7894                                    )
 7895                                }
 7896                            })
 7897                            .on_click(cx.listener(move |this, _, window, cx| {
 7898                                this.handle_feedback_click(ThreadFeedback::Negative, window, cx);
 7899                            })),
 7900                    );
 7901            }
 7902        }
 7903
 7904        if cx.has_flag::<AgentSharingFeatureFlag>()
 7905            && self.is_imported_thread(cx)
 7906            && self
 7907                .project
 7908                .read(cx)
 7909                .client()
 7910                .status()
 7911                .borrow()
 7912                .is_connected()
 7913        {
 7914            let sync_button = IconButton::new("sync-thread", IconName::ArrowCircle)
 7915                .shape(ui::IconButtonShape::Square)
 7916                .icon_size(IconSize::Small)
 7917                .icon_color(Color::Ignored)
 7918                .tooltip(Tooltip::text("Sync with source thread"))
 7919                .on_click(cx.listener(move |this, _, window, cx| {
 7920                    this.sync_thread(window, cx);
 7921                }));
 7922
 7923            container = container.child(sync_button);
 7924        }
 7925
 7926        if cx.has_flag::<AgentSharingFeatureFlag>() && !self.is_imported_thread(cx) {
 7927            let share_button = IconButton::new("share-thread", IconName::ArrowUpRight)
 7928                .shape(ui::IconButtonShape::Square)
 7929                .icon_size(IconSize::Small)
 7930                .icon_color(Color::Ignored)
 7931                .tooltip(Tooltip::text("Share Thread"))
 7932                .on_click(cx.listener(move |this, _, window, cx| {
 7933                    this.share_thread(window, cx);
 7934                }));
 7935
 7936            container = container.child(share_button);
 7937        }
 7938
 7939        container
 7940            .child(open_as_markdown)
 7941            .child(scroll_to_recent_user_prompt)
 7942            .child(scroll_to_top)
 7943            .into_any_element()
 7944    }
 7945
 7946    fn render_feedback_feedback_editor(editor: Entity<Editor>, cx: &Context<Self>) -> Div {
 7947        h_flex()
 7948            .key_context("AgentFeedbackMessageEditor")
 7949            .on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| {
 7950                if let Some(active) = this.as_active_thread_mut() {
 7951                    active.thread_feedback.dismiss_comments();
 7952                }
 7953                cx.notify();
 7954            }))
 7955            .on_action(cx.listener(move |this, _: &menu::Confirm, _window, cx| {
 7956                this.submit_feedback_message(cx);
 7957            }))
 7958            .p_2()
 7959            .mb_2()
 7960            .mx_5()
 7961            .gap_1()
 7962            .rounded_md()
 7963            .border_1()
 7964            .border_color(cx.theme().colors().border)
 7965            .bg(cx.theme().colors().editor_background)
 7966            .child(div().w_full().child(editor))
 7967            .child(
 7968                h_flex()
 7969                    .child(
 7970                        IconButton::new("dismiss-feedback-message", IconName::Close)
 7971                            .icon_color(Color::Error)
 7972                            .icon_size(IconSize::XSmall)
 7973                            .shape(ui::IconButtonShape::Square)
 7974                            .on_click(cx.listener(move |this, _, _window, cx| {
 7975                                if let Some(active) = this.as_active_thread_mut() {
 7976                                    active.thread_feedback.dismiss_comments();
 7977                                }
 7978                                cx.notify();
 7979                            })),
 7980                    )
 7981                    .child(
 7982                        IconButton::new("submit-feedback-message", IconName::Return)
 7983                            .icon_size(IconSize::XSmall)
 7984                            .shape(ui::IconButtonShape::Square)
 7985                            .on_click(cx.listener(move |this, _, _window, cx| {
 7986                                this.submit_feedback_message(cx);
 7987                            })),
 7988                    ),
 7989            )
 7990    }
 7991
 7992    fn handle_feedback_click(
 7993        &mut self,
 7994        feedback: ThreadFeedback,
 7995        window: &mut Window,
 7996        cx: &mut Context<Self>,
 7997    ) {
 7998        let Some(active) = self.as_active_thread_mut() else {
 7999            return;
 8000        };
 8001
 8002        active
 8003            .thread_feedback
 8004            .submit(active.thread.clone(), feedback, window, cx);
 8005        cx.notify();
 8006    }
 8007
 8008    fn submit_feedback_message(&mut self, cx: &mut Context<Self>) {
 8009        let Some(active) = self.as_active_thread_mut() else {
 8010            return;
 8011        };
 8012
 8013        active
 8014            .thread_feedback
 8015            .submit_comments(active.thread.clone(), cx);
 8016        cx.notify();
 8017    }
 8018
 8019    fn render_token_limit_callout(&self, cx: &mut Context<Self>) -> Option<Callout> {
 8020        let Some(active) = self.as_active_thread() else {
 8021            return None;
 8022        };
 8023
 8024        if active.token_limit_callout_dismissed {
 8025            return None;
 8026        }
 8027
 8028        let token_usage = active.thread.read(cx).token_usage()?;
 8029        let ratio = token_usage.ratio();
 8030
 8031        let (severity, icon, title) = match ratio {
 8032            acp_thread::TokenUsageRatio::Normal => return None,
 8033            acp_thread::TokenUsageRatio::Warning => (
 8034                Severity::Warning,
 8035                IconName::Warning,
 8036                "Thread reaching the token limit soon",
 8037            ),
 8038            acp_thread::TokenUsageRatio::Exceeded => (
 8039                Severity::Error,
 8040                IconName::XCircle,
 8041                "Thread reached the token limit",
 8042            ),
 8043        };
 8044
 8045        let description = "To continue, start a new thread from a summary.";
 8046
 8047        Some(
 8048            Callout::new()
 8049                .severity(severity)
 8050                .icon(icon)
 8051                .title(title)
 8052                .description(description)
 8053                .actions_slot(
 8054                    h_flex().gap_0p5().child(
 8055                        Button::new("start-new-thread", "Start New Thread")
 8056                            .label_size(LabelSize::Small)
 8057                            .on_click(cx.listener(|this, _, window, cx| {
 8058                                let Some(active) = this.as_active_thread() else {
 8059                                    return;
 8060                                };
 8061                                let session_id = active.thread.read(cx).session_id().clone();
 8062                                window.dispatch_action(
 8063                                    crate::NewNativeAgentThreadFromSummary {
 8064                                        from_session_id: session_id,
 8065                                    }
 8066                                    .boxed_clone(),
 8067                                    cx,
 8068                                );
 8069                            })),
 8070                    ),
 8071                )
 8072                .dismiss_action(self.dismiss_error_button(cx)),
 8073        )
 8074    }
 8075
 8076    fn agent_ui_font_size_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
 8077        if let Some(entry_view_state) = self
 8078            .as_active_thread()
 8079            .map(|active| &active.entry_view_state)
 8080            .cloned()
 8081        {
 8082            entry_view_state.update(cx, |entry_view_state, cx| {
 8083                entry_view_state.agent_ui_font_size_changed(cx);
 8084            });
 8085        }
 8086    }
 8087
 8088    pub(crate) fn insert_dragged_files(
 8089        &self,
 8090        paths: Vec<project::ProjectPath>,
 8091        added_worktrees: Vec<Entity<project::Worktree>>,
 8092        window: &mut Window,
 8093        cx: &mut Context<Self>,
 8094    ) {
 8095        self.message_editor.update(cx, |message_editor, cx| {
 8096            message_editor.insert_dragged_files(paths, added_worktrees, window, cx);
 8097        })
 8098    }
 8099
 8100    /// Inserts the selected text into the message editor or the message being
 8101    /// edited, if any.
 8102    pub(crate) fn insert_selections(&self, window: &mut Window, cx: &mut Context<Self>) {
 8103        self.active_editor(cx).update(cx, |editor, cx| {
 8104            editor.insert_selections(window, cx);
 8105        });
 8106    }
 8107
 8108    /// Inserts terminal text as a crease into the message editor.
 8109    pub(crate) fn insert_terminal_text(
 8110        &self,
 8111        text: String,
 8112        window: &mut Window,
 8113        cx: &mut Context<Self>,
 8114    ) {
 8115        self.message_editor.update(cx, |message_editor, cx| {
 8116            message_editor.insert_terminal_crease(text, window, cx);
 8117        });
 8118    }
 8119
 8120    /// Inserts code snippets as creases into the message editor.
 8121    pub(crate) fn insert_code_crease(
 8122        &self,
 8123        creases: Vec<(String, String)>,
 8124        window: &mut Window,
 8125        cx: &mut Context<Self>,
 8126    ) {
 8127        self.message_editor.update(cx, |message_editor, cx| {
 8128            message_editor.insert_code_creases(creases, window, cx);
 8129        });
 8130    }
 8131
 8132    fn render_codex_windows_warning(&self, cx: &mut Context<Self>) -> Callout {
 8133        Callout::new()
 8134            .icon(IconName::Warning)
 8135            .severity(Severity::Warning)
 8136            .title("Codex on Windows")
 8137            .description("For best performance, run Codex in Windows Subsystem for Linux (WSL2)")
 8138            .actions_slot(
 8139                Button::new("open-wsl-modal", "Open in WSL")
 8140                    .icon_size(IconSize::Small)
 8141                    .icon_color(Color::Muted)
 8142                    .on_click(cx.listener({
 8143                        move |_, _, _window, cx| {
 8144                            #[cfg(windows)]
 8145                            _window.dispatch_action(
 8146                                zed_actions::wsl_actions::OpenWsl::default().boxed_clone(),
 8147                                cx,
 8148                            );
 8149                            cx.notify();
 8150                        }
 8151                    })),
 8152            )
 8153            .dismiss_action(
 8154                IconButton::new("dismiss", IconName::Close)
 8155                    .icon_size(IconSize::Small)
 8156                    .icon_color(Color::Muted)
 8157                    .tooltip(Tooltip::text("Dismiss Warning"))
 8158                    .on_click(cx.listener({
 8159                        move |this, _, _, cx| {
 8160                            this.show_codex_windows_warning = false;
 8161                            cx.notify();
 8162                        }
 8163                    })),
 8164            )
 8165    }
 8166
 8167    fn clear_command_load_errors(&mut self, cx: &mut Context<Self>) {
 8168        if let Some(active) = self.as_active_thread_mut() {
 8169            active.command_load_errors_dismissed = true;
 8170        }
 8171        cx.notify();
 8172    }
 8173
 8174    fn refresh_cached_user_commands(&mut self, cx: &mut Context<Self>) {
 8175        let Some(registry) = self.slash_command_registry.clone() else {
 8176            return;
 8177        };
 8178        self.refresh_cached_user_commands_from_registry(&registry, cx);
 8179    }
 8180
 8181    fn refresh_cached_user_commands_from_registry(
 8182        &mut self,
 8183        registry: &Entity<SlashCommandRegistry>,
 8184        cx: &mut Context<Self>,
 8185    ) {
 8186        let Some(thread_state) = self.as_active_thread_mut() else {
 8187            return;
 8188        };
 8189        thread_state.refresh_cached_user_commands_from_registry(registry, cx);
 8190        cx.notify();
 8191    }
 8192
 8193    /// Returns the cached slash commands, if available.
 8194    pub fn cached_slash_commands(
 8195        &self,
 8196        _cx: &App,
 8197    ) -> collections::HashMap<String, UserSlashCommand> {
 8198        let Some(thread_state) = &self.as_active_thread() else {
 8199            return collections::HashMap::default();
 8200        };
 8201        thread_state.cached_user_commands.borrow().clone()
 8202    }
 8203
 8204    /// Returns the cached slash command errors, if available.
 8205    fn cached_slash_command_errors(&self, _cx: &App) -> Vec<CommandLoadError> {
 8206        let Some(thread_state) = &self.as_active_thread() else {
 8207            return Vec::new();
 8208        };
 8209        thread_state.cached_user_command_errors.borrow().clone()
 8210    }
 8211
 8212    fn render_thread_error(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
 8213        let view = self.as_active_thread()?;
 8214
 8215        let connection = view.thread.read(cx).connection().clone();
 8216
 8217        let content = match view.thread_error.as_ref()? {
 8218            ThreadError::Other { message, .. } => {
 8219                self.render_any_thread_error(message.clone(), window, cx)
 8220            }
 8221            ThreadError::Refusal => self.render_refusal_error(cx),
 8222            ThreadError::AuthenticationRequired(error) => {
 8223                self.render_authentication_required_error(error.clone(), connection, cx)
 8224            }
 8225            ThreadError::PaymentRequired => self.render_payment_required_error(cx),
 8226        };
 8227
 8228        Some(div().child(content))
 8229    }
 8230
 8231    fn render_new_version_callout(&self, version: &SharedString, cx: &mut Context<Self>) -> Div {
 8232        v_flex().w_full().justify_end().child(
 8233            h_flex()
 8234                .p_2()
 8235                .pr_3()
 8236                .w_full()
 8237                .gap_1p5()
 8238                .border_t_1()
 8239                .border_color(cx.theme().colors().border)
 8240                .bg(cx.theme().colors().element_background)
 8241                .child(
 8242                    h_flex()
 8243                        .flex_1()
 8244                        .gap_1p5()
 8245                        .child(
 8246                            Icon::new(IconName::Download)
 8247                                .color(Color::Accent)
 8248                                .size(IconSize::Small),
 8249                        )
 8250                        .child(Label::new("New version available").size(LabelSize::Small)),
 8251                )
 8252                .child(
 8253                    Button::new("update-button", format!("Update to v{}", version))
 8254                        .label_size(LabelSize::Small)
 8255                        .style(ButtonStyle::Tinted(TintColor::Accent))
 8256                        .on_click(cx.listener(|this, _, window, cx| {
 8257                            this.reset(window, cx);
 8258                        })),
 8259                ),
 8260        )
 8261    }
 8262
 8263    fn current_model_name(&self, cx: &App) -> SharedString {
 8264        // For native agent (Zed Agent), use the specific model name (e.g., "Claude 3.5 Sonnet")
 8265        // For ACP agents, use the agent name (e.g., "Claude Code", "Gemini CLI")
 8266        // This provides better clarity about what refused the request
 8267        if self.as_native_connection(cx).is_some() {
 8268            self.as_active_thread()
 8269                .and_then(|active| active.model_selector.as_ref())
 8270                .and_then(|selector| selector.read(cx).active_model(cx))
 8271                .map(|model| model.name.clone())
 8272                .unwrap_or_else(|| SharedString::from("The model"))
 8273        } else {
 8274            // ACP agent - use the agent name (e.g., "Claude Code", "Gemini CLI")
 8275            self.agent.name()
 8276        }
 8277    }
 8278
 8279    fn render_refusal_error(&self, cx: &mut Context<'_, Self>) -> Callout {
 8280        let model_or_agent_name = self.current_model_name(cx);
 8281        let refusal_message = format!(
 8282            "{} refused to respond to this prompt. \
 8283            This can happen when a model believes the prompt violates its content policy \
 8284            or safety guidelines, so rephrasing it can sometimes address the issue.",
 8285            model_or_agent_name
 8286        );
 8287
 8288        Callout::new()
 8289            .severity(Severity::Error)
 8290            .title("Request Refused")
 8291            .icon(IconName::XCircle)
 8292            .description(refusal_message.clone())
 8293            .actions_slot(self.create_copy_button(&refusal_message))
 8294            .dismiss_action(self.dismiss_error_button(cx))
 8295    }
 8296
 8297    fn set_can_fast_track_queue(&mut self, value: bool) {
 8298        if let Some(active) = self.as_active_thread_mut() {
 8299            active.can_fast_track_queue = value;
 8300        }
 8301    }
 8302
 8303    fn render_any_thread_error(
 8304        &mut self,
 8305        error: SharedString,
 8306        window: &mut Window,
 8307        cx: &mut Context<'_, Self>,
 8308    ) -> Callout {
 8309        let can_resume = self
 8310            .as_active_thread()
 8311            .map_or(false, |active| active.thread.read(cx).can_retry(cx));
 8312
 8313        let markdown = if let Some(thread_state) = self.as_active_thread()
 8314            && let Some(markdown) = &thread_state.thread_error_markdown
 8315        {
 8316            markdown.clone()
 8317        } else {
 8318            let markdown = cx.new(|cx| Markdown::new(error.clone(), None, None, cx));
 8319            if let Some(thread_state) = self.as_active_thread_mut() {
 8320                thread_state.thread_error_markdown = Some(markdown.clone());
 8321            }
 8322            markdown
 8323        };
 8324
 8325        let markdown_style =
 8326            MarkdownStyle::themed(MarkdownFont::Agent, window, cx).with_muted_text(cx);
 8327        let description = self
 8328            .render_markdown(markdown, markdown_style)
 8329            .into_any_element();
 8330
 8331        Callout::new()
 8332            .severity(Severity::Error)
 8333            .icon(IconName::XCircle)
 8334            .title("An Error Happened")
 8335            .description_slot(description)
 8336            .actions_slot(
 8337                h_flex()
 8338                    .gap_0p5()
 8339                    .when(can_resume, |this| {
 8340                        this.child(
 8341                            IconButton::new("retry", IconName::RotateCw)
 8342                                .icon_size(IconSize::Small)
 8343                                .tooltip(Tooltip::text("Retry Generation"))
 8344                                .on_click(cx.listener(|this, _, _window, cx| {
 8345                                    this.retry_generation(cx);
 8346                                })),
 8347                        )
 8348                    })
 8349                    .child(self.create_copy_button(error.to_string())),
 8350            )
 8351            .dismiss_action(self.dismiss_error_button(cx))
 8352    }
 8353
 8354    fn render_payment_required_error(&self, cx: &mut Context<Self>) -> Callout {
 8355        const ERROR_MESSAGE: &str =
 8356            "You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
 8357
 8358        Callout::new()
 8359            .severity(Severity::Error)
 8360            .icon(IconName::XCircle)
 8361            .title("Free Usage Exceeded")
 8362            .description(ERROR_MESSAGE)
 8363            .actions_slot(
 8364                h_flex()
 8365                    .gap_0p5()
 8366                    .child(self.upgrade_button(cx))
 8367                    .child(self.create_copy_button(ERROR_MESSAGE)),
 8368            )
 8369            .dismiss_action(self.dismiss_error_button(cx))
 8370    }
 8371
 8372    fn render_authentication_required_error(
 8373        &self,
 8374        error: SharedString,
 8375        connection: Rc<dyn AgentConnection>,
 8376        cx: &mut Context<Self>,
 8377    ) -> Callout {
 8378        Callout::new()
 8379            .severity(Severity::Error)
 8380            .title("Authentication Required")
 8381            .icon(IconName::XCircle)
 8382            .description(error.clone())
 8383            .actions_slot(
 8384                h_flex()
 8385                    .gap_0p5()
 8386                    .child(self.authenticate_button(connection, cx))
 8387                    .child(self.create_copy_button(error)),
 8388            )
 8389            .dismiss_action(self.dismiss_error_button(cx))
 8390    }
 8391
 8392    fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
 8393        let message = message.into();
 8394
 8395        CopyButton::new("copy-error-message", message).tooltip_label("Copy Error Message")
 8396    }
 8397
 8398    fn dismiss_error_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
 8399        IconButton::new("dismiss", IconName::Close)
 8400            .icon_size(IconSize::Small)
 8401            .tooltip(Tooltip::text("Dismiss"))
 8402            .on_click(cx.listener({
 8403                move |this, _, _, cx| {
 8404                    this.clear_thread_error(cx);
 8405                    cx.notify();
 8406                }
 8407            }))
 8408    }
 8409
 8410    fn authenticate_button(
 8411        &self,
 8412        connection: Rc<dyn AgentConnection>,
 8413        cx: &mut Context<Self>,
 8414    ) -> impl IntoElement {
 8415        Button::new("authenticate", "Authenticate")
 8416            .label_size(LabelSize::Small)
 8417            .style(ButtonStyle::Filled)
 8418            .on_click(cx.listener({
 8419                let connection = connection.clone();
 8420                move |this, _, window, cx| {
 8421                    let agent_name = this.agent.name();
 8422                    this.clear_thread_error(cx);
 8423                    if let Some(message) = this.in_flight_prompt.take() {
 8424                        this.message_editor.update(cx, |editor, cx| {
 8425                            editor.set_message(message, window, cx);
 8426                        });
 8427                    }
 8428                    let this = cx.weak_entity();
 8429                    window.defer(cx, {
 8430                        let connection = connection.clone();
 8431                        move |window, cx| {
 8432                            Self::handle_auth_required(
 8433                                this,
 8434                                AuthRequired::new(),
 8435                                agent_name,
 8436                                connection,
 8437                                window,
 8438                                cx,
 8439                            );
 8440                        }
 8441                    })
 8442                }
 8443            }))
 8444    }
 8445
 8446    pub(crate) fn reauthenticate(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 8447        let Some(view) = self.as_active_thread() else {
 8448            return;
 8449        };
 8450        let connection = view.thread.read(cx).connection().clone();
 8451        let agent_name = self.agent.name();
 8452        self.clear_thread_error(cx);
 8453        let this = cx.weak_entity();
 8454        window.defer(cx, |window, cx| {
 8455            Self::handle_auth_required(
 8456                this,
 8457                AuthRequired::new(),
 8458                agent_name,
 8459                connection,
 8460                window,
 8461                cx,
 8462            );
 8463        })
 8464    }
 8465
 8466    fn upgrade_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
 8467        Button::new("upgrade", "Upgrade")
 8468            .label_size(LabelSize::Small)
 8469            .style(ButtonStyle::Tinted(ui::TintColor::Accent))
 8470            .on_click(cx.listener({
 8471                move |this, _, _, cx| {
 8472                    this.clear_thread_error(cx);
 8473                    cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx));
 8474                }
 8475            }))
 8476    }
 8477
 8478    pub fn delete_history_entry(&mut self, entry: AgentSessionInfo, cx: &mut Context<Self>) {
 8479        let task = self.history.update(cx, |history, cx| {
 8480            history.delete_session(&entry.session_id, cx)
 8481        });
 8482        task.detach_and_log_err(cx);
 8483    }
 8484
 8485    /// Returns the currently active editor, either for a message that is being
 8486    /// edited or the editor for a new message.
 8487    fn active_editor(&self, cx: &App) -> Entity<MessageEditor> {
 8488        if let Some(thread_state) = self.as_active_thread()
 8489            && let Some(index) = thread_state.editing_message
 8490            && let Some(editor) = thread_state
 8491                .entry_view_state
 8492                .read(cx)
 8493                .entry(index)
 8494                .and_then(|entry| entry.message_editor())
 8495                .cloned()
 8496        {
 8497            editor
 8498        } else {
 8499            self.message_editor.clone()
 8500        }
 8501    }
 8502
 8503    fn get_agent_message_content(
 8504        entries: &[AgentThreadEntry],
 8505        entry_index: usize,
 8506        cx: &App,
 8507    ) -> Option<String> {
 8508        let entry = entries.get(entry_index)?;
 8509        if matches!(entry, AgentThreadEntry::UserMessage(_)) {
 8510            return None;
 8511        }
 8512
 8513        let start_index = (0..entry_index)
 8514            .rev()
 8515            .find(|&i| matches!(entries.get(i), Some(AgentThreadEntry::UserMessage(_))))
 8516            .map(|i| i + 1)
 8517            .unwrap_or(0);
 8518
 8519        let end_index = (entry_index + 1..entries.len())
 8520            .find(|&i| matches!(entries.get(i), Some(AgentThreadEntry::UserMessage(_))))
 8521            .map(|i| i - 1)
 8522            .unwrap_or(entries.len() - 1);
 8523
 8524        let parts: Vec<String> = (start_index..=end_index)
 8525            .filter_map(|i| entries.get(i))
 8526            .filter_map(|entry| {
 8527                if let AgentThreadEntry::AssistantMessage(message) = entry {
 8528                    let text: String = message
 8529                        .chunks
 8530                        .iter()
 8531                        .filter_map(|chunk| match chunk {
 8532                            AssistantMessageChunk::Message { block } => {
 8533                                let markdown = block.to_markdown(cx);
 8534                                if markdown.trim().is_empty() {
 8535                                    None
 8536                                } else {
 8537                                    Some(markdown.to_string())
 8538                                }
 8539                            }
 8540                            AssistantMessageChunk::Thought { .. } => None,
 8541                        })
 8542                        .collect::<Vec<_>>()
 8543                        .join("\n\n");
 8544
 8545                    if text.is_empty() { None } else { Some(text) }
 8546                } else {
 8547                    None
 8548                }
 8549            })
 8550            .collect();
 8551
 8552        let text = parts.join("\n\n");
 8553        if text.is_empty() { None } else { Some(text) }
 8554    }
 8555}
 8556
 8557fn loading_contents_spinner(size: IconSize) -> AnyElement {
 8558    Icon::new(IconName::LoadCircle)
 8559        .size(size)
 8560        .color(Color::Accent)
 8561        .with_rotate_animation(3)
 8562        .into_any_element()
 8563}
 8564
 8565fn placeholder_text(agent_name: &str, has_commands: bool) -> String {
 8566    if agent_name == "Zed Agent" {
 8567        format!("Message the {} — @ to include context", agent_name)
 8568    } else if has_commands {
 8569        format!(
 8570            "Message {} — @ to include context, / for commands",
 8571            agent_name
 8572        )
 8573    } else {
 8574        format!("Message {} — @ to include context", agent_name)
 8575    }
 8576}
 8577
 8578impl Focusable for AcpServerView {
 8579    fn focus_handle(&self, cx: &App) -> FocusHandle {
 8580        match self.as_active_thread() {
 8581            Some(_) => self.active_editor(cx).focus_handle(cx),
 8582            None => self.focus_handle.clone(),
 8583        }
 8584    }
 8585}
 8586
 8587#[cfg(any(test, feature = "test-support"))]
 8588impl AcpServerView {
 8589    /// Expands a tool call so its content is visible.
 8590    /// This is primarily useful for visual testing.
 8591    pub fn expand_tool_call(&mut self, tool_call_id: acp::ToolCallId, cx: &mut Context<Self>) {
 8592        if let Some(active) = self.as_active_thread_mut() {
 8593            active.expanded_tool_calls.insert(tool_call_id);
 8594            cx.notify();
 8595        }
 8596    }
 8597
 8598    /// Expands a subagent card so its content is visible.
 8599    /// This is primarily useful for visual testing.
 8600    pub fn expand_subagent(&mut self, session_id: acp::SessionId, cx: &mut Context<Self>) {
 8601        if let Some(active) = self.as_active_thread_mut() {
 8602            active.expanded_subagents.insert(session_id);
 8603            cx.notify();
 8604        }
 8605    }
 8606}
 8607
 8608impl Render for AcpServerView {
 8609    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 8610        self.sync_queued_message_editors(window, cx);
 8611
 8612        let has_messages = self
 8613            .as_active_thread()
 8614            .is_some_and(|active| active.list_state.item_count() > 0);
 8615
 8616        v_flex()
 8617            .size_full()
 8618            .key_context("AcpThread")
 8619            .on_action(cx.listener(|this, _: &menu::Cancel, _, cx| {
 8620                this.cancel_generation(cx);
 8621            }))
 8622            .on_action(cx.listener(Self::keep_all))
 8623            .on_action(cx.listener(Self::reject_all))
 8624            .on_action(cx.listener(Self::allow_always))
 8625            .on_action(cx.listener(Self::allow_once))
 8626            .on_action(cx.listener(Self::reject_once))
 8627            .on_action(cx.listener(Self::handle_authorize_tool_call))
 8628            .on_action(cx.listener(Self::handle_select_permission_granularity))
 8629            .on_action(cx.listener(Self::open_permission_dropdown))
 8630            .on_action(cx.listener(Self::open_add_context_menu))
 8631            .on_action(cx.listener(|this, _: &ToggleThinkingMode, _window, cx| {
 8632                if let Some(thread) = this.as_native_thread(cx) {
 8633                    thread.update(cx, |thread, cx| {
 8634                        thread.set_thinking_enabled(!thread.thinking_enabled(), cx);
 8635                    });
 8636                }
 8637            }))
 8638            .on_action(cx.listener(|this, _: &SendNextQueuedMessage, window, cx| {
 8639                this.send_queued_message_at_index(0, true, window, cx);
 8640            }))
 8641            .on_action(cx.listener(|this, _: &RemoveFirstQueuedMessage, _, cx| {
 8642                this.remove_from_queue(0, cx);
 8643                cx.notify();
 8644            }))
 8645            .on_action(cx.listener(|this, _: &EditFirstQueuedMessage, window, cx| {
 8646                if let Some(active) = this.as_active_thread()
 8647                    && let Some(editor) = active.queued_message_editors.first()
 8648                {
 8649                    window.focus(&editor.focus_handle(cx), cx);
 8650                }
 8651            }))
 8652            .on_action(cx.listener(|this, _: &ClearMessageQueue, _, cx| {
 8653                this.clear_queue(cx);
 8654                if let Some(state) = this.as_active_thread_mut() {
 8655                    state.can_fast_track_queue = false;
 8656                }
 8657                cx.notify();
 8658            }))
 8659            .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
 8660                if let Some(config_options_view) = this
 8661                    .as_active_thread()
 8662                    .and_then(|active| active.config_options_view.as_ref())
 8663                {
 8664                    let handled = config_options_view.update(cx, |view, cx| {
 8665                        view.toggle_category_picker(
 8666                            acp::SessionConfigOptionCategory::Mode,
 8667                            window,
 8668                            cx,
 8669                        )
 8670                    });
 8671                    if handled {
 8672                        return;
 8673                    }
 8674                }
 8675
 8676                if let Some(profile_selector) = this
 8677                    .as_active_thread()
 8678                    .and_then(|active| active.profile_selector.as_ref())
 8679                {
 8680                    profile_selector.read(cx).menu_handle().toggle(window, cx);
 8681                } else if let Some(mode_selector) = this
 8682                    .as_active_thread()
 8683                    .and_then(|active| active.mode_selector.as_ref())
 8684                {
 8685                    mode_selector.read(cx).menu_handle().toggle(window, cx);
 8686                }
 8687            }))
 8688            .on_action(cx.listener(|this, _: &CycleModeSelector, window, cx| {
 8689                if let Some(config_options_view) = this
 8690                    .as_active_thread()
 8691                    .and_then(|active| active.config_options_view.as_ref())
 8692                {
 8693                    let handled = config_options_view.update(cx, |view, cx| {
 8694                        view.cycle_category_option(
 8695                            acp::SessionConfigOptionCategory::Mode,
 8696                            false,
 8697                            cx,
 8698                        )
 8699                    });
 8700                    if handled {
 8701                        return;
 8702                    }
 8703                }
 8704
 8705                if let Some(profile_selector) = this
 8706                    .as_active_thread()
 8707                    .and_then(|active| active.profile_selector.as_ref())
 8708                {
 8709                    profile_selector.update(cx, |profile_selector, cx| {
 8710                        profile_selector.cycle_profile(cx);
 8711                    });
 8712                } else if let Some(mode_selector) = this
 8713                    .as_active_thread()
 8714                    .and_then(|active| active.mode_selector.as_ref())
 8715                {
 8716                    mode_selector.update(cx, |mode_selector, cx| {
 8717                        mode_selector.cycle_mode(window, cx);
 8718                    });
 8719                }
 8720            }))
 8721            .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
 8722                if let Some(config_options_view) = this
 8723                    .as_active_thread()
 8724                    .and_then(|active| active.config_options_view.as_ref())
 8725                {
 8726                    let handled = config_options_view.update(cx, |view, cx| {
 8727                        view.toggle_category_picker(
 8728                            acp::SessionConfigOptionCategory::Model,
 8729                            window,
 8730                            cx,
 8731                        )
 8732                    });
 8733                    if handled {
 8734                        return;
 8735                    }
 8736                }
 8737
 8738                if let Some(model_selector) = this
 8739                    .as_active_thread()
 8740                    .and_then(|active| active.model_selector.as_ref())
 8741                {
 8742                    model_selector
 8743                        .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
 8744                }
 8745            }))
 8746            .on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
 8747                if let Some(config_options_view) = this
 8748                    .as_active_thread()
 8749                    .and_then(|active| active.config_options_view.as_ref())
 8750                {
 8751                    let handled = config_options_view.update(cx, |view, cx| {
 8752                        view.cycle_category_option(
 8753                            acp::SessionConfigOptionCategory::Model,
 8754                            true,
 8755                            cx,
 8756                        )
 8757                    });
 8758                    if handled {
 8759                        return;
 8760                    }
 8761                }
 8762
 8763                if let Some(model_selector) = this
 8764                    .as_active_thread()
 8765                    .and_then(|active| active.model_selector.as_ref())
 8766                {
 8767                    model_selector.update(cx, |model_selector, cx| {
 8768                        model_selector.cycle_favorite_models(window, cx);
 8769                    });
 8770                }
 8771            }))
 8772            .track_focus(&self.focus_handle)
 8773            .bg(cx.theme().colors().panel_background)
 8774            .child(match &self.server_state {
 8775                ServerState::Loading { .. } => v_flex()
 8776                    .flex_1()
 8777                    .child(self.render_recent_history(cx))
 8778                    .into_any(),
 8779                ServerState::LoadError(e) => v_flex()
 8780                    .flex_1()
 8781                    .size_full()
 8782                    .items_center()
 8783                    .justify_end()
 8784                    .child(self.render_load_error(e, window, cx))
 8785                    .into_any(),
 8786                ServerState::Connected(ConnectedServerState {
 8787                    connection,
 8788                    auth_state:
 8789                        AuthState::Unauthenticated {
 8790                            description,
 8791                            configuration_view,
 8792                            pending_auth_method,
 8793                            _subscription,
 8794                        },
 8795                    ..
 8796                }) => v_flex()
 8797                    .flex_1()
 8798                    .size_full()
 8799                    .justify_end()
 8800                    .child(self.render_auth_required_state(
 8801                        connection,
 8802                        description.as_ref(),
 8803                        configuration_view.as_ref(),
 8804                        pending_auth_method.as_ref(),
 8805                        window,
 8806                        cx,
 8807                    ))
 8808                    .into_any_element(),
 8809                ServerState::Connected(connected) => {
 8810                    if let Some(current) = connected.current.as_ref() {
 8811                        v_flex()
 8812                            .flex_1()
 8813                            .map(|this| {
 8814                                let this = this.when(current.resumed_without_history, |this| {
 8815                                    this.child(self.render_resume_notice(cx))
 8816                                });
 8817                                if has_messages {
 8818                                    this.child(
 8819                                        list(
 8820                                            current.list_state.clone(),
 8821                                            cx.processor(|this, index: usize, window, cx| {
 8822                                                let Some((entry, len)) =
 8823                                                    this.as_active_thread().and_then(|active| {
 8824                                                        let entries =
 8825                                                            &active.thread.read(cx).entries();
 8826                                                        Some((entries.get(index)?, entries.len()))
 8827                                                    })
 8828                                                else {
 8829                                                    return Empty.into_any();
 8830                                                };
 8831                                                this.render_entry(index, len, entry, window, cx)
 8832                                            }),
 8833                                        )
 8834                                        .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
 8835                                        .flex_grow()
 8836                                        .into_any(),
 8837                                    )
 8838                                    .vertical_scrollbar_for(&current.list_state, window, cx)
 8839                                    .into_any()
 8840                                } else {
 8841                                    this.child(self.render_recent_history(cx)).into_any()
 8842                                }
 8843                            })
 8844                            .into_any_element()
 8845                    } else {
 8846                        div().into_any_element()
 8847                    }
 8848                }
 8849            })
 8850            // The activity bar is intentionally rendered outside of the ThreadState::Active match
 8851            // above so that the scrollbar doesn't render behind it. The current setup allows
 8852            // the scrollbar to stop exactly at the activity bar start.
 8853            .when(has_messages, |this| match self.as_active_thread() {
 8854                Some(thread) => this.children(self.render_activity_bar(&thread.thread, window, cx)),
 8855                _ => this,
 8856            })
 8857            .when(self.show_codex_windows_warning, |this| {
 8858                this.child(self.render_codex_windows_warning(cx))
 8859            })
 8860            .when_some(self.as_active_thread(), |this, thread_state| {
 8861                this.children(thread_state.render_thread_retry_status_callout())
 8862                    .children(thread_state.render_command_load_errors(cx))
 8863            })
 8864            .children(self.render_thread_error(window, cx))
 8865            .when_some(
 8866                match has_messages {
 8867                    true => None,
 8868                    false => self
 8869                        .as_active_thread()
 8870                        .and_then(|active| active.new_server_version_available.as_ref()),
 8871                },
 8872                |this, version| this.child(self.render_new_version_callout(version, cx)),
 8873            )
 8874            .children(self.render_token_limit_callout(cx))
 8875            .child(self.render_message_editor(window, cx))
 8876    }
 8877}
 8878
 8879fn plan_label_markdown_style(
 8880    status: &acp::PlanEntryStatus,
 8881    window: &Window,
 8882    cx: &App,
 8883) -> MarkdownStyle {
 8884    let default_md_style = MarkdownStyle::themed(MarkdownFont::Agent, window, cx);
 8885
 8886    MarkdownStyle {
 8887        base_text_style: TextStyle {
 8888            color: cx.theme().colors().text_muted,
 8889            strikethrough: if matches!(status, acp::PlanEntryStatus::Completed) {
 8890                Some(gpui::StrikethroughStyle {
 8891                    thickness: px(1.),
 8892                    color: Some(cx.theme().colors().text_muted.opacity(0.8)),
 8893                })
 8894            } else {
 8895                None
 8896            },
 8897            ..default_md_style.base_text_style
 8898        },
 8899        ..default_md_style
 8900    }
 8901}
 8902
 8903#[cfg(test)]
 8904pub(crate) mod tests {
 8905    use acp_thread::{
 8906        AgentSessionList, AgentSessionListRequest, AgentSessionListResponse, StubAgentConnection,
 8907    };
 8908    use action_log::ActionLog;
 8909    use agent::ToolPermissionContext;
 8910    use agent_client_protocol::SessionId;
 8911    use editor::MultiBufferOffset;
 8912    use fs::FakeFs;
 8913    use gpui::{EventEmitter, TestAppContext, VisualTestContext};
 8914    use project::Project;
 8915    use serde_json::json;
 8916    use settings::SettingsStore;
 8917    use std::any::Any;
 8918    use std::path::Path;
 8919    use std::rc::Rc;
 8920    use workspace::Item;
 8921
 8922    use super::*;
 8923
 8924    #[gpui::test]
 8925    async fn test_drop(cx: &mut TestAppContext) {
 8926        init_test(cx);
 8927
 8928        let (thread_view, _cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
 8929        let weak_view = thread_view.downgrade();
 8930        drop(thread_view);
 8931        assert!(!weak_view.is_upgradable());
 8932    }
 8933
 8934    #[gpui::test]
 8935    async fn test_notification_for_stop_event(cx: &mut TestAppContext) {
 8936        init_test(cx);
 8937
 8938        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
 8939
 8940        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
 8941        message_editor.update_in(cx, |editor, window, cx| {
 8942            editor.set_text("Hello", window, cx);
 8943        });
 8944
 8945        cx.deactivate_window();
 8946
 8947        thread_view.update_in(cx, |thread_view, window, cx| {
 8948            thread_view.send(window, cx);
 8949        });
 8950
 8951        cx.run_until_parked();
 8952
 8953        assert!(
 8954            cx.windows()
 8955                .iter()
 8956                .any(|window| window.downcast::<AgentNotification>().is_some())
 8957        );
 8958    }
 8959
 8960    #[gpui::test]
 8961    async fn test_notification_for_error(cx: &mut TestAppContext) {
 8962        init_test(cx);
 8963
 8964        let (thread_view, cx) =
 8965            setup_thread_view(StubAgentServer::new(SaboteurAgentConnection), cx).await;
 8966
 8967        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
 8968        message_editor.update_in(cx, |editor, window, cx| {
 8969            editor.set_text("Hello", window, cx);
 8970        });
 8971
 8972        cx.deactivate_window();
 8973
 8974        thread_view.update_in(cx, |thread_view, window, cx| {
 8975            thread_view.send(window, cx);
 8976        });
 8977
 8978        cx.run_until_parked();
 8979
 8980        assert!(
 8981            cx.windows()
 8982                .iter()
 8983                .any(|window| window.downcast::<AgentNotification>().is_some())
 8984        );
 8985    }
 8986
 8987    #[gpui::test]
 8988    async fn test_recent_history_refreshes_when_history_cache_updated(cx: &mut TestAppContext) {
 8989        init_test(cx);
 8990
 8991        let session_a = AgentSessionInfo::new(SessionId::new("session-a"));
 8992        let session_b = AgentSessionInfo::new(SessionId::new("session-b"));
 8993
 8994        let fs = FakeFs::new(cx.executor());
 8995        let project = Project::test(fs, [], cx).await;
 8996        let (workspace, cx) =
 8997            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 8998
 8999        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
 9000        // Create history without an initial session list - it will be set after connection
 9001        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
 9002
 9003        let thread_view = cx.update(|window, cx| {
 9004            cx.new(|cx| {
 9005                AcpServerView::new(
 9006                    Rc::new(StubAgentServer::default_response()),
 9007                    None,
 9008                    None,
 9009                    workspace.downgrade(),
 9010                    project,
 9011                    Some(thread_store),
 9012                    None,
 9013                    history.clone(),
 9014                    window,
 9015                    cx,
 9016                )
 9017            })
 9018        });
 9019
 9020        // Wait for connection to establish
 9021        cx.run_until_parked();
 9022
 9023        // Initially empty because StubAgentConnection.session_list() returns None
 9024        thread_view.read_with(cx, |view, _cx| {
 9025            assert_eq!(view.recent_history_entries.len(), 0);
 9026        });
 9027
 9028        // Now set the session list - this simulates external agents providing their history
 9029        let list_a: Rc<dyn AgentSessionList> =
 9030            Rc::new(StubSessionList::new(vec![session_a.clone()]));
 9031        history.update(cx, |history, cx| {
 9032            history.set_session_list(Some(list_a), cx);
 9033        });
 9034        cx.run_until_parked();
 9035
 9036        thread_view.read_with(cx, |view, _cx| {
 9037            assert_eq!(view.recent_history_entries.len(), 1);
 9038            assert_eq!(
 9039                view.recent_history_entries[0].session_id,
 9040                session_a.session_id
 9041            );
 9042        });
 9043
 9044        // Update to a different session list
 9045        let list_b: Rc<dyn AgentSessionList> =
 9046            Rc::new(StubSessionList::new(vec![session_b.clone()]));
 9047        history.update(cx, |history, cx| {
 9048            history.set_session_list(Some(list_b), cx);
 9049        });
 9050        cx.run_until_parked();
 9051
 9052        thread_view.read_with(cx, |view, _cx| {
 9053            assert_eq!(view.recent_history_entries.len(), 1);
 9054            assert_eq!(
 9055                view.recent_history_entries[0].session_id,
 9056                session_b.session_id
 9057            );
 9058        });
 9059    }
 9060
 9061    #[gpui::test]
 9062    async fn test_resume_without_history_adds_notice(cx: &mut TestAppContext) {
 9063        init_test(cx);
 9064
 9065        let session = AgentSessionInfo::new(SessionId::new("resume-session"));
 9066        let fs = FakeFs::new(cx.executor());
 9067        let project = Project::test(fs, [], cx).await;
 9068        let (workspace, cx) =
 9069            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 9070
 9071        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
 9072        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
 9073
 9074        let thread_view = cx.update(|window, cx| {
 9075            cx.new(|cx| {
 9076                AcpServerView::new(
 9077                    Rc::new(StubAgentServer::new(ResumeOnlyAgentConnection)),
 9078                    Some(session),
 9079                    None,
 9080                    workspace.downgrade(),
 9081                    project,
 9082                    Some(thread_store),
 9083                    None,
 9084                    history,
 9085                    window,
 9086                    cx,
 9087                )
 9088            })
 9089        });
 9090
 9091        cx.run_until_parked();
 9092
 9093        thread_view.read_with(cx, |view, _cx| {
 9094            let state = view.as_active_thread().unwrap();
 9095            assert!(state.resumed_without_history);
 9096            assert_eq!(state.list_state.item_count(), 0);
 9097        });
 9098    }
 9099
 9100    #[gpui::test]
 9101    async fn test_refusal_handling(cx: &mut TestAppContext) {
 9102        init_test(cx);
 9103
 9104        let (thread_view, cx) =
 9105            setup_thread_view(StubAgentServer::new(RefusalAgentConnection), cx).await;
 9106
 9107        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
 9108        message_editor.update_in(cx, |editor, window, cx| {
 9109            editor.set_text("Do something harmful", window, cx);
 9110        });
 9111
 9112        thread_view.update_in(cx, |thread_view, window, cx| {
 9113            thread_view.send(window, cx);
 9114        });
 9115
 9116        cx.run_until_parked();
 9117
 9118        // Check that the refusal error is set
 9119        thread_view.read_with(cx, |thread_view, _cx| {
 9120            let state = thread_view.as_active_thread().unwrap();
 9121            assert!(
 9122                matches!(state.thread_error, Some(ThreadError::Refusal)),
 9123                "Expected refusal error to be set"
 9124            );
 9125        });
 9126    }
 9127
 9128    #[gpui::test]
 9129    async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) {
 9130        init_test(cx);
 9131
 9132        let tool_call_id = acp::ToolCallId::new("1");
 9133        let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Label")
 9134            .kind(acp::ToolKind::Edit)
 9135            .content(vec!["hi".into()]);
 9136        let connection =
 9137            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
 9138                tool_call_id,
 9139                PermissionOptions::Flat(vec![acp::PermissionOption::new(
 9140                    "1",
 9141                    "Allow",
 9142                    acp::PermissionOptionKind::AllowOnce,
 9143                )]),
 9144            )]));
 9145
 9146        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
 9147
 9148        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
 9149
 9150        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
 9151        message_editor.update_in(cx, |editor, window, cx| {
 9152            editor.set_text("Hello", window, cx);
 9153        });
 9154
 9155        cx.deactivate_window();
 9156
 9157        thread_view.update_in(cx, |thread_view, window, cx| {
 9158            thread_view.send(window, cx);
 9159        });
 9160
 9161        cx.run_until_parked();
 9162
 9163        assert!(
 9164            cx.windows()
 9165                .iter()
 9166                .any(|window| window.downcast::<AgentNotification>().is_some())
 9167        );
 9168    }
 9169
 9170    #[gpui::test]
 9171    async fn test_notification_when_panel_hidden(cx: &mut TestAppContext) {
 9172        init_test(cx);
 9173
 9174        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
 9175
 9176        add_to_workspace(thread_view.clone(), cx);
 9177
 9178        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
 9179
 9180        message_editor.update_in(cx, |editor, window, cx| {
 9181            editor.set_text("Hello", window, cx);
 9182        });
 9183
 9184        // Window is active (don't deactivate), but panel will be hidden
 9185        // Note: In the test environment, the panel is not actually added to the dock,
 9186        // so is_agent_panel_hidden will return true
 9187
 9188        thread_view.update_in(cx, |thread_view, window, cx| {
 9189            thread_view.send(window, cx);
 9190        });
 9191
 9192        cx.run_until_parked();
 9193
 9194        // Should show notification because window is active but panel is hidden
 9195        assert!(
 9196            cx.windows()
 9197                .iter()
 9198                .any(|window| window.downcast::<AgentNotification>().is_some()),
 9199            "Expected notification when panel is hidden"
 9200        );
 9201    }
 9202
 9203    #[gpui::test]
 9204    async fn test_notification_still_works_when_window_inactive(cx: &mut TestAppContext) {
 9205        init_test(cx);
 9206
 9207        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
 9208
 9209        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
 9210        message_editor.update_in(cx, |editor, window, cx| {
 9211            editor.set_text("Hello", window, cx);
 9212        });
 9213
 9214        // Deactivate window - should show notification regardless of setting
 9215        cx.deactivate_window();
 9216
 9217        thread_view.update_in(cx, |thread_view, window, cx| {
 9218            thread_view.send(window, cx);
 9219        });
 9220
 9221        cx.run_until_parked();
 9222
 9223        // Should still show notification when window is inactive (existing behavior)
 9224        assert!(
 9225            cx.windows()
 9226                .iter()
 9227                .any(|window| window.downcast::<AgentNotification>().is_some()),
 9228            "Expected notification when window is inactive"
 9229        );
 9230    }
 9231
 9232    #[gpui::test]
 9233    async fn test_notification_respects_never_setting(cx: &mut TestAppContext) {
 9234        init_test(cx);
 9235
 9236        // Set notify_when_agent_waiting to Never
 9237        cx.update(|cx| {
 9238            AgentSettings::override_global(
 9239                AgentSettings {
 9240                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
 9241                    ..AgentSettings::get_global(cx).clone()
 9242                },
 9243                cx,
 9244            );
 9245        });
 9246
 9247        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
 9248
 9249        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
 9250        message_editor.update_in(cx, |editor, window, cx| {
 9251            editor.set_text("Hello", window, cx);
 9252        });
 9253
 9254        // Window is active
 9255
 9256        thread_view.update_in(cx, |thread_view, window, cx| {
 9257            thread_view.send(window, cx);
 9258        });
 9259
 9260        cx.run_until_parked();
 9261
 9262        // Should NOT show notification because notify_when_agent_waiting is Never
 9263        assert!(
 9264            !cx.windows()
 9265                .iter()
 9266                .any(|window| window.downcast::<AgentNotification>().is_some()),
 9267            "Expected no notification when notify_when_agent_waiting is Never"
 9268        );
 9269    }
 9270
 9271    #[gpui::test]
 9272    async fn test_notification_closed_when_thread_view_dropped(cx: &mut TestAppContext) {
 9273        init_test(cx);
 9274
 9275        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
 9276
 9277        let weak_view = thread_view.downgrade();
 9278
 9279        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
 9280        message_editor.update_in(cx, |editor, window, cx| {
 9281            editor.set_text("Hello", window, cx);
 9282        });
 9283
 9284        cx.deactivate_window();
 9285
 9286        thread_view.update_in(cx, |thread_view, window, cx| {
 9287            thread_view.send(window, cx);
 9288        });
 9289
 9290        cx.run_until_parked();
 9291
 9292        // Verify notification is shown
 9293        assert!(
 9294            cx.windows()
 9295                .iter()
 9296                .any(|window| window.downcast::<AgentNotification>().is_some()),
 9297            "Expected notification to be shown"
 9298        );
 9299
 9300        // Drop the thread view (simulating navigation to a new thread)
 9301        drop(thread_view);
 9302        drop(message_editor);
 9303        // Trigger an update to flush effects, which will call release_dropped_entities
 9304        cx.update(|_window, _cx| {});
 9305        cx.run_until_parked();
 9306
 9307        // Verify the entity was actually released
 9308        assert!(
 9309            !weak_view.is_upgradable(),
 9310            "Thread view entity should be released after dropping"
 9311        );
 9312
 9313        // The notification should be automatically closed via on_release
 9314        assert!(
 9315            !cx.windows()
 9316                .iter()
 9317                .any(|window| window.downcast::<AgentNotification>().is_some()),
 9318            "Notification should be closed when thread view is dropped"
 9319        );
 9320    }
 9321
 9322    async fn setup_thread_view(
 9323        agent: impl AgentServer + 'static,
 9324        cx: &mut TestAppContext,
 9325    ) -> (Entity<AcpServerView>, &mut VisualTestContext) {
 9326        let fs = FakeFs::new(cx.executor());
 9327        let project = Project::test(fs, [], cx).await;
 9328        let (workspace, cx) =
 9329            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 9330
 9331        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
 9332        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
 9333
 9334        let thread_view = cx.update(|window, cx| {
 9335            cx.new(|cx| {
 9336                AcpServerView::new(
 9337                    Rc::new(agent),
 9338                    None,
 9339                    None,
 9340                    workspace.downgrade(),
 9341                    project,
 9342                    Some(thread_store),
 9343                    None,
 9344                    history,
 9345                    window,
 9346                    cx,
 9347                )
 9348            })
 9349        });
 9350        cx.run_until_parked();
 9351        (thread_view, cx)
 9352    }
 9353
 9354    fn add_to_workspace(thread_view: Entity<AcpServerView>, cx: &mut VisualTestContext) {
 9355        let workspace = thread_view.read_with(cx, |thread_view, _cx| thread_view.workspace.clone());
 9356
 9357        workspace
 9358            .update_in(cx, |workspace, window, cx| {
 9359                workspace.add_item_to_active_pane(
 9360                    Box::new(cx.new(|_| ThreadViewItem(thread_view.clone()))),
 9361                    None,
 9362                    true,
 9363                    window,
 9364                    cx,
 9365                );
 9366            })
 9367            .unwrap();
 9368    }
 9369
 9370    struct ThreadViewItem(Entity<AcpServerView>);
 9371
 9372    impl Item for ThreadViewItem {
 9373        type Event = ();
 9374
 9375        fn include_in_nav_history() -> bool {
 9376            false
 9377        }
 9378
 9379        fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
 9380            "Test".into()
 9381        }
 9382    }
 9383
 9384    impl EventEmitter<()> for ThreadViewItem {}
 9385
 9386    impl Focusable for ThreadViewItem {
 9387        fn focus_handle(&self, cx: &App) -> FocusHandle {
 9388            self.0.read(cx).focus_handle(cx)
 9389        }
 9390    }
 9391
 9392    impl Render for ThreadViewItem {
 9393        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
 9394            self.0.clone().into_any_element()
 9395        }
 9396    }
 9397
 9398    struct StubAgentServer<C> {
 9399        connection: C,
 9400    }
 9401
 9402    impl<C> StubAgentServer<C> {
 9403        fn new(connection: C) -> Self {
 9404            Self { connection }
 9405        }
 9406    }
 9407
 9408    impl StubAgentServer<StubAgentConnection> {
 9409        fn default_response() -> Self {
 9410            let conn = StubAgentConnection::new();
 9411            conn.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
 9412                acp::ContentChunk::new("Default response".into()),
 9413            )]);
 9414            Self::new(conn)
 9415        }
 9416    }
 9417
 9418    #[derive(Clone)]
 9419    struct StubSessionList {
 9420        sessions: Vec<AgentSessionInfo>,
 9421    }
 9422
 9423    impl StubSessionList {
 9424        fn new(sessions: Vec<AgentSessionInfo>) -> Self {
 9425            Self { sessions }
 9426        }
 9427    }
 9428
 9429    impl AgentSessionList for StubSessionList {
 9430        fn list_sessions(
 9431            &self,
 9432            _request: AgentSessionListRequest,
 9433            _cx: &mut App,
 9434        ) -> Task<anyhow::Result<AgentSessionListResponse>> {
 9435            Task::ready(Ok(AgentSessionListResponse::new(self.sessions.clone())))
 9436        }
 9437        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
 9438            self
 9439        }
 9440    }
 9441
 9442    #[derive(Clone)]
 9443    struct ResumeOnlyAgentConnection;
 9444
 9445    impl AgentConnection for ResumeOnlyAgentConnection {
 9446        fn telemetry_id(&self) -> SharedString {
 9447            "resume-only".into()
 9448        }
 9449
 9450        fn new_thread(
 9451            self: Rc<Self>,
 9452            project: Entity<Project>,
 9453            _cwd: &Path,
 9454            cx: &mut gpui::App,
 9455        ) -> Task<gpui::Result<Entity<AcpThread>>> {
 9456            let action_log = cx.new(|_| ActionLog::new(project.clone()));
 9457            let thread = cx.new(|cx| {
 9458                AcpThread::new(
 9459                    "ResumeOnlyAgentConnection",
 9460                    self.clone(),
 9461                    project,
 9462                    action_log,
 9463                    SessionId::new("new-session"),
 9464                    watch::Receiver::constant(
 9465                        acp::PromptCapabilities::new()
 9466                            .image(true)
 9467                            .audio(true)
 9468                            .embedded_context(true),
 9469                    ),
 9470                    cx,
 9471                )
 9472            });
 9473            Task::ready(Ok(thread))
 9474        }
 9475
 9476        fn supports_resume_session(&self, _cx: &App) -> bool {
 9477            true
 9478        }
 9479
 9480        fn resume_session(
 9481            self: Rc<Self>,
 9482            session: AgentSessionInfo,
 9483            project: Entity<Project>,
 9484            _cwd: &Path,
 9485            cx: &mut App,
 9486        ) -> Task<gpui::Result<Entity<AcpThread>>> {
 9487            let action_log = cx.new(|_| ActionLog::new(project.clone()));
 9488            let thread = cx.new(|cx| {
 9489                AcpThread::new(
 9490                    "ResumeOnlyAgentConnection",
 9491                    self.clone(),
 9492                    project,
 9493                    action_log,
 9494                    session.session_id,
 9495                    watch::Receiver::constant(
 9496                        acp::PromptCapabilities::new()
 9497                            .image(true)
 9498                            .audio(true)
 9499                            .embedded_context(true),
 9500                    ),
 9501                    cx,
 9502                )
 9503            });
 9504            Task::ready(Ok(thread))
 9505        }
 9506
 9507        fn auth_methods(&self) -> &[acp::AuthMethod] {
 9508            &[]
 9509        }
 9510
 9511        fn authenticate(
 9512            &self,
 9513            _method_id: acp::AuthMethodId,
 9514            _cx: &mut App,
 9515        ) -> Task<gpui::Result<()>> {
 9516            Task::ready(Ok(()))
 9517        }
 9518
 9519        fn prompt(
 9520            &self,
 9521            _id: Option<acp_thread::UserMessageId>,
 9522            _params: acp::PromptRequest,
 9523            _cx: &mut App,
 9524        ) -> Task<gpui::Result<acp::PromptResponse>> {
 9525            Task::ready(Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)))
 9526        }
 9527
 9528        fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {}
 9529
 9530        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
 9531            self
 9532        }
 9533    }
 9534
 9535    impl<C> AgentServer for StubAgentServer<C>
 9536    where
 9537        C: 'static + AgentConnection + Send + Clone,
 9538    {
 9539        fn logo(&self) -> ui::IconName {
 9540            ui::IconName::Ai
 9541        }
 9542
 9543        fn name(&self) -> SharedString {
 9544            "Test".into()
 9545        }
 9546
 9547        fn connect(
 9548            &self,
 9549            _root_dir: Option<&Path>,
 9550            _delegate: AgentServerDelegate,
 9551            _cx: &mut App,
 9552        ) -> Task<gpui::Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
 9553            Task::ready(Ok((Rc::new(self.connection.clone()), None)))
 9554        }
 9555
 9556        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
 9557            self
 9558        }
 9559    }
 9560
 9561    #[derive(Clone)]
 9562    struct SaboteurAgentConnection;
 9563
 9564    impl AgentConnection for SaboteurAgentConnection {
 9565        fn telemetry_id(&self) -> SharedString {
 9566            "saboteur".into()
 9567        }
 9568
 9569        fn new_thread(
 9570            self: Rc<Self>,
 9571            project: Entity<Project>,
 9572            _cwd: &Path,
 9573            cx: &mut gpui::App,
 9574        ) -> Task<gpui::Result<Entity<AcpThread>>> {
 9575            Task::ready(Ok(cx.new(|cx| {
 9576                let action_log = cx.new(|_| ActionLog::new(project.clone()));
 9577                AcpThread::new(
 9578                    "SaboteurAgentConnection",
 9579                    self,
 9580                    project,
 9581                    action_log,
 9582                    SessionId::new("test"),
 9583                    watch::Receiver::constant(
 9584                        acp::PromptCapabilities::new()
 9585                            .image(true)
 9586                            .audio(true)
 9587                            .embedded_context(true),
 9588                    ),
 9589                    cx,
 9590                )
 9591            })))
 9592        }
 9593
 9594        fn auth_methods(&self) -> &[acp::AuthMethod] {
 9595            &[]
 9596        }
 9597
 9598        fn authenticate(
 9599            &self,
 9600            _method_id: acp::AuthMethodId,
 9601            _cx: &mut App,
 9602        ) -> Task<gpui::Result<()>> {
 9603            unimplemented!()
 9604        }
 9605
 9606        fn prompt(
 9607            &self,
 9608            _id: Option<acp_thread::UserMessageId>,
 9609            _params: acp::PromptRequest,
 9610            _cx: &mut App,
 9611        ) -> Task<gpui::Result<acp::PromptResponse>> {
 9612            Task::ready(Err(anyhow::anyhow!("Error prompting")))
 9613        }
 9614
 9615        fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
 9616            unimplemented!()
 9617        }
 9618
 9619        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
 9620            self
 9621        }
 9622    }
 9623
 9624    /// Simulates a model which always returns a refusal response
 9625    #[derive(Clone)]
 9626    struct RefusalAgentConnection;
 9627
 9628    impl AgentConnection for RefusalAgentConnection {
 9629        fn telemetry_id(&self) -> SharedString {
 9630            "refusal".into()
 9631        }
 9632
 9633        fn new_thread(
 9634            self: Rc<Self>,
 9635            project: Entity<Project>,
 9636            _cwd: &Path,
 9637            cx: &mut gpui::App,
 9638        ) -> Task<gpui::Result<Entity<AcpThread>>> {
 9639            Task::ready(Ok(cx.new(|cx| {
 9640                let action_log = cx.new(|_| ActionLog::new(project.clone()));
 9641                AcpThread::new(
 9642                    "RefusalAgentConnection",
 9643                    self,
 9644                    project,
 9645                    action_log,
 9646                    SessionId::new("test"),
 9647                    watch::Receiver::constant(
 9648                        acp::PromptCapabilities::new()
 9649                            .image(true)
 9650                            .audio(true)
 9651                            .embedded_context(true),
 9652                    ),
 9653                    cx,
 9654                )
 9655            })))
 9656        }
 9657
 9658        fn auth_methods(&self) -> &[acp::AuthMethod] {
 9659            &[]
 9660        }
 9661
 9662        fn authenticate(
 9663            &self,
 9664            _method_id: acp::AuthMethodId,
 9665            _cx: &mut App,
 9666        ) -> Task<gpui::Result<()>> {
 9667            unimplemented!()
 9668        }
 9669
 9670        fn prompt(
 9671            &self,
 9672            _id: Option<acp_thread::UserMessageId>,
 9673            _params: acp::PromptRequest,
 9674            _cx: &mut App,
 9675        ) -> Task<gpui::Result<acp::PromptResponse>> {
 9676            Task::ready(Ok(acp::PromptResponse::new(acp::StopReason::Refusal)))
 9677        }
 9678
 9679        fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
 9680            unimplemented!()
 9681        }
 9682
 9683        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
 9684            self
 9685        }
 9686    }
 9687
 9688    pub(crate) fn init_test(cx: &mut TestAppContext) {
 9689        cx.update(|cx| {
 9690            let settings_store = SettingsStore::test(cx);
 9691            cx.set_global(settings_store);
 9692            theme::init(theme::LoadThemes::JustBase, cx);
 9693            editor::init(cx);
 9694            release_channel::init(semver::Version::new(0, 0, 0), cx);
 9695            prompt_store::init(cx)
 9696        });
 9697    }
 9698
 9699    #[gpui::test]
 9700    async fn test_rewind_views(cx: &mut TestAppContext) {
 9701        init_test(cx);
 9702
 9703        let fs = FakeFs::new(cx.executor());
 9704        fs.insert_tree(
 9705            "/project",
 9706            json!({
 9707                "test1.txt": "old content 1",
 9708                "test2.txt": "old content 2"
 9709            }),
 9710        )
 9711        .await;
 9712        let project = Project::test(fs, [Path::new("/project")], cx).await;
 9713        let (workspace, cx) =
 9714            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 9715
 9716        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
 9717        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
 9718
 9719        let connection = Rc::new(StubAgentConnection::new());
 9720        let thread_view = cx.update(|window, cx| {
 9721            cx.new(|cx| {
 9722                AcpServerView::new(
 9723                    Rc::new(StubAgentServer::new(connection.as_ref().clone())),
 9724                    None,
 9725                    None,
 9726                    workspace.downgrade(),
 9727                    project.clone(),
 9728                    Some(thread_store.clone()),
 9729                    None,
 9730                    history,
 9731                    window,
 9732                    cx,
 9733                )
 9734            })
 9735        });
 9736
 9737        cx.run_until_parked();
 9738
 9739        let thread = thread_view
 9740            .read_with(cx, |view, _| {
 9741                view.as_active_thread().map(|r| r.thread.clone())
 9742            })
 9743            .unwrap();
 9744
 9745        // First user message
 9746        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(
 9747            acp::ToolCall::new("tool1", "Edit file 1")
 9748                .kind(acp::ToolKind::Edit)
 9749                .status(acp::ToolCallStatus::Completed)
 9750                .content(vec![acp::ToolCallContent::Diff(
 9751                    acp::Diff::new("/project/test1.txt", "new content 1").old_text("old content 1"),
 9752                )]),
 9753        )]);
 9754
 9755        thread
 9756            .update(cx, |thread, cx| thread.send_raw("Give me a diff", cx))
 9757            .await
 9758            .unwrap();
 9759        cx.run_until_parked();
 9760
 9761        thread.read_with(cx, |thread, _| {
 9762            assert_eq!(thread.entries().len(), 2);
 9763        });
 9764
 9765        thread_view.read_with(cx, |view, cx| {
 9766            let entry_view_state = view
 9767                .as_active_thread()
 9768                .map(|active| &active.entry_view_state)
 9769                .unwrap();
 9770            entry_view_state.read_with(cx, |entry_view_state, _| {
 9771                assert!(
 9772                    entry_view_state
 9773                        .entry(0)
 9774                        .unwrap()
 9775                        .message_editor()
 9776                        .is_some()
 9777                );
 9778                assert!(entry_view_state.entry(1).unwrap().has_content());
 9779            });
 9780        });
 9781
 9782        // Second user message
 9783        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(
 9784            acp::ToolCall::new("tool2", "Edit file 2")
 9785                .kind(acp::ToolKind::Edit)
 9786                .status(acp::ToolCallStatus::Completed)
 9787                .content(vec![acp::ToolCallContent::Diff(
 9788                    acp::Diff::new("/project/test2.txt", "new content 2").old_text("old content 2"),
 9789                )]),
 9790        )]);
 9791
 9792        thread
 9793            .update(cx, |thread, cx| thread.send_raw("Another one", cx))
 9794            .await
 9795            .unwrap();
 9796        cx.run_until_parked();
 9797
 9798        let second_user_message_id = thread.read_with(cx, |thread, _| {
 9799            assert_eq!(thread.entries().len(), 4);
 9800            let AgentThreadEntry::UserMessage(user_message) = &thread.entries()[2] else {
 9801                panic!();
 9802            };
 9803            user_message.id.clone().unwrap()
 9804        });
 9805
 9806        thread_view.read_with(cx, |view, cx| {
 9807            let entry_view_state = &view.as_active_thread().unwrap().entry_view_state;
 9808            entry_view_state.read_with(cx, |entry_view_state, _| {
 9809                assert!(
 9810                    entry_view_state
 9811                        .entry(0)
 9812                        .unwrap()
 9813                        .message_editor()
 9814                        .is_some()
 9815                );
 9816                assert!(entry_view_state.entry(1).unwrap().has_content());
 9817                assert!(
 9818                    entry_view_state
 9819                        .entry(2)
 9820                        .unwrap()
 9821                        .message_editor()
 9822                        .is_some()
 9823                );
 9824                assert!(entry_view_state.entry(3).unwrap().has_content());
 9825            });
 9826        });
 9827
 9828        // Rewind to first message
 9829        thread
 9830            .update(cx, |thread, cx| thread.rewind(second_user_message_id, cx))
 9831            .await
 9832            .unwrap();
 9833
 9834        cx.run_until_parked();
 9835
 9836        thread.read_with(cx, |thread, _| {
 9837            assert_eq!(thread.entries().len(), 2);
 9838        });
 9839
 9840        thread_view.read_with(cx, |view, cx| {
 9841            let active = view.as_active_thread().unwrap();
 9842            active
 9843                .entry_view_state
 9844                .read_with(cx, |entry_view_state, _| {
 9845                    assert!(
 9846                        entry_view_state
 9847                            .entry(0)
 9848                            .unwrap()
 9849                            .message_editor()
 9850                            .is_some()
 9851                    );
 9852                    assert!(entry_view_state.entry(1).unwrap().has_content());
 9853
 9854                    // Old views should be dropped
 9855                    assert!(entry_view_state.entry(2).is_none());
 9856                    assert!(entry_view_state.entry(3).is_none());
 9857                });
 9858        });
 9859    }
 9860
 9861    #[gpui::test]
 9862    async fn test_scroll_to_most_recent_user_prompt(cx: &mut TestAppContext) {
 9863        init_test(cx);
 9864
 9865        let connection = StubAgentConnection::new();
 9866
 9867        // Each user prompt will result in a user message entry plus an agent message entry.
 9868        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
 9869            acp::ContentChunk::new("Response 1".into()),
 9870        )]);
 9871
 9872        let (thread_view, cx) =
 9873            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
 9874
 9875        let thread = thread_view
 9876            .read_with(cx, |view, _| {
 9877                view.as_active_thread().map(|r| r.thread.clone())
 9878            })
 9879            .unwrap();
 9880
 9881        thread
 9882            .update(cx, |thread, cx| thread.send_raw("Prompt 1", cx))
 9883            .await
 9884            .unwrap();
 9885        cx.run_until_parked();
 9886
 9887        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
 9888            acp::ContentChunk::new("Response 2".into()),
 9889        )]);
 9890
 9891        thread
 9892            .update(cx, |thread, cx| thread.send_raw("Prompt 2", cx))
 9893            .await
 9894            .unwrap();
 9895        cx.run_until_parked();
 9896
 9897        // Move somewhere else first so we're not trivially already on the last user prompt.
 9898        thread_view.update(cx, |view, cx| {
 9899            view.scroll_to_top(cx);
 9900        });
 9901        cx.run_until_parked();
 9902
 9903        thread_view.update(cx, |view, cx| {
 9904            view.scroll_to_most_recent_user_prompt(cx);
 9905            let scroll_top = view
 9906                .as_active_thread()
 9907                .map(|active| &active.list_state)
 9908                .unwrap()
 9909                .logical_scroll_top();
 9910            // Entries layout is: [User1, Assistant1, User2, Assistant2]
 9911            assert_eq!(scroll_top.item_ix, 2);
 9912        });
 9913    }
 9914
 9915    #[gpui::test]
 9916    async fn test_scroll_to_most_recent_user_prompt_falls_back_to_bottom_without_user_messages(
 9917        cx: &mut TestAppContext,
 9918    ) {
 9919        init_test(cx);
 9920
 9921        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
 9922
 9923        // With no entries, scrolling should be a no-op and must not panic.
 9924        thread_view.update(cx, |view, cx| {
 9925            view.scroll_to_most_recent_user_prompt(cx);
 9926            let scroll_top = view
 9927                .as_active_thread()
 9928                .map(|active| &active.list_state)
 9929                .unwrap()
 9930                .logical_scroll_top();
 9931            assert_eq!(scroll_top.item_ix, 0);
 9932        });
 9933    }
 9934
 9935    #[gpui::test]
 9936    async fn test_message_editing_cancel(cx: &mut TestAppContext) {
 9937        init_test(cx);
 9938
 9939        let connection = StubAgentConnection::new();
 9940
 9941        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
 9942            acp::ContentChunk::new("Response".into()),
 9943        )]);
 9944
 9945        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
 9946        add_to_workspace(thread_view.clone(), cx);
 9947
 9948        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
 9949        message_editor.update_in(cx, |editor, window, cx| {
 9950            editor.set_text("Original message to edit", window, cx);
 9951        });
 9952        thread_view.update_in(cx, |thread_view, window, cx| {
 9953            thread_view.send(window, cx);
 9954        });
 9955
 9956        cx.run_until_parked();
 9957
 9958        let user_message_editor = thread_view.read_with(cx, |view, cx| {
 9959            assert_eq!(
 9960                view.as_active_thread()
 9961                    .and_then(|active| active.editing_message),
 9962                None
 9963            );
 9964
 9965            view.as_active_thread()
 9966                .map(|active| &active.entry_view_state)
 9967                .as_ref()
 9968                .unwrap()
 9969                .read(cx)
 9970                .entry(0)
 9971                .unwrap()
 9972                .message_editor()
 9973                .unwrap()
 9974                .clone()
 9975        });
 9976
 9977        // Focus
 9978        cx.focus(&user_message_editor);
 9979        thread_view.read_with(cx, |view, _cx| {
 9980            assert_eq!(
 9981                view.as_active_thread()
 9982                    .and_then(|active| active.editing_message),
 9983                Some(0)
 9984            );
 9985        });
 9986
 9987        // Edit
 9988        user_message_editor.update_in(cx, |editor, window, cx| {
 9989            editor.set_text("Edited message content", window, cx);
 9990        });
 9991
 9992        // Cancel
 9993        user_message_editor.update_in(cx, |_editor, window, cx| {
 9994            window.dispatch_action(Box::new(editor::actions::Cancel), cx);
 9995        });
 9996
 9997        thread_view.read_with(cx, |view, _cx| {
 9998            assert_eq!(
 9999                view.as_active_thread()
10000                    .and_then(|active| active.editing_message),
10001                None
10002            );
10003        });
10004
10005        user_message_editor.read_with(cx, |editor, cx| {
10006            assert_eq!(editor.text(cx), "Original message to edit");
10007        });
10008    }
10009
10010    #[gpui::test]
10011    async fn test_message_doesnt_send_if_empty(cx: &mut TestAppContext) {
10012        init_test(cx);
10013
10014        let connection = StubAgentConnection::new();
10015
10016        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
10017        add_to_workspace(thread_view.clone(), cx);
10018
10019        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
10020        message_editor.update_in(cx, |editor, window, cx| {
10021            editor.set_text("", window, cx);
10022        });
10023
10024        let thread = cx.read(|cx| {
10025            thread_view
10026                .read(cx)
10027                .as_active_thread()
10028                .unwrap()
10029                .thread
10030                .clone()
10031        });
10032        let entries_before = cx.read(|cx| thread.read(cx).entries().len());
10033
10034        thread_view.update_in(cx, |view, window, cx| {
10035            view.send(window, cx);
10036        });
10037        cx.run_until_parked();
10038
10039        let entries_after = cx.read(|cx| thread.read(cx).entries().len());
10040        assert_eq!(
10041            entries_before, entries_after,
10042            "No message should be sent when editor is empty"
10043        );
10044    }
10045
10046    #[gpui::test]
10047    async fn test_message_editing_regenerate(cx: &mut TestAppContext) {
10048        init_test(cx);
10049
10050        let connection = StubAgentConnection::new();
10051
10052        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
10053            acp::ContentChunk::new("Response".into()),
10054        )]);
10055
10056        let (thread_view, cx) =
10057            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
10058        add_to_workspace(thread_view.clone(), cx);
10059
10060        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
10061        message_editor.update_in(cx, |editor, window, cx| {
10062            editor.set_text("Original message to edit", window, cx);
10063        });
10064        thread_view.update_in(cx, |thread_view, window, cx| {
10065            thread_view.send(window, cx);
10066        });
10067
10068        cx.run_until_parked();
10069
10070        let user_message_editor = thread_view.read_with(cx, |view, cx| {
10071            assert_eq!(
10072                view.as_active_thread()
10073                    .and_then(|active| active.editing_message),
10074                None
10075            );
10076            assert_eq!(
10077                view.as_active_thread()
10078                    .unwrap()
10079                    .thread
10080                    .read(cx)
10081                    .entries()
10082                    .len(),
10083                2
10084            );
10085
10086            view.as_active_thread()
10087                .map(|active| &active.entry_view_state)
10088                .as_ref()
10089                .unwrap()
10090                .read(cx)
10091                .entry(0)
10092                .unwrap()
10093                .message_editor()
10094                .unwrap()
10095                .clone()
10096        });
10097
10098        // Focus
10099        cx.focus(&user_message_editor);
10100
10101        // Edit
10102        user_message_editor.update_in(cx, |editor, window, cx| {
10103            editor.set_text("Edited message content", window, cx);
10104        });
10105
10106        // Send
10107        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
10108            acp::ContentChunk::new("New Response".into()),
10109        )]);
10110
10111        user_message_editor.update_in(cx, |_editor, window, cx| {
10112            window.dispatch_action(Box::new(Chat), cx);
10113        });
10114
10115        cx.run_until_parked();
10116
10117        thread_view.read_with(cx, |view, cx| {
10118            assert_eq!(
10119                view.as_active_thread()
10120                    .and_then(|active| active.editing_message),
10121                None
10122            );
10123
10124            let entries = view.as_active_thread().unwrap().thread.read(cx).entries();
10125            assert_eq!(entries.len(), 2);
10126            assert_eq!(
10127                entries[0].to_markdown(cx),
10128                "## User\n\nEdited message content\n\n"
10129            );
10130            assert_eq!(
10131                entries[1].to_markdown(cx),
10132                "## Assistant\n\nNew Response\n\n"
10133            );
10134
10135            let entry_view_state = view
10136                .as_active_thread()
10137                .map(|active| &active.entry_view_state)
10138                .unwrap();
10139            let new_editor = entry_view_state.read_with(cx, |state, _cx| {
10140                assert!(!state.entry(1).unwrap().has_content());
10141                state.entry(0).unwrap().message_editor().unwrap().clone()
10142            });
10143
10144            assert_eq!(new_editor.read(cx).text(cx), "Edited message content");
10145        })
10146    }
10147
10148    #[gpui::test]
10149    async fn test_message_editing_while_generating(cx: &mut TestAppContext) {
10150        init_test(cx);
10151
10152        let connection = StubAgentConnection::new();
10153
10154        let (thread_view, cx) =
10155            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
10156        add_to_workspace(thread_view.clone(), cx);
10157
10158        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
10159        message_editor.update_in(cx, |editor, window, cx| {
10160            editor.set_text("Original message to edit", window, cx);
10161        });
10162        thread_view.update_in(cx, |thread_view, window, cx| {
10163            thread_view.send(window, cx);
10164        });
10165
10166        cx.run_until_parked();
10167
10168        let (user_message_editor, session_id) = thread_view.read_with(cx, |view, cx| {
10169            let thread = view.as_active_thread().unwrap().thread.read(cx);
10170            assert_eq!(thread.entries().len(), 1);
10171
10172            let editor = view
10173                .as_active_thread()
10174                .map(|active| &active.entry_view_state)
10175                .as_ref()
10176                .unwrap()
10177                .read(cx)
10178                .entry(0)
10179                .unwrap()
10180                .message_editor()
10181                .unwrap()
10182                .clone();
10183
10184            (editor, thread.session_id().clone())
10185        });
10186
10187        // Focus
10188        cx.focus(&user_message_editor);
10189
10190        thread_view.read_with(cx, |view, _cx| {
10191            assert_eq!(
10192                view.as_active_thread()
10193                    .and_then(|active| active.editing_message),
10194                Some(0)
10195            );
10196        });
10197
10198        // Edit
10199        user_message_editor.update_in(cx, |editor, window, cx| {
10200            editor.set_text("Edited message content", window, cx);
10201        });
10202
10203        thread_view.read_with(cx, |view, _cx| {
10204            assert_eq!(
10205                view.as_active_thread()
10206                    .and_then(|active| active.editing_message),
10207                Some(0)
10208            );
10209        });
10210
10211        // Finish streaming response
10212        cx.update(|_, cx| {
10213            connection.send_update(
10214                session_id.clone(),
10215                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("Response".into())),
10216                cx,
10217            );
10218            connection.end_turn(session_id, acp::StopReason::EndTurn);
10219        });
10220
10221        thread_view.read_with(cx, |view, _cx| {
10222            assert_eq!(
10223                view.as_active_thread()
10224                    .and_then(|active| active.editing_message),
10225                Some(0)
10226            );
10227        });
10228
10229        cx.run_until_parked();
10230
10231        // Should still be editing
10232        cx.update(|window, cx| {
10233            assert!(user_message_editor.focus_handle(cx).is_focused(window));
10234            assert_eq!(
10235                thread_view
10236                    .read(cx)
10237                    .as_active_thread()
10238                    .and_then(|active| active.editing_message),
10239                Some(0)
10240            );
10241            assert_eq!(
10242                user_message_editor.read(cx).text(cx),
10243                "Edited message content"
10244            );
10245        });
10246    }
10247
10248    struct GeneratingThreadSetup {
10249        thread_view: Entity<AcpServerView>,
10250        thread: Entity<AcpThread>,
10251        message_editor: Entity<MessageEditor>,
10252    }
10253
10254    async fn setup_generating_thread(
10255        cx: &mut TestAppContext,
10256    ) -> (GeneratingThreadSetup, &mut VisualTestContext) {
10257        let connection = StubAgentConnection::new();
10258
10259        let (thread_view, cx) =
10260            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
10261        add_to_workspace(thread_view.clone(), cx);
10262
10263        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
10264        message_editor.update_in(cx, |editor, window, cx| {
10265            editor.set_text("Hello", window, cx);
10266        });
10267        thread_view.update_in(cx, |thread_view, window, cx| {
10268            thread_view.send(window, cx);
10269        });
10270
10271        let (thread, session_id) = thread_view.read_with(cx, |view, cx| {
10272            let thread = view.as_active_thread().as_ref().unwrap().thread.clone();
10273            (thread.clone(), thread.read(cx).session_id().clone())
10274        });
10275
10276        cx.run_until_parked();
10277
10278        cx.update(|_, cx| {
10279            connection.send_update(
10280                session_id.clone(),
10281                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
10282                    "Response chunk".into(),
10283                )),
10284                cx,
10285            );
10286        });
10287
10288        cx.run_until_parked();
10289
10290        thread.read_with(cx, |thread, _cx| {
10291            assert_eq!(thread.status(), ThreadStatus::Generating);
10292        });
10293
10294        (
10295            GeneratingThreadSetup {
10296                thread_view,
10297                thread,
10298                message_editor,
10299            },
10300            cx,
10301        )
10302    }
10303
10304    #[gpui::test]
10305    async fn test_escape_cancels_generation_from_conversation_focus(cx: &mut TestAppContext) {
10306        init_test(cx);
10307
10308        let (setup, cx) = setup_generating_thread(cx).await;
10309
10310        let focus_handle = setup
10311            .thread_view
10312            .read_with(cx, |view, _cx| view.focus_handle.clone());
10313        cx.update(|window, cx| {
10314            window.focus(&focus_handle, cx);
10315        });
10316
10317        setup.thread_view.update_in(cx, |_, window, cx| {
10318            window.dispatch_action(menu::Cancel.boxed_clone(), cx);
10319        });
10320
10321        cx.run_until_parked();
10322
10323        setup.thread.read_with(cx, |thread, _cx| {
10324            assert_eq!(thread.status(), ThreadStatus::Idle);
10325        });
10326    }
10327
10328    #[gpui::test]
10329    async fn test_escape_cancels_generation_from_editor_focus(cx: &mut TestAppContext) {
10330        init_test(cx);
10331
10332        let (setup, cx) = setup_generating_thread(cx).await;
10333
10334        let editor_focus_handle = setup
10335            .message_editor
10336            .read_with(cx, |editor, cx| editor.focus_handle(cx));
10337        cx.update(|window, cx| {
10338            window.focus(&editor_focus_handle, cx);
10339        });
10340
10341        setup.message_editor.update_in(cx, |_, window, cx| {
10342            window.dispatch_action(editor::actions::Cancel.boxed_clone(), cx);
10343        });
10344
10345        cx.run_until_parked();
10346
10347        setup.thread.read_with(cx, |thread, _cx| {
10348            assert_eq!(thread.status(), ThreadStatus::Idle);
10349        });
10350    }
10351
10352    #[gpui::test]
10353    async fn test_escape_when_idle_is_noop(cx: &mut TestAppContext) {
10354        init_test(cx);
10355
10356        let (thread_view, cx) =
10357            setup_thread_view(StubAgentServer::new(StubAgentConnection::new()), cx).await;
10358        add_to_workspace(thread_view.clone(), cx);
10359
10360        let thread = thread_view.read_with(cx, |view, _cx| {
10361            view.as_active_thread().unwrap().thread.clone()
10362        });
10363
10364        thread.read_with(cx, |thread, _cx| {
10365            assert_eq!(thread.status(), ThreadStatus::Idle);
10366        });
10367
10368        let focus_handle = thread_view.read_with(cx, |view, _cx| view.focus_handle.clone());
10369        cx.update(|window, cx| {
10370            window.focus(&focus_handle, cx);
10371        });
10372
10373        thread_view.update_in(cx, |_, window, cx| {
10374            window.dispatch_action(menu::Cancel.boxed_clone(), cx);
10375        });
10376
10377        cx.run_until_parked();
10378
10379        thread.read_with(cx, |thread, _cx| {
10380            assert_eq!(thread.status(), ThreadStatus::Idle);
10381        });
10382    }
10383
10384    #[gpui::test]
10385    async fn test_interrupt(cx: &mut TestAppContext) {
10386        init_test(cx);
10387
10388        let connection = StubAgentConnection::new();
10389
10390        let (thread_view, cx) =
10391            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
10392        add_to_workspace(thread_view.clone(), cx);
10393
10394        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
10395        message_editor.update_in(cx, |editor, window, cx| {
10396            editor.set_text("Message 1", window, cx);
10397        });
10398        thread_view.update_in(cx, |thread_view, window, cx| {
10399            thread_view.send(window, cx);
10400        });
10401
10402        let (thread, session_id) = thread_view.read_with(cx, |view, cx| {
10403            let thread = view.as_active_thread().unwrap().thread.clone();
10404
10405            (thread.clone(), thread.read(cx).session_id().clone())
10406        });
10407
10408        cx.run_until_parked();
10409
10410        cx.update(|_, cx| {
10411            connection.send_update(
10412                session_id.clone(),
10413                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
10414                    "Message 1 resp".into(),
10415                )),
10416                cx,
10417            );
10418        });
10419
10420        cx.run_until_parked();
10421
10422        thread.read_with(cx, |thread, cx| {
10423            assert_eq!(
10424                thread.to_markdown(cx),
10425                indoc::indoc! {"
10426                    ## User
10427
10428                    Message 1
10429
10430                    ## Assistant
10431
10432                    Message 1 resp
10433
10434                "}
10435            )
10436        });
10437
10438        message_editor.update_in(cx, |editor, window, cx| {
10439            editor.set_text("Message 2", window, cx);
10440        });
10441        thread_view.update_in(cx, |thread_view, window, cx| {
10442            thread_view.interrupt_and_send(window, cx);
10443        });
10444
10445        cx.update(|_, cx| {
10446            // Simulate a response sent after beginning to cancel
10447            connection.send_update(
10448                session_id.clone(),
10449                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("onse".into())),
10450                cx,
10451            );
10452        });
10453
10454        cx.run_until_parked();
10455
10456        // Last Message 1 response should appear before Message 2
10457        thread.read_with(cx, |thread, cx| {
10458            assert_eq!(
10459                thread.to_markdown(cx),
10460                indoc::indoc! {"
10461                    ## User
10462
10463                    Message 1
10464
10465                    ## Assistant
10466
10467                    Message 1 response
10468
10469                    ## User
10470
10471                    Message 2
10472
10473                "}
10474            )
10475        });
10476
10477        cx.update(|_, cx| {
10478            connection.send_update(
10479                session_id.clone(),
10480                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
10481                    "Message 2 response".into(),
10482                )),
10483                cx,
10484            );
10485            connection.end_turn(session_id.clone(), acp::StopReason::EndTurn);
10486        });
10487
10488        cx.run_until_parked();
10489
10490        thread.read_with(cx, |thread, cx| {
10491            assert_eq!(
10492                thread.to_markdown(cx),
10493                indoc::indoc! {"
10494                    ## User
10495
10496                    Message 1
10497
10498                    ## Assistant
10499
10500                    Message 1 response
10501
10502                    ## User
10503
10504                    Message 2
10505
10506                    ## Assistant
10507
10508                    Message 2 response
10509
10510                "}
10511            )
10512        });
10513    }
10514
10515    #[gpui::test]
10516    async fn test_message_editing_insert_selections(cx: &mut TestAppContext) {
10517        init_test(cx);
10518
10519        let connection = StubAgentConnection::new();
10520        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
10521            acp::ContentChunk::new("Response".into()),
10522        )]);
10523
10524        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
10525        add_to_workspace(thread_view.clone(), cx);
10526
10527        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
10528        message_editor.update_in(cx, |editor, window, cx| {
10529            editor.set_text("Original message to edit", window, cx)
10530        });
10531        thread_view.update_in(cx, |thread_view, window, cx| thread_view.send(window, cx));
10532        cx.run_until_parked();
10533
10534        let user_message_editor = thread_view.read_with(cx, |thread_view, cx| {
10535            thread_view
10536                .as_active_thread()
10537                .map(|active| &active.entry_view_state)
10538                .as_ref()
10539                .unwrap()
10540                .read(cx)
10541                .entry(0)
10542                .expect("Should have at least one entry")
10543                .message_editor()
10544                .expect("Should have message editor")
10545                .clone()
10546        });
10547
10548        cx.focus(&user_message_editor);
10549        thread_view.read_with(cx, |view, _cx| {
10550            assert_eq!(
10551                view.as_active_thread()
10552                    .and_then(|active| active.editing_message),
10553                Some(0)
10554            );
10555        });
10556
10557        // Ensure to edit the focused message before proceeding otherwise, since
10558        // its content is not different from what was sent, focus will be lost.
10559        user_message_editor.update_in(cx, |editor, window, cx| {
10560            editor.set_text("Original message to edit with ", window, cx)
10561        });
10562
10563        // Create a simple buffer with some text so we can create a selection
10564        // that will then be added to the message being edited.
10565        let (workspace, project) = thread_view.read_with(cx, |thread_view, _cx| {
10566            (thread_view.workspace.clone(), thread_view.project.clone())
10567        });
10568        let buffer = project.update(cx, |project, cx| {
10569            project.create_local_buffer("let a = 10 + 10;", None, false, cx)
10570        });
10571
10572        workspace
10573            .update_in(cx, |workspace, window, cx| {
10574                let editor = cx.new(|cx| {
10575                    let mut editor =
10576                        Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx);
10577
10578                    editor.change_selections(Default::default(), window, cx, |selections| {
10579                        selections.select_ranges([MultiBufferOffset(8)..MultiBufferOffset(15)]);
10580                    });
10581
10582                    editor
10583                });
10584                workspace.add_item_to_active_pane(Box::new(editor), None, false, window, cx);
10585            })
10586            .unwrap();
10587
10588        thread_view.update_in(cx, |view, window, cx| {
10589            assert_eq!(
10590                view.as_active_thread()
10591                    .and_then(|active| active.editing_message),
10592                Some(0)
10593            );
10594            view.insert_selections(window, cx);
10595        });
10596
10597        user_message_editor.read_with(cx, |editor, cx| {
10598            let text = editor.editor().read(cx).text(cx);
10599            let expected_text = String::from("Original message to edit with selection ");
10600
10601            assert_eq!(text, expected_text);
10602        });
10603    }
10604
10605    #[gpui::test]
10606    async fn test_insert_selections(cx: &mut TestAppContext) {
10607        init_test(cx);
10608
10609        let connection = StubAgentConnection::new();
10610        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
10611            acp::ContentChunk::new("Response".into()),
10612        )]);
10613
10614        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
10615        add_to_workspace(thread_view.clone(), cx);
10616
10617        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
10618        message_editor.update_in(cx, |editor, window, cx| {
10619            editor.set_text("Can you review this snippet ", window, cx)
10620        });
10621
10622        // Create a simple buffer with some text so we can create a selection
10623        // that will then be added to the message being edited.
10624        let (workspace, project) = thread_view.read_with(cx, |thread_view, _cx| {
10625            (thread_view.workspace.clone(), thread_view.project.clone())
10626        });
10627        let buffer = project.update(cx, |project, cx| {
10628            project.create_local_buffer("let a = 10 + 10;", None, false, cx)
10629        });
10630
10631        workspace
10632            .update_in(cx, |workspace, window, cx| {
10633                let editor = cx.new(|cx| {
10634                    let mut editor =
10635                        Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx);
10636
10637                    editor.change_selections(Default::default(), window, cx, |selections| {
10638                        selections.select_ranges([MultiBufferOffset(8)..MultiBufferOffset(15)]);
10639                    });
10640
10641                    editor
10642                });
10643                workspace.add_item_to_active_pane(Box::new(editor), None, false, window, cx);
10644            })
10645            .unwrap();
10646
10647        thread_view.update_in(cx, |view, window, cx| {
10648            assert_eq!(
10649                view.as_active_thread()
10650                    .and_then(|active| active.editing_message),
10651                None
10652            );
10653            view.insert_selections(window, cx);
10654        });
10655
10656        thread_view.read_with(cx, |thread_view, cx| {
10657            let text = thread_view.message_editor.read(cx).text(cx);
10658            let expected_txt = String::from("Can you review this snippet selection ");
10659
10660            assert_eq!(text, expected_txt);
10661        })
10662    }
10663
10664    #[gpui::test]
10665    async fn test_tool_permission_buttons_terminal_with_pattern(cx: &mut TestAppContext) {
10666        init_test(cx);
10667
10668        let tool_call_id = acp::ToolCallId::new("terminal-1");
10669        let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Run `cargo build --release`")
10670            .kind(acp::ToolKind::Edit);
10671
10672        let permission_options = ToolPermissionContext::new("terminal", "cargo build --release")
10673            .build_permission_options();
10674
10675        let connection =
10676            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
10677                tool_call_id.clone(),
10678                permission_options,
10679            )]));
10680
10681        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
10682
10683        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
10684
10685        // Disable notifications to avoid popup windows
10686        cx.update(|_window, cx| {
10687            AgentSettings::override_global(
10688                AgentSettings {
10689                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
10690                    ..AgentSettings::get_global(cx).clone()
10691                },
10692                cx,
10693            );
10694        });
10695
10696        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
10697        message_editor.update_in(cx, |editor, window, cx| {
10698            editor.set_text("Run cargo build", window, cx);
10699        });
10700
10701        thread_view.update_in(cx, |thread_view, window, cx| {
10702            thread_view.send(window, cx);
10703        });
10704
10705        cx.run_until_parked();
10706
10707        // Verify the tool call is in WaitingForConfirmation state with the expected options
10708        thread_view.read_with(cx, |thread_view, cx| {
10709            let thread = thread_view
10710                .as_active_thread()
10711                .expect("Thread should exist")
10712                .thread
10713                .clone();
10714            let thread = thread.read(cx);
10715
10716            let tool_call = thread.entries().iter().find_map(|entry| {
10717                if let acp_thread::AgentThreadEntry::ToolCall(call) = entry {
10718                    Some(call)
10719                } else {
10720                    None
10721                }
10722            });
10723
10724            assert!(tool_call.is_some(), "Expected a tool call entry");
10725            let tool_call = tool_call.unwrap();
10726
10727            // Verify it's waiting for confirmation
10728            assert!(
10729                matches!(
10730                    tool_call.status,
10731                    acp_thread::ToolCallStatus::WaitingForConfirmation { .. }
10732                ),
10733                "Expected WaitingForConfirmation status, got {:?}",
10734                tool_call.status
10735            );
10736
10737            // Verify the options count (granularity options only, no separate Deny option)
10738            if let acp_thread::ToolCallStatus::WaitingForConfirmation { options, .. } =
10739                &tool_call.status
10740            {
10741                let PermissionOptions::Dropdown(choices) = options else {
10742                    panic!("Expected dropdown permission options");
10743                };
10744
10745                assert_eq!(
10746                    choices.len(),
10747                    3,
10748                    "Expected 3 permission options (granularity only)"
10749                );
10750
10751                // Verify specific button labels (now using neutral names)
10752                let labels: Vec<&str> = choices
10753                    .iter()
10754                    .map(|choice| choice.allow.name.as_ref())
10755                    .collect();
10756                assert!(
10757                    labels.contains(&"Always for terminal"),
10758                    "Missing 'Always for terminal' option"
10759                );
10760                assert!(
10761                    labels.contains(&"Always for `cargo` commands"),
10762                    "Missing pattern option"
10763                );
10764                assert!(
10765                    labels.contains(&"Only this time"),
10766                    "Missing 'Only this time' option"
10767                );
10768            }
10769        });
10770    }
10771
10772    #[gpui::test]
10773    async fn test_tool_permission_buttons_edit_file_with_path_pattern(cx: &mut TestAppContext) {
10774        init_test(cx);
10775
10776        let tool_call_id = acp::ToolCallId::new("edit-file-1");
10777        let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Edit `src/main.rs`")
10778            .kind(acp::ToolKind::Edit);
10779
10780        let permission_options =
10781            ToolPermissionContext::new("edit_file", "src/main.rs").build_permission_options();
10782
10783        let connection =
10784            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
10785                tool_call_id.clone(),
10786                permission_options,
10787            )]));
10788
10789        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
10790
10791        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
10792
10793        // Disable notifications
10794        cx.update(|_window, cx| {
10795            AgentSettings::override_global(
10796                AgentSettings {
10797                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
10798                    ..AgentSettings::get_global(cx).clone()
10799                },
10800                cx,
10801            );
10802        });
10803
10804        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
10805        message_editor.update_in(cx, |editor, window, cx| {
10806            editor.set_text("Edit the main file", window, cx);
10807        });
10808
10809        thread_view.update_in(cx, |thread_view, window, cx| {
10810            thread_view.send(window, cx);
10811        });
10812
10813        cx.run_until_parked();
10814
10815        // Verify the options
10816        thread_view.read_with(cx, |thread_view, cx| {
10817            let thread = thread_view
10818                .as_active_thread()
10819                .expect("Thread should exist")
10820                .thread
10821                .clone();
10822            let thread = thread.read(cx);
10823
10824            let tool_call = thread.entries().iter().find_map(|entry| {
10825                if let acp_thread::AgentThreadEntry::ToolCall(call) = entry {
10826                    Some(call)
10827                } else {
10828                    None
10829                }
10830            });
10831
10832            assert!(tool_call.is_some(), "Expected a tool call entry");
10833            let tool_call = tool_call.unwrap();
10834
10835            if let acp_thread::ToolCallStatus::WaitingForConfirmation { options, .. } =
10836                &tool_call.status
10837            {
10838                let PermissionOptions::Dropdown(choices) = options else {
10839                    panic!("Expected dropdown permission options");
10840                };
10841
10842                let labels: Vec<&str> = choices
10843                    .iter()
10844                    .map(|choice| choice.allow.name.as_ref())
10845                    .collect();
10846                assert!(
10847                    labels.contains(&"Always for edit file"),
10848                    "Missing 'Always for edit file' option"
10849                );
10850                assert!(
10851                    labels.contains(&"Always for `src/`"),
10852                    "Missing path pattern option"
10853                );
10854            } else {
10855                panic!("Expected WaitingForConfirmation status");
10856            }
10857        });
10858    }
10859
10860    #[gpui::test]
10861    async fn test_tool_permission_buttons_fetch_with_domain_pattern(cx: &mut TestAppContext) {
10862        init_test(cx);
10863
10864        let tool_call_id = acp::ToolCallId::new("fetch-1");
10865        let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Fetch `https://docs.rs/gpui`")
10866            .kind(acp::ToolKind::Fetch);
10867
10868        let permission_options =
10869            ToolPermissionContext::new("fetch", "https://docs.rs/gpui").build_permission_options();
10870
10871        let connection =
10872            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
10873                tool_call_id.clone(),
10874                permission_options,
10875            )]));
10876
10877        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
10878
10879        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
10880
10881        // Disable notifications
10882        cx.update(|_window, cx| {
10883            AgentSettings::override_global(
10884                AgentSettings {
10885                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
10886                    ..AgentSettings::get_global(cx).clone()
10887                },
10888                cx,
10889            );
10890        });
10891
10892        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
10893        message_editor.update_in(cx, |editor, window, cx| {
10894            editor.set_text("Fetch the docs", window, cx);
10895        });
10896
10897        thread_view.update_in(cx, |thread_view, window, cx| {
10898            thread_view.send(window, cx);
10899        });
10900
10901        cx.run_until_parked();
10902
10903        // Verify the options
10904        thread_view.read_with(cx, |thread_view, cx| {
10905            let thread = thread_view
10906                .as_active_thread()
10907                .expect("Thread should exist")
10908                .thread
10909                .clone();
10910            let thread = thread.read(cx);
10911
10912            let tool_call = thread.entries().iter().find_map(|entry| {
10913                if let acp_thread::AgentThreadEntry::ToolCall(call) = entry {
10914                    Some(call)
10915                } else {
10916                    None
10917                }
10918            });
10919
10920            assert!(tool_call.is_some(), "Expected a tool call entry");
10921            let tool_call = tool_call.unwrap();
10922
10923            if let acp_thread::ToolCallStatus::WaitingForConfirmation { options, .. } =
10924                &tool_call.status
10925            {
10926                let PermissionOptions::Dropdown(choices) = options else {
10927                    panic!("Expected dropdown permission options");
10928                };
10929
10930                let labels: Vec<&str> = choices
10931                    .iter()
10932                    .map(|choice| choice.allow.name.as_ref())
10933                    .collect();
10934                assert!(
10935                    labels.contains(&"Always for fetch"),
10936                    "Missing 'Always for fetch' option"
10937                );
10938                assert!(
10939                    labels.contains(&"Always for `docs.rs`"),
10940                    "Missing domain pattern option"
10941                );
10942            } else {
10943                panic!("Expected WaitingForConfirmation status");
10944            }
10945        });
10946    }
10947
10948    #[gpui::test]
10949    async fn test_tool_permission_buttons_without_pattern(cx: &mut TestAppContext) {
10950        init_test(cx);
10951
10952        let tool_call_id = acp::ToolCallId::new("terminal-no-pattern-1");
10953        let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Run `./deploy.sh --production`")
10954            .kind(acp::ToolKind::Edit);
10955
10956        // No pattern button since ./deploy.sh doesn't match the alphanumeric pattern
10957        let permission_options = ToolPermissionContext::new("terminal", "./deploy.sh --production")
10958            .build_permission_options();
10959
10960        let connection =
10961            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
10962                tool_call_id.clone(),
10963                permission_options,
10964            )]));
10965
10966        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
10967
10968        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
10969
10970        // Disable notifications
10971        cx.update(|_window, cx| {
10972            AgentSettings::override_global(
10973                AgentSettings {
10974                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
10975                    ..AgentSettings::get_global(cx).clone()
10976                },
10977                cx,
10978            );
10979        });
10980
10981        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
10982        message_editor.update_in(cx, |editor, window, cx| {
10983            editor.set_text("Run the deploy script", window, cx);
10984        });
10985
10986        thread_view.update_in(cx, |thread_view, window, cx| {
10987            thread_view.send(window, cx);
10988        });
10989
10990        cx.run_until_parked();
10991
10992        // Verify only 2 options (no pattern button when command doesn't match pattern)
10993        thread_view.read_with(cx, |thread_view, cx| {
10994            let thread = thread_view
10995                .as_active_thread()
10996                .expect("Thread should exist")
10997                .thread
10998                .clone();
10999            let thread = thread.read(cx);
11000
11001            let tool_call = thread.entries().iter().find_map(|entry| {
11002                if let acp_thread::AgentThreadEntry::ToolCall(call) = entry {
11003                    Some(call)
11004                } else {
11005                    None
11006                }
11007            });
11008
11009            assert!(tool_call.is_some(), "Expected a tool call entry");
11010            let tool_call = tool_call.unwrap();
11011
11012            if let acp_thread::ToolCallStatus::WaitingForConfirmation { options, .. } =
11013                &tool_call.status
11014            {
11015                let PermissionOptions::Dropdown(choices) = options else {
11016                    panic!("Expected dropdown permission options");
11017                };
11018
11019                assert_eq!(
11020                    choices.len(),
11021                    2,
11022                    "Expected 2 permission options (no pattern option)"
11023                );
11024
11025                let labels: Vec<&str> = choices
11026                    .iter()
11027                    .map(|choice| choice.allow.name.as_ref())
11028                    .collect();
11029                assert!(
11030                    labels.contains(&"Always for terminal"),
11031                    "Missing 'Always for terminal' option"
11032                );
11033                assert!(
11034                    labels.contains(&"Only this time"),
11035                    "Missing 'Only this time' option"
11036                );
11037                // Should NOT contain a pattern option
11038                assert!(
11039                    !labels.iter().any(|l| l.contains("commands")),
11040                    "Should not have pattern option"
11041                );
11042            } else {
11043                panic!("Expected WaitingForConfirmation status");
11044            }
11045        });
11046    }
11047
11048    #[gpui::test]
11049    async fn test_authorize_tool_call_action_triggers_authorization(cx: &mut TestAppContext) {
11050        init_test(cx);
11051
11052        let tool_call_id = acp::ToolCallId::new("action-test-1");
11053        let tool_call =
11054            acp::ToolCall::new(tool_call_id.clone(), "Run `cargo test`").kind(acp::ToolKind::Edit);
11055
11056        let permission_options =
11057            ToolPermissionContext::new("terminal", "cargo test").build_permission_options();
11058
11059        let connection =
11060            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
11061                tool_call_id.clone(),
11062                permission_options,
11063            )]));
11064
11065        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
11066
11067        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
11068        add_to_workspace(thread_view.clone(), cx);
11069
11070        cx.update(|_window, cx| {
11071            AgentSettings::override_global(
11072                AgentSettings {
11073                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
11074                    ..AgentSettings::get_global(cx).clone()
11075                },
11076                cx,
11077            );
11078        });
11079
11080        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
11081        message_editor.update_in(cx, |editor, window, cx| {
11082            editor.set_text("Run tests", window, cx);
11083        });
11084
11085        thread_view.update_in(cx, |thread_view, window, cx| {
11086            thread_view.send(window, cx);
11087        });
11088
11089        cx.run_until_parked();
11090
11091        // Verify tool call is waiting for confirmation
11092        thread_view.read_with(cx, |thread_view, cx| {
11093            let thread = thread_view
11094                .as_active_thread()
11095                .expect("Thread should exist")
11096                .thread
11097                .clone();
11098            let thread = thread.read(cx);
11099            let tool_call = thread.first_tool_awaiting_confirmation();
11100            assert!(
11101                tool_call.is_some(),
11102                "Expected a tool call waiting for confirmation"
11103            );
11104        });
11105
11106        // Dispatch the AuthorizeToolCall action (simulating dropdown menu selection)
11107        thread_view.update_in(cx, |_, window, cx| {
11108            window.dispatch_action(
11109                crate::AuthorizeToolCall {
11110                    tool_call_id: "action-test-1".to_string(),
11111                    option_id: "allow".to_string(),
11112                    option_kind: "AllowOnce".to_string(),
11113                }
11114                .boxed_clone(),
11115                cx,
11116            );
11117        });
11118
11119        cx.run_until_parked();
11120
11121        // Verify tool call is no longer waiting for confirmation (was authorized)
11122        thread_view.read_with(cx, |thread_view, cx| {
11123            let thread = thread_view.as_active_thread().expect("Thread should exist").thread.clone();
11124            let thread = thread.read(cx);
11125            let tool_call = thread.first_tool_awaiting_confirmation();
11126            assert!(
11127                tool_call.is_none(),
11128                "Tool call should no longer be waiting for confirmation after AuthorizeToolCall action"
11129            );
11130        });
11131    }
11132
11133    #[gpui::test]
11134    async fn test_authorize_tool_call_action_with_pattern_option(cx: &mut TestAppContext) {
11135        init_test(cx);
11136
11137        let tool_call_id = acp::ToolCallId::new("pattern-action-test-1");
11138        let tool_call =
11139            acp::ToolCall::new(tool_call_id.clone(), "Run `npm install`").kind(acp::ToolKind::Edit);
11140
11141        let permission_options =
11142            ToolPermissionContext::new("terminal", "npm install").build_permission_options();
11143
11144        let connection =
11145            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
11146                tool_call_id.clone(),
11147                permission_options.clone(),
11148            )]));
11149
11150        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
11151
11152        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
11153        add_to_workspace(thread_view.clone(), cx);
11154
11155        cx.update(|_window, cx| {
11156            AgentSettings::override_global(
11157                AgentSettings {
11158                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
11159                    ..AgentSettings::get_global(cx).clone()
11160                },
11161                cx,
11162            );
11163        });
11164
11165        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
11166        message_editor.update_in(cx, |editor, window, cx| {
11167            editor.set_text("Install dependencies", window, cx);
11168        });
11169
11170        thread_view.update_in(cx, |thread_view, window, cx| {
11171            thread_view.send(window, cx);
11172        });
11173
11174        cx.run_until_parked();
11175
11176        // Find the pattern option ID
11177        let pattern_option = match &permission_options {
11178            PermissionOptions::Dropdown(choices) => choices
11179                .iter()
11180                .find(|choice| {
11181                    choice
11182                        .allow
11183                        .option_id
11184                        .0
11185                        .starts_with("always_allow_pattern:")
11186                })
11187                .map(|choice| &choice.allow)
11188                .expect("Should have a pattern option for npm command"),
11189            _ => panic!("Expected dropdown permission options"),
11190        };
11191
11192        // Dispatch action with the pattern option (simulating "Always allow `npm` commands")
11193        thread_view.update_in(cx, |_, window, cx| {
11194            window.dispatch_action(
11195                crate::AuthorizeToolCall {
11196                    tool_call_id: "pattern-action-test-1".to_string(),
11197                    option_id: pattern_option.option_id.0.to_string(),
11198                    option_kind: "AllowAlways".to_string(),
11199                }
11200                .boxed_clone(),
11201                cx,
11202            );
11203        });
11204
11205        cx.run_until_parked();
11206
11207        // Verify tool call was authorized
11208        thread_view.read_with(cx, |thread_view, cx| {
11209            let thread = thread_view
11210                .as_active_thread()
11211                .expect("Thread should exist")
11212                .thread
11213                .clone();
11214            let thread = thread.read(cx);
11215            let tool_call = thread.first_tool_awaiting_confirmation();
11216            assert!(
11217                tool_call.is_none(),
11218                "Tool call should be authorized after selecting pattern option"
11219            );
11220        });
11221    }
11222
11223    #[gpui::test]
11224    async fn test_granularity_selection_updates_state(cx: &mut TestAppContext) {
11225        init_test(cx);
11226
11227        let tool_call_id = acp::ToolCallId::new("granularity-test-1");
11228        let tool_call =
11229            acp::ToolCall::new(tool_call_id.clone(), "Run `cargo build`").kind(acp::ToolKind::Edit);
11230
11231        let permission_options =
11232            ToolPermissionContext::new("terminal", "cargo build").build_permission_options();
11233
11234        let connection =
11235            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
11236                tool_call_id.clone(),
11237                permission_options.clone(),
11238            )]));
11239
11240        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
11241
11242        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
11243        add_to_workspace(thread_view.clone(), cx);
11244
11245        cx.update(|_window, cx| {
11246            AgentSettings::override_global(
11247                AgentSettings {
11248                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
11249                    ..AgentSettings::get_global(cx).clone()
11250                },
11251                cx,
11252            );
11253        });
11254
11255        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
11256        message_editor.update_in(cx, |editor, window, cx| {
11257            editor.set_text("Build the project", window, cx);
11258        });
11259
11260        thread_view.update_in(cx, |thread_view, window, cx| {
11261            thread_view.send(window, cx);
11262        });
11263
11264        cx.run_until_parked();
11265
11266        // Verify default granularity is the last option (index 2 = "Only this time")
11267        thread_view.read_with(cx, |thread_view, _cx| {
11268            let state = thread_view.as_active_thread().unwrap();
11269            let selected = state.selected_permission_granularity.get(&tool_call_id);
11270            assert!(
11271                selected.is_none(),
11272                "Should have no selection initially (defaults to last)"
11273            );
11274        });
11275
11276        // Select the first option (index 0 = "Always for terminal")
11277        thread_view.update_in(cx, |_, window, cx| {
11278            window.dispatch_action(
11279                crate::SelectPermissionGranularity {
11280                    tool_call_id: "granularity-test-1".to_string(),
11281                    index: 0,
11282                }
11283                .boxed_clone(),
11284                cx,
11285            );
11286        });
11287
11288        cx.run_until_parked();
11289
11290        // Verify the selection was updated
11291        thread_view.read_with(cx, |thread_view, _cx| {
11292            let state = thread_view.as_active_thread().unwrap();
11293            let selected = state.selected_permission_granularity.get(&tool_call_id);
11294            assert_eq!(selected, Some(&0), "Should have selected index 0");
11295        });
11296    }
11297
11298    #[gpui::test]
11299    async fn test_allow_button_uses_selected_granularity(cx: &mut TestAppContext) {
11300        init_test(cx);
11301
11302        let tool_call_id = acp::ToolCallId::new("allow-granularity-test-1");
11303        let tool_call =
11304            acp::ToolCall::new(tool_call_id.clone(), "Run `npm install`").kind(acp::ToolKind::Edit);
11305
11306        let permission_options =
11307            ToolPermissionContext::new("terminal", "npm install").build_permission_options();
11308
11309        // Verify we have the expected options
11310        let PermissionOptions::Dropdown(choices) = &permission_options else {
11311            panic!("Expected dropdown permission options");
11312        };
11313
11314        assert_eq!(choices.len(), 3);
11315        assert!(
11316            choices[0]
11317                .allow
11318                .option_id
11319                .0
11320                .contains("always_allow:terminal")
11321        );
11322        assert!(
11323            choices[1]
11324                .allow
11325                .option_id
11326                .0
11327                .contains("always_allow_pattern:terminal")
11328        );
11329        assert_eq!(choices[2].allow.option_id.0.as_ref(), "allow");
11330
11331        let connection =
11332            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
11333                tool_call_id.clone(),
11334                permission_options.clone(),
11335            )]));
11336
11337        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
11338
11339        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
11340        add_to_workspace(thread_view.clone(), cx);
11341
11342        cx.update(|_window, cx| {
11343            AgentSettings::override_global(
11344                AgentSettings {
11345                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
11346                    ..AgentSettings::get_global(cx).clone()
11347                },
11348                cx,
11349            );
11350        });
11351
11352        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
11353        message_editor.update_in(cx, |editor, window, cx| {
11354            editor.set_text("Install dependencies", window, cx);
11355        });
11356
11357        thread_view.update_in(cx, |thread_view, window, cx| {
11358            thread_view.send(window, cx);
11359        });
11360
11361        cx.run_until_parked();
11362
11363        // Select the pattern option (index 1 = "Always for `npm` commands")
11364        thread_view.update_in(cx, |_, window, cx| {
11365            window.dispatch_action(
11366                crate::SelectPermissionGranularity {
11367                    tool_call_id: "allow-granularity-test-1".to_string(),
11368                    index: 1,
11369                }
11370                .boxed_clone(),
11371                cx,
11372            );
11373        });
11374
11375        cx.run_until_parked();
11376
11377        // Simulate clicking the Allow button by dispatching AllowOnce action
11378        // which should use the selected granularity
11379        thread_view.update_in(cx, |thread_view, window, cx| {
11380            thread_view.allow_once(&AllowOnce, window, cx);
11381        });
11382
11383        cx.run_until_parked();
11384
11385        // Verify tool call was authorized
11386        thread_view.read_with(cx, |thread_view, cx| {
11387            let thread = thread_view
11388                .as_active_thread()
11389                .expect("Thread should exist")
11390                .thread
11391                .clone();
11392            let thread = thread.read(cx);
11393            let tool_call = thread.first_tool_awaiting_confirmation();
11394            assert!(
11395                tool_call.is_none(),
11396                "Tool call should be authorized after Allow with pattern granularity"
11397            );
11398        });
11399    }
11400
11401    #[gpui::test]
11402    async fn test_deny_button_uses_selected_granularity(cx: &mut TestAppContext) {
11403        init_test(cx);
11404
11405        let tool_call_id = acp::ToolCallId::new("deny-granularity-test-1");
11406        let tool_call =
11407            acp::ToolCall::new(tool_call_id.clone(), "Run `git push`").kind(acp::ToolKind::Edit);
11408
11409        let permission_options =
11410            ToolPermissionContext::new("terminal", "git push").build_permission_options();
11411
11412        let connection =
11413            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
11414                tool_call_id.clone(),
11415                permission_options.clone(),
11416            )]));
11417
11418        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
11419
11420        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
11421        add_to_workspace(thread_view.clone(), cx);
11422
11423        cx.update(|_window, cx| {
11424            AgentSettings::override_global(
11425                AgentSettings {
11426                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
11427                    ..AgentSettings::get_global(cx).clone()
11428                },
11429                cx,
11430            );
11431        });
11432
11433        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
11434        message_editor.update_in(cx, |editor, window, cx| {
11435            editor.set_text("Push changes", window, cx);
11436        });
11437
11438        thread_view.update_in(cx, |thread_view, window, cx| {
11439            thread_view.send(window, cx);
11440        });
11441
11442        cx.run_until_parked();
11443
11444        // Use default granularity (last option = "Only this time")
11445        // Simulate clicking the Deny button
11446        thread_view.update_in(cx, |thread_view, window, cx| {
11447            thread_view.reject_once(&RejectOnce, window, cx);
11448        });
11449
11450        cx.run_until_parked();
11451
11452        // Verify tool call was rejected (no longer waiting for confirmation)
11453        thread_view.read_with(cx, |thread_view, cx| {
11454            let thread = thread_view
11455                .as_active_thread()
11456                .expect("Thread should exist")
11457                .thread
11458                .clone();
11459            let thread = thread.read(cx);
11460            let tool_call = thread.first_tool_awaiting_confirmation();
11461            assert!(
11462                tool_call.is_none(),
11463                "Tool call should be rejected after Deny"
11464            );
11465        });
11466    }
11467
11468    #[gpui::test]
11469    async fn test_option_id_transformation_for_allow() {
11470        let permission_options = ToolPermissionContext::new("terminal", "cargo build --release")
11471            .build_permission_options();
11472
11473        let PermissionOptions::Dropdown(choices) = permission_options else {
11474            panic!("Expected dropdown permission options");
11475        };
11476
11477        let allow_ids: Vec<String> = choices
11478            .iter()
11479            .map(|choice| choice.allow.option_id.0.to_string())
11480            .collect();
11481
11482        assert!(allow_ids.contains(&"always_allow:terminal".to_string()));
11483        assert!(allow_ids.contains(&"allow".to_string()));
11484        assert!(
11485            allow_ids
11486                .iter()
11487                .any(|id| id.starts_with("always_allow_pattern:terminal:")),
11488            "Missing allow pattern option"
11489        );
11490    }
11491
11492    #[gpui::test]
11493    async fn test_option_id_transformation_for_deny() {
11494        let permission_options = ToolPermissionContext::new("terminal", "cargo build --release")
11495            .build_permission_options();
11496
11497        let PermissionOptions::Dropdown(choices) = permission_options else {
11498            panic!("Expected dropdown permission options");
11499        };
11500
11501        let deny_ids: Vec<String> = choices
11502            .iter()
11503            .map(|choice| choice.deny.option_id.0.to_string())
11504            .collect();
11505
11506        assert!(deny_ids.contains(&"always_deny:terminal".to_string()));
11507        assert!(deny_ids.contains(&"deny".to_string()));
11508        assert!(
11509            deny_ids
11510                .iter()
11511                .any(|id| id.starts_with("always_deny_pattern:terminal:")),
11512            "Missing deny pattern option"
11513        );
11514    }
11515}