thread_view.rs

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