thread_view.rs

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