thread_view.rs

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