active_thread.rs

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