thread_view.rs

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