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 has_commands = !session_capabilities.read().available_commands().is_empty();
 348        let placeholder = placeholder_text(agent_display_name.as_ref(), has_commands);
 349
 350        let history_subscription = history.as_ref().map(|h| {
 351            cx.observe(h, |this, history, cx| {
 352                this.update_recent_history_from_cache(&history, cx);
 353            })
 354        });
 355
 356        let mut should_auto_submit = false;
 357        let mut show_external_source_prompt_warning = false;
 358
 359        let message_editor = cx.new(|cx| {
 360            let mut editor = MessageEditor::new(
 361                workspace.clone(),
 362                project.clone(),
 363                thread_store,
 364                history.as_ref().map(|h| h.downgrade()),
 365                prompt_store,
 366                session_capabilities.clone(),
 367                agent_id.clone(),
 368                &placeholder,
 369                editor::EditorMode::AutoHeight {
 370                    min_lines: AgentSettings::get_global(cx).message_editor_min_lines,
 371                    max_lines: Some(AgentSettings::get_global(cx).set_message_editor_max_lines()),
 372                },
 373                window,
 374                cx,
 375            );
 376            if let Some(content) = initial_content {
 377                match content {
 378                    AgentInitialContent::ThreadSummary { session_id, title } => {
 379                        editor.insert_thread_summary(session_id, title, window, cx);
 380                    }
 381                    AgentInitialContent::ContentBlock {
 382                        blocks,
 383                        auto_submit,
 384                    } => {
 385                        should_auto_submit = auto_submit;
 386                        editor.set_message(blocks, window, cx);
 387                    }
 388                    AgentInitialContent::FromExternalSource(prompt) => {
 389                        show_external_source_prompt_warning = true;
 390                        // SECURITY: Be explicit about not auto submitting prompt from external source.
 391                        should_auto_submit = false;
 392                        editor.set_message(
 393                            vec![acp::ContentBlock::Text(acp::TextContent::new(
 394                                prompt.into_string(),
 395                            ))],
 396                            window,
 397                            cx,
 398                        );
 399                    }
 400                }
 401            } else if let Some(draft) = thread.read(cx).draft_prompt() {
 402                editor.set_message(draft.to_vec(), window, cx);
 403            }
 404            editor
 405        });
 406
 407        let show_codex_windows_warning = cfg!(windows)
 408            && project.upgrade().is_some_and(|p| p.read(cx).is_local())
 409            && agent_id.as_ref() == "Codex";
 410
 411        let title_editor = {
 412            let can_edit = thread.update(cx, |thread, cx| thread.can_set_title(cx));
 413            let editor = cx.new(|cx| {
 414                let mut editor = Editor::single_line(window, cx);
 415                if let Some(title) = thread.read(cx).title() {
 416                    editor.set_text(title, window, cx);
 417                } else {
 418                    editor.set_text(DEFAULT_THREAD_TITLE, window, cx);
 419                }
 420                editor.set_read_only(!can_edit);
 421                editor
 422            });
 423            subscriptions.push(cx.subscribe_in(&editor, window, Self::handle_title_editor_event));
 424            editor
 425        };
 426
 427        subscriptions.push(cx.subscribe_in(
 428            &entry_view_state,
 429            window,
 430            Self::handle_entry_view_event,
 431        ));
 432
 433        subscriptions.push(cx.subscribe_in(
 434            &message_editor,
 435            window,
 436            Self::handle_message_editor_event,
 437        ));
 438
 439        subscriptions.push(cx.observe(&message_editor, |this, editor, cx| {
 440            let is_empty = editor.read(cx).text(cx).is_empty();
 441            let draft_contents_task = if is_empty {
 442                None
 443            } else {
 444                Some(editor.update(cx, |editor, cx| editor.draft_contents(cx)))
 445            };
 446            this._draft_resolve_task = Some(cx.spawn(async move |this, cx| {
 447                let draft = if let Some(task) = draft_contents_task {
 448                    let blocks = task.await.ok().filter(|b| !b.is_empty());
 449                    blocks
 450                } else {
 451                    None
 452                };
 453                this.update(cx, |this, cx| {
 454                    this.thread.update(cx, |thread, _cx| {
 455                        thread.set_draft_prompt(draft);
 456                    });
 457                    this.schedule_save(cx);
 458                })
 459                .ok();
 460            }));
 461        }));
 462
 463        let recent_history_entries = history
 464            .as_ref()
 465            .map(|h| h.read(cx).get_recent_sessions(3))
 466            .unwrap_or_default();
 467
 468        let mut this = Self {
 469            id,
 470            parent_id,
 471            focus_handle: cx.focus_handle(),
 472            thread,
 473            conversation,
 474            server_view,
 475            agent_icon,
 476            agent_icon_from_external_svg,
 477            agent_id,
 478            workspace,
 479            entry_view_state,
 480            title_editor,
 481            config_options_view,
 482            mode_selector,
 483            model_selector,
 484            profile_selector,
 485            list_state,
 486            session_capabilities,
 487            resumed_without_history,
 488            _subscriptions: subscriptions,
 489            permission_dropdown_handle: PopoverMenuHandle::default(),
 490            thread_retry_status: None,
 491            thread_error: None,
 492            thread_error_markdown: None,
 493            token_limit_callout_dismissed: false,
 494            last_token_limit_telemetry: None,
 495            thread_feedback: Default::default(),
 496            expanded_tool_calls: HashSet::default(),
 497            expanded_tool_call_raw_inputs: HashSet::default(),
 498            expanded_thinking_blocks: HashSet::default(),
 499            auto_expanded_thinking_block: None,
 500            user_toggled_thinking_blocks: HashSet::default(),
 501            subagent_scroll_handles: RefCell::new(HashMap::default()),
 502            edits_expanded: false,
 503            plan_expanded: false,
 504            queue_expanded: true,
 505            editor_expanded: false,
 506            should_be_following: false,
 507            editing_message: None,
 508            local_queued_messages: Vec::new(),
 509            queued_message_editors: Vec::new(),
 510            queued_message_editor_subscriptions: Vec::new(),
 511            last_synced_queue_length: 0,
 512            turn_fields: TurnFields::default(),
 513            discarded_partial_edits: HashSet::default(),
 514            is_loading_contents: false,
 515            new_server_version_available: None,
 516            permission_selections: HashMap::default(),
 517            resume_thread_metadata: None,
 518            _cancel_task: None,
 519            _save_task: None,
 520            _draft_resolve_task: None,
 521            skip_queue_processing_count: 0,
 522            user_interrupted_generation: false,
 523            can_fast_track_queue: false,
 524            hovered_edited_file_buttons: None,
 525            in_flight_prompt: None,
 526            message_editor,
 527            add_context_menu_handle: PopoverMenuHandle::default(),
 528            thinking_effort_menu_handle: PopoverMenuHandle::default(),
 529            project,
 530            recent_history_entries,
 531            hovered_recent_history_item: None,
 532            show_external_source_prompt_warning,
 533            history,
 534            _history_subscription: history_subscription,
 535            show_codex_windows_warning,
 536            generating_indicator_in_list: false,
 537        };
 538
 539        this.sync_generating_indicator(cx);
 540        this.sync_editor_mode_for_empty_state(cx);
 541        let list_state_for_scroll = this.list_state.clone();
 542        let thread_view = cx.entity().downgrade();
 543
 544        this.list_state
 545            .set_scroll_handler(move |_event, _window, cx| {
 546                let list_state = list_state_for_scroll.clone();
 547                let thread_view = thread_view.clone();
 548                // N.B. We must defer because the scroll handler is called while the
 549                // ListState's RefCell is mutably borrowed. Reading logical_scroll_top()
 550                // directly would panic from a double borrow.
 551                cx.defer(move |cx| {
 552                    let scroll_top = list_state.logical_scroll_top();
 553                    let _ = thread_view.update(cx, |this, cx| {
 554                        if let Some(thread) = this.as_native_thread(cx) {
 555                            thread.update(cx, |thread, _cx| {
 556                                thread.set_ui_scroll_position(Some(scroll_top));
 557                            });
 558                        }
 559                        this.schedule_save(cx);
 560                    });
 561                });
 562            });
 563
 564        if should_auto_submit {
 565            this.send(window, cx);
 566        }
 567        this
 568    }
 569
 570    /// Schedule a throttled save of the thread state (draft prompt, scroll position, etc.).
 571    /// Multiple calls within `SERIALIZATION_THROTTLE_TIME` are coalesced into a single save.
 572    fn schedule_save(&mut self, cx: &mut Context<Self>) {
 573        self._save_task = Some(cx.spawn(async move |this, cx| {
 574            cx.background_executor()
 575                .timer(SERIALIZATION_THROTTLE_TIME)
 576                .await;
 577            this.update(cx, |this, cx| {
 578                if let Some(thread) = this.as_native_thread(cx) {
 579                    thread.update(cx, |_thread, cx| cx.notify());
 580                }
 581            })
 582            .ok();
 583        }));
 584    }
 585
 586    pub fn handle_message_editor_event(
 587        &mut self,
 588        _editor: &Entity<MessageEditor>,
 589        event: &MessageEditorEvent,
 590        window: &mut Window,
 591        cx: &mut Context<Self>,
 592    ) {
 593        match event {
 594            MessageEditorEvent::Send => self.send(window, cx),
 595            MessageEditorEvent::SendImmediately => self.interrupt_and_send(window, cx),
 596            MessageEditorEvent::Cancel => self.cancel_generation(cx),
 597            MessageEditorEvent::Focus => {
 598                self.cancel_editing(&Default::default(), window, cx);
 599            }
 600            MessageEditorEvent::LostFocus => {}
 601            MessageEditorEvent::InputAttempted { .. } => {}
 602        }
 603    }
 604
 605    pub(crate) fn as_native_connection(
 606        &self,
 607        cx: &App,
 608    ) -> Option<Rc<agent::NativeAgentConnection>> {
 609        let acp_thread = self.thread.read(cx);
 610        acp_thread.connection().clone().downcast()
 611    }
 612
 613    pub fn as_native_thread(&self, cx: &App) -> Option<Entity<agent::Thread>> {
 614        let acp_thread = self.thread.read(cx);
 615        self.as_native_connection(cx)?
 616            .thread(acp_thread.session_id(), cx)
 617    }
 618
 619    /// Resolves the message editor's contents into content blocks. For profiles
 620    /// that do not enable any tools, directory mentions are expanded to inline
 621    /// file contents since the agent can't read files on its own.
 622    fn resolve_message_contents(
 623        &self,
 624        message_editor: &Entity<MessageEditor>,
 625        cx: &mut App,
 626    ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> {
 627        let expand = self.as_native_thread(cx).is_some_and(|thread| {
 628            let thread = thread.read(cx);
 629            AgentSettings::get_global(cx)
 630                .profiles
 631                .get(thread.profile())
 632                .is_some_and(|profile| profile.tools.is_empty())
 633        });
 634        message_editor.update(cx, |message_editor, cx| message_editor.contents(expand, cx))
 635    }
 636
 637    pub fn current_model_id(&self, cx: &App) -> Option<String> {
 638        let selector = self.model_selector.as_ref()?;
 639        let model = selector.read(cx).active_model(cx)?;
 640        Some(model.id.to_string())
 641    }
 642
 643    pub fn current_mode_id(&self, cx: &App) -> Option<Arc<str>> {
 644        if let Some(thread) = self.as_native_thread(cx) {
 645            Some(thread.read(cx).profile().0.clone())
 646        } else {
 647            let mode_selector = self.mode_selector.as_ref()?;
 648            Some(mode_selector.read(cx).mode().0)
 649        }
 650    }
 651
 652    fn is_subagent(&self) -> bool {
 653        self.parent_id.is_some()
 654    }
 655
 656    /// Returns the currently active editor, either for a message that is being
 657    /// edited or the editor for a new message.
 658    pub(crate) fn active_editor(&self, cx: &App) -> Entity<MessageEditor> {
 659        if let Some(index) = self.editing_message
 660            && let Some(editor) = self
 661                .entry_view_state
 662                .read(cx)
 663                .entry(index)
 664                .and_then(|entry| entry.message_editor())
 665                .cloned()
 666        {
 667            editor
 668        } else {
 669            self.message_editor.clone()
 670        }
 671    }
 672
 673    pub fn has_queued_messages(&self) -> bool {
 674        !self.local_queued_messages.is_empty()
 675    }
 676
 677    pub fn is_imported_thread(&self, cx: &App) -> bool {
 678        let Some(thread) = self.as_native_thread(cx) else {
 679            return false;
 680        };
 681        thread.read(cx).is_imported()
 682    }
 683
 684    // events
 685
 686    pub fn handle_entry_view_event(
 687        &mut self,
 688        _: &Entity<EntryViewState>,
 689        event: &EntryViewEvent,
 690        window: &mut Window,
 691        cx: &mut Context<Self>,
 692    ) {
 693        match &event.view_event {
 694            ViewEvent::NewDiff(tool_call_id) => {
 695                if AgentSettings::get_global(cx).expand_edit_card {
 696                    self.expanded_tool_calls.insert(tool_call_id.clone());
 697                }
 698            }
 699            ViewEvent::NewTerminal(tool_call_id) => {
 700                if AgentSettings::get_global(cx).expand_terminal_card {
 701                    self.expanded_tool_calls.insert(tool_call_id.clone());
 702                }
 703            }
 704            ViewEvent::TerminalMovedToBackground(tool_call_id) => {
 705                self.expanded_tool_calls.remove(tool_call_id);
 706            }
 707            ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => {
 708                if let Some(AgentThreadEntry::UserMessage(user_message)) =
 709                    self.thread.read(cx).entries().get(event.entry_index)
 710                    && user_message.id.is_some()
 711                    && !self.is_subagent()
 712                {
 713                    self.editing_message = Some(event.entry_index);
 714                    cx.notify();
 715                }
 716            }
 717            ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::LostFocus) => {
 718                if let Some(AgentThreadEntry::UserMessage(user_message)) =
 719                    self.thread.read(cx).entries().get(event.entry_index)
 720                    && user_message.id.is_some()
 721                    && !self.is_subagent()
 722                {
 723                    if editor.read(cx).text(cx).as_str() == user_message.content.to_markdown(cx) {
 724                        self.editing_message = None;
 725                        cx.notify();
 726                    }
 727                }
 728            }
 729            ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::SendImmediately) => {}
 730            ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => {
 731                if !self.is_subagent() {
 732                    self.regenerate(event.entry_index, editor.clone(), window, cx);
 733                }
 734            }
 735            ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Cancel) => {
 736                self.cancel_editing(&Default::default(), window, cx);
 737            }
 738            ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::InputAttempted { .. }) => {}
 739            ViewEvent::OpenDiffLocation {
 740                path,
 741                position,
 742                split,
 743            } => {
 744                self.open_diff_location(path, *position, *split, window, cx);
 745            }
 746        }
 747    }
 748
 749    fn open_diff_location(
 750        &self,
 751        path: &str,
 752        position: Point,
 753        split: bool,
 754        window: &mut Window,
 755        cx: &mut Context<Self>,
 756    ) {
 757        let Some(project) = self.project.upgrade() else {
 758            return;
 759        };
 760        let Some(project_path) = project.read(cx).find_project_path(path, cx) else {
 761            return;
 762        };
 763
 764        let open_task = if split {
 765            self.workspace
 766                .update(cx, |workspace, cx| {
 767                    workspace.split_path(project_path, window, cx)
 768                })
 769                .log_err()
 770        } else {
 771            self.workspace
 772                .update(cx, |workspace, cx| {
 773                    workspace.open_path(project_path, None, true, window, cx)
 774                })
 775                .log_err()
 776        };
 777
 778        let Some(open_task) = open_task else {
 779            return;
 780        };
 781
 782        window
 783            .spawn(cx, async move |cx| {
 784                let item = open_task.await?;
 785                let Some(editor) = item.downcast::<Editor>() else {
 786                    return anyhow::Ok(());
 787                };
 788                editor.update_in(cx, |editor, window, cx| {
 789                    editor.change_selections(
 790                        SelectionEffects::scroll(Autoscroll::center()),
 791                        window,
 792                        cx,
 793                        |selections| {
 794                            selections.select_ranges([position..position]);
 795                        },
 796                    );
 797                })?;
 798                anyhow::Ok(())
 799            })
 800            .detach_and_log_err(cx);
 801    }
 802
 803    // turns
 804
 805    pub fn start_turn(&mut self, cx: &mut Context<Self>) -> usize {
 806        self.turn_fields.turn_generation += 1;
 807        let generation = self.turn_fields.turn_generation;
 808        self.turn_fields.turn_started_at = Some(Instant::now());
 809        self.turn_fields.last_turn_duration = None;
 810        self.turn_fields.last_turn_tokens = None;
 811        self.turn_fields.turn_tokens = Some(0);
 812        self.turn_fields._turn_timer_task = Some(cx.spawn(async move |this, cx| {
 813            loop {
 814                cx.background_executor().timer(Duration::from_secs(1)).await;
 815                if this.update(cx, |_, cx| cx.notify()).is_err() {
 816                    break;
 817                }
 818            }
 819        }));
 820        generation
 821    }
 822
 823    pub fn stop_turn(&mut self, generation: usize, _cx: &mut Context<Self>) {
 824        if self.turn_fields.turn_generation != generation {
 825            return;
 826        }
 827        self.turn_fields.last_turn_duration = self
 828            .turn_fields
 829            .turn_started_at
 830            .take()
 831            .map(|started| started.elapsed());
 832        self.turn_fields.last_turn_tokens = self.turn_fields.turn_tokens.take();
 833        self.turn_fields._turn_timer_task = None;
 834    }
 835
 836    pub fn update_turn_tokens(&mut self, cx: &App) {
 837        if let Some(usage) = self.thread.read(cx).token_usage() {
 838            if let Some(tokens) = &mut self.turn_fields.turn_tokens {
 839                *tokens += usage.output_tokens;
 840            }
 841        }
 842    }
 843
 844    // sending
 845
 846    fn clear_external_source_prompt_warning(&mut self, cx: &mut Context<Self>) {
 847        if self.show_external_source_prompt_warning {
 848            self.show_external_source_prompt_warning = false;
 849            cx.notify();
 850        }
 851    }
 852
 853    pub fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 854        let thread = &self.thread;
 855
 856        if self.is_loading_contents {
 857            return;
 858        }
 859
 860        let message_editor = self.message_editor.clone();
 861
 862        // Intercept the first send so the agent panel can capture the full
 863        // content blocks — needed for "Start thread in New Worktree",
 864        // which must create a workspace before sending the message there.
 865        let intercept_first_send = self.thread.read(cx).entries().is_empty()
 866            && !message_editor.read(cx).is_empty(cx)
 867            && self
 868                .workspace
 869                .upgrade()
 870                .and_then(|workspace| workspace.read(cx).panel::<AgentPanel>(cx))
 871                .is_some_and(|panel| {
 872                    !matches!(
 873                        panel.read(cx).start_thread_in(),
 874                        StartThreadIn::LocalProject
 875                    )
 876                });
 877
 878        if intercept_first_send {
 879            cx.emit(AcpThreadViewEvent::MessageSentOrQueued);
 880            let content_task = self.resolve_message_contents(&message_editor, cx);
 881
 882            cx.spawn(async move |this, cx| match content_task.await {
 883                Ok((content, _tracked_buffers)) => {
 884                    if content.is_empty() {
 885                        return;
 886                    }
 887
 888                    this.update(cx, |_, cx| {
 889                        cx.emit(AcpThreadViewEvent::FirstSendRequested { content });
 890                    })
 891                    .ok();
 892                }
 893                Err(error) => {
 894                    this.update(cx, |this, cx| {
 895                        this.handle_thread_error(error, cx);
 896                    })
 897                    .ok();
 898                }
 899            })
 900            .detach();
 901
 902            return;
 903        }
 904
 905        let is_editor_empty = message_editor.read(cx).is_empty(cx);
 906        let is_generating = thread.read(cx).status() != ThreadStatus::Idle;
 907
 908        let has_queued = self.has_queued_messages();
 909        if is_editor_empty && self.can_fast_track_queue && has_queued {
 910            self.can_fast_track_queue = false;
 911            cx.emit(AcpThreadViewEvent::MessageSentOrQueued);
 912            self.send_queued_message_at_index(0, true, window, cx);
 913            return;
 914        }
 915
 916        if is_editor_empty {
 917            return;
 918        }
 919
 920        if is_generating {
 921            cx.emit(AcpThreadViewEvent::MessageSentOrQueued);
 922            self.queue_message(message_editor, window, cx);
 923            return;
 924        }
 925
 926        let text = message_editor.read(cx).text(cx);
 927        let text = text.trim();
 928        if text == "/login" || text == "/logout" {
 929            let connection = thread.read(cx).connection().clone();
 930            let can_login = !connection.auth_methods().is_empty();
 931            // Does the agent have a specific logout command? Prefer that in case they need to reset internal state.
 932            let logout_supported = text == "/logout"
 933                && self
 934                    .session_capabilities
 935                    .read()
 936                    .available_commands()
 937                    .iter()
 938                    .any(|command| command.name == "logout");
 939            if can_login && !logout_supported {
 940                message_editor.update(cx, |editor, cx| editor.clear(window, cx));
 941                self.clear_external_source_prompt_warning(cx);
 942
 943                let connection = self.thread.read(cx).connection().clone();
 944                window.defer(cx, {
 945                    let agent_id = self.agent_id.clone();
 946                    let server_view = self.server_view.clone();
 947                    move |window, cx| {
 948                        ConversationView::handle_auth_required(
 949                            server_view.clone(),
 950                            AuthRequired::new(),
 951                            agent_id,
 952                            connection,
 953                            window,
 954                            cx,
 955                        );
 956                    }
 957                });
 958                cx.notify();
 959                return;
 960            }
 961        }
 962
 963        cx.emit(AcpThreadViewEvent::MessageSentOrQueued);
 964        self.send_impl(message_editor, window, cx)
 965    }
 966
 967    pub fn send_impl(
 968        &mut self,
 969        message_editor: Entity<MessageEditor>,
 970        window: &mut Window,
 971        cx: &mut Context<Self>,
 972    ) {
 973        let contents = self.resolve_message_contents(&message_editor, cx);
 974
 975        self.thread_error.take();
 976        self.thread_feedback.clear();
 977        self.editing_message.take();
 978
 979        if self.should_be_following {
 980            self.workspace
 981                .update(cx, |workspace, cx| {
 982                    workspace.follow(CollaboratorId::Agent, window, cx);
 983                })
 984                .ok();
 985        }
 986
 987        let contents_task = cx.spawn_in(window, async move |_this, cx| {
 988            let (contents, tracked_buffers) = contents.await?;
 989
 990            if contents.is_empty() {
 991                return Ok(None);
 992            }
 993
 994            let _ = cx.update(|window, cx| {
 995                message_editor.update(cx, |message_editor, cx| {
 996                    message_editor.clear(window, cx);
 997                });
 998            });
 999
1000            Ok(Some((contents, tracked_buffers)))
1001        });
1002
1003        self.send_content(contents_task, window, cx);
1004    }
1005
1006    pub fn send_content(
1007        &mut self,
1008        contents_task: Task<anyhow::Result<Option<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>>>,
1009        window: &mut Window,
1010        cx: &mut Context<Self>,
1011    ) {
1012        let session_id = self.thread.read(cx).session_id().clone();
1013        let parent_session_id = self.thread.read(cx).parent_session_id().cloned();
1014        let agent_telemetry_id = self.thread.read(cx).connection().telemetry_id();
1015        let is_first_message = self.thread.read(cx).entries().is_empty();
1016        let thread = self.thread.downgrade();
1017
1018        self.is_loading_contents = true;
1019
1020        let model_id = self.current_model_id(cx);
1021        let mode_id = self.current_mode_id(cx);
1022        let guard = cx.new(|_| ());
1023        cx.observe_release(&guard, |this, _guard, cx| {
1024            this.is_loading_contents = false;
1025            cx.notify();
1026        })
1027        .detach();
1028
1029        let task = cx.spawn_in(window, async move |this, cx| {
1030            let Some((contents, tracked_buffers)) = contents_task.await? else {
1031                return Ok(());
1032            };
1033
1034            let generation = this.update(cx, |this, cx| {
1035                this.clear_external_source_prompt_warning(cx);
1036                let generation = this.start_turn(cx);
1037                this.in_flight_prompt = Some(contents.clone());
1038                generation
1039            })?;
1040
1041            this.update_in(cx, |this, _window, cx| {
1042                this.set_editor_is_expanded(false, cx);
1043            })?;
1044
1045            let _ = this.update(cx, |this, cx| {
1046                this.list_state.scroll_to_end();
1047                cx.notify();
1048            });
1049
1050            let _stop_turn = defer({
1051                let this = this.clone();
1052                let mut cx = cx.clone();
1053                move || {
1054                    this.update(&mut cx, |this, cx| {
1055                        this.stop_turn(generation, cx);
1056                        cx.notify();
1057                    })
1058                    .ok();
1059                }
1060            });
1061            if is_first_message && thread.read_with(cx, |thread, _cx| thread.title().is_none())? {
1062                let text: String = contents
1063                    .iter()
1064                    .filter_map(|block| match block {
1065                        acp::ContentBlock::Text(text_content) => Some(text_content.text.clone()),
1066                        acp::ContentBlock::ResourceLink(resource_link) => {
1067                            Some(format!("@{}", resource_link.name))
1068                        }
1069                        _ => None,
1070                    })
1071                    .collect::<Vec<_>>()
1072                    .join(" ");
1073                let text = text.lines().next().unwrap_or("").trim();
1074                if !text.is_empty() {
1075                    let title: SharedString = util::truncate_and_trailoff(text, 200).into();
1076                    thread.update(cx, |thread, cx| {
1077                        thread.set_provisional_title(title, cx);
1078                    })?;
1079                }
1080            }
1081
1082            let turn_start_time = Instant::now();
1083            let send = thread.update(cx, |thread, cx| {
1084                thread.action_log().update(cx, |action_log, cx| {
1085                    for buffer in tracked_buffers {
1086                        action_log.buffer_read(buffer, cx)
1087                    }
1088                });
1089                drop(guard);
1090
1091                telemetry::event!(
1092                    "Agent Message Sent",
1093                    agent = agent_telemetry_id,
1094                    session = session_id,
1095                    parent_session_id = parent_session_id.as_ref().map(|id| id.to_string()),
1096                    model = model_id,
1097                    mode = mode_id
1098                );
1099
1100                thread.send(contents, cx)
1101            })?;
1102
1103            let _ = this.update(cx, |this, cx| {
1104                this.sync_generating_indicator(cx);
1105                cx.notify();
1106            });
1107
1108            let res = send.await;
1109            let turn_time_ms = turn_start_time.elapsed().as_millis();
1110            drop(_stop_turn);
1111            let status = if res.is_ok() {
1112                let _ = this.update(cx, |this, _| this.in_flight_prompt.take());
1113                "success"
1114            } else {
1115                "failure"
1116            };
1117            telemetry::event!(
1118                "Agent Turn Completed",
1119                agent = agent_telemetry_id,
1120                session = session_id,
1121                parent_session_id = parent_session_id.as_ref().map(|id| id.to_string()),
1122                model = model_id,
1123                mode = mode_id,
1124                status,
1125                turn_time_ms,
1126            );
1127            res.map(|_| ())
1128        });
1129
1130        cx.spawn(async move |this, cx| {
1131            if let Err(err) = task.await {
1132                this.update(cx, |this, cx| {
1133                    this.handle_thread_error(err, cx);
1134                })
1135                .ok();
1136            } else {
1137                this.update(cx, |this, cx| {
1138                    let should_be_following = this
1139                        .workspace
1140                        .update(cx, |workspace, _| {
1141                            workspace.is_being_followed(CollaboratorId::Agent)
1142                        })
1143                        .unwrap_or_default();
1144                    this.should_be_following = should_be_following;
1145                })
1146                .ok();
1147            }
1148        })
1149        .detach();
1150    }
1151
1152    pub fn interrupt_and_send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1153        let thread = &self.thread;
1154
1155        if self.is_loading_contents {
1156            return;
1157        }
1158
1159        let message_editor = self.message_editor.clone();
1160        if thread.read(cx).status() == ThreadStatus::Idle {
1161            self.send_impl(message_editor, window, cx);
1162            return;
1163        }
1164
1165        self.stop_current_and_send_new_message(message_editor, window, cx);
1166    }
1167
1168    fn stop_current_and_send_new_message(
1169        &mut self,
1170        message_editor: Entity<MessageEditor>,
1171        window: &mut Window,
1172        cx: &mut Context<Self>,
1173    ) {
1174        let thread = self.thread.clone();
1175        self.skip_queue_processing_count = 0;
1176        self.user_interrupted_generation = true;
1177
1178        let cancelled = thread.update(cx, |thread, cx| thread.cancel(cx));
1179
1180        cx.spawn_in(window, async move |this, cx| {
1181            cancelled.await;
1182
1183            this.update_in(cx, |this, window, cx| {
1184                this.send_impl(message_editor, window, cx);
1185            })
1186            .ok();
1187        })
1188        .detach();
1189    }
1190
1191    pub(crate) fn handle_thread_error(
1192        &mut self,
1193        error: impl Into<ThreadError>,
1194        cx: &mut Context<Self>,
1195    ) {
1196        let error = error.into();
1197        self.emit_thread_error_telemetry(&error, cx);
1198        self.thread_error = Some(error);
1199        cx.notify();
1200    }
1201
1202    fn emit_thread_error_telemetry(&self, error: &ThreadError, cx: &mut Context<Self>) {
1203        let (error_kind, acp_error_code, message): (&str, Option<SharedString>, SharedString) =
1204            match error {
1205                ThreadError::PaymentRequired => (
1206                    "payment_required",
1207                    None,
1208                    "You reached your free usage limit. Upgrade to Zed Pro for more prompts."
1209                        .into(),
1210                ),
1211                ThreadError::Refusal => {
1212                    let model_or_agent_name = self.current_model_name(cx);
1213                    let message = format!(
1214                        "{} 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.",
1215                        model_or_agent_name
1216                    );
1217                    ("refusal", None, message.into())
1218                }
1219                ThreadError::AuthenticationRequired(message) => {
1220                    ("authentication_required", None, message.clone())
1221                }
1222                ThreadError::Other {
1223                    acp_error_code,
1224                    message,
1225                } => ("other", acp_error_code.clone(), message.clone()),
1226            };
1227
1228        let agent_telemetry_id = self.thread.read(cx).connection().telemetry_id();
1229        let session_id = self.thread.read(cx).session_id().clone();
1230        let parent_session_id = self
1231            .thread
1232            .read(cx)
1233            .parent_session_id()
1234            .map(|id| id.to_string());
1235
1236        telemetry::event!(
1237            "Agent Panel Error Shown",
1238            agent = agent_telemetry_id,
1239            session_id = session_id,
1240            parent_session_id = parent_session_id,
1241            kind = error_kind,
1242            acp_error_code = acp_error_code,
1243            message = message,
1244        );
1245    }
1246
1247    pub fn cancel_generation(&mut self, cx: &mut Context<Self>) {
1248        self.thread_retry_status.take();
1249        self.thread_error.take();
1250        self.user_interrupted_generation = true;
1251        self._cancel_task = Some(self.thread.update(cx, |thread, cx| thread.cancel(cx)));
1252        self.sync_generating_indicator(cx);
1253        cx.notify();
1254    }
1255
1256    pub fn retry_generation(&mut self, cx: &mut Context<Self>) {
1257        self.thread_error.take();
1258
1259        let thread = &self.thread;
1260        if !thread.read(cx).can_retry(cx) {
1261            return;
1262        }
1263
1264        let task = thread.update(cx, |thread, cx| thread.retry(cx));
1265        self.sync_generating_indicator(cx);
1266        cx.notify();
1267        cx.spawn(async move |this, cx| {
1268            let result = task.await;
1269
1270            this.update(cx, |this, cx| {
1271                if let Err(err) = result {
1272                    this.handle_thread_error(err, cx);
1273                }
1274            })
1275        })
1276        .detach();
1277    }
1278
1279    pub fn regenerate(
1280        &mut self,
1281        entry_ix: usize,
1282        message_editor: Entity<MessageEditor>,
1283        window: &mut Window,
1284        cx: &mut Context<Self>,
1285    ) {
1286        if self.is_loading_contents {
1287            return;
1288        }
1289        let thread = self.thread.clone();
1290
1291        let Some(user_message_id) = thread.update(cx, |thread, _| {
1292            thread.entries().get(entry_ix)?.user_message()?.id.clone()
1293        }) else {
1294            return;
1295        };
1296
1297        cx.spawn_in(window, async move |this, cx| {
1298            // Check if there are any edits from prompts before the one being regenerated.
1299            //
1300            // If there are, we keep/accept them since we're not regenerating the prompt that created them.
1301            //
1302            // If editing the prompt that generated the edits, they are auto-rejected
1303            // through the `rewind` function in the `acp_thread`.
1304            let has_earlier_edits = thread.read_with(cx, |thread, _| {
1305                thread
1306                    .entries()
1307                    .iter()
1308                    .take(entry_ix)
1309                    .any(|entry| entry.diffs().next().is_some())
1310            });
1311
1312            if has_earlier_edits {
1313                thread.update(cx, |thread, cx| {
1314                    thread.action_log().update(cx, |action_log, cx| {
1315                        action_log.keep_all_edits(None, cx);
1316                    });
1317                });
1318            }
1319
1320            thread
1321                .update(cx, |thread, cx| thread.rewind(user_message_id, cx))
1322                .await?;
1323            this.update_in(cx, |thread, window, cx| {
1324                thread.send_impl(message_editor, window, cx);
1325                thread.focus_handle(cx).focus(window, cx);
1326            })?;
1327            anyhow::Ok(())
1328        })
1329        .detach_and_log_err(cx);
1330    }
1331
1332    // message queueing
1333
1334    fn queue_message(
1335        &mut self,
1336        message_editor: Entity<MessageEditor>,
1337        window: &mut Window,
1338        cx: &mut Context<Self>,
1339    ) {
1340        let is_idle = self.thread.read(cx).status() == acp_thread::ThreadStatus::Idle;
1341
1342        if is_idle {
1343            self.send_impl(message_editor, window, cx);
1344            return;
1345        }
1346
1347        let contents = self.resolve_message_contents(&message_editor, cx);
1348
1349        cx.spawn_in(window, async move |this, cx| {
1350            let (content, tracked_buffers) = contents.await?;
1351
1352            if content.is_empty() {
1353                return Ok::<(), anyhow::Error>(());
1354            }
1355
1356            this.update_in(cx, |this, window, cx| {
1357                this.add_to_queue(content, tracked_buffers, cx);
1358                this.can_fast_track_queue = true;
1359                message_editor.update(cx, |message_editor, cx| {
1360                    message_editor.clear(window, cx);
1361                });
1362                cx.notify();
1363            })?;
1364            Ok(())
1365        })
1366        .detach_and_log_err(cx);
1367    }
1368
1369    pub fn add_to_queue(
1370        &mut self,
1371        content: Vec<acp::ContentBlock>,
1372        tracked_buffers: Vec<Entity<Buffer>>,
1373        cx: &mut Context<Self>,
1374    ) {
1375        self.local_queued_messages.push(QueuedMessage {
1376            content,
1377            tracked_buffers,
1378        });
1379        self.sync_queue_flag_to_native_thread(cx);
1380    }
1381
1382    pub fn remove_from_queue(
1383        &mut self,
1384        index: usize,
1385        cx: &mut Context<Self>,
1386    ) -> Option<QueuedMessage> {
1387        if index < self.local_queued_messages.len() {
1388            let removed = self.local_queued_messages.remove(index);
1389            self.sync_queue_flag_to_native_thread(cx);
1390            Some(removed)
1391        } else {
1392            None
1393        }
1394    }
1395
1396    pub fn sync_queue_flag_to_native_thread(&self, cx: &mut Context<Self>) {
1397        if let Some(native_thread) = self.as_native_thread(cx) {
1398            let has_queued = self.has_queued_messages();
1399            native_thread.update(cx, |thread, _| {
1400                thread.set_has_queued_message(has_queued);
1401            });
1402        }
1403    }
1404
1405    pub fn send_queued_message_at_index(
1406        &mut self,
1407        index: usize,
1408        is_send_now: bool,
1409        window: &mut Window,
1410        cx: &mut Context<Self>,
1411    ) {
1412        let Some(queued) = self.remove_from_queue(index, cx) else {
1413            return;
1414        };
1415        let content = queued.content;
1416        let tracked_buffers = queued.tracked_buffers;
1417
1418        // Only increment skip count for "Send Now" operations (out-of-order sends)
1419        // Normal auto-processing from the Stopped handler doesn't need to skip.
1420        // We only skip the Stopped event from the cancelled generation, NOT the
1421        // Stopped event from the newly sent message (which should trigger queue processing).
1422        if is_send_now {
1423            let is_generating =
1424                self.thread.read(cx).status() == acp_thread::ThreadStatus::Generating;
1425            self.skip_queue_processing_count += if is_generating { 1 } else { 0 };
1426        }
1427
1428        let cancelled = self.thread.update(cx, |thread, cx| thread.cancel(cx));
1429
1430        let workspace = self.workspace.clone();
1431
1432        let should_be_following = self.should_be_following;
1433        let contents_task = cx.spawn_in(window, async move |_this, cx| {
1434            cancelled.await;
1435            if should_be_following {
1436                workspace
1437                    .update_in(cx, |workspace, window, cx| {
1438                        workspace.follow(CollaboratorId::Agent, window, cx);
1439                    })
1440                    .ok();
1441            }
1442
1443            Ok(Some((content, tracked_buffers)))
1444        });
1445
1446        self.send_content(contents_task, window, cx);
1447    }
1448
1449    pub fn move_queued_message_to_main_editor(
1450        &mut self,
1451        index: usize,
1452        inserted_text: Option<&str>,
1453        cursor_offset: Option<usize>,
1454        window: &mut Window,
1455        cx: &mut Context<Self>,
1456    ) -> bool {
1457        let Some(queued_message) = self.remove_from_queue(index, cx) else {
1458            return false;
1459        };
1460        let queued_content = queued_message.content;
1461        let message_editor = self.message_editor.clone();
1462        let inserted_text = inserted_text.map(ToOwned::to_owned);
1463
1464        window.focus(&message_editor.focus_handle(cx), cx);
1465
1466        if message_editor.read(cx).is_empty(cx) {
1467            message_editor.update(cx, |editor, cx| {
1468                editor.set_message(queued_content, window, cx);
1469                if let Some(offset) = cursor_offset {
1470                    editor.set_cursor_offset(offset, window, cx);
1471                }
1472                if let Some(inserted_text) = inserted_text.as_deref() {
1473                    editor.insert_text(inserted_text, window, cx);
1474                }
1475            });
1476            cx.notify();
1477            return true;
1478        }
1479
1480        // Adjust cursor offset accounting for existing content
1481        let existing_len = message_editor.read(cx).text(cx).len();
1482        let separator = "\n\n";
1483
1484        message_editor.update(cx, |editor, cx| {
1485            editor.append_message(queued_content, Some(separator), window, cx);
1486            if let Some(offset) = cursor_offset {
1487                let adjusted_offset = existing_len + separator.len() + offset;
1488                editor.set_cursor_offset(adjusted_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
1495        cx.notify();
1496        true
1497    }
1498
1499    // editor methods
1500
1501    pub fn expand_message_editor(
1502        &mut self,
1503        _: &ExpandMessageEditor,
1504        _window: &mut Window,
1505        cx: &mut Context<Self>,
1506    ) {
1507        self.set_editor_is_expanded(!self.editor_expanded, cx);
1508        cx.stop_propagation();
1509        cx.notify();
1510    }
1511
1512    pub fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {
1513        self.editor_expanded = is_expanded;
1514        self.message_editor.update(cx, |editor, cx| {
1515            if is_expanded {
1516                editor.set_mode(
1517                    EditorMode::Full {
1518                        scale_ui_elements_with_buffer_font_size: false,
1519                        show_active_line_background: false,
1520                        sizing_behavior: SizingBehavior::ExcludeOverscrollMargin,
1521                    },
1522                    cx,
1523                )
1524            } else {
1525                let agent_settings = AgentSettings::get_global(cx);
1526                editor.set_mode(
1527                    EditorMode::AutoHeight {
1528                        min_lines: agent_settings.message_editor_min_lines,
1529                        max_lines: Some(agent_settings.set_message_editor_max_lines()),
1530                    },
1531                    cx,
1532                )
1533            }
1534        });
1535        cx.notify();
1536    }
1537
1538    pub fn handle_title_editor_event(
1539        &mut self,
1540        title_editor: &Entity<Editor>,
1541        event: &EditorEvent,
1542        window: &mut Window,
1543        cx: &mut Context<Self>,
1544    ) {
1545        let thread = &self.thread;
1546
1547        match event {
1548            EditorEvent::BufferEdited => {
1549                // We only want to set the title if the user has actively edited
1550                // it. If the title editor is not focused, we programmatically
1551                // changed the text, so we don't want to set the title again.
1552                if !title_editor.read(cx).is_focused(window) {
1553                    return;
1554                }
1555
1556                let new_title = title_editor.read(cx).text(cx);
1557                thread.update(cx, |thread, cx| {
1558                    thread
1559                        .set_title(new_title.into(), cx)
1560                        .detach_and_log_err(cx);
1561                })
1562            }
1563            EditorEvent::Blurred => {
1564                if title_editor.read(cx).text(cx).is_empty() {
1565                    title_editor.update(cx, |editor, cx| {
1566                        editor.set_text(DEFAULT_THREAD_TITLE, window, cx);
1567                    });
1568                }
1569            }
1570            _ => {}
1571        }
1572    }
1573
1574    pub fn cancel_editing(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
1575        if let Some(index) = self.editing_message.take()
1576            && let Some(editor) = &self
1577                .entry_view_state
1578                .read(cx)
1579                .entry(index)
1580                .and_then(|e| e.message_editor())
1581                .cloned()
1582        {
1583            editor.update(cx, |editor, cx| {
1584                if let Some(user_message) = self
1585                    .thread
1586                    .read(cx)
1587                    .entries()
1588                    .get(index)
1589                    .and_then(|e| e.user_message())
1590                {
1591                    editor.set_message(user_message.chunks.clone(), window, cx);
1592                }
1593            })
1594        };
1595        self.message_editor.focus_handle(cx).focus(window, cx);
1596        cx.notify();
1597    }
1598
1599    pub fn authorize_tool_call(
1600        &mut self,
1601        session_id: acp::SessionId,
1602        tool_call_id: acp::ToolCallId,
1603        outcome: SelectedPermissionOutcome,
1604        window: &mut Window,
1605        cx: &mut Context<Self>,
1606    ) {
1607        self.conversation.update(cx, |conversation, cx| {
1608            conversation.authorize_tool_call(session_id, tool_call_id, outcome, cx);
1609        });
1610        if self.should_be_following {
1611            self.workspace
1612                .update(cx, |workspace, cx| {
1613                    workspace.follow(CollaboratorId::Agent, window, cx);
1614                })
1615                .ok();
1616        }
1617        cx.notify();
1618    }
1619
1620    pub fn allow_always(&mut self, _: &AllowAlways, window: &mut Window, cx: &mut Context<Self>) {
1621        self.authorize_pending_tool_call(acp::PermissionOptionKind::AllowAlways, window, cx);
1622    }
1623
1624    pub fn allow_once(&mut self, _: &AllowOnce, window: &mut Window, cx: &mut Context<Self>) {
1625        self.authorize_pending_with_granularity(true, window, cx);
1626    }
1627
1628    pub fn reject_once(&mut self, _: &RejectOnce, window: &mut Window, cx: &mut Context<Self>) {
1629        self.authorize_pending_with_granularity(false, window, cx);
1630    }
1631
1632    pub fn authorize_pending_tool_call(
1633        &mut self,
1634        kind: acp::PermissionOptionKind,
1635        window: &mut Window,
1636        cx: &mut Context<Self>,
1637    ) -> Option<()> {
1638        self.conversation.update(cx, |conversation, cx| {
1639            conversation.authorize_pending_tool_call(&self.id, kind, cx)
1640        })?;
1641        if self.should_be_following {
1642            self.workspace
1643                .update(cx, |workspace, cx| {
1644                    workspace.follow(CollaboratorId::Agent, window, cx);
1645                })
1646                .ok();
1647        }
1648        cx.notify();
1649        Some(())
1650    }
1651
1652    fn is_waiting_for_confirmation(entry: &AgentThreadEntry) -> bool {
1653        if let AgentThreadEntry::ToolCall(tool_call) = entry {
1654            matches!(
1655                tool_call.status,
1656                ToolCallStatus::WaitingForConfirmation { .. }
1657            )
1658        } else {
1659            false
1660        }
1661    }
1662
1663    fn handle_authorize_tool_call(
1664        &mut self,
1665        action: &AuthorizeToolCall,
1666        window: &mut Window,
1667        cx: &mut Context<Self>,
1668    ) {
1669        let tool_call_id = acp::ToolCallId::new(action.tool_call_id.clone());
1670        let option_id = acp::PermissionOptionId::new(action.option_id.clone());
1671        let option_kind = match action.option_kind.as_str() {
1672            "AllowOnce" => acp::PermissionOptionKind::AllowOnce,
1673            "AllowAlways" => acp::PermissionOptionKind::AllowAlways,
1674            "RejectOnce" => acp::PermissionOptionKind::RejectOnce,
1675            "RejectAlways" => acp::PermissionOptionKind::RejectAlways,
1676            _ => acp::PermissionOptionKind::AllowOnce,
1677        };
1678
1679        self.authorize_tool_call(
1680            self.id.clone(),
1681            tool_call_id,
1682            SelectedPermissionOutcome::new(option_id, option_kind),
1683            window,
1684            cx,
1685        );
1686    }
1687
1688    pub fn handle_select_permission_granularity(
1689        &mut self,
1690        action: &SelectPermissionGranularity,
1691        _window: &mut Window,
1692        cx: &mut Context<Self>,
1693    ) {
1694        let tool_call_id = acp::ToolCallId::new(action.tool_call_id.clone());
1695        self.permission_selections
1696            .insert(tool_call_id, PermissionSelection::Choice(action.index));
1697
1698        cx.notify();
1699    }
1700
1701    pub fn handle_toggle_command_pattern(
1702        &mut self,
1703        action: &crate::ToggleCommandPattern,
1704        _window: &mut Window,
1705        cx: &mut Context<Self>,
1706    ) {
1707        let tool_call_id = acp::ToolCallId::new(action.tool_call_id.clone());
1708
1709        match self.permission_selections.get_mut(&tool_call_id) {
1710            Some(PermissionSelection::SelectedPatterns(checked)) => {
1711                // Already in pattern mode — toggle the individual pattern.
1712                if let Some(pos) = checked.iter().position(|&i| i == action.pattern_index) {
1713                    checked.swap_remove(pos);
1714                } else {
1715                    checked.push(action.pattern_index);
1716                }
1717            }
1718            _ => {
1719                // First click: activate "Select options" with all patterns checked.
1720                let thread = self.thread.read(cx);
1721                let pattern_count = thread
1722                    .entries()
1723                    .iter()
1724                    .find_map(|entry| {
1725                        if let AgentThreadEntry::ToolCall(call) = entry {
1726                            if call.id == tool_call_id {
1727                                if let ToolCallStatus::WaitingForConfirmation { options, .. } =
1728                                    &call.status
1729                                {
1730                                    if let PermissionOptions::DropdownWithPatterns {
1731                                        patterns,
1732                                        ..
1733                                    } = options
1734                                    {
1735                                        return Some(patterns.len());
1736                                    }
1737                                }
1738                            }
1739                        }
1740                        None
1741                    })
1742                    .unwrap_or(0);
1743                self.permission_selections.insert(
1744                    tool_call_id,
1745                    PermissionSelection::SelectedPatterns((0..pattern_count).collect()),
1746                );
1747            }
1748        }
1749        cx.notify();
1750    }
1751
1752    fn authorize_pending_with_granularity(
1753        &mut self,
1754        is_allow: bool,
1755        window: &mut Window,
1756        cx: &mut Context<Self>,
1757    ) -> Option<()> {
1758        let (session_id, tool_call_id, options) =
1759            self.conversation.read(cx).pending_tool_call(&self.id, cx)?;
1760        let options = options.clone();
1761        self.authorize_with_granularity(session_id, tool_call_id, &options, is_allow, window, cx)
1762    }
1763
1764    fn authorize_with_granularity(
1765        &mut self,
1766        session_id: acp::SessionId,
1767        tool_call_id: acp::ToolCallId,
1768        options: &PermissionOptions,
1769        is_allow: bool,
1770        window: &mut Window,
1771        cx: &mut Context<Self>,
1772    ) -> Option<()> {
1773        let choices = match options {
1774            PermissionOptions::Dropdown(choices) => choices.as_slice(),
1775            PermissionOptions::DropdownWithPatterns { choices, .. } => choices.as_slice(),
1776            _ => {
1777                let kind = if is_allow {
1778                    acp::PermissionOptionKind::AllowOnce
1779                } else {
1780                    acp::PermissionOptionKind::RejectOnce
1781                };
1782                return self.authorize_pending_tool_call(kind, window, cx);
1783            }
1784        };
1785
1786        let selection = self.permission_selections.get(&tool_call_id);
1787
1788        // When in per-command pattern mode, use the checked patterns.
1789        if let Some(PermissionSelection::SelectedPatterns(checked)) = selection {
1790            if let Some(outcome) = options.build_outcome_for_checked_patterns(checked, is_allow) {
1791                self.authorize_tool_call(session_id, tool_call_id, outcome, window, cx);
1792                return Some(());
1793            }
1794        }
1795
1796        // Use the selected granularity choice ("Always for terminal" or "Only this time")
1797        let selected_index = selection
1798            .and_then(|s| s.choice_index())
1799            .unwrap_or_else(|| choices.len().saturating_sub(1));
1800
1801        let selected_choice = choices.get(selected_index).or(choices.last())?;
1802        let outcome = selected_choice.build_outcome(is_allow);
1803
1804        self.authorize_tool_call(session_id, tool_call_id, outcome, window, cx);
1805
1806        Some(())
1807    }
1808
1809    // edits
1810
1811    pub fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context<Self>) {
1812        let thread = &self.thread;
1813        let telemetry = ActionLogTelemetry::from(thread.read(cx));
1814        let action_log = thread.read(cx).action_log().clone();
1815        action_log.update(cx, |action_log, cx| {
1816            action_log.keep_all_edits(Some(telemetry), cx)
1817        });
1818    }
1819
1820    pub fn reject_all(&mut self, _: &RejectAll, _window: &mut Window, cx: &mut Context<Self>) {
1821        let thread = &self.thread;
1822        let telemetry = ActionLogTelemetry::from(thread.read(cx));
1823        let action_log = thread.read(cx).action_log().clone();
1824        let has_changes = action_log.read(cx).changed_buffers(cx).len() > 0;
1825
1826        action_log
1827            .update(cx, |action_log, cx| {
1828                action_log.reject_all_edits(Some(telemetry), cx)
1829            })
1830            .detach();
1831
1832        if has_changes {
1833            if let Some(workspace) = self.workspace.upgrade() {
1834                workspace.update(cx, |workspace, cx| {
1835                    crate::ui::show_undo_reject_toast(workspace, action_log, cx);
1836                });
1837            }
1838        }
1839    }
1840
1841    pub fn undo_last_reject(
1842        &mut self,
1843        _: &UndoLastReject,
1844        _window: &mut Window,
1845        cx: &mut Context<Self>,
1846    ) {
1847        let thread = &self.thread;
1848        let action_log = thread.read(cx).action_log().clone();
1849        action_log
1850            .update(cx, |action_log, cx| action_log.undo_last_reject(cx))
1851            .detach()
1852    }
1853
1854    pub fn open_edited_buffer(
1855        &mut self,
1856        buffer: &Entity<Buffer>,
1857        window: &mut Window,
1858        cx: &mut Context<Self>,
1859    ) {
1860        let thread = &self.thread;
1861
1862        let Some(diff) =
1863            AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err()
1864        else {
1865            return;
1866        };
1867
1868        diff.update(cx, |diff, cx| {
1869            diff.move_to_path(PathKey::for_buffer(buffer, cx), window, cx)
1870        })
1871    }
1872
1873    // thread stuff
1874
1875    fn share_thread(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
1876        let Some((thread, project)) = self.as_native_thread(cx).zip(self.project.upgrade()) else {
1877            return;
1878        };
1879
1880        let client = project.read(cx).client();
1881        let workspace = self.workspace.clone();
1882        let session_id = thread.read(cx).id().to_string();
1883
1884        let load_task = thread.read(cx).to_db(cx);
1885
1886        cx.spawn(async move |_this, cx| {
1887            let db_thread = load_task.await;
1888
1889            let shared_thread = SharedThread::from_db_thread(&db_thread);
1890            let thread_data = shared_thread.to_bytes()?;
1891            let title = shared_thread.title.to_string();
1892
1893            client
1894                .request(proto::ShareAgentThread {
1895                    session_id: session_id.clone(),
1896                    title,
1897                    thread_data,
1898                })
1899                .await?;
1900
1901            let share_url = client::zed_urls::shared_agent_thread_url(&session_id);
1902
1903            cx.update(|cx| {
1904                if let Some(workspace) = workspace.upgrade() {
1905                    workspace.update(cx, |workspace, cx| {
1906                        struct ThreadSharedToast;
1907                        workspace.show_toast(
1908                            Toast::new(
1909                                NotificationId::unique::<ThreadSharedToast>(),
1910                                "Thread shared!",
1911                            )
1912                            .on_click(
1913                                "Copy URL",
1914                                move |_window, cx| {
1915                                    cx.write_to_clipboard(ClipboardItem::new_string(
1916                                        share_url.clone(),
1917                                    ));
1918                                },
1919                            ),
1920                            cx,
1921                        );
1922                    });
1923                }
1924            });
1925
1926            anyhow::Ok(())
1927        })
1928        .detach_and_log_err(cx);
1929    }
1930
1931    pub fn sync_thread(
1932        &mut self,
1933        project: Entity<Project>,
1934        server_view: Entity<ConversationView>,
1935        window: &mut Window,
1936        cx: &mut Context<Self>,
1937    ) {
1938        if !self.is_imported_thread(cx) {
1939            return;
1940        }
1941
1942        let Some(session_list) = self
1943            .as_native_connection(cx)
1944            .and_then(|connection| connection.session_list(cx))
1945            .and_then(|list| list.downcast::<NativeAgentSessionList>())
1946        else {
1947            return;
1948        };
1949        let thread_store = session_list.thread_store().clone();
1950
1951        let client = project.read(cx).client();
1952        let session_id = self.thread.read(cx).session_id().clone();
1953        cx.spawn_in(window, async move |this, cx| {
1954            let response = client
1955                .request(proto::GetSharedAgentThread {
1956                    session_id: session_id.to_string(),
1957                })
1958                .await?;
1959
1960            let shared_thread = SharedThread::from_bytes(&response.thread_data)?;
1961
1962            let db_thread = shared_thread.to_db_thread();
1963
1964            thread_store
1965                .update(&mut cx.clone(), |store, cx| {
1966                    store.save_thread(session_id.clone(), db_thread, Default::default(), cx)
1967                })
1968                .await?;
1969
1970            server_view.update_in(cx, |server_view, window, cx| server_view.reset(window, cx))?;
1971
1972            this.update_in(cx, |this, _window, cx| {
1973                if let Some(workspace) = this.workspace.upgrade() {
1974                    workspace.update(cx, |workspace, cx| {
1975                        struct ThreadSyncedToast;
1976                        workspace.show_toast(
1977                            Toast::new(
1978                                NotificationId::unique::<ThreadSyncedToast>(),
1979                                "Thread synced with latest version",
1980                            )
1981                            .autohide(),
1982                            cx,
1983                        );
1984                    });
1985                }
1986            })?;
1987
1988            anyhow::Ok(())
1989        })
1990        .detach_and_log_err(cx);
1991    }
1992
1993    pub fn restore_checkpoint(&mut self, message_id: &UserMessageId, cx: &mut Context<Self>) {
1994        self.thread
1995            .update(cx, |thread, cx| {
1996                thread.restore_checkpoint(message_id.clone(), cx)
1997            })
1998            .detach_and_log_err(cx);
1999    }
2000
2001    pub fn clear_thread_error(&mut self, cx: &mut Context<Self>) {
2002        self.thread_error = None;
2003        self.thread_error_markdown = None;
2004        self.token_limit_callout_dismissed = true;
2005        cx.notify();
2006    }
2007
2008    fn is_following(&self, cx: &App) -> bool {
2009        match self.thread.read(cx).status() {
2010            ThreadStatus::Generating => self
2011                .workspace
2012                .read_with(cx, |workspace, _| {
2013                    workspace.is_being_followed(CollaboratorId::Agent)
2014                })
2015                .unwrap_or(false),
2016            _ => self.should_be_following,
2017        }
2018    }
2019
2020    fn toggle_following(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2021        let following = self.is_following(cx);
2022
2023        self.should_be_following = !following;
2024        if self.thread.read(cx).status() == ThreadStatus::Generating {
2025            self.workspace
2026                .update(cx, |workspace, cx| {
2027                    if following {
2028                        workspace.unfollow(CollaboratorId::Agent, window, cx);
2029                    } else {
2030                        workspace.follow(CollaboratorId::Agent, window, cx);
2031                    }
2032                })
2033                .ok();
2034        }
2035
2036        telemetry::event!("Follow Agent Selected", following = !following);
2037    }
2038
2039    // other
2040
2041    pub fn render_thread_retry_status_callout(&self) -> Option<Callout> {
2042        let state = self.thread_retry_status.as_ref()?;
2043
2044        let next_attempt_in = state
2045            .duration
2046            .saturating_sub(Instant::now().saturating_duration_since(state.started_at));
2047        if next_attempt_in.is_zero() {
2048            return None;
2049        }
2050
2051        let next_attempt_in_secs = next_attempt_in.as_secs() + 1;
2052
2053        let retry_message = if state.max_attempts == 1 {
2054            if next_attempt_in_secs == 1 {
2055                "Retrying. Next attempt in 1 second.".to_string()
2056            } else {
2057                format!("Retrying. Next attempt in {next_attempt_in_secs} seconds.")
2058            }
2059        } else if next_attempt_in_secs == 1 {
2060            format!(
2061                "Retrying. Next attempt in 1 second (Attempt {} of {}).",
2062                state.attempt, state.max_attempts,
2063            )
2064        } else {
2065            format!(
2066                "Retrying. Next attempt in {next_attempt_in_secs} seconds (Attempt {} of {}).",
2067                state.attempt, state.max_attempts,
2068            )
2069        };
2070
2071        Some(
2072            Callout::new()
2073                .icon(IconName::Warning)
2074                .severity(Severity::Warning)
2075                .title(state.last_error.clone())
2076                .description(retry_message),
2077        )
2078    }
2079
2080    pub fn handle_open_rules(
2081        &mut self,
2082        _: &ClickEvent,
2083        window: &mut Window,
2084        cx: &mut Context<Self>,
2085    ) {
2086        let Some(thread) = self.as_native_thread(cx) else {
2087            return;
2088        };
2089        let project_context = thread.read(cx).project_context().read(cx);
2090
2091        let project_entry_ids = project_context
2092            .worktrees
2093            .iter()
2094            .flat_map(|worktree| worktree.rules_file.as_ref())
2095            .map(|rules_file| ProjectEntryId::from_usize(rules_file.project_entry_id))
2096            .collect::<Vec<_>>();
2097
2098        self.workspace
2099            .update(cx, move |workspace, cx| {
2100                // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
2101                // files clear. For example, if rules file 1 is already open but rules file 2 is not,
2102                // this would open and focus rules file 2 in a tab that is not next to rules file 1.
2103                let project = workspace.project().read(cx);
2104                let project_paths = project_entry_ids
2105                    .into_iter()
2106                    .flat_map(|entry_id| project.path_for_entry(entry_id, cx))
2107                    .collect::<Vec<_>>();
2108                for project_path in project_paths {
2109                    workspace
2110                        .open_path(project_path, None, true, window, cx)
2111                        .detach_and_log_err(cx);
2112                }
2113            })
2114            .ok();
2115    }
2116
2117    fn activity_bar_bg(&self, cx: &Context<Self>) -> Hsla {
2118        let editor_bg_color = cx.theme().colors().editor_background;
2119        let active_color = cx.theme().colors().element_selected;
2120        editor_bg_color.blend(active_color.opacity(0.3))
2121    }
2122
2123    pub fn render_activity_bar(
2124        &self,
2125        window: &mut Window,
2126        cx: &Context<Self>,
2127    ) -> Option<AnyElement> {
2128        let thread = self.thread.read(cx);
2129        let action_log = thread.action_log();
2130        let telemetry = ActionLogTelemetry::from(thread);
2131        let changed_buffers = action_log.read(cx).changed_buffers(cx);
2132        let plan = thread.plan();
2133        let queue_is_empty = !self.has_queued_messages();
2134
2135        let subagents_awaiting_permission = self.render_subagents_awaiting_permission(cx);
2136        let has_subagents_awaiting = subagents_awaiting_permission.is_some();
2137
2138        if changed_buffers.is_empty()
2139            && plan.is_empty()
2140            && queue_is_empty
2141            && !has_subagents_awaiting
2142        {
2143            return None;
2144        }
2145
2146        // Temporarily always enable ACP edit controls. This is temporary, to lessen the
2147        // impact of a nasty bug that causes them to sometimes be disabled when they shouldn't
2148        // be, which blocks you from being able to accept or reject edits. This switches the
2149        // bug to be that sometimes it's enabled when it shouldn't be, which at least doesn't
2150        // block you from using the panel.
2151        let pending_edits = false;
2152
2153        let plan_expanded = self.plan_expanded;
2154        let edits_expanded = self.edits_expanded;
2155        let queue_expanded = self.queue_expanded;
2156
2157        v_flex()
2158            .mx_2()
2159            .bg(self.activity_bar_bg(cx))
2160            .border_1()
2161            .border_b_0()
2162            .border_color(cx.theme().colors().border)
2163            .rounded_t_md()
2164            .shadow(vec![gpui::BoxShadow {
2165                color: gpui::black().opacity(0.12),
2166                offset: point(px(1.), px(-1.)),
2167                blur_radius: px(2.),
2168                spread_radius: px(0.),
2169            }])
2170            .when_some(subagents_awaiting_permission, |this, element| {
2171                this.child(element)
2172            })
2173            .when(
2174                has_subagents_awaiting
2175                    && (!plan.is_empty() || !changed_buffers.is_empty() || !queue_is_empty),
2176                |this| this.child(Divider::horizontal().color(DividerColor::Border)),
2177            )
2178            .when(!plan.is_empty(), |this| {
2179                this.child(self.render_plan_summary(plan, window, cx))
2180                    .when(plan_expanded, |parent| {
2181                        parent.child(self.render_plan_entries(plan, window, cx))
2182                    })
2183            })
2184            .when(!plan.is_empty() && !changed_buffers.is_empty(), |this| {
2185                this.child(Divider::horizontal().color(DividerColor::Border))
2186            })
2187            .when(
2188                !changed_buffers.is_empty() && thread.parent_session_id().is_none(),
2189                |this| {
2190                    this.child(self.render_edits_summary(
2191                        &changed_buffers,
2192                        edits_expanded,
2193                        pending_edits,
2194                        cx,
2195                    ))
2196                    .when(edits_expanded, |parent| {
2197                        parent.child(self.render_edited_files(
2198                            action_log,
2199                            telemetry.clone(),
2200                            &changed_buffers,
2201                            pending_edits,
2202                            cx,
2203                        ))
2204                    })
2205                },
2206            )
2207            .when(!queue_is_empty, |this| {
2208                this.when(!plan.is_empty() || !changed_buffers.is_empty(), |this| {
2209                    this.child(Divider::horizontal().color(DividerColor::Border))
2210                })
2211                .child(self.render_message_queue_summary(window, cx))
2212                .when(queue_expanded, |parent| {
2213                    parent.child(self.render_message_queue_entries(window, cx))
2214                })
2215            })
2216            .into_any()
2217            .into()
2218    }
2219
2220    fn render_edited_files(
2221        &self,
2222        action_log: &Entity<ActionLog>,
2223        telemetry: ActionLogTelemetry,
2224        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
2225        pending_edits: bool,
2226        cx: &Context<Self>,
2227    ) -> impl IntoElement {
2228        let editor_bg_color = cx.theme().colors().editor_background;
2229
2230        // Sort edited files alphabetically for consistency with Git diff view
2231        let mut sorted_buffers: Vec<_> = changed_buffers.iter().collect();
2232        sorted_buffers.sort_by(|(buffer_a, _), (buffer_b, _)| {
2233            let path_a = buffer_a.read(cx).file().map(|f| f.path().clone());
2234            let path_b = buffer_b.read(cx).file().map(|f| f.path().clone());
2235            path_a.cmp(&path_b)
2236        });
2237
2238        v_flex()
2239            .id("edited_files_list")
2240            .max_h_40()
2241            .overflow_y_scroll()
2242            .children(
2243                sorted_buffers
2244                    .into_iter()
2245                    .enumerate()
2246                    .flat_map(|(index, (buffer, diff))| {
2247                        let file = buffer.read(cx).file()?;
2248                        let path = file.path();
2249                        let path_style = file.path_style(cx);
2250                        let separator = file.path_style(cx).primary_separator();
2251
2252                        let file_path = path.parent().and_then(|parent| {
2253                            if parent.is_empty() {
2254                                None
2255                            } else {
2256                                Some(
2257                                    Label::new(format!(
2258                                        "{}{separator}",
2259                                        parent.display(path_style)
2260                                    ))
2261                                    .color(Color::Muted)
2262                                    .size(LabelSize::XSmall)
2263                                    .buffer_font(cx),
2264                                )
2265                            }
2266                        });
2267
2268                        let file_name = path.file_name().map(|name| {
2269                            Label::new(name.to_string())
2270                                .size(LabelSize::XSmall)
2271                                .buffer_font(cx)
2272                                .ml_1()
2273                        });
2274
2275                        let full_path = path.display(path_style).to_string();
2276
2277                        let file_icon = FileIcons::get_icon(path.as_std_path(), cx)
2278                            .map(Icon::from_path)
2279                            .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
2280                            .unwrap_or_else(|| {
2281                                Icon::new(IconName::File)
2282                                    .color(Color::Muted)
2283                                    .size(IconSize::Small)
2284                            });
2285
2286                        let file_stats = DiffStats::single_file(buffer.read(cx), diff.read(cx), cx);
2287
2288                        let buttons = self.render_edited_files_buttons(
2289                            index,
2290                            buffer,
2291                            action_log,
2292                            &telemetry,
2293                            pending_edits,
2294                            editor_bg_color,
2295                            cx,
2296                        );
2297
2298                        let element = h_flex()
2299                            .group("edited-code")
2300                            .id(("file-container", index))
2301                            .relative()
2302                            .min_w_0()
2303                            .p_1p5()
2304                            .gap_2()
2305                            .justify_between()
2306                            .bg(editor_bg_color)
2307                            .when(index < changed_buffers.len() - 1, |parent| {
2308                                parent.border_color(cx.theme().colors().border).border_b_1()
2309                            })
2310                            .child(
2311                                h_flex()
2312                                    .id(("file-name-path", index))
2313                                    .cursor_pointer()
2314                                    .pr_0p5()
2315                                    .gap_0p5()
2316                                    .rounded_xs()
2317                                    .child(file_icon)
2318                                    .children(file_name)
2319                                    .children(file_path)
2320                                    .child(
2321                                        DiffStat::new(
2322                                            "file",
2323                                            file_stats.lines_added as usize,
2324                                            file_stats.lines_removed as usize,
2325                                        )
2326                                        .label_size(LabelSize::XSmall),
2327                                    )
2328                                    .hover(|s| s.bg(cx.theme().colors().element_hover))
2329                                    .tooltip({
2330                                        move |_, cx| {
2331                                            Tooltip::with_meta(
2332                                                "Go to File",
2333                                                None,
2334                                                full_path.clone(),
2335                                                cx,
2336                                            )
2337                                        }
2338                                    })
2339                                    .on_click({
2340                                        let buffer = buffer.clone();
2341                                        cx.listener(move |this, _, window, cx| {
2342                                            this.open_edited_buffer(&buffer, window, cx);
2343                                        })
2344                                    }),
2345                            )
2346                            .child(buttons);
2347
2348                        Some(element)
2349                    }),
2350            )
2351            .into_any_element()
2352    }
2353
2354    fn render_edited_files_buttons(
2355        &self,
2356        index: usize,
2357        buffer: &Entity<Buffer>,
2358        action_log: &Entity<ActionLog>,
2359        telemetry: &ActionLogTelemetry,
2360        pending_edits: bool,
2361        editor_bg_color: Hsla,
2362        cx: &Context<Self>,
2363    ) -> impl IntoElement {
2364        h_flex()
2365            .id("edited-buttons-container")
2366            .visible_on_hover("edited-code")
2367            .absolute()
2368            .right_0()
2369            .px_1()
2370            .gap_1()
2371            .bg(editor_bg_color)
2372            .on_hover(cx.listener(move |this, is_hovered, _window, cx| {
2373                if *is_hovered {
2374                    this.hovered_edited_file_buttons = Some(index);
2375                } else if this.hovered_edited_file_buttons == Some(index) {
2376                    this.hovered_edited_file_buttons = None;
2377                }
2378                cx.notify();
2379            }))
2380            .child(
2381                Button::new("review", "Review")
2382                    .label_size(LabelSize::Small)
2383                    .on_click({
2384                        let buffer = buffer.clone();
2385                        cx.listener(move |this, _, window, cx| {
2386                            this.open_edited_buffer(&buffer, window, cx);
2387                        })
2388                    }),
2389            )
2390            .child(
2391                Button::new(("reject-file", index), "Reject")
2392                    .label_size(LabelSize::Small)
2393                    .disabled(pending_edits)
2394                    .on_click({
2395                        let buffer = buffer.clone();
2396                        let action_log = action_log.clone();
2397                        let telemetry = telemetry.clone();
2398                        move |_, _, cx| {
2399                            action_log.update(cx, |action_log, cx| {
2400                                action_log
2401                                    .reject_edits_in_ranges(
2402                                        buffer.clone(),
2403                                        vec![Anchor::min_max_range_for_buffer(
2404                                            buffer.read(cx).remote_id(),
2405                                        )],
2406                                        Some(telemetry.clone()),
2407                                        cx,
2408                                    )
2409                                    .0
2410                                    .detach_and_log_err(cx);
2411                            })
2412                        }
2413                    }),
2414            )
2415            .child(
2416                Button::new(("keep-file", index), "Keep")
2417                    .label_size(LabelSize::Small)
2418                    .disabled(pending_edits)
2419                    .on_click({
2420                        let buffer = buffer.clone();
2421                        let action_log = action_log.clone();
2422                        let telemetry = telemetry.clone();
2423                        move |_, _, cx| {
2424                            action_log.update(cx, |action_log, cx| {
2425                                action_log.keep_edits_in_range(
2426                                    buffer.clone(),
2427                                    Anchor::min_max_range_for_buffer(buffer.read(cx).remote_id()),
2428                                    Some(telemetry.clone()),
2429                                    cx,
2430                                );
2431                            })
2432                        }
2433                    }),
2434            )
2435    }
2436
2437    fn render_subagents_awaiting_permission(&self, cx: &Context<Self>) -> Option<AnyElement> {
2438        let awaiting = self.conversation.read(cx).subagents_awaiting_permission(cx);
2439
2440        if awaiting.is_empty() {
2441            return None;
2442        }
2443
2444        let thread = self.thread.read(cx);
2445        let entries = thread.entries();
2446        let mut subagent_items: Vec<(SharedString, usize)> = Vec::new();
2447
2448        for (session_id, _) in &awaiting {
2449            for (entry_ix, entry) in entries.iter().enumerate() {
2450                if let AgentThreadEntry::ToolCall(tool_call) = entry {
2451                    if let Some(info) = &tool_call.subagent_session_info {
2452                        if &info.session_id == session_id {
2453                            let subagent_summary: SharedString = {
2454                                let summary_text = tool_call.label.read(cx).source().to_string();
2455                                if !summary_text.is_empty() {
2456                                    summary_text.into()
2457                                } else {
2458                                    "Subagent".into()
2459                                }
2460                            };
2461                            subagent_items.push((subagent_summary, entry_ix));
2462                            break;
2463                        }
2464                    }
2465                }
2466            }
2467        }
2468
2469        if subagent_items.is_empty() {
2470            return None;
2471        }
2472
2473        let item_count = subagent_items.len();
2474
2475        Some(
2476            v_flex()
2477                .child(
2478                    h_flex()
2479                        .py_1()
2480                        .px_2()
2481                        .w_full()
2482                        .gap_1()
2483                        .border_b_1()
2484                        .border_color(cx.theme().colors().border)
2485                        .child(
2486                            Label::new("Subagents Awaiting Permission:")
2487                                .size(LabelSize::Small)
2488                                .color(Color::Muted),
2489                        )
2490                        .child(Label::new(item_count.to_string()).size(LabelSize::Small)),
2491                )
2492                .child(
2493                    v_flex().children(subagent_items.into_iter().enumerate().map(
2494                        |(ix, (label, entry_ix))| {
2495                            let is_last = ix == item_count - 1;
2496                            let group = format!("group-{}", entry_ix);
2497
2498                            h_flex()
2499                                .cursor_pointer()
2500                                .id(format!("subagent-permission-{}", entry_ix))
2501                                .group(&group)
2502                                .p_1()
2503                                .pl_2()
2504                                .min_w_0()
2505                                .w_full()
2506                                .gap_1()
2507                                .justify_between()
2508                                .bg(cx.theme().colors().editor_background)
2509                                .hover(|s| s.bg(cx.theme().colors().element_hover))
2510                                .when(!is_last, |this| {
2511                                    this.border_b_1().border_color(cx.theme().colors().border)
2512                                })
2513                                .child(
2514                                    h_flex()
2515                                        .gap_1p5()
2516                                        .child(
2517                                            Icon::new(IconName::Circle)
2518                                                .size(IconSize::XSmall)
2519                                                .color(Color::Warning),
2520                                        )
2521                                        .child(
2522                                            Label::new(label)
2523                                                .size(LabelSize::Small)
2524                                                .color(Color::Muted)
2525                                                .truncate(),
2526                                        ),
2527                                )
2528                                .child(
2529                                    div().visible_on_hover(&group).child(
2530                                        Label::new("Scroll to Subagent")
2531                                            .size(LabelSize::Small)
2532                                            .color(Color::Muted)
2533                                            .truncate(),
2534                                    ),
2535                                )
2536                                .on_click(cx.listener(move |this, _, _, cx| {
2537                                    this.list_state.scroll_to(ListOffset {
2538                                        item_ix: entry_ix,
2539                                        offset_in_item: px(0.0),
2540                                    });
2541                                    cx.notify();
2542                                }))
2543                        },
2544                    )),
2545                )
2546                .into_any(),
2547        )
2548    }
2549
2550    fn render_message_queue_summary(
2551        &self,
2552        _window: &mut Window,
2553        cx: &Context<Self>,
2554    ) -> impl IntoElement {
2555        let queue_count = self.local_queued_messages.len();
2556        let title: SharedString = if queue_count == 1 {
2557            "1 Queued Message".into()
2558        } else {
2559            format!("{} Queued Messages", queue_count).into()
2560        };
2561
2562        h_flex()
2563            .p_1()
2564            .w_full()
2565            .gap_1()
2566            .justify_between()
2567            .when(self.queue_expanded, |this| {
2568                this.border_b_1().border_color(cx.theme().colors().border)
2569            })
2570            .child(
2571                h_flex()
2572                    .id("queue_summary")
2573                    .gap_1()
2574                    .child(Disclosure::new("queue_disclosure", self.queue_expanded))
2575                    .child(Label::new(title).size(LabelSize::Small).color(Color::Muted))
2576                    .on_click(cx.listener(|this, _, _, cx| {
2577                        this.queue_expanded = !this.queue_expanded;
2578                        cx.notify();
2579                    })),
2580            )
2581            .child(
2582                Button::new("clear_queue", "Clear All")
2583                    .label_size(LabelSize::Small)
2584                    .key_binding(
2585                        KeyBinding::for_action(&ClearMessageQueue, cx)
2586                            .map(|kb| kb.size(rems_from_px(12.))),
2587                    )
2588                    .on_click(cx.listener(|this, _, _, cx| {
2589                        this.clear_queue(cx);
2590                        this.can_fast_track_queue = false;
2591                        cx.notify();
2592                    })),
2593            )
2594            .into_any_element()
2595    }
2596
2597    fn clear_queue(&mut self, cx: &mut Context<Self>) {
2598        self.local_queued_messages.clear();
2599        self.sync_queue_flag_to_native_thread(cx);
2600    }
2601
2602    fn render_plan_summary(
2603        &self,
2604        plan: &Plan,
2605        window: &mut Window,
2606        cx: &Context<Self>,
2607    ) -> impl IntoElement {
2608        let plan_expanded = self.plan_expanded;
2609        let stats = plan.stats();
2610
2611        let title = if let Some(entry) = stats.in_progress_entry
2612            && !plan_expanded
2613        {
2614            h_flex()
2615                .cursor_default()
2616                .relative()
2617                .w_full()
2618                .gap_1()
2619                .truncate()
2620                .child(
2621                    Label::new("Current:")
2622                        .size(LabelSize::Small)
2623                        .color(Color::Muted),
2624                )
2625                .child(
2626                    div()
2627                        .text_xs()
2628                        .text_color(cx.theme().colors().text_muted)
2629                        .line_clamp(1)
2630                        .child(MarkdownElement::new(
2631                            entry.content.clone(),
2632                            plan_label_markdown_style(&entry.status, window, cx),
2633                        )),
2634                )
2635                .when(stats.pending > 0, |this| {
2636                    this.child(
2637                        h_flex()
2638                            .absolute()
2639                            .top_0()
2640                            .right_0()
2641                            .h_full()
2642                            .child(div().min_w_8().h_full().bg(linear_gradient(
2643                                90.,
2644                                linear_color_stop(self.activity_bar_bg(cx), 1.),
2645                                linear_color_stop(self.activity_bar_bg(cx).opacity(0.2), 0.),
2646                            )))
2647                            .child(
2648                                div().pr_0p5().bg(self.activity_bar_bg(cx)).child(
2649                                    Label::new(format!("{} left", stats.pending))
2650                                        .size(LabelSize::Small)
2651                                        .color(Color::Muted),
2652                                ),
2653                            ),
2654                    )
2655                })
2656        } else {
2657            let status_label = if stats.pending == 0 {
2658                "All Done".to_string()
2659            } else if stats.completed == 0 {
2660                format!("{} Tasks", plan.entries.len())
2661            } else {
2662                format!("{}/{}", stats.completed, plan.entries.len())
2663            };
2664
2665            h_flex()
2666                .w_full()
2667                .gap_1()
2668                .justify_between()
2669                .child(
2670                    Label::new("Plan")
2671                        .size(LabelSize::Small)
2672                        .color(Color::Muted),
2673                )
2674                .child(
2675                    Label::new(status_label)
2676                        .size(LabelSize::Small)
2677                        .color(Color::Muted)
2678                        .mr_1(),
2679                )
2680        };
2681
2682        h_flex()
2683            .id("plan_summary")
2684            .p_1()
2685            .w_full()
2686            .gap_1()
2687            .when(plan_expanded, |this| {
2688                this.border_b_1().border_color(cx.theme().colors().border)
2689            })
2690            .child(Disclosure::new("plan_disclosure", plan_expanded))
2691            .child(title.flex_1())
2692            .child(
2693                IconButton::new("dismiss-plan", IconName::Close)
2694                    .icon_size(IconSize::XSmall)
2695                    .shape(ui::IconButtonShape::Square)
2696                    .tooltip(Tooltip::text("Clear plan"))
2697                    .on_click(cx.listener(|this, _, _, cx| {
2698                        this.thread.update(cx, |thread, cx| thread.clear_plan(cx));
2699                        cx.stop_propagation();
2700                    })),
2701            )
2702            .on_click(cx.listener(|this, _, _, cx| {
2703                this.plan_expanded = !this.plan_expanded;
2704                cx.notify();
2705            }))
2706            .into_any_element()
2707    }
2708
2709    fn render_plan_entries(
2710        &self,
2711        plan: &Plan,
2712        window: &mut Window,
2713        cx: &Context<Self>,
2714    ) -> impl IntoElement {
2715        v_flex()
2716            .id("plan_items_list")
2717            .max_h_40()
2718            .overflow_y_scroll()
2719            .children(plan.entries.iter().enumerate().flat_map(|(index, entry)| {
2720                let element = h_flex()
2721                    .py_1()
2722                    .px_2()
2723                    .gap_2()
2724                    .justify_between()
2725                    .bg(cx.theme().colors().editor_background)
2726                    .when(index < plan.entries.len() - 1, |parent| {
2727                        parent.border_color(cx.theme().colors().border).border_b_1()
2728                    })
2729                    .child(
2730                        h_flex()
2731                            .id(("plan_entry", index))
2732                            .gap_1p5()
2733                            .max_w_full()
2734                            .overflow_x_scroll()
2735                            .text_xs()
2736                            .text_color(cx.theme().colors().text_muted)
2737                            .child(match entry.status {
2738                                acp::PlanEntryStatus::InProgress => {
2739                                    Icon::new(IconName::TodoProgress)
2740                                        .size(IconSize::Small)
2741                                        .color(Color::Accent)
2742                                        .with_rotate_animation(2)
2743                                        .into_any_element()
2744                                }
2745                                acp::PlanEntryStatus::Completed => {
2746                                    Icon::new(IconName::TodoComplete)
2747                                        .size(IconSize::Small)
2748                                        .color(Color::Success)
2749                                        .into_any_element()
2750                                }
2751                                acp::PlanEntryStatus::Pending | _ => {
2752                                    Icon::new(IconName::TodoPending)
2753                                        .size(IconSize::Small)
2754                                        .color(Color::Muted)
2755                                        .into_any_element()
2756                                }
2757                            })
2758                            .child(MarkdownElement::new(
2759                                entry.content.clone(),
2760                                plan_label_markdown_style(&entry.status, window, cx),
2761                            )),
2762                    );
2763
2764                Some(element)
2765            }))
2766            .into_any_element()
2767    }
2768
2769    fn render_completed_plan(
2770        &self,
2771        entries: &[PlanEntry],
2772        window: &Window,
2773        cx: &Context<Self>,
2774    ) -> AnyElement {
2775        v_flex()
2776            .px_5()
2777            .py_1p5()
2778            .w_full()
2779            .child(
2780                v_flex()
2781                    .w_full()
2782                    .rounded_md()
2783                    .border_1()
2784                    .border_color(self.tool_card_border_color(cx))
2785                    .child(
2786                        h_flex()
2787                            .px_2()
2788                            .py_1()
2789                            .gap_1()
2790                            .bg(self.tool_card_header_bg(cx))
2791                            .border_b_1()
2792                            .border_color(self.tool_card_border_color(cx))
2793                            .child(
2794                                Label::new("Completed Plan")
2795                                    .size(LabelSize::Small)
2796                                    .color(Color::Muted),
2797                            )
2798                            .child(
2799                                Label::new(format!(
2800                                    "{} {}",
2801                                    entries.len(),
2802                                    if entries.len() == 1 { "step" } else { "steps" }
2803                                ))
2804                                .size(LabelSize::Small)
2805                                .color(Color::Muted),
2806                            ),
2807                    )
2808                    .child(
2809                        v_flex().children(entries.iter().enumerate().map(|(index, entry)| {
2810                            h_flex()
2811                                .py_1()
2812                                .px_2()
2813                                .gap_1p5()
2814                                .when(index < entries.len() - 1, |this| {
2815                                    this.border_b_1().border_color(cx.theme().colors().border)
2816                                })
2817                                .child(
2818                                    Icon::new(IconName::TodoComplete)
2819                                        .size(IconSize::Small)
2820                                        .color(Color::Success),
2821                                )
2822                                .child(
2823                                    div()
2824                                        .max_w_full()
2825                                        .overflow_x_hidden()
2826                                        .text_xs()
2827                                        .text_color(cx.theme().colors().text_muted)
2828                                        .child(MarkdownElement::new(
2829                                            entry.content.clone(),
2830                                            default_markdown_style(window, cx),
2831                                        )),
2832                                )
2833                        })),
2834                    ),
2835            )
2836            .into_any()
2837    }
2838
2839    fn render_edits_summary(
2840        &self,
2841        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
2842        expanded: bool,
2843        pending_edits: bool,
2844        cx: &Context<Self>,
2845    ) -> Div {
2846        const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete.";
2847
2848        let focus_handle = self.focus_handle(cx);
2849
2850        h_flex()
2851            .p_1()
2852            .justify_between()
2853            .flex_wrap()
2854            .when(expanded, |this| {
2855                this.border_b_1().border_color(cx.theme().colors().border)
2856            })
2857            .child(
2858                h_flex()
2859                    .id("edits-container")
2860                    .cursor_pointer()
2861                    .gap_1()
2862                    .child(Disclosure::new("edits-disclosure", expanded))
2863                    .map(|this| {
2864                        if pending_edits {
2865                            this.child(
2866                                Label::new(format!(
2867                                    "Editing {} {}",
2868                                    changed_buffers.len(),
2869                                    if changed_buffers.len() == 1 {
2870                                        "file"
2871                                    } else {
2872                                        "files"
2873                                    }
2874                                ))
2875                                .color(Color::Muted)
2876                                .size(LabelSize::Small)
2877                                .with_animation(
2878                                    "edit-label",
2879                                    Animation::new(Duration::from_secs(2))
2880                                        .repeat()
2881                                        .with_easing(pulsating_between(0.3, 0.7)),
2882                                    |label, delta| label.alpha(delta),
2883                                ),
2884                            )
2885                        } else {
2886                            let stats = DiffStats::all_files(changed_buffers, cx);
2887                            let dot_divider = || {
2888                                Label::new("")
2889                                    .size(LabelSize::XSmall)
2890                                    .color(Color::Disabled)
2891                            };
2892
2893                            this.child(
2894                                Label::new("Edits")
2895                                    .size(LabelSize::Small)
2896                                    .color(Color::Muted),
2897                            )
2898                            .child(dot_divider())
2899                            .child(
2900                                Label::new(format!(
2901                                    "{} {}",
2902                                    changed_buffers.len(),
2903                                    if changed_buffers.len() == 1 {
2904                                        "file"
2905                                    } else {
2906                                        "files"
2907                                    }
2908                                ))
2909                                .size(LabelSize::Small)
2910                                .color(Color::Muted),
2911                            )
2912                            .child(dot_divider())
2913                            .child(DiffStat::new(
2914                                "total",
2915                                stats.lines_added as usize,
2916                                stats.lines_removed as usize,
2917                            ))
2918                        }
2919                    })
2920                    .on_click(cx.listener(|this, _, _, cx| {
2921                        this.edits_expanded = !this.edits_expanded;
2922                        cx.notify();
2923                    })),
2924            )
2925            .child(
2926                h_flex()
2927                    .gap_1()
2928                    .child(
2929                        IconButton::new("review-changes", IconName::ListTodo)
2930                            .icon_size(IconSize::Small)
2931                            .tooltip({
2932                                let focus_handle = focus_handle.clone();
2933                                move |_window, cx| {
2934                                    Tooltip::for_action_in(
2935                                        "Review Changes",
2936                                        &OpenAgentDiff,
2937                                        &focus_handle,
2938                                        cx,
2939                                    )
2940                                }
2941                            })
2942                            .on_click(cx.listener(|_, _, window, cx| {
2943                                window.dispatch_action(OpenAgentDiff.boxed_clone(), cx);
2944                            })),
2945                    )
2946                    .child(Divider::vertical().color(DividerColor::Border))
2947                    .child(
2948                        Button::new("reject-all-changes", "Reject All")
2949                            .label_size(LabelSize::Small)
2950                            .disabled(pending_edits)
2951                            .when(pending_edits, |this| {
2952                                this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
2953                            })
2954                            .key_binding(
2955                                KeyBinding::for_action_in(&RejectAll, &focus_handle.clone(), cx)
2956                                    .map(|kb| kb.size(rems_from_px(12.))),
2957                            )
2958                            .on_click(cx.listener(move |this, _, window, cx| {
2959                                this.reject_all(&RejectAll, window, cx);
2960                            })),
2961                    )
2962                    .child(
2963                        Button::new("keep-all-changes", "Keep All")
2964                            .label_size(LabelSize::Small)
2965                            .disabled(pending_edits)
2966                            .when(pending_edits, |this| {
2967                                this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
2968                            })
2969                            .key_binding(
2970                                KeyBinding::for_action_in(&KeepAll, &focus_handle, cx)
2971                                    .map(|kb| kb.size(rems_from_px(12.))),
2972                            )
2973                            .on_click(cx.listener(move |this, _, window, cx| {
2974                                this.keep_all(&KeepAll, window, cx);
2975                            })),
2976                    ),
2977            )
2978    }
2979
2980    fn is_subagent_canceled_or_failed(&self, cx: &App) -> bool {
2981        let Some(parent_session_id) = self.parent_id.as_ref() else {
2982            return false;
2983        };
2984
2985        let my_session_id = self.thread.read(cx).session_id().clone();
2986
2987        self.server_view
2988            .upgrade()
2989            .and_then(|sv| sv.read(cx).thread_view(parent_session_id))
2990            .is_some_and(|parent_view| {
2991                parent_view
2992                    .read(cx)
2993                    .thread
2994                    .read(cx)
2995                    .tool_call_for_subagent(&my_session_id)
2996                    .is_some_and(|tc| {
2997                        matches!(
2998                            tc.status,
2999                            ToolCallStatus::Canceled
3000                                | ToolCallStatus::Failed
3001                                | ToolCallStatus::Rejected
3002                        )
3003                    })
3004            })
3005    }
3006
3007    pub(crate) fn render_subagent_titlebar(&mut self, cx: &mut Context<Self>) -> Option<Div> {
3008        let Some(parent_session_id) = self.parent_id.clone() else {
3009            return None;
3010        };
3011
3012        let server_view = self.server_view.clone();
3013        let thread = self.thread.clone();
3014        let is_done = thread.read(cx).status() == ThreadStatus::Idle;
3015        let is_canceled_or_failed = self.is_subagent_canceled_or_failed(cx);
3016
3017        let max_content_width = AgentSettings::get_global(cx).max_content_width;
3018
3019        Some(
3020            h_flex()
3021                .w_full()
3022                .h(Tab::container_height(cx))
3023                .border_b_1()
3024                .when(is_done && is_canceled_or_failed, |this| {
3025                    this.border_dashed()
3026                })
3027                .border_color(cx.theme().colors().border)
3028                .bg(cx.theme().colors().editor_background.opacity(0.2))
3029                .child(
3030                    h_flex()
3031                        .size_full()
3032                        .max_w(max_content_width)
3033                        .mx_auto()
3034                        .pl_2()
3035                        .pr_1()
3036                        .flex_shrink_0()
3037                        .justify_between()
3038                        .gap_1()
3039                        .child(
3040                            h_flex()
3041                                .flex_1()
3042                                .gap_2()
3043                                .child(
3044                                    Icon::new(IconName::ForwardArrowUp)
3045                                        .size(IconSize::Small)
3046                                        .color(Color::Muted),
3047                                )
3048                                .child(self.title_editor.clone())
3049                                .when(is_done && is_canceled_or_failed, |this| {
3050                                    this.child(Icon::new(IconName::Close).color(Color::Error))
3051                                })
3052                                .when(is_done && !is_canceled_or_failed, |this| {
3053                                    this.child(Icon::new(IconName::Check).color(Color::Success))
3054                                }),
3055                        )
3056                        .child(
3057                            h_flex()
3058                                .gap_0p5()
3059                                .when(!is_done, |this| {
3060                                    this.child(
3061                                        IconButton::new("stop_subagent", IconName::Stop)
3062                                            .icon_size(IconSize::Small)
3063                                            .icon_color(Color::Error)
3064                                            .tooltip(Tooltip::text("Stop Subagent"))
3065                                            .on_click(move |_, _, cx| {
3066                                                thread.update(cx, |thread, cx| {
3067                                                    thread.cancel(cx).detach();
3068                                                });
3069                                            }),
3070                                    )
3071                                })
3072                                .child(
3073                                    IconButton::new("minimize_subagent", IconName::Dash)
3074                                        .icon_size(IconSize::Small)
3075                                        .tooltip(Tooltip::text("Minimize Subagent"))
3076                                        .on_click(move |_, window, cx| {
3077                                            let _ = server_view.update(cx, |server_view, cx| {
3078                                                server_view.navigate_to_session(
3079                                                    parent_session_id.clone(),
3080                                                    window,
3081                                                    cx,
3082                                                );
3083                                            });
3084                                        }),
3085                                ),
3086                        ),
3087                ),
3088        )
3089    }
3090
3091    pub(crate) fn render_message_editor(
3092        &mut self,
3093        window: &mut Window,
3094        cx: &mut Context<Self>,
3095    ) -> AnyElement {
3096        if self.is_subagent() {
3097            return div().into_any_element();
3098        }
3099
3100        let focus_handle = self.message_editor.focus_handle(cx);
3101        let editor_bg_color = cx.theme().colors().editor_background;
3102        let editor_expanded = self.editor_expanded;
3103        let has_messages = self.list_state.item_count() > 0;
3104        let v2_empty_state = cx.has_flag::<AgentV2FeatureFlag>() && !has_messages;
3105        let (expand_icon, expand_tooltip) = if editor_expanded {
3106            (IconName::Minimize, "Minimize Message Editor")
3107        } else {
3108            (IconName::Maximize, "Expand Message Editor")
3109        };
3110
3111        let max_content_width = AgentSettings::get_global(cx).max_content_width;
3112
3113        v_flex()
3114            .on_action(cx.listener(Self::expand_message_editor))
3115            .p_2()
3116            .gap_2()
3117            .when(!v2_empty_state, |this| {
3118                this.border_t_1().border_color(cx.theme().colors().border)
3119            })
3120            .bg(editor_bg_color)
3121            .when(v2_empty_state, |this| this.flex_1().size_full())
3122            .when(editor_expanded && !v2_empty_state, |this| {
3123                this.h(vh(0.8, window)).size_full().justify_between()
3124            })
3125            .child(
3126                v_flex()
3127                    .flex_1()
3128                    .w_full()
3129                    .max_w(max_content_width)
3130                    .mx_auto()
3131                    .child(
3132                        v_flex()
3133                            .relative()
3134                            .size_full()
3135                            .when(v2_empty_state, |this| this.flex_1())
3136                            .pt_1()
3137                            .pr_2p5()
3138                            .child(self.message_editor.clone())
3139                            .when(!v2_empty_state, |this| {
3140                                this.child(
3141                                    h_flex()
3142                                        .absolute()
3143                                        .top_0()
3144                                        .right_0()
3145                                        .opacity(0.5)
3146                                        .hover(|this| this.opacity(1.0))
3147                                        .child(
3148                                            IconButton::new("toggle-height", expand_icon)
3149                                                .icon_size(IconSize::Small)
3150                                                .icon_color(Color::Muted)
3151                                                .tooltip({
3152                                                    move |_window, cx| {
3153                                                        Tooltip::for_action_in(
3154                                                            expand_tooltip,
3155                                                            &ExpandMessageEditor,
3156                                                            &focus_handle,
3157                                                            cx,
3158                                                        )
3159                                                    }
3160                                                })
3161                                                .on_click(cx.listener(|this, _, window, cx| {
3162                                                    this.expand_message_editor(
3163                                                        &ExpandMessageEditor,
3164                                                        window,
3165                                                        cx,
3166                                                    );
3167                                                })),
3168                                        ),
3169                                )
3170                            }),
3171                    )
3172                    .child(
3173                        h_flex()
3174                            .flex_none()
3175                            .flex_wrap()
3176                            .justify_between()
3177                            .child(
3178                                h_flex()
3179                                    .gap_0p5()
3180                                    .child(self.render_add_context_button(cx))
3181                                    .child(self.render_follow_toggle(cx))
3182                                    .children(self.render_fast_mode_control(cx))
3183                                    .children(self.render_thinking_control(cx)),
3184                            )
3185                            .child(
3186                                h_flex()
3187                                    .gap_1()
3188                                    .children(self.render_token_usage(cx))
3189                                    .children(self.profile_selector.clone())
3190                                    .map(|this| {
3191                                        // Either config_options_view OR (mode_selector + model_selector)
3192                                        match self.config_options_view.clone() {
3193                                            Some(config_view) => this.child(config_view),
3194                                            None => this
3195                                                .children(self.mode_selector.clone())
3196                                                .children(self.model_selector.clone()),
3197                                        }
3198                                    })
3199                                    .child(self.render_send_button(cx)),
3200                            ),
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.p_2().when(context_ix > 0, |this| {
7415                        this.border_t_1()
7416                            .border_color(self.tool_card_border_color(cx))
7417                    })
7418                } else {
7419                    this.ml(rems(0.4))
7420                        .px_3p5()
7421                        .border_l_1()
7422                        .border_color(self.tool_card_border_color(cx))
7423                }
7424            })
7425            .text_xs()
7426            .text_color(cx.theme().colors().text_muted)
7427            .child(self.render_markdown(
7428                markdown,
7429                MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
7430            ))
7431            .when(!card_layout, |this| {
7432                this.child(
7433                    IconButton::new(button_id, IconName::ChevronUp)
7434                        .full_width()
7435                        .style(ButtonStyle::Outlined)
7436                        .icon_color(Color::Muted)
7437                        .on_click(cx.listener({
7438                            move |this: &mut Self, _, _, cx: &mut Context<Self>| {
7439                                this.expanded_tool_calls.remove(&tool_call_id);
7440                                cx.notify();
7441                            }
7442                        })),
7443                )
7444            })
7445            .into_any_element()
7446    }
7447
7448    fn render_image_output(
7449        &self,
7450        entry_ix: usize,
7451        image: Arc<gpui::Image>,
7452        location: Option<acp::ToolCallLocation>,
7453        card_layout: bool,
7454        show_dimensions: bool,
7455        cx: &Context<Self>,
7456    ) -> AnyElement {
7457        let dimensions_label = if show_dimensions {
7458            let format_name = match image.format() {
7459                gpui::ImageFormat::Png => "PNG",
7460                gpui::ImageFormat::Jpeg => "JPEG",
7461                gpui::ImageFormat::Webp => "WebP",
7462                gpui::ImageFormat::Gif => "GIF",
7463                gpui::ImageFormat::Svg => "SVG",
7464                gpui::ImageFormat::Bmp => "BMP",
7465                gpui::ImageFormat::Tiff => "TIFF",
7466                gpui::ImageFormat::Ico => "ICO",
7467            };
7468            let dimensions = image::ImageReader::new(std::io::Cursor::new(image.bytes()))
7469                .with_guessed_format()
7470                .ok()
7471                .and_then(|reader| reader.into_dimensions().ok());
7472            dimensions.map(|(w, h)| format!("{}×{} {}", w, h, format_name))
7473        } else {
7474            None
7475        };
7476
7477        v_flex()
7478            .gap_2()
7479            .map(|this| {
7480                if card_layout {
7481                    this
7482                } else {
7483                    this.ml(rems(0.4))
7484                        .px_3p5()
7485                        .border_l_1()
7486                        .border_color(self.tool_card_border_color(cx))
7487                }
7488            })
7489            .when(dimensions_label.is_some() || location.is_some(), |this| {
7490                this.child(
7491                    h_flex()
7492                        .w_full()
7493                        .justify_between()
7494                        .items_center()
7495                        .children(dimensions_label.map(|label| {
7496                            Label::new(label)
7497                                .size(LabelSize::XSmall)
7498                                .color(Color::Muted)
7499                                .buffer_font(cx)
7500                        }))
7501                        .when_some(location, |this, _loc| {
7502                            this.child(
7503                                Button::new(("go-to-file", entry_ix), "Go to File")
7504                                    .label_size(LabelSize::Small)
7505                                    .on_click(cx.listener(move |this, _, window, cx| {
7506                                        this.open_tool_call_location(entry_ix, 0, window, cx);
7507                                    })),
7508                            )
7509                        }),
7510                )
7511            })
7512            .child(
7513                img(image)
7514                    .max_w_96()
7515                    .max_h_96()
7516                    .object_fit(ObjectFit::ScaleDown),
7517            )
7518            .into_any_element()
7519    }
7520
7521    fn render_subagent_tool_call(
7522        &self,
7523        active_session_id: &acp::SessionId,
7524        entry_ix: usize,
7525        tool_call: &ToolCall,
7526        subagent_session_id: Option<acp::SessionId>,
7527        focus_handle: &FocusHandle,
7528        window: &Window,
7529        cx: &Context<Self>,
7530    ) -> Div {
7531        let subagent_thread_view = subagent_session_id.and_then(|id| {
7532            self.server_view
7533                .upgrade()
7534                .and_then(|server_view| server_view.read(cx).as_connected())
7535                .and_then(|connected| connected.threads.get(&id))
7536        });
7537
7538        let content = self.render_subagent_card(
7539            active_session_id,
7540            entry_ix,
7541            subagent_thread_view,
7542            tool_call,
7543            focus_handle,
7544            window,
7545            cx,
7546        );
7547
7548        v_flex().mx_5().my_1p5().gap_3().child(content)
7549    }
7550
7551    fn render_subagent_card(
7552        &self,
7553        active_session_id: &acp::SessionId,
7554        entry_ix: usize,
7555        thread_view: Option<&Entity<ThreadView>>,
7556        tool_call: &ToolCall,
7557        focus_handle: &FocusHandle,
7558        window: &Window,
7559        cx: &Context<Self>,
7560    ) -> AnyElement {
7561        let thread = thread_view
7562            .as_ref()
7563            .map(|view| view.read(cx).thread.clone());
7564        let subagent_session_id = thread
7565            .as_ref()
7566            .map(|thread| thread.read(cx).session_id().clone());
7567        let action_log = thread.as_ref().map(|thread| thread.read(cx).action_log());
7568        let changed_buffers = action_log
7569            .map(|log| log.read(cx).changed_buffers(cx))
7570            .unwrap_or_default();
7571
7572        let is_pending_tool_call = thread
7573            .as_ref()
7574            .and_then(|thread| {
7575                self.conversation
7576                    .read(cx)
7577                    .pending_tool_call(thread.read(cx).session_id(), cx)
7578            })
7579            .is_some();
7580
7581        let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
7582        let files_changed = changed_buffers.len();
7583        let diff_stats = DiffStats::all_files(&changed_buffers, cx);
7584
7585        let is_running = matches!(
7586            tool_call.status,
7587            ToolCallStatus::Pending
7588                | ToolCallStatus::InProgress
7589                | ToolCallStatus::WaitingForConfirmation { .. }
7590        );
7591
7592        let is_failed = matches!(
7593            tool_call.status,
7594            ToolCallStatus::Failed | ToolCallStatus::Rejected
7595        );
7596
7597        let is_cancelled = matches!(tool_call.status, ToolCallStatus::Canceled)
7598            || tool_call.content.iter().any(|c| match c {
7599                ToolCallContent::ContentBlock(ContentBlock::Markdown { markdown }) => {
7600                    markdown.read(cx).source() == "User canceled"
7601                }
7602                _ => false,
7603            });
7604
7605        let thread_title = thread
7606            .as_ref()
7607            .and_then(|t| t.read(cx).title())
7608            .filter(|t| !t.is_empty());
7609        let tool_call_label = tool_call.label.read(cx).source().to_string();
7610        let has_tool_call_label = !tool_call_label.is_empty();
7611
7612        let has_title = thread_title.is_some() || has_tool_call_label;
7613        let has_no_title_or_canceled = !has_title || is_failed || is_cancelled;
7614
7615        let title: SharedString = if let Some(thread_title) = thread_title {
7616            thread_title
7617        } else if !tool_call_label.is_empty() {
7618            tool_call_label.into()
7619        } else if is_cancelled {
7620            "Subagent Canceled".into()
7621        } else if is_failed {
7622            "Subagent Failed".into()
7623        } else {
7624            "Spawning Agent…".into()
7625        };
7626
7627        let card_header_id = format!("subagent-header-{}", entry_ix);
7628        let status_icon = format!("status-icon-{}", entry_ix);
7629        let diff_stat_id = format!("subagent-diff-{}", entry_ix);
7630
7631        let icon = h_flex().w_4().justify_center().child(if is_running {
7632            SpinnerLabel::new()
7633                .size(LabelSize::Small)
7634                .into_any_element()
7635        } else if is_cancelled {
7636            div()
7637                .id(status_icon)
7638                .child(
7639                    Icon::new(IconName::Circle)
7640                        .size(IconSize::Small)
7641                        .color(Color::Custom(
7642                            cx.theme().colors().icon_disabled.opacity(0.5),
7643                        )),
7644                )
7645                .tooltip(Tooltip::text("Subagent Cancelled"))
7646                .into_any_element()
7647        } else if is_failed {
7648            div()
7649                .id(status_icon)
7650                .child(
7651                    Icon::new(IconName::Close)
7652                        .size(IconSize::Small)
7653                        .color(Color::Error),
7654                )
7655                .tooltip(Tooltip::text("Subagent Failed"))
7656                .into_any_element()
7657        } else {
7658            Icon::new(IconName::Check)
7659                .size(IconSize::Small)
7660                .color(Color::Success)
7661                .into_any_element()
7662        });
7663
7664        let has_expandable_content = thread
7665            .as_ref()
7666            .map_or(false, |thread| !thread.read(cx).entries().is_empty());
7667
7668        let tooltip_meta_description = if is_expanded {
7669            "Click to Collapse"
7670        } else {
7671            "Click to Preview"
7672        };
7673
7674        let error_message = self.subagent_error_message(&tool_call.status, tool_call, cx);
7675
7676        v_flex()
7677            .w_full()
7678            .rounded_md()
7679            .border_1()
7680            .when(has_no_title_or_canceled, |this| this.border_dashed())
7681            .border_color(self.tool_card_border_color(cx))
7682            .overflow_hidden()
7683            .child(
7684                h_flex()
7685                    .group(&card_header_id)
7686                    .h_8()
7687                    .p_1()
7688                    .w_full()
7689                    .justify_between()
7690                    .when(!has_no_title_or_canceled, |this| {
7691                        this.bg(self.tool_card_header_bg(cx))
7692                    })
7693                    .child(
7694                        h_flex()
7695                            .id(format!("subagent-title-{}", entry_ix))
7696                            .px_1()
7697                            .min_w_0()
7698                            .size_full()
7699                            .gap_2()
7700                            .justify_between()
7701                            .rounded_sm()
7702                            .overflow_hidden()
7703                            .child(
7704                                h_flex()
7705                                    .min_w_0()
7706                                    .w_full()
7707                                    .gap_1p5()
7708                                    .child(icon)
7709                                    .child(
7710                                        Label::new(title.to_string())
7711                                            .size(LabelSize::Custom(self.tool_name_font_size()))
7712                                            .truncate(),
7713                                    )
7714                                    .when(files_changed > 0, |this| {
7715                                        this.child(
7716                                            Label::new(format!(
7717                                                "{} {} changed",
7718                                                files_changed,
7719                                                if files_changed == 1 { "file" } else { "files" }
7720                                            ))
7721                                            .size(LabelSize::Custom(self.tool_name_font_size()))
7722                                            .color(Color::Muted),
7723                                        )
7724                                        .child(
7725                                            DiffStat::new(
7726                                                diff_stat_id.clone(),
7727                                                diff_stats.lines_added as usize,
7728                                                diff_stats.lines_removed as usize,
7729                                            )
7730                                            .label_size(LabelSize::Custom(
7731                                                self.tool_name_font_size(),
7732                                            )),
7733                                        )
7734                                    }),
7735                            )
7736                            .when(!has_no_title_or_canceled && !is_pending_tool_call, |this| {
7737                                this.tooltip(move |_, cx| {
7738                                    Tooltip::with_meta(
7739                                        title.to_string(),
7740                                        None,
7741                                        tooltip_meta_description,
7742                                        cx,
7743                                    )
7744                                })
7745                            })
7746                            .when(has_expandable_content && !is_pending_tool_call, |this| {
7747                                this.cursor_pointer()
7748                                    .hover(|s| s.bg(cx.theme().colors().element_hover))
7749                                    .child(
7750                                        div().visible_on_hover(card_header_id).child(
7751                                            Icon::new(if is_expanded {
7752                                                IconName::ChevronUp
7753                                            } else {
7754                                                IconName::ChevronDown
7755                                            })
7756                                            .color(Color::Muted)
7757                                            .size(IconSize::Small),
7758                                        ),
7759                                    )
7760                                    .on_click(cx.listener({
7761                                        let tool_call_id = tool_call.id.clone();
7762                                        move |this, _, _, cx| {
7763                                            if this.expanded_tool_calls.contains(&tool_call_id) {
7764                                                this.expanded_tool_calls.remove(&tool_call_id);
7765                                            } else {
7766                                                this.expanded_tool_calls
7767                                                    .insert(tool_call_id.clone());
7768                                            }
7769                                            let expanded =
7770                                                this.expanded_tool_calls.contains(&tool_call_id);
7771                                            telemetry::event!("Subagent Toggled", expanded);
7772                                            cx.notify();
7773                                        }
7774                                    }))
7775                            }),
7776                    )
7777                    .when(is_running && subagent_session_id.is_some(), |buttons| {
7778                        buttons.child(
7779                            IconButton::new(format!("stop-subagent-{}", entry_ix), IconName::Stop)
7780                                .icon_size(IconSize::Small)
7781                                .icon_color(Color::Error)
7782                                .tooltip(Tooltip::text("Stop Subagent"))
7783                                .when_some(
7784                                    thread_view
7785                                        .as_ref()
7786                                        .map(|view| view.read(cx).thread.clone()),
7787                                    |this, thread| {
7788                                        this.on_click(cx.listener(
7789                                            move |_this, _event, _window, cx| {
7790                                                telemetry::event!("Subagent Stopped");
7791                                                thread.update(cx, |thread, cx| {
7792                                                    thread.cancel(cx).detach();
7793                                                });
7794                                            },
7795                                        ))
7796                                    },
7797                                ),
7798                        )
7799                    }),
7800            )
7801            .when_some(thread_view, |this, thread_view| {
7802                let thread = &thread_view.read(cx).thread;
7803                let pending_tool_call = self
7804                    .conversation
7805                    .read(cx)
7806                    .pending_tool_call(thread.read(cx).session_id(), cx);
7807
7808                let session_id = thread.read(cx).session_id().clone();
7809
7810                let fullscreen_toggle = h_flex()
7811                    .id(entry_ix)
7812                    .py_1()
7813                    .w_full()
7814                    .justify_center()
7815                    .border_t_1()
7816                    .when(is_failed, |this| this.border_dashed())
7817                    .border_color(self.tool_card_border_color(cx))
7818                    .cursor_pointer()
7819                    .hover(|s| s.bg(cx.theme().colors().element_hover))
7820                    .child(
7821                        Icon::new(IconName::Maximize)
7822                            .color(Color::Muted)
7823                            .size(IconSize::Small),
7824                    )
7825                    .tooltip(Tooltip::text("Make Subagent Full Screen"))
7826                    .on_click(cx.listener(move |this, _event, window, cx| {
7827                        telemetry::event!("Subagent Maximized");
7828                        this.server_view
7829                            .update(cx, |this, cx| {
7830                                this.navigate_to_session(session_id.clone(), window, cx);
7831                            })
7832                            .ok();
7833                    }));
7834
7835                if is_running && let Some((_, subagent_tool_call_id, _)) = pending_tool_call {
7836                    if let Some((entry_ix, tool_call)) =
7837                        thread.read(cx).tool_call(&subagent_tool_call_id)
7838                    {
7839                        this.child(Divider::horizontal().color(DividerColor::Border))
7840                            .child(thread_view.read(cx).render_any_tool_call(
7841                                active_session_id,
7842                                entry_ix,
7843                                tool_call,
7844                                focus_handle,
7845                                true,
7846                                window,
7847                                cx,
7848                            ))
7849                            .child(fullscreen_toggle)
7850                    } else {
7851                        this
7852                    }
7853                } else {
7854                    this.when(is_expanded, |this| {
7855                        this.child(self.render_subagent_expanded_content(
7856                            thread_view,
7857                            tool_call,
7858                            window,
7859                            cx,
7860                        ))
7861                        .when_some(error_message, |this, message| {
7862                            this.child(
7863                                Callout::new()
7864                                    .severity(Severity::Error)
7865                                    .icon(IconName::XCircle)
7866                                    .title(message),
7867                            )
7868                        })
7869                        .child(fullscreen_toggle)
7870                    })
7871                }
7872            })
7873            .into_any_element()
7874    }
7875
7876    fn render_subagent_expanded_content(
7877        &self,
7878        thread_view: &Entity<ThreadView>,
7879        tool_call: &ToolCall,
7880        window: &Window,
7881        cx: &Context<Self>,
7882    ) -> impl IntoElement {
7883        const MAX_PREVIEW_ENTRIES: usize = 8;
7884
7885        let subagent_view = thread_view.read(cx);
7886        let session_id = subagent_view.thread.read(cx).session_id().clone();
7887
7888        let is_canceled_or_failed = matches!(
7889            tool_call.status,
7890            ToolCallStatus::Canceled | ToolCallStatus::Failed | ToolCallStatus::Rejected
7891        );
7892
7893        let editor_bg = cx.theme().colors().editor_background;
7894        let overlay = {
7895            div()
7896                .absolute()
7897                .inset_0()
7898                .size_full()
7899                .bg(linear_gradient(
7900                    180.,
7901                    linear_color_stop(editor_bg.opacity(0.5), 0.),
7902                    linear_color_stop(editor_bg.opacity(0.), 0.1),
7903                ))
7904                .block_mouse_except_scroll()
7905        };
7906
7907        let entries = subagent_view.thread.read(cx).entries();
7908        let total_entries = entries.len();
7909        let mut entry_range = if let Some(info) = tool_call.subagent_session_info.as_ref() {
7910            info.message_start_index
7911                ..info
7912                    .message_end_index
7913                    .map(|i| (i + 1).min(total_entries))
7914                    .unwrap_or(total_entries)
7915        } else {
7916            0..total_entries
7917        };
7918        entry_range.start = entry_range
7919            .end
7920            .saturating_sub(MAX_PREVIEW_ENTRIES)
7921            .max(entry_range.start);
7922        let start_ix = entry_range.start;
7923
7924        let scroll_handle = self
7925            .subagent_scroll_handles
7926            .borrow_mut()
7927            .entry(session_id.clone())
7928            .or_default()
7929            .clone();
7930
7931        scroll_handle.scroll_to_bottom();
7932
7933        let rendered_entries: Vec<AnyElement> = entries
7934            .get(entry_range)
7935            .unwrap_or_default()
7936            .iter()
7937            .enumerate()
7938            .map(|(i, entry)| {
7939                let actual_ix = start_ix + i;
7940                subagent_view.render_entry(actual_ix, total_entries, entry, window, cx)
7941            })
7942            .collect();
7943
7944        v_flex()
7945            .w_full()
7946            .border_t_1()
7947            .when(is_canceled_or_failed, |this| this.border_dashed())
7948            .border_color(self.tool_card_border_color(cx))
7949            .overflow_hidden()
7950            .child(
7951                div()
7952                    .pb_1()
7953                    .min_h_0()
7954                    .id(format!("subagent-entries-{}", session_id))
7955                    .track_scroll(&scroll_handle)
7956                    .children(rendered_entries),
7957            )
7958            .h_56()
7959            .child(overlay)
7960            .into_any_element()
7961    }
7962
7963    fn subagent_error_message(
7964        &self,
7965        status: &ToolCallStatus,
7966        tool_call: &ToolCall,
7967        cx: &App,
7968    ) -> Option<SharedString> {
7969        if matches!(status, ToolCallStatus::Failed) {
7970            tool_call.content.iter().find_map(|content| {
7971                if let ToolCallContent::ContentBlock(block) = content {
7972                    if let acp_thread::ContentBlock::Markdown { markdown } = block {
7973                        let source = markdown.read(cx).source().to_string();
7974                        if !source.is_empty() {
7975                            if source == "User canceled" {
7976                                return None;
7977                            } else {
7978                                return Some(SharedString::from(source));
7979                            }
7980                        }
7981                    }
7982                }
7983                None
7984            })
7985        } else {
7986            None
7987        }
7988    }
7989
7990    fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
7991        cx.theme()
7992            .colors()
7993            .element_background
7994            .blend(cx.theme().colors().editor_foreground.opacity(0.025))
7995    }
7996
7997    fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
7998        cx.theme().colors().border.opacity(0.8)
7999    }
8000
8001    fn tool_name_font_size(&self) -> Rems {
8002        rems_from_px(13.)
8003    }
8004
8005    pub(crate) fn render_thread_error(
8006        &mut self,
8007        window: &mut Window,
8008        cx: &mut Context<Self>,
8009    ) -> Option<Div> {
8010        let content = match self.thread_error.as_ref()? {
8011            ThreadError::Other { message, .. } => {
8012                self.render_any_thread_error(message.clone(), window, cx)
8013            }
8014            ThreadError::Refusal => self.render_refusal_error(cx),
8015            ThreadError::AuthenticationRequired(error) => {
8016                self.render_authentication_required_error(error.clone(), cx)
8017            }
8018            ThreadError::PaymentRequired => self.render_payment_required_error(cx),
8019        };
8020
8021        Some(div().child(content))
8022    }
8023
8024    fn render_refusal_error(&self, cx: &mut Context<'_, Self>) -> Callout {
8025        let model_or_agent_name = self.current_model_name(cx);
8026        let refusal_message = format!(
8027            "{} refused to respond to this prompt. \
8028            This can happen when a model believes the prompt violates its content policy \
8029            or safety guidelines, so rephrasing it can sometimes address the issue.",
8030            model_or_agent_name
8031        );
8032
8033        Callout::new()
8034            .severity(Severity::Error)
8035            .title("Request Refused")
8036            .icon(IconName::XCircle)
8037            .description(refusal_message.clone())
8038            .actions_slot(self.create_copy_button(&refusal_message))
8039            .dismiss_action(self.dismiss_error_button(cx))
8040    }
8041
8042    fn render_authentication_required_error(
8043        &self,
8044        error: SharedString,
8045        cx: &mut Context<Self>,
8046    ) -> Callout {
8047        Callout::new()
8048            .severity(Severity::Error)
8049            .title("Authentication Required")
8050            .icon(IconName::XCircle)
8051            .description(error.clone())
8052            .actions_slot(
8053                h_flex()
8054                    .gap_0p5()
8055                    .child(self.authenticate_button(cx))
8056                    .child(self.create_copy_button(error)),
8057            )
8058            .dismiss_action(self.dismiss_error_button(cx))
8059    }
8060
8061    fn render_payment_required_error(&self, cx: &mut Context<Self>) -> Callout {
8062        const ERROR_MESSAGE: &str =
8063            "You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
8064
8065        Callout::new()
8066            .severity(Severity::Error)
8067            .icon(IconName::XCircle)
8068            .title("Free Usage Exceeded")
8069            .description(ERROR_MESSAGE)
8070            .actions_slot(
8071                h_flex()
8072                    .gap_0p5()
8073                    .child(self.upgrade_button(cx))
8074                    .child(self.create_copy_button(ERROR_MESSAGE)),
8075            )
8076            .dismiss_action(self.dismiss_error_button(cx))
8077    }
8078
8079    fn upgrade_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
8080        Button::new("upgrade", "Upgrade")
8081            .label_size(LabelSize::Small)
8082            .style(ButtonStyle::Tinted(ui::TintColor::Accent))
8083            .on_click(cx.listener({
8084                move |this, _, _, cx| {
8085                    this.clear_thread_error(cx);
8086                    cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx));
8087                }
8088            }))
8089    }
8090
8091    fn authenticate_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
8092        Button::new("authenticate", "Authenticate")
8093            .label_size(LabelSize::Small)
8094            .style(ButtonStyle::Filled)
8095            .on_click(cx.listener({
8096                move |this, _, window, cx| {
8097                    let server_view = this.server_view.clone();
8098                    let agent_name = this.agent_id.clone();
8099
8100                    this.clear_thread_error(cx);
8101                    if let Some(message) = this.in_flight_prompt.take() {
8102                        this.message_editor.update(cx, |editor, cx| {
8103                            editor.set_message(message, window, cx);
8104                        });
8105                    }
8106                    let connection = this.thread.read(cx).connection().clone();
8107                    window.defer(cx, |window, cx| {
8108                        ConversationView::handle_auth_required(
8109                            server_view,
8110                            AuthRequired::new(),
8111                            agent_name,
8112                            connection,
8113                            window,
8114                            cx,
8115                        );
8116                    })
8117                }
8118            }))
8119    }
8120
8121    fn current_model_name(&self, cx: &App) -> SharedString {
8122        // For native agent (Zed Agent), use the specific model name (e.g., "Claude 3.5 Sonnet")
8123        // For ACP agents, use the agent name (e.g., "Claude Agent", "Gemini CLI")
8124        // This provides better clarity about what refused the request
8125        if self.as_native_connection(cx).is_some() {
8126            self.model_selector
8127                .clone()
8128                .and_then(|selector| selector.read(cx).active_model(cx))
8129                .map(|model| model.name.clone())
8130                .unwrap_or_else(|| SharedString::from("The model"))
8131        } else {
8132            // ACP agent - use the agent name (e.g., "Claude Agent", "Gemini CLI")
8133            self.agent_id.0.clone()
8134        }
8135    }
8136
8137    fn render_any_thread_error(
8138        &mut self,
8139        error: SharedString,
8140        window: &mut Window,
8141        cx: &mut Context<'_, Self>,
8142    ) -> Callout {
8143        let can_resume = self.thread.read(cx).can_retry(cx);
8144
8145        let markdown = if let Some(markdown) = &self.thread_error_markdown {
8146            markdown.clone()
8147        } else {
8148            let markdown = cx.new(|cx| Markdown::new(error.clone(), None, None, cx));
8149            self.thread_error_markdown = Some(markdown.clone());
8150            markdown
8151        };
8152
8153        let markdown_style =
8154            MarkdownStyle::themed(MarkdownFont::Agent, window, cx).with_muted_text(cx);
8155        let description = self
8156            .render_markdown(markdown, markdown_style)
8157            .into_any_element();
8158
8159        Callout::new()
8160            .severity(Severity::Error)
8161            .icon(IconName::XCircle)
8162            .title("An Error Happened")
8163            .description_slot(description)
8164            .actions_slot(
8165                h_flex()
8166                    .gap_0p5()
8167                    .when(can_resume, |this| {
8168                        this.child(
8169                            IconButton::new("retry", IconName::RotateCw)
8170                                .icon_size(IconSize::Small)
8171                                .tooltip(Tooltip::text("Retry Generation"))
8172                                .on_click(cx.listener(|this, _, _window, cx| {
8173                                    this.retry_generation(cx);
8174                                })),
8175                        )
8176                    })
8177                    .child(self.create_copy_button(error.to_string())),
8178            )
8179            .dismiss_action(self.dismiss_error_button(cx))
8180    }
8181
8182    fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
8183        let workspace = self.workspace.clone();
8184        MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
8185            open_link(text, &workspace, window, cx);
8186        })
8187    }
8188
8189    fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
8190        let message = message.into();
8191
8192        CopyButton::new("copy-error-message", message).tooltip_label("Copy Error Message")
8193    }
8194
8195    fn dismiss_error_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
8196        IconButton::new("dismiss", IconName::Close)
8197            .icon_size(IconSize::Small)
8198            .tooltip(Tooltip::text("Dismiss"))
8199            .on_click(cx.listener({
8200                move |this, _, _, cx| {
8201                    this.clear_thread_error(cx);
8202                    cx.notify();
8203                }
8204            }))
8205    }
8206
8207    fn render_resume_notice(_cx: &Context<Self>) -> AnyElement {
8208        let description = "This agent does not support viewing previous messages. However, your session will still continue from where you last left off.";
8209
8210        div()
8211            .px_2()
8212            .pt_2()
8213            .pb_3()
8214            .w_full()
8215            .child(
8216                Callout::new()
8217                    .severity(Severity::Info)
8218                    .icon(IconName::Info)
8219                    .title("Resumed Session")
8220                    .description(description),
8221            )
8222            .into_any_element()
8223    }
8224
8225    fn update_recent_history_from_cache(
8226        &mut self,
8227        history: &Entity<ThreadHistory>,
8228        cx: &mut Context<Self>,
8229    ) {
8230        self.recent_history_entries = history.read(cx).get_recent_sessions(3);
8231        self.hovered_recent_history_item = None;
8232        cx.notify();
8233    }
8234
8235    fn render_empty_state_section_header(
8236        &self,
8237        label: impl Into<SharedString>,
8238        action_slot: Option<AnyElement>,
8239        cx: &mut Context<Self>,
8240    ) -> impl IntoElement {
8241        div().pl_1().pr_1p5().child(
8242            h_flex()
8243                .mt_2()
8244                .pl_1p5()
8245                .pb_1()
8246                .w_full()
8247                .justify_between()
8248                .border_b_1()
8249                .border_color(cx.theme().colors().border_variant)
8250                .child(
8251                    Label::new(label.into())
8252                        .size(LabelSize::Small)
8253                        .color(Color::Muted),
8254                )
8255                .children(action_slot),
8256        )
8257    }
8258
8259    fn render_recent_history(&self, cx: &mut Context<Self>) -> AnyElement {
8260        let render_history = !self.recent_history_entries.is_empty();
8261
8262        v_flex()
8263            .size_full()
8264            .when(render_history, |this| {
8265                let recent_history = self.recent_history_entries.clone();
8266                this.justify_end().child(
8267                    v_flex()
8268                        .child(
8269                            self.render_empty_state_section_header(
8270                                "Recent",
8271                                Some(
8272                                    Button::new("view-history", "View All")
8273                                        .style(ButtonStyle::Subtle)
8274                                        .label_size(LabelSize::Small)
8275                                        .key_binding(
8276                                            KeyBinding::for_action_in(
8277                                                &OpenHistory,
8278                                                &self.focus_handle(cx),
8279                                                cx,
8280                                            )
8281                                            .map(|kb| kb.size(rems_from_px(12.))),
8282                                        )
8283                                        .on_click(move |_event, window, cx| {
8284                                            window.dispatch_action(OpenHistory.boxed_clone(), cx);
8285                                        })
8286                                        .into_any_element(),
8287                                ),
8288                                cx,
8289                            ),
8290                        )
8291                        .child(v_flex().p_1().pr_1p5().gap_1().children({
8292                            let supports_delete = self
8293                                .history
8294                                .as_ref()
8295                                .map_or(false, |h| h.read(cx).supports_delete());
8296                            recent_history
8297                                .into_iter()
8298                                .enumerate()
8299                                .map(move |(index, entry)| {
8300                                    // TODO: Add keyboard navigation.
8301                                    let is_hovered =
8302                                        self.hovered_recent_history_item == Some(index);
8303                                    crate::thread_history_view::HistoryEntryElement::new(
8304                                        entry,
8305                                        self.server_view.clone(),
8306                                    )
8307                                    .hovered(is_hovered)
8308                                    .supports_delete(supports_delete)
8309                                    .on_hover(cx.listener(move |this, is_hovered, _window, cx| {
8310                                        if *is_hovered {
8311                                            this.hovered_recent_history_item = Some(index);
8312                                        } else if this.hovered_recent_history_item == Some(index) {
8313                                            this.hovered_recent_history_item = None;
8314                                        }
8315                                        cx.notify();
8316                                    }))
8317                                    .into_any_element()
8318                                })
8319                        })),
8320                )
8321            })
8322            .into_any()
8323    }
8324
8325    fn render_codex_windows_warning(&self, cx: &mut Context<Self>) -> Callout {
8326        Callout::new()
8327            .icon(IconName::Warning)
8328            .severity(Severity::Warning)
8329            .title("Codex on Windows")
8330            .description("For best performance, run Codex in Windows Subsystem for Linux (WSL2)")
8331            .actions_slot(
8332                Button::new("open-wsl-modal", "Open in WSL").on_click(cx.listener({
8333                    move |_, _, _window, cx| {
8334                        #[cfg(windows)]
8335                        _window.dispatch_action(
8336                            zed_actions::wsl_actions::OpenWsl::default().boxed_clone(),
8337                            cx,
8338                        );
8339                        cx.notify();
8340                    }
8341                })),
8342            )
8343            .dismiss_action(
8344                IconButton::new("dismiss", IconName::Close)
8345                    .icon_size(IconSize::Small)
8346                    .icon_color(Color::Muted)
8347                    .tooltip(Tooltip::text("Dismiss Warning"))
8348                    .on_click(cx.listener({
8349                        move |this, _, _, cx| {
8350                            this.show_codex_windows_warning = false;
8351                            cx.notify();
8352                        }
8353                    })),
8354            )
8355    }
8356
8357    fn render_external_source_prompt_warning(&self, cx: &mut Context<Self>) -> Callout {
8358        Callout::new()
8359            .icon(IconName::Warning)
8360            .severity(Severity::Warning)
8361            .title("Review before sending")
8362            .description("This prompt was pre-filled by an external link. Read it carefully before you send it.")
8363            .dismiss_action(
8364                IconButton::new("dismiss-external-source-prompt-warning", IconName::Close)
8365                    .icon_size(IconSize::Small)
8366                    .icon_color(Color::Muted)
8367                    .tooltip(Tooltip::text("Dismiss Warning"))
8368                    .on_click(cx.listener({
8369                        move |this, _, _, cx| {
8370                            this.show_external_source_prompt_warning = false;
8371                            cx.notify();
8372                        }
8373                    })),
8374            )
8375    }
8376
8377    fn render_new_version_callout(&self, version: &SharedString, cx: &mut Context<Self>) -> Div {
8378        let server_view = self.server_view.clone();
8379        let has_version = !version.is_empty();
8380        let title = if has_version {
8381            "New version available"
8382        } else {
8383            "Agent update available"
8384        };
8385        let button_label = if has_version {
8386            format!("Update to v{}", version)
8387        } else {
8388            "Reconnect".to_string()
8389        };
8390
8391        v_flex().w_full().justify_end().child(
8392            h_flex()
8393                .p_2()
8394                .pr_3()
8395                .w_full()
8396                .gap_1p5()
8397                .border_t_1()
8398                .border_color(cx.theme().colors().border)
8399                .bg(cx.theme().colors().element_background)
8400                .child(
8401                    h_flex()
8402                        .flex_1()
8403                        .gap_1p5()
8404                        .child(
8405                            Icon::new(IconName::Download)
8406                                .color(Color::Accent)
8407                                .size(IconSize::Small),
8408                        )
8409                        .child(Label::new(title).size(LabelSize::Small)),
8410                )
8411                .child(
8412                    Button::new("update-button", button_label)
8413                        .label_size(LabelSize::Small)
8414                        .style(ButtonStyle::Tinted(TintColor::Accent))
8415                        .on_click(move |_, window, cx| {
8416                            server_view
8417                                .update(cx, |view, cx| view.reset(window, cx))
8418                                .ok();
8419                        }),
8420                ),
8421        )
8422    }
8423
8424    fn render_token_limit_callout(&self, cx: &mut Context<Self>) -> Option<Callout> {
8425        if self.token_limit_callout_dismissed {
8426            return None;
8427        }
8428
8429        let token_usage = self.thread.read(cx).token_usage()?;
8430        let ratio = token_usage.ratio();
8431
8432        let (severity, icon, title) = match ratio {
8433            acp_thread::TokenUsageRatio::Normal => return None,
8434            acp_thread::TokenUsageRatio::Warning => (
8435                Severity::Warning,
8436                IconName::Warning,
8437                "Thread reaching the token limit soon",
8438            ),
8439            acp_thread::TokenUsageRatio::Exceeded => (
8440                Severity::Error,
8441                IconName::XCircle,
8442                "Thread reached the token limit",
8443            ),
8444        };
8445
8446        let description = "To continue, start a new thread from a summary.";
8447
8448        Some(
8449            Callout::new()
8450                .severity(severity)
8451                .icon(icon)
8452                .title(title)
8453                .description(description)
8454                .actions_slot(
8455                    h_flex().gap_0p5().child(
8456                        Button::new("start-new-thread", "Start New Thread")
8457                            .label_size(LabelSize::Small)
8458                            .on_click(cx.listener(|this, _, window, cx| {
8459                                let session_id = this.thread.read(cx).session_id().clone();
8460                                window.dispatch_action(
8461                                    crate::NewNativeAgentThreadFromSummary {
8462                                        from_session_id: session_id,
8463                                    }
8464                                    .boxed_clone(),
8465                                    cx,
8466                                );
8467                            })),
8468                    ),
8469                )
8470                .dismiss_action(self.dismiss_error_button(cx)),
8471        )
8472    }
8473
8474    fn open_permission_dropdown(
8475        &mut self,
8476        _: &crate::OpenPermissionDropdown,
8477        window: &mut Window,
8478        cx: &mut Context<Self>,
8479    ) {
8480        let menu_handle = self.permission_dropdown_handle.clone();
8481        window.defer(cx, move |window, cx| {
8482            menu_handle.toggle(window, cx);
8483        });
8484    }
8485
8486    fn open_add_context_menu(
8487        &mut self,
8488        _action: &OpenAddContextMenu,
8489        window: &mut Window,
8490        cx: &mut Context<Self>,
8491    ) {
8492        let menu_handle = self.add_context_menu_handle.clone();
8493        window.defer(cx, move |window, cx| {
8494            menu_handle.toggle(window, cx);
8495        });
8496    }
8497
8498    fn toggle_fast_mode(&mut self, cx: &mut Context<Self>) {
8499        if !self.fast_mode_available(cx) {
8500            return;
8501        }
8502        let Some(thread) = self.as_native_thread(cx) else {
8503            return;
8504        };
8505        thread.update(cx, |thread, cx| {
8506            thread.set_speed(
8507                thread
8508                    .speed()
8509                    .map(|speed| speed.toggle())
8510                    .unwrap_or(Speed::Fast),
8511                cx,
8512            );
8513        });
8514    }
8515
8516    fn cycle_thinking_effort(&mut self, cx: &mut Context<Self>) {
8517        let Some(thread) = self.as_native_thread(cx) else {
8518            return;
8519        };
8520
8521        let (effort_levels, current_effort) = {
8522            let thread_ref = thread.read(cx);
8523            let Some(model) = thread_ref.model() else {
8524                return;
8525            };
8526            if !model.supports_thinking() || !thread_ref.thinking_enabled() {
8527                return;
8528            }
8529            let effort_levels = model.supported_effort_levels();
8530            if effort_levels.is_empty() {
8531                return;
8532            }
8533            let current_effort = thread_ref.thinking_effort().cloned();
8534            (effort_levels, current_effort)
8535        };
8536
8537        let current_index = current_effort.and_then(|current| {
8538            effort_levels
8539                .iter()
8540                .position(|level| level.value == current)
8541        });
8542        let next_index = match current_index {
8543            Some(index) => (index + 1) % effort_levels.len(),
8544            None => 0,
8545        };
8546        let next_effort = effort_levels[next_index].value.to_string();
8547
8548        thread.update(cx, |thread, cx| {
8549            thread.set_thinking_effort(Some(next_effort.clone()), cx);
8550
8551            let fs = thread.project().read(cx).fs().clone();
8552            update_settings_file(fs, cx, move |settings, _| {
8553                if let Some(agent) = settings.agent.as_mut()
8554                    && let Some(default_model) = agent.default_model.as_mut()
8555                {
8556                    default_model.effort = Some(next_effort);
8557                }
8558            });
8559        });
8560    }
8561
8562    fn toggle_thinking_effort_menu(
8563        &mut self,
8564        _action: &ToggleThinkingEffortMenu,
8565        window: &mut Window,
8566        cx: &mut Context<Self>,
8567    ) {
8568        let menu_handle = self.thinking_effort_menu_handle.clone();
8569        window.defer(cx, move |window, cx| {
8570            menu_handle.toggle(window, cx);
8571        });
8572    }
8573}
8574
8575impl Render for ThreadView {
8576    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
8577        let has_messages = self.list_state.item_count() > 0;
8578        let v2_empty_state = cx.has_flag::<AgentV2FeatureFlag>() && !has_messages;
8579
8580        let max_content_width = AgentSettings::get_global(cx).max_content_width;
8581
8582        let conversation = v_flex()
8583            .mx_auto()
8584            .max_w(max_content_width)
8585            .when(!v2_empty_state, |this| this.flex_1().size_full())
8586            .map(|this| {
8587                let this = this.when(self.resumed_without_history, |this| {
8588                    this.child(Self::render_resume_notice(cx))
8589                });
8590                if has_messages {
8591                    let list_state = self.list_state.clone();
8592                    this.child(self.render_entries(cx))
8593                        .vertical_scrollbar_for(&list_state, window, cx)
8594                        .into_any()
8595                } else if v2_empty_state {
8596                    this.into_any()
8597                } else {
8598                    this.child(self.render_recent_history(cx)).into_any()
8599                }
8600            });
8601
8602        v_flex()
8603            .key_context("AcpThread")
8604            .track_focus(&self.focus_handle)
8605            .on_action(cx.listener(|this, _: &menu::Cancel, _, cx| {
8606                if this.parent_id.is_none() {
8607                    this.cancel_generation(cx);
8608                }
8609            }))
8610            .on_action(cx.listener(|this, _: &workspace::GoBack, window, cx| {
8611                if let Some(parent_session_id) = this.parent_id.clone() {
8612                    this.server_view
8613                        .update(cx, |view, cx| {
8614                            view.navigate_to_session(parent_session_id, window, cx);
8615                        })
8616                        .ok();
8617                }
8618            }))
8619            .on_action(cx.listener(Self::keep_all))
8620            .on_action(cx.listener(Self::reject_all))
8621            .on_action(cx.listener(Self::undo_last_reject))
8622            .on_action(cx.listener(Self::allow_always))
8623            .on_action(cx.listener(Self::allow_once))
8624            .on_action(cx.listener(Self::reject_once))
8625            .on_action(cx.listener(Self::handle_authorize_tool_call))
8626            .on_action(cx.listener(Self::handle_select_permission_granularity))
8627            .on_action(cx.listener(Self::handle_toggle_command_pattern))
8628            .on_action(cx.listener(Self::open_permission_dropdown))
8629            .on_action(cx.listener(Self::open_add_context_menu))
8630            .on_action(cx.listener(Self::scroll_output_page_up))
8631            .on_action(cx.listener(Self::scroll_output_page_down))
8632            .on_action(cx.listener(Self::scroll_output_line_up))
8633            .on_action(cx.listener(Self::scroll_output_line_down))
8634            .on_action(cx.listener(Self::scroll_output_to_top))
8635            .on_action(cx.listener(Self::scroll_output_to_bottom))
8636            .on_action(cx.listener(Self::scroll_output_to_previous_message))
8637            .on_action(cx.listener(Self::scroll_output_to_next_message))
8638            .on_action(cx.listener(|this, _: &ToggleFastMode, _window, cx| {
8639                this.toggle_fast_mode(cx);
8640            }))
8641            .on_action(cx.listener(|this, _: &ToggleThinkingMode, _window, cx| {
8642                if this.thread.read(cx).status() != ThreadStatus::Idle {
8643                    return;
8644                }
8645                if let Some(thread) = this.as_native_thread(cx) {
8646                    thread.update(cx, |thread, cx| {
8647                        thread.set_thinking_enabled(!thread.thinking_enabled(), cx);
8648                    });
8649                }
8650            }))
8651            .on_action(cx.listener(|this, _: &CycleThinkingEffort, _window, cx| {
8652                if this.thread.read(cx).status() != ThreadStatus::Idle {
8653                    return;
8654                }
8655                this.cycle_thinking_effort(cx);
8656            }))
8657            .on_action(
8658                cx.listener(|this, action: &ToggleThinkingEffortMenu, window, cx| {
8659                    if this.thread.read(cx).status() != ThreadStatus::Idle {
8660                        return;
8661                    }
8662                    this.toggle_thinking_effort_menu(action, window, cx);
8663                }),
8664            )
8665            .on_action(cx.listener(|this, _: &SendNextQueuedMessage, window, cx| {
8666                this.send_queued_message_at_index(0, true, window, cx);
8667            }))
8668            .on_action(cx.listener(|this, _: &RemoveFirstQueuedMessage, _, cx| {
8669                this.remove_from_queue(0, cx);
8670                cx.notify();
8671            }))
8672            .on_action(cx.listener(|this, _: &EditFirstQueuedMessage, window, cx| {
8673                this.move_queued_message_to_main_editor(0, None, None, window, cx);
8674            }))
8675            .on_action(cx.listener(|this, _: &ClearMessageQueue, _, cx| {
8676                this.local_queued_messages.clear();
8677                this.sync_queue_flag_to_native_thread(cx);
8678                this.can_fast_track_queue = false;
8679                cx.notify();
8680            }))
8681            .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
8682                if this.thread.read(cx).status() != ThreadStatus::Idle {
8683                    return;
8684                }
8685                if let Some(config_options_view) = this.config_options_view.clone() {
8686                    let handled = config_options_view.update(cx, |view, cx| {
8687                        view.toggle_category_picker(
8688                            acp::SessionConfigOptionCategory::Mode,
8689                            window,
8690                            cx,
8691                        )
8692                    });
8693                    if handled {
8694                        return;
8695                    }
8696                }
8697
8698                if let Some(profile_selector) = this.profile_selector.clone() {
8699                    profile_selector.read(cx).menu_handle().toggle(window, cx);
8700                } else if let Some(mode_selector) = this.mode_selector.clone() {
8701                    mode_selector.read(cx).menu_handle().toggle(window, cx);
8702                }
8703            }))
8704            .on_action(cx.listener(|this, _: &CycleModeSelector, window, cx| {
8705                if this.thread.read(cx).status() != ThreadStatus::Idle {
8706                    return;
8707                }
8708                if let Some(config_options_view) = this.config_options_view.clone() {
8709                    let handled = config_options_view.update(cx, |view, cx| {
8710                        view.cycle_category_option(
8711                            acp::SessionConfigOptionCategory::Mode,
8712                            false,
8713                            cx,
8714                        )
8715                    });
8716                    if handled {
8717                        return;
8718                    }
8719                }
8720
8721                if let Some(profile_selector) = this.profile_selector.clone() {
8722                    profile_selector.update(cx, |profile_selector, cx| {
8723                        profile_selector.cycle_profile(cx);
8724                    });
8725                } else if let Some(mode_selector) = this.mode_selector.clone() {
8726                    mode_selector.update(cx, |mode_selector, cx| {
8727                        mode_selector.cycle_mode(window, cx);
8728                    });
8729                }
8730            }))
8731            .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
8732                if this.thread.read(cx).status() != ThreadStatus::Idle {
8733                    return;
8734                }
8735                if let Some(config_options_view) = this.config_options_view.clone() {
8736                    let handled = config_options_view.update(cx, |view, cx| {
8737                        view.toggle_category_picker(
8738                            acp::SessionConfigOptionCategory::Model,
8739                            window,
8740                            cx,
8741                        )
8742                    });
8743                    if handled {
8744                        return;
8745                    }
8746                }
8747
8748                if let Some(model_selector) = this.model_selector.clone() {
8749                    model_selector
8750                        .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
8751                }
8752            }))
8753            .on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
8754                if this.thread.read(cx).status() != ThreadStatus::Idle {
8755                    return;
8756                }
8757                if let Some(config_options_view) = this.config_options_view.clone() {
8758                    let handled = config_options_view.update(cx, |view, cx| {
8759                        view.cycle_category_option(
8760                            acp::SessionConfigOptionCategory::Model,
8761                            true,
8762                            cx,
8763                        )
8764                    });
8765                    if handled {
8766                        return;
8767                    }
8768                }
8769
8770                if let Some(model_selector) = this.model_selector.clone() {
8771                    model_selector.update(cx, |model_selector, cx| {
8772                        model_selector.cycle_favorite_models(window, cx);
8773                    });
8774                }
8775            }))
8776            .size_full()
8777            .children(self.render_subagent_titlebar(cx))
8778            .child(conversation)
8779            .children(self.render_activity_bar(window, cx))
8780            .when(self.show_external_source_prompt_warning, |this| {
8781                this.child(self.render_external_source_prompt_warning(cx))
8782            })
8783            .when(self.show_codex_windows_warning, |this| {
8784                this.child(self.render_codex_windows_warning(cx))
8785            })
8786            .children(self.render_thread_retry_status_callout())
8787            .children(self.render_thread_error(window, cx))
8788            .when_some(
8789                match has_messages {
8790                    true => None,
8791                    false => self.new_server_version_available.clone(),
8792                },
8793                |this, version| this.child(self.render_new_version_callout(&version, cx)),
8794            )
8795            .children(self.render_token_limit_callout(cx))
8796            .child(self.render_message_editor(window, cx))
8797    }
8798}
8799
8800pub(crate) fn open_link(
8801    url: SharedString,
8802    workspace: &WeakEntity<Workspace>,
8803    window: &mut Window,
8804    cx: &mut App,
8805) {
8806    let Some(workspace) = workspace.upgrade() else {
8807        cx.open_url(&url);
8808        return;
8809    };
8810
8811    if let Some(mention) = MentionUri::parse(&url, workspace.read(cx).path_style(cx)).log_err() {
8812        workspace.update(cx, |workspace, cx| match mention {
8813            MentionUri::File { abs_path } => {
8814                let project = workspace.project();
8815                let Some(path) =
8816                    project.update(cx, |project, cx| project.find_project_path(abs_path, cx))
8817                else {
8818                    return;
8819                };
8820
8821                workspace
8822                    .open_path(path, None, true, window, cx)
8823                    .detach_and_log_err(cx);
8824            }
8825            MentionUri::PastedImage { .. } => {}
8826            MentionUri::Directory { abs_path } => {
8827                let project = workspace.project();
8828                let Some(entry_id) = project.update(cx, |project, cx| {
8829                    let path = project.find_project_path(abs_path, cx)?;
8830                    project.entry_for_path(&path, cx).map(|entry| entry.id)
8831                }) else {
8832                    return;
8833                };
8834
8835                project.update(cx, |_, cx| {
8836                    cx.emit(project::Event::RevealInProjectPanel(entry_id));
8837                });
8838            }
8839            MentionUri::Symbol {
8840                abs_path: path,
8841                line_range,
8842                ..
8843            }
8844            | MentionUri::Selection {
8845                abs_path: Some(path),
8846                line_range,
8847            } => {
8848                let project = workspace.project();
8849                let Some(path) =
8850                    project.update(cx, |project, cx| project.find_project_path(path, cx))
8851                else {
8852                    return;
8853                };
8854
8855                let item = workspace.open_path(path, None, true, window, cx);
8856                window
8857                    .spawn(cx, async move |cx| {
8858                        let Some(editor) = item.await?.downcast::<Editor>() else {
8859                            return Ok(());
8860                        };
8861                        let range =
8862                            Point::new(*line_range.start(), 0)..Point::new(*line_range.start(), 0);
8863                        editor
8864                            .update_in(cx, |editor, window, cx| {
8865                                editor.change_selections(
8866                                    SelectionEffects::scroll(Autoscroll::center()),
8867                                    window,
8868                                    cx,
8869                                    |s| s.select_ranges(vec![range]),
8870                                );
8871                            })
8872                            .ok();
8873                        anyhow::Ok(())
8874                    })
8875                    .detach_and_log_err(cx);
8876            }
8877            MentionUri::Selection { abs_path: None, .. } => {}
8878            MentionUri::Thread { id, name } => {
8879                if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
8880                    panel.update(cx, |panel, cx| {
8881                        panel.open_thread(id, None, Some(name.into()), window, cx)
8882                    });
8883                }
8884            }
8885            MentionUri::Rule { id, .. } => {
8886                let PromptId::User { uuid } = id else {
8887                    return;
8888                };
8889                window.dispatch_action(
8890                    Box::new(OpenRulesLibrary {
8891                        prompt_to_select: Some(uuid.0),
8892                    }),
8893                    cx,
8894                )
8895            }
8896            MentionUri::Fetch { url } => {
8897                cx.open_url(url.as_str());
8898            }
8899            MentionUri::Diagnostics { .. } => {}
8900            MentionUri::TerminalSelection { .. } => {}
8901            MentionUri::GitDiff { .. } => {}
8902            MentionUri::MergeConflict { .. } => {}
8903        })
8904    } else {
8905        cx.open_url(&url);
8906    }
8907}