thread_view.rs

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