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