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