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