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