thread_view.rs

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