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