thread_view.rs

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