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