thread_view.rs

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