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