thread_view.rs

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