active_thread.rs

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