thread_view.rs

   1use crate::{
   2    DEFAULT_THREAD_TITLE, SelectPermissionGranularity,
   3    agent_configuration::configure_context_server_modal::default_markdown_style,
   4};
   5use std::cell::RefCell;
   6
   7use acp_thread::{ContentBlock, PlanEntry};
   8use cloud_api_types::{SubmitAgentThreadFeedbackBody, SubmitAgentThreadFeedbackCommentsBody};
   9use editor::actions::OpenExcerpts;
  10use feature_flags::AcpBetaFeatureFlag;
  11
  12use crate::message_editor::SharedSessionCapabilities;
  13
  14use gpui::{Corner, List};
  15use heapless::Vec as ArrayVec;
  16use language_model::{LanguageModelEffortLevel, Speed};
  17use settings::{SidebarSide, update_settings_file};
  18use ui::{ButtonLike, SpinnerLabel, SpinnerVariant, SplitButton, SplitButtonStyle, Tab};
  19use workspace::SERIALIZATION_THROTTLE_TIME;
  20
  21use super::*;
  22
  23#[derive(Default)]
  24struct ThreadFeedbackState {
  25    feedback: Option<ThreadFeedback>,
  26    comments_editor: Option<Entity<Editor>>,
  27}
  28
  29impl ThreadFeedbackState {
  30    pub fn submit(
  31        &mut self,
  32        thread: Entity<AcpThread>,
  33        feedback: ThreadFeedback,
  34        window: &mut Window,
  35        cx: &mut App,
  36    ) {
  37        let Some(telemetry) = thread.read(cx).connection().telemetry() else {
  38            return;
  39        };
  40
  41        let project = thread.read(cx).project().read(cx);
  42        let client = project.client();
  43        let user_store = project.user_store();
  44        let organization = user_store.read(cx).current_organization();
  45
  46        if self.feedback == Some(feedback) {
  47            return;
  48        }
  49
  50        self.feedback = Some(feedback);
  51        match feedback {
  52            ThreadFeedback::Positive => {
  53                self.comments_editor = None;
  54            }
  55            ThreadFeedback::Negative => {
  56                self.comments_editor = Some(Self::build_feedback_comments_editor(window, cx));
  57            }
  58        }
  59        let session_id = thread.read(cx).session_id().clone();
  60        let parent_session_id = thread.read(cx).parent_session_id().cloned();
  61        let agent_telemetry_id = thread.read(cx).connection().telemetry_id();
  62        let task = telemetry.thread_data(&session_id, cx);
  63        let rating = match feedback {
  64            ThreadFeedback::Positive => "positive",
  65            ThreadFeedback::Negative => "negative",
  66        };
  67        cx.background_spawn(async move {
  68            let thread = task.await?;
  69
  70            client
  71                .cloud_client()
  72                .submit_agent_feedback(SubmitAgentThreadFeedbackBody {
  73                    organization_id: organization.map(|organization| organization.id.clone()),
  74                    agent: agent_telemetry_id.to_string(),
  75                    session_id: session_id.to_string(),
  76                    parent_session_id: parent_session_id.map(|id| id.to_string()),
  77                    rating: rating.to_string(),
  78                    thread,
  79                })
  80                .await?;
  81
  82            anyhow::Ok(())
  83        })
  84        .detach_and_log_err(cx);
  85    }
  86
  87    pub fn submit_comments(&mut self, thread: Entity<AcpThread>, cx: &mut App) {
  88        let Some(telemetry) = thread.read(cx).connection().telemetry() else {
  89            return;
  90        };
  91
  92        let Some(comments) = self
  93            .comments_editor
  94            .as_ref()
  95            .map(|editor| editor.read(cx).text(cx))
  96            .filter(|text| !text.trim().is_empty())
  97        else {
  98            return;
  99        };
 100
 101        self.comments_editor.take();
 102
 103        let project = thread.read(cx).project().read(cx);
 104        let client = project.client();
 105        let user_store = project.user_store();
 106        let organization = user_store.read(cx).current_organization();
 107
 108        let session_id = thread.read(cx).session_id().clone();
 109        let agent_telemetry_id = thread.read(cx).connection().telemetry_id();
 110        let task = telemetry.thread_data(&session_id, cx);
 111        cx.background_spawn(async move {
 112            let thread = task.await?;
 113
 114            client
 115                .cloud_client()
 116                .submit_agent_feedback_comments(SubmitAgentThreadFeedbackCommentsBody {
 117                    organization_id: organization.map(|organization| organization.id.clone()),
 118                    agent: agent_telemetry_id.to_string(),
 119                    session_id: session_id.to_string(),
 120                    comments,
 121                    thread,
 122                })
 123                .await?;
 124
 125            anyhow::Ok(())
 126        })
 127        .detach_and_log_err(cx);
 128    }
 129
 130    pub fn clear(&mut self) {
 131        *self = Self::default()
 132    }
 133
 134    pub fn dismiss_comments(&mut self) {
 135        self.comments_editor.take();
 136    }
 137
 138    fn build_feedback_comments_editor(window: &mut Window, cx: &mut App) -> Entity<Editor> {
 139        let buffer = cx.new(|cx| {
 140            let empty_string = String::new();
 141            MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx)
 142        });
 143
 144        let editor = cx.new(|cx| {
 145            let mut editor = Editor::new(
 146                editor::EditorMode::AutoHeight {
 147                    min_lines: 1,
 148                    max_lines: Some(4),
 149                },
 150                buffer,
 151                None,
 152                window,
 153                cx,
 154            );
 155            editor.set_placeholder_text(
 156                "What went wrong? Share your feedback so we can improve.",
 157                window,
 158                cx,
 159            );
 160            editor
 161        });
 162
 163        editor.read(cx).focus_handle(cx).focus(window, cx);
 164        editor
 165    }
 166}
 167
 168struct GeneratingSpinner {
 169    variant: SpinnerVariant,
 170}
 171
 172impl GeneratingSpinner {
 173    fn new(variant: SpinnerVariant) -> Self {
 174        Self { variant }
 175    }
 176}
 177
 178impl Render for GeneratingSpinner {
 179    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
 180        SpinnerLabel::with_variant(self.variant).size(LabelSize::Small)
 181    }
 182}
 183
 184#[derive(IntoElement)]
 185struct GeneratingSpinnerElement {
 186    variant: SpinnerVariant,
 187}
 188
 189impl GeneratingSpinnerElement {
 190    fn new(variant: SpinnerVariant) -> Self {
 191        Self { variant }
 192    }
 193}
 194
 195impl RenderOnce for GeneratingSpinnerElement {
 196    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
 197        let id = match self.variant {
 198            SpinnerVariant::Dots => "generating-spinner-view",
 199            SpinnerVariant::Sand => "confirmation-spinner-view",
 200            _ => "spinner-view",
 201        };
 202        window.with_id(id, |window| {
 203            window.use_state(cx, |_, _| GeneratingSpinner::new(self.variant))
 204        })
 205    }
 206}
 207
 208pub enum AcpThreadViewEvent {
 209    MessageSentOrQueued,
 210}
 211
 212impl EventEmitter<AcpThreadViewEvent> for ThreadView {}
 213
 214/// Tracks the user's permission dropdown selection state for a specific tool call.
 215///
 216/// Default (no entry in the map) means the last dropdown choice is selected,
 217/// which is typically "Only this time".
 218#[derive(Clone)]
 219pub(crate) enum PermissionSelection {
 220    /// A specific choice from the dropdown (e.g., "Always for terminal", "Only this time").
 221    /// The index corresponds to the position in the `choices` list from `PermissionOptions`.
 222    Choice(usize),
 223    /// "Select options…" mode where individual command patterns can be toggled.
 224    /// Contains the indices of checked patterns in the `patterns` list.
 225    /// All patterns start checked when this mode is first activated.
 226    SelectedPatterns(Vec<usize>),
 227}
 228
 229impl PermissionSelection {
 230    /// Returns the choice index if a specific dropdown choice is selected,
 231    /// or `None` if in per-command pattern mode.
 232    pub(crate) fn choice_index(&self) -> Option<usize> {
 233        match self {
 234            Self::Choice(index) => Some(*index),
 235            Self::SelectedPatterns(_) => None,
 236        }
 237    }
 238
 239    fn is_pattern_checked(&self, index: usize) -> bool {
 240        match self {
 241            Self::SelectedPatterns(checked) => checked.contains(&index),
 242            _ => false,
 243        }
 244    }
 245
 246    fn has_any_checked_patterns(&self) -> bool {
 247        match self {
 248            Self::SelectedPatterns(checked) => !checked.is_empty(),
 249            _ => false,
 250        }
 251    }
 252
 253    fn toggle_pattern(&mut self, index: usize) {
 254        if let Self::SelectedPatterns(checked) = self {
 255            if let Some(pos) = checked.iter().position(|&i| i == index) {
 256                checked.swap_remove(pos);
 257            } else {
 258                checked.push(index);
 259            }
 260        }
 261    }
 262}
 263
 264pub struct ThreadView {
 265    pub session_id: acp::SessionId,
 266    pub parent_session_id: Option<acp::SessionId>,
 267    pub thread: Entity<AcpThread>,
 268    pub(crate) conversation: Entity<super::Conversation>,
 269    pub server_view: WeakEntity<ConversationView>,
 270    pub agent_icon: IconName,
 271    pub agent_icon_from_external_svg: Option<SharedString>,
 272    pub agent_id: AgentId,
 273    pub focus_handle: FocusHandle,
 274    pub workspace: WeakEntity<Workspace>,
 275    pub entry_view_state: Entity<EntryViewState>,
 276    pub title_editor: Entity<Editor>,
 277    pub config_options_view: Option<Entity<ConfigOptionsView>>,
 278    pub mode_selector: Option<Entity<ModeSelector>>,
 279    pub model_selector: Option<Entity<ModelSelectorPopover>>,
 280    pub profile_selector: Option<Entity<ProfileSelector>>,
 281    pub permission_dropdown_handle: PopoverMenuHandle<ContextMenu>,
 282    pub thread_retry_status: Option<RetryStatus>,
 283    pub(super) thread_error: Option<ThreadError>,
 284    pub thread_error_markdown: Option<Entity<Markdown>>,
 285    pub token_limit_callout_dismissed: bool,
 286    pub last_token_limit_telemetry: Option<acp_thread::TokenUsageRatio>,
 287    thread_feedback: ThreadFeedbackState,
 288    pub list_state: ListState,
 289    pub session_capabilities: SharedSessionCapabilities,
 290    /// Tracks which tool calls have their content/output expanded.
 291    /// Used for showing/hiding tool call results, terminal output, etc.
 292    pub expanded_tool_calls: HashSet<agent_client_protocol::ToolCallId>,
 293    pub expanded_tool_call_raw_inputs: HashSet<agent_client_protocol::ToolCallId>,
 294    pub expanded_thinking_blocks: HashSet<(usize, usize)>,
 295    auto_expanded_thinking_block: Option<(usize, usize)>,
 296    user_toggled_thinking_blocks: HashSet<(usize, usize)>,
 297    pub subagent_scroll_handles: RefCell<HashMap<acp::SessionId, ScrollHandle>>,
 298    pub edits_expanded: bool,
 299    pub plan_expanded: bool,
 300    pub queue_expanded: bool,
 301    pub editor_expanded: bool,
 302    pub should_be_following: bool,
 303    pub editing_message: Option<usize>,
 304    pub local_queued_messages: Vec<QueuedMessage>,
 305    pub queued_message_editors: Vec<Entity<MessageEditor>>,
 306    pub queued_message_editor_subscriptions: Vec<Subscription>,
 307    pub last_synced_queue_length: usize,
 308    pub turn_fields: TurnFields,
 309    pub discarded_partial_edits: HashSet<agent_client_protocol::ToolCallId>,
 310    pub is_loading_contents: bool,
 311    pub new_server_version_available: Option<SharedString>,
 312    pub resumed_without_history: bool,
 313    pub(crate) permission_selections:
 314        HashMap<agent_client_protocol::ToolCallId, PermissionSelection>,
 315    pub resume_thread_metadata: Option<AgentSessionInfo>,
 316    pub _cancel_task: Option<Task<()>>,
 317    _save_task: Option<Task<()>>,
 318    _draft_resolve_task: Option<Task<()>>,
 319    pub skip_queue_processing_count: usize,
 320    pub user_interrupted_generation: bool,
 321    pub can_fast_track_queue: bool,
 322    pub hovered_edited_file_buttons: Option<usize>,
 323    pub in_flight_prompt: Option<Vec<acp::ContentBlock>>,
 324    pub _subscriptions: Vec<Subscription>,
 325    pub message_editor: Entity<MessageEditor>,
 326    pub add_context_menu_handle: PopoverMenuHandle<ContextMenu>,
 327    pub thinking_effort_menu_handle: PopoverMenuHandle<ContextMenu>,
 328    pub project: WeakEntity<Project>,
 329    pub recent_history_entries: Vec<AgentSessionInfo>,
 330    pub hovered_recent_history_item: Option<usize>,
 331    pub show_external_source_prompt_warning: bool,
 332    pub show_codex_windows_warning: bool,
 333    pub multi_root_callout_dismissed: bool,
 334    pub generating_indicator_in_list: bool,
 335    pub history: Option<Entity<ThreadHistory>>,
 336    pub _history_subscription: Option<Subscription>,
 337}
 338impl Focusable for ThreadView {
 339    fn focus_handle(&self, cx: &App) -> FocusHandle {
 340        if self.parent_session_id.is_some() {
 341            self.focus_handle.clone()
 342        } else {
 343            self.active_editor(cx).focus_handle(cx)
 344        }
 345    }
 346}
 347
 348#[derive(Default)]
 349pub struct TurnFields {
 350    pub _turn_timer_task: Option<Task<()>>,
 351    pub last_turn_duration: Option<Duration>,
 352    pub last_turn_tokens: Option<u64>,
 353    pub turn_generation: usize,
 354    pub turn_started_at: Option<Instant>,
 355    pub turn_tokens: Option<u64>,
 356}
 357
 358impl ThreadView {
 359    pub(crate) fn new(
 360        thread: Entity<AcpThread>,
 361        conversation: Entity<super::Conversation>,
 362        server_view: WeakEntity<ConversationView>,
 363        agent_icon: IconName,
 364        agent_icon_from_external_svg: Option<SharedString>,
 365        agent_id: AgentId,
 366        agent_display_name: SharedString,
 367        workspace: WeakEntity<Workspace>,
 368        entry_view_state: Entity<EntryViewState>,
 369        config_options_view: Option<Entity<ConfigOptionsView>>,
 370        mode_selector: Option<Entity<ModeSelector>>,
 371        model_selector: Option<Entity<ModelSelectorPopover>>,
 372        profile_selector: Option<Entity<ProfileSelector>>,
 373        list_state: ListState,
 374        session_capabilities: SharedSessionCapabilities,
 375        resumed_without_history: bool,
 376        project: WeakEntity<Project>,
 377        thread_store: Option<Entity<ThreadStore>>,
 378        history: Option<Entity<ThreadHistory>>,
 379        prompt_store: Option<Entity<PromptStore>>,
 380        initial_content: Option<AgentInitialContent>,
 381        mut subscriptions: Vec<Subscription>,
 382        window: &mut Window,
 383        cx: &mut Context<Self>,
 384    ) -> Self {
 385        let session_id = thread.read(cx).session_id().clone();
 386        let parent_session_id = thread.read(cx).parent_session_id().cloned();
 387
 388        let has_commands = !session_capabilities.read().available_commands().is_empty();
 389        let placeholder = placeholder_text(agent_display_name.as_ref(), has_commands);
 390
 391        let history_subscription = history.as_ref().map(|h| {
 392            cx.observe(h, |this, history, cx| {
 393                this.update_recent_history_from_cache(&history, cx);
 394            })
 395        });
 396
 397        let mut should_auto_submit = false;
 398        let mut show_external_source_prompt_warning = false;
 399
 400        let message_editor = cx.new(|cx| {
 401            let mut editor = MessageEditor::new(
 402                workspace.clone(),
 403                project.clone(),
 404                thread_store,
 405                history.as_ref().map(|h| h.downgrade()),
 406                prompt_store,
 407                session_capabilities.clone(),
 408                agent_id.clone(),
 409                &placeholder,
 410                editor::EditorMode::AutoHeight {
 411                    min_lines: AgentSettings::get_global(cx).message_editor_min_lines,
 412                    max_lines: Some(AgentSettings::get_global(cx).set_message_editor_max_lines()),
 413                },
 414                window,
 415                cx,
 416            );
 417            if let Some(content) = initial_content {
 418                match content {
 419                    AgentInitialContent::ThreadSummary { session_id, title } => {
 420                        editor.insert_thread_summary(session_id, title, window, cx);
 421                    }
 422                    AgentInitialContent::ContentBlock {
 423                        blocks,
 424                        auto_submit,
 425                    } => {
 426                        should_auto_submit = auto_submit;
 427                        editor.set_message(blocks, window, cx);
 428                    }
 429                    AgentInitialContent::FromExternalSource(prompt) => {
 430                        show_external_source_prompt_warning = true;
 431                        // SECURITY: Be explicit about not auto submitting prompt from external source.
 432                        should_auto_submit = false;
 433                        editor.set_message(
 434                            vec![acp::ContentBlock::Text(acp::TextContent::new(
 435                                prompt.into_string(),
 436                            ))],
 437                            window,
 438                            cx,
 439                        );
 440                    }
 441                }
 442            } else if let Some(draft) = thread.read(cx).draft_prompt() {
 443                editor.set_message(draft.to_vec(), window, cx);
 444            }
 445            editor
 446        });
 447
 448        let show_codex_windows_warning = cfg!(windows)
 449            && project.upgrade().is_some_and(|p| p.read(cx).is_local())
 450            && agent_id.as_ref() == "Codex";
 451
 452        let title_editor = {
 453            let can_edit = thread.update(cx, |thread, cx| thread.can_set_title(cx));
 454            let editor = cx.new(|cx| {
 455                let mut editor = Editor::single_line(window, cx);
 456                if let Some(title) = thread.read(cx).title() {
 457                    editor.set_text(title, window, cx);
 458                } else {
 459                    editor.set_text(DEFAULT_THREAD_TITLE, window, cx);
 460                }
 461                editor.set_read_only(!can_edit);
 462                editor
 463            });
 464            subscriptions.push(cx.subscribe_in(&editor, window, Self::handle_title_editor_event));
 465            editor
 466        };
 467
 468        subscriptions.push(cx.subscribe_in(
 469            &entry_view_state,
 470            window,
 471            Self::handle_entry_view_event,
 472        ));
 473
 474        subscriptions.push(cx.subscribe_in(
 475            &message_editor,
 476            window,
 477            Self::handle_message_editor_event,
 478        ));
 479
 480        subscriptions.push(cx.observe(&message_editor, |this, editor, cx| {
 481            let is_empty = editor.read(cx).text(cx).is_empty();
 482            let draft_contents_task = if is_empty {
 483                None
 484            } else {
 485                Some(editor.update(cx, |editor, cx| editor.draft_contents(cx)))
 486            };
 487            this._draft_resolve_task = Some(cx.spawn(async move |this, cx| {
 488                let draft = if let Some(task) = draft_contents_task {
 489                    let blocks = task.await.ok().filter(|b| !b.is_empty());
 490                    blocks
 491                } else {
 492                    None
 493                };
 494                this.update(cx, |this, cx| {
 495                    this.thread.update(cx, |thread, cx| {
 496                        thread.set_draft_prompt(draft, cx);
 497                    });
 498                    this.schedule_save(cx);
 499                })
 500                .ok();
 501            }));
 502        }));
 503
 504        let recent_history_entries = history
 505            .as_ref()
 506            .map(|h| h.read(cx).get_recent_sessions(3))
 507            .unwrap_or_default();
 508
 509        let mut this = Self {
 510            session_id,
 511            parent_session_id,
 512            focus_handle: cx.focus_handle(),
 513            thread,
 514            conversation,
 515            server_view,
 516            agent_icon,
 517            agent_icon_from_external_svg,
 518            agent_id,
 519            workspace,
 520            entry_view_state,
 521            title_editor,
 522            config_options_view,
 523            mode_selector,
 524            model_selector,
 525            profile_selector,
 526            list_state,
 527            session_capabilities,
 528            resumed_without_history,
 529            _subscriptions: subscriptions,
 530            permission_dropdown_handle: PopoverMenuHandle::default(),
 531            thread_retry_status: None,
 532            thread_error: None,
 533            thread_error_markdown: None,
 534            token_limit_callout_dismissed: false,
 535            last_token_limit_telemetry: None,
 536            thread_feedback: Default::default(),
 537            expanded_tool_calls: HashSet::default(),
 538            expanded_tool_call_raw_inputs: HashSet::default(),
 539            expanded_thinking_blocks: HashSet::default(),
 540            auto_expanded_thinking_block: None,
 541            user_toggled_thinking_blocks: HashSet::default(),
 542            subagent_scroll_handles: RefCell::new(HashMap::default()),
 543            edits_expanded: false,
 544            plan_expanded: false,
 545            queue_expanded: true,
 546            editor_expanded: false,
 547            should_be_following: false,
 548            editing_message: None,
 549            local_queued_messages: Vec::new(),
 550            queued_message_editors: Vec::new(),
 551            queued_message_editor_subscriptions: Vec::new(),
 552            last_synced_queue_length: 0,
 553            turn_fields: TurnFields::default(),
 554            discarded_partial_edits: HashSet::default(),
 555            is_loading_contents: false,
 556            new_server_version_available: None,
 557            permission_selections: HashMap::default(),
 558            resume_thread_metadata: None,
 559            _cancel_task: None,
 560            _save_task: None,
 561            _draft_resolve_task: None,
 562            skip_queue_processing_count: 0,
 563            user_interrupted_generation: false,
 564            can_fast_track_queue: false,
 565            hovered_edited_file_buttons: None,
 566            in_flight_prompt: None,
 567            message_editor,
 568            add_context_menu_handle: PopoverMenuHandle::default(),
 569            thinking_effort_menu_handle: PopoverMenuHandle::default(),
 570            project,
 571            recent_history_entries,
 572            hovered_recent_history_item: None,
 573            show_external_source_prompt_warning,
 574            history,
 575            _history_subscription: history_subscription,
 576            show_codex_windows_warning,
 577            multi_root_callout_dismissed: false,
 578            generating_indicator_in_list: false,
 579        };
 580
 581        this.sync_generating_indicator(cx);
 582        this.sync_editor_mode_for_empty_state(cx);
 583        let list_state_for_scroll = this.list_state.clone();
 584        let thread_view = cx.entity().downgrade();
 585
 586        this.list_state
 587            .set_scroll_handler(move |_event, _window, cx| {
 588                let list_state = list_state_for_scroll.clone();
 589                let thread_view = thread_view.clone();
 590                // N.B. We must defer because the scroll handler is called while the
 591                // ListState's RefCell is mutably borrowed. Reading logical_scroll_top()
 592                // directly would panic from a double borrow.
 593                cx.defer(move |cx| {
 594                    let scroll_top = list_state.logical_scroll_top();
 595                    let _ = thread_view.update(cx, |this, cx| {
 596                        if let Some(thread) = this.as_native_thread(cx) {
 597                            thread.update(cx, |thread, _cx| {
 598                                thread.set_ui_scroll_position(Some(scroll_top));
 599                            });
 600                        }
 601                        this.schedule_save(cx);
 602                    });
 603                });
 604            });
 605
 606        if should_auto_submit {
 607            this.send(window, cx);
 608        }
 609        this
 610    }
 611
 612    /// Schedule a throttled save of the thread state (draft prompt, scroll position, etc.).
 613    /// Multiple calls within `SERIALIZATION_THROTTLE_TIME` are coalesced into a single save.
 614    fn schedule_save(&mut self, cx: &mut Context<Self>) {
 615        self._save_task = Some(cx.spawn(async move |this, cx| {
 616            cx.background_executor()
 617                .timer(SERIALIZATION_THROTTLE_TIME)
 618                .await;
 619            this.update(cx, |this, cx| {
 620                if let Some(thread) = this.as_native_thread(cx) {
 621                    thread.update(cx, |_thread, cx| cx.notify());
 622                }
 623            })
 624            .ok();
 625        }));
 626    }
 627
 628    pub fn handle_message_editor_event(
 629        &mut self,
 630        _editor: &Entity<MessageEditor>,
 631        event: &MessageEditorEvent,
 632        window: &mut Window,
 633        cx: &mut Context<Self>,
 634    ) {
 635        match event {
 636            MessageEditorEvent::Send => self.send(window, cx),
 637            MessageEditorEvent::SendImmediately => self.interrupt_and_send(window, cx),
 638            MessageEditorEvent::Cancel => self.cancel_generation(cx),
 639            MessageEditorEvent::Focus => {
 640                self.cancel_editing(&Default::default(), window, cx);
 641            }
 642            MessageEditorEvent::LostFocus => {}
 643            MessageEditorEvent::InputAttempted { .. } => {}
 644        }
 645    }
 646
 647    pub fn is_draft(&self, cx: &App) -> bool {
 648        self.thread.read(cx).entries().is_empty()
 649    }
 650
 651    pub(crate) fn as_native_connection(
 652        &self,
 653        cx: &App,
 654    ) -> Option<Rc<agent::NativeAgentConnection>> {
 655        let acp_thread = self.thread.read(cx);
 656        acp_thread.connection().clone().downcast()
 657    }
 658
 659    pub fn as_native_thread(&self, cx: &App) -> Option<Entity<agent::Thread>> {
 660        let acp_thread = self.thread.read(cx);
 661        self.as_native_connection(cx)?
 662            .thread(acp_thread.session_id(), cx)
 663    }
 664
 665    /// Resolves the message editor's contents into content blocks. For profiles
 666    /// that do not enable any tools, directory mentions are expanded to inline
 667    /// file contents since the agent can't read files on its own.
 668    fn resolve_message_contents(
 669        &self,
 670        message_editor: &Entity<MessageEditor>,
 671        cx: &mut App,
 672    ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> {
 673        let expand = self.as_native_thread(cx).is_some_and(|thread| {
 674            let thread = thread.read(cx);
 675            AgentSettings::get_global(cx)
 676                .profiles
 677                .get(thread.profile())
 678                .is_some_and(|profile| profile.tools.is_empty())
 679        });
 680        message_editor.update(cx, |message_editor, cx| message_editor.contents(expand, cx))
 681    }
 682
 683    pub fn current_model_id(&self, cx: &App) -> Option<String> {
 684        let selector = self.model_selector.as_ref()?;
 685        let model = selector.read(cx).active_model(cx)?;
 686        Some(model.id.to_string())
 687    }
 688
 689    pub fn current_mode_id(&self, cx: &App) -> Option<Arc<str>> {
 690        if let Some(thread) = self.as_native_thread(cx) {
 691            Some(thread.read(cx).profile().0.clone())
 692        } else {
 693            let mode_selector = self.mode_selector.as_ref()?;
 694            Some(mode_selector.read(cx).mode().0)
 695        }
 696    }
 697
 698    fn is_subagent(&self) -> bool {
 699        self.parent_session_id.is_some()
 700    }
 701
 702    /// Returns the currently active editor, either for a message that is being
 703    /// edited or the editor for a new message.
 704    pub(crate) fn active_editor(&self, cx: &App) -> Entity<MessageEditor> {
 705        if let Some(index) = self.editing_message
 706            && let Some(editor) = self
 707                .entry_view_state
 708                .read(cx)
 709                .entry(index)
 710                .and_then(|entry| entry.message_editor())
 711                .cloned()
 712        {
 713            editor
 714        } else {
 715            self.message_editor.clone()
 716        }
 717    }
 718
 719    pub fn has_queued_messages(&self) -> bool {
 720        !self.local_queued_messages.is_empty()
 721    }
 722
 723    pub fn is_imported_thread(&self, cx: &App) -> bool {
 724        let Some(thread) = self.as_native_thread(cx) else {
 725            return false;
 726        };
 727        thread.read(cx).is_imported()
 728    }
 729
 730    // events
 731
 732    pub fn handle_entry_view_event(
 733        &mut self,
 734        _: &Entity<EntryViewState>,
 735        event: &EntryViewEvent,
 736        window: &mut Window,
 737        cx: &mut Context<Self>,
 738    ) {
 739        match &event.view_event {
 740            ViewEvent::NewDiff(tool_call_id) => {
 741                if AgentSettings::get_global(cx).expand_edit_card {
 742                    self.expanded_tool_calls.insert(tool_call_id.clone());
 743                }
 744            }
 745            ViewEvent::NewTerminal(tool_call_id) => {
 746                if AgentSettings::get_global(cx).expand_terminal_card {
 747                    self.expanded_tool_calls.insert(tool_call_id.clone());
 748                }
 749            }
 750            ViewEvent::TerminalMovedToBackground(tool_call_id) => {
 751                self.expanded_tool_calls.remove(tool_call_id);
 752            }
 753            ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => {
 754                if let Some(AgentThreadEntry::UserMessage(user_message)) =
 755                    self.thread.read(cx).entries().get(event.entry_index)
 756                    && self.thread.read(cx).supports_truncate(cx)
 757                    && user_message.id.is_some()
 758                    && !self.is_subagent()
 759                {
 760                    self.editing_message = Some(event.entry_index);
 761                    cx.notify();
 762                }
 763            }
 764            ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::LostFocus) => {
 765                if let Some(AgentThreadEntry::UserMessage(user_message)) =
 766                    self.thread.read(cx).entries().get(event.entry_index)
 767                    && self.thread.read(cx).supports_truncate(cx)
 768                    && user_message.id.is_some()
 769                    && !self.is_subagent()
 770                {
 771                    if editor.read(cx).text(cx).as_str() == user_message.content.to_markdown(cx) {
 772                        self.editing_message = None;
 773                        cx.notify();
 774                    }
 775                }
 776            }
 777            ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::SendImmediately) => {}
 778            ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => {
 779                if !self.is_subagent() {
 780                    self.regenerate(event.entry_index, editor.clone(), window, cx);
 781                }
 782            }
 783            ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Cancel) => {
 784                self.cancel_editing(&Default::default(), window, cx);
 785            }
 786            ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::InputAttempted { .. }) => {}
 787            ViewEvent::OpenDiffLocation {
 788                path,
 789                position,
 790                split,
 791            } => {
 792                self.open_diff_location(path, *position, *split, window, cx);
 793            }
 794        }
 795    }
 796
 797    fn open_diff_location(
 798        &self,
 799        path: &str,
 800        position: Point,
 801        split: bool,
 802        window: &mut Window,
 803        cx: &mut Context<Self>,
 804    ) {
 805        let Some(project) = self.project.upgrade() else {
 806            return;
 807        };
 808        let Some(project_path) = project.read(cx).find_project_path(path, cx) else {
 809            return;
 810        };
 811
 812        let open_task = if split {
 813            self.workspace
 814                .update(cx, |workspace, cx| {
 815                    workspace.split_path(project_path, window, cx)
 816                })
 817                .log_err()
 818        } else {
 819            self.workspace
 820                .update(cx, |workspace, cx| {
 821                    workspace.open_path(project_path, None, true, window, cx)
 822                })
 823                .log_err()
 824        };
 825
 826        let Some(open_task) = open_task else {
 827            return;
 828        };
 829
 830        window
 831            .spawn(cx, async move |cx| {
 832                let item = open_task.await?;
 833                let Some(editor) = item.downcast::<Editor>() else {
 834                    return anyhow::Ok(());
 835                };
 836                editor.update_in(cx, |editor, window, cx| {
 837                    editor.change_selections(
 838                        SelectionEffects::scroll(Autoscroll::center()),
 839                        window,
 840                        cx,
 841                        |selections| {
 842                            selections.select_ranges([position..position]);
 843                        },
 844                    );
 845                })?;
 846                anyhow::Ok(())
 847            })
 848            .detach_and_log_err(cx);
 849    }
 850
 851    // turns
 852
 853    pub fn start_turn(&mut self, cx: &mut Context<Self>) -> usize {
 854        self.turn_fields.turn_generation += 1;
 855        let generation = self.turn_fields.turn_generation;
 856        self.turn_fields.turn_started_at = Some(Instant::now());
 857        self.turn_fields.last_turn_duration = None;
 858        self.turn_fields.last_turn_tokens = None;
 859        self.turn_fields.turn_tokens = Some(0);
 860        self.turn_fields._turn_timer_task = Some(cx.spawn(async move |this, cx| {
 861            loop {
 862                cx.background_executor().timer(Duration::from_secs(1)).await;
 863                if this.update(cx, |_, cx| cx.notify()).is_err() {
 864                    break;
 865                }
 866            }
 867        }));
 868        generation
 869    }
 870
 871    pub fn stop_turn(&mut self, generation: usize, _cx: &mut Context<Self>) {
 872        if self.turn_fields.turn_generation != generation {
 873            return;
 874        }
 875        self.turn_fields.last_turn_duration = self
 876            .turn_fields
 877            .turn_started_at
 878            .take()
 879            .map(|started| started.elapsed());
 880        self.turn_fields.last_turn_tokens = self.turn_fields.turn_tokens.take();
 881        self.turn_fields._turn_timer_task = None;
 882    }
 883
 884    pub fn update_turn_tokens(&mut self, cx: &App) {
 885        if let Some(usage) = self.thread.read(cx).token_usage() {
 886            if let Some(tokens) = &mut self.turn_fields.turn_tokens {
 887                *tokens += usage.output_tokens;
 888                self.emit_token_limit_telemetry_if_needed(cx);
 889            }
 890        }
 891    }
 892
 893    fn emit_token_limit_telemetry_if_needed(&mut self, cx: &App) {
 894        let (ratio, agent_telemetry_id, session_id) = {
 895            let thread_data = self.thread.read(cx);
 896            let Some(token_usage) = thread_data.token_usage() else {
 897                return;
 898            };
 899            (
 900                token_usage.ratio(),
 901                thread_data.connection().telemetry_id(),
 902                thread_data.session_id().clone(),
 903            )
 904        };
 905
 906        let kind = match ratio {
 907            acp_thread::TokenUsageRatio::Normal => {
 908                self.last_token_limit_telemetry = None;
 909                return;
 910            }
 911            acp_thread::TokenUsageRatio::Warning => "warning",
 912            acp_thread::TokenUsageRatio::Exceeded => "exceeded",
 913        };
 914
 915        let should_skip = self
 916            .last_token_limit_telemetry
 917            .as_ref()
 918            .is_some_and(|last| *last >= ratio);
 919        if should_skip {
 920            return;
 921        }
 922
 923        self.last_token_limit_telemetry = Some(ratio);
 924
 925        telemetry::event!(
 926            "Agent Token Limit Warning",
 927            agent = agent_telemetry_id,
 928            session_id = session_id,
 929            kind = kind,
 930        );
 931    }
 932
 933    // sending
 934
 935    fn clear_external_source_prompt_warning(&mut self, cx: &mut Context<Self>) {
 936        if self.show_external_source_prompt_warning {
 937            self.show_external_source_prompt_warning = false;
 938            cx.notify();
 939        }
 940    }
 941
 942    pub fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 943        let thread = &self.thread;
 944
 945        if self.is_loading_contents {
 946            return;
 947        }
 948
 949        let message_editor = self.message_editor.clone();
 950
 951        let is_editor_empty = message_editor.read(cx).is_empty(cx);
 952        let is_generating = thread.read(cx).status() != ThreadStatus::Idle;
 953
 954        let has_queued = self.has_queued_messages();
 955        if is_editor_empty && self.can_fast_track_queue && has_queued {
 956            self.can_fast_track_queue = false;
 957            cx.emit(AcpThreadViewEvent::MessageSentOrQueued);
 958            self.send_queued_message_at_index(0, true, window, cx);
 959            return;
 960        }
 961
 962        if is_editor_empty {
 963            return;
 964        }
 965
 966        if is_generating {
 967            cx.emit(AcpThreadViewEvent::MessageSentOrQueued);
 968            self.queue_message(message_editor, window, cx);
 969            return;
 970        }
 971
 972        let text = message_editor.read(cx).text(cx);
 973        let text = text.trim();
 974        if text == "/login" || text == "/logout" {
 975            let connection = thread.read(cx).connection().clone();
 976            let can_login = !connection.auth_methods().is_empty();
 977            // Does the agent have a specific logout command? Prefer that in case they need to reset internal state.
 978            let logout_supported = text == "/logout"
 979                && self
 980                    .session_capabilities
 981                    .read()
 982                    .available_commands()
 983                    .iter()
 984                    .any(|command| command.name == "logout");
 985            if can_login && !logout_supported {
 986                message_editor.update(cx, |editor, cx| editor.clear(window, cx));
 987                self.clear_external_source_prompt_warning(cx);
 988
 989                let connection = self.thread.read(cx).connection().clone();
 990                window.defer(cx, {
 991                    let agent_id = self.agent_id.clone();
 992                    let server_view = self.server_view.clone();
 993                    move |window, cx| {
 994                        ConversationView::handle_auth_required(
 995                            server_view.clone(),
 996                            AuthRequired::new(),
 997                            agent_id,
 998                            connection,
 999                            window,
1000                            cx,
1001                        );
1002                    }
1003                });
1004                cx.notify();
1005                return;
1006            }
1007        }
1008
1009        cx.emit(AcpThreadViewEvent::MessageSentOrQueued);
1010        self.send_impl(message_editor, window, cx)
1011    }
1012
1013    pub fn send_impl(
1014        &mut self,
1015        message_editor: Entity<MessageEditor>,
1016        window: &mut Window,
1017        cx: &mut Context<Self>,
1018    ) {
1019        let contents = self.resolve_message_contents(&message_editor, cx);
1020
1021        self.thread_error.take();
1022        self.thread_feedback.clear();
1023        self.editing_message.take();
1024
1025        if self.should_be_following {
1026            self.workspace
1027                .update(cx, |workspace, cx| {
1028                    workspace.follow(CollaboratorId::Agent, window, cx);
1029                })
1030                .ok();
1031        }
1032
1033        let contents_task = cx.spawn_in(window, async move |_this, cx| {
1034            let (contents, tracked_buffers) = contents.await?;
1035
1036            if contents.is_empty() {
1037                return Ok(None);
1038            }
1039
1040            let _ = cx.update(|window, cx| {
1041                message_editor.update(cx, |message_editor, cx| {
1042                    message_editor.clear(window, cx);
1043                });
1044            });
1045
1046            Ok(Some((contents, tracked_buffers)))
1047        });
1048
1049        self.send_content(contents_task, window, cx);
1050    }
1051
1052    pub fn send_content(
1053        &mut self,
1054        contents_task: Task<anyhow::Result<Option<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>>>,
1055        window: &mut Window,
1056        cx: &mut Context<Self>,
1057    ) {
1058        let session_id = self.thread.read(cx).session_id().clone();
1059        let parent_session_id = self.thread.read(cx).parent_session_id().cloned();
1060        let agent_telemetry_id = self.thread.read(cx).connection().telemetry_id();
1061        let is_first_message = self.thread.read(cx).entries().is_empty();
1062        let thread = self.thread.downgrade();
1063
1064        self.is_loading_contents = true;
1065
1066        let model_id = self.current_model_id(cx);
1067        let mode_id = self.current_mode_id(cx);
1068        let guard = cx.new(|_| ());
1069        cx.observe_release(&guard, |this, _guard, cx| {
1070            this.is_loading_contents = false;
1071            cx.notify();
1072        })
1073        .detach();
1074
1075        let side = match AgentSettings::get_global(cx).sidebar_side() {
1076            SidebarSide::Left => "left",
1077            SidebarSide::Right => "right",
1078        };
1079
1080        let task = cx.spawn_in(window, async move |this, cx| {
1081            let Some((contents, tracked_buffers)) = contents_task.await? else {
1082                return Ok(());
1083            };
1084
1085            let generation = this.update(cx, |this, cx| {
1086                this.clear_external_source_prompt_warning(cx);
1087                let generation = this.start_turn(cx);
1088                this.in_flight_prompt = Some(contents.clone());
1089                generation
1090            })?;
1091
1092            this.update_in(cx, |this, _window, cx| {
1093                this.set_editor_is_expanded(false, cx);
1094            })?;
1095
1096            let _ = this.update(cx, |this, cx| {
1097                this.list_state.scroll_to_end();
1098                cx.notify();
1099            });
1100
1101            let _stop_turn = defer({
1102                let this = this.clone();
1103                let mut cx = cx.clone();
1104                move || {
1105                    this.update(&mut cx, |this, cx| {
1106                        this.stop_turn(generation, cx);
1107                        cx.notify();
1108                    })
1109                    .ok();
1110                }
1111            });
1112            if is_first_message && thread.read_with(cx, |thread, _cx| thread.title().is_none())? {
1113                let text: String = contents
1114                    .iter()
1115                    .filter_map(|block| match block {
1116                        acp::ContentBlock::Text(text_content) => Some(text_content.text.clone()),
1117                        acp::ContentBlock::ResourceLink(resource_link) => {
1118                            Some(format!("@{}", resource_link.name))
1119                        }
1120                        _ => None,
1121                    })
1122                    .collect::<Vec<_>>()
1123                    .join(" ");
1124                let text = text.lines().next().unwrap_or("").trim();
1125                if !text.is_empty() {
1126                    let title: SharedString = util::truncate_and_trailoff(text, 200).into();
1127                    thread.update(cx, |thread, cx| {
1128                        thread.set_provisional_title(title, cx);
1129                    })?;
1130                }
1131            }
1132
1133            let turn_start_time = Instant::now();
1134            let send = thread.update(cx, |thread, cx| {
1135                thread.action_log().update(cx, |action_log, cx| {
1136                    for buffer in tracked_buffers {
1137                        action_log.buffer_read(buffer, cx)
1138                    }
1139                });
1140                drop(guard);
1141
1142                telemetry::event!(
1143                    "Agent Message Sent",
1144                    agent = agent_telemetry_id,
1145                    session = session_id,
1146                    parent_session_id = parent_session_id.as_ref().map(|id| id.to_string()),
1147                    model = model_id,
1148                    mode = mode_id,
1149                    side = side
1150                );
1151
1152                thread.send(contents, cx)
1153            })?;
1154
1155            let _ = this.update(cx, |this, cx| {
1156                this.sync_generating_indicator(cx);
1157                cx.notify();
1158            });
1159
1160            let res = send.await;
1161            let turn_time_ms = turn_start_time.elapsed().as_millis();
1162            drop(_stop_turn);
1163            let status = if res.is_ok() {
1164                let _ = this.update(cx, |this, _| this.in_flight_prompt.take());
1165                "success"
1166            } else {
1167                "failure"
1168            };
1169            telemetry::event!(
1170                "Agent Turn Completed",
1171                agent = agent_telemetry_id,
1172                session = session_id,
1173                parent_session_id = parent_session_id.as_ref().map(|id| id.to_string()),
1174                model = model_id,
1175                mode = mode_id,
1176                status,
1177                turn_time_ms,
1178                side = side
1179            );
1180            res.map(|_| ())
1181        });
1182
1183        cx.spawn(async move |this, cx| {
1184            if let Err(err) = task.await {
1185                this.update(cx, |this, cx| {
1186                    this.handle_thread_error(err, cx);
1187                })
1188                .ok();
1189            } else {
1190                this.update(cx, |this, cx| {
1191                    let should_be_following = this
1192                        .workspace
1193                        .update(cx, |workspace, _| {
1194                            workspace.is_being_followed(CollaboratorId::Agent)
1195                        })
1196                        .unwrap_or_default();
1197                    this.should_be_following = should_be_following;
1198                })
1199                .ok();
1200            }
1201        })
1202        .detach();
1203    }
1204
1205    pub fn interrupt_and_send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1206        let thread = &self.thread;
1207
1208        if self.is_loading_contents {
1209            return;
1210        }
1211
1212        let message_editor = self.message_editor.clone();
1213        if thread.read(cx).status() == ThreadStatus::Idle {
1214            self.send_impl(message_editor, window, cx);
1215            return;
1216        }
1217
1218        self.stop_current_and_send_new_message(message_editor, window, cx);
1219    }
1220
1221    fn stop_current_and_send_new_message(
1222        &mut self,
1223        message_editor: Entity<MessageEditor>,
1224        window: &mut Window,
1225        cx: &mut Context<Self>,
1226    ) {
1227        let thread = self.thread.clone();
1228        self.skip_queue_processing_count = 0;
1229        self.user_interrupted_generation = true;
1230
1231        let cancelled = thread.update(cx, |thread, cx| thread.cancel(cx));
1232
1233        cx.spawn_in(window, async move |this, cx| {
1234            cancelled.await;
1235
1236            this.update_in(cx, |this, window, cx| {
1237                this.send_impl(message_editor, window, cx);
1238            })
1239            .ok();
1240        })
1241        .detach();
1242    }
1243
1244    pub(crate) fn handle_thread_error(
1245        &mut self,
1246        error: impl Into<ThreadError>,
1247        cx: &mut Context<Self>,
1248    ) {
1249        let error = error.into();
1250        self.emit_thread_error_telemetry(&error, cx);
1251        self.thread_error = Some(error);
1252        cx.notify();
1253    }
1254
1255    fn emit_thread_error_telemetry(&self, error: &ThreadError, cx: &mut Context<Self>) {
1256        let (error_kind, acp_error_code, message): (&str, Option<SharedString>, SharedString) =
1257            match error {
1258                ThreadError::PaymentRequired => (
1259                    "payment_required",
1260                    None,
1261                    "You reached your free usage limit. Upgrade to Zed Pro for more prompts."
1262                        .into(),
1263                ),
1264                ThreadError::Refusal => {
1265                    let model_or_agent_name = self.current_model_name(cx);
1266                    let message = format!(
1267                        "{} 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.",
1268                        model_or_agent_name
1269                    );
1270                    ("refusal", None, message.into())
1271                }
1272                ThreadError::AuthenticationRequired(message) => {
1273                    ("authentication_required", None, message.clone())
1274                }
1275                ThreadError::RateLimitExceeded { provider } => (
1276                    "rate_limit_exceeded",
1277                    None,
1278                    format!("{provider}'s rate limit was reached.").into(),
1279                ),
1280                ThreadError::ServerOverloaded { provider } => (
1281                    "server_overloaded",
1282                    None,
1283                    format!("{provider}'s servers are temporarily unavailable.").into(),
1284                ),
1285                ThreadError::PromptTooLarge => (
1286                    "prompt_too_large",
1287                    None,
1288                    "Context too large for the model's context window.".into(),
1289                ),
1290                ThreadError::NoApiKey { provider } => (
1291                    "no_api_key",
1292                    None,
1293                    format!("No API key configured for {provider}.").into(),
1294                ),
1295                ThreadError::StreamError { provider } => (
1296                    "stream_error",
1297                    None,
1298                    format!("Connection to {provider}'s API was interrupted.").into(),
1299                ),
1300                ThreadError::InvalidApiKey { provider } => (
1301                    "invalid_api_key",
1302                    None,
1303                    format!("Invalid or expired API key for {provider}.").into(),
1304                ),
1305                ThreadError::PermissionDenied { provider } => (
1306                    "permission_denied",
1307                    None,
1308                    format!(
1309                        "{provider}'s API rejected the request due to insufficient permissions."
1310                    )
1311                    .into(),
1312                ),
1313                ThreadError::RequestFailed => (
1314                    "request_failed",
1315                    None,
1316                    "Request could not be completed after multiple attempts.".into(),
1317                ),
1318                ThreadError::MaxOutputTokens => (
1319                    "max_output_tokens",
1320                    None,
1321                    "Model reached its maximum output length.".into(),
1322                ),
1323                ThreadError::NoModelSelected => {
1324                    ("no_model_selected", None, "No model selected.".into())
1325                }
1326                ThreadError::ApiError { provider } => (
1327                    "api_error",
1328                    None,
1329                    format!("{provider}'s API returned an unexpected error.").into(),
1330                ),
1331                ThreadError::Other {
1332                    acp_error_code,
1333                    message,
1334                } => ("other", acp_error_code.clone(), message.clone()),
1335            };
1336
1337        let agent_telemetry_id = self.thread.read(cx).connection().telemetry_id();
1338        let session_id = self.thread.read(cx).session_id().clone();
1339        let parent_session_id = self
1340            .thread
1341            .read(cx)
1342            .parent_session_id()
1343            .map(|id| id.to_string());
1344
1345        telemetry::event!(
1346            "Agent Panel Error Shown",
1347            agent = agent_telemetry_id,
1348            session_id = session_id,
1349            parent_session_id = parent_session_id,
1350            kind = error_kind,
1351            acp_error_code = acp_error_code,
1352            message = message,
1353        );
1354    }
1355
1356    pub fn cancel_generation(&mut self, cx: &mut Context<Self>) {
1357        self.thread_retry_status.take();
1358        self.thread_error.take();
1359        self.user_interrupted_generation = true;
1360        self._cancel_task = Some(self.thread.update(cx, |thread, cx| thread.cancel(cx)));
1361        self.sync_generating_indicator(cx);
1362        cx.notify();
1363    }
1364
1365    pub fn retry_generation(&mut self, cx: &mut Context<Self>) {
1366        self.thread_error.take();
1367
1368        let thread = &self.thread;
1369        if !thread.read(cx).can_retry(cx) {
1370            return;
1371        }
1372
1373        let task = thread.update(cx, |thread, cx| thread.retry(cx));
1374        self.sync_generating_indicator(cx);
1375        cx.notify();
1376        cx.spawn(async move |this, cx| {
1377            let result = task.await;
1378
1379            this.update(cx, |this, cx| {
1380                if let Err(err) = result {
1381                    this.handle_thread_error(err, cx);
1382                }
1383            })
1384        })
1385        .detach();
1386    }
1387
1388    pub fn regenerate(
1389        &mut self,
1390        entry_ix: usize,
1391        message_editor: Entity<MessageEditor>,
1392        window: &mut Window,
1393        cx: &mut Context<Self>,
1394    ) {
1395        if self.is_loading_contents {
1396            return;
1397        }
1398        let thread = self.thread.clone();
1399
1400        let Some(user_message_id) = thread.update(cx, |thread, _| {
1401            thread.entries().get(entry_ix)?.user_message()?.id.clone()
1402        }) else {
1403            return;
1404        };
1405
1406        cx.spawn_in(window, async move |this, cx| {
1407            // Check if there are any edits from prompts before the one being regenerated.
1408            //
1409            // If there are, we keep/accept them since we're not regenerating the prompt that created them.
1410            //
1411            // If editing the prompt that generated the edits, they are auto-rejected
1412            // through the `rewind` function in the `acp_thread`.
1413            let has_earlier_edits = thread.read_with(cx, |thread, _| {
1414                thread
1415                    .entries()
1416                    .iter()
1417                    .take(entry_ix)
1418                    .any(|entry| entry.diffs().next().is_some())
1419            });
1420
1421            if has_earlier_edits {
1422                thread.update(cx, |thread, cx| {
1423                    thread.action_log().update(cx, |action_log, cx| {
1424                        action_log.keep_all_edits(None, cx);
1425                    });
1426                });
1427            }
1428
1429            thread
1430                .update(cx, |thread, cx| thread.rewind(user_message_id, cx))
1431                .await?;
1432            this.update_in(cx, |thread, window, cx| {
1433                thread.send_impl(message_editor, window, cx);
1434                thread.focus_handle(cx).focus(window, cx);
1435            })?;
1436            anyhow::Ok(())
1437        })
1438        .detach_and_log_err(cx);
1439    }
1440
1441    // message queueing
1442
1443    fn queue_message(
1444        &mut self,
1445        message_editor: Entity<MessageEditor>,
1446        window: &mut Window,
1447        cx: &mut Context<Self>,
1448    ) {
1449        let is_idle = self.thread.read(cx).status() == acp_thread::ThreadStatus::Idle;
1450
1451        if is_idle {
1452            self.send_impl(message_editor, window, cx);
1453            return;
1454        }
1455
1456        let contents = self.resolve_message_contents(&message_editor, cx);
1457
1458        cx.spawn_in(window, async move |this, cx| {
1459            let (content, tracked_buffers) = contents.await?;
1460
1461            if content.is_empty() {
1462                return Ok::<(), anyhow::Error>(());
1463            }
1464
1465            this.update_in(cx, |this, window, cx| {
1466                this.add_to_queue(content, tracked_buffers, cx);
1467                this.can_fast_track_queue = true;
1468                message_editor.update(cx, |message_editor, cx| {
1469                    message_editor.clear(window, cx);
1470                });
1471                cx.notify();
1472            })?;
1473            Ok(())
1474        })
1475        .detach_and_log_err(cx);
1476    }
1477
1478    pub fn add_to_queue(
1479        &mut self,
1480        content: Vec<acp::ContentBlock>,
1481        tracked_buffers: Vec<Entity<Buffer>>,
1482        cx: &mut Context<Self>,
1483    ) {
1484        self.local_queued_messages.push(QueuedMessage {
1485            content,
1486            tracked_buffers,
1487        });
1488        self.sync_queue_flag_to_native_thread(cx);
1489    }
1490
1491    pub fn remove_from_queue(
1492        &mut self,
1493        index: usize,
1494        cx: &mut Context<Self>,
1495    ) -> Option<QueuedMessage> {
1496        if index < self.local_queued_messages.len() {
1497            let removed = self.local_queued_messages.remove(index);
1498            self.sync_queue_flag_to_native_thread(cx);
1499            Some(removed)
1500        } else {
1501            None
1502        }
1503    }
1504
1505    pub fn sync_queue_flag_to_native_thread(&self, cx: &mut Context<Self>) {
1506        if let Some(native_thread) = self.as_native_thread(cx) {
1507            let has_queued = self.has_queued_messages();
1508            native_thread.update(cx, |thread, _| {
1509                thread.set_has_queued_message(has_queued);
1510            });
1511        }
1512    }
1513
1514    pub fn send_queued_message_at_index(
1515        &mut self,
1516        index: usize,
1517        is_send_now: bool,
1518        window: &mut Window,
1519        cx: &mut Context<Self>,
1520    ) {
1521        let Some(queued) = self.remove_from_queue(index, cx) else {
1522            return;
1523        };
1524
1525        self.message_editor.focus_handle(cx).focus(window, cx);
1526
1527        let content = queued.content;
1528        let tracked_buffers = queued.tracked_buffers;
1529
1530        // Only increment skip count for "Send Now" operations (out-of-order sends)
1531        // Normal auto-processing from the Stopped handler doesn't need to skip.
1532        // We only skip the Stopped event from the cancelled generation, NOT the
1533        // Stopped event from the newly sent message (which should trigger queue processing).
1534        if is_send_now {
1535            let is_generating =
1536                self.thread.read(cx).status() == acp_thread::ThreadStatus::Generating;
1537            self.skip_queue_processing_count += if is_generating { 1 } else { 0 };
1538        }
1539
1540        let cancelled = self.thread.update(cx, |thread, cx| thread.cancel(cx));
1541
1542        let workspace = self.workspace.clone();
1543
1544        let should_be_following = self.should_be_following;
1545        let contents_task = cx.spawn_in(window, async move |_this, cx| {
1546            cancelled.await;
1547            if should_be_following {
1548                workspace
1549                    .update_in(cx, |workspace, window, cx| {
1550                        workspace.follow(CollaboratorId::Agent, window, cx);
1551                    })
1552                    .ok();
1553            }
1554
1555            Ok(Some((content, tracked_buffers)))
1556        });
1557
1558        self.send_content(contents_task, window, cx);
1559    }
1560
1561    pub fn move_queued_message_to_main_editor(
1562        &mut self,
1563        index: usize,
1564        inserted_text: Option<&str>,
1565        cursor_offset: Option<usize>,
1566        window: &mut Window,
1567        cx: &mut Context<Self>,
1568    ) -> bool {
1569        let Some(queued_message) = self.remove_from_queue(index, cx) else {
1570            return false;
1571        };
1572        let queued_content = queued_message.content;
1573        let message_editor = self.message_editor.clone();
1574        let inserted_text = inserted_text.map(ToOwned::to_owned);
1575
1576        window.focus(&message_editor.focus_handle(cx), cx);
1577
1578        if message_editor.read(cx).is_empty(cx) {
1579            message_editor.update(cx, |editor, cx| {
1580                editor.set_message(queued_content, window, cx);
1581                if let Some(offset) = cursor_offset {
1582                    editor.set_cursor_offset(offset, window, cx);
1583                }
1584                if let Some(inserted_text) = inserted_text.as_deref() {
1585                    editor.insert_text(inserted_text, window, cx);
1586                }
1587            });
1588            cx.notify();
1589            return true;
1590        }
1591
1592        // Adjust cursor offset accounting for existing content
1593        let existing_len = message_editor.read(cx).text(cx).len();
1594        let separator = "\n\n";
1595
1596        message_editor.update(cx, |editor, cx| {
1597            editor.append_message(queued_content, Some(separator), window, cx);
1598            if let Some(offset) = cursor_offset {
1599                let adjusted_offset = existing_len + separator.len() + offset;
1600                editor.set_cursor_offset(adjusted_offset, window, cx);
1601            }
1602            if let Some(inserted_text) = inserted_text.as_deref() {
1603                editor.insert_text(inserted_text, window, cx);
1604            }
1605        });
1606
1607        cx.notify();
1608        true
1609    }
1610
1611    // editor methods
1612
1613    pub fn expand_message_editor(
1614        &mut self,
1615        _: &ExpandMessageEditor,
1616        _window: &mut Window,
1617        cx: &mut Context<Self>,
1618    ) {
1619        if self.list_state.item_count() == 0 {
1620            return;
1621        }
1622        self.set_editor_is_expanded(!self.editor_expanded, cx);
1623        cx.stop_propagation();
1624        cx.notify();
1625    }
1626
1627    pub fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {
1628        self.editor_expanded = is_expanded;
1629        self.message_editor.update(cx, |editor, cx| {
1630            if is_expanded {
1631                editor.set_mode(
1632                    EditorMode::Full {
1633                        scale_ui_elements_with_buffer_font_size: false,
1634                        show_active_line_background: false,
1635                        sizing_behavior: SizingBehavior::ExcludeOverscrollMargin,
1636                    },
1637                    cx,
1638                )
1639            } else {
1640                let agent_settings = AgentSettings::get_global(cx);
1641                editor.set_mode(
1642                    EditorMode::AutoHeight {
1643                        min_lines: agent_settings.message_editor_min_lines,
1644                        max_lines: Some(agent_settings.set_message_editor_max_lines()),
1645                    },
1646                    cx,
1647                )
1648            }
1649        });
1650        cx.notify();
1651    }
1652
1653    pub fn handle_title_editor_event(
1654        &mut self,
1655        title_editor: &Entity<Editor>,
1656        event: &EditorEvent,
1657        window: &mut Window,
1658        cx: &mut Context<Self>,
1659    ) {
1660        let thread = &self.thread;
1661
1662        match event {
1663            EditorEvent::BufferEdited => {
1664                // We only want to set the title if the user has actively edited
1665                // it. If the title editor is not focused, we programmatically
1666                // changed the text, so we don't want to set the title again.
1667                if !title_editor.read(cx).is_focused(window) {
1668                    return;
1669                }
1670
1671                let new_title = title_editor.read(cx).text(cx);
1672                thread.update(cx, |thread, cx| {
1673                    thread
1674                        .set_title(new_title.into(), cx)
1675                        .detach_and_log_err(cx);
1676                })
1677            }
1678            EditorEvent::Blurred => {
1679                if title_editor.read(cx).text(cx).is_empty() {
1680                    title_editor.update(cx, |editor, cx| {
1681                        editor.set_text(DEFAULT_THREAD_TITLE, window, cx);
1682                    });
1683                }
1684            }
1685            _ => {}
1686        }
1687    }
1688
1689    pub fn cancel_editing(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
1690        if let Some(index) = self.editing_message.take()
1691            && let Some(editor) = &self
1692                .entry_view_state
1693                .read(cx)
1694                .entry(index)
1695                .and_then(|e| e.message_editor())
1696                .cloned()
1697        {
1698            editor.update(cx, |editor, cx| {
1699                if let Some(user_message) = self
1700                    .thread
1701                    .read(cx)
1702                    .entries()
1703                    .get(index)
1704                    .and_then(|e| e.user_message())
1705                {
1706                    editor.set_message(user_message.chunks.clone(), window, cx);
1707                }
1708            })
1709        };
1710        self.message_editor.focus_handle(cx).focus(window, cx);
1711        cx.notify();
1712    }
1713
1714    pub fn authorize_tool_call(
1715        &mut self,
1716        session_id: acp::SessionId,
1717        tool_call_id: acp::ToolCallId,
1718        outcome: SelectedPermissionOutcome,
1719        window: &mut Window,
1720        cx: &mut Context<Self>,
1721    ) {
1722        self.conversation.update(cx, |conversation, cx| {
1723            conversation.authorize_tool_call(session_id, tool_call_id, outcome, cx);
1724        });
1725        if self.should_be_following {
1726            self.workspace
1727                .update(cx, |workspace, cx| {
1728                    workspace.follow(CollaboratorId::Agent, window, cx);
1729                })
1730                .ok();
1731        }
1732        cx.notify();
1733    }
1734
1735    pub fn allow_always(&mut self, _: &AllowAlways, window: &mut Window, cx: &mut Context<Self>) {
1736        self.authorize_pending_tool_call(acp::PermissionOptionKind::AllowAlways, window, cx);
1737    }
1738
1739    pub fn allow_once(&mut self, _: &AllowOnce, window: &mut Window, cx: &mut Context<Self>) {
1740        self.authorize_pending_with_granularity(true, window, cx);
1741    }
1742
1743    pub fn reject_once(&mut self, _: &RejectOnce, window: &mut Window, cx: &mut Context<Self>) {
1744        self.authorize_pending_with_granularity(false, window, cx);
1745    }
1746
1747    pub fn authorize_pending_tool_call(
1748        &mut self,
1749        kind: acp::PermissionOptionKind,
1750        window: &mut Window,
1751        cx: &mut Context<Self>,
1752    ) -> Option<()> {
1753        let session_id = self.thread.read(cx).session_id().clone();
1754        self.conversation.update(cx, |conversation, cx| {
1755            conversation.authorize_pending_tool_call(&session_id, kind, cx)
1756        })?;
1757        if self.should_be_following {
1758            self.workspace
1759                .update(cx, |workspace, cx| {
1760                    workspace.follow(CollaboratorId::Agent, window, cx);
1761                })
1762                .ok();
1763        }
1764        cx.notify();
1765        Some(())
1766    }
1767
1768    fn is_waiting_for_confirmation(entry: &AgentThreadEntry) -> bool {
1769        if let AgentThreadEntry::ToolCall(tool_call) = entry {
1770            matches!(
1771                tool_call.status,
1772                ToolCallStatus::WaitingForConfirmation { .. }
1773            )
1774        } else {
1775            false
1776        }
1777    }
1778
1779    fn handle_authorize_tool_call(
1780        &mut self,
1781        action: &AuthorizeToolCall,
1782        window: &mut Window,
1783        cx: &mut Context<Self>,
1784    ) {
1785        let tool_call_id = acp::ToolCallId::new(action.tool_call_id.clone());
1786        let option_id = acp::PermissionOptionId::new(action.option_id.clone());
1787        let option_kind = match action.option_kind.as_str() {
1788            "AllowOnce" => acp::PermissionOptionKind::AllowOnce,
1789            "AllowAlways" => acp::PermissionOptionKind::AllowAlways,
1790            "RejectOnce" => acp::PermissionOptionKind::RejectOnce,
1791            "RejectAlways" => acp::PermissionOptionKind::RejectAlways,
1792            _ => acp::PermissionOptionKind::AllowOnce,
1793        };
1794
1795        let session_id = self.thread.read(cx).session_id().clone();
1796        self.authorize_tool_call(
1797            session_id,
1798            tool_call_id,
1799            SelectedPermissionOutcome::new(option_id, option_kind),
1800            window,
1801            cx,
1802        );
1803    }
1804
1805    pub fn handle_select_permission_granularity(
1806        &mut self,
1807        action: &SelectPermissionGranularity,
1808        _window: &mut Window,
1809        cx: &mut Context<Self>,
1810    ) {
1811        let tool_call_id = acp::ToolCallId::new(action.tool_call_id.clone());
1812        self.permission_selections
1813            .insert(tool_call_id, PermissionSelection::Choice(action.index));
1814
1815        cx.notify();
1816    }
1817
1818    pub fn handle_toggle_command_pattern(
1819        &mut self,
1820        action: &crate::ToggleCommandPattern,
1821        _window: &mut Window,
1822        cx: &mut Context<Self>,
1823    ) {
1824        let tool_call_id = acp::ToolCallId::new(action.tool_call_id.clone());
1825
1826        match self.permission_selections.get_mut(&tool_call_id) {
1827            Some(PermissionSelection::SelectedPatterns(checked)) => {
1828                // Already in pattern mode — toggle the individual pattern.
1829                if let Some(pos) = checked.iter().position(|&i| i == action.pattern_index) {
1830                    checked.swap_remove(pos);
1831                } else {
1832                    checked.push(action.pattern_index);
1833                }
1834            }
1835            _ => {
1836                // First click: activate "Select options" with all patterns checked.
1837                let thread = self.thread.read(cx);
1838                let pattern_count = thread
1839                    .entries()
1840                    .iter()
1841                    .find_map(|entry| {
1842                        if let AgentThreadEntry::ToolCall(call) = entry {
1843                            if call.id == tool_call_id {
1844                                if let ToolCallStatus::WaitingForConfirmation { options, .. } =
1845                                    &call.status
1846                                {
1847                                    if let PermissionOptions::DropdownWithPatterns {
1848                                        patterns,
1849                                        ..
1850                                    } = options
1851                                    {
1852                                        return Some(patterns.len());
1853                                    }
1854                                }
1855                            }
1856                        }
1857                        None
1858                    })
1859                    .unwrap_or(0);
1860                self.permission_selections.insert(
1861                    tool_call_id,
1862                    PermissionSelection::SelectedPatterns((0..pattern_count).collect()),
1863                );
1864            }
1865        }
1866        cx.notify();
1867    }
1868
1869    fn authorize_pending_with_granularity(
1870        &mut self,
1871        is_allow: bool,
1872        window: &mut Window,
1873        cx: &mut Context<Self>,
1874    ) -> Option<()> {
1875        let session_id = self.thread.read(cx).session_id().clone();
1876        let (returned_session_id, tool_call_id, options) = self
1877            .conversation
1878            .read(cx)
1879            .pending_tool_call(&session_id, cx)?;
1880        let options = options.clone();
1881        self.authorize_with_granularity(
1882            returned_session_id,
1883            tool_call_id,
1884            &options,
1885            is_allow,
1886            window,
1887            cx,
1888        )
1889    }
1890
1891    fn authorize_with_granularity(
1892        &mut self,
1893        session_id: acp::SessionId,
1894        tool_call_id: acp::ToolCallId,
1895        options: &PermissionOptions,
1896        is_allow: bool,
1897        window: &mut Window,
1898        cx: &mut Context<Self>,
1899    ) -> Option<()> {
1900        let choices = match options {
1901            PermissionOptions::Dropdown(choices) => choices.as_slice(),
1902            PermissionOptions::DropdownWithPatterns { choices, .. } => choices.as_slice(),
1903            _ => {
1904                let kind = if is_allow {
1905                    acp::PermissionOptionKind::AllowOnce
1906                } else {
1907                    acp::PermissionOptionKind::RejectOnce
1908                };
1909                return self.authorize_pending_tool_call(kind, window, cx);
1910            }
1911        };
1912
1913        let selection = self.permission_selections.get(&tool_call_id);
1914
1915        // When in per-command pattern mode, use the checked patterns.
1916        if let Some(PermissionSelection::SelectedPatterns(checked)) = selection {
1917            if let Some(outcome) = options.build_outcome_for_checked_patterns(checked, is_allow) {
1918                self.authorize_tool_call(session_id, tool_call_id, outcome, window, cx);
1919                return Some(());
1920            }
1921        }
1922
1923        // Use the selected granularity choice ("Always for terminal" or "Only this time")
1924        let selected_index = selection
1925            .and_then(|s| s.choice_index())
1926            .unwrap_or_else(|| choices.len().saturating_sub(1));
1927
1928        let selected_choice = choices.get(selected_index).or(choices.last())?;
1929        let outcome = selected_choice.build_outcome(is_allow);
1930
1931        self.authorize_tool_call(session_id, tool_call_id, outcome, window, cx);
1932
1933        Some(())
1934    }
1935
1936    // edits
1937
1938    pub fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context<Self>) {
1939        let thread = &self.thread;
1940        let telemetry = ActionLogTelemetry::from(thread.read(cx));
1941        let action_log = thread.read(cx).action_log().clone();
1942        action_log.update(cx, |action_log, cx| {
1943            action_log.keep_all_edits(Some(telemetry), cx)
1944        });
1945    }
1946
1947    pub fn reject_all(&mut self, _: &RejectAll, _window: &mut Window, cx: &mut Context<Self>) {
1948        let thread = &self.thread;
1949        let telemetry = ActionLogTelemetry::from(thread.read(cx));
1950        let action_log = thread.read(cx).action_log().clone();
1951        let has_changes = action_log.read(cx).changed_buffers(cx).len() > 0;
1952
1953        action_log
1954            .update(cx, |action_log, cx| {
1955                action_log.reject_all_edits(Some(telemetry), cx)
1956            })
1957            .detach();
1958
1959        if has_changes {
1960            if let Some(workspace) = self.workspace.upgrade() {
1961                workspace.update(cx, |workspace, cx| {
1962                    crate::ui::show_undo_reject_toast(workspace, action_log, cx);
1963                });
1964            }
1965        }
1966    }
1967
1968    pub fn undo_last_reject(
1969        &mut self,
1970        _: &UndoLastReject,
1971        _window: &mut Window,
1972        cx: &mut Context<Self>,
1973    ) {
1974        let thread = &self.thread;
1975        let action_log = thread.read(cx).action_log().clone();
1976        action_log
1977            .update(cx, |action_log, cx| action_log.undo_last_reject(cx))
1978            .detach()
1979    }
1980
1981    pub fn open_edited_buffer(
1982        &mut self,
1983        buffer: &Entity<Buffer>,
1984        window: &mut Window,
1985        cx: &mut Context<Self>,
1986    ) {
1987        let thread = &self.thread;
1988
1989        let Some(diff) =
1990            AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err()
1991        else {
1992            return;
1993        };
1994
1995        diff.update(cx, |diff, cx| {
1996            diff.move_to_path(PathKey::for_buffer(buffer, cx), window, cx)
1997        })
1998    }
1999
2000    // thread stuff
2001
2002    fn share_thread(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
2003        let Some((thread, project)) = self.as_native_thread(cx).zip(self.project.upgrade()) else {
2004            return;
2005        };
2006
2007        let client = project.read(cx).client();
2008        let workspace = self.workspace.clone();
2009        let session_id = thread.read(cx).id().to_string();
2010
2011        let load_task = thread.read(cx).to_db(cx);
2012
2013        cx.spawn(async move |_this, cx| {
2014            let db_thread = load_task.await;
2015
2016            let shared_thread = SharedThread::from_db_thread(&db_thread);
2017            let thread_data = shared_thread.to_bytes()?;
2018            let title = shared_thread.title.to_string();
2019
2020            client
2021                .request(proto::ShareAgentThread {
2022                    session_id: session_id.clone(),
2023                    title,
2024                    thread_data,
2025                })
2026                .await?;
2027
2028            let share_url = client::zed_urls::shared_agent_thread_url(&session_id);
2029
2030            cx.update(|cx| {
2031                if let Some(workspace) = workspace.upgrade() {
2032                    workspace.update(cx, |workspace, cx| {
2033                        struct ThreadSharedToast;
2034                        workspace.show_toast(
2035                            Toast::new(
2036                                NotificationId::unique::<ThreadSharedToast>(),
2037                                "Thread shared!",
2038                            )
2039                            .on_click(
2040                                "Copy URL",
2041                                move |_window, cx| {
2042                                    cx.write_to_clipboard(ClipboardItem::new_string(
2043                                        share_url.clone(),
2044                                    ));
2045                                },
2046                            ),
2047                            cx,
2048                        );
2049                    });
2050                }
2051            });
2052
2053            anyhow::Ok(())
2054        })
2055        .detach_and_log_err(cx);
2056    }
2057
2058    pub fn sync_thread(
2059        &mut self,
2060        project: Entity<Project>,
2061        server_view: Entity<ConversationView>,
2062        window: &mut Window,
2063        cx: &mut Context<Self>,
2064    ) {
2065        if !self.is_imported_thread(cx) {
2066            return;
2067        }
2068
2069        let Some(session_list) = self
2070            .as_native_connection(cx)
2071            .and_then(|connection| connection.session_list(cx))
2072            .and_then(|list| list.downcast::<NativeAgentSessionList>())
2073        else {
2074            return;
2075        };
2076        let thread_store = session_list.thread_store().clone();
2077
2078        let client = project.read(cx).client();
2079        let session_id = self.thread.read(cx).session_id().clone();
2080        cx.spawn_in(window, async move |this, cx| {
2081            let response = client
2082                .request(proto::GetSharedAgentThread {
2083                    session_id: session_id.to_string(),
2084                })
2085                .await?;
2086
2087            let shared_thread = SharedThread::from_bytes(&response.thread_data)?;
2088
2089            let db_thread = shared_thread.to_db_thread();
2090
2091            thread_store
2092                .update(&mut cx.clone(), |store, cx| {
2093                    store.save_thread(session_id.clone(), db_thread, Default::default(), cx)
2094                })
2095                .await?;
2096
2097            server_view.update_in(cx, |server_view, window, cx| server_view.reset(window, cx))?;
2098
2099            this.update_in(cx, |this, _window, cx| {
2100                if let Some(workspace) = this.workspace.upgrade() {
2101                    workspace.update(cx, |workspace, cx| {
2102                        struct ThreadSyncedToast;
2103                        workspace.show_toast(
2104                            Toast::new(
2105                                NotificationId::unique::<ThreadSyncedToast>(),
2106                                "Thread synced with latest version",
2107                            )
2108                            .autohide(),
2109                            cx,
2110                        );
2111                    });
2112                }
2113            })?;
2114
2115            anyhow::Ok(())
2116        })
2117        .detach_and_log_err(cx);
2118    }
2119
2120    pub fn restore_checkpoint(&mut self, message_id: &UserMessageId, cx: &mut Context<Self>) {
2121        self.thread
2122            .update(cx, |thread, cx| {
2123                thread.restore_checkpoint(message_id.clone(), cx)
2124            })
2125            .detach_and_log_err(cx);
2126    }
2127
2128    pub fn clear_thread_error(&mut self, cx: &mut Context<Self>) {
2129        self.thread_error = None;
2130        self.thread_error_markdown = None;
2131        self.token_limit_callout_dismissed = true;
2132        cx.notify();
2133    }
2134
2135    fn is_following(&self, cx: &App) -> bool {
2136        match self.thread.read(cx).status() {
2137            ThreadStatus::Generating => self
2138                .workspace
2139                .read_with(cx, |workspace, _| {
2140                    workspace.is_being_followed(CollaboratorId::Agent)
2141                })
2142                .unwrap_or(false),
2143            _ => self.should_be_following,
2144        }
2145    }
2146
2147    fn toggle_following(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2148        let following = self.is_following(cx);
2149
2150        self.should_be_following = !following;
2151        if self.thread.read(cx).status() == ThreadStatus::Generating {
2152            self.workspace
2153                .update(cx, |workspace, cx| {
2154                    if following {
2155                        workspace.unfollow(CollaboratorId::Agent, window, cx);
2156                    } else {
2157                        workspace.follow(CollaboratorId::Agent, window, cx);
2158                    }
2159                })
2160                .ok();
2161        }
2162
2163        telemetry::event!("Follow Agent Selected", following = !following);
2164    }
2165
2166    // other
2167
2168    pub fn render_thread_retry_status_callout(&self) -> Option<Callout> {
2169        let state = self.thread_retry_status.as_ref()?;
2170
2171        let next_attempt_in = state
2172            .duration
2173            .saturating_sub(Instant::now().saturating_duration_since(state.started_at));
2174        if next_attempt_in.is_zero() {
2175            return None;
2176        }
2177
2178        let next_attempt_in_secs = next_attempt_in.as_secs() + 1;
2179
2180        let retry_message = if state.max_attempts == 1 {
2181            if next_attempt_in_secs == 1 {
2182                "Retrying. Next attempt in 1 second.".to_string()
2183            } else {
2184                format!("Retrying. Next attempt in {next_attempt_in_secs} seconds.")
2185            }
2186        } else if next_attempt_in_secs == 1 {
2187            format!(
2188                "Retrying. Next attempt in 1 second (Attempt {} of {}).",
2189                state.attempt, state.max_attempts,
2190            )
2191        } else {
2192            format!(
2193                "Retrying. Next attempt in {next_attempt_in_secs} seconds (Attempt {} of {}).",
2194                state.attempt, state.max_attempts,
2195            )
2196        };
2197
2198        Some(
2199            Callout::new()
2200                .icon(IconName::Warning)
2201                .severity(Severity::Warning)
2202                .title(state.last_error.clone())
2203                .description(retry_message),
2204        )
2205    }
2206
2207    pub fn handle_open_rules(
2208        &mut self,
2209        _: &ClickEvent,
2210        window: &mut Window,
2211        cx: &mut Context<Self>,
2212    ) {
2213        let Some(thread) = self.as_native_thread(cx) else {
2214            return;
2215        };
2216        let project_context = thread.read(cx).project_context().read(cx);
2217
2218        let project_entry_ids = project_context
2219            .worktrees
2220            .iter()
2221            .flat_map(|worktree| worktree.rules_file.as_ref())
2222            .map(|rules_file| ProjectEntryId::from_usize(rules_file.project_entry_id))
2223            .collect::<Vec<_>>();
2224
2225        self.workspace
2226            .update(cx, move |workspace, cx| {
2227                // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
2228                // files clear. For example, if rules file 1 is already open but rules file 2 is not,
2229                // this would open and focus rules file 2 in a tab that is not next to rules file 1.
2230                let project = workspace.project().read(cx);
2231                let project_paths = project_entry_ids
2232                    .into_iter()
2233                    .flat_map(|entry_id| project.path_for_entry(entry_id, cx))
2234                    .collect::<Vec<_>>();
2235                for project_path in project_paths {
2236                    workspace
2237                        .open_path(project_path, None, true, window, cx)
2238                        .detach_and_log_err(cx);
2239                }
2240            })
2241            .ok();
2242    }
2243
2244    fn activity_bar_bg(&self, cx: &Context<Self>) -> Hsla {
2245        let editor_bg_color = cx.theme().colors().editor_background;
2246        let active_color = cx.theme().colors().element_selected;
2247        editor_bg_color.blend(active_color.opacity(0.3))
2248    }
2249
2250    pub fn render_activity_bar(
2251        &self,
2252        window: &mut Window,
2253        cx: &Context<Self>,
2254    ) -> Option<AnyElement> {
2255        let thread = self.thread.read(cx);
2256        let action_log = thread.action_log();
2257        let telemetry = ActionLogTelemetry::from(thread);
2258        let changed_buffers = action_log.read(cx).changed_buffers(cx);
2259        let plan = thread.plan();
2260        let queue_is_empty = !self.has_queued_messages();
2261
2262        let subagents_awaiting_permission = self.render_subagents_awaiting_permission(cx);
2263        let has_subagents_awaiting = subagents_awaiting_permission.is_some();
2264
2265        if changed_buffers.is_empty()
2266            && plan.is_empty()
2267            && queue_is_empty
2268            && !has_subagents_awaiting
2269        {
2270            return None;
2271        }
2272
2273        // Temporarily always enable ACP edit controls. This is temporary, to lessen the
2274        // impact of a nasty bug that causes them to sometimes be disabled when they shouldn't
2275        // be, which blocks you from being able to accept or reject edits. This switches the
2276        // bug to be that sometimes it's enabled when it shouldn't be, which at least doesn't
2277        // block you from using the panel.
2278        let pending_edits = false;
2279
2280        let plan_expanded = self.plan_expanded;
2281        let edits_expanded = self.edits_expanded;
2282        let queue_expanded = self.queue_expanded;
2283
2284        let max_content_width = AgentSettings::get_global(cx).max_content_width;
2285
2286        h_flex()
2287            .w_full()
2288            .justify_center()
2289            .child(
2290                v_flex()
2291                    .flex_basis(max_content_width)
2292                    .flex_shrink()
2293                    .flex_grow_0()
2294                    .mx_2()
2295                    .bg(self.activity_bar_bg(cx))
2296                    .border_1()
2297                    .border_b_0()
2298                    .border_color(cx.theme().colors().border)
2299                    .rounded_t_md()
2300                    .shadow(vec![gpui::BoxShadow {
2301                        color: gpui::black().opacity(0.12),
2302                        offset: point(px(1.), px(-1.)),
2303                        blur_radius: px(2.),
2304                        spread_radius: px(0.),
2305                    }])
2306                    .when_some(subagents_awaiting_permission, |this, element| {
2307                        this.child(element)
2308                    })
2309                    .when(
2310                        has_subagents_awaiting
2311                            && (!plan.is_empty() || !changed_buffers.is_empty() || !queue_is_empty),
2312                        |this| this.child(Divider::horizontal().color(DividerColor::Border)),
2313                    )
2314                    .when(!plan.is_empty(), |this| {
2315                        this.child(self.render_plan_summary(plan, window, cx))
2316                            .when(plan_expanded, |parent| {
2317                                parent.child(self.render_plan_entries(plan, window, cx))
2318                            })
2319                    })
2320                    .when(!plan.is_empty() && !changed_buffers.is_empty(), |this| {
2321                        this.child(Divider::horizontal().color(DividerColor::Border))
2322                    })
2323                    .when(
2324                        !changed_buffers.is_empty() && thread.parent_session_id().is_none(),
2325                        |this| {
2326                            this.child(self.render_edits_summary(
2327                                &changed_buffers,
2328                                edits_expanded,
2329                                pending_edits,
2330                                cx,
2331                            ))
2332                            .when(edits_expanded, |parent| {
2333                                parent.child(self.render_edited_files(
2334                                    action_log,
2335                                    telemetry.clone(),
2336                                    &changed_buffers,
2337                                    pending_edits,
2338                                    cx,
2339                                ))
2340                            })
2341                        },
2342                    )
2343                    .when(!queue_is_empty, |this| {
2344                        this.when(!plan.is_empty() || !changed_buffers.is_empty(), |this| {
2345                            this.child(Divider::horizontal().color(DividerColor::Border))
2346                        })
2347                        .child(self.render_message_queue_summary(window, cx))
2348                        .when(queue_expanded, |parent| {
2349                            parent.child(self.render_message_queue_entries(window, cx))
2350                        })
2351                    }),
2352            )
2353            .into_any()
2354            .into()
2355    }
2356
2357    fn render_edited_files(
2358        &self,
2359        action_log: &Entity<ActionLog>,
2360        telemetry: ActionLogTelemetry,
2361        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
2362        pending_edits: bool,
2363        cx: &Context<Self>,
2364    ) -> impl IntoElement {
2365        let editor_bg_color = cx.theme().colors().editor_background;
2366
2367        // Sort edited files alphabetically for consistency with Git diff view
2368        let mut sorted_buffers: Vec<_> = changed_buffers.iter().collect();
2369        sorted_buffers.sort_by(|(buffer_a, _), (buffer_b, _)| {
2370            let path_a = buffer_a.read(cx).file().map(|f| f.path().clone());
2371            let path_b = buffer_b.read(cx).file().map(|f| f.path().clone());
2372            path_a.cmp(&path_b)
2373        });
2374
2375        v_flex()
2376            .id("edited_files_list")
2377            .max_h_40()
2378            .overflow_y_scroll()
2379            .children(
2380                sorted_buffers
2381                    .into_iter()
2382                    .enumerate()
2383                    .flat_map(|(index, (buffer, diff))| {
2384                        let file = buffer.read(cx).file()?;
2385                        let path = file.path();
2386                        let path_style = file.path_style(cx);
2387                        let separator = file.path_style(cx).primary_separator();
2388
2389                        let file_path = path.parent().and_then(|parent| {
2390                            if parent.is_empty() {
2391                                None
2392                            } else {
2393                                Some(
2394                                    Label::new(format!(
2395                                        "{}{separator}",
2396                                        parent.display(path_style)
2397                                    ))
2398                                    .color(Color::Muted)
2399                                    .size(LabelSize::XSmall)
2400                                    .buffer_font(cx),
2401                                )
2402                            }
2403                        });
2404
2405                        let file_name = path.file_name().map(|name| {
2406                            Label::new(name.to_string())
2407                                .size(LabelSize::XSmall)
2408                                .buffer_font(cx)
2409                                .ml_1()
2410                        });
2411
2412                        let full_path = path.display(path_style).to_string();
2413
2414                        let file_icon = FileIcons::get_icon(path.as_std_path(), cx)
2415                            .map(Icon::from_path)
2416                            .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
2417                            .unwrap_or_else(|| {
2418                                Icon::new(IconName::File)
2419                                    .color(Color::Muted)
2420                                    .size(IconSize::Small)
2421                            });
2422
2423                        let file_stats = DiffStats::single_file(buffer.read(cx), diff.read(cx), cx);
2424
2425                        let buttons = self.render_edited_files_buttons(
2426                            index,
2427                            buffer,
2428                            action_log,
2429                            &telemetry,
2430                            pending_edits,
2431                            editor_bg_color,
2432                            cx,
2433                        );
2434
2435                        let element = h_flex()
2436                            .group("edited-code")
2437                            .id(("file-container", index))
2438                            .relative()
2439                            .min_w_0()
2440                            .p_1p5()
2441                            .gap_2()
2442                            .justify_between()
2443                            .bg(editor_bg_color)
2444                            .when(index < changed_buffers.len() - 1, |parent| {
2445                                parent.border_color(cx.theme().colors().border).border_b_1()
2446                            })
2447                            .child(
2448                                h_flex()
2449                                    .id(("file-name-path", index))
2450                                    .cursor_pointer()
2451                                    .pr_0p5()
2452                                    .gap_0p5()
2453                                    .rounded_xs()
2454                                    .child(file_icon)
2455                                    .children(file_name)
2456                                    .children(file_path)
2457                                    .child(
2458                                        DiffStat::new(
2459                                            "file",
2460                                            file_stats.lines_added as usize,
2461                                            file_stats.lines_removed as usize,
2462                                        )
2463                                        .label_size(LabelSize::XSmall),
2464                                    )
2465                                    .hover(|s| s.bg(cx.theme().colors().element_hover))
2466                                    .tooltip({
2467                                        move |_, cx| {
2468                                            Tooltip::with_meta(
2469                                                "Go to File",
2470                                                None,
2471                                                full_path.clone(),
2472                                                cx,
2473                                            )
2474                                        }
2475                                    })
2476                                    .on_click({
2477                                        let buffer = buffer.clone();
2478                                        cx.listener(move |this, _, window, cx| {
2479                                            this.open_edited_buffer(&buffer, window, cx);
2480                                        })
2481                                    }),
2482                            )
2483                            .child(buttons);
2484
2485                        Some(element)
2486                    }),
2487            )
2488            .into_any_element()
2489    }
2490
2491    fn render_edited_files_buttons(
2492        &self,
2493        index: usize,
2494        buffer: &Entity<Buffer>,
2495        action_log: &Entity<ActionLog>,
2496        telemetry: &ActionLogTelemetry,
2497        pending_edits: bool,
2498        editor_bg_color: Hsla,
2499        cx: &Context<Self>,
2500    ) -> impl IntoElement {
2501        h_flex()
2502            .id("edited-buttons-container")
2503            .visible_on_hover("edited-code")
2504            .absolute()
2505            .right_0()
2506            .px_1()
2507            .gap_1()
2508            .bg(editor_bg_color)
2509            .on_hover(cx.listener(move |this, is_hovered, _window, cx| {
2510                if *is_hovered {
2511                    this.hovered_edited_file_buttons = Some(index);
2512                } else if this.hovered_edited_file_buttons == Some(index) {
2513                    this.hovered_edited_file_buttons = None;
2514                }
2515                cx.notify();
2516            }))
2517            .child(
2518                Button::new("review", "Review")
2519                    .label_size(LabelSize::Small)
2520                    .on_click({
2521                        let buffer = buffer.clone();
2522                        cx.listener(move |this, _, window, cx| {
2523                            this.open_edited_buffer(&buffer, window, cx);
2524                        })
2525                    }),
2526            )
2527            .child(
2528                Button::new(("reject-file", index), "Reject")
2529                    .label_size(LabelSize::Small)
2530                    .disabled(pending_edits)
2531                    .on_click({
2532                        let buffer = buffer.clone();
2533                        let action_log = action_log.clone();
2534                        let telemetry = telemetry.clone();
2535                        move |_, _, cx| {
2536                            action_log.update(cx, |action_log, cx| {
2537                                action_log
2538                                    .reject_edits_in_ranges(
2539                                        buffer.clone(),
2540                                        vec![Anchor::min_max_range_for_buffer(
2541                                            buffer.read(cx).remote_id(),
2542                                        )],
2543                                        Some(telemetry.clone()),
2544                                        cx,
2545                                    )
2546                                    .0
2547                                    .detach_and_log_err(cx);
2548                            })
2549                        }
2550                    }),
2551            )
2552            .child(
2553                Button::new(("keep-file", index), "Keep")
2554                    .label_size(LabelSize::Small)
2555                    .disabled(pending_edits)
2556                    .on_click({
2557                        let buffer = buffer.clone();
2558                        let action_log = action_log.clone();
2559                        let telemetry = telemetry.clone();
2560                        move |_, _, cx| {
2561                            action_log.update(cx, |action_log, cx| {
2562                                action_log.keep_edits_in_range(
2563                                    buffer.clone(),
2564                                    Anchor::min_max_range_for_buffer(buffer.read(cx).remote_id()),
2565                                    Some(telemetry.clone()),
2566                                    cx,
2567                                );
2568                            })
2569                        }
2570                    }),
2571            )
2572    }
2573
2574    fn collect_subagent_items_for_sessions(
2575        entries: &[AgentThreadEntry],
2576        awaiting_session_ids: &[acp::SessionId],
2577        cx: &App,
2578    ) -> Vec<(SharedString, usize)> {
2579        let tool_calls_by_session: HashMap<_, _> = entries
2580            .iter()
2581            .enumerate()
2582            .filter_map(|(entry_ix, entry)| {
2583                let AgentThreadEntry::ToolCall(tool_call) = entry else {
2584                    return None;
2585                };
2586                let info = tool_call.subagent_session_info.as_ref()?;
2587                let summary_text = tool_call.label.read(cx).source().to_string();
2588                let subagent_summary = if summary_text.is_empty() {
2589                    SharedString::from("Subagent")
2590                } else {
2591                    SharedString::from(summary_text)
2592                };
2593                Some((info.session_id.clone(), (subagent_summary, entry_ix)))
2594            })
2595            .collect();
2596
2597        awaiting_session_ids
2598            .iter()
2599            .filter_map(|session_id| tool_calls_by_session.get(session_id).cloned())
2600            .collect()
2601    }
2602
2603    fn render_subagents_awaiting_permission(&self, cx: &Context<Self>) -> Option<AnyElement> {
2604        let awaiting = self.conversation.read(cx).subagents_awaiting_permission(cx);
2605
2606        if awaiting.is_empty() {
2607            return None;
2608        }
2609
2610        let awaiting_session_ids: Vec<_> = awaiting
2611            .iter()
2612            .map(|(session_id, _)| session_id.clone())
2613            .collect();
2614
2615        let thread = self.thread.read(cx);
2616        let entries = thread.entries();
2617        let subagent_items =
2618            Self::collect_subagent_items_for_sessions(entries, &awaiting_session_ids, cx);
2619
2620        if subagent_items.is_empty() {
2621            return None;
2622        }
2623
2624        let item_count = subagent_items.len();
2625
2626        Some(
2627            v_flex()
2628                .child(
2629                    h_flex()
2630                        .py_1()
2631                        .px_2()
2632                        .w_full()
2633                        .gap_1()
2634                        .border_b_1()
2635                        .border_color(cx.theme().colors().border)
2636                        .child(
2637                            Label::new("Subagents Awaiting Permission:")
2638                                .size(LabelSize::Small)
2639                                .color(Color::Muted),
2640                        )
2641                        .child(Label::new(item_count.to_string()).size(LabelSize::Small)),
2642                )
2643                .child(
2644                    v_flex().children(subagent_items.into_iter().enumerate().map(
2645                        |(ix, (label, entry_ix))| {
2646                            let is_last = ix == item_count - 1;
2647                            let group = format!("group-{}", entry_ix);
2648
2649                            h_flex()
2650                                .cursor_pointer()
2651                                .id(format!("subagent-permission-{}", entry_ix))
2652                                .group(&group)
2653                                .p_1()
2654                                .pl_2()
2655                                .min_w_0()
2656                                .w_full()
2657                                .gap_1()
2658                                .justify_between()
2659                                .bg(cx.theme().colors().editor_background)
2660                                .hover(|s| s.bg(cx.theme().colors().element_hover))
2661                                .when(!is_last, |this| {
2662                                    this.border_b_1().border_color(cx.theme().colors().border)
2663                                })
2664                                .child(
2665                                    h_flex()
2666                                        .gap_1p5()
2667                                        .child(
2668                                            Icon::new(IconName::Circle)
2669                                                .size(IconSize::XSmall)
2670                                                .color(Color::Warning),
2671                                        )
2672                                        .child(
2673                                            Label::new(label)
2674                                                .size(LabelSize::Small)
2675                                                .color(Color::Muted)
2676                                                .truncate(),
2677                                        ),
2678                                )
2679                                .child(
2680                                    div().visible_on_hover(&group).child(
2681                                        Label::new("Scroll to Subagent")
2682                                            .size(LabelSize::Small)
2683                                            .color(Color::Muted)
2684                                            .truncate(),
2685                                    ),
2686                                )
2687                                .on_click(cx.listener(move |this, _, _, cx| {
2688                                    this.list_state.scroll_to(ListOffset {
2689                                        item_ix: entry_ix,
2690                                        offset_in_item: px(0.0),
2691                                    });
2692                                    cx.notify();
2693                                }))
2694                        },
2695                    )),
2696                )
2697                .into_any(),
2698        )
2699    }
2700
2701    fn render_message_queue_summary(
2702        &self,
2703        _window: &mut Window,
2704        cx: &Context<Self>,
2705    ) -> impl IntoElement {
2706        let queue_count = self.local_queued_messages.len();
2707        let title: SharedString = if queue_count == 1 {
2708            "1 Queued Message".into()
2709        } else {
2710            format!("{} Queued Messages", queue_count).into()
2711        };
2712
2713        h_flex()
2714            .p_1()
2715            .w_full()
2716            .gap_1()
2717            .justify_between()
2718            .when(self.queue_expanded, |this| {
2719                this.border_b_1().border_color(cx.theme().colors().border)
2720            })
2721            .child(
2722                h_flex()
2723                    .id("queue_summary")
2724                    .gap_1()
2725                    .child(Disclosure::new("queue_disclosure", self.queue_expanded))
2726                    .child(Label::new(title).size(LabelSize::Small).color(Color::Muted))
2727                    .on_click(cx.listener(|this, _, _, cx| {
2728                        this.queue_expanded = !this.queue_expanded;
2729                        cx.notify();
2730                    })),
2731            )
2732            .child(
2733                Button::new("clear_queue", "Clear All")
2734                    .label_size(LabelSize::Small)
2735                    .key_binding(
2736                        KeyBinding::for_action(&ClearMessageQueue, cx)
2737                            .map(|kb| kb.size(rems_from_px(12.))),
2738                    )
2739                    .on_click(cx.listener(|this, _, _, cx| {
2740                        this.clear_queue(cx);
2741                        this.can_fast_track_queue = false;
2742                        cx.notify();
2743                    })),
2744            )
2745            .into_any_element()
2746    }
2747
2748    fn clear_queue(&mut self, cx: &mut Context<Self>) {
2749        self.local_queued_messages.clear();
2750        self.sync_queue_flag_to_native_thread(cx);
2751    }
2752
2753    fn render_plan_summary(
2754        &self,
2755        plan: &Plan,
2756        window: &mut Window,
2757        cx: &Context<Self>,
2758    ) -> impl IntoElement {
2759        let plan_expanded = self.plan_expanded;
2760        let stats = plan.stats();
2761
2762        let title = if let Some(entry) = stats.in_progress_entry
2763            && !plan_expanded
2764        {
2765            h_flex()
2766                .cursor_default()
2767                .relative()
2768                .w_full()
2769                .gap_1()
2770                .truncate()
2771                .child(
2772                    Label::new("Current:")
2773                        .size(LabelSize::Small)
2774                        .color(Color::Muted),
2775                )
2776                .child(
2777                    div()
2778                        .text_xs()
2779                        .text_color(cx.theme().colors().text_muted)
2780                        .line_clamp(1)
2781                        .child(MarkdownElement::new(
2782                            entry.content.clone(),
2783                            plan_label_markdown_style(&entry.status, window, cx),
2784                        )),
2785                )
2786                .when(stats.pending > 0, |this| {
2787                    this.child(
2788                        h_flex()
2789                            .absolute()
2790                            .top_0()
2791                            .right_0()
2792                            .h_full()
2793                            .child(div().min_w_8().h_full().bg(linear_gradient(
2794                                90.,
2795                                linear_color_stop(self.activity_bar_bg(cx), 1.),
2796                                linear_color_stop(self.activity_bar_bg(cx).opacity(0.2), 0.),
2797                            )))
2798                            .child(
2799                                div().pr_0p5().bg(self.activity_bar_bg(cx)).child(
2800                                    Label::new(format!("{} left", stats.pending))
2801                                        .size(LabelSize::Small)
2802                                        .color(Color::Muted),
2803                                ),
2804                            ),
2805                    )
2806                })
2807        } else {
2808            let status_label = if stats.pending == 0 {
2809                "All Done".to_string()
2810            } else if stats.completed == 0 {
2811                format!("{} Tasks", plan.entries.len())
2812            } else {
2813                format!("{}/{}", stats.completed, plan.entries.len())
2814            };
2815
2816            h_flex()
2817                .w_full()
2818                .gap_1()
2819                .justify_between()
2820                .child(
2821                    Label::new("Plan")
2822                        .size(LabelSize::Small)
2823                        .color(Color::Muted),
2824                )
2825                .child(
2826                    Label::new(status_label)
2827                        .size(LabelSize::Small)
2828                        .color(Color::Muted)
2829                        .mr_1(),
2830                )
2831        };
2832
2833        h_flex()
2834            .id("plan_summary")
2835            .p_1()
2836            .w_full()
2837            .gap_1()
2838            .when(plan_expanded, |this| {
2839                this.border_b_1().border_color(cx.theme().colors().border)
2840            })
2841            .child(Disclosure::new("plan_disclosure", plan_expanded))
2842            .child(title.flex_1())
2843            .child(
2844                IconButton::new("dismiss-plan", IconName::Close)
2845                    .icon_size(IconSize::XSmall)
2846                    .shape(ui::IconButtonShape::Square)
2847                    .tooltip(Tooltip::text("Clear plan"))
2848                    .on_click(cx.listener(|this, _, _, cx| {
2849                        this.thread.update(cx, |thread, cx| thread.clear_plan(cx));
2850                        cx.stop_propagation();
2851                    })),
2852            )
2853            .on_click(cx.listener(|this, _, _, cx| {
2854                this.plan_expanded = !this.plan_expanded;
2855                cx.notify();
2856            }))
2857            .into_any_element()
2858    }
2859
2860    fn render_plan_entries(
2861        &self,
2862        plan: &Plan,
2863        window: &mut Window,
2864        cx: &Context<Self>,
2865    ) -> impl IntoElement {
2866        v_flex()
2867            .id("plan_items_list")
2868            .max_h_40()
2869            .overflow_y_scroll()
2870            .children(plan.entries.iter().enumerate().flat_map(|(index, entry)| {
2871                let element = h_flex()
2872                    .py_1()
2873                    .px_2()
2874                    .gap_2()
2875                    .justify_between()
2876                    .bg(cx.theme().colors().editor_background)
2877                    .when(index < plan.entries.len() - 1, |parent| {
2878                        parent.border_color(cx.theme().colors().border).border_b_1()
2879                    })
2880                    .child(
2881                        h_flex()
2882                            .id(("plan_entry", index))
2883                            .gap_1p5()
2884                            .max_w_full()
2885                            .overflow_x_scroll()
2886                            .text_xs()
2887                            .text_color(cx.theme().colors().text_muted)
2888                            .child(match entry.status {
2889                                acp::PlanEntryStatus::InProgress => {
2890                                    Icon::new(IconName::TodoProgress)
2891                                        .size(IconSize::Small)
2892                                        .color(Color::Accent)
2893                                        .with_rotate_animation(2)
2894                                        .into_any_element()
2895                                }
2896                                acp::PlanEntryStatus::Completed => {
2897                                    Icon::new(IconName::TodoComplete)
2898                                        .size(IconSize::Small)
2899                                        .color(Color::Success)
2900                                        .into_any_element()
2901                                }
2902                                acp::PlanEntryStatus::Pending | _ => {
2903                                    Icon::new(IconName::TodoPending)
2904                                        .size(IconSize::Small)
2905                                        .color(Color::Muted)
2906                                        .into_any_element()
2907                                }
2908                            })
2909                            .child(MarkdownElement::new(
2910                                entry.content.clone(),
2911                                plan_label_markdown_style(&entry.status, window, cx),
2912                            )),
2913                    );
2914
2915                Some(element)
2916            }))
2917            .into_any_element()
2918    }
2919
2920    fn render_completed_plan(
2921        &self,
2922        entries: &[PlanEntry],
2923        window: &Window,
2924        cx: &Context<Self>,
2925    ) -> AnyElement {
2926        v_flex()
2927            .px_5()
2928            .py_1p5()
2929            .w_full()
2930            .child(
2931                v_flex()
2932                    .w_full()
2933                    .rounded_md()
2934                    .border_1()
2935                    .border_color(self.tool_card_border_color(cx))
2936                    .child(
2937                        h_flex()
2938                            .px_2()
2939                            .py_1()
2940                            .gap_1()
2941                            .bg(self.tool_card_header_bg(cx))
2942                            .border_b_1()
2943                            .border_color(self.tool_card_border_color(cx))
2944                            .child(
2945                                Label::new("Completed Plan")
2946                                    .size(LabelSize::Small)
2947                                    .color(Color::Muted),
2948                            )
2949                            .child(
2950                                Label::new(format!(
2951                                    "{} {}",
2952                                    entries.len(),
2953                                    if entries.len() == 1 { "step" } else { "steps" }
2954                                ))
2955                                .size(LabelSize::Small)
2956                                .color(Color::Muted),
2957                            ),
2958                    )
2959                    .child(
2960                        v_flex().children(entries.iter().enumerate().map(|(index, entry)| {
2961                            h_flex()
2962                                .py_1()
2963                                .px_2()
2964                                .gap_1p5()
2965                                .when(index < entries.len() - 1, |this| {
2966                                    this.border_b_1().border_color(cx.theme().colors().border)
2967                                })
2968                                .child(
2969                                    Icon::new(IconName::TodoComplete)
2970                                        .size(IconSize::Small)
2971                                        .color(Color::Success),
2972                                )
2973                                .child(
2974                                    div()
2975                                        .max_w_full()
2976                                        .overflow_x_hidden()
2977                                        .text_xs()
2978                                        .text_color(cx.theme().colors().text_muted)
2979                                        .child(MarkdownElement::new(
2980                                            entry.content.clone(),
2981                                            default_markdown_style(window, cx),
2982                                        )),
2983                                )
2984                        })),
2985                    ),
2986            )
2987            .into_any()
2988    }
2989
2990    fn render_edits_summary(
2991        &self,
2992        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
2993        expanded: bool,
2994        pending_edits: bool,
2995        cx: &Context<Self>,
2996    ) -> Div {
2997        const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete.";
2998
2999        let focus_handle = self.focus_handle(cx);
3000
3001        h_flex()
3002            .p_1()
3003            .justify_between()
3004            .flex_wrap()
3005            .when(expanded, |this| {
3006                this.border_b_1().border_color(cx.theme().colors().border)
3007            })
3008            .child(
3009                h_flex()
3010                    .id("edits-container")
3011                    .cursor_pointer()
3012                    .gap_1()
3013                    .child(Disclosure::new("edits-disclosure", expanded))
3014                    .map(|this| {
3015                        if pending_edits {
3016                            this.child(
3017                                Label::new(format!(
3018                                    "Editing {} {}",
3019                                    changed_buffers.len(),
3020                                    if changed_buffers.len() == 1 {
3021                                        "file"
3022                                    } else {
3023                                        "files"
3024                                    }
3025                                ))
3026                                .color(Color::Muted)
3027                                .size(LabelSize::Small)
3028                                .with_animation(
3029                                    "edit-label",
3030                                    Animation::new(Duration::from_secs(2))
3031                                        .repeat()
3032                                        .with_easing(pulsating_between(0.3, 0.7)),
3033                                    |label, delta| label.alpha(delta),
3034                                ),
3035                            )
3036                        } else {
3037                            let stats = DiffStats::all_files(changed_buffers, cx);
3038                            let dot_divider = || {
3039                                Label::new("")
3040                                    .size(LabelSize::XSmall)
3041                                    .color(Color::Disabled)
3042                            };
3043
3044                            this.child(
3045                                Label::new("Edits")
3046                                    .size(LabelSize::Small)
3047                                    .color(Color::Muted),
3048                            )
3049                            .child(dot_divider())
3050                            .child(
3051                                Label::new(format!(
3052                                    "{} {}",
3053                                    changed_buffers.len(),
3054                                    if changed_buffers.len() == 1 {
3055                                        "file"
3056                                    } else {
3057                                        "files"
3058                                    }
3059                                ))
3060                                .size(LabelSize::Small)
3061                                .color(Color::Muted),
3062                            )
3063                            .child(dot_divider())
3064                            .child(DiffStat::new(
3065                                "total",
3066                                stats.lines_added as usize,
3067                                stats.lines_removed as usize,
3068                            ))
3069                        }
3070                    })
3071                    .on_click(cx.listener(|this, _, _, cx| {
3072                        this.edits_expanded = !this.edits_expanded;
3073                        cx.notify();
3074                    })),
3075            )
3076            .child(
3077                h_flex()
3078                    .gap_1()
3079                    .child(
3080                        IconButton::new("review-changes", IconName::ListTodo)
3081                            .icon_size(IconSize::Small)
3082                            .tooltip({
3083                                let focus_handle = focus_handle.clone();
3084                                move |_window, cx| {
3085                                    Tooltip::for_action_in(
3086                                        "Review Changes",
3087                                        &OpenAgentDiff,
3088                                        &focus_handle,
3089                                        cx,
3090                                    )
3091                                }
3092                            })
3093                            .on_click(cx.listener(|_, _, window, cx| {
3094                                window.dispatch_action(OpenAgentDiff.boxed_clone(), cx);
3095                            })),
3096                    )
3097                    .child(Divider::vertical().color(DividerColor::Border))
3098                    .child(
3099                        Button::new("reject-all-changes", "Reject All")
3100                            .label_size(LabelSize::Small)
3101                            .disabled(pending_edits)
3102                            .when(pending_edits, |this| {
3103                                this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
3104                            })
3105                            .key_binding(
3106                                KeyBinding::for_action_in(&RejectAll, &focus_handle.clone(), cx)
3107                                    .map(|kb| kb.size(rems_from_px(12.))),
3108                            )
3109                            .on_click(cx.listener(move |this, _, window, cx| {
3110                                this.reject_all(&RejectAll, window, cx);
3111                            })),
3112                    )
3113                    .child(
3114                        Button::new("keep-all-changes", "Keep All")
3115                            .label_size(LabelSize::Small)
3116                            .disabled(pending_edits)
3117                            .when(pending_edits, |this| {
3118                                this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
3119                            })
3120                            .key_binding(
3121                                KeyBinding::for_action_in(&KeepAll, &focus_handle, cx)
3122                                    .map(|kb| kb.size(rems_from_px(12.))),
3123                            )
3124                            .on_click(cx.listener(move |this, _, window, cx| {
3125                                this.keep_all(&KeepAll, window, cx);
3126                            })),
3127                    ),
3128            )
3129    }
3130
3131    fn is_subagent_canceled_or_failed(&self, cx: &App) -> bool {
3132        let Some(parent_session_id) = self.parent_session_id.as_ref() else {
3133            return false;
3134        };
3135
3136        let my_session_id = self.thread.read(cx).session_id().clone();
3137
3138        self.server_view
3139            .upgrade()
3140            .and_then(|sv| sv.read(cx).thread_view(parent_session_id))
3141            .is_some_and(|parent_view| {
3142                parent_view
3143                    .read(cx)
3144                    .thread
3145                    .read(cx)
3146                    .tool_call_for_subagent(&my_session_id)
3147                    .is_some_and(|tc| {
3148                        matches!(
3149                            tc.status,
3150                            ToolCallStatus::Canceled
3151                                | ToolCallStatus::Failed
3152                                | ToolCallStatus::Rejected
3153                        )
3154                    })
3155            })
3156    }
3157
3158    pub(crate) fn render_subagent_titlebar(&mut self, cx: &mut Context<Self>) -> Option<Div> {
3159        if self.parent_session_id.is_none() {
3160            return None;
3161        }
3162        let parent_session_id = self.thread.read(cx).parent_session_id()?.clone();
3163
3164        let server_view = self.server_view.clone();
3165        let thread = self.thread.clone();
3166        let is_done = thread.read(cx).status() == ThreadStatus::Idle;
3167        let is_canceled_or_failed = self.is_subagent_canceled_or_failed(cx);
3168
3169        let max_content_width = AgentSettings::get_global(cx).max_content_width;
3170
3171        Some(
3172            h_flex()
3173                .w_full()
3174                .h(Tab::container_height(cx))
3175                .border_b_1()
3176                .when(is_done && is_canceled_or_failed, |this| {
3177                    this.border_dashed()
3178                })
3179                .border_color(cx.theme().colors().border)
3180                .bg(cx.theme().colors().editor_background.opacity(0.2))
3181                .child(
3182                    h_flex()
3183                        .size_full()
3184                        .max_w(max_content_width)
3185                        .mx_auto()
3186                        .pl_2()
3187                        .pr_1()
3188                        .flex_shrink_0()
3189                        .justify_between()
3190                        .gap_1()
3191                        .child(
3192                            h_flex()
3193                                .flex_1()
3194                                .gap_2()
3195                                .child(
3196                                    Icon::new(IconName::ForwardArrowUp)
3197                                        .size(IconSize::Small)
3198                                        .color(Color::Muted),
3199                                )
3200                                .child(self.title_editor.clone())
3201                                .when(is_done && is_canceled_or_failed, |this| {
3202                                    this.child(Icon::new(IconName::Close).color(Color::Error))
3203                                })
3204                                .when(is_done && !is_canceled_or_failed, |this| {
3205                                    this.child(Icon::new(IconName::Check).color(Color::Success))
3206                                }),
3207                        )
3208                        .child(
3209                            h_flex()
3210                                .gap_0p5()
3211                                .when(!is_done, |this| {
3212                                    this.child(
3213                                        IconButton::new("stop_subagent", IconName::Stop)
3214                                            .icon_size(IconSize::Small)
3215                                            .icon_color(Color::Error)
3216                                            .tooltip(Tooltip::text("Stop Subagent"))
3217                                            .on_click(move |_, _, cx| {
3218                                                thread.update(cx, |thread, cx| {
3219                                                    thread.cancel(cx).detach();
3220                                                });
3221                                            }),
3222                                    )
3223                                })
3224                                .child(
3225                                    IconButton::new("minimize_subagent", IconName::Dash)
3226                                        .icon_size(IconSize::Small)
3227                                        .tooltip(Tooltip::text("Minimize Subagent"))
3228                                        .on_click(move |_, window, cx| {
3229                                            let _ = server_view.update(cx, |server_view, cx| {
3230                                                server_view.navigate_to_thread(
3231                                                    parent_session_id.clone(),
3232                                                    window,
3233                                                    cx,
3234                                                );
3235                                            });
3236                                        }),
3237                                ),
3238                        ),
3239                ),
3240        )
3241    }
3242
3243    pub(crate) fn render_message_editor(
3244        &mut self,
3245        window: &mut Window,
3246        cx: &mut Context<Self>,
3247    ) -> AnyElement {
3248        if self.is_subagent() {
3249            return div().into_any_element();
3250        }
3251
3252        let focus_handle = self.message_editor.focus_handle(cx);
3253        let editor_bg_color = cx.theme().colors().editor_background;
3254
3255        let editor_expanded = self.editor_expanded;
3256        let (expand_icon, expand_tooltip) = if editor_expanded {
3257            (IconName::Minimize, "Minimize Message Editor")
3258        } else {
3259            (IconName::Maximize, "Expand Message Editor")
3260        };
3261
3262        let max_content_width = AgentSettings::get_global(cx).max_content_width;
3263        let has_messages = self.list_state.item_count() > 0;
3264        let fills_container = !has_messages || editor_expanded;
3265
3266        h_flex()
3267            .p_2()
3268            .bg(editor_bg_color)
3269            .justify_center()
3270            .map(|this| {
3271                if has_messages {
3272                    this.on_action(cx.listener(Self::expand_message_editor))
3273                        .border_t_1()
3274                        .border_color(cx.theme().colors().border)
3275                        .when(editor_expanded, |this| this.h(vh(0.8, window)))
3276                } else {
3277                    this.flex_1().size_full()
3278                }
3279            })
3280            .child(
3281                v_flex()
3282                    .flex_basis(max_content_width)
3283                    .flex_shrink()
3284                    .flex_grow_0()
3285                    .when(fills_container, |this| this.h_full())
3286                    .justify_between()
3287                    .gap_2()
3288                    .child(
3289                        v_flex()
3290                            .relative()
3291                            .w_full()
3292                            .min_h_0()
3293                            .when(fills_container, |this| this.flex_1())
3294                            .pt_1()
3295                            .pr_2p5()
3296                            .child(self.message_editor.clone())
3297                            .when(has_messages, |this| {
3298                                this.child(
3299                                    h_flex()
3300                                        .absolute()
3301                                        .top_0()
3302                                        .right_0()
3303                                        .opacity(0.5)
3304                                        .hover(|s| s.opacity(1.0))
3305                                        .child(
3306                                            IconButton::new("toggle-height", expand_icon)
3307                                                .icon_size(IconSize::Small)
3308                                                .icon_color(Color::Muted)
3309                                                .tooltip({
3310                                                    move |_window, cx| {
3311                                                        Tooltip::for_action_in(
3312                                                            expand_tooltip,
3313                                                            &ExpandMessageEditor,
3314                                                            &focus_handle,
3315                                                            cx,
3316                                                        )
3317                                                    }
3318                                                })
3319                                                .on_click(cx.listener(|this, _, window, cx| {
3320                                                    this.expand_message_editor(
3321                                                        &ExpandMessageEditor,
3322                                                        window,
3323                                                        cx,
3324                                                    );
3325                                                })),
3326                                        ),
3327                                )
3328                            }),
3329                    )
3330                    .child(
3331                        h_flex()
3332                            .w_full()
3333                            .flex_none()
3334                            .flex_wrap()
3335                            .justify_between()
3336                            .child(
3337                                h_flex()
3338                                    .gap_0p5()
3339                                    .child(self.render_add_context_button(cx))
3340                                    .child(self.render_follow_toggle(cx))
3341                                    .children(self.render_fast_mode_control(cx))
3342                                    .children(self.render_thinking_control(cx)),
3343                            )
3344                            .child(
3345                                h_flex()
3346                                    .gap_1()
3347                                    .children(self.render_token_usage(cx))
3348                                    .children(self.profile_selector.clone())
3349                                    .map(|this| match self.config_options_view.clone() {
3350                                        Some(config_view) => this.child(config_view),
3351                                        None => this
3352                                            .children(self.mode_selector.clone())
3353                                            .children(self.model_selector.clone()),
3354                                    })
3355                                    .child(self.render_send_button(cx)),
3356                            ),
3357                    ),
3358            )
3359            .into_any()
3360    }
3361
3362    fn render_message_queue_entries(
3363        &self,
3364        _window: &mut Window,
3365        cx: &Context<Self>,
3366    ) -> impl IntoElement {
3367        let message_editor = self.message_editor.read(cx);
3368        let focus_handle = message_editor.focus_handle(cx);
3369
3370        let queued_message_editors = &self.queued_message_editors;
3371        let queue_len = queued_message_editors.len();
3372        let can_fast_track = self.can_fast_track_queue && queue_len > 0;
3373
3374        v_flex()
3375            .id("message_queue_list")
3376            .max_h_40()
3377            .overflow_y_scroll()
3378            .children(
3379                queued_message_editors
3380                    .iter()
3381                    .enumerate()
3382                    .map(|(index, editor)| {
3383                        let is_next = index == 0;
3384                        let (icon_color, tooltip_text) = if is_next {
3385                            (Color::Accent, "Next in Queue")
3386                        } else {
3387                            (Color::Muted, "In Queue")
3388                        };
3389
3390                        let editor_focused = editor.focus_handle(cx).is_focused(_window);
3391                        let keybinding_size = rems_from_px(12.);
3392
3393                        h_flex()
3394                            .group("queue_entry")
3395                            .w_full()
3396                            .p_1p5()
3397                            .gap_1()
3398                            .bg(cx.theme().colors().editor_background)
3399                            .when(index < queue_len - 1, |this| {
3400                                this.border_b_1()
3401                                    .border_color(cx.theme().colors().border_variant)
3402                            })
3403                            .child(
3404                                div()
3405                                    .id("next_in_queue")
3406                                    .child(
3407                                        Icon::new(IconName::Circle)
3408                                            .size(IconSize::Small)
3409                                            .color(icon_color),
3410                                    )
3411                                    .tooltip(Tooltip::text(tooltip_text)),
3412                            )
3413                            .child(editor.clone())
3414                            .child(if editor_focused {
3415                                h_flex()
3416                                    .gap_1()
3417                                    .min_w(rems_from_px(150.))
3418                                    .justify_end()
3419                                    .child(
3420                                        IconButton::new(("edit", index), IconName::Pencil)
3421                                            .icon_size(IconSize::Small)
3422                                            .tooltip(|_window, cx| {
3423                                                Tooltip::with_meta(
3424                                                    "Edit Queued Message",
3425                                                    None,
3426                                                    "Type anything to edit",
3427                                                    cx,
3428                                                )
3429                                            })
3430                                            .on_click(cx.listener(move |this, _, window, cx| {
3431                                                this.move_queued_message_to_main_editor(
3432                                                    index, None, None, window, cx,
3433                                                );
3434                                            })),
3435                                    )
3436                                    .child(
3437                                        Button::new(("send_now_focused", index), "Send Now")
3438                                            .label_size(LabelSize::Small)
3439                                            .style(ButtonStyle::Outlined)
3440                                            .key_binding(
3441                                                KeyBinding::for_action_in(
3442                                                    &SendImmediately,
3443                                                    &editor.focus_handle(cx),
3444                                                    cx,
3445                                                )
3446                                                .map(|kb| kb.size(keybinding_size)),
3447                                            )
3448                                            .on_click(cx.listener(move |this, _, window, cx| {
3449                                                this.send_queued_message_at_index(
3450                                                    index, true, window, cx,
3451                                                );
3452                                            })),
3453                                    )
3454                            } else {
3455                                h_flex()
3456                                    .when(!is_next, |this| this.visible_on_hover("queue_entry"))
3457                                    .gap_1()
3458                                    .min_w(rems_from_px(150.))
3459                                    .justify_end()
3460                                    .child(
3461                                        IconButton::new(("delete", index), IconName::Trash)
3462                                            .icon_size(IconSize::Small)
3463                                            .tooltip({
3464                                                let focus_handle = focus_handle.clone();
3465                                                move |_window, cx| {
3466                                                    if is_next {
3467                                                        Tooltip::for_action_in(
3468                                                            "Remove Message from Queue",
3469                                                            &RemoveFirstQueuedMessage,
3470                                                            &focus_handle,
3471                                                            cx,
3472                                                        )
3473                                                    } else {
3474                                                        Tooltip::simple(
3475                                                            "Remove Message from Queue",
3476                                                            cx,
3477                                                        )
3478                                                    }
3479                                                }
3480                                            })
3481                                            .on_click(cx.listener(move |this, _, _, cx| {
3482                                                this.remove_from_queue(index, cx);
3483                                                cx.notify();
3484                                            })),
3485                                    )
3486                                    .child(
3487                                        IconButton::new(("edit", index), IconName::Pencil)
3488                                            .icon_size(IconSize::Small)
3489                                            .tooltip({
3490                                                let focus_handle = focus_handle.clone();
3491                                                move |_window, cx| {
3492                                                    if is_next {
3493                                                        Tooltip::for_action_in(
3494                                                            "Edit",
3495                                                            &EditFirstQueuedMessage,
3496                                                            &focus_handle,
3497                                                            cx,
3498                                                        )
3499                                                    } else {
3500                                                        Tooltip::simple("Edit", cx)
3501                                                    }
3502                                                }
3503                                            })
3504                                            .on_click(cx.listener(move |this, _, window, cx| {
3505                                                this.move_queued_message_to_main_editor(
3506                                                    index, None, None, window, cx,
3507                                                );
3508                                            })),
3509                                    )
3510                                    .child(
3511                                        Button::new(("send_now", index), "Send Now")
3512                                            .label_size(LabelSize::Small)
3513                                            .when(is_next, |this| this.style(ButtonStyle::Outlined))
3514                                            .when(is_next && message_editor.is_empty(cx), |this| {
3515                                                let action: Box<dyn gpui::Action> =
3516                                                    if can_fast_track {
3517                                                        Box::new(Chat)
3518                                                    } else {
3519                                                        Box::new(SendNextQueuedMessage)
3520                                                    };
3521
3522                                                this.key_binding(
3523                                                    KeyBinding::for_action_in(
3524                                                        action.as_ref(),
3525                                                        &focus_handle.clone(),
3526                                                        cx,
3527                                                    )
3528                                                    .map(|kb| kb.size(keybinding_size)),
3529                                                )
3530                                            })
3531                                            .on_click(cx.listener(move |this, _, window, cx| {
3532                                                this.send_queued_message_at_index(
3533                                                    index, true, window, cx,
3534                                                );
3535                                            })),
3536                                    )
3537                            })
3538                    }),
3539            )
3540            .into_any_element()
3541    }
3542
3543    fn supports_split_token_display(&self, cx: &App) -> bool {
3544        self.as_native_thread(cx)
3545            .and_then(|thread| thread.read(cx).model())
3546            .is_some_and(|model| model.supports_split_token_display())
3547    }
3548
3549    fn render_token_usage(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
3550        let thread = self.thread.read(cx);
3551        let usage = thread.token_usage()?;
3552        let show_split = self.supports_split_token_display(cx);
3553
3554        let cost_label = if cx.has_flag::<AcpBetaFeatureFlag>() {
3555            thread.cost().map(|cost| {
3556                let precision = if cost.amount > 0.0 && cost.amount < 0.01 {
3557                    4
3558                } else {
3559                    2
3560                };
3561                format!("{:.prec$} {}", cost.amount, cost.currency, prec = precision)
3562            })
3563        } else {
3564            None
3565        };
3566
3567        let progress_color = |ratio: f32| -> Hsla {
3568            if ratio >= 0.85 {
3569                cx.theme().status().warning
3570            } else {
3571                cx.theme().colors().text_muted
3572            }
3573        };
3574
3575        let used = crate::humanize_token_count(usage.used_tokens);
3576        let max = crate::humanize_token_count(usage.max_tokens);
3577        let input_tokens_label = crate::humanize_token_count(usage.input_tokens);
3578        let output_tokens_label = crate::humanize_token_count(usage.output_tokens);
3579
3580        let progress_ratio = if usage.max_tokens > 0 {
3581            usage.used_tokens as f32 / usage.max_tokens as f32
3582        } else {
3583            0.0
3584        };
3585
3586        let ring_size = px(16.0);
3587        let stroke_width = px(2.);
3588
3589        let percentage = format!("{}%", (progress_ratio * 100.0).round() as u32);
3590
3591        let tooltip_separator_color = Color::Custom(cx.theme().colors().text_disabled.opacity(0.6));
3592
3593        let (user_rules_count, first_user_rules_id, project_rules_count, project_entry_ids) = self
3594            .as_native_thread(cx)
3595            .map(|thread| {
3596                let project_context = thread.read(cx).project_context().read(cx);
3597                let user_rules_count = project_context.user_rules.len();
3598                let first_user_rules_id = project_context.user_rules.first().map(|r| r.uuid.0);
3599                let project_entry_ids = project_context
3600                    .worktrees
3601                    .iter()
3602                    .filter_map(|wt| wt.rules_file.as_ref())
3603                    .map(|rf| ProjectEntryId::from_usize(rf.project_entry_id))
3604                    .collect::<Vec<_>>();
3605                let project_rules_count = project_entry_ids.len();
3606                (
3607                    user_rules_count,
3608                    first_user_rules_id,
3609                    project_rules_count,
3610                    project_entry_ids,
3611                )
3612            })
3613            .unwrap_or_default();
3614
3615        let workspace = self.workspace.clone();
3616
3617        let max_output_tokens = self
3618            .as_native_thread(cx)
3619            .and_then(|thread| thread.read(cx).model())
3620            .and_then(|model| model.max_output_tokens())
3621            .unwrap_or(0);
3622        let input_max_label =
3623            crate::humanize_token_count(usage.max_tokens.saturating_sub(max_output_tokens));
3624        let output_max_label = crate::humanize_token_count(max_output_tokens);
3625
3626        let build_tooltip = {
3627            move |_window: &mut Window, cx: &mut App| {
3628                let percentage = percentage.clone();
3629                let used = used.clone();
3630                let max = max.clone();
3631                let input_tokens_label = input_tokens_label.clone();
3632                let output_tokens_label = output_tokens_label.clone();
3633                let input_max_label = input_max_label.clone();
3634                let output_max_label = output_max_label.clone();
3635                let project_entry_ids = project_entry_ids.clone();
3636                let workspace = workspace.clone();
3637                let cost_label = cost_label.clone();
3638                cx.new(move |_cx| TokenUsageTooltip {
3639                    percentage,
3640                    used,
3641                    max,
3642                    input_tokens: input_tokens_label,
3643                    output_tokens: output_tokens_label,
3644                    input_max: input_max_label,
3645                    output_max: output_max_label,
3646                    show_split,
3647                    cost_label,
3648                    separator_color: tooltip_separator_color,
3649                    user_rules_count,
3650                    first_user_rules_id,
3651                    project_rules_count,
3652                    project_entry_ids,
3653                    workspace,
3654                })
3655                .into()
3656            }
3657        };
3658
3659        if show_split {
3660            let input_max_raw = usage.max_tokens.saturating_sub(max_output_tokens);
3661            let output_max_raw = max_output_tokens;
3662
3663            let input_ratio = if input_max_raw > 0 {
3664                usage.input_tokens as f32 / input_max_raw as f32
3665            } else {
3666                0.0
3667            };
3668            let output_ratio = if output_max_raw > 0 {
3669                usage.output_tokens as f32 / output_max_raw as f32
3670            } else {
3671                0.0
3672            };
3673
3674            Some(
3675                h_flex()
3676                    .id("split_token_usage")
3677                    .flex_shrink_0()
3678                    .gap_1p5()
3679                    .mr_1()
3680                    .child(
3681                        h_flex()
3682                            .gap_0p5()
3683                            .child(
3684                                Icon::new(IconName::ArrowUp)
3685                                    .size(IconSize::XSmall)
3686                                    .color(Color::Muted),
3687                            )
3688                            .child(
3689                                CircularProgress::new(
3690                                    usage.input_tokens as f32,
3691                                    input_max_raw as f32,
3692                                    ring_size,
3693                                    cx,
3694                                )
3695                                .stroke_width(stroke_width)
3696                                .progress_color(progress_color(input_ratio)),
3697                            ),
3698                    )
3699                    .child(
3700                        h_flex()
3701                            .gap_0p5()
3702                            .child(
3703                                Icon::new(IconName::ArrowDown)
3704                                    .size(IconSize::XSmall)
3705                                    .color(Color::Muted),
3706                            )
3707                            .child(
3708                                CircularProgress::new(
3709                                    usage.output_tokens as f32,
3710                                    output_max_raw as f32,
3711                                    ring_size,
3712                                    cx,
3713                                )
3714                                .stroke_width(stroke_width)
3715                                .progress_color(progress_color(output_ratio)),
3716                            ),
3717                    )
3718                    .hoverable_tooltip(build_tooltip)
3719                    .into_any_element(),
3720            )
3721        } else {
3722            Some(
3723                h_flex()
3724                    .id("circular_progress_tokens")
3725                    .mt_px()
3726                    .mr_1()
3727                    .child(
3728                        CircularProgress::new(
3729                            usage.used_tokens as f32,
3730                            usage.max_tokens as f32,
3731                            ring_size,
3732                            cx,
3733                        )
3734                        .stroke_width(stroke_width)
3735                        .progress_color(progress_color(progress_ratio)),
3736                    )
3737                    .hoverable_tooltip(build_tooltip)
3738                    .into_any_element(),
3739            )
3740        }
3741    }
3742
3743    fn fast_mode_available(&self, cx: &Context<Self>) -> bool {
3744        if !cx.is_staff() {
3745            return false;
3746        }
3747        self.as_native_thread(cx)
3748            .and_then(|thread| thread.read(cx).model())
3749            .map(|model| model.supports_fast_mode())
3750            .unwrap_or(false)
3751    }
3752
3753    fn render_fast_mode_control(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
3754        if !self.fast_mode_available(cx) {
3755            return None;
3756        }
3757
3758        let thread = self.as_native_thread(cx)?.read(cx);
3759
3760        let (tooltip_label, color, icon) = if matches!(thread.speed(), Some(Speed::Fast)) {
3761            ("Disable Fast Mode", Color::Muted, IconName::FastForward)
3762        } else {
3763            (
3764                "Enable Fast Mode",
3765                Color::Custom(cx.theme().colors().icon_disabled.opacity(0.8)),
3766                IconName::FastForwardOff,
3767            )
3768        };
3769
3770        let focus_handle = self.message_editor.focus_handle(cx);
3771
3772        Some(
3773            IconButton::new("fast-mode", icon)
3774                .icon_size(IconSize::Small)
3775                .icon_color(color)
3776                .tooltip(move |_, cx| {
3777                    Tooltip::for_action_in(tooltip_label, &ToggleFastMode, &focus_handle, cx)
3778                })
3779                .on_click(cx.listener(move |this, _, _window, cx| {
3780                    this.toggle_fast_mode(cx);
3781                }))
3782                .into_any_element(),
3783        )
3784    }
3785
3786    fn render_thinking_control(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
3787        let thread = self.as_native_thread(cx)?.read(cx);
3788        let model = thread.model()?;
3789
3790        let supports_thinking = model.supports_thinking();
3791        if !supports_thinking {
3792            return None;
3793        }
3794
3795        let thinking = thread.thinking_enabled();
3796
3797        let (tooltip_label, icon, color) = if thinking {
3798            (
3799                "Disable Thinking Mode",
3800                IconName::ThinkingMode,
3801                Color::Muted,
3802            )
3803        } else {
3804            (
3805                "Enable Thinking Mode",
3806                IconName::ThinkingModeOff,
3807                Color::Custom(cx.theme().colors().icon_disabled.opacity(0.8)),
3808            )
3809        };
3810
3811        let focus_handle = self.message_editor.focus_handle(cx);
3812
3813        let thinking_toggle = IconButton::new("thinking-mode", icon)
3814            .icon_size(IconSize::Small)
3815            .icon_color(color)
3816            .tooltip(move |_, cx| {
3817                Tooltip::for_action_in(tooltip_label, &ToggleThinkingMode, &focus_handle, cx)
3818            })
3819            .on_click(cx.listener(move |this, _, _window, cx| {
3820                if let Some(thread) = this.as_native_thread(cx) {
3821                    thread.update(cx, |thread, cx| {
3822                        let enable_thinking = !thread.thinking_enabled();
3823                        thread.set_thinking_enabled(enable_thinking, cx);
3824
3825                        let fs = thread.project().read(cx).fs().clone();
3826                        update_settings_file(fs, cx, move |settings, _| {
3827                            if let Some(agent) = settings.agent.as_mut()
3828                                && let Some(default_model) = agent.default_model.as_mut()
3829                            {
3830                                default_model.enable_thinking = enable_thinking;
3831                            }
3832                        });
3833                    });
3834                }
3835            }));
3836
3837        if model.supported_effort_levels().is_empty() {
3838            return Some(thinking_toggle.into_any_element());
3839        }
3840
3841        if !model.supported_effort_levels().is_empty() && !thinking {
3842            return Some(thinking_toggle.into_any_element());
3843        }
3844
3845        let left_btn = thinking_toggle;
3846        let right_btn = self.render_effort_selector(
3847            model.supported_effort_levels(),
3848            thread.thinking_effort().cloned(),
3849            cx,
3850        );
3851
3852        Some(
3853            SplitButton::new(left_btn, right_btn.into_any_element())
3854                .style(SplitButtonStyle::Transparent)
3855                .into_any_element(),
3856        )
3857    }
3858
3859    fn render_effort_selector(
3860        &self,
3861        supported_effort_levels: Vec<LanguageModelEffortLevel>,
3862        selected_effort: Option<String>,
3863        cx: &Context<Self>,
3864    ) -> impl IntoElement {
3865        let weak_self = cx.weak_entity();
3866
3867        let default_effort_level = supported_effort_levels
3868            .iter()
3869            .find(|effort_level| effort_level.is_default)
3870            .cloned();
3871
3872        let selected = selected_effort.and_then(|effort| {
3873            supported_effort_levels
3874                .iter()
3875                .find(|level| level.value == effort)
3876                .cloned()
3877        });
3878
3879        let label = selected
3880            .clone()
3881            .or(default_effort_level)
3882            .map_or("Select Effort".into(), |effort| effort.name);
3883
3884        let (label_color, icon) = if self.thinking_effort_menu_handle.is_deployed() {
3885            (Color::Accent, IconName::ChevronUp)
3886        } else {
3887            (Color::Muted, IconName::ChevronDown)
3888        };
3889
3890        let focus_handle = self.message_editor.focus_handle(cx);
3891        let show_cycle_row = supported_effort_levels.len() > 1;
3892
3893        let tooltip = Tooltip::element({
3894            move |_, cx| {
3895                let mut content = v_flex().gap_1().child(
3896                    h_flex()
3897                        .gap_2()
3898                        .justify_between()
3899                        .child(Label::new("Change Thinking Effort"))
3900                        .child(KeyBinding::for_action_in(
3901                            &ToggleThinkingEffortMenu,
3902                            &focus_handle,
3903                            cx,
3904                        )),
3905                );
3906
3907                if show_cycle_row {
3908                    content = content.child(
3909                        h_flex()
3910                            .pt_1()
3911                            .gap_2()
3912                            .justify_between()
3913                            .border_t_1()
3914                            .border_color(cx.theme().colors().border_variant)
3915                            .child(Label::new("Cycle Thinking Effort"))
3916                            .child(KeyBinding::for_action_in(
3917                                &CycleThinkingEffort,
3918                                &focus_handle,
3919                                cx,
3920                            )),
3921                    );
3922                }
3923
3924                content.into_any_element()
3925            }
3926        });
3927
3928        PopoverMenu::new("effort-selector")
3929            .trigger_with_tooltip(
3930                ButtonLike::new_rounded_right("effort-selector-trigger")
3931                    .selected_style(ButtonStyle::Tinted(TintColor::Accent))
3932                    .child(Label::new(label).size(LabelSize::Small).color(label_color))
3933                    .child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted)),
3934                tooltip,
3935            )
3936            .menu(move |window, cx| {
3937                Some(ContextMenu::build(window, cx, |mut menu, _window, _cx| {
3938                    menu = menu.header("Change Thinking Effort");
3939
3940                    for effort_level in supported_effort_levels.clone() {
3941                        let is_selected = selected
3942                            .as_ref()
3943                            .is_some_and(|selected| selected.value == effort_level.value);
3944                        let entry = ContextMenuEntry::new(effort_level.name)
3945                            .toggleable(IconPosition::End, is_selected);
3946
3947                        menu.push_item(entry.handler({
3948                            let effort = effort_level.value.clone();
3949                            let weak_self = weak_self.clone();
3950                            move |_window, cx| {
3951                                let effort = effort.clone();
3952                                weak_self
3953                                    .update(cx, |this, cx| {
3954                                        if let Some(thread) = this.as_native_thread(cx) {
3955                                            thread.update(cx, |thread, cx| {
3956                                                thread.set_thinking_effort(
3957                                                    Some(effort.to_string()),
3958                                                    cx,
3959                                                );
3960
3961                                                let fs = thread.project().read(cx).fs().clone();
3962                                                update_settings_file(fs, cx, move |settings, _| {
3963                                                    if let Some(agent) = settings.agent.as_mut()
3964                                                        && let Some(default_model) =
3965                                                            agent.default_model.as_mut()
3966                                                    {
3967                                                        default_model.effort =
3968                                                            Some(effort.to_string());
3969                                                    }
3970                                                });
3971                                            });
3972                                        }
3973                                    })
3974                                    .ok();
3975                            }
3976                        }));
3977                    }
3978
3979                    menu
3980                }))
3981            })
3982            .with_handle(self.thinking_effort_menu_handle.clone())
3983            .offset(gpui::Point {
3984                x: px(0.0),
3985                y: px(-2.0),
3986            })
3987            .anchor(Corner::BottomLeft)
3988    }
3989
3990    fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
3991        let message_editor = self.message_editor.read(cx);
3992        let is_editor_empty = message_editor.is_empty(cx);
3993        let focus_handle = message_editor.focus_handle(cx);
3994
3995        let is_generating = self.thread.read(cx).status() != ThreadStatus::Idle;
3996
3997        if self.is_loading_contents {
3998            div()
3999                .id("loading-message-content")
4000                .px_1()
4001                .tooltip(Tooltip::text("Loading Added Context…"))
4002                .child(loading_contents_spinner(IconSize::default()))
4003                .into_any_element()
4004        } else if is_generating && is_editor_empty {
4005            IconButton::new("stop-generation", IconName::Stop)
4006                .icon_color(Color::Error)
4007                .style(ButtonStyle::Tinted(TintColor::Error))
4008                .tooltip(move |_window, cx| {
4009                    Tooltip::for_action("Stop Generation", &editor::actions::Cancel, cx)
4010                })
4011                .on_click(cx.listener(|this, _event, _, cx| this.cancel_generation(cx)))
4012                .into_any_element()
4013        } else {
4014            let send_icon = if is_generating {
4015                IconName::QueueMessage
4016            } else {
4017                IconName::Send
4018            };
4019            IconButton::new("send-message", send_icon)
4020                .style(ButtonStyle::Filled)
4021                .map(|this| {
4022                    if is_editor_empty && !is_generating {
4023                        this.disabled(true).icon_color(Color::Muted)
4024                    } else {
4025                        this.icon_color(Color::Accent)
4026                    }
4027                })
4028                .tooltip(move |_window, cx| {
4029                    if is_editor_empty && !is_generating {
4030                        Tooltip::for_action("Type to Send", &Chat, cx)
4031                    } else if is_generating {
4032                        let focus_handle = focus_handle.clone();
4033
4034                        Tooltip::element(move |_window, cx| {
4035                            v_flex()
4036                                .gap_1()
4037                                .child(
4038                                    h_flex()
4039                                        .gap_2()
4040                                        .justify_between()
4041                                        .child(Label::new("Queue and Send"))
4042                                        .child(KeyBinding::for_action_in(&Chat, &focus_handle, cx)),
4043                                )
4044                                .child(
4045                                    h_flex()
4046                                        .pt_1()
4047                                        .gap_2()
4048                                        .justify_between()
4049                                        .border_t_1()
4050                                        .border_color(cx.theme().colors().border_variant)
4051                                        .child(Label::new("Send Immediately"))
4052                                        .child(KeyBinding::for_action_in(
4053                                            &SendImmediately,
4054                                            &focus_handle,
4055                                            cx,
4056                                        )),
4057                                )
4058                                .into_any_element()
4059                        })(_window, cx)
4060                    } else {
4061                        Tooltip::for_action("Send Message", &Chat, cx)
4062                    }
4063                })
4064                .on_click(cx.listener(|this, _, window, cx| {
4065                    this.send(window, cx);
4066                }))
4067                .into_any_element()
4068        }
4069    }
4070
4071    fn render_add_context_button(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
4072        let focus_handle = self.message_editor.focus_handle(cx);
4073        let weak_self = cx.weak_entity();
4074
4075        PopoverMenu::new("add-context-menu")
4076            .trigger_with_tooltip(
4077                IconButton::new("add-context", IconName::Plus)
4078                    .icon_size(IconSize::Small)
4079                    .icon_color(Color::Muted),
4080                {
4081                    move |_window, cx| {
4082                        Tooltip::for_action_in(
4083                            "Add Context",
4084                            &OpenAddContextMenu,
4085                            &focus_handle,
4086                            cx,
4087                        )
4088                    }
4089                },
4090            )
4091            .anchor(Corner::BottomLeft)
4092            .with_handle(self.add_context_menu_handle.clone())
4093            .offset(gpui::Point {
4094                x: px(0.0),
4095                y: px(-2.0),
4096            })
4097            .menu(move |window, cx| {
4098                weak_self
4099                    .update(cx, |this, cx| this.build_add_context_menu(window, cx))
4100                    .ok()
4101            })
4102    }
4103
4104    fn build_add_context_menu(
4105        &self,
4106        window: &mut Window,
4107        cx: &mut Context<Self>,
4108    ) -> Entity<ContextMenu> {
4109        let message_editor = self.message_editor.clone();
4110        let workspace = self.workspace.clone();
4111        let session_capabilities = self.session_capabilities.read();
4112        let supports_images = session_capabilities.supports_images();
4113        let supports_embedded_context = session_capabilities.supports_embedded_context();
4114
4115        let has_editor_selection = workspace
4116            .upgrade()
4117            .and_then(|ws| {
4118                ws.read(cx)
4119                    .active_item(cx)
4120                    .and_then(|item| item.downcast::<Editor>())
4121            })
4122            .is_some_and(|editor| {
4123                editor.update(cx, |editor, cx| {
4124                    editor.has_non_empty_selection(&editor.display_snapshot(cx))
4125                })
4126            });
4127
4128        let has_terminal_selection = workspace
4129            .upgrade()
4130            .and_then(|ws| ws.read(cx).panel::<TerminalPanel>(cx))
4131            .is_some_and(|panel| !panel.read(cx).terminal_selections(cx).is_empty());
4132
4133        let has_selection = has_editor_selection || has_terminal_selection;
4134
4135        ContextMenu::build(window, cx, move |menu, _window, _cx| {
4136            menu.key_context("AddContextMenu")
4137                .header("Context")
4138                .item(
4139                    ContextMenuEntry::new("Files & Directories")
4140                        .icon(IconName::File)
4141                        .icon_color(Color::Muted)
4142                        .icon_size(IconSize::XSmall)
4143                        .handler({
4144                            let message_editor = message_editor.clone();
4145                            move |window, cx| {
4146                                message_editor.focus_handle(cx).focus(window, cx);
4147                                message_editor.update(cx, |editor, cx| {
4148                                    editor.insert_context_type("file", window, cx);
4149                                });
4150                            }
4151                        }),
4152                )
4153                .item(
4154                    ContextMenuEntry::new("Symbols")
4155                        .icon(IconName::Code)
4156                        .icon_color(Color::Muted)
4157                        .icon_size(IconSize::XSmall)
4158                        .handler({
4159                            let message_editor = message_editor.clone();
4160                            move |window, cx| {
4161                                message_editor.focus_handle(cx).focus(window, cx);
4162                                message_editor.update(cx, |editor, cx| {
4163                                    editor.insert_context_type("symbol", window, cx);
4164                                });
4165                            }
4166                        }),
4167                )
4168                .item(
4169                    ContextMenuEntry::new("Threads")
4170                        .icon(IconName::Thread)
4171                        .icon_color(Color::Muted)
4172                        .icon_size(IconSize::XSmall)
4173                        .handler({
4174                            let message_editor = message_editor.clone();
4175                            move |window, cx| {
4176                                message_editor.focus_handle(cx).focus(window, cx);
4177                                message_editor.update(cx, |editor, cx| {
4178                                    editor.insert_context_type("thread", window, cx);
4179                                });
4180                            }
4181                        }),
4182                )
4183                .item(
4184                    ContextMenuEntry::new("Rules")
4185                        .icon(IconName::Reader)
4186                        .icon_color(Color::Muted)
4187                        .icon_size(IconSize::XSmall)
4188                        .handler({
4189                            let message_editor = message_editor.clone();
4190                            move |window, cx| {
4191                                message_editor.focus_handle(cx).focus(window, cx);
4192                                message_editor.update(cx, |editor, cx| {
4193                                    editor.insert_context_type("rule", window, cx);
4194                                });
4195                            }
4196                        }),
4197                )
4198                .item(
4199                    ContextMenuEntry::new("Image")
4200                        .icon(IconName::Image)
4201                        .icon_color(Color::Muted)
4202                        .icon_size(IconSize::XSmall)
4203                        .disabled(!supports_images)
4204                        .handler({
4205                            let message_editor = message_editor.clone();
4206                            move |window, cx| {
4207                                message_editor.focus_handle(cx).focus(window, cx);
4208                                message_editor.update(cx, |editor, cx| {
4209                                    editor.add_images_from_picker(window, cx);
4210                                });
4211                            }
4212                        }),
4213                )
4214                .item(
4215                    ContextMenuEntry::new("Selection")
4216                        .icon(IconName::CursorIBeam)
4217                        .icon_color(Color::Muted)
4218                        .icon_size(IconSize::XSmall)
4219                        .disabled(!has_selection)
4220                        .handler({
4221                            move |window, cx| {
4222                                window.dispatch_action(
4223                                    zed_actions::agent::AddSelectionToThread.boxed_clone(),
4224                                    cx,
4225                                );
4226                            }
4227                        }),
4228                )
4229                .item(
4230                    ContextMenuEntry::new("Branch Diff")
4231                        .icon(IconName::GitBranch)
4232                        .icon_color(Color::Muted)
4233                        .icon_size(IconSize::XSmall)
4234                        .disabled(!supports_embedded_context)
4235                        .handler({
4236                            move |window, cx| {
4237                                message_editor.update(cx, |editor, cx| {
4238                                    editor.insert_branch_diff_crease(window, cx);
4239                                });
4240                            }
4241                        }),
4242                )
4243        })
4244    }
4245
4246    fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
4247        let following = self.is_following(cx);
4248
4249        let tooltip_label = if following {
4250            if self.agent_id.as_ref() == agent::ZED_AGENT_ID.as_ref() {
4251                format!("Stop Following the {}", self.agent_id)
4252            } else {
4253                format!("Stop Following {}", self.agent_id)
4254            }
4255        } else {
4256            if self.agent_id.as_ref() == agent::ZED_AGENT_ID.as_ref() {
4257                format!("Follow the {}", self.agent_id)
4258            } else {
4259                format!("Follow {}", self.agent_id)
4260            }
4261        };
4262
4263        IconButton::new("follow-agent", IconName::Crosshair)
4264            .icon_size(IconSize::Small)
4265            .icon_color(Color::Muted)
4266            .toggle_state(following)
4267            .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
4268            .tooltip(move |_window, cx| {
4269                if following {
4270                    Tooltip::for_action(tooltip_label.clone(), &Follow, cx)
4271                } else {
4272                    Tooltip::with_meta(
4273                        tooltip_label.clone(),
4274                        Some(&Follow),
4275                        "Track the agent's location as it reads and edits files.",
4276                        cx,
4277                    )
4278                }
4279            })
4280            .on_click(cx.listener(move |this, _, window, cx| {
4281                this.toggle_following(window, cx);
4282            }))
4283    }
4284}
4285
4286struct TokenUsageTooltip {
4287    percentage: String,
4288    used: String,
4289    max: String,
4290    input_tokens: String,
4291    output_tokens: String,
4292    input_max: String,
4293    output_max: String,
4294    show_split: bool,
4295    cost_label: Option<String>,
4296    separator_color: Color,
4297    user_rules_count: usize,
4298    first_user_rules_id: Option<uuid::Uuid>,
4299    project_rules_count: usize,
4300    project_entry_ids: Vec<ProjectEntryId>,
4301    workspace: WeakEntity<Workspace>,
4302}
4303
4304impl Render for TokenUsageTooltip {
4305    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4306        let separator_color = self.separator_color;
4307        let percentage = self.percentage.clone();
4308        let used = self.used.clone();
4309        let max = self.max.clone();
4310        let input_tokens = self.input_tokens.clone();
4311        let output_tokens = self.output_tokens.clone();
4312        let input_max = self.input_max.clone();
4313        let output_max = self.output_max.clone();
4314        let show_split = self.show_split;
4315        let cost_label = self.cost_label.clone();
4316        let user_rules_count = self.user_rules_count;
4317        let first_user_rules_id = self.first_user_rules_id;
4318        let project_rules_count = self.project_rules_count;
4319        let project_entry_ids = self.project_entry_ids.clone();
4320        let workspace = self.workspace.clone();
4321
4322        ui::tooltip_container(cx, move |container, cx| {
4323            container
4324                .min_w_40()
4325                .child(
4326                    Label::new("Context")
4327                        .color(Color::Muted)
4328                        .size(LabelSize::Small),
4329                )
4330                .when(!show_split, |this| {
4331                    this.child(
4332                        h_flex()
4333                            .gap_0p5()
4334                            .child(Label::new(percentage.clone()))
4335                            .child(Label::new("\u{2022}").color(separator_color).mx_1())
4336                            .child(Label::new(used.clone()))
4337                            .child(Label::new("/").color(separator_color))
4338                            .child(Label::new(max.clone()).color(Color::Muted)),
4339                    )
4340                })
4341                .when(show_split, |this| {
4342                    this.child(
4343                        v_flex()
4344                            .gap_0p5()
4345                            .child(
4346                                h_flex()
4347                                    .gap_0p5()
4348                                    .child(Label::new("Input:").color(Color::Muted).mr_0p5())
4349                                    .child(Label::new(input_tokens))
4350                                    .child(Label::new("/").color(separator_color))
4351                                    .child(Label::new(input_max).color(Color::Muted)),
4352                            )
4353                            .child(
4354                                h_flex()
4355                                    .gap_0p5()
4356                                    .child(Label::new("Output:").color(Color::Muted).mr_0p5())
4357                                    .child(Label::new(output_tokens))
4358                                    .child(Label::new("/").color(separator_color))
4359                                    .child(Label::new(output_max).color(Color::Muted)),
4360                            ),
4361                    )
4362                })
4363                .when_some(cost_label, |this, cost_label| {
4364                    this.child(
4365                        v_flex()
4366                            .mt_1p5()
4367                            .pt_1p5()
4368                            .gap_0p5()
4369                            .border_t_1()
4370                            .border_color(cx.theme().colors().border_variant)
4371                            .child(
4372                                Label::new("Cost")
4373                                    .color(Color::Muted)
4374                                    .size(LabelSize::Small),
4375                            )
4376                            .child(Label::new(cost_label)),
4377                    )
4378                })
4379                .when(
4380                    user_rules_count > 0 || project_rules_count > 0,
4381                    move |this| {
4382                        this.child(
4383                            v_flex()
4384                                .mt_1p5()
4385                                .pt_1p5()
4386                                .pb_0p5()
4387                                .gap_0p5()
4388                                .border_t_1()
4389                                .border_color(cx.theme().colors().border_variant)
4390                                .child(
4391                                    Label::new("Rules")
4392                                        .color(Color::Muted)
4393                                        .size(LabelSize::Small),
4394                                )
4395                                .child(
4396                                    v_flex()
4397                                        .mx_neg_1()
4398                                        .when(user_rules_count > 0, move |this| {
4399                                            this.child(
4400                                                Button::new(
4401                                                    "open-user-rules",
4402                                                    format!("{} user rules", user_rules_count),
4403                                                )
4404                                                .end_icon(
4405                                                    Icon::new(IconName::ArrowUpRight)
4406                                                        .color(Color::Muted)
4407                                                        .size(IconSize::XSmall),
4408                                                )
4409                                                .on_click(move |_, window, cx| {
4410                                                    window.dispatch_action(
4411                                                        Box::new(OpenRulesLibrary {
4412                                                            prompt_to_select: first_user_rules_id,
4413                                                        }),
4414                                                        cx,
4415                                                    );
4416                                                }),
4417                                            )
4418                                        })
4419                                        .when(project_rules_count > 0, move |this| {
4420                                            let workspace = workspace.clone();
4421                                            let project_entry_ids = project_entry_ids.clone();
4422                                            this.child(
4423                                                Button::new(
4424                                                    "open-project-rules",
4425                                                    format!(
4426                                                        "{} project rules",
4427                                                        project_rules_count
4428                                                    ),
4429                                                )
4430                                                .end_icon(
4431                                                    Icon::new(IconName::ArrowUpRight)
4432                                                        .color(Color::Muted)
4433                                                        .size(IconSize::XSmall),
4434                                                )
4435                                                .on_click(move |_, window, cx| {
4436                                                    let _ =
4437                                                        workspace.update(cx, |workspace, cx| {
4438                                                            let project =
4439                                                                workspace.project().read(cx);
4440                                                            let paths = project_entry_ids
4441                                                                .iter()
4442                                                                .flat_map(|id| {
4443                                                                    project.path_for_entry(*id, cx)
4444                                                                })
4445                                                                .collect::<Vec<_>>();
4446                                                            for path in paths {
4447                                                                workspace
4448                                                                    .open_path(
4449                                                                        path, None, true, window,
4450                                                                        cx,
4451                                                                    )
4452                                                                    .detach_and_log_err(cx);
4453                                                            }
4454                                                        });
4455                                                }),
4456                                            )
4457                                        }),
4458                                ),
4459                        )
4460                    },
4461                )
4462        })
4463    }
4464}
4465
4466impl ThreadView {
4467    fn render_entries(&mut self, cx: &mut Context<Self>) -> List {
4468        let max_content_width = AgentSettings::get_global(cx).max_content_width;
4469        let centered_container = move |content: AnyElement| {
4470            h_flex()
4471                .w_full()
4472                .justify_center()
4473                .child(div().max_w(max_content_width).w_full().child(content))
4474        };
4475
4476        list(
4477            self.list_state.clone(),
4478            cx.processor(move |this, index: usize, window, cx| {
4479                let entries = this.thread.read(cx).entries();
4480                if let Some(entry) = entries.get(index) {
4481                    let rendered = this.render_entry(index, entries.len(), entry, window, cx);
4482                    centered_container(rendered.into_any_element()).into_any_element()
4483                } else if this.generating_indicator_in_list {
4484                    let confirmation = entries
4485                        .last()
4486                        .is_some_and(|entry| Self::is_waiting_for_confirmation(entry));
4487                    let rendered = this.render_generating(confirmation, cx);
4488                    centered_container(rendered.into_any_element()).into_any_element()
4489                } else {
4490                    Empty.into_any()
4491                }
4492            }),
4493        )
4494        .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
4495        .flex_grow()
4496    }
4497
4498    fn render_entry(
4499        &self,
4500        entry_ix: usize,
4501        total_entries: usize,
4502        entry: &AgentThreadEntry,
4503        window: &Window,
4504        cx: &Context<Self>,
4505    ) -> AnyElement {
4506        let is_indented = entry.is_indented();
4507        let is_first_indented = is_indented
4508            && self
4509                .thread
4510                .read(cx)
4511                .entries()
4512                .get(entry_ix.saturating_sub(1))
4513                .is_none_or(|entry| !entry.is_indented());
4514
4515        let primary = match &entry {
4516            AgentThreadEntry::UserMessage(message) => {
4517                let Some(editor) = self
4518                    .entry_view_state
4519                    .read(cx)
4520                    .entry(entry_ix)
4521                    .and_then(|entry| entry.message_editor())
4522                    .cloned()
4523                else {
4524                    return Empty.into_any_element();
4525                };
4526
4527                let editing = self.editing_message == Some(entry_ix);
4528                let editor_focus = editor.focus_handle(cx).is_focused(window);
4529                let focus_border = cx.theme().colors().border_focused;
4530
4531                let has_checkpoint_button = message
4532                    .checkpoint
4533                    .as_ref()
4534                    .is_some_and(|checkpoint| checkpoint.show);
4535
4536                let is_subagent = self.is_subagent();
4537                let can_rewind = self.thread.read(cx).supports_truncate(cx);
4538                let is_editable = can_rewind && message.id.is_some() && !is_subagent;
4539                let agent_name = if is_subagent {
4540                    "subagents".into()
4541                } else {
4542                    self.agent_id.clone()
4543                };
4544
4545                v_flex()
4546                    .id(("user_message", entry_ix))
4547                    .map(|this| {
4548                        if is_first_indented {
4549                            this.pt_0p5()
4550                        } else {
4551                            this.pt_2()
4552                        }
4553                    })
4554                    .pb_3()
4555                    .px_2()
4556                    .gap_1p5()
4557                    .w_full()
4558                    .when(is_editable && has_checkpoint_button, |this| {
4559                        this.children(message.id.clone().map(|message_id| {
4560                            h_flex()
4561                                .px_3()
4562                                .gap_2()
4563                                .child(Divider::horizontal())
4564                                .child(
4565                                    Button::new("restore-checkpoint", "Restore Checkpoint")
4566                                        .start_icon(Icon::new(IconName::Undo).size(IconSize::XSmall).color(Color::Muted))
4567                                        .label_size(LabelSize::XSmall)
4568                                        .color(Color::Muted)
4569                                        .tooltip(Tooltip::text("Restores all files in the project to the content they had at this point in the conversation."))
4570                                        .on_click(cx.listener(move |this, _, _window, cx| {
4571                                            this.restore_checkpoint(&message_id, cx);
4572                                        }))
4573                                )
4574                                .child(Divider::horizontal())
4575                        }))
4576                    })
4577                    .child(
4578                        div()
4579                            .relative()
4580                            .child(
4581                                div()
4582                                    .py_3()
4583                                    .px_2()
4584                                    .rounded_md()
4585                                    .bg(cx.theme().colors().editor_background)
4586                                    .border_1()
4587                                    .when(is_indented, |this| {
4588                                        this.py_2().px_2().shadow_sm()
4589                                    })
4590                                    .border_color(cx.theme().colors().border)
4591                                    .map(|this| {
4592                                        if !is_editable {
4593                                            if is_subagent {
4594                                                return this.border_dashed();
4595                                            }
4596                                            return this;
4597                                        }
4598                                        if editing && editor_focus {
4599                                            return this.border_color(focus_border);
4600                                        }
4601                                        if editing && !editor_focus {
4602                                            return this.border_dashed()
4603                                        }
4604                                        this.shadow_md().hover(|s| {
4605                                            s.border_color(focus_border.opacity(0.8))
4606                                        })
4607                                    })
4608                                    .text_xs()
4609                                    .child(editor.clone().into_any_element())
4610                            )
4611                            .when(editor_focus, |this| {
4612                                let base_container = h_flex()
4613                                    .absolute()
4614                                    .top_neg_3p5()
4615                                    .right_3()
4616                                    .gap_1()
4617                                    .rounded_sm()
4618                                    .border_1()
4619                                    .border_color(cx.theme().colors().border)
4620                                    .bg(cx.theme().colors().editor_background)
4621                                    .overflow_hidden();
4622
4623                                let is_loading_contents = self.is_loading_contents;
4624                                if is_editable {
4625                                    this.child(
4626                                        base_container
4627                                            .child(
4628                                                IconButton::new("cancel", IconName::Close)
4629                                                    .disabled(is_loading_contents)
4630                                                    .icon_color(Color::Error)
4631                                                    .icon_size(IconSize::XSmall)
4632                                                    .on_click(cx.listener(Self::cancel_editing))
4633                                            )
4634                                            .child(
4635                                                if is_loading_contents {
4636                                                    div()
4637                                                        .id("loading-edited-message-content")
4638                                                        .tooltip(Tooltip::text("Loading Added Context…"))
4639                                                        .child(loading_contents_spinner(IconSize::XSmall))
4640                                                        .into_any_element()
4641                                                } else {
4642                                                    IconButton::new("regenerate", IconName::Return)
4643                                                        .icon_color(Color::Muted)
4644                                                        .icon_size(IconSize::XSmall)
4645                                                        .tooltip(Tooltip::text(
4646                                                            "Editing will restart the thread from this point."
4647                                                        ))
4648                                                        .on_click(cx.listener({
4649                                                            let editor = editor.clone();
4650                                                            move |this, _, window, cx| {
4651                                                                this.regenerate(
4652                                                                    entry_ix, editor.clone(), window, cx,
4653                                                                );
4654                                                            }
4655                                                        })).into_any_element()
4656                                                }
4657                                            )
4658                                    )
4659                                } else {
4660                                    this.child(
4661                                        base_container
4662                                            .border_dashed()
4663                                            .child(IconButton::new("non_editable", IconName::PencilUnavailable)
4664                                                .icon_size(IconSize::Small)
4665                                                .icon_color(Color::Muted)
4666                                                .style(ButtonStyle::Transparent)
4667                                                .tooltip(Tooltip::element({
4668                                                    let agent_name = agent_name.clone();
4669                                                    move |_, _| {
4670                                                        v_flex()
4671                                                            .gap_1()
4672                                                            .child(Label::new("Unavailable Editing"))
4673                                                            .child(
4674                                                                div().max_w_64().child(
4675                                                                    Label::new(format!(
4676                                                                        "Editing previous messages is not available for {} yet.",
4677                                                                        agent_name
4678                                                                    ))
4679                                                                    .size(LabelSize::Small)
4680                                                                    .color(Color::Muted),
4681                                                                ),
4682                                                            )
4683                                                            .into_any_element()
4684                                                    }
4685                                                }))),
4686                                    )
4687                                }
4688                            }),
4689                    )
4690                    .into_any()
4691            }
4692            AgentThreadEntry::AssistantMessage(AssistantMessage {
4693                chunks,
4694                indented: _,
4695                is_subagent_output: _,
4696            }) => {
4697                let mut is_blank = true;
4698                let is_last = entry_ix + 1 == total_entries;
4699
4700                let style = MarkdownStyle::themed(MarkdownFont::Agent, window, cx);
4701                let message_body = v_flex()
4702                    .w_full()
4703                    .gap_3()
4704                    .children(chunks.iter().enumerate().filter_map(
4705                        |(chunk_ix, chunk)| match chunk {
4706                            AssistantMessageChunk::Message { block } => {
4707                                block.markdown().and_then(|md| {
4708                                    let this_is_blank = md.read(cx).source().trim().is_empty();
4709                                    is_blank = is_blank && this_is_blank;
4710                                    if this_is_blank {
4711                                        return None;
4712                                    }
4713
4714                                    Some(
4715                                        self.render_markdown(md.clone(), style.clone())
4716                                            .into_any_element(),
4717                                    )
4718                                })
4719                            }
4720                            AssistantMessageChunk::Thought { block } => {
4721                                block.markdown().and_then(|md| {
4722                                    let this_is_blank = md.read(cx).source().trim().is_empty();
4723                                    is_blank = is_blank && this_is_blank;
4724                                    if this_is_blank {
4725                                        return None;
4726                                    }
4727                                    Some(
4728                                        self.render_thinking_block(
4729                                            entry_ix,
4730                                            chunk_ix,
4731                                            md.clone(),
4732                                            window,
4733                                            cx,
4734                                        )
4735                                        .into_any_element(),
4736                                    )
4737                                })
4738                            }
4739                        },
4740                    ))
4741                    .into_any();
4742
4743                if is_blank {
4744                    Empty.into_any()
4745                } else {
4746                    v_flex()
4747                        .px_5()
4748                        .py_1p5()
4749                        .when(is_last, |this| this.pb_4())
4750                        .w_full()
4751                        .text_ui(cx)
4752                        .child(self.render_message_context_menu(entry_ix, message_body, cx))
4753                        .when_some(
4754                            self.entry_view_state
4755                                .read(cx)
4756                                .entry(entry_ix)
4757                                .and_then(|entry| entry.focus_handle(cx)),
4758                            |this, handle| this.track_focus(&handle),
4759                        )
4760                        .into_any()
4761                }
4762            }
4763            AgentThreadEntry::ToolCall(tool_call) => self
4764                .render_any_tool_call(
4765                    self.thread.read(cx).session_id(),
4766                    entry_ix,
4767                    tool_call,
4768                    &self.focus_handle(cx),
4769                    false,
4770                    window,
4771                    cx,
4772                )
4773                .into_any(),
4774            AgentThreadEntry::CompletedPlan(entries) => {
4775                self.render_completed_plan(entries, window, cx)
4776            }
4777        };
4778
4779        let is_subagent_output = self.is_subagent()
4780            && matches!(entry, AgentThreadEntry::AssistantMessage(msg) if msg.is_subagent_output);
4781
4782        let primary = if is_subagent_output {
4783            v_flex()
4784                .w_full()
4785                .child(
4786                    h_flex()
4787                        .id("subagent_output")
4788                        .px_5()
4789                        .py_1()
4790                        .gap_2()
4791                        .child(Divider::horizontal())
4792                        .child(
4793                            h_flex()
4794                                .gap_1()
4795                                .child(
4796                                    Icon::new(IconName::ForwardArrowUp)
4797                                        .color(Color::Muted)
4798                                        .size(IconSize::Small),
4799                                )
4800                                .child(
4801                                    Label::new("Subagent Output")
4802                                        .size(LabelSize::Custom(self.tool_name_font_size()))
4803                                        .color(Color::Muted),
4804                                ),
4805                        )
4806                        .child(Divider::horizontal())
4807                        .tooltip(Tooltip::text("Everything below this line was sent as output from this subagent to the main agent.")),
4808                )
4809                .child(primary)
4810                .into_any_element()
4811        } else {
4812            primary
4813        };
4814
4815        let thread = self.thread.clone();
4816
4817        let primary = if is_indented {
4818            let line_top = if is_first_indented {
4819                rems_from_px(-12.0)
4820            } else {
4821                rems_from_px(0.0)
4822            };
4823
4824            div()
4825                .relative()
4826                .w_full()
4827                .pl_5()
4828                .bg(cx.theme().colors().panel_background.opacity(0.2))
4829                .child(
4830                    div()
4831                        .absolute()
4832                        .left(rems_from_px(18.0))
4833                        .top(line_top)
4834                        .bottom_0()
4835                        .w_px()
4836                        .bg(cx.theme().colors().border.opacity(0.6)),
4837                )
4838                .child(primary)
4839                .into_any_element()
4840        } else {
4841            primary
4842        };
4843
4844        let needs_confirmation = Self::is_waiting_for_confirmation(entry);
4845
4846        let comments_editor = self.thread_feedback.comments_editor.clone();
4847
4848        let primary = if entry_ix + 1 == total_entries {
4849            v_flex()
4850                .w_full()
4851                .child(primary)
4852                .when(!needs_confirmation, |this| {
4853                    this.child(self.render_thread_controls(&thread, cx))
4854                })
4855                .when_some(comments_editor, |this, editor| {
4856                    this.child(Self::render_feedback_feedback_editor(editor, cx))
4857                })
4858                .into_any_element()
4859        } else {
4860            primary
4861        };
4862
4863        if let Some(editing_index) = self.editing_message
4864            && editing_index < entry_ix
4865        {
4866            let is_subagent = self.is_subagent();
4867
4868            let backdrop = div()
4869                .id(("backdrop", entry_ix))
4870                .size_full()
4871                .absolute()
4872                .inset_0()
4873                .bg(cx.theme().colors().panel_background)
4874                .opacity(0.8)
4875                .block_mouse_except_scroll()
4876                .on_click(cx.listener(Self::cancel_editing));
4877
4878            div()
4879                .relative()
4880                .child(primary)
4881                .when(!is_subagent, |this| this.child(backdrop))
4882                .into_any_element()
4883        } else {
4884            primary
4885        }
4886    }
4887
4888    fn render_feedback_feedback_editor(editor: Entity<Editor>, cx: &Context<Self>) -> Div {
4889        h_flex()
4890            .key_context("AgentFeedbackMessageEditor")
4891            .on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| {
4892                this.thread_feedback.dismiss_comments();
4893                cx.notify();
4894            }))
4895            .on_action(cx.listener(move |this, _: &menu::Confirm, _window, cx| {
4896                this.submit_feedback_message(cx);
4897            }))
4898            .p_2()
4899            .mb_2()
4900            .mx_5()
4901            .gap_1()
4902            .rounded_md()
4903            .border_1()
4904            .border_color(cx.theme().colors().border)
4905            .bg(cx.theme().colors().editor_background)
4906            .child(div().w_full().child(editor))
4907            .child(
4908                h_flex()
4909                    .child(
4910                        IconButton::new("dismiss-feedback-message", IconName::Close)
4911                            .icon_color(Color::Error)
4912                            .icon_size(IconSize::XSmall)
4913                            .shape(ui::IconButtonShape::Square)
4914                            .on_click(cx.listener(move |this, _, _window, cx| {
4915                                this.thread_feedback.dismiss_comments();
4916                                cx.notify();
4917                            })),
4918                    )
4919                    .child(
4920                        IconButton::new("submit-feedback-message", IconName::Return)
4921                            .icon_size(IconSize::XSmall)
4922                            .shape(ui::IconButtonShape::Square)
4923                            .on_click(cx.listener(move |this, _, _window, cx| {
4924                                this.submit_feedback_message(cx);
4925                            })),
4926                    ),
4927            )
4928    }
4929
4930    fn render_thread_controls(
4931        &self,
4932        thread: &Entity<AcpThread>,
4933        cx: &Context<Self>,
4934    ) -> impl IntoElement {
4935        let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
4936        if is_generating {
4937            return Empty.into_any_element();
4938        }
4939
4940        let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
4941            .shape(ui::IconButtonShape::Square)
4942            .icon_size(IconSize::Small)
4943            .icon_color(Color::Ignored)
4944            .tooltip(Tooltip::text("Open Thread as Markdown"))
4945            .on_click(cx.listener(move |this, _, window, cx| {
4946                if let Some(workspace) = this.workspace.upgrade() {
4947                    this.open_thread_as_markdown(workspace, window, cx)
4948                        .detach_and_log_err(cx);
4949                }
4950            }));
4951
4952        let scroll_to_recent_user_prompt =
4953            IconButton::new("scroll_to_recent_user_prompt", IconName::ForwardArrow)
4954                .shape(ui::IconButtonShape::Square)
4955                .icon_size(IconSize::Small)
4956                .icon_color(Color::Ignored)
4957                .tooltip(Tooltip::text("Scroll To Most Recent User Prompt"))
4958                .on_click(cx.listener(move |this, _, _, cx| {
4959                    this.scroll_to_most_recent_user_prompt(cx);
4960                }));
4961
4962        let scroll_to_top = IconButton::new("scroll_to_top", IconName::ArrowUp)
4963            .shape(ui::IconButtonShape::Square)
4964            .icon_size(IconSize::Small)
4965            .icon_color(Color::Ignored)
4966            .tooltip(Tooltip::text("Scroll To Top"))
4967            .on_click(cx.listener(move |this, _, _, cx| {
4968                this.scroll_to_top(cx);
4969            }));
4970
4971        let show_stats = AgentSettings::get_global(cx).show_turn_stats;
4972        let last_turn_clock = show_stats
4973            .then(|| {
4974                self.turn_fields
4975                    .last_turn_duration
4976                    .filter(|&duration| duration > STOPWATCH_THRESHOLD)
4977                    .map(|duration| {
4978                        Label::new(duration_alt_display(duration))
4979                            .size(LabelSize::Small)
4980                            .color(Color::Muted)
4981                    })
4982            })
4983            .flatten();
4984
4985        let last_turn_tokens_label = last_turn_clock
4986            .is_some()
4987            .then(|| {
4988                self.turn_fields
4989                    .last_turn_tokens
4990                    .filter(|&tokens| tokens > TOKEN_THRESHOLD)
4991                    .map(|tokens| {
4992                        Label::new(format!("{} tokens", crate::humanize_token_count(tokens)))
4993                            .size(LabelSize::Small)
4994                            .color(Color::Muted)
4995                    })
4996            })
4997            .flatten();
4998
4999        let mut container = h_flex()
5000            .w_full()
5001            .py_2()
5002            .px_5()
5003            .gap_px()
5004            .opacity(0.6)
5005            .hover(|s| s.opacity(1.))
5006            .justify_end()
5007            .when(
5008                last_turn_tokens_label.is_some() || last_turn_clock.is_some(),
5009                |this| {
5010                    this.child(
5011                        h_flex()
5012                            .gap_1()
5013                            .px_1()
5014                            .when_some(last_turn_tokens_label, |this, label| this.child(label))
5015                            .when_some(last_turn_clock, |this, label| this.child(label)),
5016                    )
5017                },
5018            );
5019
5020        let enable_thread_feedback = util::maybe!({
5021            let project = thread.read(cx).project().read(cx);
5022            let user_store = project.user_store();
5023            if let Some(configuration) = user_store.read(cx).current_organization_configuration() {
5024                if !configuration.is_agent_thread_feedback_enabled {
5025                    return false;
5026                }
5027            }
5028
5029            AgentSettings::get_global(cx).enable_feedback
5030                && self.thread.read(cx).connection().telemetry().is_some()
5031        });
5032
5033        if enable_thread_feedback {
5034            let feedback = self.thread_feedback.feedback;
5035
5036            let tooltip_meta = || {
5037                SharedString::new(
5038                    "Rating the thread sends all of your current conversation to the Zed team.",
5039                )
5040            };
5041
5042            container = container
5043                    .child(
5044                        IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
5045                            .shape(ui::IconButtonShape::Square)
5046                            .icon_size(IconSize::Small)
5047                            .icon_color(match feedback {
5048                                Some(ThreadFeedback::Positive) => Color::Accent,
5049                                _ => Color::Ignored,
5050                            })
5051                            .tooltip(move |window, cx| match feedback {
5052                                Some(ThreadFeedback::Positive) => {
5053                                    Tooltip::text("Thanks for your feedback!")(window, cx)
5054                                }
5055                                _ => {
5056                                    Tooltip::with_meta("Helpful Response", None, tooltip_meta(), cx)
5057                                }
5058                            })
5059                            .on_click(cx.listener(move |this, _, window, cx| {
5060                                this.handle_feedback_click(ThreadFeedback::Positive, window, cx);
5061                            })),
5062                    )
5063                    .child(
5064                        IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
5065                            .shape(ui::IconButtonShape::Square)
5066                            .icon_size(IconSize::Small)
5067                            .icon_color(match feedback {
5068                                Some(ThreadFeedback::Negative) => Color::Accent,
5069                                _ => Color::Ignored,
5070                            })
5071                            .tooltip(move |window, cx| match feedback {
5072                                Some(ThreadFeedback::Negative) => {
5073                                    Tooltip::text(
5074                                    "We appreciate your feedback and will use it to improve in the future.",
5075                                )(window, cx)
5076                                }
5077                                _ => {
5078                                    Tooltip::with_meta(
5079                                        "Not Helpful Response",
5080                                        None,
5081                                        tooltip_meta(),
5082                                        cx,
5083                                    )
5084                                }
5085                            })
5086                            .on_click(cx.listener(move |this, _, window, cx| {
5087                                this.handle_feedback_click(ThreadFeedback::Negative, window, cx);
5088                            })),
5089                    );
5090        }
5091
5092        if let Some(project) = self.project.upgrade()
5093            && let Some(server_view) = self.server_view.upgrade()
5094            && cx.has_flag::<AgentSharingFeatureFlag>()
5095            && project.read(cx).client().status().borrow().is_connected()
5096        {
5097            let button = if self.is_imported_thread(cx) {
5098                IconButton::new("sync-thread", IconName::ArrowCircle)
5099                    .shape(ui::IconButtonShape::Square)
5100                    .icon_size(IconSize::Small)
5101                    .icon_color(Color::Ignored)
5102                    .tooltip(Tooltip::text("Sync with source thread"))
5103                    .on_click(cx.listener(move |this, _, window, cx| {
5104                        this.sync_thread(project.clone(), server_view.clone(), window, cx);
5105                    }))
5106            } else {
5107                IconButton::new("share-thread", IconName::ArrowUpRight)
5108                    .shape(ui::IconButtonShape::Square)
5109                    .icon_size(IconSize::Small)
5110                    .icon_color(Color::Ignored)
5111                    .tooltip(Tooltip::text("Share Thread"))
5112                    .on_click(cx.listener(move |this, _, window, cx| {
5113                        this.share_thread(window, cx);
5114                    }))
5115            };
5116
5117            container = container.child(button);
5118        }
5119
5120        container
5121            .child(open_as_markdown)
5122            .child(scroll_to_recent_user_prompt)
5123            .child(scroll_to_top)
5124            .into_any_element()
5125    }
5126
5127    pub(crate) fn scroll_to_most_recent_user_prompt(&mut self, cx: &mut Context<Self>) {
5128        let entries = self.thread.read(cx).entries();
5129        if entries.is_empty() {
5130            return;
5131        }
5132
5133        // Find the most recent user message and scroll it to the top of the viewport.
5134        // (Fallback: if no user message exists, scroll to the bottom.)
5135        if let Some(ix) = entries
5136            .iter()
5137            .rposition(|entry| matches!(entry, AgentThreadEntry::UserMessage(_)))
5138        {
5139            self.list_state.scroll_to(ListOffset {
5140                item_ix: ix,
5141                offset_in_item: px(0.0),
5142            });
5143            cx.notify();
5144        } else {
5145            self.scroll_to_end(cx);
5146        }
5147    }
5148
5149    pub fn scroll_to_end(&mut self, cx: &mut Context<Self>) {
5150        self.list_state.scroll_to_end();
5151        cx.notify();
5152    }
5153
5154    fn handle_feedback_click(
5155        &mut self,
5156        feedback: ThreadFeedback,
5157        window: &mut Window,
5158        cx: &mut Context<Self>,
5159    ) {
5160        self.thread_feedback
5161            .submit(self.thread.clone(), feedback, window, cx);
5162        cx.notify();
5163    }
5164
5165    fn submit_feedback_message(&mut self, cx: &mut Context<Self>) {
5166        let thread = self.thread.clone();
5167        self.thread_feedback.submit_comments(thread, cx);
5168        cx.notify();
5169    }
5170
5171    pub(crate) fn scroll_to_top(&mut self, cx: &mut Context<Self>) {
5172        self.list_state.scroll_to(ListOffset::default());
5173        cx.notify();
5174    }
5175
5176    fn scroll_output_page_up(
5177        &mut self,
5178        _: &ScrollOutputPageUp,
5179        _window: &mut Window,
5180        cx: &mut Context<Self>,
5181    ) {
5182        let page_height = self.list_state.viewport_bounds().size.height;
5183        self.list_state.scroll_by(-page_height * 0.9);
5184        cx.notify();
5185    }
5186
5187    fn scroll_output_page_down(
5188        &mut self,
5189        _: &ScrollOutputPageDown,
5190        _window: &mut Window,
5191        cx: &mut Context<Self>,
5192    ) {
5193        let page_height = self.list_state.viewport_bounds().size.height;
5194        self.list_state.scroll_by(page_height * 0.9);
5195        cx.notify();
5196    }
5197
5198    fn scroll_output_line_up(
5199        &mut self,
5200        _: &ScrollOutputLineUp,
5201        window: &mut Window,
5202        cx: &mut Context<Self>,
5203    ) {
5204        self.list_state.scroll_by(-window.line_height() * 3.);
5205        cx.notify();
5206    }
5207
5208    fn scroll_output_line_down(
5209        &mut self,
5210        _: &ScrollOutputLineDown,
5211        window: &mut Window,
5212        cx: &mut Context<Self>,
5213    ) {
5214        self.list_state.scroll_by(window.line_height() * 3.);
5215        cx.notify();
5216    }
5217
5218    fn scroll_output_to_top(
5219        &mut self,
5220        _: &ScrollOutputToTop,
5221        _window: &mut Window,
5222        cx: &mut Context<Self>,
5223    ) {
5224        self.scroll_to_top(cx);
5225    }
5226
5227    fn scroll_output_to_bottom(
5228        &mut self,
5229        _: &ScrollOutputToBottom,
5230        _window: &mut Window,
5231        cx: &mut Context<Self>,
5232    ) {
5233        self.scroll_to_end(cx);
5234    }
5235
5236    fn scroll_output_to_previous_message(
5237        &mut self,
5238        _: &ScrollOutputToPreviousMessage,
5239        _window: &mut Window,
5240        cx: &mut Context<Self>,
5241    ) {
5242        let entries = self.thread.read(cx).entries();
5243        let current_ix = self.list_state.logical_scroll_top().item_ix;
5244        if let Some(target_ix) = (0..current_ix)
5245            .rev()
5246            .find(|&i| matches!(entries.get(i), Some(AgentThreadEntry::UserMessage(_))))
5247        {
5248            self.list_state.scroll_to(ListOffset {
5249                item_ix: target_ix,
5250                offset_in_item: px(0.),
5251            });
5252            cx.notify();
5253        }
5254    }
5255
5256    fn scroll_output_to_next_message(
5257        &mut self,
5258        _: &ScrollOutputToNextMessage,
5259        _window: &mut Window,
5260        cx: &mut Context<Self>,
5261    ) {
5262        let entries = self.thread.read(cx).entries();
5263        let current_ix = self.list_state.logical_scroll_top().item_ix;
5264        if let Some(target_ix) = (current_ix + 1..entries.len())
5265            .find(|&i| matches!(entries.get(i), Some(AgentThreadEntry::UserMessage(_))))
5266        {
5267            self.list_state.scroll_to(ListOffset {
5268                item_ix: target_ix,
5269                offset_in_item: px(0.),
5270            });
5271            cx.notify();
5272        }
5273    }
5274
5275    pub fn open_thread_as_markdown(
5276        &self,
5277        workspace: Entity<Workspace>,
5278        window: &mut Window,
5279        cx: &mut App,
5280    ) -> Task<Result<()>> {
5281        let markdown_language_task = workspace
5282            .read(cx)
5283            .app_state()
5284            .languages
5285            .language_for_name("Markdown");
5286
5287        let thread = self.thread.read(cx);
5288        let thread_title = thread
5289            .title()
5290            .unwrap_or_else(|| DEFAULT_THREAD_TITLE.into())
5291            .to_string();
5292        let markdown = thread.to_markdown(cx);
5293
5294        let project = workspace.read(cx).project().clone();
5295        window.spawn(cx, async move |cx| {
5296            let markdown_language = markdown_language_task.await?;
5297
5298            let buffer = project
5299                .update(cx, |project, cx| {
5300                    project.create_buffer(Some(markdown_language), false, cx)
5301                })
5302                .await?;
5303
5304            buffer.update(cx, |buffer, cx| {
5305                buffer.set_text(markdown, cx);
5306                buffer.set_capability(language::Capability::ReadWrite, cx);
5307            });
5308
5309            workspace.update_in(cx, |workspace, window, cx| {
5310                let buffer = cx
5311                    .new(|cx| MultiBuffer::singleton(buffer, cx).with_title(thread_title.clone()));
5312
5313                workspace.add_item_to_active_pane(
5314                    Box::new(cx.new(|cx| {
5315                        let mut editor =
5316                            Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
5317                        editor.set_breadcrumb_header(thread_title);
5318                        editor.disable_mouse_wheel_zoom();
5319                        editor
5320                    })),
5321                    None,
5322                    true,
5323                    window,
5324                    cx,
5325                );
5326            })?;
5327            anyhow::Ok(())
5328        })
5329    }
5330
5331    pub(crate) fn sync_editor_mode_for_empty_state(&mut self, cx: &mut Context<Self>) {
5332        let has_messages = self.list_state.item_count() > 0;
5333        let v2_empty_state = !has_messages;
5334
5335        let mode = if v2_empty_state {
5336            EditorMode::Full {
5337                scale_ui_elements_with_buffer_font_size: false,
5338                show_active_line_background: false,
5339                sizing_behavior: SizingBehavior::Default,
5340            }
5341        } else {
5342            EditorMode::AutoHeight {
5343                min_lines: AgentSettings::get_global(cx).message_editor_min_lines,
5344                max_lines: Some(AgentSettings::get_global(cx).set_message_editor_max_lines()),
5345            }
5346        };
5347        self.message_editor.update(cx, |editor, cx| {
5348            editor.set_mode(mode, cx);
5349        });
5350    }
5351
5352    /// Ensures the list item count includes (or excludes) an extra item for the generating indicator
5353    pub(crate) fn sync_generating_indicator(&mut self, cx: &App) {
5354        let is_generating = matches!(self.thread.read(cx).status(), ThreadStatus::Generating);
5355
5356        if is_generating && !self.generating_indicator_in_list {
5357            let entries_count = self.thread.read(cx).entries().len();
5358            self.list_state.splice(entries_count..entries_count, 1);
5359            self.generating_indicator_in_list = true;
5360        } else if !is_generating && self.generating_indicator_in_list {
5361            let entries_count = self.thread.read(cx).entries().len();
5362            self.list_state.splice(entries_count..entries_count + 1, 0);
5363            self.generating_indicator_in_list = false;
5364        }
5365    }
5366
5367    fn render_generating(&self, confirmation: bool, cx: &App) -> impl IntoElement {
5368        let show_stats = AgentSettings::get_global(cx).show_turn_stats;
5369        let elapsed_label = show_stats
5370            .then(|| {
5371                self.turn_fields.turn_started_at.and_then(|started_at| {
5372                    let elapsed = started_at.elapsed();
5373                    (elapsed > STOPWATCH_THRESHOLD).then(|| duration_alt_display(elapsed))
5374                })
5375            })
5376            .flatten();
5377
5378        let is_blocked_on_terminal_command =
5379            !confirmation && self.is_blocked_on_terminal_command(cx);
5380        let is_waiting = confirmation || self.thread.read(cx).has_in_progress_tool_calls();
5381
5382        let turn_tokens_label = elapsed_label
5383            .is_some()
5384            .then(|| {
5385                self.turn_fields
5386                    .turn_tokens
5387                    .filter(|&tokens| tokens > TOKEN_THRESHOLD)
5388                    .map(|tokens| crate::humanize_token_count(tokens))
5389            })
5390            .flatten();
5391
5392        let arrow_icon = if is_waiting {
5393            IconName::ArrowUp
5394        } else {
5395            IconName::ArrowDown
5396        };
5397
5398        h_flex()
5399            .id("generating-spinner")
5400            .py_2()
5401            .px(rems_from_px(22.))
5402            .gap_2()
5403            .map(|this| {
5404                if confirmation {
5405                    this.child(
5406                        h_flex()
5407                            .w_2()
5408                            .justify_center()
5409                            .child(GeneratingSpinnerElement::new(SpinnerVariant::Sand)),
5410                    )
5411                    .child(
5412                        div().min_w(rems(8.)).child(
5413                            LoadingLabel::new("Awaiting Confirmation")
5414                                .size(LabelSize::Small)
5415                                .color(Color::Muted),
5416                        ),
5417                    )
5418                } else if is_blocked_on_terminal_command {
5419                    this
5420                } else {
5421                    this.child(
5422                        h_flex()
5423                            .w_2()
5424                            .justify_center()
5425                            .child(GeneratingSpinnerElement::new(SpinnerVariant::Dots)),
5426                    )
5427                }
5428            })
5429            .when_some(elapsed_label, |this, elapsed| {
5430                this.child(
5431                    Label::new(elapsed)
5432                        .size(LabelSize::Small)
5433                        .color(Color::Muted),
5434                )
5435            })
5436            .when_some(turn_tokens_label, |this, tokens| {
5437                this.child(
5438                    h_flex()
5439                        .gap_0p5()
5440                        .child(
5441                            Icon::new(arrow_icon)
5442                                .size(IconSize::XSmall)
5443                                .color(Color::Muted),
5444                        )
5445                        .child(
5446                            Label::new(format!("{} tokens", tokens))
5447                                .size(LabelSize::Small)
5448                                .color(Color::Muted),
5449                        ),
5450                )
5451            })
5452            .into_any_element()
5453    }
5454
5455    pub(crate) fn auto_expand_streaming_thought(&mut self, cx: &mut Context<Self>) {
5456        let thinking_display = AgentSettings::get_global(cx).thinking_display;
5457
5458        if !matches!(
5459            thinking_display,
5460            ThinkingBlockDisplay::Auto | ThinkingBlockDisplay::Preview
5461        ) {
5462            return;
5463        }
5464
5465        let key = {
5466            let thread = self.thread.read(cx);
5467            if thread.status() != ThreadStatus::Generating {
5468                return;
5469            }
5470            let entries = thread.entries();
5471            let last_ix = entries.len().saturating_sub(1);
5472            match entries.get(last_ix) {
5473                Some(AgentThreadEntry::AssistantMessage(msg)) => match msg.chunks.last() {
5474                    Some(AssistantMessageChunk::Thought { .. }) => {
5475                        Some((last_ix, msg.chunks.len() - 1))
5476                    }
5477                    _ => None,
5478                },
5479                _ => None,
5480            }
5481        };
5482
5483        if let Some(key) = key {
5484            if self.auto_expanded_thinking_block != Some(key) {
5485                self.auto_expanded_thinking_block = Some(key);
5486                self.expanded_thinking_blocks.insert(key);
5487                cx.notify();
5488            }
5489        } else if self.auto_expanded_thinking_block.is_some() {
5490            if thinking_display == ThinkingBlockDisplay::Auto {
5491                if let Some(key) = self.auto_expanded_thinking_block {
5492                    if !self.user_toggled_thinking_blocks.contains(&key) {
5493                        self.expanded_thinking_blocks.remove(&key);
5494                    }
5495                }
5496            }
5497            self.auto_expanded_thinking_block = None;
5498            cx.notify();
5499        }
5500    }
5501
5502    pub(crate) fn clear_auto_expand_tracking(&mut self) {
5503        self.auto_expanded_thinking_block = None;
5504    }
5505
5506    fn toggle_thinking_block_expansion(&mut self, key: (usize, usize), cx: &mut Context<Self>) {
5507        let thinking_display = AgentSettings::get_global(cx).thinking_display;
5508
5509        match thinking_display {
5510            ThinkingBlockDisplay::Auto => {
5511                let is_open = self.expanded_thinking_blocks.contains(&key)
5512                    || self.user_toggled_thinking_blocks.contains(&key);
5513
5514                if is_open {
5515                    self.expanded_thinking_blocks.remove(&key);
5516                    self.user_toggled_thinking_blocks.remove(&key);
5517                } else {
5518                    self.expanded_thinking_blocks.insert(key);
5519                    self.user_toggled_thinking_blocks.insert(key);
5520                }
5521            }
5522            ThinkingBlockDisplay::Preview => {
5523                let is_user_expanded = self.user_toggled_thinking_blocks.contains(&key);
5524                let is_in_expanded_set = self.expanded_thinking_blocks.contains(&key);
5525
5526                if is_user_expanded {
5527                    self.user_toggled_thinking_blocks.remove(&key);
5528                    self.expanded_thinking_blocks.remove(&key);
5529                } else if is_in_expanded_set {
5530                    self.user_toggled_thinking_blocks.insert(key);
5531                } else {
5532                    self.expanded_thinking_blocks.insert(key);
5533                    self.user_toggled_thinking_blocks.insert(key);
5534                }
5535            }
5536            ThinkingBlockDisplay::AlwaysExpanded => {
5537                if self.user_toggled_thinking_blocks.contains(&key) {
5538                    self.user_toggled_thinking_blocks.remove(&key);
5539                } else {
5540                    self.user_toggled_thinking_blocks.insert(key);
5541                }
5542            }
5543            ThinkingBlockDisplay::AlwaysCollapsed => {
5544                if self.user_toggled_thinking_blocks.contains(&key) {
5545                    self.user_toggled_thinking_blocks.remove(&key);
5546                    self.expanded_thinking_blocks.remove(&key);
5547                } else {
5548                    self.expanded_thinking_blocks.insert(key);
5549                    self.user_toggled_thinking_blocks.insert(key);
5550                }
5551            }
5552        }
5553
5554        cx.notify();
5555    }
5556
5557    fn render_thinking_block(
5558        &self,
5559        entry_ix: usize,
5560        chunk_ix: usize,
5561        chunk: Entity<Markdown>,
5562        window: &Window,
5563        cx: &Context<Self>,
5564    ) -> AnyElement {
5565        let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix));
5566        let card_header_id = SharedString::from("inner-card-header");
5567
5568        let key = (entry_ix, chunk_ix);
5569
5570        let thinking_display = AgentSettings::get_global(cx).thinking_display;
5571        let is_user_toggled = self.user_toggled_thinking_blocks.contains(&key);
5572        let is_in_expanded_set = self.expanded_thinking_blocks.contains(&key);
5573
5574        let (is_open, is_constrained) = match thinking_display {
5575            ThinkingBlockDisplay::Auto => {
5576                let is_open = is_user_toggled || is_in_expanded_set;
5577                (is_open, false)
5578            }
5579            ThinkingBlockDisplay::Preview => {
5580                let is_open = is_user_toggled || is_in_expanded_set;
5581                let is_constrained = is_in_expanded_set && !is_user_toggled;
5582                (is_open, is_constrained)
5583            }
5584            ThinkingBlockDisplay::AlwaysExpanded => (!is_user_toggled, false),
5585            ThinkingBlockDisplay::AlwaysCollapsed => (is_user_toggled, false),
5586        };
5587
5588        let should_auto_scroll = self.auto_expanded_thinking_block == Some(key);
5589
5590        let scroll_handle = self
5591            .entry_view_state
5592            .read(cx)
5593            .entry(entry_ix)
5594            .and_then(|entry| entry.scroll_handle_for_assistant_message_chunk(chunk_ix));
5595
5596        if should_auto_scroll {
5597            if let Some(ref handle) = scroll_handle {
5598                handle.scroll_to_bottom();
5599            }
5600        }
5601
5602        let panel_bg = cx.theme().colors().panel_background;
5603
5604        v_flex()
5605            .gap_1()
5606            .child(
5607                h_flex()
5608                    .id(header_id)
5609                    .group(&card_header_id)
5610                    .relative()
5611                    .w_full()
5612                    .pr_1()
5613                    .justify_between()
5614                    .child(
5615                        h_flex()
5616                            .h(window.line_height() - px(2.))
5617                            .gap_1p5()
5618                            .overflow_hidden()
5619                            .child(
5620                                Icon::new(IconName::ToolThink)
5621                                    .size(IconSize::Small)
5622                                    .color(Color::Muted),
5623                            )
5624                            .child(
5625                                div()
5626                                    .text_size(self.tool_name_font_size())
5627                                    .text_color(cx.theme().colors().text_muted)
5628                                    .child("Thinking"),
5629                            ),
5630                    )
5631                    .child(
5632                        Disclosure::new(("expand", entry_ix), is_open)
5633                            .opened_icon(IconName::ChevronUp)
5634                            .closed_icon(IconName::ChevronDown)
5635                            .visible_on_hover(&card_header_id)
5636                            .on_click(cx.listener(
5637                                move |this, _event: &ClickEvent, _window, cx| {
5638                                    this.toggle_thinking_block_expansion(key, cx);
5639                                },
5640                            )),
5641                    )
5642                    .on_click(cx.listener(move |this, _event: &ClickEvent, _window, cx| {
5643                        this.toggle_thinking_block_expansion(key, cx);
5644                    })),
5645            )
5646            .when(is_open, |this| {
5647                this.child(
5648                    div()
5649                        .when(is_constrained, |this| this.relative())
5650                        .child(
5651                            div()
5652                                .id(("thinking-content", chunk_ix))
5653                                .ml_1p5()
5654                                .pl_3p5()
5655                                .border_l_1()
5656                                .border_color(self.tool_card_border_color(cx))
5657                                .when(is_constrained, |this| this.max_h_64())
5658                                .when_some(scroll_handle, |this, scroll_handle| {
5659                                    this.track_scroll(&scroll_handle)
5660                                })
5661                                .overflow_hidden()
5662                                .child(self.render_markdown(
5663                                    chunk,
5664                                    MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
5665                                )),
5666                        )
5667                        .when(is_constrained, |this| {
5668                            this.child(
5669                                div()
5670                                    .absolute()
5671                                    .inset_0()
5672                                    .size_full()
5673                                    .bg(linear_gradient(
5674                                        180.,
5675                                        linear_color_stop(panel_bg.opacity(0.8), 0.),
5676                                        linear_color_stop(panel_bg.opacity(0.), 0.1),
5677                                    ))
5678                                    .block_mouse_except_scroll(),
5679                            )
5680                        }),
5681                )
5682            })
5683            .into_any_element()
5684    }
5685
5686    fn render_message_context_menu(
5687        &self,
5688        entry_ix: usize,
5689        message_body: AnyElement,
5690        cx: &Context<Self>,
5691    ) -> AnyElement {
5692        let entity = cx.entity();
5693        let workspace = self.workspace.clone();
5694
5695        right_click_menu(format!("agent_context_menu-{}", entry_ix))
5696            .trigger(move |_, _, _| message_body)
5697            .menu(move |window, cx| {
5698                let focus = window.focused(cx);
5699                let entity = entity.clone();
5700                let workspace = workspace.clone();
5701
5702                ContextMenu::build(window, cx, move |menu, _, cx| {
5703                    let this = entity.read(cx);
5704                    let is_at_top = this.list_state.logical_scroll_top().item_ix == 0;
5705
5706                    let has_selection = this
5707                        .thread
5708                        .read(cx)
5709                        .entries()
5710                        .get(entry_ix)
5711                        .and_then(|entry| match &entry {
5712                            AgentThreadEntry::AssistantMessage(msg) => Some(&msg.chunks),
5713                            _ => None,
5714                        })
5715                        .map(|chunks| {
5716                            chunks.iter().any(|chunk| {
5717                                let md = match chunk {
5718                                    AssistantMessageChunk::Message { block } => block.markdown(),
5719                                    AssistantMessageChunk::Thought { block } => block.markdown(),
5720                                };
5721                                md.map_or(false, |m| m.read(cx).selected_text().is_some())
5722                            })
5723                        })
5724                        .unwrap_or(false);
5725
5726                    let copy_this_agent_response =
5727                        ContextMenuEntry::new("Copy This Agent Response").handler({
5728                            let entity = entity.clone();
5729                            move |_, cx| {
5730                                entity.update(cx, |this, cx| {
5731                                    let entries = this.thread.read(cx).entries();
5732                                    if let Some(text) =
5733                                        Self::get_agent_message_content(entries, entry_ix, cx)
5734                                    {
5735                                        cx.write_to_clipboard(ClipboardItem::new_string(text));
5736                                    }
5737                                });
5738                            }
5739                        });
5740
5741                    let scroll_item = if is_at_top {
5742                        ContextMenuEntry::new("Scroll to Bottom").handler({
5743                            let entity = entity.clone();
5744                            move |_, cx| {
5745                                entity.update(cx, |this, cx| {
5746                                    this.scroll_to_end(cx);
5747                                });
5748                            }
5749                        })
5750                    } else {
5751                        ContextMenuEntry::new("Scroll to Top").handler({
5752                            let entity = entity.clone();
5753                            move |_, cx| {
5754                                entity.update(cx, |this, cx| {
5755                                    this.scroll_to_top(cx);
5756                                });
5757                            }
5758                        })
5759                    };
5760
5761                    let open_thread_as_markdown = ContextMenuEntry::new("Open Thread as Markdown")
5762                        .handler({
5763                            let entity = entity.clone();
5764                            let workspace = workspace.clone();
5765                            move |window, cx| {
5766                                if let Some(workspace) = workspace.upgrade() {
5767                                    entity
5768                                        .update(cx, |this, cx| {
5769                                            this.open_thread_as_markdown(workspace, window, cx)
5770                                        })
5771                                        .detach_and_log_err(cx);
5772                                }
5773                            }
5774                        });
5775
5776                    menu.when_some(focus, |menu, focus| menu.context(focus))
5777                        .action_disabled_when(
5778                            !has_selection,
5779                            "Copy Selection",
5780                            Box::new(markdown::CopyAsMarkdown),
5781                        )
5782                        .item(copy_this_agent_response)
5783                        .separator()
5784                        .item(scroll_item)
5785                        .item(open_thread_as_markdown)
5786                })
5787            })
5788            .into_any_element()
5789    }
5790
5791    fn get_agent_message_content(
5792        entries: &[AgentThreadEntry],
5793        entry_index: usize,
5794        cx: &App,
5795    ) -> Option<String> {
5796        let entry = entries.get(entry_index)?;
5797        if matches!(entry, AgentThreadEntry::UserMessage(_)) {
5798            return None;
5799        }
5800
5801        let start_index = (0..entry_index)
5802            .rev()
5803            .find(|&i| matches!(entries.get(i), Some(AgentThreadEntry::UserMessage(_))))
5804            .map(|i| i + 1)
5805            .unwrap_or(0);
5806
5807        let end_index = (entry_index + 1..entries.len())
5808            .find(|&i| matches!(entries.get(i), Some(AgentThreadEntry::UserMessage(_))))
5809            .map(|i| i - 1)
5810            .unwrap_or(entries.len() - 1);
5811
5812        let parts: Vec<String> = (start_index..=end_index)
5813            .filter_map(|i| entries.get(i))
5814            .filter_map(|entry| {
5815                if let AgentThreadEntry::AssistantMessage(message) = entry {
5816                    let text: String = message
5817                        .chunks
5818                        .iter()
5819                        .filter_map(|chunk| match chunk {
5820                            AssistantMessageChunk::Message { block } => {
5821                                let markdown = block.to_markdown(cx);
5822                                if markdown.trim().is_empty() {
5823                                    None
5824                                } else {
5825                                    Some(markdown.to_string())
5826                                }
5827                            }
5828                            AssistantMessageChunk::Thought { .. } => None,
5829                        })
5830                        .collect::<Vec<_>>()
5831                        .join("\n\n");
5832
5833                    if text.is_empty() { None } else { Some(text) }
5834                } else {
5835                    None
5836                }
5837            })
5838            .collect();
5839
5840        let text = parts.join("\n\n");
5841        if text.is_empty() { None } else { Some(text) }
5842    }
5843
5844    fn is_blocked_on_terminal_command(&self, cx: &App) -> bool {
5845        let thread = self.thread.read(cx);
5846        if !matches!(thread.status(), ThreadStatus::Generating) {
5847            return false;
5848        }
5849
5850        let mut has_running_terminal_call = false;
5851
5852        for entry in thread.entries().iter().rev() {
5853            match entry {
5854                AgentThreadEntry::UserMessage(_) => break,
5855                AgentThreadEntry::ToolCall(tool_call)
5856                    if matches!(
5857                        tool_call.status,
5858                        ToolCallStatus::InProgress | ToolCallStatus::Pending
5859                    ) =>
5860                {
5861                    if matches!(tool_call.kind, acp::ToolKind::Execute) {
5862                        has_running_terminal_call = true;
5863                    } else {
5864                        return false;
5865                    }
5866                }
5867                AgentThreadEntry::ToolCall(_)
5868                | AgentThreadEntry::AssistantMessage(_)
5869                | AgentThreadEntry::CompletedPlan(_) => {}
5870            }
5871        }
5872
5873        has_running_terminal_call
5874    }
5875
5876    fn render_collapsible_command(
5877        &self,
5878        group: SharedString,
5879        is_preview: bool,
5880        command_source: &str,
5881        cx: &Context<Self>,
5882    ) -> Div {
5883        v_flex()
5884            .group(group.clone())
5885            .p_1p5()
5886            .bg(self.tool_card_header_bg(cx))
5887            .when(is_preview, |this| {
5888                this.pt_1().child(
5889                    // Wrapping this label on a container with 24px height to avoid
5890                    // layout shift when it changes from being a preview label
5891                    // to the actual path where the command will run in
5892                    h_flex().h_6().child(
5893                        Label::new("Run Command")
5894                            .buffer_font(cx)
5895                            .size(LabelSize::XSmall)
5896                            .color(Color::Muted),
5897                    ),
5898                )
5899            })
5900            .children(command_source.lines().map(|line| {
5901                let text: SharedString = if line.is_empty() {
5902                    " ".into()
5903                } else {
5904                    line.to_string().into()
5905                };
5906
5907                Label::new(text).buffer_font(cx).size(LabelSize::Small)
5908            }))
5909            .child(
5910                div().absolute().top_1().right_1().child(
5911                    CopyButton::new("copy-command", command_source.to_string())
5912                        .tooltip_label("Copy Command")
5913                        .visible_on_hover(group),
5914                ),
5915            )
5916    }
5917
5918    fn render_terminal_tool_call(
5919        &self,
5920        active_session_id: &acp::SessionId,
5921        entry_ix: usize,
5922        terminal: &Entity<acp_thread::Terminal>,
5923        tool_call: &ToolCall,
5924        focus_handle: &FocusHandle,
5925        is_subagent: bool,
5926        window: &Window,
5927        cx: &Context<Self>,
5928    ) -> AnyElement {
5929        let terminal_data = terminal.read(cx);
5930        let working_dir = terminal_data.working_dir();
5931        let command = terminal_data.command();
5932        let started_at = terminal_data.started_at();
5933
5934        let tool_failed = matches!(
5935            &tool_call.status,
5936            ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed
5937        );
5938
5939        let confirmation_options = match &tool_call.status {
5940            ToolCallStatus::WaitingForConfirmation { options, .. } => Some(options),
5941            _ => None,
5942        };
5943        let needs_confirmation = confirmation_options.is_some();
5944
5945        let output = terminal_data.output();
5946        let command_finished = output.is_some()
5947            && !matches!(
5948                tool_call.status,
5949                ToolCallStatus::InProgress | ToolCallStatus::Pending
5950            );
5951        let truncated_output =
5952            output.is_some_and(|output| output.original_content_len > output.content.len());
5953        let output_line_count = output.map(|output| output.content_line_count).unwrap_or(0);
5954
5955        let command_failed = command_finished
5956            && output.is_some_and(|o| o.exit_status.is_some_and(|status| !status.success()));
5957
5958        let time_elapsed = if let Some(output) = output {
5959            output.ended_at.duration_since(started_at)
5960        } else {
5961            started_at.elapsed()
5962        };
5963
5964        let header_id =
5965            SharedString::from(format!("terminal-tool-header-{}", terminal.entity_id()));
5966        let header_group = SharedString::from(format!(
5967            "terminal-tool-header-group-{}",
5968            terminal.entity_id()
5969        ));
5970        let header_bg = cx
5971            .theme()
5972            .colors()
5973            .element_background
5974            .blend(cx.theme().colors().editor_foreground.opacity(0.025));
5975        let border_color = cx.theme().colors().border.opacity(0.6);
5976
5977        let working_dir = working_dir
5978            .as_ref()
5979            .map(|path| path.display().to_string())
5980            .unwrap_or_else(|| "current directory".to_string());
5981
5982        // Since the command's source is wrapped in a markdown code block
5983        // (```\n...\n```), we need to strip that so we're left with only the
5984        // command's content.
5985        let command_source = command.read(cx).source();
5986        let command_content = command_source
5987            .strip_prefix("```\n")
5988            .and_then(|s| s.strip_suffix("\n```"))
5989            .unwrap_or(&command_source);
5990
5991        let command_element =
5992            self.render_collapsible_command(header_group.clone(), false, command_content, cx);
5993
5994        let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
5995
5996        let header = h_flex()
5997            .id(header_id)
5998            .pt_1()
5999            .pl_1p5()
6000            .pr_1()
6001            .flex_none()
6002            .gap_1()
6003            .justify_between()
6004            .rounded_t_md()
6005            .child(
6006                div()
6007                    .id(("command-target-path", terminal.entity_id()))
6008                    .w_full()
6009                    .max_w_full()
6010                    .overflow_x_scroll()
6011                    .child(
6012                        Label::new(working_dir)
6013                            .buffer_font(cx)
6014                            .size(LabelSize::XSmall)
6015                            .color(Color::Muted),
6016                    ),
6017            )
6018            .child(
6019                Disclosure::new(
6020                    SharedString::from(format!(
6021                        "terminal-tool-disclosure-{}",
6022                        terminal.entity_id()
6023                    )),
6024                    is_expanded,
6025                )
6026                .opened_icon(IconName::ChevronUp)
6027                .closed_icon(IconName::ChevronDown)
6028                .visible_on_hover(&header_group)
6029                .on_click(cx.listener({
6030                    let id = tool_call.id.clone();
6031                    move |this, _event, _window, cx| {
6032                        if is_expanded {
6033                            this.expanded_tool_calls.remove(&id);
6034                        } else {
6035                            this.expanded_tool_calls.insert(id.clone());
6036                        }
6037                        cx.notify();
6038                    }
6039                })),
6040            )
6041            .when(time_elapsed > Duration::from_secs(10), |header| {
6042                header.child(
6043                    Label::new(format!("({})", duration_alt_display(time_elapsed)))
6044                        .buffer_font(cx)
6045                        .color(Color::Muted)
6046                        .size(LabelSize::XSmall),
6047                )
6048            })
6049            .when(!command_finished && !needs_confirmation, |header| {
6050                header
6051                    .gap_1p5()
6052                    .child(
6053                        Icon::new(IconName::ArrowCircle)
6054                            .size(IconSize::XSmall)
6055                            .color(Color::Muted)
6056                            .with_rotate_animation(2)
6057                    )
6058                    .child(div().h(relative(0.6)).ml_1p5().child(Divider::vertical().color(DividerColor::Border)))
6059                    .child(
6060                        IconButton::new(
6061                            SharedString::from(format!("stop-terminal-{}", terminal.entity_id())),
6062                            IconName::Stop
6063                        )
6064                        .icon_size(IconSize::Small)
6065                        .icon_color(Color::Error)
6066                        .tooltip(move |_window, cx| {
6067                            Tooltip::with_meta(
6068                                "Stop This Command",
6069                                None,
6070                                "Also possible by placing your cursor inside the terminal and using regular terminal bindings.",
6071                                cx,
6072                            )
6073                        })
6074                        .on_click({
6075                            let terminal = terminal.clone();
6076                            cx.listener(move |this, _event, _window, cx| {
6077                                terminal.update(cx, |terminal, cx| {
6078                                    terminal.stop_by_user(cx);
6079                                });
6080                                if AgentSettings::get_global(cx).cancel_generation_on_terminal_stop {
6081                                    this.cancel_generation(cx);
6082                                }
6083                            })
6084                        }),
6085                    )
6086            })
6087            .when(truncated_output, |header| {
6088                let tooltip = if let Some(output) = output {
6089                    if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
6090                       format!("Output exceeded terminal max lines and was \
6091                            truncated, the model received the first {}.", format_file_size(output.content.len() as u64, true))
6092                    } else {
6093                        format!(
6094                            "Output is {} long, and to avoid unexpected token usage, \
6095                                only {} was sent back to the agent.",
6096                            format_file_size(output.original_content_len as u64, true),
6097                             format_file_size(output.content.len() as u64, true)
6098                        )
6099                    }
6100                } else {
6101                    "Output was truncated".to_string()
6102                };
6103
6104                header.child(
6105                    h_flex()
6106                        .id(("terminal-tool-truncated-label", terminal.entity_id()))
6107                        .gap_1()
6108                        .child(
6109                            Icon::new(IconName::Info)
6110                                .size(IconSize::XSmall)
6111                                .color(Color::Ignored),
6112                        )
6113                        .child(
6114                            Label::new("Truncated")
6115                                .color(Color::Muted)
6116                                .size(LabelSize::XSmall),
6117                        )
6118                        .tooltip(Tooltip::text(tooltip)),
6119                )
6120            })
6121            .when(tool_failed || command_failed, |header| {
6122                header.child(
6123                    div()
6124                        .id(("terminal-tool-error-code-indicator", terminal.entity_id()))
6125                        .child(
6126                            Icon::new(IconName::Close)
6127                                .size(IconSize::Small)
6128                                .color(Color::Error),
6129                        )
6130                        .when_some(output.and_then(|o| o.exit_status), |this, status| {
6131                            this.tooltip(Tooltip::text(format!(
6132                                "Exited with code {}",
6133                                status.code().unwrap_or(-1),
6134                            )))
6135                        }),
6136                )
6137            })
6138;
6139
6140        let terminal_view = self
6141            .entry_view_state
6142            .read(cx)
6143            .entry(entry_ix)
6144            .and_then(|entry| entry.terminal(terminal));
6145
6146        v_flex()
6147            .when(!is_subagent, |this| {
6148                this.my_1p5()
6149                    .mx_5()
6150                    .border_1()
6151                    .when(tool_failed || command_failed, |card| card.border_dashed())
6152                    .border_color(border_color)
6153                    .rounded_md()
6154            })
6155            .overflow_hidden()
6156            .child(
6157                v_flex()
6158                    .group(&header_group)
6159                    .bg(header_bg)
6160                    .text_xs()
6161                    .child(header)
6162                    .child(command_element),
6163            )
6164            .when(is_expanded && terminal_view.is_some(), |this| {
6165                this.child(
6166                    div()
6167                        .pt_2()
6168                        .border_t_1()
6169                        .when(tool_failed || command_failed, |card| card.border_dashed())
6170                        .border_color(border_color)
6171                        .bg(cx.theme().colors().editor_background)
6172                        .rounded_b_md()
6173                        .text_ui_sm(cx)
6174                        .h_full()
6175                        .children(terminal_view.map(|terminal_view| {
6176                            let element = if terminal_view
6177                                .read(cx)
6178                                .content_mode(window, cx)
6179                                .is_scrollable()
6180                            {
6181                                div().h_72().child(terminal_view).into_any_element()
6182                            } else {
6183                                terminal_view.into_any_element()
6184                            };
6185
6186                            div()
6187                                .on_action(cx.listener(|_this, _: &NewTerminal, window, cx| {
6188                                    window.dispatch_action(NewThread.boxed_clone(), cx);
6189                                    cx.stop_propagation();
6190                                }))
6191                                .child(element)
6192                                .into_any_element()
6193                        })),
6194                )
6195            })
6196            .when_some(confirmation_options, |this, options| {
6197                let is_first = self.is_first_tool_call(active_session_id, &tool_call.id, cx);
6198                this.child(self.render_permission_buttons(
6199                    self.thread.read(cx).session_id().clone(),
6200                    is_first,
6201                    options,
6202                    entry_ix,
6203                    tool_call.id.clone(),
6204                    focus_handle,
6205                    cx,
6206                ))
6207            })
6208            .into_any()
6209    }
6210
6211    fn is_first_tool_call(
6212        &self,
6213        active_session_id: &acp::SessionId,
6214        tool_call_id: &acp::ToolCallId,
6215        cx: &App,
6216    ) -> bool {
6217        self.conversation
6218            .read(cx)
6219            .pending_tool_call(active_session_id, cx)
6220            .map_or(false, |(pending_session_id, pending_tool_call_id, _)| {
6221                self.thread.read(cx).session_id() == &pending_session_id
6222                    && tool_call_id == &pending_tool_call_id
6223            })
6224    }
6225
6226    fn render_any_tool_call(
6227        &self,
6228        active_session_id: &acp::SessionId,
6229        entry_ix: usize,
6230        tool_call: &ToolCall,
6231        focus_handle: &FocusHandle,
6232        is_subagent: bool,
6233        window: &Window,
6234        cx: &Context<Self>,
6235    ) -> Div {
6236        let has_terminals = tool_call.terminals().next().is_some();
6237
6238        div().w_full().map(|this| {
6239            if tool_call.is_subagent() {
6240                this.child(
6241                    self.render_subagent_tool_call(
6242                        active_session_id,
6243                        entry_ix,
6244                        tool_call,
6245                        tool_call
6246                            .subagent_session_info
6247                            .as_ref()
6248                            .map(|i| i.session_id.clone()),
6249                        focus_handle,
6250                        window,
6251                        cx,
6252                    ),
6253                )
6254            } else if has_terminals {
6255                this.children(tool_call.terminals().map(|terminal| {
6256                    self.render_terminal_tool_call(
6257                        active_session_id,
6258                        entry_ix,
6259                        terminal,
6260                        tool_call,
6261                        focus_handle,
6262                        is_subagent,
6263                        window,
6264                        cx,
6265                    )
6266                }))
6267            } else {
6268                this.child(self.render_tool_call(
6269                    active_session_id,
6270                    entry_ix,
6271                    tool_call,
6272                    focus_handle,
6273                    is_subagent,
6274                    window,
6275                    cx,
6276                ))
6277            }
6278        })
6279    }
6280
6281    fn render_tool_call(
6282        &self,
6283        active_session_id: &acp::SessionId,
6284        entry_ix: usize,
6285        tool_call: &ToolCall,
6286        focus_handle: &FocusHandle,
6287        is_subagent: bool,
6288        window: &Window,
6289        cx: &Context<Self>,
6290    ) -> Div {
6291        let has_location = tool_call.locations.len() == 1;
6292        let card_header_id = SharedString::from("inner-tool-call-header");
6293
6294        let failed_or_canceled = match &tool_call.status {
6295            ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => true,
6296            _ => false,
6297        };
6298
6299        let needs_confirmation = matches!(
6300            tool_call.status,
6301            ToolCallStatus::WaitingForConfirmation { .. }
6302        );
6303        let is_terminal_tool = matches!(tool_call.kind, acp::ToolKind::Execute);
6304
6305        let is_edit =
6306            matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some();
6307
6308        let is_cancelled_edit = is_edit && matches!(tool_call.status, ToolCallStatus::Canceled);
6309        let (has_revealed_diff, tool_call_output_focus, tool_call_output_focus_handle) = tool_call
6310            .diffs()
6311            .next()
6312            .and_then(|diff| {
6313                let editor = self
6314                    .entry_view_state
6315                    .read(cx)
6316                    .entry(entry_ix)
6317                    .and_then(|entry| entry.editor_for_diff(diff))?;
6318                let has_revealed_diff = diff.read(cx).has_revealed_range(cx);
6319                let has_focus = editor.read(cx).is_focused(window);
6320                let focus_handle = editor.focus_handle(cx);
6321                Some((has_revealed_diff, has_focus, focus_handle))
6322            })
6323            .unwrap_or_else(|| (false, false, focus_handle.clone()));
6324
6325        let use_card_layout = needs_confirmation || is_edit || is_terminal_tool;
6326
6327        let has_image_content = tool_call.content.iter().any(|c| c.image().is_some());
6328        let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
6329        let mut is_open = self.expanded_tool_calls.contains(&tool_call.id);
6330
6331        is_open |= needs_confirmation;
6332
6333        let should_show_raw_input = !is_terminal_tool && !is_edit && !has_image_content;
6334
6335        let input_output_header = |label: SharedString| {
6336            Label::new(label)
6337                .size(LabelSize::XSmall)
6338                .color(Color::Muted)
6339                .buffer_font(cx)
6340        };
6341
6342        let tool_output_display = if is_open {
6343            match &tool_call.status {
6344                ToolCallStatus::WaitingForConfirmation { options, .. } => v_flex()
6345                    .w_full()
6346                    .children(
6347                        tool_call
6348                            .content
6349                            .iter()
6350                            .enumerate()
6351                            .map(|(content_ix, content)| {
6352                                div()
6353                                    .child(self.render_tool_call_content(
6354                                        active_session_id,
6355                                        entry_ix,
6356                                        content,
6357                                        content_ix,
6358                                        tool_call,
6359                                        use_card_layout,
6360                                        has_image_content,
6361                                        failed_or_canceled,
6362                                        focus_handle,
6363                                        window,
6364                                        cx,
6365                                    ))
6366                                    .into_any_element()
6367                            }),
6368                    )
6369                    .when(should_show_raw_input, |this| {
6370                        let is_raw_input_expanded =
6371                            self.expanded_tool_call_raw_inputs.contains(&tool_call.id);
6372
6373                        let input_header = if is_raw_input_expanded {
6374                            "Raw Input:"
6375                        } else {
6376                            "View Raw Input"
6377                        };
6378
6379                        this.child(
6380                            v_flex()
6381                                .p_2()
6382                                .gap_1()
6383                                .border_t_1()
6384                                .border_color(self.tool_card_border_color(cx))
6385                                .child(
6386                                    h_flex()
6387                                        .id("disclosure_container")
6388                                        .pl_0p5()
6389                                        .gap_1()
6390                                        .justify_between()
6391                                        .rounded_xs()
6392                                        .hover(|s| s.bg(cx.theme().colors().element_hover))
6393                                        .child(input_output_header(input_header.into()))
6394                                        .child(
6395                                            Disclosure::new(
6396                                                ("raw-input-disclosure", entry_ix),
6397                                                is_raw_input_expanded,
6398                                            )
6399                                            .opened_icon(IconName::ChevronUp)
6400                                            .closed_icon(IconName::ChevronDown),
6401                                        )
6402                                        .on_click(cx.listener({
6403                                            let id = tool_call.id.clone();
6404
6405                                            move |this: &mut Self, _, _, cx| {
6406                                                if this.expanded_tool_call_raw_inputs.contains(&id)
6407                                                {
6408                                                    this.expanded_tool_call_raw_inputs.remove(&id);
6409                                                } else {
6410                                                    this.expanded_tool_call_raw_inputs
6411                                                        .insert(id.clone());
6412                                                }
6413                                                cx.notify();
6414                                            }
6415                                        })),
6416                                )
6417                                .when(is_raw_input_expanded, |this| {
6418                                    this.children(tool_call.raw_input_markdown.clone().map(
6419                                        |input| {
6420                                            self.render_markdown(
6421                                                input,
6422                                                MarkdownStyle::themed(
6423                                                    MarkdownFont::Agent,
6424                                                    window,
6425                                                    cx,
6426                                                ),
6427                                            )
6428                                        },
6429                                    ))
6430                                }),
6431                        )
6432                    })
6433                    .child(self.render_permission_buttons(
6434                        self.thread.read(cx).session_id().clone(),
6435                        self.is_first_tool_call(active_session_id, &tool_call.id, cx),
6436                        options,
6437                        entry_ix,
6438                        tool_call.id.clone(),
6439                        focus_handle,
6440                        cx,
6441                    ))
6442                    .into_any(),
6443                ToolCallStatus::Pending | ToolCallStatus::InProgress
6444                    if is_edit
6445                        && tool_call.content.is_empty()
6446                        && self.as_native_connection(cx).is_some() =>
6447                {
6448                    self.render_diff_loading(cx)
6449                }
6450                ToolCallStatus::Pending
6451                | ToolCallStatus::InProgress
6452                | ToolCallStatus::Completed
6453                | ToolCallStatus::Failed
6454                | ToolCallStatus::Canceled => v_flex()
6455                    .when(should_show_raw_input, |this| {
6456                        this.mt_1p5().w_full().child(
6457                            v_flex()
6458                                .ml(rems(0.4))
6459                                .px_3p5()
6460                                .pb_1()
6461                                .gap_1()
6462                                .border_l_1()
6463                                .border_color(self.tool_card_border_color(cx))
6464                                .child(input_output_header("Raw Input:".into()))
6465                                .children(tool_call.raw_input_markdown.clone().map(|input| {
6466                                    div().id(("tool-call-raw-input-markdown", entry_ix)).child(
6467                                        self.render_markdown(
6468                                            input,
6469                                            MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
6470                                        ),
6471                                    )
6472                                }))
6473                                .child(input_output_header("Output:".into())),
6474                        )
6475                    })
6476                    .children(
6477                        tool_call
6478                            .content
6479                            .iter()
6480                            .enumerate()
6481                            .map(|(content_ix, content)| {
6482                                div().id(("tool-call-output", entry_ix)).child(
6483                                    self.render_tool_call_content(
6484                                        active_session_id,
6485                                        entry_ix,
6486                                        content,
6487                                        content_ix,
6488                                        tool_call,
6489                                        use_card_layout,
6490                                        has_image_content,
6491                                        failed_or_canceled,
6492                                        focus_handle,
6493                                        window,
6494                                        cx,
6495                                    ),
6496                                )
6497                            }),
6498                    )
6499                    .into_any(),
6500                ToolCallStatus::Rejected => Empty.into_any(),
6501            }
6502            .into()
6503        } else {
6504            None
6505        };
6506
6507        v_flex()
6508            .map(|this| {
6509                if is_subagent {
6510                    this
6511                } else if use_card_layout {
6512                    this.my_1p5()
6513                        .rounded_md()
6514                        .border_1()
6515                        .when(failed_or_canceled, |this| this.border_dashed())
6516                        .border_color(self.tool_card_border_color(cx))
6517                        .bg(cx.theme().colors().editor_background)
6518                        .overflow_hidden()
6519                } else {
6520                    this.my_1()
6521                }
6522            })
6523            .when(!is_subagent, |this| {
6524                this.map(|this| {
6525                    if has_location && !use_card_layout {
6526                        this.ml_4()
6527                    } else {
6528                        this.ml_5()
6529                    }
6530                })
6531                .mr_5()
6532            })
6533            .map(|this| {
6534                if is_terminal_tool {
6535                    let label_source = tool_call.label.read(cx).source();
6536                    this.child(self.render_collapsible_command(
6537                        card_header_id.clone(),
6538                        true,
6539                        label_source,
6540                        cx,
6541                    ))
6542                } else {
6543                    this.child(
6544                        h_flex()
6545                            .group(&card_header_id)
6546                            .relative()
6547                            .w_full()
6548                            .justify_between()
6549                            .when(use_card_layout, |this| {
6550                                this.p_0p5()
6551                                    .rounded_t(rems_from_px(5.))
6552                                    .bg(self.tool_card_header_bg(cx))
6553                            })
6554                            .child(self.render_tool_call_label(
6555                                entry_ix,
6556                                tool_call,
6557                                is_edit,
6558                                is_cancelled_edit,
6559                                has_revealed_diff,
6560                                use_card_layout,
6561                                window,
6562                                cx,
6563                            ))
6564                            .child(
6565                                h_flex()
6566                                    .when(is_collapsible || failed_or_canceled, |this| {
6567                                        let diff_for_discard = if has_revealed_diff
6568                                            && is_cancelled_edit
6569                                        {
6570                                            tool_call.diffs().next().cloned()
6571                                        } else {
6572                                            None
6573                                        };
6574
6575                                        this.child(
6576                                            h_flex()
6577                                                .pr_0p5()
6578                                                .gap_1()
6579                                                .when(is_collapsible, |this| {
6580                                                    this.child(
6581                                                        Disclosure::new(
6582                                                            ("expand-output", entry_ix),
6583                                                            is_open,
6584                                                        )
6585                                                        .opened_icon(IconName::ChevronUp)
6586                                                        .closed_icon(IconName::ChevronDown)
6587                                                        .visible_on_hover(&card_header_id)
6588                                                        .on_click(cx.listener({
6589                                                            let id = tool_call.id.clone();
6590                                                            move |this: &mut Self,
6591                                                                  _,
6592                                                                  _,
6593                                                                  cx: &mut Context<Self>| {
6594                                                                if is_open {
6595                                                                    this.expanded_tool_calls
6596                                                                        .remove(&id);
6597                                                                } else {
6598                                                                    this.expanded_tool_calls
6599                                                                        .insert(id.clone());
6600                                                                }
6601                                                                cx.notify();
6602                                                            }
6603                                                        })),
6604                                                    )
6605                                                })
6606                                                .when(failed_or_canceled, |this| {
6607                                                    if is_cancelled_edit && !has_revealed_diff {
6608                                                        this.child(
6609                                                            div()
6610                                                                .id(entry_ix)
6611                                                                .tooltip(Tooltip::text(
6612                                                                    "Interrupted Edit",
6613                                                                ))
6614                                                                .child(
6615                                                                    Icon::new(IconName::XCircle)
6616                                                                        .color(Color::Muted)
6617                                                                        .size(IconSize::Small),
6618                                                                ),
6619                                                        )
6620                                                    } else if is_cancelled_edit {
6621                                                        this
6622                                                    } else {
6623                                                        this.child(
6624                                                            Icon::new(IconName::Close)
6625                                                                .color(Color::Error)
6626                                                                .size(IconSize::Small),
6627                                                        )
6628                                                    }
6629                                                })
6630                                                .when_some(diff_for_discard, |this, diff| {
6631                                                    let tool_call_id = tool_call.id.clone();
6632                                                    let is_discarded = self
6633                                                        .discarded_partial_edits
6634                                                        .contains(&tool_call_id);
6635
6636                                                    this.when(!is_discarded, |this| {
6637                                                        this.child(
6638                                                            IconButton::new(
6639                                                                ("discard-partial-edit", entry_ix),
6640                                                                IconName::Undo,
6641                                                            )
6642                                                            .icon_size(IconSize::Small)
6643                                                            .tooltip(move |_, cx| {
6644                                                                Tooltip::with_meta(
6645                                                                    "Discard Interrupted Edit",
6646                                                                    None,
6647                                                                    "You can discard this interrupted partial edit and restore the original file content.",
6648                                                                    cx,
6649                                                                )
6650                                                            })
6651                                                            .on_click(cx.listener({
6652                                                                let tool_call_id =
6653                                                                    tool_call_id.clone();
6654                                                                move |this, _, _window, cx| {
6655                                                                    let diff_data = diff.read(cx);
6656                                                                    let base_text = diff_data
6657                                                                        .base_text()
6658                                                                        .clone();
6659                                                                    let buffer =
6660                                                                        diff_data.buffer().clone();
6661                                                                    buffer.update(
6662                                                                        cx,
6663                                                                        |buffer, cx| {
6664                                                                            buffer.set_text(
6665                                                                                base_text.as_ref(),
6666                                                                                cx,
6667                                                                            );
6668                                                                        },
6669                                                                    );
6670                                                                    this.discarded_partial_edits
6671                                                                        .insert(
6672                                                                            tool_call_id.clone(),
6673                                                                        );
6674                                                                    cx.notify();
6675                                                                }
6676                                                            })),
6677                                                        )
6678                                                    })
6679                                                }),
6680                                        )
6681                                    })
6682                                    .when(tool_call_output_focus, |this| {
6683                                        this.child(
6684                                            Button::new("open-file-button", "Open File")
6685                                                .style(ButtonStyle::Outlined)
6686                                                .label_size(LabelSize::Small)
6687                                                .key_binding(
6688                                                    KeyBinding::for_action_in(&OpenExcerpts, &tool_call_output_focus_handle, cx)
6689                                                        .map(|s| s.size(rems_from_px(12.))),
6690                                                )
6691                                                .on_click(|_, window, cx| {
6692                                                    window.dispatch_action(
6693                                                        Box::new(OpenExcerpts),
6694                                                        cx,
6695                                                    )
6696                                                }),
6697                                        )
6698                                    }),
6699                            )
6700
6701                    )
6702                }
6703            })
6704            .children(tool_output_display)
6705    }
6706
6707    fn render_permission_buttons(
6708        &self,
6709        session_id: acp::SessionId,
6710        is_first: bool,
6711        options: &PermissionOptions,
6712        entry_ix: usize,
6713        tool_call_id: acp::ToolCallId,
6714        focus_handle: &FocusHandle,
6715        cx: &Context<Self>,
6716    ) -> Div {
6717        match options {
6718            PermissionOptions::Flat(options) => self.render_permission_buttons_flat(
6719                session_id,
6720                is_first,
6721                options,
6722                entry_ix,
6723                tool_call_id,
6724                focus_handle,
6725                cx,
6726            ),
6727            PermissionOptions::Dropdown(choices) => self.render_permission_buttons_with_dropdown(
6728                is_first,
6729                choices,
6730                None,
6731                entry_ix,
6732                tool_call_id,
6733                focus_handle,
6734                cx,
6735            ),
6736            PermissionOptions::DropdownWithPatterns {
6737                choices,
6738                patterns,
6739                tool_name,
6740            } => self.render_permission_buttons_with_dropdown(
6741                is_first,
6742                choices,
6743                Some((patterns, tool_name)),
6744                entry_ix,
6745                tool_call_id,
6746                focus_handle,
6747                cx,
6748            ),
6749        }
6750    }
6751
6752    fn render_permission_buttons_with_dropdown(
6753        &self,
6754        is_first: bool,
6755        choices: &[PermissionOptionChoice],
6756        patterns: Option<(&[PermissionPattern], &str)>,
6757        entry_ix: usize,
6758        tool_call_id: acp::ToolCallId,
6759        focus_handle: &FocusHandle,
6760        cx: &Context<Self>,
6761    ) -> Div {
6762        let selection = self.permission_selections.get(&tool_call_id);
6763
6764        let selected_index = selection
6765            .and_then(|s| s.choice_index())
6766            .unwrap_or_else(|| choices.len().saturating_sub(1));
6767
6768        let dropdown_label: SharedString =
6769            if matches!(selection, Some(PermissionSelection::SelectedPatterns(_))) {
6770                "Always for selected commands".into()
6771            } else {
6772                choices
6773                    .get(selected_index)
6774                    .or(choices.last())
6775                    .map(|choice| choice.label())
6776                    .unwrap_or_else(|| "Only this time".into())
6777            };
6778
6779        let dropdown = if let Some((pattern_list, tool_name)) = patterns {
6780            self.render_permission_granularity_dropdown_with_patterns(
6781                choices,
6782                pattern_list,
6783                tool_name,
6784                dropdown_label,
6785                entry_ix,
6786                tool_call_id.clone(),
6787                is_first,
6788                cx,
6789            )
6790        } else {
6791            self.render_permission_granularity_dropdown(
6792                choices,
6793                dropdown_label,
6794                entry_ix,
6795                tool_call_id.clone(),
6796                selected_index,
6797                is_first,
6798                cx,
6799            )
6800        };
6801
6802        h_flex()
6803            .w_full()
6804            .p_1()
6805            .gap_2()
6806            .justify_between()
6807            .border_t_1()
6808            .border_color(self.tool_card_border_color(cx))
6809            .child(
6810                h_flex()
6811                    .gap_0p5()
6812                    .child(
6813                        Button::new(("allow-btn", entry_ix), "Allow")
6814                            .start_icon(
6815                                Icon::new(IconName::Check)
6816                                    .size(IconSize::XSmall)
6817                                    .color(Color::Success),
6818                            )
6819                            .label_size(LabelSize::Small)
6820                            .when(is_first, |this| {
6821                                this.key_binding(
6822                                    KeyBinding::for_action_in(
6823                                        &AllowOnce as &dyn Action,
6824                                        focus_handle,
6825                                        cx,
6826                                    )
6827                                    .map(|kb| kb.size(rems_from_px(12.))),
6828                                )
6829                            })
6830                            .on_click(cx.listener({
6831                                move |this, _, window, cx| {
6832                                    this.authorize_pending_with_granularity(true, window, cx);
6833                                }
6834                            })),
6835                    )
6836                    .child(
6837                        Button::new(("deny-btn", entry_ix), "Deny")
6838                            .start_icon(
6839                                Icon::new(IconName::Close)
6840                                    .size(IconSize::XSmall)
6841                                    .color(Color::Error),
6842                            )
6843                            .label_size(LabelSize::Small)
6844                            .when(is_first, |this| {
6845                                this.key_binding(
6846                                    KeyBinding::for_action_in(
6847                                        &RejectOnce as &dyn Action,
6848                                        focus_handle,
6849                                        cx,
6850                                    )
6851                                    .map(|kb| kb.size(rems_from_px(12.))),
6852                                )
6853                            })
6854                            .on_click(cx.listener({
6855                                move |this, _, window, cx| {
6856                                    this.authorize_pending_with_granularity(false, window, cx);
6857                                }
6858                            })),
6859                    ),
6860            )
6861            .child(dropdown)
6862    }
6863
6864    fn render_permission_granularity_dropdown(
6865        &self,
6866        choices: &[PermissionOptionChoice],
6867        current_label: SharedString,
6868        entry_ix: usize,
6869        tool_call_id: acp::ToolCallId,
6870        selected_index: usize,
6871        is_first: bool,
6872        cx: &Context<Self>,
6873    ) -> AnyElement {
6874        let menu_options: Vec<(usize, SharedString)> = choices
6875            .iter()
6876            .enumerate()
6877            .map(|(i, choice)| (i, choice.label()))
6878            .collect();
6879
6880        let permission_dropdown_handle = self.permission_dropdown_handle.clone();
6881
6882        PopoverMenu::new(("permission-granularity", entry_ix))
6883            .with_handle(permission_dropdown_handle)
6884            .trigger(
6885                Button::new(("granularity-trigger", entry_ix), current_label)
6886                    .end_icon(
6887                        Icon::new(IconName::ChevronDown)
6888                            .size(IconSize::XSmall)
6889                            .color(Color::Muted),
6890                    )
6891                    .label_size(LabelSize::Small)
6892                    .when(is_first, |this| {
6893                        this.key_binding(
6894                            KeyBinding::for_action_in(
6895                                &crate::OpenPermissionDropdown as &dyn Action,
6896                                &self.focus_handle(cx),
6897                                cx,
6898                            )
6899                            .map(|kb| kb.size(rems_from_px(12.))),
6900                        )
6901                    }),
6902            )
6903            .menu(move |window, cx| {
6904                let tool_call_id = tool_call_id.clone();
6905                let options = menu_options.clone();
6906
6907                Some(ContextMenu::build(window, cx, move |mut menu, _, _| {
6908                    for (index, display_name) in options.iter() {
6909                        let display_name = display_name.clone();
6910                        let index = *index;
6911                        let tool_call_id_for_entry = tool_call_id.clone();
6912                        let is_selected = index == selected_index;
6913                        menu = menu.toggleable_entry(
6914                            display_name,
6915                            is_selected,
6916                            IconPosition::End,
6917                            None,
6918                            move |window, cx| {
6919                                window.dispatch_action(
6920                                    SelectPermissionGranularity {
6921                                        tool_call_id: tool_call_id_for_entry.0.to_string(),
6922                                        index,
6923                                    }
6924                                    .boxed_clone(),
6925                                    cx,
6926                                );
6927                            },
6928                        );
6929                    }
6930
6931                    menu
6932                }))
6933            })
6934            .into_any_element()
6935    }
6936
6937    fn render_permission_granularity_dropdown_with_patterns(
6938        &self,
6939        choices: &[PermissionOptionChoice],
6940        patterns: &[PermissionPattern],
6941        _tool_name: &str,
6942        current_label: SharedString,
6943        entry_ix: usize,
6944        tool_call_id: acp::ToolCallId,
6945        is_first: bool,
6946        cx: &Context<Self>,
6947    ) -> AnyElement {
6948        let default_choice_index = choices.len().saturating_sub(1);
6949        let menu_options: Vec<(usize, SharedString)> = choices
6950            .iter()
6951            .enumerate()
6952            .map(|(i, choice)| (i, choice.label()))
6953            .collect();
6954
6955        let pattern_options: Vec<(usize, SharedString)> = patterns
6956            .iter()
6957            .enumerate()
6958            .map(|(i, cp)| {
6959                (
6960                    i,
6961                    SharedString::from(format!("Always for `{}` commands", cp.display_name)),
6962                )
6963            })
6964            .collect();
6965
6966        let pattern_count = patterns.len();
6967        let permission_dropdown_handle = self.permission_dropdown_handle.clone();
6968        let view = cx.entity().downgrade();
6969
6970        PopoverMenu::new(("permission-granularity", entry_ix))
6971            .with_handle(permission_dropdown_handle.clone())
6972            .anchor(Corner::TopRight)
6973            .attach(Corner::BottomRight)
6974            .trigger(
6975                Button::new(("granularity-trigger", entry_ix), current_label)
6976                    .end_icon(
6977                        Icon::new(IconName::ChevronDown)
6978                            .size(IconSize::XSmall)
6979                            .color(Color::Muted),
6980                    )
6981                    .label_size(LabelSize::Small)
6982                    .when(is_first, |this| {
6983                        this.key_binding(
6984                            KeyBinding::for_action_in(
6985                                &crate::OpenPermissionDropdown as &dyn Action,
6986                                &self.focus_handle(cx),
6987                                cx,
6988                            )
6989                            .map(|kb| kb.size(rems_from_px(12.))),
6990                        )
6991                    }),
6992            )
6993            .menu(move |window, cx| {
6994                let tool_call_id = tool_call_id.clone();
6995                let options = menu_options.clone();
6996                let patterns = pattern_options.clone();
6997                let view = view.clone();
6998                let dropdown_handle = permission_dropdown_handle.clone();
6999
7000                Some(ContextMenu::build_persistent(
7001                    window,
7002                    cx,
7003                    move |menu, _window, cx| {
7004                        let mut menu = menu;
7005
7006                        // Read fresh selection state from the view on each rebuild.
7007                        let selection: Option<PermissionSelection> = view.upgrade().and_then(|v| {
7008                            let view = v.read(cx);
7009                            view.permission_selections.get(&tool_call_id).cloned()
7010                        });
7011
7012                        let is_pattern_mode =
7013                            matches!(selection, Some(PermissionSelection::SelectedPatterns(_)));
7014
7015                        // Granularity choices: "Always for terminal", "Only this time"
7016                        for (index, display_name) in options.iter() {
7017                            let display_name = display_name.clone();
7018                            let index = *index;
7019                            let tool_call_id_for_entry = tool_call_id.clone();
7020                            let is_selected = !is_pattern_mode
7021                                && selection
7022                                    .as_ref()
7023                                    .and_then(|s| s.choice_index())
7024                                    .map_or(index == default_choice_index, |ci| ci == index);
7025
7026                            let view = view.clone();
7027                            menu = menu.toggleable_entry(
7028                                display_name,
7029                                is_selected,
7030                                IconPosition::End,
7031                                None,
7032                                move |_window, cx| {
7033                                    view.update(cx, |this, cx| {
7034                                        this.permission_selections.insert(
7035                                            tool_call_id_for_entry.clone(),
7036                                            PermissionSelection::Choice(index),
7037                                        );
7038                                        cx.notify();
7039                                    })
7040                                    .log_err();
7041                                },
7042                            );
7043                        }
7044
7045                        menu = menu.separator().header("Select Options…");
7046
7047                        for (pattern_index, label) in patterns.iter() {
7048                            let label = label.clone();
7049                            let pattern_index = *pattern_index;
7050                            let tool_call_id_for_pattern = tool_call_id.clone();
7051                            let is_checked = selection
7052                                .as_ref()
7053                                .is_some_and(|s| s.is_pattern_checked(pattern_index));
7054
7055                            let view = view.clone();
7056                            menu = menu.toggleable_entry(
7057                                label,
7058                                is_checked,
7059                                IconPosition::End,
7060                                None,
7061                                move |_window, cx| {
7062                                    view.update(cx, |this, cx| {
7063                                        let selection = this
7064                                            .permission_selections
7065                                            .get_mut(&tool_call_id_for_pattern);
7066
7067                                        match selection {
7068                                            Some(PermissionSelection::SelectedPatterns(_)) => {
7069                                                // Already in pattern mode — toggle.
7070                                                this.permission_selections
7071                                                    .get_mut(&tool_call_id_for_pattern)
7072                                                    .expect("just matched above")
7073                                                    .toggle_pattern(pattern_index);
7074                                            }
7075                                            _ => {
7076                                                // First click: activate pattern mode
7077                                                // with all patterns checked.
7078                                                this.permission_selections.insert(
7079                                                    tool_call_id_for_pattern.clone(),
7080                                                    PermissionSelection::SelectedPatterns(
7081                                                        (0..pattern_count).collect(),
7082                                                    ),
7083                                                );
7084                                            }
7085                                        }
7086                                        cx.notify();
7087                                    })
7088                                    .log_err();
7089                                },
7090                            );
7091                        }
7092
7093                        let any_patterns_checked = selection
7094                            .as_ref()
7095                            .is_some_and(|s| s.has_any_checked_patterns());
7096                        let dropdown_handle = dropdown_handle.clone();
7097                        menu = menu.custom_row(move |_window, _cx| {
7098                            div()
7099                                .py_1()
7100                                .w_full()
7101                                .child(
7102                                    Button::new("apply-patterns", "Apply")
7103                                        .full_width()
7104                                        .style(ButtonStyle::Outlined)
7105                                        .label_size(LabelSize::Small)
7106                                        .disabled(!any_patterns_checked)
7107                                        .on_click({
7108                                            let dropdown_handle = dropdown_handle.clone();
7109                                            move |_event, _window, cx| {
7110                                                dropdown_handle.hide(cx);
7111                                            }
7112                                        }),
7113                                )
7114                                .into_any_element()
7115                        });
7116
7117                        menu
7118                    },
7119                ))
7120            })
7121            .into_any_element()
7122    }
7123
7124    fn render_permission_buttons_flat(
7125        &self,
7126        session_id: acp::SessionId,
7127        is_first: bool,
7128        options: &[acp::PermissionOption],
7129        entry_ix: usize,
7130        tool_call_id: acp::ToolCallId,
7131        focus_handle: &FocusHandle,
7132        cx: &Context<Self>,
7133    ) -> Div {
7134        let mut seen_kinds: ArrayVec<acp::PermissionOptionKind, 3, u8> = ArrayVec::new();
7135
7136        div()
7137            .p_1()
7138            .border_t_1()
7139            .border_color(self.tool_card_border_color(cx))
7140            .w_full()
7141            .v_flex()
7142            .gap_0p5()
7143            .children(options.iter().map(move |option| {
7144                let option_id = SharedString::from(option.option_id.0.clone());
7145                Button::new((option_id, entry_ix), option.name.clone())
7146                    .map(|this| {
7147                        let (icon, action) = match option.kind {
7148                            acp::PermissionOptionKind::AllowOnce => (
7149                                Icon::new(IconName::Check)
7150                                    .size(IconSize::XSmall)
7151                                    .color(Color::Success),
7152                                Some(&AllowOnce as &dyn Action),
7153                            ),
7154                            acp::PermissionOptionKind::AllowAlways => (
7155                                Icon::new(IconName::CheckDouble)
7156                                    .size(IconSize::XSmall)
7157                                    .color(Color::Success),
7158                                Some(&AllowAlways as &dyn Action),
7159                            ),
7160                            acp::PermissionOptionKind::RejectOnce => (
7161                                Icon::new(IconName::Close)
7162                                    .size(IconSize::XSmall)
7163                                    .color(Color::Error),
7164                                Some(&RejectOnce as &dyn Action),
7165                            ),
7166                            acp::PermissionOptionKind::RejectAlways | _ => (
7167                                Icon::new(IconName::Close)
7168                                    .size(IconSize::XSmall)
7169                                    .color(Color::Error),
7170                                None,
7171                            ),
7172                        };
7173
7174                        let this = this.start_icon(icon);
7175
7176                        let Some(action) = action else {
7177                            return this;
7178                        };
7179
7180                        if !is_first || seen_kinds.contains(&option.kind) {
7181                            return this;
7182                        }
7183
7184                        seen_kinds.push(option.kind).unwrap();
7185
7186                        this.key_binding(
7187                            KeyBinding::for_action_in(action, focus_handle, cx)
7188                                .map(|kb| kb.size(rems_from_px(12.))),
7189                        )
7190                    })
7191                    .label_size(LabelSize::Small)
7192                    .on_click(cx.listener({
7193                        let tool_call_id = tool_call_id.clone();
7194                        let option_id = option.option_id.clone();
7195                        let option_kind = option.kind;
7196                        let session_id = session_id.clone();
7197                        move |this, _, window, cx| {
7198                            this.authorize_tool_call(
7199                                session_id.clone(),
7200                                tool_call_id.clone(),
7201                                SelectedPermissionOutcome::new(option_id.clone(), option_kind),
7202                                window,
7203                                cx,
7204                            );
7205                        }
7206                    }))
7207            }))
7208    }
7209
7210    fn render_diff_loading(&self, cx: &Context<Self>) -> AnyElement {
7211        let bar = |n: u64, width_class: &str| {
7212            let bg_color = cx.theme().colors().element_active;
7213            let base = h_flex().h_1().rounded_full();
7214
7215            let modified = match width_class {
7216                "w_4_5" => base.w_3_4(),
7217                "w_1_4" => base.w_1_4(),
7218                "w_2_4" => base.w_2_4(),
7219                "w_3_5" => base.w_3_5(),
7220                "w_2_5" => base.w_2_5(),
7221                _ => base.w_1_2(),
7222            };
7223
7224            modified.with_animation(
7225                ElementId::Integer(n),
7226                Animation::new(Duration::from_secs(2)).repeat(),
7227                move |tab, delta| {
7228                    let delta = (delta - 0.15 * n as f32) / 0.7;
7229                    let delta = 1.0 - (0.5 - delta).abs() * 2.;
7230                    let delta = ease_in_out(delta.clamp(0., 1.));
7231                    let delta = 0.1 + 0.9 * delta;
7232
7233                    tab.bg(bg_color.opacity(delta))
7234                },
7235            )
7236        };
7237
7238        v_flex()
7239            .p_3()
7240            .gap_1()
7241            .rounded_b_md()
7242            .bg(cx.theme().colors().editor_background)
7243            .child(bar(0, "w_4_5"))
7244            .child(bar(1, "w_1_4"))
7245            .child(bar(2, "w_2_4"))
7246            .child(bar(3, "w_3_5"))
7247            .child(bar(4, "w_2_5"))
7248            .into_any_element()
7249    }
7250
7251    fn render_tool_call_label(
7252        &self,
7253        entry_ix: usize,
7254        tool_call: &ToolCall,
7255        is_edit: bool,
7256        has_failed: bool,
7257        has_revealed_diff: bool,
7258        use_card_layout: bool,
7259        window: &Window,
7260        cx: &Context<Self>,
7261    ) -> Div {
7262        let has_location = tool_call.locations.len() == 1;
7263        let is_file = tool_call.kind == acp::ToolKind::Edit && has_location;
7264        let is_subagent_tool_call = tool_call.is_subagent();
7265
7266        let file_icon = if has_location {
7267            FileIcons::get_icon(&tool_call.locations[0].path, cx)
7268                .map(|from_path| Icon::from_path(from_path).color(Color::Muted))
7269                .unwrap_or(Icon::new(IconName::ToolPencil).color(Color::Muted))
7270        } else {
7271            Icon::new(IconName::ToolPencil).color(Color::Muted)
7272        };
7273
7274        let tool_icon = if is_file && has_failed && has_revealed_diff {
7275            div()
7276                .id(entry_ix)
7277                .tooltip(Tooltip::text("Interrupted Edit"))
7278                .child(DecoratedIcon::new(
7279                    file_icon,
7280                    Some(
7281                        IconDecoration::new(
7282                            IconDecorationKind::Triangle,
7283                            self.tool_card_header_bg(cx),
7284                            cx,
7285                        )
7286                        .color(cx.theme().status().warning)
7287                        .position(gpui::Point {
7288                            x: px(-2.),
7289                            y: px(-2.),
7290                        }),
7291                    ),
7292                ))
7293                .into_any_element()
7294        } else if is_file {
7295            div().child(file_icon).into_any_element()
7296        } else if is_subagent_tool_call {
7297            Icon::new(self.agent_icon)
7298                .size(IconSize::Small)
7299                .color(Color::Muted)
7300                .into_any_element()
7301        } else {
7302            Icon::new(match tool_call.kind {
7303                acp::ToolKind::Read => IconName::ToolSearch,
7304                acp::ToolKind::Edit => IconName::ToolPencil,
7305                acp::ToolKind::Delete => IconName::ToolDeleteFile,
7306                acp::ToolKind::Move => IconName::ArrowRightLeft,
7307                acp::ToolKind::Search => IconName::ToolSearch,
7308                acp::ToolKind::Execute => IconName::ToolTerminal,
7309                acp::ToolKind::Think => IconName::ToolThink,
7310                acp::ToolKind::Fetch => IconName::ToolWeb,
7311                acp::ToolKind::SwitchMode => IconName::ArrowRightLeft,
7312                acp::ToolKind::Other | _ => IconName::ToolHammer,
7313            })
7314            .size(IconSize::Small)
7315            .color(Color::Muted)
7316            .into_any_element()
7317        };
7318
7319        let gradient_overlay = {
7320            div()
7321                .absolute()
7322                .top_0()
7323                .right_0()
7324                .w_12()
7325                .h_full()
7326                .map(|this| {
7327                    if use_card_layout {
7328                        this.bg(linear_gradient(
7329                            90.,
7330                            linear_color_stop(self.tool_card_header_bg(cx), 1.),
7331                            linear_color_stop(self.tool_card_header_bg(cx).opacity(0.2), 0.),
7332                        ))
7333                    } else {
7334                        this.bg(linear_gradient(
7335                            90.,
7336                            linear_color_stop(cx.theme().colors().panel_background, 1.),
7337                            linear_color_stop(
7338                                cx.theme().colors().panel_background.opacity(0.2),
7339                                0.,
7340                            ),
7341                        ))
7342                    }
7343                })
7344        };
7345
7346        h_flex()
7347            .relative()
7348            .w_full()
7349            .h(window.line_height() - px(2.))
7350            .text_size(self.tool_name_font_size())
7351            .gap_1p5()
7352            .when(has_location || use_card_layout, |this| this.px_1())
7353            .when(has_location, |this| {
7354                this.cursor(CursorStyle::PointingHand)
7355                    .rounded(rems_from_px(3.)) // Concentric border radius
7356                    .hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.5)))
7357            })
7358            .overflow_hidden()
7359            .child(tool_icon)
7360            .child(if has_location {
7361                h_flex()
7362                    .id(("open-tool-call-location", entry_ix))
7363                    .w_full()
7364                    .map(|this| {
7365                        if use_card_layout {
7366                            this.text_color(cx.theme().colors().text)
7367                        } else {
7368                            this.text_color(cx.theme().colors().text_muted)
7369                        }
7370                    })
7371                    .child(
7372                        self.render_markdown(
7373                            tool_call.label.clone(),
7374                            MarkdownStyle {
7375                                prevent_mouse_interaction: true,
7376                                ..MarkdownStyle::themed(MarkdownFont::Agent, window, cx)
7377                                    .with_muted_text(cx)
7378                            },
7379                        ),
7380                    )
7381                    .tooltip(Tooltip::text("Go to File"))
7382                    .on_click(cx.listener(move |this, _, window, cx| {
7383                        this.open_tool_call_location(entry_ix, 0, window, cx);
7384                    }))
7385                    .into_any_element()
7386            } else {
7387                h_flex()
7388                    .w_full()
7389                    .child(self.render_markdown(
7390                        tool_call.label.clone(),
7391                        MarkdownStyle::themed(MarkdownFont::Agent, window, cx).with_muted_text(cx),
7392                    ))
7393                    .into_any()
7394            })
7395            .when(!is_edit, |this| this.child(gradient_overlay))
7396    }
7397
7398    fn open_tool_call_location(
7399        &self,
7400        entry_ix: usize,
7401        location_ix: usize,
7402        window: &mut Window,
7403        cx: &mut Context<Self>,
7404    ) -> Option<()> {
7405        let (tool_call_location, agent_location) = self
7406            .thread
7407            .read(cx)
7408            .entries()
7409            .get(entry_ix)?
7410            .location(location_ix)?;
7411
7412        let project_path = self
7413            .project
7414            .upgrade()?
7415            .read(cx)
7416            .find_project_path(&tool_call_location.path, cx)?;
7417
7418        let open_task = self
7419            .workspace
7420            .update(cx, |workspace, cx| {
7421                workspace.open_path(project_path, None, true, window, cx)
7422            })
7423            .log_err()?;
7424        window
7425            .spawn(cx, async move |cx| {
7426                let item = open_task.await?;
7427
7428                let Some(active_editor) = item.downcast::<Editor>() else {
7429                    return anyhow::Ok(());
7430                };
7431
7432                active_editor.update_in(cx, |editor, window, cx| {
7433                    let snapshot = editor.buffer().read(cx).snapshot(cx);
7434                    if snapshot.as_singleton().is_some()
7435                        && let Some(anchor) = snapshot.anchor_in_excerpt(agent_location.position)
7436                    {
7437                        editor.change_selections(Default::default(), window, cx, |selections| {
7438                            selections.select_anchor_ranges([anchor..anchor]);
7439                        })
7440                    } else {
7441                        let row = tool_call_location.line.unwrap_or_default();
7442                        editor.change_selections(Default::default(), window, cx, |selections| {
7443                            selections.select_ranges([Point::new(row, 0)..Point::new(row, 0)]);
7444                        })
7445                    }
7446                })?;
7447
7448                anyhow::Ok(())
7449            })
7450            .detach_and_log_err(cx);
7451
7452        None
7453    }
7454
7455    fn render_tool_call_content(
7456        &self,
7457        session_id: &acp::SessionId,
7458        entry_ix: usize,
7459        content: &ToolCallContent,
7460        context_ix: usize,
7461        tool_call: &ToolCall,
7462        card_layout: bool,
7463        is_image_tool_call: bool,
7464        has_failed: bool,
7465        focus_handle: &FocusHandle,
7466        window: &Window,
7467        cx: &Context<Self>,
7468    ) -> AnyElement {
7469        match content {
7470            ToolCallContent::ContentBlock(content) => {
7471                if let Some(resource_link) = content.resource_link() {
7472                    self.render_resource_link(resource_link, cx)
7473                } else if let Some(markdown) = content.markdown() {
7474                    self.render_markdown_output(
7475                        markdown.clone(),
7476                        tool_call.id.clone(),
7477                        context_ix,
7478                        card_layout,
7479                        window,
7480                        cx,
7481                    )
7482                } else if let Some(image) = content.image() {
7483                    let location = tool_call.locations.first().cloned();
7484                    self.render_image_output(
7485                        entry_ix,
7486                        image.clone(),
7487                        location,
7488                        card_layout,
7489                        is_image_tool_call,
7490                        cx,
7491                    )
7492                } else {
7493                    Empty.into_any_element()
7494                }
7495            }
7496            ToolCallContent::Diff(diff) => {
7497                self.render_diff_editor(entry_ix, diff, tool_call, has_failed, cx)
7498            }
7499            ToolCallContent::Terminal(terminal) => self.render_terminal_tool_call(
7500                session_id,
7501                entry_ix,
7502                terminal,
7503                tool_call,
7504                focus_handle,
7505                false,
7506                window,
7507                cx,
7508            ),
7509        }
7510    }
7511
7512    fn render_resource_link(
7513        &self,
7514        resource_link: &acp::ResourceLink,
7515        cx: &Context<Self>,
7516    ) -> AnyElement {
7517        let uri: SharedString = resource_link.uri.clone().into();
7518        let is_file = resource_link.uri.strip_prefix("file://");
7519
7520        let Some(project) = self.project.upgrade() else {
7521            return Empty.into_any_element();
7522        };
7523
7524        let label: SharedString = if let Some(abs_path) = is_file {
7525            if let Some(project_path) = project
7526                .read(cx)
7527                .project_path_for_absolute_path(&Path::new(abs_path), cx)
7528                && let Some(worktree) = project
7529                    .read(cx)
7530                    .worktree_for_id(project_path.worktree_id, cx)
7531            {
7532                worktree
7533                    .read(cx)
7534                    .full_path(&project_path.path)
7535                    .to_string_lossy()
7536                    .to_string()
7537                    .into()
7538            } else {
7539                abs_path.to_string().into()
7540            }
7541        } else {
7542            uri.clone()
7543        };
7544
7545        let button_id = SharedString::from(format!("item-{}", uri));
7546
7547        div()
7548            .ml(rems(0.4))
7549            .pl_2p5()
7550            .border_l_1()
7551            .border_color(self.tool_card_border_color(cx))
7552            .overflow_hidden()
7553            .child(
7554                Button::new(button_id, label)
7555                    .label_size(LabelSize::Small)
7556                    .color(Color::Muted)
7557                    .truncate(true)
7558                    .when(is_file.is_none(), |this| {
7559                        this.end_icon(
7560                            Icon::new(IconName::ArrowUpRight)
7561                                .size(IconSize::XSmall)
7562                                .color(Color::Muted),
7563                        )
7564                    })
7565                    .on_click(cx.listener({
7566                        let workspace = self.workspace.clone();
7567                        move |_, _, window, cx: &mut Context<Self>| {
7568                            open_link(uri.clone(), &workspace, window, cx);
7569                        }
7570                    })),
7571            )
7572            .into_any_element()
7573    }
7574
7575    fn render_diff_editor(
7576        &self,
7577        entry_ix: usize,
7578        diff: &Entity<acp_thread::Diff>,
7579        tool_call: &ToolCall,
7580        has_failed: bool,
7581        cx: &Context<Self>,
7582    ) -> AnyElement {
7583        let tool_progress = matches!(
7584            &tool_call.status,
7585            ToolCallStatus::InProgress | ToolCallStatus::Pending
7586        );
7587
7588        let revealed_diff_editor = if let Some(entry) =
7589            self.entry_view_state.read(cx).entry(entry_ix)
7590            && let Some(editor) = entry.editor_for_diff(diff)
7591            && diff.read(cx).has_revealed_range(cx)
7592        {
7593            Some(editor)
7594        } else {
7595            None
7596        };
7597
7598        let show_top_border = !has_failed || revealed_diff_editor.is_some();
7599
7600        v_flex()
7601            .h_full()
7602            .when(show_top_border, |this| {
7603                this.border_t_1()
7604                    .when(has_failed, |this| this.border_dashed())
7605                    .border_color(self.tool_card_border_color(cx))
7606            })
7607            .child(if let Some(editor) = revealed_diff_editor {
7608                editor.into_any_element()
7609            } else if tool_progress && self.as_native_connection(cx).is_some() {
7610                self.render_diff_loading(cx)
7611            } else {
7612                Empty.into_any()
7613            })
7614            .into_any()
7615    }
7616
7617    fn render_markdown_output(
7618        &self,
7619        markdown: Entity<Markdown>,
7620        tool_call_id: acp::ToolCallId,
7621        context_ix: usize,
7622        card_layout: bool,
7623        window: &Window,
7624        cx: &Context<Self>,
7625    ) -> AnyElement {
7626        let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id));
7627
7628        v_flex()
7629            .gap_2()
7630            .map(|this| {
7631                if card_layout {
7632                    this.p_2().when(context_ix > 0, |this| {
7633                        this.border_t_1()
7634                            .border_color(self.tool_card_border_color(cx))
7635                    })
7636                } else {
7637                    this.ml(rems(0.4))
7638                        .px_3p5()
7639                        .border_l_1()
7640                        .border_color(self.tool_card_border_color(cx))
7641                }
7642            })
7643            .text_xs()
7644            .text_color(cx.theme().colors().text_muted)
7645            .child(self.render_markdown(
7646                markdown,
7647                MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
7648            ))
7649            .when(!card_layout, |this| {
7650                this.child(
7651                    IconButton::new(button_id, IconName::ChevronUp)
7652                        .full_width()
7653                        .style(ButtonStyle::Outlined)
7654                        .icon_color(Color::Muted)
7655                        .on_click(cx.listener({
7656                            move |this: &mut Self, _, _, cx: &mut Context<Self>| {
7657                                this.expanded_tool_calls.remove(&tool_call_id);
7658                                cx.notify();
7659                            }
7660                        })),
7661                )
7662            })
7663            .into_any_element()
7664    }
7665
7666    fn render_image_output(
7667        &self,
7668        entry_ix: usize,
7669        image: Arc<gpui::Image>,
7670        location: Option<acp::ToolCallLocation>,
7671        card_layout: bool,
7672        show_dimensions: bool,
7673        cx: &Context<Self>,
7674    ) -> AnyElement {
7675        let dimensions_label = if show_dimensions {
7676            let format_name = match image.format() {
7677                gpui::ImageFormat::Png => "PNG",
7678                gpui::ImageFormat::Jpeg => "JPEG",
7679                gpui::ImageFormat::Webp => "WebP",
7680                gpui::ImageFormat::Gif => "GIF",
7681                gpui::ImageFormat::Svg => "SVG",
7682                gpui::ImageFormat::Bmp => "BMP",
7683                gpui::ImageFormat::Tiff => "TIFF",
7684                gpui::ImageFormat::Ico => "ICO",
7685            };
7686            let dimensions = image::ImageReader::new(std::io::Cursor::new(image.bytes()))
7687                .with_guessed_format()
7688                .ok()
7689                .and_then(|reader| reader.into_dimensions().ok());
7690            dimensions.map(|(w, h)| format!("{}×{} {}", w, h, format_name))
7691        } else {
7692            None
7693        };
7694
7695        v_flex()
7696            .gap_2()
7697            .map(|this| {
7698                if card_layout {
7699                    this
7700                } else {
7701                    this.ml(rems(0.4))
7702                        .px_3p5()
7703                        .border_l_1()
7704                        .border_color(self.tool_card_border_color(cx))
7705                }
7706            })
7707            .when(dimensions_label.is_some() || location.is_some(), |this| {
7708                this.child(
7709                    h_flex()
7710                        .w_full()
7711                        .justify_between()
7712                        .items_center()
7713                        .children(dimensions_label.map(|label| {
7714                            Label::new(label)
7715                                .size(LabelSize::XSmall)
7716                                .color(Color::Muted)
7717                                .buffer_font(cx)
7718                        }))
7719                        .when_some(location, |this, _loc| {
7720                            this.child(
7721                                Button::new(("go-to-file", entry_ix), "Go to File")
7722                                    .label_size(LabelSize::Small)
7723                                    .on_click(cx.listener(move |this, _, window, cx| {
7724                                        this.open_tool_call_location(entry_ix, 0, window, cx);
7725                                    })),
7726                            )
7727                        }),
7728                )
7729            })
7730            .child(
7731                img(image)
7732                    .max_w_96()
7733                    .max_h_96()
7734                    .object_fit(ObjectFit::ScaleDown),
7735            )
7736            .into_any_element()
7737    }
7738
7739    fn render_subagent_tool_call(
7740        &self,
7741        active_session_id: &acp::SessionId,
7742        entry_ix: usize,
7743        tool_call: &ToolCall,
7744        subagent_session_id: Option<acp::SessionId>,
7745        focus_handle: &FocusHandle,
7746        window: &Window,
7747        cx: &Context<Self>,
7748    ) -> Div {
7749        let subagent_thread_view = subagent_session_id.and_then(|session_id| {
7750            self.server_view
7751                .upgrade()
7752                .and_then(|server_view| server_view.read(cx).as_connected())
7753                .and_then(|connected| connected.threads.get(&session_id))
7754        });
7755
7756        let content = self.render_subagent_card(
7757            active_session_id,
7758            entry_ix,
7759            subagent_thread_view,
7760            tool_call,
7761            focus_handle,
7762            window,
7763            cx,
7764        );
7765
7766        v_flex().mx_5().my_1p5().gap_3().child(content)
7767    }
7768
7769    fn render_subagent_card(
7770        &self,
7771        active_session_id: &acp::SessionId,
7772        entry_ix: usize,
7773        thread_view: Option<&Entity<ThreadView>>,
7774        tool_call: &ToolCall,
7775        focus_handle: &FocusHandle,
7776        window: &Window,
7777        cx: &Context<Self>,
7778    ) -> AnyElement {
7779        let thread = thread_view
7780            .as_ref()
7781            .map(|view| view.read(cx).thread.clone());
7782        let subagent_session_id = thread
7783            .as_ref()
7784            .map(|thread| thread.read(cx).session_id().clone());
7785        let action_log = thread.as_ref().map(|thread| thread.read(cx).action_log());
7786        let changed_buffers = action_log
7787            .map(|log| log.read(cx).changed_buffers(cx))
7788            .unwrap_or_default();
7789
7790        let is_pending_tool_call = thread_view
7791            .as_ref()
7792            .and_then(|tv| {
7793                let sid = tv.read(cx).thread.read(cx).session_id();
7794                self.conversation.read(cx).pending_tool_call(sid, cx)
7795            })
7796            .is_some();
7797
7798        let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
7799        let files_changed = changed_buffers.len();
7800        let diff_stats = DiffStats::all_files(&changed_buffers, cx);
7801
7802        let is_running = matches!(
7803            tool_call.status,
7804            ToolCallStatus::Pending
7805                | ToolCallStatus::InProgress
7806                | ToolCallStatus::WaitingForConfirmation { .. }
7807        );
7808
7809        let is_failed = matches!(
7810            tool_call.status,
7811            ToolCallStatus::Failed | ToolCallStatus::Rejected
7812        );
7813
7814        let is_cancelled = matches!(tool_call.status, ToolCallStatus::Canceled)
7815            || tool_call.content.iter().any(|c| match c {
7816                ToolCallContent::ContentBlock(ContentBlock::Markdown { markdown }) => {
7817                    markdown.read(cx).source() == "User canceled"
7818                }
7819                _ => false,
7820            });
7821
7822        let thread_title = thread
7823            .as_ref()
7824            .and_then(|t| t.read(cx).title())
7825            .filter(|t| !t.is_empty());
7826        let tool_call_label = tool_call.label.read(cx).source().to_string();
7827        let has_tool_call_label = !tool_call_label.is_empty();
7828
7829        let has_title = thread_title.is_some() || has_tool_call_label;
7830        let has_no_title_or_canceled = !has_title || is_failed || is_cancelled;
7831
7832        let title: SharedString = if let Some(thread_title) = thread_title {
7833            thread_title
7834        } else if !tool_call_label.is_empty() {
7835            tool_call_label.into()
7836        } else if is_cancelled {
7837            "Subagent Canceled".into()
7838        } else if is_failed {
7839            "Subagent Failed".into()
7840        } else {
7841            "Spawning Agent…".into()
7842        };
7843
7844        let card_header_id = format!("subagent-header-{}", entry_ix);
7845        let status_icon = format!("status-icon-{}", entry_ix);
7846        let diff_stat_id = format!("subagent-diff-{}", entry_ix);
7847
7848        let icon = h_flex().w_4().justify_center().child(if is_running {
7849            SpinnerLabel::new()
7850                .size(LabelSize::Small)
7851                .into_any_element()
7852        } else if is_cancelled {
7853            div()
7854                .id(status_icon)
7855                .child(
7856                    Icon::new(IconName::Circle)
7857                        .size(IconSize::Small)
7858                        .color(Color::Custom(
7859                            cx.theme().colors().icon_disabled.opacity(0.5),
7860                        )),
7861                )
7862                .tooltip(Tooltip::text("Subagent Cancelled"))
7863                .into_any_element()
7864        } else if is_failed {
7865            div()
7866                .id(status_icon)
7867                .child(
7868                    Icon::new(IconName::Close)
7869                        .size(IconSize::Small)
7870                        .color(Color::Error),
7871                )
7872                .tooltip(Tooltip::text("Subagent Failed"))
7873                .into_any_element()
7874        } else {
7875            Icon::new(IconName::Check)
7876                .size(IconSize::Small)
7877                .color(Color::Success)
7878                .into_any_element()
7879        });
7880
7881        let has_expandable_content = thread
7882            .as_ref()
7883            .map_or(false, |thread| !thread.read(cx).entries().is_empty());
7884
7885        let tooltip_meta_description = if is_expanded {
7886            "Click to Collapse"
7887        } else {
7888            "Click to Preview"
7889        };
7890
7891        let error_message = self.subagent_error_message(&tool_call.status, tool_call, cx);
7892
7893        v_flex()
7894            .w_full()
7895            .rounded_md()
7896            .border_1()
7897            .when(has_no_title_or_canceled, |this| this.border_dashed())
7898            .border_color(self.tool_card_border_color(cx))
7899            .overflow_hidden()
7900            .child(
7901                h_flex()
7902                    .group(&card_header_id)
7903                    .h_8()
7904                    .p_1()
7905                    .w_full()
7906                    .justify_between()
7907                    .when(!has_no_title_or_canceled, |this| {
7908                        this.bg(self.tool_card_header_bg(cx))
7909                    })
7910                    .child(
7911                        h_flex()
7912                            .id(format!("subagent-title-{}", entry_ix))
7913                            .px_1()
7914                            .min_w_0()
7915                            .size_full()
7916                            .gap_2()
7917                            .justify_between()
7918                            .rounded_sm()
7919                            .overflow_hidden()
7920                            .child(
7921                                h_flex()
7922                                    .min_w_0()
7923                                    .w_full()
7924                                    .gap_1p5()
7925                                    .child(icon)
7926                                    .child(
7927                                        Label::new(title.to_string())
7928                                            .size(LabelSize::Custom(self.tool_name_font_size()))
7929                                            .truncate(),
7930                                    )
7931                                    .when(files_changed > 0, |this| {
7932                                        this.child(
7933                                            Label::new(format!(
7934                                                "{} {} changed",
7935                                                files_changed,
7936                                                if files_changed == 1 { "file" } else { "files" }
7937                                            ))
7938                                            .size(LabelSize::Custom(self.tool_name_font_size()))
7939                                            .color(Color::Muted),
7940                                        )
7941                                        .child(
7942                                            DiffStat::new(
7943                                                diff_stat_id.clone(),
7944                                                diff_stats.lines_added as usize,
7945                                                diff_stats.lines_removed as usize,
7946                                            )
7947                                            .label_size(LabelSize::Custom(
7948                                                self.tool_name_font_size(),
7949                                            )),
7950                                        )
7951                                    }),
7952                            )
7953                            .when(!has_no_title_or_canceled && !is_pending_tool_call, |this| {
7954                                this.tooltip(move |_, cx| {
7955                                    Tooltip::with_meta(
7956                                        title.to_string(),
7957                                        None,
7958                                        tooltip_meta_description,
7959                                        cx,
7960                                    )
7961                                })
7962                            })
7963                            .when(has_expandable_content && !is_pending_tool_call, |this| {
7964                                this.cursor_pointer()
7965                                    .hover(|s| s.bg(cx.theme().colors().element_hover))
7966                                    .child(
7967                                        div().visible_on_hover(card_header_id).child(
7968                                            Icon::new(if is_expanded {
7969                                                IconName::ChevronUp
7970                                            } else {
7971                                                IconName::ChevronDown
7972                                            })
7973                                            .color(Color::Muted)
7974                                            .size(IconSize::Small),
7975                                        ),
7976                                    )
7977                                    .on_click(cx.listener({
7978                                        let tool_call_id = tool_call.id.clone();
7979                                        move |this, _, _, cx| {
7980                                            if this.expanded_tool_calls.contains(&tool_call_id) {
7981                                                this.expanded_tool_calls.remove(&tool_call_id);
7982                                            } else {
7983                                                this.expanded_tool_calls
7984                                                    .insert(tool_call_id.clone());
7985                                            }
7986                                            let expanded =
7987                                                this.expanded_tool_calls.contains(&tool_call_id);
7988                                            telemetry::event!("Subagent Toggled", expanded);
7989                                            cx.notify();
7990                                        }
7991                                    }))
7992                            }),
7993                    )
7994                    .when(is_running && subagent_session_id.is_some(), |buttons| {
7995                        buttons.child(
7996                            IconButton::new(format!("stop-subagent-{}", entry_ix), IconName::Stop)
7997                                .icon_size(IconSize::Small)
7998                                .icon_color(Color::Error)
7999                                .tooltip(Tooltip::text("Stop Subagent"))
8000                                .when_some(
8001                                    thread_view
8002                                        .as_ref()
8003                                        .map(|view| view.read(cx).thread.clone()),
8004                                    |this, thread| {
8005                                        this.on_click(cx.listener(
8006                                            move |_this, _event, _window, cx| {
8007                                                telemetry::event!("Subagent Stopped");
8008                                                thread.update(cx, |thread, cx| {
8009                                                    thread.cancel(cx).detach();
8010                                                });
8011                                            },
8012                                        ))
8013                                    },
8014                                ),
8015                        )
8016                    }),
8017            )
8018            .when_some(thread_view, |this, thread_view| {
8019                let thread = &thread_view.read(cx).thread;
8020                let tv_session_id = thread.read(cx).session_id();
8021                let pending_tool_call = self
8022                    .conversation
8023                    .read(cx)
8024                    .pending_tool_call(tv_session_id, cx);
8025
8026                let nav_session_id = tv_session_id.clone();
8027
8028                let fullscreen_toggle = h_flex()
8029                    .id(entry_ix)
8030                    .py_1()
8031                    .w_full()
8032                    .justify_center()
8033                    .border_t_1()
8034                    .when(is_failed, |this| this.border_dashed())
8035                    .border_color(self.tool_card_border_color(cx))
8036                    .cursor_pointer()
8037                    .hover(|s| s.bg(cx.theme().colors().element_hover))
8038                    .child(
8039                        Icon::new(IconName::Maximize)
8040                            .color(Color::Muted)
8041                            .size(IconSize::Small),
8042                    )
8043                    .tooltip(Tooltip::text("Make Subagent Full Screen"))
8044                    .on_click(cx.listener(move |this, _event, window, cx| {
8045                        telemetry::event!("Subagent Maximized");
8046                        this.server_view
8047                            .update(cx, |this, cx| {
8048                                this.navigate_to_thread(nav_session_id.clone(), window, cx);
8049                            })
8050                            .ok();
8051                    }));
8052
8053                if is_running && let Some((_, subagent_tool_call_id, _)) = pending_tool_call {
8054                    if let Some((entry_ix, tool_call)) =
8055                        thread.read(cx).tool_call(&subagent_tool_call_id)
8056                    {
8057                        this.child(Divider::horizontal().color(DividerColor::Border))
8058                            .child(thread_view.read(cx).render_any_tool_call(
8059                                active_session_id,
8060                                entry_ix,
8061                                tool_call,
8062                                focus_handle,
8063                                true,
8064                                window,
8065                                cx,
8066                            ))
8067                            .child(fullscreen_toggle)
8068                    } else {
8069                        this
8070                    }
8071                } else {
8072                    this.when(is_expanded, |this| {
8073                        this.child(self.render_subagent_expanded_content(
8074                            thread_view,
8075                            tool_call,
8076                            window,
8077                            cx,
8078                        ))
8079                        .when_some(error_message, |this, message| {
8080                            this.child(
8081                                Callout::new()
8082                                    .severity(Severity::Error)
8083                                    .icon(IconName::XCircle)
8084                                    .title(message),
8085                            )
8086                        })
8087                        .child(fullscreen_toggle)
8088                    })
8089                }
8090            })
8091            .into_any_element()
8092    }
8093
8094    fn render_subagent_expanded_content(
8095        &self,
8096        thread_view: &Entity<ThreadView>,
8097        tool_call: &ToolCall,
8098        window: &Window,
8099        cx: &Context<Self>,
8100    ) -> impl IntoElement {
8101        const MAX_PREVIEW_ENTRIES: usize = 8;
8102
8103        let subagent_view = thread_view.read(cx);
8104        let session_id = subagent_view.thread.read(cx).session_id().clone();
8105
8106        let is_canceled_or_failed = matches!(
8107            tool_call.status,
8108            ToolCallStatus::Canceled | ToolCallStatus::Failed | ToolCallStatus::Rejected
8109        );
8110
8111        let editor_bg = cx.theme().colors().editor_background;
8112        let overlay = {
8113            div()
8114                .absolute()
8115                .inset_0()
8116                .size_full()
8117                .bg(linear_gradient(
8118                    180.,
8119                    linear_color_stop(editor_bg.opacity(0.5), 0.),
8120                    linear_color_stop(editor_bg.opacity(0.), 0.1),
8121                ))
8122                .block_mouse_except_scroll()
8123        };
8124
8125        let entries = subagent_view.thread.read(cx).entries();
8126        let total_entries = entries.len();
8127        let mut entry_range = if let Some(info) = tool_call.subagent_session_info.as_ref() {
8128            info.message_start_index
8129                ..info
8130                    .message_end_index
8131                    .map(|i| (i + 1).min(total_entries))
8132                    .unwrap_or(total_entries)
8133        } else {
8134            0..total_entries
8135        };
8136        entry_range.start = entry_range
8137            .end
8138            .saturating_sub(MAX_PREVIEW_ENTRIES)
8139            .max(entry_range.start);
8140        let start_ix = entry_range.start;
8141
8142        let scroll_handle = self
8143            .subagent_scroll_handles
8144            .borrow_mut()
8145            .entry(subagent_view.session_id.clone())
8146            .or_default()
8147            .clone();
8148
8149        scroll_handle.scroll_to_bottom();
8150
8151        let rendered_entries: Vec<AnyElement> = entries
8152            .get(entry_range)
8153            .unwrap_or_default()
8154            .iter()
8155            .enumerate()
8156            .map(|(i, entry)| {
8157                let actual_ix = start_ix + i;
8158                subagent_view.render_entry(actual_ix, total_entries, entry, window, cx)
8159            })
8160            .collect();
8161
8162        v_flex()
8163            .w_full()
8164            .border_t_1()
8165            .when(is_canceled_or_failed, |this| this.border_dashed())
8166            .border_color(self.tool_card_border_color(cx))
8167            .overflow_hidden()
8168            .child(
8169                div()
8170                    .pb_1()
8171                    .min_h_0()
8172                    .id(format!("subagent-entries-{}", session_id))
8173                    .track_scroll(&scroll_handle)
8174                    .children(rendered_entries),
8175            )
8176            .h_56()
8177            .child(overlay)
8178            .into_any_element()
8179    }
8180
8181    fn subagent_error_message(
8182        &self,
8183        status: &ToolCallStatus,
8184        tool_call: &ToolCall,
8185        cx: &App,
8186    ) -> Option<SharedString> {
8187        if matches!(status, ToolCallStatus::Failed) {
8188            tool_call.content.iter().find_map(|content| {
8189                if let ToolCallContent::ContentBlock(block) = content {
8190                    if let acp_thread::ContentBlock::Markdown { markdown } = block {
8191                        let source = markdown.read(cx).source().to_string();
8192                        if !source.is_empty() {
8193                            if source == "User canceled" {
8194                                return None;
8195                            } else {
8196                                return Some(SharedString::from(source));
8197                            }
8198                        }
8199                    }
8200                }
8201                None
8202            })
8203        } else {
8204            None
8205        }
8206    }
8207
8208    fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
8209        cx.theme()
8210            .colors()
8211            .element_background
8212            .blend(cx.theme().colors().editor_foreground.opacity(0.025))
8213    }
8214
8215    fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
8216        cx.theme().colors().border.opacity(0.8)
8217    }
8218
8219    fn tool_name_font_size(&self) -> Rems {
8220        rems_from_px(13.)
8221    }
8222
8223    pub(crate) fn render_thread_error(
8224        &mut self,
8225        window: &mut Window,
8226        cx: &mut Context<Self>,
8227    ) -> Option<Div> {
8228        let content = match self.thread_error.as_ref()? {
8229            ThreadError::Other { message, .. } => {
8230                self.render_any_thread_error(message.clone(), window, cx)
8231            }
8232            ThreadError::Refusal => self.render_refusal_error(cx),
8233            ThreadError::AuthenticationRequired(error) => {
8234                self.render_authentication_required_error(error.clone(), cx)
8235            }
8236            ThreadError::PaymentRequired => self.render_payment_required_error(cx),
8237            ThreadError::RateLimitExceeded { provider } => self.render_error_callout(
8238                "Rate Limit Reached",
8239                format!(
8240                    "{provider}'s rate limit was reached. Zed will retry automatically. \
8241                    You can also wait a moment and try again."
8242                )
8243                .into(),
8244                true,
8245                true,
8246                cx,
8247            ),
8248            ThreadError::ServerOverloaded { provider } => self.render_error_callout(
8249                "Provider Unavailable",
8250                format!(
8251                    "{provider}'s servers are temporarily unavailable. Zed will retry \
8252                    automatically. If the problem persists, check the provider's status page."
8253                )
8254                .into(),
8255                true,
8256                true,
8257                cx,
8258            ),
8259            ThreadError::PromptTooLarge => self.render_prompt_too_large_error(cx),
8260            ThreadError::NoApiKey { provider } => self.render_error_callout(
8261                "API Key Missing",
8262                format!(
8263                    "No API key is configured for {provider}. \
8264                    Add your key via the Agent Panel settings to continue."
8265                )
8266                .into(),
8267                false,
8268                true,
8269                cx,
8270            ),
8271            ThreadError::StreamError { provider } => self.render_error_callout(
8272                "Connection Interrupted",
8273                format!(
8274                    "The connection to {provider}'s API was interrupted. Zed will retry \
8275                    automatically. If the problem persists, check your network connection."
8276                )
8277                .into(),
8278                true,
8279                true,
8280                cx,
8281            ),
8282            ThreadError::InvalidApiKey { provider } => self.render_error_callout(
8283                "Invalid API Key",
8284                format!(
8285                    "The API key for {provider} is invalid or has expired. \
8286                    Update your key via the Agent Panel settings to continue."
8287                )
8288                .into(),
8289                false,
8290                false,
8291                cx,
8292            ),
8293            ThreadError::PermissionDenied { provider } => self.render_error_callout(
8294                "Permission Denied",
8295                format!(
8296                    "{provider}'s API rejected the request due to insufficient permissions. \
8297                    Check that your API key has access to this model."
8298                )
8299                .into(),
8300                false,
8301                false,
8302                cx,
8303            ),
8304            ThreadError::RequestFailed => self.render_error_callout(
8305                "Request Failed",
8306                "The request could not be completed after multiple attempts. \
8307                Try again in a moment."
8308                    .into(),
8309                true,
8310                false,
8311                cx,
8312            ),
8313            ThreadError::MaxOutputTokens => self.render_error_callout(
8314                "Output Limit Reached",
8315                "The model stopped because it reached its maximum output length. \
8316                You can ask it to continue where it left off."
8317                    .into(),
8318                false,
8319                false,
8320                cx,
8321            ),
8322            ThreadError::NoModelSelected => self.render_error_callout(
8323                "No Model Selected",
8324                "Select a model from the model picker below to get started.".into(),
8325                false,
8326                false,
8327                cx,
8328            ),
8329            ThreadError::ApiError { provider } => self.render_error_callout(
8330                "API Error",
8331                format!(
8332                    "{provider}'s API returned an unexpected error. \
8333                    If the problem persists, try switching models or restarting Zed."
8334                )
8335                .into(),
8336                true,
8337                true,
8338                cx,
8339            ),
8340        };
8341
8342        Some(div().child(content))
8343    }
8344
8345    fn render_refusal_error(&self, cx: &mut Context<'_, Self>) -> Callout {
8346        let model_or_agent_name = self.current_model_name(cx);
8347        let refusal_message = format!(
8348            "{} refused to respond to this prompt. \
8349            This can happen when a model believes the prompt violates its content policy \
8350            or safety guidelines, so rephrasing it can sometimes address the issue.",
8351            model_or_agent_name
8352        );
8353
8354        Callout::new()
8355            .severity(Severity::Error)
8356            .title("Request Refused")
8357            .icon(IconName::XCircle)
8358            .description(refusal_message.clone())
8359            .actions_slot(self.create_copy_button(&refusal_message))
8360            .dismiss_action(self.dismiss_error_button(cx))
8361    }
8362
8363    fn render_authentication_required_error(
8364        &self,
8365        error: SharedString,
8366        cx: &mut Context<Self>,
8367    ) -> Callout {
8368        Callout::new()
8369            .severity(Severity::Error)
8370            .title("Authentication Required")
8371            .icon(IconName::XCircle)
8372            .description(error.clone())
8373            .actions_slot(
8374                h_flex()
8375                    .gap_0p5()
8376                    .child(self.authenticate_button(cx))
8377                    .child(self.create_copy_button(error)),
8378            )
8379            .dismiss_action(self.dismiss_error_button(cx))
8380    }
8381
8382    fn render_payment_required_error(&self, cx: &mut Context<Self>) -> Callout {
8383        const ERROR_MESSAGE: &str =
8384            "You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
8385
8386        Callout::new()
8387            .severity(Severity::Error)
8388            .icon(IconName::XCircle)
8389            .title("Free Usage Exceeded")
8390            .description(ERROR_MESSAGE)
8391            .actions_slot(
8392                h_flex()
8393                    .gap_0p5()
8394                    .child(self.upgrade_button(cx))
8395                    .child(self.create_copy_button(ERROR_MESSAGE)),
8396            )
8397            .dismiss_action(self.dismiss_error_button(cx))
8398    }
8399
8400    fn render_error_callout(
8401        &self,
8402        title: &'static str,
8403        message: SharedString,
8404        show_retry: bool,
8405        show_copy: bool,
8406        cx: &mut Context<Self>,
8407    ) -> Callout {
8408        let can_resume = show_retry && self.thread.read(cx).can_retry(cx);
8409        let show_actions = can_resume || show_copy;
8410
8411        Callout::new()
8412            .severity(Severity::Error)
8413            .icon(IconName::XCircle)
8414            .title(title)
8415            .description(message.clone())
8416            .when(show_actions, |callout| {
8417                callout.actions_slot(
8418                    h_flex()
8419                        .gap_0p5()
8420                        .when(can_resume, |this| this.child(self.retry_button(cx)))
8421                        .when(show_copy, |this| {
8422                            this.child(self.create_copy_button(message.clone()))
8423                        }),
8424                )
8425            })
8426            .dismiss_action(self.dismiss_error_button(cx))
8427    }
8428
8429    fn render_prompt_too_large_error(&self, cx: &mut Context<Self>) -> Callout {
8430        const MESSAGE: &str = "This conversation is too long for the model's context window. \
8431            Start a new thread or remove some attached files to continue.";
8432
8433        Callout::new()
8434            .severity(Severity::Error)
8435            .icon(IconName::XCircle)
8436            .title("Context Too Large")
8437            .description(MESSAGE)
8438            .actions_slot(
8439                h_flex()
8440                    .gap_0p5()
8441                    .child(self.new_thread_button(cx))
8442                    .child(self.create_copy_button(MESSAGE)),
8443            )
8444            .dismiss_action(self.dismiss_error_button(cx))
8445    }
8446
8447    fn retry_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
8448        Button::new("retry", "Retry")
8449            .label_size(LabelSize::Small)
8450            .style(ButtonStyle::Filled)
8451            .on_click(cx.listener(|this, _, _, cx| {
8452                this.retry_generation(cx);
8453            }))
8454    }
8455
8456    fn new_thread_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
8457        Button::new("new_thread", "New Thread")
8458            .label_size(LabelSize::Small)
8459            .style(ButtonStyle::Filled)
8460            .on_click(cx.listener(|this, _, window, cx| {
8461                this.clear_thread_error(cx);
8462                window.dispatch_action(NewThread.boxed_clone(), cx);
8463            }))
8464    }
8465
8466    fn upgrade_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
8467        Button::new("upgrade", "Upgrade")
8468            .label_size(LabelSize::Small)
8469            .style(ButtonStyle::Tinted(ui::TintColor::Accent))
8470            .on_click(cx.listener({
8471                move |this, _, _, cx| {
8472                    this.clear_thread_error(cx);
8473                    cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx));
8474                }
8475            }))
8476    }
8477
8478    fn authenticate_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
8479        Button::new("authenticate", "Authenticate")
8480            .label_size(LabelSize::Small)
8481            .style(ButtonStyle::Filled)
8482            .on_click(cx.listener({
8483                move |this, _, window, cx| {
8484                    let server_view = this.server_view.clone();
8485                    let agent_name = this.agent_id.clone();
8486
8487                    this.clear_thread_error(cx);
8488                    if let Some(message) = this.in_flight_prompt.take() {
8489                        this.message_editor.update(cx, |editor, cx| {
8490                            editor.set_message(message, window, cx);
8491                        });
8492                    }
8493                    let connection = this.thread.read(cx).connection().clone();
8494                    window.defer(cx, |window, cx| {
8495                        ConversationView::handle_auth_required(
8496                            server_view,
8497                            AuthRequired::new(),
8498                            agent_name,
8499                            connection,
8500                            window,
8501                            cx,
8502                        );
8503                    })
8504                }
8505            }))
8506    }
8507
8508    fn current_model_name(&self, cx: &App) -> SharedString {
8509        // For native agent (Zed Agent), use the specific model name (e.g., "Claude 3.5 Sonnet")
8510        // For ACP agents, use the agent name (e.g., "Claude Agent", "Gemini CLI")
8511        // This provides better clarity about what refused the request
8512        if self.as_native_connection(cx).is_some() {
8513            self.model_selector
8514                .clone()
8515                .and_then(|selector| selector.read(cx).active_model(cx))
8516                .map(|model| model.name.clone())
8517                .unwrap_or_else(|| SharedString::from("The model"))
8518        } else {
8519            // ACP agent - use the agent name (e.g., "Claude Agent", "Gemini CLI")
8520            self.agent_id.0.clone()
8521        }
8522    }
8523
8524    fn render_any_thread_error(
8525        &mut self,
8526        error: SharedString,
8527        window: &mut Window,
8528        cx: &mut Context<'_, Self>,
8529    ) -> Callout {
8530        let can_resume = self.thread.read(cx).can_retry(cx);
8531
8532        let markdown = if let Some(markdown) = &self.thread_error_markdown {
8533            markdown.clone()
8534        } else {
8535            let markdown = cx.new(|cx| Markdown::new(error.clone(), None, None, cx));
8536            self.thread_error_markdown = Some(markdown.clone());
8537            markdown
8538        };
8539
8540        let markdown_style =
8541            MarkdownStyle::themed(MarkdownFont::Agent, window, cx).with_muted_text(cx);
8542        let description = self
8543            .render_markdown(markdown, markdown_style)
8544            .into_any_element();
8545
8546        Callout::new()
8547            .severity(Severity::Error)
8548            .icon(IconName::XCircle)
8549            .title("An Error Happened")
8550            .description_slot(description)
8551            .actions_slot(
8552                h_flex()
8553                    .gap_0p5()
8554                    .when(can_resume, |this| {
8555                        this.child(
8556                            IconButton::new("retry", IconName::RotateCw)
8557                                .icon_size(IconSize::Small)
8558                                .tooltip(Tooltip::text("Retry Generation"))
8559                                .on_click(cx.listener(|this, _, _window, cx| {
8560                                    this.retry_generation(cx);
8561                                })),
8562                        )
8563                    })
8564                    .child(self.create_copy_button(error.to_string())),
8565            )
8566            .dismiss_action(self.dismiss_error_button(cx))
8567    }
8568
8569    fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
8570        let workspace = self.workspace.clone();
8571        MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
8572            open_link(text, &workspace, window, cx);
8573        })
8574    }
8575
8576    fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
8577        let message = message.into();
8578
8579        CopyButton::new("copy-error-message", message).tooltip_label("Copy Error Message")
8580    }
8581
8582    fn dismiss_error_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
8583        IconButton::new("dismiss", IconName::Close)
8584            .icon_size(IconSize::Small)
8585            .tooltip(Tooltip::text("Dismiss"))
8586            .on_click(cx.listener({
8587                move |this, _, _, cx| {
8588                    this.clear_thread_error(cx);
8589                    cx.notify();
8590                }
8591            }))
8592    }
8593
8594    fn render_resume_notice(_cx: &Context<Self>) -> AnyElement {
8595        let description = "This agent does not support viewing previous messages. However, your session will still continue from where you last left off.";
8596
8597        Callout::new()
8598            .border_position(ui::BorderPosition::Bottom)
8599            .severity(Severity::Info)
8600            .icon(IconName::Info)
8601            .title("Resumed Session")
8602            .description(description)
8603            .into_any_element()
8604    }
8605
8606    fn update_recent_history_from_cache(
8607        &mut self,
8608        history: &Entity<ThreadHistory>,
8609        cx: &mut Context<Self>,
8610    ) {
8611        self.recent_history_entries = history.read(cx).get_recent_sessions(3);
8612        self.hovered_recent_history_item = None;
8613        cx.notify();
8614    }
8615
8616    fn render_codex_windows_warning(&self, cx: &mut Context<Self>) -> Callout {
8617        Callout::new()
8618            .icon(IconName::Warning)
8619            .severity(Severity::Warning)
8620            .title("Codex on Windows")
8621            .description("For best performance, run Codex in Windows Subsystem for Linux (WSL2)")
8622            .actions_slot(
8623                Button::new("open-wsl-modal", "Open in WSL").on_click(cx.listener({
8624                    move |_, _, _window, cx| {
8625                        #[cfg(windows)]
8626                        _window.dispatch_action(
8627                            zed_actions::wsl_actions::OpenWsl::default().boxed_clone(),
8628                            cx,
8629                        );
8630                        cx.notify();
8631                    }
8632                })),
8633            )
8634            .dismiss_action(
8635                IconButton::new("dismiss", IconName::Close)
8636                    .icon_size(IconSize::Small)
8637                    .icon_color(Color::Muted)
8638                    .tooltip(Tooltip::text("Dismiss Warning"))
8639                    .on_click(cx.listener({
8640                        move |this, _, _, cx| {
8641                            this.show_codex_windows_warning = false;
8642                            cx.notify();
8643                        }
8644                    })),
8645            )
8646    }
8647
8648    fn render_external_source_prompt_warning(&self, cx: &mut Context<Self>) -> Callout {
8649        Callout::new()
8650            .icon(IconName::Warning)
8651            .severity(Severity::Warning)
8652            .title("Review before sending")
8653            .description("This prompt was pre-filled by an external link. Read it carefully before you send it.")
8654            .dismiss_action(
8655                IconButton::new("dismiss-external-source-prompt-warning", IconName::Close)
8656                    .icon_size(IconSize::Small)
8657                    .icon_color(Color::Muted)
8658                    .tooltip(Tooltip::text("Dismiss Warning"))
8659                    .on_click(cx.listener({
8660                        move |this, _, _, cx| {
8661                            this.show_external_source_prompt_warning = false;
8662                            cx.notify();
8663                        }
8664                    })),
8665            )
8666    }
8667
8668    fn render_multi_root_callout(&self, cx: &mut Context<Self>) -> Option<Callout> {
8669        if self.multi_root_callout_dismissed {
8670            return None;
8671        }
8672
8673        if self.as_native_connection(cx).is_some() {
8674            return None;
8675        }
8676
8677        let project = self.project.upgrade()?;
8678        let worktree_count = project.read(cx).visible_worktrees(cx).count();
8679        if worktree_count <= 1 {
8680            return None;
8681        }
8682
8683        let work_dirs = self.thread.read(cx).work_dirs()?;
8684        let active_dir = work_dirs
8685            .ordered_paths()
8686            .next()
8687            .and_then(|p| p.file_name())
8688            .map(|name| name.to_string_lossy().to_string())
8689            .unwrap_or_else(|| "one folder".to_string());
8690
8691        let description = format!(
8692            "This agent only operates on \"{}\". Other folders in this workspace are not accessible to it.",
8693            active_dir
8694        );
8695
8696        Some(
8697            Callout::new()
8698                .severity(Severity::Warning)
8699                .icon(IconName::Warning)
8700                .title("External Agents currently don't support multi-root workspaces")
8701                .description(description)
8702                .border_position(ui::BorderPosition::Bottom)
8703                .dismiss_action(
8704                    IconButton::new("dismiss-multi-root-callout", IconName::Close)
8705                        .icon_size(IconSize::Small)
8706                        .tooltip(Tooltip::text("Dismiss"))
8707                        .on_click(cx.listener(|this, _, _, cx| {
8708                            this.multi_root_callout_dismissed = true;
8709                            cx.notify();
8710                        })),
8711                ),
8712        )
8713    }
8714
8715    fn render_new_version_callout(&self, version: &SharedString, cx: &mut Context<Self>) -> Div {
8716        let server_view = self.server_view.clone();
8717        let has_version = !version.is_empty();
8718        let title = if has_version {
8719            "New version available"
8720        } else {
8721            "Agent update available"
8722        };
8723        let button_label = if has_version {
8724            format!("Update to v{}", version)
8725        } else {
8726            "Reconnect".to_string()
8727        };
8728
8729        v_flex().w_full().justify_end().child(
8730            h_flex()
8731                .p_2()
8732                .pr_3()
8733                .w_full()
8734                .gap_1p5()
8735                .border_t_1()
8736                .border_color(cx.theme().colors().border)
8737                .bg(cx.theme().colors().element_background)
8738                .child(
8739                    h_flex()
8740                        .flex_1()
8741                        .gap_1p5()
8742                        .child(
8743                            Icon::new(IconName::Download)
8744                                .color(Color::Accent)
8745                                .size(IconSize::Small),
8746                        )
8747                        .child(Label::new(title).size(LabelSize::Small)),
8748                )
8749                .child(
8750                    Button::new("update-button", button_label)
8751                        .label_size(LabelSize::Small)
8752                        .style(ButtonStyle::Tinted(TintColor::Accent))
8753                        .on_click(move |_, window, cx| {
8754                            server_view
8755                                .update(cx, |view, cx| view.reset(window, cx))
8756                                .ok();
8757                        }),
8758                ),
8759        )
8760    }
8761
8762    fn render_token_limit_callout(&self, cx: &mut Context<Self>) -> Option<Callout> {
8763        if self.token_limit_callout_dismissed || self.as_native_thread(cx).is_none() {
8764            return None;
8765        }
8766
8767        let token_usage = self.thread.read(cx).token_usage()?;
8768        let ratio = token_usage.ratio();
8769
8770        let (severity, icon, title) = match ratio {
8771            acp_thread::TokenUsageRatio::Normal => return None,
8772            acp_thread::TokenUsageRatio::Warning => (
8773                Severity::Warning,
8774                IconName::Warning,
8775                "Thread reaching the token limit soon",
8776            ),
8777            acp_thread::TokenUsageRatio::Exceeded => (
8778                Severity::Error,
8779                IconName::XCircle,
8780                "Thread reached the token limit",
8781            ),
8782        };
8783
8784        let description = "To continue, start a new thread from a summary.";
8785
8786        Some(
8787            Callout::new()
8788                .severity(severity)
8789                .icon(icon)
8790                .title(title)
8791                .description(description)
8792                .actions_slot(
8793                    h_flex().gap_0p5().child(
8794                        Button::new("start-new-thread", "Start New Thread")
8795                            .label_size(LabelSize::Small)
8796                            .on_click(cx.listener(|this, _, window, cx| {
8797                                let session_id = this.thread.read(cx).session_id().clone();
8798                                window.dispatch_action(
8799                                    crate::NewNativeAgentThreadFromSummary {
8800                                        from_session_id: session_id,
8801                                    }
8802                                    .boxed_clone(),
8803                                    cx,
8804                                );
8805                            })),
8806                    ),
8807                )
8808                .dismiss_action(self.dismiss_error_button(cx)),
8809        )
8810    }
8811
8812    fn open_permission_dropdown(
8813        &mut self,
8814        _: &crate::OpenPermissionDropdown,
8815        window: &mut Window,
8816        cx: &mut Context<Self>,
8817    ) {
8818        let menu_handle = self.permission_dropdown_handle.clone();
8819        window.defer(cx, move |window, cx| {
8820            menu_handle.toggle(window, cx);
8821        });
8822    }
8823
8824    fn open_add_context_menu(
8825        &mut self,
8826        _action: &OpenAddContextMenu,
8827        window: &mut Window,
8828        cx: &mut Context<Self>,
8829    ) {
8830        let menu_handle = self.add_context_menu_handle.clone();
8831        window.defer(cx, move |window, cx| {
8832            menu_handle.toggle(window, cx);
8833        });
8834    }
8835
8836    fn toggle_fast_mode(&mut self, cx: &mut Context<Self>) {
8837        if !self.fast_mode_available(cx) {
8838            return;
8839        }
8840        let Some(thread) = self.as_native_thread(cx) else {
8841            return;
8842        };
8843        thread.update(cx, |thread, cx| {
8844            let new_speed = thread
8845                .speed()
8846                .map(|speed| speed.toggle())
8847                .unwrap_or(Speed::Fast);
8848            thread.set_speed(new_speed, cx);
8849
8850            let fs = thread.project().read(cx).fs().clone();
8851            update_settings_file(fs, cx, move |settings, _| {
8852                if let Some(agent) = settings.agent.as_mut()
8853                    && let Some(default_model) = agent.default_model.as_mut()
8854                {
8855                    default_model.speed = Some(new_speed);
8856                }
8857            });
8858        });
8859    }
8860
8861    fn cycle_thinking_effort(&mut self, cx: &mut Context<Self>) {
8862        let Some(thread) = self.as_native_thread(cx) else {
8863            return;
8864        };
8865
8866        let (effort_levels, current_effort) = {
8867            let thread_ref = thread.read(cx);
8868            let Some(model) = thread_ref.model() else {
8869                return;
8870            };
8871            if !model.supports_thinking() || !thread_ref.thinking_enabled() {
8872                return;
8873            }
8874            let effort_levels = model.supported_effort_levels();
8875            if effort_levels.is_empty() {
8876                return;
8877            }
8878            let current_effort = thread_ref.thinking_effort().cloned();
8879            (effort_levels, current_effort)
8880        };
8881
8882        let current_index = current_effort.and_then(|current| {
8883            effort_levels
8884                .iter()
8885                .position(|level| level.value == current)
8886        });
8887        let next_index = match current_index {
8888            Some(index) => (index + 1) % effort_levels.len(),
8889            None => 0,
8890        };
8891        let next_effort = effort_levels[next_index].value.to_string();
8892
8893        thread.update(cx, |thread, cx| {
8894            thread.set_thinking_effort(Some(next_effort.clone()), cx);
8895
8896            let fs = thread.project().read(cx).fs().clone();
8897            update_settings_file(fs, cx, move |settings, _| {
8898                if let Some(agent) = settings.agent.as_mut()
8899                    && let Some(default_model) = agent.default_model.as_mut()
8900                {
8901                    default_model.effort = Some(next_effort);
8902                }
8903            });
8904        });
8905    }
8906
8907    fn toggle_thinking_effort_menu(
8908        &mut self,
8909        _action: &ToggleThinkingEffortMenu,
8910        window: &mut Window,
8911        cx: &mut Context<Self>,
8912    ) {
8913        let menu_handle = self.thinking_effort_menu_handle.clone();
8914        window.defer(cx, move |window, cx| {
8915            menu_handle.toggle(window, cx);
8916        });
8917    }
8918}
8919
8920impl Render for ThreadView {
8921    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
8922        let has_messages = self.list_state.item_count() > 0;
8923        let list_state = self.list_state.clone();
8924
8925        let conversation = v_flex()
8926            .when(self.resumed_without_history, |this| {
8927                this.child(Self::render_resume_notice(cx))
8928            })
8929            .map(|this| {
8930                if has_messages {
8931                    this.flex_1()
8932                        .size_full()
8933                        .child(self.render_entries(cx))
8934                        .vertical_scrollbar_for(&list_state, window, cx)
8935                        .into_any()
8936                } else {
8937                    this.into_any()
8938                }
8939            });
8940
8941        v_flex()
8942            .key_context("AcpThread")
8943            .track_focus(&self.focus_handle)
8944            .on_action(cx.listener(|this, _: &menu::Cancel, _, cx| {
8945                if this.parent_session_id.is_none() {
8946                    this.cancel_generation(cx);
8947                }
8948            }))
8949            .on_action(cx.listener(|this, _: &workspace::GoBack, window, cx| {
8950                if let Some(parent_session_id) = this.thread.read(cx).parent_session_id().cloned() {
8951                    this.server_view
8952                        .update(cx, |view, cx| {
8953                            view.navigate_to_thread(parent_session_id, window, cx);
8954                        })
8955                        .ok();
8956                }
8957            }))
8958            .on_action(cx.listener(Self::keep_all))
8959            .on_action(cx.listener(Self::reject_all))
8960            .on_action(cx.listener(Self::undo_last_reject))
8961            .on_action(cx.listener(Self::allow_always))
8962            .on_action(cx.listener(Self::allow_once))
8963            .on_action(cx.listener(Self::reject_once))
8964            .on_action(cx.listener(Self::handle_authorize_tool_call))
8965            .on_action(cx.listener(Self::handle_select_permission_granularity))
8966            .on_action(cx.listener(Self::handle_toggle_command_pattern))
8967            .on_action(cx.listener(Self::open_permission_dropdown))
8968            .on_action(cx.listener(Self::open_add_context_menu))
8969            .on_action(cx.listener(Self::scroll_output_page_up))
8970            .on_action(cx.listener(Self::scroll_output_page_down))
8971            .on_action(cx.listener(Self::scroll_output_line_up))
8972            .on_action(cx.listener(Self::scroll_output_line_down))
8973            .on_action(cx.listener(Self::scroll_output_to_top))
8974            .on_action(cx.listener(Self::scroll_output_to_bottom))
8975            .on_action(cx.listener(Self::scroll_output_to_previous_message))
8976            .on_action(cx.listener(Self::scroll_output_to_next_message))
8977            .on_action(cx.listener(|this, _: &ToggleFastMode, _window, cx| {
8978                this.toggle_fast_mode(cx);
8979            }))
8980            .on_action(cx.listener(|this, _: &ToggleThinkingMode, _window, cx| {
8981                if this.thread.read(cx).status() != ThreadStatus::Idle {
8982                    return;
8983                }
8984                if let Some(thread) = this.as_native_thread(cx) {
8985                    thread.update(cx, |thread, cx| {
8986                        thread.set_thinking_enabled(!thread.thinking_enabled(), cx);
8987                    });
8988                }
8989            }))
8990            .on_action(cx.listener(|this, _: &CycleThinkingEffort, _window, cx| {
8991                if this.thread.read(cx).status() != ThreadStatus::Idle {
8992                    return;
8993                }
8994                this.cycle_thinking_effort(cx);
8995            }))
8996            .on_action(
8997                cx.listener(|this, action: &ToggleThinkingEffortMenu, window, cx| {
8998                    if this.thread.read(cx).status() != ThreadStatus::Idle {
8999                        return;
9000                    }
9001                    this.toggle_thinking_effort_menu(action, window, cx);
9002                }),
9003            )
9004            .on_action(cx.listener(|this, _: &SendNextQueuedMessage, window, cx| {
9005                this.send_queued_message_at_index(0, true, window, cx);
9006            }))
9007            .on_action(cx.listener(|this, _: &RemoveFirstQueuedMessage, _, cx| {
9008                this.remove_from_queue(0, cx);
9009                cx.notify();
9010            }))
9011            .on_action(cx.listener(|this, _: &EditFirstQueuedMessage, window, cx| {
9012                this.move_queued_message_to_main_editor(0, None, None, window, cx);
9013            }))
9014            .on_action(cx.listener(|this, _: &ClearMessageQueue, _, cx| {
9015                this.local_queued_messages.clear();
9016                this.sync_queue_flag_to_native_thread(cx);
9017                this.can_fast_track_queue = false;
9018                cx.notify();
9019            }))
9020            .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
9021                if this.thread.read(cx).status() != ThreadStatus::Idle {
9022                    return;
9023                }
9024                if let Some(config_options_view) = this.config_options_view.clone() {
9025                    let handled = config_options_view.update(cx, |view, cx| {
9026                        view.toggle_category_picker(
9027                            acp::SessionConfigOptionCategory::Mode,
9028                            window,
9029                            cx,
9030                        )
9031                    });
9032                    if handled {
9033                        return;
9034                    }
9035                }
9036
9037                if let Some(profile_selector) = this.profile_selector.clone() {
9038                    profile_selector.read(cx).menu_handle().toggle(window, cx);
9039                } else if let Some(mode_selector) = this.mode_selector.clone() {
9040                    mode_selector.read(cx).menu_handle().toggle(window, cx);
9041                }
9042            }))
9043            .on_action(cx.listener(|this, _: &CycleModeSelector, window, cx| {
9044                if this.thread.read(cx).status() != ThreadStatus::Idle {
9045                    return;
9046                }
9047                if let Some(config_options_view) = this.config_options_view.clone() {
9048                    let handled = config_options_view.update(cx, |view, cx| {
9049                        view.cycle_category_option(
9050                            acp::SessionConfigOptionCategory::Mode,
9051                            false,
9052                            cx,
9053                        )
9054                    });
9055                    if handled {
9056                        return;
9057                    }
9058                }
9059
9060                if let Some(profile_selector) = this.profile_selector.clone() {
9061                    profile_selector.update(cx, |profile_selector, cx| {
9062                        profile_selector.cycle_profile(cx);
9063                    });
9064                } else if let Some(mode_selector) = this.mode_selector.clone() {
9065                    mode_selector.update(cx, |mode_selector, cx| {
9066                        mode_selector.cycle_mode(window, cx);
9067                    });
9068                }
9069            }))
9070            .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
9071                if this.thread.read(cx).status() != ThreadStatus::Idle {
9072                    return;
9073                }
9074                if let Some(config_options_view) = this.config_options_view.clone() {
9075                    let handled = config_options_view.update(cx, |view, cx| {
9076                        view.toggle_category_picker(
9077                            acp::SessionConfigOptionCategory::Model,
9078                            window,
9079                            cx,
9080                        )
9081                    });
9082                    if handled {
9083                        return;
9084                    }
9085                }
9086
9087                if let Some(model_selector) = this.model_selector.clone() {
9088                    model_selector
9089                        .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
9090                }
9091            }))
9092            .on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
9093                if this.thread.read(cx).status() != ThreadStatus::Idle {
9094                    return;
9095                }
9096                if let Some(config_options_view) = this.config_options_view.clone() {
9097                    let handled = config_options_view.update(cx, |view, cx| {
9098                        view.cycle_category_option(
9099                            acp::SessionConfigOptionCategory::Model,
9100                            true,
9101                            cx,
9102                        )
9103                    });
9104                    if handled {
9105                        return;
9106                    }
9107                }
9108
9109                if let Some(model_selector) = this.model_selector.clone() {
9110                    model_selector.update(cx, |model_selector, cx| {
9111                        model_selector.cycle_favorite_models(window, cx);
9112                    });
9113                }
9114            }))
9115            .size_full()
9116            .children(self.render_subagent_titlebar(cx))
9117            .child(conversation)
9118            .children(self.render_multi_root_callout(cx))
9119            .children(self.render_activity_bar(window, cx))
9120            .when(self.show_external_source_prompt_warning, |this| {
9121                this.child(self.render_external_source_prompt_warning(cx))
9122            })
9123            .when(self.show_codex_windows_warning, |this| {
9124                this.child(self.render_codex_windows_warning(cx))
9125            })
9126            .children(self.render_thread_retry_status_callout())
9127            .children(self.render_thread_error(window, cx))
9128            .when_some(
9129                match has_messages {
9130                    true => None,
9131                    false => self.new_server_version_available.clone(),
9132                },
9133                |this, version| this.child(self.render_new_version_callout(&version, cx)),
9134            )
9135            .children(self.render_token_limit_callout(cx))
9136            .child(self.render_message_editor(window, cx))
9137    }
9138}
9139
9140pub(crate) fn open_link(
9141    url: SharedString,
9142    workspace: &WeakEntity<Workspace>,
9143    window: &mut Window,
9144    cx: &mut App,
9145) {
9146    let Some(workspace) = workspace.upgrade() else {
9147        cx.open_url(&url);
9148        return;
9149    };
9150
9151    if let Some(mention) = MentionUri::parse(&url, workspace.read(cx).path_style(cx)).log_err() {
9152        workspace.update(cx, |workspace, cx| match mention {
9153            MentionUri::File { abs_path } => {
9154                let project = workspace.project();
9155                let Some(path) =
9156                    project.update(cx, |project, cx| project.find_project_path(abs_path, cx))
9157                else {
9158                    return;
9159                };
9160
9161                workspace
9162                    .open_path(path, None, true, window, cx)
9163                    .detach_and_log_err(cx);
9164            }
9165            MentionUri::PastedImage { .. } => {}
9166            MentionUri::Directory { abs_path } => {
9167                let project = workspace.project();
9168                let Some(entry_id) = project.update(cx, |project, cx| {
9169                    let path = project.find_project_path(abs_path, cx)?;
9170                    project.entry_for_path(&path, cx).map(|entry| entry.id)
9171                }) else {
9172                    return;
9173                };
9174
9175                project.update(cx, |_, cx| {
9176                    cx.emit(project::Event::RevealInProjectPanel(entry_id));
9177                });
9178            }
9179            MentionUri::Symbol {
9180                abs_path: path,
9181                line_range,
9182                ..
9183            }
9184            | MentionUri::Selection {
9185                abs_path: Some(path),
9186                line_range,
9187            } => {
9188                let project = workspace.project();
9189                let Some(path) =
9190                    project.update(cx, |project, cx| project.find_project_path(path, cx))
9191                else {
9192                    return;
9193                };
9194
9195                let item = workspace.open_path(path, None, true, window, cx);
9196                window
9197                    .spawn(cx, async move |cx| {
9198                        let Some(editor) = item.await?.downcast::<Editor>() else {
9199                            return Ok(());
9200                        };
9201                        let range =
9202                            Point::new(*line_range.start(), 0)..Point::new(*line_range.start(), 0);
9203                        editor
9204                            .update_in(cx, |editor, window, cx| {
9205                                editor.change_selections(
9206                                    SelectionEffects::scroll(Autoscroll::center()),
9207                                    window,
9208                                    cx,
9209                                    |s| s.select_ranges(vec![range]),
9210                                );
9211                            })
9212                            .ok();
9213                        anyhow::Ok(())
9214                    })
9215                    .detach_and_log_err(cx);
9216            }
9217            MentionUri::Selection { abs_path: None, .. } => {}
9218            MentionUri::Thread { id, name } => {
9219                if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
9220                    panel.update(cx, |panel, cx| {
9221                        panel.open_thread(id, None, Some(name.into()), window, cx)
9222                    });
9223                }
9224            }
9225            MentionUri::Rule { id, .. } => {
9226                let PromptId::User { uuid } = id else {
9227                    return;
9228                };
9229                window.dispatch_action(
9230                    Box::new(OpenRulesLibrary {
9231                        prompt_to_select: Some(uuid.0),
9232                    }),
9233                    cx,
9234                )
9235            }
9236            MentionUri::Fetch { url } => {
9237                cx.open_url(url.as_str());
9238            }
9239            MentionUri::Diagnostics { .. } => {}
9240            MentionUri::TerminalSelection { .. } => {}
9241            MentionUri::GitDiff { .. } => {}
9242            MentionUri::MergeConflict { .. } => {}
9243        })
9244    } else {
9245        cx.open_url(&url);
9246    }
9247}