thread_view.rs

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