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