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, color) = if thinking {
2773            (
2774                "Disable Thinking Mode",
2775                IconName::ThinkingMode,
2776                Color::Muted,
2777            )
2778        } else {
2779            (
2780                "Enable Thinking Mode",
2781                IconName::ThinkingModeOff,
2782                Color::Custom(cx.theme().colors().icon_disabled.opacity(0.8)),
2783            )
2784        };
2785
2786        let focus_handle = self.message_editor.focus_handle(cx);
2787
2788        let thinking_toggle = IconButton::new("thinking-mode", icon)
2789            .icon_size(IconSize::Small)
2790            .icon_color(color)
2791            .tooltip(move |_, cx| {
2792                Tooltip::for_action_in(tooltip_label, &ToggleThinkingMode, &focus_handle, cx)
2793            })
2794            .on_click(cx.listener(move |this, _, _window, cx| {
2795                if let Some(thread) = this.as_native_thread(cx) {
2796                    thread.update(cx, |thread, cx| {
2797                        let enable_thinking = !thread.thinking_enabled();
2798                        thread.set_thinking_enabled(enable_thinking, cx);
2799
2800                        let fs = thread.project().read(cx).fs().clone();
2801                        update_settings_file(fs, cx, move |settings, _| {
2802                            if let Some(agent) = settings.agent.as_mut()
2803                                && let Some(default_model) = agent.default_model.as_mut()
2804                            {
2805                                default_model.enable_thinking = enable_thinking;
2806                            }
2807                        });
2808                    });
2809                }
2810            }));
2811
2812        if model.supported_effort_levels().is_empty() {
2813            return Some(thinking_toggle.into_any_element());
2814        }
2815
2816        if !model.supported_effort_levels().is_empty() && !thinking {
2817            return Some(thinking_toggle.into_any_element());
2818        }
2819
2820        let left_btn = thinking_toggle;
2821        let right_btn = self.render_effort_selector(
2822            model.supported_effort_levels(),
2823            thread.thinking_effort().cloned(),
2824            cx,
2825        );
2826
2827        Some(
2828            SplitButton::new(left_btn, right_btn.into_any_element())
2829                .style(SplitButtonStyle::Transparent)
2830                .into_any_element(),
2831        )
2832    }
2833
2834    fn render_effort_selector(
2835        &self,
2836        supported_effort_levels: Vec<LanguageModelEffortLevel>,
2837        selected_effort: Option<String>,
2838        cx: &Context<Self>,
2839    ) -> impl IntoElement {
2840        let weak_self = cx.weak_entity();
2841
2842        let default_effort_level = supported_effort_levels
2843            .iter()
2844            .find(|effort_level| effort_level.is_default)
2845            .cloned();
2846
2847        let selected = selected_effort.and_then(|effort| {
2848            supported_effort_levels
2849                .iter()
2850                .find(|level| level.value == effort)
2851                .cloned()
2852        });
2853
2854        let label = selected
2855            .clone()
2856            .or(default_effort_level)
2857            .map_or("Select Effort".into(), |effort| effort.name);
2858
2859        let (label_color, icon) = if self.thinking_effort_menu_handle.is_deployed() {
2860            (Color::Accent, IconName::ChevronUp)
2861        } else {
2862            (Color::Muted, IconName::ChevronDown)
2863        };
2864
2865        let focus_handle = self.message_editor.focus_handle(cx);
2866        let show_cycle_row = supported_effort_levels.len() > 1;
2867
2868        let tooltip = Tooltip::element({
2869            move |_, cx| {
2870                let mut content = v_flex().gap_1().child(
2871                    h_flex()
2872                        .gap_2()
2873                        .justify_between()
2874                        .child(Label::new("Change Thinking Effort"))
2875                        .child(KeyBinding::for_action_in(
2876                            &ToggleThinkingEffortMenu,
2877                            &focus_handle,
2878                            cx,
2879                        )),
2880                );
2881
2882                if show_cycle_row {
2883                    content = content.child(
2884                        h_flex()
2885                            .pt_1()
2886                            .gap_2()
2887                            .justify_between()
2888                            .border_t_1()
2889                            .border_color(cx.theme().colors().border_variant)
2890                            .child(Label::new("Cycle Thinking Effort"))
2891                            .child(KeyBinding::for_action_in(
2892                                &CycleThinkingEffort,
2893                                &focus_handle,
2894                                cx,
2895                            )),
2896                    );
2897                }
2898
2899                content.into_any_element()
2900            }
2901        });
2902
2903        PopoverMenu::new("effort-selector")
2904            .trigger_with_tooltip(
2905                ButtonLike::new_rounded_right("effort-selector-trigger")
2906                    .selected_style(ButtonStyle::Tinted(TintColor::Accent))
2907                    .child(Label::new(label).size(LabelSize::Small).color(label_color))
2908                    .child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted)),
2909                tooltip,
2910            )
2911            .menu(move |window, cx| {
2912                Some(ContextMenu::build(window, cx, |mut menu, _window, _cx| {
2913                    menu = menu.header("Change Thinking Effort");
2914
2915                    for effort_level in supported_effort_levels.clone() {
2916                        let is_selected = selected
2917                            .as_ref()
2918                            .is_some_and(|selected| selected.value == effort_level.value);
2919                        let entry = ContextMenuEntry::new(effort_level.name)
2920                            .toggleable(IconPosition::End, is_selected);
2921
2922                        menu.push_item(entry.handler({
2923                            let effort = effort_level.value.clone();
2924                            let weak_self = weak_self.clone();
2925                            move |_window, cx| {
2926                                let effort = effort.clone();
2927                                weak_self
2928                                    .update(cx, |this, cx| {
2929                                        if let Some(thread) = this.as_native_thread(cx) {
2930                                            thread.update(cx, |thread, cx| {
2931                                                thread.set_thinking_effort(
2932                                                    Some(effort.to_string()),
2933                                                    cx,
2934                                                );
2935
2936                                                let fs = thread.project().read(cx).fs().clone();
2937                                                update_settings_file(fs, cx, move |settings, _| {
2938                                                    if let Some(agent) = settings.agent.as_mut()
2939                                                        && let Some(default_model) =
2940                                                            agent.default_model.as_mut()
2941                                                    {
2942                                                        default_model.effort =
2943                                                            Some(effort.to_string());
2944                                                    }
2945                                                });
2946                                            });
2947                                        }
2948                                    })
2949                                    .ok();
2950                            }
2951                        }));
2952                    }
2953
2954                    menu
2955                }))
2956            })
2957            .with_handle(self.thinking_effort_menu_handle.clone())
2958            .offset(gpui::Point {
2959                x: px(0.0),
2960                y: px(-2.0),
2961            })
2962            .anchor(Corner::BottomLeft)
2963    }
2964
2965    fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
2966        let message_editor = self.message_editor.read(cx);
2967        let is_editor_empty = message_editor.is_empty(cx);
2968        let focus_handle = message_editor.focus_handle(cx);
2969
2970        let is_generating = self.thread.read(cx).status() != ThreadStatus::Idle;
2971
2972        if self.is_loading_contents {
2973            div()
2974                .id("loading-message-content")
2975                .px_1()
2976                .tooltip(Tooltip::text("Loading Added Context…"))
2977                .child(loading_contents_spinner(IconSize::default()))
2978                .into_any_element()
2979        } else if is_generating && is_editor_empty {
2980            IconButton::new("stop-generation", IconName::Stop)
2981                .icon_color(Color::Error)
2982                .style(ButtonStyle::Tinted(TintColor::Error))
2983                .tooltip(move |_window, cx| {
2984                    Tooltip::for_action("Stop Generation", &editor::actions::Cancel, cx)
2985                })
2986                .on_click(cx.listener(|this, _event, _, cx| this.cancel_generation(cx)))
2987                .into_any_element()
2988        } else {
2989            IconButton::new("send-message", IconName::Send)
2990                .style(ButtonStyle::Filled)
2991                .map(|this| {
2992                    if is_editor_empty && !is_generating {
2993                        this.disabled(true).icon_color(Color::Muted)
2994                    } else {
2995                        this.icon_color(Color::Accent)
2996                    }
2997                })
2998                .tooltip(move |_window, cx| {
2999                    if is_editor_empty && !is_generating {
3000                        Tooltip::for_action("Type to Send", &Chat, cx)
3001                    } else if is_generating {
3002                        let focus_handle = focus_handle.clone();
3003
3004                        Tooltip::element(move |_window, cx| {
3005                            v_flex()
3006                                .gap_1()
3007                                .child(
3008                                    h_flex()
3009                                        .gap_2()
3010                                        .justify_between()
3011                                        .child(Label::new("Queue and Send"))
3012                                        .child(KeyBinding::for_action_in(&Chat, &focus_handle, cx)),
3013                                )
3014                                .child(
3015                                    h_flex()
3016                                        .pt_1()
3017                                        .gap_2()
3018                                        .justify_between()
3019                                        .border_t_1()
3020                                        .border_color(cx.theme().colors().border_variant)
3021                                        .child(Label::new("Send Immediately"))
3022                                        .child(KeyBinding::for_action_in(
3023                                            &SendImmediately,
3024                                            &focus_handle,
3025                                            cx,
3026                                        )),
3027                                )
3028                                .into_any_element()
3029                        })(_window, cx)
3030                    } else {
3031                        Tooltip::for_action("Send Message", &Chat, cx)
3032                    }
3033                })
3034                .on_click(cx.listener(|this, _, window, cx| {
3035                    this.send(window, cx);
3036                }))
3037                .into_any_element()
3038        }
3039    }
3040
3041    fn render_add_context_button(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
3042        let focus_handle = self.message_editor.focus_handle(cx);
3043        let weak_self = cx.weak_entity();
3044
3045        PopoverMenu::new("add-context-menu")
3046            .trigger_with_tooltip(
3047                IconButton::new("add-context", IconName::Plus)
3048                    .icon_size(IconSize::Small)
3049                    .icon_color(Color::Muted),
3050                {
3051                    move |_window, cx| {
3052                        Tooltip::for_action_in(
3053                            "Add Context",
3054                            &OpenAddContextMenu,
3055                            &focus_handle,
3056                            cx,
3057                        )
3058                    }
3059                },
3060            )
3061            .anchor(Corner::BottomLeft)
3062            .with_handle(self.add_context_menu_handle.clone())
3063            .offset(gpui::Point {
3064                x: px(0.0),
3065                y: px(-2.0),
3066            })
3067            .menu(move |window, cx| {
3068                weak_self
3069                    .update(cx, |this, cx| this.build_add_context_menu(window, cx))
3070                    .ok()
3071            })
3072    }
3073
3074    fn build_add_context_menu(
3075        &self,
3076        window: &mut Window,
3077        cx: &mut Context<Self>,
3078    ) -> Entity<ContextMenu> {
3079        let message_editor = self.message_editor.clone();
3080        let workspace = self.workspace.clone();
3081        let supports_images = self.prompt_capabilities.borrow().image;
3082
3083        let has_editor_selection = workspace
3084            .upgrade()
3085            .and_then(|ws| {
3086                ws.read(cx)
3087                    .active_item(cx)
3088                    .and_then(|item| item.downcast::<Editor>())
3089            })
3090            .is_some_and(|editor| {
3091                editor.update(cx, |editor, cx| {
3092                    editor.has_non_empty_selection(&editor.display_snapshot(cx))
3093                })
3094            });
3095
3096        let has_terminal_selection = workspace
3097            .upgrade()
3098            .and_then(|ws| ws.read(cx).panel::<TerminalPanel>(cx))
3099            .is_some_and(|panel| !panel.read(cx).terminal_selections(cx).is_empty());
3100
3101        let has_selection = has_editor_selection || has_terminal_selection;
3102
3103        ContextMenu::build(window, cx, move |menu, _window, _cx| {
3104            menu.key_context("AddContextMenu")
3105                .header("Context")
3106                .item(
3107                    ContextMenuEntry::new("Files & Directories")
3108                        .icon(IconName::File)
3109                        .icon_color(Color::Muted)
3110                        .icon_size(IconSize::XSmall)
3111                        .handler({
3112                            let message_editor = message_editor.clone();
3113                            move |window, cx| {
3114                                message_editor.focus_handle(cx).focus(window, cx);
3115                                message_editor.update(cx, |editor, cx| {
3116                                    editor.insert_context_type("file", window, cx);
3117                                });
3118                            }
3119                        }),
3120                )
3121                .item(
3122                    ContextMenuEntry::new("Symbols")
3123                        .icon(IconName::Code)
3124                        .icon_color(Color::Muted)
3125                        .icon_size(IconSize::XSmall)
3126                        .handler({
3127                            let message_editor = message_editor.clone();
3128                            move |window, cx| {
3129                                message_editor.focus_handle(cx).focus(window, cx);
3130                                message_editor.update(cx, |editor, cx| {
3131                                    editor.insert_context_type("symbol", window, cx);
3132                                });
3133                            }
3134                        }),
3135                )
3136                .item(
3137                    ContextMenuEntry::new("Threads")
3138                        .icon(IconName::Thread)
3139                        .icon_color(Color::Muted)
3140                        .icon_size(IconSize::XSmall)
3141                        .handler({
3142                            let message_editor = message_editor.clone();
3143                            move |window, cx| {
3144                                message_editor.focus_handle(cx).focus(window, cx);
3145                                message_editor.update(cx, |editor, cx| {
3146                                    editor.insert_context_type("thread", window, cx);
3147                                });
3148                            }
3149                        }),
3150                )
3151                .item(
3152                    ContextMenuEntry::new("Rules")
3153                        .icon(IconName::Reader)
3154                        .icon_color(Color::Muted)
3155                        .icon_size(IconSize::XSmall)
3156                        .handler({
3157                            let message_editor = message_editor.clone();
3158                            move |window, cx| {
3159                                message_editor.focus_handle(cx).focus(window, cx);
3160                                message_editor.update(cx, |editor, cx| {
3161                                    editor.insert_context_type("rule", window, cx);
3162                                });
3163                            }
3164                        }),
3165                )
3166                .item(
3167                    ContextMenuEntry::new("Image")
3168                        .icon(IconName::Image)
3169                        .icon_color(Color::Muted)
3170                        .icon_size(IconSize::XSmall)
3171                        .disabled(!supports_images)
3172                        .handler({
3173                            let message_editor = message_editor.clone();
3174                            move |window, cx| {
3175                                message_editor.focus_handle(cx).focus(window, cx);
3176                                message_editor.update(cx, |editor, cx| {
3177                                    editor.add_images_from_picker(window, cx);
3178                                });
3179                            }
3180                        }),
3181                )
3182                .item(
3183                    ContextMenuEntry::new("Selection")
3184                        .icon(IconName::CursorIBeam)
3185                        .icon_color(Color::Muted)
3186                        .icon_size(IconSize::XSmall)
3187                        .disabled(!has_selection)
3188                        .handler({
3189                            move |window, cx| {
3190                                window.dispatch_action(
3191                                    zed_actions::agent::AddSelectionToThread.boxed_clone(),
3192                                    cx,
3193                                );
3194                            }
3195                        }),
3196                )
3197        })
3198    }
3199
3200    fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
3201        let following = self.is_following(cx);
3202
3203        let tooltip_label = if following {
3204            if self.agent_name == "Zed Agent" {
3205                format!("Stop Following the {}", self.agent_name)
3206            } else {
3207                format!("Stop Following {}", self.agent_name)
3208            }
3209        } else {
3210            if self.agent_name == "Zed Agent" {
3211                format!("Follow the {}", self.agent_name)
3212            } else {
3213                format!("Follow {}", self.agent_name)
3214            }
3215        };
3216
3217        IconButton::new("follow-agent", IconName::Crosshair)
3218            .icon_size(IconSize::Small)
3219            .icon_color(Color::Muted)
3220            .toggle_state(following)
3221            .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
3222            .tooltip(move |_window, cx| {
3223                if following {
3224                    Tooltip::for_action(tooltip_label.clone(), &Follow, cx)
3225                } else {
3226                    Tooltip::with_meta(
3227                        tooltip_label.clone(),
3228                        Some(&Follow),
3229                        "Track the agent's location as it reads and edits files.",
3230                        cx,
3231                    )
3232                }
3233            })
3234            .on_click(cx.listener(move |this, _, window, cx| {
3235                this.toggle_following(window, cx);
3236            }))
3237    }
3238}
3239
3240impl AcpThreadView {
3241    pub(crate) fn render_entries(&mut self, cx: &mut Context<Self>) -> List {
3242        list(
3243            self.list_state.clone(),
3244            cx.processor(|this, index: usize, window, cx| {
3245                let entries = this.thread.read(cx).entries();
3246                let Some(entry) = entries.get(index) else {
3247                    return Empty.into_any();
3248                };
3249                this.render_entry(index, entries.len(), entry, window, cx)
3250            }),
3251        )
3252        .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
3253        .flex_grow()
3254    }
3255
3256    fn render_entry(
3257        &self,
3258        entry_ix: usize,
3259        total_entries: usize,
3260        entry: &AgentThreadEntry,
3261        window: &mut Window,
3262        cx: &Context<Self>,
3263    ) -> AnyElement {
3264        let is_indented = entry.is_indented();
3265        let is_first_indented = is_indented
3266            && self
3267                .thread
3268                .read(cx)
3269                .entries()
3270                .get(entry_ix.saturating_sub(1))
3271                .is_none_or(|entry| !entry.is_indented());
3272
3273        let primary = match &entry {
3274            AgentThreadEntry::UserMessage(message) => {
3275                let Some(editor) = self
3276                    .entry_view_state
3277                    .read(cx)
3278                    .entry(entry_ix)
3279                    .and_then(|entry| entry.message_editor())
3280                    .cloned()
3281                else {
3282                    return Empty.into_any_element();
3283                };
3284
3285                let editing = self.editing_message == Some(entry_ix);
3286                let editor_focus = editor.focus_handle(cx).is_focused(window);
3287                let focus_border = cx.theme().colors().border_focused;
3288
3289                let rules_item = if entry_ix == 0 {
3290                    self.render_rules_item(cx)
3291                } else {
3292                    None
3293                };
3294
3295                let has_checkpoint_button = message
3296                    .checkpoint
3297                    .as_ref()
3298                    .is_some_and(|checkpoint| checkpoint.show);
3299
3300                let agent_name = self.agent_name.clone();
3301                let is_subagent = self.is_subagent();
3302
3303                let non_editable_icon = || {
3304                    IconButton::new("non_editable", IconName::PencilUnavailable)
3305                        .icon_size(IconSize::Small)
3306                        .icon_color(Color::Muted)
3307                        .style(ButtonStyle::Transparent)
3308                };
3309
3310                v_flex()
3311                    .id(("user_message", entry_ix))
3312                    .map(|this| {
3313                        if is_first_indented {
3314                            this.pt_0p5()
3315                        } else if entry_ix == 0 && !has_checkpoint_button && rules_item.is_none()  {
3316                            this.pt(rems_from_px(18.))
3317                        } else if rules_item.is_some() {
3318                            this.pt_3()
3319                        } else {
3320                            this.pt_2()
3321                        }
3322                    })
3323                    .pb_3()
3324                    .px_2()
3325                    .gap_1p5()
3326                    .w_full()
3327                    .children(rules_item)
3328                    .children(message.id.clone().and_then(|message_id| {
3329                        message.checkpoint.as_ref()?.show.then(|| {
3330                            h_flex()
3331                                .px_3()
3332                                .gap_2()
3333                                .child(Divider::horizontal())
3334                                .child(
3335                                    Button::new("restore-checkpoint", "Restore Checkpoint")
3336                                        .icon(IconName::Undo)
3337                                        .icon_size(IconSize::XSmall)
3338                                        .icon_position(IconPosition::Start)
3339                                        .label_size(LabelSize::XSmall)
3340                                        .icon_color(Color::Muted)
3341                                        .color(Color::Muted)
3342                                        .tooltip(Tooltip::text("Restores all files in the project to the content they had at this point in the conversation."))
3343                                        .on_click(cx.listener(move |this, _, _window, cx| {
3344                                            this.restore_checkpoint(&message_id, cx);
3345                                        }))
3346                                )
3347                                .child(Divider::horizontal())
3348                        })
3349                    }))
3350                    .child(
3351                        div()
3352                            .relative()
3353                            .child(
3354                                div()
3355                                    .py_3()
3356                                    .px_2()
3357                                    .rounded_md()
3358                                    .bg(cx.theme().colors().editor_background)
3359                                    .border_1()
3360                                    .when(is_indented, |this| {
3361                                        this.py_2().px_2().shadow_sm()
3362                                    })
3363                                    .border_color(cx.theme().colors().border)
3364                                    .map(|this| {
3365                                        if is_subagent {
3366                                            return this.border_dashed();
3367                                        }
3368                                        if editing && editor_focus {
3369                                            return this.border_color(focus_border);
3370                                        }
3371                                        if editing && !editor_focus {
3372                                            return this.border_dashed()
3373                                        }
3374                                        if message.id.is_some() {
3375                                            return this.shadow_md().hover(|s| {
3376                                                s.border_color(focus_border.opacity(0.8))
3377                                            });
3378                                        }
3379                                        this
3380                                    })
3381                                    .text_xs()
3382                                    .child(editor.clone().into_any_element())
3383                            )
3384                            .when(editor_focus, |this| {
3385                                let base_container = h_flex()
3386                                    .absolute()
3387                                    .top_neg_3p5()
3388                                    .right_3()
3389                                    .gap_1()
3390                                    .rounded_sm()
3391                                    .border_1()
3392                                    .border_color(cx.theme().colors().border)
3393                                    .bg(cx.theme().colors().editor_background)
3394                                    .overflow_hidden();
3395
3396                                let is_loading_contents = self.is_loading_contents;
3397                                if is_subagent {
3398                                    this.child(
3399                                        base_container.border_dashed().child(
3400                                            non_editable_icon().tooltip(move |_, cx| {
3401                                                Tooltip::with_meta(
3402                                                    "Unavailable Editing",
3403                                                    None,
3404                                                    "Editing subagent messages is currently not supported.",
3405                                                    cx,
3406                                                )
3407                                            }),
3408                                        ),
3409                                    )
3410                                } else if message.id.is_some() {
3411                                    this.child(
3412                                        base_container
3413                                            .child(
3414                                                IconButton::new("cancel", IconName::Close)
3415                                                    .disabled(is_loading_contents)
3416                                                    .icon_color(Color::Error)
3417                                                    .icon_size(IconSize::XSmall)
3418                                                    .on_click(cx.listener(Self::cancel_editing))
3419                                            )
3420                                            .child(
3421                                                if is_loading_contents {
3422                                                    div()
3423                                                        .id("loading-edited-message-content")
3424                                                        .tooltip(Tooltip::text("Loading Added Context…"))
3425                                                        .child(loading_contents_spinner(IconSize::XSmall))
3426                                                        .into_any_element()
3427                                                } else {
3428                                                    IconButton::new("regenerate", IconName::Return)
3429                                                        .icon_color(Color::Muted)
3430                                                        .icon_size(IconSize::XSmall)
3431                                                        .tooltip(Tooltip::text(
3432                                                            "Editing will restart the thread from this point."
3433                                                        ))
3434                                                        .on_click(cx.listener({
3435                                                            let editor = editor.clone();
3436                                                            move |this, _, window, cx| {
3437                                                                this.regenerate(
3438                                                                    entry_ix, editor.clone(), window, cx,
3439                                                                );
3440                                                            }
3441                                                        })).into_any_element()
3442                                                }
3443                                            )
3444                                    )
3445                                } else {
3446                                    this.child(
3447                                        base_container
3448                                            .border_dashed()
3449                                            .child(
3450                                                non_editable_icon()
3451                                                    .tooltip(Tooltip::element({
3452                                                        move |_, _| {
3453                                                            v_flex()
3454                                                                .gap_1()
3455                                                                .child(Label::new("Unavailable Editing")).child(
3456                                                                    div().max_w_64().child(
3457                                                                        Label::new(format!(
3458                                                                            "Editing previous messages is not available for {} yet.",
3459                                                                            agent_name.clone()
3460                                                                        ))
3461                                                                        .size(LabelSize::Small)
3462                                                                        .color(Color::Muted),
3463                                                                    ),
3464                                                                )
3465                                                                .into_any_element()
3466                                                        }
3467                                                    }))
3468                                            )
3469                                    )
3470                                }
3471                            }),
3472                    )
3473                    .into_any()
3474            }
3475            AgentThreadEntry::AssistantMessage(AssistantMessage {
3476                chunks,
3477                indented: _,
3478            }) => {
3479                let mut is_blank = true;
3480                let is_last = entry_ix + 1 == total_entries;
3481
3482                let style = MarkdownStyle::themed(MarkdownFont::Agent, window, cx);
3483                let message_body = v_flex()
3484                    .w_full()
3485                    .gap_3()
3486                    .children(chunks.iter().enumerate().filter_map(
3487                        |(chunk_ix, chunk)| match chunk {
3488                            AssistantMessageChunk::Message { block } => {
3489                                block.markdown().and_then(|md| {
3490                                    let this_is_blank = md.read(cx).source().trim().is_empty();
3491                                    is_blank = is_blank && this_is_blank;
3492                                    if this_is_blank {
3493                                        return None;
3494                                    }
3495
3496                                    Some(
3497                                        self.render_markdown(md.clone(), style.clone())
3498                                            .into_any_element(),
3499                                    )
3500                                })
3501                            }
3502                            AssistantMessageChunk::Thought { block } => {
3503                                block.markdown().and_then(|md| {
3504                                    let this_is_blank = md.read(cx).source().trim().is_empty();
3505                                    is_blank = is_blank && this_is_blank;
3506                                    if this_is_blank {
3507                                        return None;
3508                                    }
3509                                    Some(
3510                                        self.render_thinking_block(
3511                                            entry_ix,
3512                                            chunk_ix,
3513                                            md.clone(),
3514                                            window,
3515                                            cx,
3516                                        )
3517                                        .into_any_element(),
3518                                    )
3519                                })
3520                            }
3521                        },
3522                    ))
3523                    .into_any();
3524
3525                if is_blank {
3526                    Empty.into_any()
3527                } else {
3528                    v_flex()
3529                        .px_5()
3530                        .py_1p5()
3531                        .when(is_last, |this| this.pb_4())
3532                        .w_full()
3533                        .text_ui(cx)
3534                        .child(self.render_message_context_menu(entry_ix, message_body, cx))
3535                        .into_any()
3536                }
3537            }
3538            AgentThreadEntry::ToolCall(tool_call) => {
3539                let has_terminals = tool_call.terminals().next().is_some();
3540
3541                div()
3542                    .w_full()
3543                    .map(|this| {
3544                        if has_terminals {
3545                            this.children(tool_call.terminals().map(|terminal| {
3546                                self.render_terminal_tool_call(
3547                                    entry_ix, terminal, tool_call, window, cx,
3548                                )
3549                            }))
3550                        } else {
3551                            this.child(self.render_tool_call(entry_ix, tool_call, window, cx))
3552                        }
3553                    })
3554                    .into_any()
3555            }
3556        };
3557
3558        let primary = if is_indented {
3559            let line_top = if is_first_indented {
3560                rems_from_px(-12.0)
3561            } else {
3562                rems_from_px(0.0)
3563            };
3564
3565            div()
3566                .relative()
3567                .w_full()
3568                .pl_5()
3569                .bg(cx.theme().colors().panel_background.opacity(0.2))
3570                .child(
3571                    div()
3572                        .absolute()
3573                        .left(rems_from_px(18.0))
3574                        .top(line_top)
3575                        .bottom_0()
3576                        .w_px()
3577                        .bg(cx.theme().colors().border.opacity(0.6)),
3578                )
3579                .child(primary)
3580                .into_any_element()
3581        } else {
3582            primary
3583        };
3584
3585        let needs_confirmation = if let AgentThreadEntry::ToolCall(tool_call) = entry {
3586            matches!(
3587                tool_call.status,
3588                ToolCallStatus::WaitingForConfirmation { .. }
3589            )
3590        } else {
3591            false
3592        };
3593
3594        let thread = self.thread.clone();
3595        let comments_editor = self.thread_feedback.comments_editor.clone();
3596
3597        let primary = if entry_ix == total_entries - 1 {
3598            v_flex()
3599                .w_full()
3600                .child(primary)
3601                .map(|this| {
3602                    if needs_confirmation {
3603                        this.child(self.render_generating(true, cx))
3604                    } else {
3605                        this.child(self.render_thread_controls(&thread, cx))
3606                    }
3607                })
3608                .when_some(comments_editor, |this, editor| {
3609                    this.child(Self::render_feedback_feedback_editor(editor, cx))
3610                })
3611                .into_any_element()
3612        } else {
3613            primary
3614        };
3615
3616        if let Some(editing_index) = self.editing_message
3617            && editing_index < entry_ix
3618        {
3619            let backdrop = div()
3620                .id(("backdrop", entry_ix))
3621                .size_full()
3622                .absolute()
3623                .inset_0()
3624                .bg(cx.theme().colors().panel_background)
3625                .opacity(0.8)
3626                .block_mouse_except_scroll()
3627                .on_click(cx.listener(Self::cancel_editing));
3628
3629            div()
3630                .relative()
3631                .child(primary)
3632                .child(backdrop)
3633                .into_any_element()
3634        } else {
3635            primary
3636        }
3637    }
3638
3639    fn render_feedback_feedback_editor(editor: Entity<Editor>, cx: &Context<Self>) -> Div {
3640        h_flex()
3641            .key_context("AgentFeedbackMessageEditor")
3642            .on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| {
3643                this.thread_feedback.dismiss_comments();
3644                cx.notify();
3645            }))
3646            .on_action(cx.listener(move |this, _: &menu::Confirm, _window, cx| {
3647                this.submit_feedback_message(cx);
3648            }))
3649            .p_2()
3650            .mb_2()
3651            .mx_5()
3652            .gap_1()
3653            .rounded_md()
3654            .border_1()
3655            .border_color(cx.theme().colors().border)
3656            .bg(cx.theme().colors().editor_background)
3657            .child(div().w_full().child(editor))
3658            .child(
3659                h_flex()
3660                    .child(
3661                        IconButton::new("dismiss-feedback-message", IconName::Close)
3662                            .icon_color(Color::Error)
3663                            .icon_size(IconSize::XSmall)
3664                            .shape(ui::IconButtonShape::Square)
3665                            .on_click(cx.listener(move |this, _, _window, cx| {
3666                                this.thread_feedback.dismiss_comments();
3667                                cx.notify();
3668                            })),
3669                    )
3670                    .child(
3671                        IconButton::new("submit-feedback-message", IconName::Return)
3672                            .icon_size(IconSize::XSmall)
3673                            .shape(ui::IconButtonShape::Square)
3674                            .on_click(cx.listener(move |this, _, _window, cx| {
3675                                this.submit_feedback_message(cx);
3676                            })),
3677                    ),
3678            )
3679    }
3680
3681    fn render_thread_controls(
3682        &self,
3683        thread: &Entity<AcpThread>,
3684        cx: &Context<Self>,
3685    ) -> impl IntoElement {
3686        let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
3687        if is_generating {
3688            return self.render_generating(false, cx).into_any_element();
3689        }
3690
3691        let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
3692            .shape(ui::IconButtonShape::Square)
3693            .icon_size(IconSize::Small)
3694            .icon_color(Color::Ignored)
3695            .tooltip(Tooltip::text("Open Thread as Markdown"))
3696            .on_click(cx.listener(move |this, _, window, cx| {
3697                if let Some(workspace) = this.workspace.upgrade() {
3698                    this.open_thread_as_markdown(workspace, window, cx)
3699                        .detach_and_log_err(cx);
3700                }
3701            }));
3702
3703        let scroll_to_recent_user_prompt =
3704            IconButton::new("scroll_to_recent_user_prompt", IconName::ForwardArrow)
3705                .shape(ui::IconButtonShape::Square)
3706                .icon_size(IconSize::Small)
3707                .icon_color(Color::Ignored)
3708                .tooltip(Tooltip::text("Scroll To Most Recent User Prompt"))
3709                .on_click(cx.listener(move |this, _, _, cx| {
3710                    this.scroll_to_most_recent_user_prompt(cx);
3711                }));
3712
3713        let scroll_to_top = IconButton::new("scroll_to_top", IconName::ArrowUp)
3714            .shape(ui::IconButtonShape::Square)
3715            .icon_size(IconSize::Small)
3716            .icon_color(Color::Ignored)
3717            .tooltip(Tooltip::text("Scroll To Top"))
3718            .on_click(cx.listener(move |this, _, _, cx| {
3719                this.scroll_to_top(cx);
3720            }));
3721
3722        let show_stats = AgentSettings::get_global(cx).show_turn_stats;
3723        let last_turn_clock = show_stats
3724            .then(|| {
3725                self.turn_fields
3726                    .last_turn_duration
3727                    .filter(|&duration| duration > STOPWATCH_THRESHOLD)
3728                    .map(|duration| {
3729                        Label::new(duration_alt_display(duration))
3730                            .size(LabelSize::Small)
3731                            .color(Color::Muted)
3732                    })
3733            })
3734            .flatten();
3735
3736        let last_turn_tokens_label = last_turn_clock
3737            .is_some()
3738            .then(|| {
3739                self.turn_fields
3740                    .last_turn_tokens
3741                    .filter(|&tokens| tokens > TOKEN_THRESHOLD)
3742                    .map(|tokens| {
3743                        Label::new(format!(
3744                            "{} tokens",
3745                            crate::text_thread_editor::humanize_token_count(tokens)
3746                        ))
3747                        .size(LabelSize::Small)
3748                        .color(Color::Muted)
3749                    })
3750            })
3751            .flatten();
3752
3753        let mut container = h_flex()
3754            .w_full()
3755            .py_2()
3756            .px_5()
3757            .gap_px()
3758            .opacity(0.6)
3759            .hover(|s| s.opacity(1.))
3760            .justify_end()
3761            .when(
3762                last_turn_tokens_label.is_some() || last_turn_clock.is_some(),
3763                |this| {
3764                    this.child(
3765                        h_flex()
3766                            .gap_1()
3767                            .px_1()
3768                            .when_some(last_turn_tokens_label, |this, label| this.child(label))
3769                            .when_some(last_turn_clock, |this, label| this.child(label)),
3770                    )
3771                },
3772            );
3773
3774        if AgentSettings::get_global(cx).enable_feedback
3775            && self.thread.read(cx).connection().telemetry().is_some()
3776        {
3777            let feedback = self.thread_feedback.feedback;
3778
3779            let tooltip_meta = || {
3780                SharedString::new(
3781                    "Rating the thread sends all of your current conversation to the Zed team.",
3782                )
3783            };
3784
3785            container = container
3786                    .child(
3787                        IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
3788                            .shape(ui::IconButtonShape::Square)
3789                            .icon_size(IconSize::Small)
3790                            .icon_color(match feedback {
3791                                Some(ThreadFeedback::Positive) => Color::Accent,
3792                                _ => Color::Ignored,
3793                            })
3794                            .tooltip(move |window, cx| match feedback {
3795                                Some(ThreadFeedback::Positive) => {
3796                                    Tooltip::text("Thanks for your feedback!")(window, cx)
3797                                }
3798                                _ => {
3799                                    Tooltip::with_meta("Helpful Response", None, tooltip_meta(), cx)
3800                                }
3801                            })
3802                            .on_click(cx.listener(move |this, _, window, cx| {
3803                                this.handle_feedback_click(ThreadFeedback::Positive, window, cx);
3804                            })),
3805                    )
3806                    .child(
3807                        IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
3808                            .shape(ui::IconButtonShape::Square)
3809                            .icon_size(IconSize::Small)
3810                            .icon_color(match feedback {
3811                                Some(ThreadFeedback::Negative) => Color::Accent,
3812                                _ => Color::Ignored,
3813                            })
3814                            .tooltip(move |window, cx| match feedback {
3815                                Some(ThreadFeedback::Negative) => {
3816                                    Tooltip::text(
3817                                    "We appreciate your feedback and will use it to improve in the future.",
3818                                )(window, cx)
3819                                }
3820                                _ => {
3821                                    Tooltip::with_meta(
3822                                        "Not Helpful Response",
3823                                        None,
3824                                        tooltip_meta(),
3825                                        cx,
3826                                    )
3827                                }
3828                            })
3829                            .on_click(cx.listener(move |this, _, window, cx| {
3830                                this.handle_feedback_click(ThreadFeedback::Negative, window, cx);
3831                            })),
3832                    );
3833        }
3834
3835        if let Some(project) = self.project.upgrade()
3836            && let Some(server_view) = self.server_view.upgrade()
3837            && cx.has_flag::<AgentSharingFeatureFlag>()
3838            && project.read(cx).client().status().borrow().is_connected()
3839        {
3840            let button = if self.is_imported_thread(cx) {
3841                IconButton::new("sync-thread", IconName::ArrowCircle)
3842                    .shape(ui::IconButtonShape::Square)
3843                    .icon_size(IconSize::Small)
3844                    .icon_color(Color::Ignored)
3845                    .tooltip(Tooltip::text("Sync with source thread"))
3846                    .on_click(cx.listener(move |this, _, window, cx| {
3847                        this.sync_thread(project.clone(), server_view.clone(), window, cx);
3848                    }))
3849            } else {
3850                IconButton::new("share-thread", IconName::ArrowUpRight)
3851                    .shape(ui::IconButtonShape::Square)
3852                    .icon_size(IconSize::Small)
3853                    .icon_color(Color::Ignored)
3854                    .tooltip(Tooltip::text("Share Thread"))
3855                    .on_click(cx.listener(move |this, _, window, cx| {
3856                        this.share_thread(window, cx);
3857                    }))
3858            };
3859
3860            container = container.child(button);
3861        }
3862
3863        container
3864            .child(open_as_markdown)
3865            .child(scroll_to_recent_user_prompt)
3866            .child(scroll_to_top)
3867            .into_any_element()
3868    }
3869
3870    pub(crate) fn scroll_to_most_recent_user_prompt(&mut self, cx: &mut Context<Self>) {
3871        let entries = self.thread.read(cx).entries();
3872        if entries.is_empty() {
3873            return;
3874        }
3875
3876        // Find the most recent user message and scroll it to the top of the viewport.
3877        // (Fallback: if no user message exists, scroll to the bottom.)
3878        if let Some(ix) = entries
3879            .iter()
3880            .rposition(|entry| matches!(entry, AgentThreadEntry::UserMessage(_)))
3881        {
3882            self.list_state.scroll_to(ListOffset {
3883                item_ix: ix,
3884                offset_in_item: px(0.0),
3885            });
3886            cx.notify();
3887        } else {
3888            self.scroll_to_bottom(cx);
3889        }
3890    }
3891
3892    pub fn scroll_to_bottom(&mut self, cx: &mut Context<Self>) {
3893        let entry_count = self.thread.read(cx).entries().len();
3894        self.list_state.reset(entry_count);
3895        cx.notify();
3896    }
3897
3898    fn handle_feedback_click(
3899        &mut self,
3900        feedback: ThreadFeedback,
3901        window: &mut Window,
3902        cx: &mut Context<Self>,
3903    ) {
3904        self.thread_feedback
3905            .submit(self.thread.clone(), feedback, window, cx);
3906        cx.notify();
3907    }
3908
3909    fn submit_feedback_message(&mut self, cx: &mut Context<Self>) {
3910        let thread = self.thread.clone();
3911        self.thread_feedback.submit_comments(thread, cx);
3912        cx.notify();
3913    }
3914
3915    pub(crate) fn scroll_to_top(&mut self, cx: &mut Context<Self>) {
3916        self.list_state.scroll_to(ListOffset::default());
3917        cx.notify();
3918    }
3919
3920    pub fn open_thread_as_markdown(
3921        &self,
3922        workspace: Entity<Workspace>,
3923        window: &mut Window,
3924        cx: &mut App,
3925    ) -> Task<Result<()>> {
3926        let markdown_language_task = workspace
3927            .read(cx)
3928            .app_state()
3929            .languages
3930            .language_for_name("Markdown");
3931
3932        let thread = self.thread.read(cx);
3933        let thread_title = thread.title().to_string();
3934        let markdown = thread.to_markdown(cx);
3935
3936        let project = workspace.read(cx).project().clone();
3937        window.spawn(cx, async move |cx| {
3938            let markdown_language = markdown_language_task.await?;
3939
3940            let buffer = project
3941                .update(cx, |project, cx| {
3942                    project.create_buffer(Some(markdown_language), false, cx)
3943                })
3944                .await?;
3945
3946            buffer.update(cx, |buffer, cx| {
3947                buffer.set_text(markdown, cx);
3948                buffer.set_capability(language::Capability::ReadWrite, cx);
3949            });
3950
3951            workspace.update_in(cx, |workspace, window, cx| {
3952                let buffer = cx
3953                    .new(|cx| MultiBuffer::singleton(buffer, cx).with_title(thread_title.clone()));
3954
3955                workspace.add_item_to_active_pane(
3956                    Box::new(cx.new(|cx| {
3957                        let mut editor =
3958                            Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
3959                        editor.set_breadcrumb_header(thread_title);
3960                        editor
3961                    })),
3962                    None,
3963                    true,
3964                    window,
3965                    cx,
3966                );
3967            })?;
3968            anyhow::Ok(())
3969        })
3970    }
3971
3972    fn render_generating(&self, confirmation: bool, cx: &App) -> impl IntoElement {
3973        let show_stats = AgentSettings::get_global(cx).show_turn_stats;
3974        let elapsed_label = show_stats
3975            .then(|| {
3976                self.turn_fields.turn_started_at.and_then(|started_at| {
3977                    let elapsed = started_at.elapsed();
3978                    (elapsed > STOPWATCH_THRESHOLD).then(|| duration_alt_display(elapsed))
3979                })
3980            })
3981            .flatten();
3982
3983        let is_waiting = confirmation || self.thread.read(cx).has_in_progress_tool_calls();
3984
3985        let turn_tokens_label = elapsed_label
3986            .is_some()
3987            .then(|| {
3988                self.turn_fields
3989                    .turn_tokens
3990                    .filter(|&tokens| tokens > TOKEN_THRESHOLD)
3991                    .map(|tokens| crate::text_thread_editor::humanize_token_count(tokens))
3992            })
3993            .flatten();
3994
3995        let arrow_icon = if is_waiting {
3996            IconName::ArrowUp
3997        } else {
3998            IconName::ArrowDown
3999        };
4000
4001        h_flex()
4002            .id("generating-spinner")
4003            .py_2()
4004            .px(rems_from_px(22.))
4005            .gap_2()
4006            .map(|this| {
4007                if confirmation {
4008                    this.child(
4009                        h_flex()
4010                            .w_2()
4011                            .child(SpinnerLabel::sand().size(LabelSize::Small)),
4012                    )
4013                    .child(
4014                        div().min_w(rems(8.)).child(
4015                            LoadingLabel::new("Awaiting Confirmation")
4016                                .size(LabelSize::Small)
4017                                .color(Color::Muted),
4018                        ),
4019                    )
4020                } else {
4021                    this.child(SpinnerLabel::new().size(LabelSize::Small))
4022                }
4023            })
4024            .when_some(elapsed_label, |this, elapsed| {
4025                this.child(
4026                    Label::new(elapsed)
4027                        .size(LabelSize::Small)
4028                        .color(Color::Muted),
4029                )
4030            })
4031            .when_some(turn_tokens_label, |this, tokens| {
4032                this.child(
4033                    h_flex()
4034                        .gap_0p5()
4035                        .child(
4036                            Icon::new(arrow_icon)
4037                                .size(IconSize::XSmall)
4038                                .color(Color::Muted),
4039                        )
4040                        .child(
4041                            Label::new(format!("{} tokens", tokens))
4042                                .size(LabelSize::Small)
4043                                .color(Color::Muted),
4044                        ),
4045                )
4046            })
4047            .into_any_element()
4048    }
4049
4050    fn render_thinking_block(
4051        &self,
4052        entry_ix: usize,
4053        chunk_ix: usize,
4054        chunk: Entity<Markdown>,
4055        window: &Window,
4056        cx: &Context<Self>,
4057    ) -> AnyElement {
4058        let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix));
4059        let card_header_id = SharedString::from("inner-card-header");
4060
4061        let key = (entry_ix, chunk_ix);
4062
4063        let is_open = self.expanded_thinking_blocks.contains(&key);
4064
4065        let scroll_handle = self
4066            .entry_view_state
4067            .read(cx)
4068            .entry(entry_ix)
4069            .and_then(|entry| entry.scroll_handle_for_assistant_message_chunk(chunk_ix));
4070
4071        let thinking_content = {
4072            div()
4073                .id(("thinking-content", chunk_ix))
4074                .when_some(scroll_handle, |this, scroll_handle| {
4075                    this.track_scroll(&scroll_handle)
4076                })
4077                .text_ui_sm(cx)
4078                .overflow_hidden()
4079                .child(self.render_markdown(
4080                    chunk,
4081                    MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
4082                ))
4083        };
4084
4085        v_flex()
4086            .gap_1()
4087            .child(
4088                h_flex()
4089                    .id(header_id)
4090                    .group(&card_header_id)
4091                    .relative()
4092                    .w_full()
4093                    .pr_1()
4094                    .justify_between()
4095                    .child(
4096                        h_flex()
4097                            .h(window.line_height() - px(2.))
4098                            .gap_1p5()
4099                            .overflow_hidden()
4100                            .child(
4101                                Icon::new(IconName::ToolThink)
4102                                    .size(IconSize::Small)
4103                                    .color(Color::Muted),
4104                            )
4105                            .child(
4106                                div()
4107                                    .text_size(self.tool_name_font_size())
4108                                    .text_color(cx.theme().colors().text_muted)
4109                                    .child("Thinking"),
4110                            ),
4111                    )
4112                    .child(
4113                        Disclosure::new(("expand", entry_ix), is_open)
4114                            .opened_icon(IconName::ChevronUp)
4115                            .closed_icon(IconName::ChevronDown)
4116                            .visible_on_hover(&card_header_id)
4117                            .on_click(cx.listener({
4118                                move |this, _event, _window, cx| {
4119                                    if is_open {
4120                                        this.expanded_thinking_blocks.remove(&key);
4121                                    } else {
4122                                        this.expanded_thinking_blocks.insert(key);
4123                                    }
4124                                    cx.notify();
4125                                }
4126                            })),
4127                    )
4128                    .on_click(cx.listener(move |this, _event, _window, cx| {
4129                        if is_open {
4130                            this.expanded_thinking_blocks.remove(&key);
4131                        } else {
4132                            this.expanded_thinking_blocks.insert(key);
4133                        }
4134                        cx.notify();
4135                    })),
4136            )
4137            .when(is_open, |this| {
4138                this.child(
4139                    div()
4140                        .ml_1p5()
4141                        .pl_3p5()
4142                        .border_l_1()
4143                        .border_color(self.tool_card_border_color(cx))
4144                        .child(thinking_content),
4145                )
4146            })
4147            .into_any_element()
4148    }
4149
4150    fn render_message_context_menu(
4151        &self,
4152        entry_ix: usize,
4153        message_body: AnyElement,
4154        cx: &Context<Self>,
4155    ) -> AnyElement {
4156        let entity = cx.entity();
4157        let workspace = self.workspace.clone();
4158
4159        right_click_menu(format!("agent_context_menu-{}", entry_ix))
4160            .trigger(move |_, _, _| message_body)
4161            .menu(move |window, cx| {
4162                let focus = window.focused(cx);
4163                let entity = entity.clone();
4164                let workspace = workspace.clone();
4165
4166                ContextMenu::build(window, cx, move |menu, _, cx| {
4167                    let this = entity.read(cx);
4168                    let is_at_top = this.list_state.logical_scroll_top().item_ix == 0;
4169
4170                    let has_selection = this
4171                        .thread
4172                        .read(cx)
4173                        .entries()
4174                        .get(entry_ix)
4175                        .and_then(|entry| match &entry {
4176                            AgentThreadEntry::AssistantMessage(msg) => Some(&msg.chunks),
4177                            _ => None,
4178                        })
4179                        .map(|chunks| {
4180                            chunks.iter().any(|chunk| {
4181                                let md = match chunk {
4182                                    AssistantMessageChunk::Message { block } => block.markdown(),
4183                                    AssistantMessageChunk::Thought { block } => block.markdown(),
4184                                };
4185                                md.map_or(false, |m| m.read(cx).selected_text().is_some())
4186                            })
4187                        })
4188                        .unwrap_or(false);
4189
4190                    let copy_this_agent_response =
4191                        ContextMenuEntry::new("Copy This Agent Response").handler({
4192                            let entity = entity.clone();
4193                            move |_, cx| {
4194                                entity.update(cx, |this, cx| {
4195                                    let entries = this.thread.read(cx).entries();
4196                                    if let Some(text) =
4197                                        Self::get_agent_message_content(entries, entry_ix, cx)
4198                                    {
4199                                        cx.write_to_clipboard(ClipboardItem::new_string(text));
4200                                    }
4201                                });
4202                            }
4203                        });
4204
4205                    let scroll_item = if is_at_top {
4206                        ContextMenuEntry::new("Scroll to Bottom").handler({
4207                            let entity = entity.clone();
4208                            move |_, cx| {
4209                                entity.update(cx, |this, cx| {
4210                                    this.scroll_to_bottom(cx);
4211                                });
4212                            }
4213                        })
4214                    } else {
4215                        ContextMenuEntry::new("Scroll to Top").handler({
4216                            let entity = entity.clone();
4217                            move |_, cx| {
4218                                entity.update(cx, |this, cx| {
4219                                    this.scroll_to_top(cx);
4220                                });
4221                            }
4222                        })
4223                    };
4224
4225                    let open_thread_as_markdown = ContextMenuEntry::new("Open Thread as Markdown")
4226                        .handler({
4227                            let entity = entity.clone();
4228                            let workspace = workspace.clone();
4229                            move |window, cx| {
4230                                if let Some(workspace) = workspace.upgrade() {
4231                                    entity
4232                                        .update(cx, |this, cx| {
4233                                            this.open_thread_as_markdown(workspace, window, cx)
4234                                        })
4235                                        .detach_and_log_err(cx);
4236                                }
4237                            }
4238                        });
4239
4240                    menu.when_some(focus, |menu, focus| menu.context(focus))
4241                        .action_disabled_when(
4242                            !has_selection,
4243                            "Copy Selection",
4244                            Box::new(markdown::CopyAsMarkdown),
4245                        )
4246                        .item(copy_this_agent_response)
4247                        .separator()
4248                        .item(scroll_item)
4249                        .item(open_thread_as_markdown)
4250                })
4251            })
4252            .into_any_element()
4253    }
4254
4255    fn get_agent_message_content(
4256        entries: &[AgentThreadEntry],
4257        entry_index: usize,
4258        cx: &App,
4259    ) -> Option<String> {
4260        let entry = entries.get(entry_index)?;
4261        if matches!(entry, AgentThreadEntry::UserMessage(_)) {
4262            return None;
4263        }
4264
4265        let start_index = (0..entry_index)
4266            .rev()
4267            .find(|&i| matches!(entries.get(i), Some(AgentThreadEntry::UserMessage(_))))
4268            .map(|i| i + 1)
4269            .unwrap_or(0);
4270
4271        let end_index = (entry_index + 1..entries.len())
4272            .find(|&i| matches!(entries.get(i), Some(AgentThreadEntry::UserMessage(_))))
4273            .map(|i| i - 1)
4274            .unwrap_or(entries.len() - 1);
4275
4276        let parts: Vec<String> = (start_index..=end_index)
4277            .filter_map(|i| entries.get(i))
4278            .filter_map(|entry| {
4279                if let AgentThreadEntry::AssistantMessage(message) = entry {
4280                    let text: String = message
4281                        .chunks
4282                        .iter()
4283                        .filter_map(|chunk| match chunk {
4284                            AssistantMessageChunk::Message { block } => {
4285                                let markdown = block.to_markdown(cx);
4286                                if markdown.trim().is_empty() {
4287                                    None
4288                                } else {
4289                                    Some(markdown.to_string())
4290                                }
4291                            }
4292                            AssistantMessageChunk::Thought { .. } => None,
4293                        })
4294                        .collect::<Vec<_>>()
4295                        .join("\n\n");
4296
4297                    if text.is_empty() { None } else { Some(text) }
4298                } else {
4299                    None
4300                }
4301            })
4302            .collect();
4303
4304        let text = parts.join("\n\n");
4305        if text.is_empty() { None } else { Some(text) }
4306    }
4307
4308    fn render_collapsible_command(
4309        &self,
4310        is_preview: bool,
4311        command_source: &str,
4312        tool_call_id: &acp::ToolCallId,
4313        cx: &Context<Self>,
4314    ) -> Div {
4315        let command_group =
4316            SharedString::from(format!("collapsible-command-group-{}", tool_call_id));
4317
4318        v_flex()
4319            .group(command_group.clone())
4320            .bg(self.tool_card_header_bg(cx))
4321            .child(
4322                v_flex()
4323                    .p_1p5()
4324                    .when(is_preview, |this| {
4325                        this.pt_1().child(
4326                            // Wrapping this label on a container with 24px height to avoid
4327                            // layout shift when it changes from being a preview label
4328                            // to the actual path where the command will run in
4329                            h_flex().h_6().child(
4330                                Label::new("Run Command")
4331                                    .buffer_font(cx)
4332                                    .size(LabelSize::XSmall)
4333                                    .color(Color::Muted),
4334                            ),
4335                        )
4336                    })
4337                    .children(command_source.lines().map(|line| {
4338                        let text: SharedString = if line.is_empty() {
4339                            " ".into()
4340                        } else {
4341                            line.to_string().into()
4342                        };
4343
4344                        Label::new(text).buffer_font(cx).size(LabelSize::Small)
4345                    }))
4346                    .child(
4347                        div().absolute().top_1().right_1().child(
4348                            CopyButton::new("copy-command", command_source.to_string())
4349                                .tooltip_label("Copy Command")
4350                                .visible_on_hover(command_group),
4351                        ),
4352                    ),
4353            )
4354    }
4355
4356    fn render_terminal_tool_call(
4357        &self,
4358        entry_ix: usize,
4359        terminal: &Entity<acp_thread::Terminal>,
4360        tool_call: &ToolCall,
4361        window: &Window,
4362        cx: &Context<Self>,
4363    ) -> AnyElement {
4364        let terminal_data = terminal.read(cx);
4365        let working_dir = terminal_data.working_dir();
4366        let command = terminal_data.command();
4367        let started_at = terminal_data.started_at();
4368
4369        let tool_failed = matches!(
4370            &tool_call.status,
4371            ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed
4372        );
4373
4374        let output = terminal_data.output();
4375        let command_finished = output.is_some();
4376        let truncated_output =
4377            output.is_some_and(|output| output.original_content_len > output.content.len());
4378        let output_line_count = output.map(|output| output.content_line_count).unwrap_or(0);
4379
4380        let command_failed = command_finished
4381            && output.is_some_and(|o| o.exit_status.is_some_and(|status| !status.success()));
4382
4383        let time_elapsed = if let Some(output) = output {
4384            output.ended_at.duration_since(started_at)
4385        } else {
4386            started_at.elapsed()
4387        };
4388
4389        let header_id =
4390            SharedString::from(format!("terminal-tool-header-{}", terminal.entity_id()));
4391        let header_group = SharedString::from(format!(
4392            "terminal-tool-header-group-{}",
4393            terminal.entity_id()
4394        ));
4395        let header_bg = cx
4396            .theme()
4397            .colors()
4398            .element_background
4399            .blend(cx.theme().colors().editor_foreground.opacity(0.025));
4400        let border_color = cx.theme().colors().border.opacity(0.6);
4401
4402        let working_dir = working_dir
4403            .as_ref()
4404            .map(|path| path.display().to_string())
4405            .unwrap_or_else(|| "current directory".to_string());
4406
4407        // Since the command's source is wrapped in a markdown code block
4408        // (```\n...\n```), we need to strip that so we're left with only the
4409        // command's content.
4410        let command_source = command.read(cx).source();
4411        let command_content = command_source
4412            .strip_prefix("```\n")
4413            .and_then(|s| s.strip_suffix("\n```"))
4414            .unwrap_or(&command_source);
4415
4416        let command_element =
4417            self.render_collapsible_command(false, command_content, &tool_call.id, cx);
4418
4419        let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
4420
4421        let header = h_flex()
4422            .id(header_id)
4423            .px_1p5()
4424            .pt_1()
4425            .flex_none()
4426            .gap_1()
4427            .justify_between()
4428            .rounded_t_md()
4429            .child(
4430                div()
4431                    .id(("command-target-path", terminal.entity_id()))
4432                    .w_full()
4433                    .max_w_full()
4434                    .overflow_x_scroll()
4435                    .child(
4436                        Label::new(working_dir)
4437                            .buffer_font(cx)
4438                            .size(LabelSize::XSmall)
4439                            .color(Color::Muted),
4440                    ),
4441            )
4442            .when(!command_finished, |header| {
4443                header
4444                    .gap_1p5()
4445                    .child(
4446                        Button::new(
4447                            SharedString::from(format!("stop-terminal-{}", terminal.entity_id())),
4448                            "Stop",
4449                        )
4450                        .icon(IconName::Stop)
4451                        .icon_position(IconPosition::Start)
4452                        .icon_size(IconSize::Small)
4453                        .icon_color(Color::Error)
4454                        .label_size(LabelSize::Small)
4455                        .tooltip(move |_window, cx| {
4456                            Tooltip::with_meta(
4457                                "Stop This Command",
4458                                None,
4459                                "Also possible by placing your cursor inside the terminal and using regular terminal bindings.",
4460                                cx,
4461                            )
4462                        })
4463                        .on_click({
4464                            let terminal = terminal.clone();
4465                            cx.listener(move |this, _event, _window, cx| {
4466                                terminal.update(cx, |terminal, cx| {
4467                                    terminal.stop_by_user(cx);
4468                                });
4469                                if AgentSettings::get_global(cx).cancel_generation_on_terminal_stop {
4470                                    this.cancel_generation(cx);
4471                                }
4472                            })
4473                        }),
4474                    )
4475                    .child(Divider::vertical())
4476                    .child(
4477                        Icon::new(IconName::ArrowCircle)
4478                            .size(IconSize::XSmall)
4479                            .color(Color::Info)
4480                            .with_rotate_animation(2)
4481                    )
4482            })
4483            .when(truncated_output, |header| {
4484                let tooltip = if let Some(output) = output {
4485                    if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
4486                       format!("Output exceeded terminal max lines and was \
4487                            truncated, the model received the first {}.", format_file_size(output.content.len() as u64, true))
4488                    } else {
4489                        format!(
4490                            "Output is {} long, and to avoid unexpected token usage, \
4491                                only {} was sent back to the agent.",
4492                            format_file_size(output.original_content_len as u64, true),
4493                             format_file_size(output.content.len() as u64, true)
4494                        )
4495                    }
4496                } else {
4497                    "Output was truncated".to_string()
4498                };
4499
4500                header.child(
4501                    h_flex()
4502                        .id(("terminal-tool-truncated-label", terminal.entity_id()))
4503                        .gap_1()
4504                        .child(
4505                            Icon::new(IconName::Info)
4506                                .size(IconSize::XSmall)
4507                                .color(Color::Ignored),
4508                        )
4509                        .child(
4510                            Label::new("Truncated")
4511                                .color(Color::Muted)
4512                                .size(LabelSize::XSmall),
4513                        )
4514                        .tooltip(Tooltip::text(tooltip)),
4515                )
4516            })
4517            .when(time_elapsed > Duration::from_secs(10), |header| {
4518                header.child(
4519                    Label::new(format!("({})", duration_alt_display(time_elapsed)))
4520                        .buffer_font(cx)
4521                        .color(Color::Muted)
4522                        .size(LabelSize::XSmall),
4523                )
4524            })
4525            .when(tool_failed || command_failed, |header| {
4526                header.child(
4527                    div()
4528                        .id(("terminal-tool-error-code-indicator", terminal.entity_id()))
4529                        .child(
4530                            Icon::new(IconName::Close)
4531                                .size(IconSize::Small)
4532                                .color(Color::Error),
4533                        )
4534                        .when_some(output.and_then(|o| o.exit_status), |this, status| {
4535                            this.tooltip(Tooltip::text(format!(
4536                                "Exited with code {}",
4537                                status.code().unwrap_or(-1),
4538                            )))
4539                        }),
4540                )
4541            })
4542            .child(
4543                Disclosure::new(
4544                    SharedString::from(format!(
4545                        "terminal-tool-disclosure-{}",
4546                        terminal.entity_id()
4547                    )),
4548                    is_expanded,
4549                )
4550                .opened_icon(IconName::ChevronUp)
4551                .closed_icon(IconName::ChevronDown)
4552                .visible_on_hover(&header_group)
4553                .on_click(cx.listener({
4554                    let id = tool_call.id.clone();
4555                    move |this, _event, _window, cx| {
4556                        if is_expanded {
4557                            this.expanded_tool_calls.remove(&id);
4558                        } else {
4559                            this.expanded_tool_calls.insert(id.clone());
4560                        }
4561                        cx.notify();
4562                    }
4563                })),
4564            );
4565
4566        let terminal_view = self
4567            .entry_view_state
4568            .read(cx)
4569            .entry(entry_ix)
4570            .and_then(|entry| entry.terminal(terminal));
4571
4572        v_flex()
4573            .my_1p5()
4574            .mx_5()
4575            .border_1()
4576            .when(tool_failed || command_failed, |card| card.border_dashed())
4577            .border_color(border_color)
4578            .rounded_md()
4579            .overflow_hidden()
4580            .child(
4581                v_flex()
4582                    .group(&header_group)
4583                    .bg(header_bg)
4584                    .text_xs()
4585                    .child(header)
4586                    .child(command_element),
4587            )
4588            .when(is_expanded && terminal_view.is_some(), |this| {
4589                this.child(
4590                    div()
4591                        .pt_2()
4592                        .border_t_1()
4593                        .when(tool_failed || command_failed, |card| card.border_dashed())
4594                        .border_color(border_color)
4595                        .bg(cx.theme().colors().editor_background)
4596                        .rounded_b_md()
4597                        .text_ui_sm(cx)
4598                        .h_full()
4599                        .children(terminal_view.map(|terminal_view| {
4600                            let element = if terminal_view
4601                                .read(cx)
4602                                .content_mode(window, cx)
4603                                .is_scrollable()
4604                            {
4605                                div().h_72().child(terminal_view).into_any_element()
4606                            } else {
4607                                terminal_view.into_any_element()
4608                            };
4609
4610                            div()
4611                                .on_action(cx.listener(|_this, _: &NewTerminal, window, cx| {
4612                                    window.dispatch_action(NewThread.boxed_clone(), cx);
4613                                    cx.stop_propagation();
4614                                }))
4615                                .child(element)
4616                                .into_any_element()
4617                        })),
4618                )
4619            })
4620            .into_any()
4621    }
4622
4623    fn render_tool_call(
4624        &self,
4625        entry_ix: usize,
4626        tool_call: &ToolCall,
4627        window: &Window,
4628        cx: &Context<Self>,
4629    ) -> Div {
4630        let has_location = tool_call.locations.len() == 1;
4631        let card_header_id = SharedString::from("inner-tool-call-header");
4632
4633        let failed_or_canceled = match &tool_call.status {
4634            ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => true,
4635            _ => false,
4636        };
4637
4638        let needs_confirmation = matches!(
4639            tool_call.status,
4640            ToolCallStatus::WaitingForConfirmation { .. }
4641        );
4642        let is_terminal_tool = matches!(tool_call.kind, acp::ToolKind::Execute);
4643
4644        let is_edit =
4645            matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some();
4646
4647        // For subagent tool calls, render the subagent cards directly without wrapper
4648        if tool_call.is_subagent() {
4649            return self.render_subagent_tool_call(
4650                entry_ix,
4651                tool_call,
4652                tool_call.subagent_session_id.clone(),
4653                window,
4654                cx,
4655            );
4656        }
4657
4658        let is_cancelled_edit = is_edit && matches!(tool_call.status, ToolCallStatus::Canceled);
4659        let has_revealed_diff = tool_call.diffs().next().is_some_and(|diff| {
4660            self.entry_view_state
4661                .read(cx)
4662                .entry(entry_ix)
4663                .and_then(|entry| entry.editor_for_diff(diff))
4664                .is_some()
4665                && diff.read(cx).has_revealed_range(cx)
4666        });
4667
4668        let use_card_layout = needs_confirmation || is_edit || is_terminal_tool;
4669
4670        let has_image_content = tool_call.content.iter().any(|c| c.image().is_some());
4671        let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
4672        let mut is_open = self.expanded_tool_calls.contains(&tool_call.id);
4673
4674        is_open |= needs_confirmation;
4675
4676        let should_show_raw_input = !is_terminal_tool && !is_edit && !has_image_content;
4677
4678        let input_output_header = |label: SharedString| {
4679            Label::new(label)
4680                .size(LabelSize::XSmall)
4681                .color(Color::Muted)
4682                .buffer_font(cx)
4683        };
4684
4685        let tool_output_display = if is_open {
4686            match &tool_call.status {
4687                ToolCallStatus::WaitingForConfirmation { options, .. } => v_flex()
4688                    .w_full()
4689                    .children(
4690                        tool_call
4691                            .content
4692                            .iter()
4693                            .enumerate()
4694                            .map(|(content_ix, content)| {
4695                                div()
4696                                    .child(self.render_tool_call_content(
4697                                        entry_ix,
4698                                        content,
4699                                        content_ix,
4700                                        tool_call,
4701                                        use_card_layout,
4702                                        has_image_content,
4703                                        failed_or_canceled,
4704                                        window,
4705                                        cx,
4706                                    ))
4707                                    .into_any_element()
4708                            }),
4709                    )
4710                    .when(should_show_raw_input, |this| {
4711                        let is_raw_input_expanded =
4712                            self.expanded_tool_call_raw_inputs.contains(&tool_call.id);
4713
4714                        let input_header = if is_raw_input_expanded {
4715                            "Raw Input:"
4716                        } else {
4717                            "View Raw Input"
4718                        };
4719
4720                        this.child(
4721                            v_flex()
4722                                .p_2()
4723                                .gap_1()
4724                                .border_t_1()
4725                                .border_color(self.tool_card_border_color(cx))
4726                                .child(
4727                                    h_flex()
4728                                        .id("disclosure_container")
4729                                        .pl_0p5()
4730                                        .gap_1()
4731                                        .justify_between()
4732                                        .rounded_xs()
4733                                        .hover(|s| s.bg(cx.theme().colors().element_hover))
4734                                        .child(input_output_header(input_header.into()))
4735                                        .child(
4736                                            Disclosure::new(
4737                                                ("raw-input-disclosure", entry_ix),
4738                                                is_raw_input_expanded,
4739                                            )
4740                                            .opened_icon(IconName::ChevronUp)
4741                                            .closed_icon(IconName::ChevronDown),
4742                                        )
4743                                        .on_click(cx.listener({
4744                                            let id = tool_call.id.clone();
4745
4746                                            move |this: &mut Self, _, _, cx| {
4747                                                if this.expanded_tool_call_raw_inputs.contains(&id)
4748                                                {
4749                                                    this.expanded_tool_call_raw_inputs.remove(&id);
4750                                                } else {
4751                                                    this.expanded_tool_call_raw_inputs
4752                                                        .insert(id.clone());
4753                                                }
4754                                                cx.notify();
4755                                            }
4756                                        })),
4757                                )
4758                                .when(is_raw_input_expanded, |this| {
4759                                    this.children(tool_call.raw_input_markdown.clone().map(
4760                                        |input| {
4761                                            self.render_markdown(
4762                                                input,
4763                                                MarkdownStyle::themed(
4764                                                    MarkdownFont::Agent,
4765                                                    window,
4766                                                    cx,
4767                                                ),
4768                                            )
4769                                        },
4770                                    ))
4771                                }),
4772                        )
4773                    })
4774                    .child(self.render_permission_buttons(
4775                        options,
4776                        entry_ix,
4777                        tool_call.id.clone(),
4778                        cx,
4779                    ))
4780                    .into_any(),
4781                ToolCallStatus::Pending | ToolCallStatus::InProgress
4782                    if is_edit
4783                        && tool_call.content.is_empty()
4784                        && self.as_native_connection(cx).is_some() =>
4785                {
4786                    self.render_diff_loading(cx)
4787                }
4788                ToolCallStatus::Pending
4789                | ToolCallStatus::InProgress
4790                | ToolCallStatus::Completed
4791                | ToolCallStatus::Failed
4792                | ToolCallStatus::Canceled => v_flex()
4793                    .when(should_show_raw_input, |this| {
4794                        this.mt_1p5().w_full().child(
4795                            v_flex()
4796                                .ml(rems(0.4))
4797                                .px_3p5()
4798                                .pb_1()
4799                                .gap_1()
4800                                .border_l_1()
4801                                .border_color(self.tool_card_border_color(cx))
4802                                .child(input_output_header("Raw Input:".into()))
4803                                .children(tool_call.raw_input_markdown.clone().map(|input| {
4804                                    div().id(("tool-call-raw-input-markdown", entry_ix)).child(
4805                                        self.render_markdown(
4806                                            input,
4807                                            MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
4808                                        ),
4809                                    )
4810                                }))
4811                                .child(input_output_header("Output:".into())),
4812                        )
4813                    })
4814                    .children(
4815                        tool_call
4816                            .content
4817                            .iter()
4818                            .enumerate()
4819                            .map(|(content_ix, content)| {
4820                                div().id(("tool-call-output", entry_ix)).child(
4821                                    self.render_tool_call_content(
4822                                        entry_ix,
4823                                        content,
4824                                        content_ix,
4825                                        tool_call,
4826                                        use_card_layout,
4827                                        has_image_content,
4828                                        failed_or_canceled,
4829                                        window,
4830                                        cx,
4831                                    ),
4832                                )
4833                            }),
4834                    )
4835                    .into_any(),
4836                ToolCallStatus::Rejected => Empty.into_any(),
4837            }
4838            .into()
4839        } else {
4840            None
4841        };
4842
4843        v_flex()
4844            .map(|this| {
4845                if use_card_layout {
4846                    this.my_1p5()
4847                        .rounded_md()
4848                        .border_1()
4849                        .when(failed_or_canceled, |this| this.border_dashed())
4850                        .border_color(self.tool_card_border_color(cx))
4851                        .bg(cx.theme().colors().editor_background)
4852                        .overflow_hidden()
4853                } else {
4854                    this.my_1()
4855                }
4856            })
4857            .map(|this| {
4858                if has_location && !use_card_layout {
4859                    this.ml_4()
4860                } else {
4861                    this.ml_5()
4862                }
4863            })
4864            .mr_5()
4865            .map(|this| {
4866                if is_terminal_tool {
4867                    let label_source = tool_call.label.read(cx).source();
4868                    this.child(self.render_collapsible_command(true, label_source, &tool_call.id, cx))
4869                } else {
4870                    this.child(
4871                        h_flex()
4872                            .group(&card_header_id)
4873                            .relative()
4874                            .w_full()
4875                            .gap_1()
4876                            .justify_between()
4877                            .when(use_card_layout, |this| {
4878                                this.p_0p5()
4879                                    .rounded_t(rems_from_px(5.))
4880                                    .bg(self.tool_card_header_bg(cx))
4881                            })
4882                            .child(self.render_tool_call_label(
4883                                entry_ix,
4884                                tool_call,
4885                                is_edit,
4886                                is_cancelled_edit,
4887                                has_revealed_diff,
4888                                use_card_layout,
4889                                window,
4890                                cx,
4891                            ))
4892                            .when(is_collapsible || failed_or_canceled, |this| {
4893                                let diff_for_discard =
4894                                    if has_revealed_diff && is_cancelled_edit && cx.has_flag::<AgentV2FeatureFlag>() {
4895                                        tool_call.diffs().next().cloned()
4896                                    } else {
4897                                        None
4898                                    };
4899                                this.child(
4900                                    h_flex()
4901                                        .px_1()
4902                                        .when_some(diff_for_discard.clone(), |this, _| this.pr_0p5())
4903                                        .gap_1()
4904                                        .when(is_collapsible, |this| {
4905                                            this.child(
4906                                            Disclosure::new(("expand-output", entry_ix), is_open)
4907                                                .opened_icon(IconName::ChevronUp)
4908                                                .closed_icon(IconName::ChevronDown)
4909                                                .visible_on_hover(&card_header_id)
4910                                                .on_click(cx.listener({
4911                                                    let id = tool_call.id.clone();
4912                                                    move |this: &mut Self, _, _, cx: &mut Context<Self>| {
4913                                                                if is_open {
4914                                                                    this
4915                                                                        .expanded_tool_calls.remove(&id);
4916                                                                } else {
4917                                                                    this.expanded_tool_calls.insert(id.clone());
4918                                                                }
4919                                                            cx.notify();
4920                                                    }
4921                                                })),
4922                                        )
4923                                        })
4924                                        .when(failed_or_canceled, |this| {
4925                                            if is_cancelled_edit && !has_revealed_diff {
4926                                                this.child(
4927                                                    div()
4928                                                        .id(entry_ix)
4929                                                        .tooltip(Tooltip::text(
4930                                                            "Interrupted Edit",
4931                                                        ))
4932                                                        .child(
4933                                                            Icon::new(IconName::XCircle)
4934                                                                .color(Color::Muted)
4935                                                                .size(IconSize::Small),
4936                                                        ),
4937                                                )
4938                                            } else if is_cancelled_edit {
4939                                                this
4940                                            } else {
4941                                                this.child(
4942                                                    Icon::new(IconName::Close)
4943                                                        .color(Color::Error)
4944                                                        .size(IconSize::Small),
4945                                                )
4946                                            }
4947                                        })
4948                                        .when_some(diff_for_discard, |this, diff| {
4949                                            let tool_call_id = tool_call.id.clone();
4950                                            let is_discarded = self.discarded_partial_edits.contains(&tool_call_id);
4951                                            this.when(!is_discarded, |this| {
4952                                                this.child(
4953                                                    IconButton::new(
4954                                                        ("discard-partial-edit", entry_ix),
4955                                                        IconName::Undo,
4956                                                    )
4957                                                    .icon_size(IconSize::Small)
4958                                                    .tooltip(move |_, cx| Tooltip::with_meta(
4959                                                        "Discard Interrupted Edit",
4960                                                        None,
4961                                                        "You can discard this interrupted partial edit and restore the original file content.",
4962                                                        cx
4963                                                    ))
4964                                                    .on_click(cx.listener({
4965                                                        let tool_call_id = tool_call_id.clone();
4966                                                        move |this, _, _window, cx| {
4967                                                            let diff_data = diff.read(cx);
4968                                                            let base_text = diff_data.base_text().clone();
4969                                                            let buffer = diff_data.buffer().clone();
4970                                                            buffer.update(cx, |buffer, cx| {
4971                                                                buffer.set_text(base_text.as_ref(), cx);
4972                                                            });
4973                                                            this.discarded_partial_edits.insert(tool_call_id.clone());
4974                                                            cx.notify();
4975                                                        }
4976                                                    })),
4977                                                )
4978                                            })
4979                                        })
4980
4981                                )
4982                            }),
4983                    )
4984                }
4985            })
4986            .children(tool_output_display)
4987    }
4988
4989    fn render_permission_buttons(
4990        &self,
4991        options: &PermissionOptions,
4992        entry_ix: usize,
4993        tool_call_id: acp::ToolCallId,
4994        cx: &Context<Self>,
4995    ) -> Div {
4996        match options {
4997            PermissionOptions::Flat(options) => {
4998                self.render_permission_buttons_flat(options, entry_ix, tool_call_id, cx)
4999            }
5000            PermissionOptions::Dropdown(options) => {
5001                self.render_permission_buttons_dropdown(options, entry_ix, tool_call_id, cx)
5002            }
5003        }
5004    }
5005
5006    fn render_permission_buttons_dropdown(
5007        &self,
5008        choices: &[PermissionOptionChoice],
5009        entry_ix: usize,
5010        tool_call_id: acp::ToolCallId,
5011        cx: &Context<Self>,
5012    ) -> Div {
5013        let is_first = self
5014            .thread
5015            .read(cx)
5016            .first_tool_awaiting_confirmation()
5017            .is_some_and(|call| call.id == tool_call_id);
5018
5019        // Get the selected granularity index, defaulting to the last option ("Only this time")
5020        let selected_index = self
5021            .selected_permission_granularity
5022            .get(&tool_call_id)
5023            .copied()
5024            .unwrap_or_else(|| choices.len().saturating_sub(1));
5025
5026        let selected_choice = choices.get(selected_index).or(choices.last());
5027
5028        let dropdown_label: SharedString = selected_choice
5029            .map(|choice| choice.label())
5030            .unwrap_or_else(|| "Only this time".into());
5031
5032        let (allow_option_id, allow_option_kind, deny_option_id, deny_option_kind) =
5033            if let Some(choice) = selected_choice {
5034                (
5035                    choice.allow.option_id.clone(),
5036                    choice.allow.kind,
5037                    choice.deny.option_id.clone(),
5038                    choice.deny.kind,
5039                )
5040            } else {
5041                (
5042                    acp::PermissionOptionId::new("allow"),
5043                    acp::PermissionOptionKind::AllowOnce,
5044                    acp::PermissionOptionId::new("deny"),
5045                    acp::PermissionOptionKind::RejectOnce,
5046                )
5047            };
5048
5049        h_flex()
5050            .w_full()
5051            .p_1()
5052            .gap_2()
5053            .justify_between()
5054            .border_t_1()
5055            .border_color(self.tool_card_border_color(cx))
5056            .child(
5057                h_flex()
5058                    .gap_0p5()
5059                    .child(
5060                        Button::new(("allow-btn", entry_ix), "Allow")
5061                            .icon(IconName::Check)
5062                            .icon_color(Color::Success)
5063                            .icon_position(IconPosition::Start)
5064                            .icon_size(IconSize::XSmall)
5065                            .label_size(LabelSize::Small)
5066                            .when(is_first, |this| {
5067                                this.key_binding(
5068                                    KeyBinding::for_action_in(
5069                                        &AllowOnce as &dyn Action,
5070                                        &self.focus_handle(cx),
5071                                        cx,
5072                                    )
5073                                    .map(|kb| kb.size(rems_from_px(10.))),
5074                                )
5075                            })
5076                            .on_click(cx.listener({
5077                                let tool_call_id = tool_call_id.clone();
5078                                let option_id = allow_option_id;
5079                                let option_kind = allow_option_kind;
5080                                move |this, _, window, cx| {
5081                                    this.authorize_tool_call(
5082                                        tool_call_id.clone(),
5083                                        option_id.clone(),
5084                                        option_kind,
5085                                        window,
5086                                        cx,
5087                                    );
5088                                }
5089                            })),
5090                    )
5091                    .child(
5092                        Button::new(("deny-btn", entry_ix), "Deny")
5093                            .icon(IconName::Close)
5094                            .icon_color(Color::Error)
5095                            .icon_position(IconPosition::Start)
5096                            .icon_size(IconSize::XSmall)
5097                            .label_size(LabelSize::Small)
5098                            .when(is_first, |this| {
5099                                this.key_binding(
5100                                    KeyBinding::for_action_in(
5101                                        &RejectOnce as &dyn Action,
5102                                        &self.focus_handle(cx),
5103                                        cx,
5104                                    )
5105                                    .map(|kb| kb.size(rems_from_px(10.))),
5106                                )
5107                            })
5108                            .on_click(cx.listener({
5109                                let tool_call_id = tool_call_id.clone();
5110                                let option_id = deny_option_id;
5111                                let option_kind = deny_option_kind;
5112                                move |this, _, window, cx| {
5113                                    this.authorize_tool_call(
5114                                        tool_call_id.clone(),
5115                                        option_id.clone(),
5116                                        option_kind,
5117                                        window,
5118                                        cx,
5119                                    );
5120                                }
5121                            })),
5122                    ),
5123            )
5124            .child(self.render_permission_granularity_dropdown(
5125                choices,
5126                dropdown_label,
5127                entry_ix,
5128                tool_call_id,
5129                selected_index,
5130                is_first,
5131                cx,
5132            ))
5133    }
5134
5135    fn render_permission_granularity_dropdown(
5136        &self,
5137        choices: &[PermissionOptionChoice],
5138        current_label: SharedString,
5139        entry_ix: usize,
5140        tool_call_id: acp::ToolCallId,
5141        selected_index: usize,
5142        is_first: bool,
5143        cx: &Context<Self>,
5144    ) -> AnyElement {
5145        let menu_options: Vec<(usize, SharedString)> = choices
5146            .iter()
5147            .enumerate()
5148            .map(|(i, choice)| (i, choice.label()))
5149            .collect();
5150
5151        let permission_dropdown_handle = self.permission_dropdown_handle.clone();
5152
5153        PopoverMenu::new(("permission-granularity", entry_ix))
5154            .with_handle(permission_dropdown_handle)
5155            .trigger(
5156                Button::new(("granularity-trigger", entry_ix), current_label)
5157                    .icon(IconName::ChevronDown)
5158                    .icon_size(IconSize::XSmall)
5159                    .icon_color(Color::Muted)
5160                    .label_size(LabelSize::Small)
5161                    .when(is_first, |this| {
5162                        this.key_binding(
5163                            KeyBinding::for_action_in(
5164                                &crate::OpenPermissionDropdown as &dyn Action,
5165                                &self.focus_handle(cx),
5166                                cx,
5167                            )
5168                            .map(|kb| kb.size(rems_from_px(10.))),
5169                        )
5170                    }),
5171            )
5172            .menu(move |window, cx| {
5173                let tool_call_id = tool_call_id.clone();
5174                let options = menu_options.clone();
5175
5176                Some(ContextMenu::build(window, cx, move |mut menu, _, _| {
5177                    for (index, display_name) in options.iter() {
5178                        let display_name = display_name.clone();
5179                        let index = *index;
5180                        let tool_call_id_for_entry = tool_call_id.clone();
5181                        let is_selected = index == selected_index;
5182
5183                        menu = menu.toggleable_entry(
5184                            display_name,
5185                            is_selected,
5186                            IconPosition::End,
5187                            None,
5188                            move |window, cx| {
5189                                window.dispatch_action(
5190                                    SelectPermissionGranularity {
5191                                        tool_call_id: tool_call_id_for_entry.0.to_string(),
5192                                        index,
5193                                    }
5194                                    .boxed_clone(),
5195                                    cx,
5196                                );
5197                            },
5198                        );
5199                    }
5200
5201                    menu
5202                }))
5203            })
5204            .into_any_element()
5205    }
5206
5207    fn render_permission_buttons_flat(
5208        &self,
5209        options: &[acp::PermissionOption],
5210        entry_ix: usize,
5211        tool_call_id: acp::ToolCallId,
5212        cx: &Context<Self>,
5213    ) -> Div {
5214        let is_first = self
5215            .thread
5216            .read(cx)
5217            .first_tool_awaiting_confirmation()
5218            .is_some_and(|call| call.id == tool_call_id);
5219        let mut seen_kinds: ArrayVec<acp::PermissionOptionKind, 3> = ArrayVec::new();
5220
5221        div()
5222            .p_1()
5223            .border_t_1()
5224            .border_color(self.tool_card_border_color(cx))
5225            .w_full()
5226            .v_flex()
5227            .gap_0p5()
5228            .children(options.iter().map(move |option| {
5229                let option_id = SharedString::from(option.option_id.0.clone());
5230                Button::new((option_id, entry_ix), option.name.clone())
5231                    .map(|this| {
5232                        let (this, action) = match option.kind {
5233                            acp::PermissionOptionKind::AllowOnce => (
5234                                this.icon(IconName::Check).icon_color(Color::Success),
5235                                Some(&AllowOnce as &dyn Action),
5236                            ),
5237                            acp::PermissionOptionKind::AllowAlways => (
5238                                this.icon(IconName::CheckDouble).icon_color(Color::Success),
5239                                Some(&AllowAlways as &dyn Action),
5240                            ),
5241                            acp::PermissionOptionKind::RejectOnce => (
5242                                this.icon(IconName::Close).icon_color(Color::Error),
5243                                Some(&RejectOnce as &dyn Action),
5244                            ),
5245                            acp::PermissionOptionKind::RejectAlways | _ => {
5246                                (this.icon(IconName::Close).icon_color(Color::Error), None)
5247                            }
5248                        };
5249
5250                        let Some(action) = action else {
5251                            return this;
5252                        };
5253
5254                        if !is_first || seen_kinds.contains(&option.kind) {
5255                            return this;
5256                        }
5257
5258                        seen_kinds.push(option.kind);
5259
5260                        this.key_binding(
5261                            KeyBinding::for_action_in(action, &self.focus_handle(cx), cx)
5262                                .map(|kb| kb.size(rems_from_px(10.))),
5263                        )
5264                    })
5265                    .icon_position(IconPosition::Start)
5266                    .icon_size(IconSize::XSmall)
5267                    .label_size(LabelSize::Small)
5268                    .on_click(cx.listener({
5269                        let tool_call_id = tool_call_id.clone();
5270                        let option_id = option.option_id.clone();
5271                        let option_kind = option.kind;
5272                        move |this, _, window, cx| {
5273                            this.authorize_tool_call(
5274                                tool_call_id.clone(),
5275                                option_id.clone(),
5276                                option_kind,
5277                                window,
5278                                cx,
5279                            );
5280                        }
5281                    }))
5282            }))
5283    }
5284
5285    fn render_diff_loading(&self, cx: &Context<Self>) -> AnyElement {
5286        let bar = |n: u64, width_class: &str| {
5287            let bg_color = cx.theme().colors().element_active;
5288            let base = h_flex().h_1().rounded_full();
5289
5290            let modified = match width_class {
5291                "w_4_5" => base.w_3_4(),
5292                "w_1_4" => base.w_1_4(),
5293                "w_2_4" => base.w_2_4(),
5294                "w_3_5" => base.w_3_5(),
5295                "w_2_5" => base.w_2_5(),
5296                _ => base.w_1_2(),
5297            };
5298
5299            modified.with_animation(
5300                ElementId::Integer(n),
5301                Animation::new(Duration::from_secs(2)).repeat(),
5302                move |tab, delta| {
5303                    let delta = (delta - 0.15 * n as f32) / 0.7;
5304                    let delta = 1.0 - (0.5 - delta).abs() * 2.;
5305                    let delta = ease_in_out(delta.clamp(0., 1.));
5306                    let delta = 0.1 + 0.9 * delta;
5307
5308                    tab.bg(bg_color.opacity(delta))
5309                },
5310            )
5311        };
5312
5313        v_flex()
5314            .p_3()
5315            .gap_1()
5316            .rounded_b_md()
5317            .bg(cx.theme().colors().editor_background)
5318            .child(bar(0, "w_4_5"))
5319            .child(bar(1, "w_1_4"))
5320            .child(bar(2, "w_2_4"))
5321            .child(bar(3, "w_3_5"))
5322            .child(bar(4, "w_2_5"))
5323            .into_any_element()
5324    }
5325
5326    fn render_tool_call_label(
5327        &self,
5328        entry_ix: usize,
5329        tool_call: &ToolCall,
5330        is_edit: bool,
5331        has_failed: bool,
5332        has_revealed_diff: bool,
5333        use_card_layout: bool,
5334        window: &Window,
5335        cx: &Context<Self>,
5336    ) -> Div {
5337        let has_location = tool_call.locations.len() == 1;
5338        let is_file = tool_call.kind == acp::ToolKind::Edit && has_location;
5339        let is_subagent_tool_call = tool_call.is_subagent();
5340
5341        let file_icon = if has_location {
5342            FileIcons::get_icon(&tool_call.locations[0].path, cx)
5343                .map(Icon::from_path)
5344                .unwrap_or(Icon::new(IconName::ToolPencil))
5345        } else {
5346            Icon::new(IconName::ToolPencil)
5347        };
5348
5349        let tool_icon = if is_file && has_failed && has_revealed_diff {
5350            div()
5351                .id(entry_ix)
5352                .tooltip(Tooltip::text("Interrupted Edit"))
5353                .child(DecoratedIcon::new(
5354                    file_icon,
5355                    Some(
5356                        IconDecoration::new(
5357                            IconDecorationKind::Triangle,
5358                            self.tool_card_header_bg(cx),
5359                            cx,
5360                        )
5361                        .color(cx.theme().status().warning)
5362                        .position(gpui::Point {
5363                            x: px(-2.),
5364                            y: px(-2.),
5365                        }),
5366                    ),
5367                ))
5368                .into_any_element()
5369        } else if is_file {
5370            div().child(file_icon).into_any_element()
5371        } else if is_subagent_tool_call {
5372            Icon::new(self.agent_icon)
5373                .size(IconSize::Small)
5374                .color(Color::Muted)
5375                .into_any_element()
5376        } else {
5377            Icon::new(match tool_call.kind {
5378                acp::ToolKind::Read => IconName::ToolSearch,
5379                acp::ToolKind::Edit => IconName::ToolPencil,
5380                acp::ToolKind::Delete => IconName::ToolDeleteFile,
5381                acp::ToolKind::Move => IconName::ArrowRightLeft,
5382                acp::ToolKind::Search => IconName::ToolSearch,
5383                acp::ToolKind::Execute => IconName::ToolTerminal,
5384                acp::ToolKind::Think => IconName::ToolThink,
5385                acp::ToolKind::Fetch => IconName::ToolWeb,
5386                acp::ToolKind::SwitchMode => IconName::ArrowRightLeft,
5387                acp::ToolKind::Other | _ => IconName::ToolHammer,
5388            })
5389            .size(IconSize::Small)
5390            .color(Color::Muted)
5391            .into_any_element()
5392        };
5393
5394        let gradient_overlay = {
5395            div()
5396                .absolute()
5397                .top_0()
5398                .right_0()
5399                .w_12()
5400                .h_full()
5401                .map(|this| {
5402                    if use_card_layout {
5403                        this.bg(linear_gradient(
5404                            90.,
5405                            linear_color_stop(self.tool_card_header_bg(cx), 1.),
5406                            linear_color_stop(self.tool_card_header_bg(cx).opacity(0.2), 0.),
5407                        ))
5408                    } else {
5409                        this.bg(linear_gradient(
5410                            90.,
5411                            linear_color_stop(cx.theme().colors().panel_background, 1.),
5412                            linear_color_stop(
5413                                cx.theme().colors().panel_background.opacity(0.2),
5414                                0.,
5415                            ),
5416                        ))
5417                    }
5418                })
5419        };
5420
5421        h_flex()
5422            .relative()
5423            .w_full()
5424            .h(window.line_height() - px(2.))
5425            .text_size(self.tool_name_font_size())
5426            .gap_1p5()
5427            .when(has_location || use_card_layout, |this| this.px_1())
5428            .when(has_location, |this| {
5429                this.cursor(CursorStyle::PointingHand)
5430                    .rounded(rems_from_px(3.)) // Concentric border radius
5431                    .hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.5)))
5432            })
5433            .overflow_hidden()
5434            .child(tool_icon)
5435            .child(if has_location {
5436                h_flex()
5437                    .id(("open-tool-call-location", entry_ix))
5438                    .w_full()
5439                    .map(|this| {
5440                        if use_card_layout {
5441                            this.text_color(cx.theme().colors().text)
5442                        } else {
5443                            this.text_color(cx.theme().colors().text_muted)
5444                        }
5445                    })
5446                    .child(
5447                        self.render_markdown(
5448                            tool_call.label.clone(),
5449                            MarkdownStyle {
5450                                prevent_mouse_interaction: true,
5451                                ..MarkdownStyle::themed(MarkdownFont::Agent, window, cx)
5452                                    .with_muted_text(cx)
5453                            },
5454                        ),
5455                    )
5456                    .tooltip(Tooltip::text("Go to File"))
5457                    .on_click(cx.listener(move |this, _, window, cx| {
5458                        this.open_tool_call_location(entry_ix, 0, window, cx);
5459                    }))
5460                    .into_any_element()
5461            } else {
5462                h_flex()
5463                    .w_full()
5464                    .child(self.render_markdown(
5465                        tool_call.label.clone(),
5466                        MarkdownStyle::themed(MarkdownFont::Agent, window, cx).with_muted_text(cx),
5467                    ))
5468                    .into_any()
5469            })
5470            .when(!is_edit, |this| this.child(gradient_overlay))
5471    }
5472
5473    fn open_tool_call_location(
5474        &self,
5475        entry_ix: usize,
5476        location_ix: usize,
5477        window: &mut Window,
5478        cx: &mut Context<Self>,
5479    ) -> Option<()> {
5480        let (tool_call_location, agent_location) = self
5481            .thread
5482            .read(cx)
5483            .entries()
5484            .get(entry_ix)?
5485            .location(location_ix)?;
5486
5487        let project_path = self
5488            .project
5489            .upgrade()?
5490            .read(cx)
5491            .find_project_path(&tool_call_location.path, cx)?;
5492
5493        let open_task = self
5494            .workspace
5495            .update(cx, |workspace, cx| {
5496                workspace.open_path(project_path, None, true, window, cx)
5497            })
5498            .log_err()?;
5499        window
5500            .spawn(cx, async move |cx| {
5501                let item = open_task.await?;
5502
5503                let Some(active_editor) = item.downcast::<Editor>() else {
5504                    return anyhow::Ok(());
5505                };
5506
5507                active_editor.update_in(cx, |editor, window, cx| {
5508                    let multibuffer = editor.buffer().read(cx);
5509                    let buffer = multibuffer.as_singleton();
5510                    if agent_location.buffer.upgrade() == buffer {
5511                        let excerpt_id = multibuffer.excerpt_ids().first().cloned();
5512                        let anchor =
5513                            editor::Anchor::in_buffer(excerpt_id.unwrap(), agent_location.position);
5514                        editor.change_selections(Default::default(), window, cx, |selections| {
5515                            selections.select_anchor_ranges([anchor..anchor]);
5516                        })
5517                    } else {
5518                        let row = tool_call_location.line.unwrap_or_default();
5519                        editor.change_selections(Default::default(), window, cx, |selections| {
5520                            selections.select_ranges([Point::new(row, 0)..Point::new(row, 0)]);
5521                        })
5522                    }
5523                })?;
5524
5525                anyhow::Ok(())
5526            })
5527            .detach_and_log_err(cx);
5528
5529        None
5530    }
5531
5532    fn render_tool_call_content(
5533        &self,
5534        entry_ix: usize,
5535        content: &ToolCallContent,
5536        context_ix: usize,
5537        tool_call: &ToolCall,
5538        card_layout: bool,
5539        is_image_tool_call: bool,
5540        has_failed: bool,
5541        window: &Window,
5542        cx: &Context<Self>,
5543    ) -> AnyElement {
5544        match content {
5545            ToolCallContent::ContentBlock(content) => {
5546                if let Some(resource_link) = content.resource_link() {
5547                    self.render_resource_link(resource_link, cx)
5548                } else if let Some(markdown) = content.markdown() {
5549                    self.render_markdown_output(
5550                        markdown.clone(),
5551                        tool_call.id.clone(),
5552                        context_ix,
5553                        card_layout,
5554                        window,
5555                        cx,
5556                    )
5557                } else if let Some(image) = content.image() {
5558                    let location = tool_call.locations.first().cloned();
5559                    self.render_image_output(
5560                        entry_ix,
5561                        image.clone(),
5562                        location,
5563                        card_layout,
5564                        is_image_tool_call,
5565                        cx,
5566                    )
5567                } else {
5568                    Empty.into_any_element()
5569                }
5570            }
5571            ToolCallContent::Diff(diff) => {
5572                self.render_diff_editor(entry_ix, diff, tool_call, has_failed, cx)
5573            }
5574            ToolCallContent::Terminal(terminal) => {
5575                self.render_terminal_tool_call(entry_ix, terminal, tool_call, window, cx)
5576            }
5577        }
5578    }
5579
5580    fn render_resource_link(
5581        &self,
5582        resource_link: &acp::ResourceLink,
5583        cx: &Context<Self>,
5584    ) -> AnyElement {
5585        let uri: SharedString = resource_link.uri.clone().into();
5586        let is_file = resource_link.uri.strip_prefix("file://");
5587
5588        let Some(project) = self.project.upgrade() else {
5589            return Empty.into_any_element();
5590        };
5591
5592        let label: SharedString = if let Some(abs_path) = is_file {
5593            if let Some(project_path) = project
5594                .read(cx)
5595                .project_path_for_absolute_path(&Path::new(abs_path), cx)
5596                && let Some(worktree) = project
5597                    .read(cx)
5598                    .worktree_for_id(project_path.worktree_id, cx)
5599            {
5600                worktree
5601                    .read(cx)
5602                    .full_path(&project_path.path)
5603                    .to_string_lossy()
5604                    .to_string()
5605                    .into()
5606            } else {
5607                abs_path.to_string().into()
5608            }
5609        } else {
5610            uri.clone()
5611        };
5612
5613        let button_id = SharedString::from(format!("item-{}", uri));
5614
5615        div()
5616            .ml(rems(0.4))
5617            .pl_2p5()
5618            .border_l_1()
5619            .border_color(self.tool_card_border_color(cx))
5620            .overflow_hidden()
5621            .child(
5622                Button::new(button_id, label)
5623                    .label_size(LabelSize::Small)
5624                    .color(Color::Muted)
5625                    .truncate(true)
5626                    .when(is_file.is_none(), |this| {
5627                        this.icon(IconName::ArrowUpRight)
5628                            .icon_size(IconSize::XSmall)
5629                            .icon_color(Color::Muted)
5630                    })
5631                    .on_click(cx.listener({
5632                        let workspace = self.workspace.clone();
5633                        move |_, _, window, cx: &mut Context<Self>| {
5634                            open_link(uri.clone(), &workspace, window, cx);
5635                        }
5636                    })),
5637            )
5638            .into_any_element()
5639    }
5640
5641    fn render_diff_editor(
5642        &self,
5643        entry_ix: usize,
5644        diff: &Entity<acp_thread::Diff>,
5645        tool_call: &ToolCall,
5646        has_failed: bool,
5647        cx: &Context<Self>,
5648    ) -> AnyElement {
5649        let tool_progress = matches!(
5650            &tool_call.status,
5651            ToolCallStatus::InProgress | ToolCallStatus::Pending
5652        );
5653
5654        let revealed_diff_editor = if let Some(entry) =
5655            self.entry_view_state.read(cx).entry(entry_ix)
5656            && let Some(editor) = entry.editor_for_diff(diff)
5657            && diff.read(cx).has_revealed_range(cx)
5658        {
5659            Some(editor)
5660        } else {
5661            None
5662        };
5663
5664        let show_top_border = !has_failed || revealed_diff_editor.is_some();
5665
5666        v_flex()
5667            .h_full()
5668            .when(show_top_border, |this| {
5669                this.border_t_1()
5670                    .when(has_failed, |this| this.border_dashed())
5671                    .border_color(self.tool_card_border_color(cx))
5672            })
5673            .child(if let Some(editor) = revealed_diff_editor {
5674                editor.into_any_element()
5675            } else if tool_progress && self.as_native_connection(cx).is_some() {
5676                self.render_diff_loading(cx)
5677            } else {
5678                Empty.into_any()
5679            })
5680            .into_any()
5681    }
5682
5683    fn render_markdown_output(
5684        &self,
5685        markdown: Entity<Markdown>,
5686        tool_call_id: acp::ToolCallId,
5687        context_ix: usize,
5688        card_layout: bool,
5689        window: &Window,
5690        cx: &Context<Self>,
5691    ) -> AnyElement {
5692        let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id));
5693
5694        v_flex()
5695            .gap_2()
5696            .map(|this| {
5697                if card_layout {
5698                    this.when(context_ix > 0, |this| {
5699                        this.pt_2()
5700                            .border_t_1()
5701                            .border_color(self.tool_card_border_color(cx))
5702                    })
5703                } else {
5704                    this.ml(rems(0.4))
5705                        .px_3p5()
5706                        .border_l_1()
5707                        .border_color(self.tool_card_border_color(cx))
5708                }
5709            })
5710            .text_xs()
5711            .text_color(cx.theme().colors().text_muted)
5712            .child(self.render_markdown(
5713                markdown,
5714                MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
5715            ))
5716            .when(!card_layout, |this| {
5717                this.child(
5718                    IconButton::new(button_id, IconName::ChevronUp)
5719                        .full_width()
5720                        .style(ButtonStyle::Outlined)
5721                        .icon_color(Color::Muted)
5722                        .on_click(cx.listener({
5723                            move |this: &mut Self, _, _, cx: &mut Context<Self>| {
5724                                this.expanded_tool_calls.remove(&tool_call_id);
5725                                cx.notify();
5726                            }
5727                        })),
5728                )
5729            })
5730            .into_any_element()
5731    }
5732
5733    fn render_image_output(
5734        &self,
5735        entry_ix: usize,
5736        image: Arc<gpui::Image>,
5737        location: Option<acp::ToolCallLocation>,
5738        card_layout: bool,
5739        show_dimensions: bool,
5740        cx: &Context<Self>,
5741    ) -> AnyElement {
5742        let dimensions_label = if show_dimensions {
5743            let format_name = match image.format() {
5744                gpui::ImageFormat::Png => "PNG",
5745                gpui::ImageFormat::Jpeg => "JPEG",
5746                gpui::ImageFormat::Webp => "WebP",
5747                gpui::ImageFormat::Gif => "GIF",
5748                gpui::ImageFormat::Svg => "SVG",
5749                gpui::ImageFormat::Bmp => "BMP",
5750                gpui::ImageFormat::Tiff => "TIFF",
5751                gpui::ImageFormat::Ico => "ICO",
5752            };
5753            let dimensions = image::ImageReader::new(std::io::Cursor::new(image.bytes()))
5754                .with_guessed_format()
5755                .ok()
5756                .and_then(|reader| reader.into_dimensions().ok());
5757            dimensions.map(|(w, h)| format!("{}×{} {}", w, h, format_name))
5758        } else {
5759            None
5760        };
5761
5762        v_flex()
5763            .gap_2()
5764            .map(|this| {
5765                if card_layout {
5766                    this
5767                } else {
5768                    this.ml(rems(0.4))
5769                        .px_3p5()
5770                        .border_l_1()
5771                        .border_color(self.tool_card_border_color(cx))
5772                }
5773            })
5774            .when(dimensions_label.is_some() || location.is_some(), |this| {
5775                this.child(
5776                    h_flex()
5777                        .w_full()
5778                        .justify_between()
5779                        .items_center()
5780                        .children(dimensions_label.map(|label| {
5781                            Label::new(label)
5782                                .size(LabelSize::XSmall)
5783                                .color(Color::Muted)
5784                                .buffer_font(cx)
5785                        }))
5786                        .when_some(location, |this, _loc| {
5787                            this.child(
5788                                Button::new(("go-to-file", entry_ix), "Go to File")
5789                                    .label_size(LabelSize::Small)
5790                                    .on_click(cx.listener(move |this, _, window, cx| {
5791                                        this.open_tool_call_location(entry_ix, 0, window, cx);
5792                                    })),
5793                            )
5794                        }),
5795                )
5796            })
5797            .child(
5798                img(image)
5799                    .max_w_96()
5800                    .max_h_96()
5801                    .object_fit(ObjectFit::ScaleDown),
5802            )
5803            .into_any_element()
5804    }
5805
5806    fn render_subagent_tool_call(
5807        &self,
5808        entry_ix: usize,
5809        tool_call: &ToolCall,
5810        subagent_session_id: Option<acp::SessionId>,
5811        window: &Window,
5812        cx: &Context<Self>,
5813    ) -> Div {
5814        let tool_call_status = &tool_call.status;
5815
5816        let subagent_thread_view = subagent_session_id.and_then(|id| {
5817            self.server_view
5818                .upgrade()
5819                .and_then(|server_view| server_view.read(cx).as_connected())
5820                .and_then(|connected| connected.threads.get(&id))
5821        });
5822
5823        let content = self.render_subagent_card(
5824            entry_ix,
5825            0,
5826            subagent_thread_view,
5827            tool_call_status,
5828            window,
5829            cx,
5830        );
5831
5832        v_flex().mx_5().my_1p5().gap_3().child(content)
5833    }
5834
5835    fn render_subagent_card(
5836        &self,
5837        entry_ix: usize,
5838        context_ix: usize,
5839        thread_view: Option<&Entity<AcpThreadView>>,
5840        tool_call_status: &ToolCallStatus,
5841        window: &Window,
5842        cx: &Context<Self>,
5843    ) -> AnyElement {
5844        let thread = thread_view
5845            .as_ref()
5846            .map(|view| view.read(cx).thread.clone());
5847        let session_id = thread
5848            .as_ref()
5849            .map(|thread| thread.read(cx).session_id().clone());
5850        let action_log = thread.as_ref().map(|thread| thread.read(cx).action_log());
5851        let changed_buffers = action_log
5852            .map(|log| log.read(cx).changed_buffers(cx))
5853            .unwrap_or_default();
5854
5855        let is_expanded = if let Some(session_id) = &session_id {
5856            self.expanded_subagents.contains(session_id)
5857        } else {
5858            false
5859        };
5860        let files_changed = changed_buffers.len();
5861        let diff_stats = DiffStats::all_files(&changed_buffers, cx);
5862
5863        let is_running = matches!(
5864            tool_call_status,
5865            ToolCallStatus::Pending | ToolCallStatus::InProgress
5866        );
5867        let is_canceled_or_failed = matches!(
5868            tool_call_status,
5869            ToolCallStatus::Canceled | ToolCallStatus::Failed | ToolCallStatus::Rejected
5870        );
5871
5872        let title = thread
5873            .as_ref()
5874            .map(|t| t.read(cx).title())
5875            .unwrap_or_else(|| {
5876                if is_canceled_or_failed {
5877                    "Subagent Canceled"
5878                } else {
5879                    "Spawning Subagent…"
5880                }
5881                .into()
5882            });
5883
5884        let card_header_id = format!("subagent-header-{}-{}", entry_ix, context_ix);
5885        let diff_stat_id = format!("subagent-diff-{}-{}", entry_ix, context_ix);
5886
5887        let icon = h_flex().w_4().justify_center().child(if is_running {
5888            SpinnerLabel::new()
5889                .size(LabelSize::Small)
5890                .into_any_element()
5891        } else if is_canceled_or_failed {
5892            Icon::new(IconName::Close)
5893                .size(IconSize::Small)
5894                .color(Color::Error)
5895                .into_any_element()
5896        } else {
5897            Icon::new(IconName::Check)
5898                .size(IconSize::Small)
5899                .color(Color::Success)
5900                .into_any_element()
5901        });
5902
5903        let has_expandable_content = thread.as_ref().map_or(false, |thread| {
5904            thread.read(cx).entries().iter().rev().any(|entry| {
5905                if let AgentThreadEntry::AssistantMessage(msg) = entry {
5906                    msg.chunks.iter().any(|chunk| match chunk {
5907                        AssistantMessageChunk::Message { block } => block.markdown().is_some(),
5908                        AssistantMessageChunk::Thought { block } => block.markdown().is_some(),
5909                    })
5910                } else {
5911                    false
5912                }
5913            })
5914        });
5915
5916        v_flex()
5917            .w_full()
5918            .rounded_md()
5919            .border_1()
5920            .border_color(self.tool_card_border_color(cx))
5921            .overflow_hidden()
5922            .child(
5923                h_flex()
5924                    .group(&card_header_id)
5925                    .p_1()
5926                    .pl_1p5()
5927                    .w_full()
5928                    .gap_1()
5929                    .justify_between()
5930                    .bg(self.tool_card_header_bg(cx))
5931                    .child(
5932                        h_flex()
5933                            .gap_1p5()
5934                            .child(icon)
5935                            .child(Label::new(title.to_string()).size(LabelSize::Small))
5936                            .when(files_changed > 0, |this| {
5937                                this.child(
5938                                    h_flex()
5939                                        .gap_1()
5940                                        .child(
5941                                            Label::new(format!(
5942                                                "{} {} changed",
5943                                                files_changed,
5944                                                if files_changed == 1 { "file" } else { "files" }
5945                                            ))
5946                                            .size(LabelSize::Small)
5947                                            .color(Color::Muted),
5948                                        )
5949                                        .child(DiffStat::new(
5950                                            diff_stat_id.clone(),
5951                                            diff_stats.lines_added as usize,
5952                                            diff_stats.lines_removed as usize,
5953                                        )),
5954                                )
5955                            }),
5956                    )
5957                    .when_some(session_id, |this, session_id| {
5958                        this.child(
5959                            h_flex()
5960                                .when(has_expandable_content, |this| {
5961                                    this.child(
5962                                        IconButton::new(
5963                                            format!(
5964                                                "subagent-disclosure-{}-{}",
5965                                                entry_ix, context_ix
5966                                            ),
5967                                            if is_expanded {
5968                                                IconName::ChevronUp
5969                                            } else {
5970                                                IconName::ChevronDown
5971                                            },
5972                                        )
5973                                        .icon_color(Color::Muted)
5974                                        .icon_size(IconSize::Small)
5975                                        .disabled(!has_expandable_content)
5976                                        .visible_on_hover(card_header_id.clone())
5977                                        .on_click(
5978                                            cx.listener({
5979                                                let session_id = session_id.clone();
5980                                                move |this, _, _, cx| {
5981                                                    if this.expanded_subagents.contains(&session_id)
5982                                                    {
5983                                                        this.expanded_subagents.remove(&session_id);
5984                                                    } else {
5985                                                        this.expanded_subagents
5986                                                            .insert(session_id.clone());
5987                                                    }
5988                                                    cx.notify();
5989                                                }
5990                                            }),
5991                                        ),
5992                                    )
5993                                })
5994                                .child(
5995                                    IconButton::new(
5996                                        format!("expand-subagent-{}-{}", entry_ix, context_ix),
5997                                        IconName::Maximize,
5998                                    )
5999                                    .icon_color(Color::Muted)
6000                                    .icon_size(IconSize::Small)
6001                                    .tooltip(Tooltip::text("Expand Subagent"))
6002                                    .visible_on_hover(card_header_id)
6003                                    .on_click(cx.listener(
6004                                        move |this, _event, window, cx| {
6005                                            this.server_view
6006                                                .update(cx, |this, cx| {
6007                                                    this.navigate_to_session(
6008                                                        session_id.clone(),
6009                                                        window,
6010                                                        cx,
6011                                                    );
6012                                                })
6013                                                .ok();
6014                                        },
6015                                    )),
6016                                )
6017                                .when(is_running, |buttons| {
6018                                    buttons.child(
6019                                        IconButton::new(
6020                                            format!("stop-subagent-{}-{}", entry_ix, context_ix),
6021                                            IconName::Stop,
6022                                        )
6023                                        .icon_size(IconSize::Small)
6024                                        .icon_color(Color::Error)
6025                                        .tooltip(Tooltip::text("Stop Subagent"))
6026                                        .when_some(
6027                                            thread_view
6028                                                .as_ref()
6029                                                .map(|view| view.read(cx).thread.clone()),
6030                                            |this, thread| {
6031                                                this.on_click(cx.listener(
6032                                                    move |_this, _event, _window, cx| {
6033                                                        thread.update(cx, |thread, _cx| {
6034                                                            thread.stop_by_user();
6035                                                        });
6036                                                    },
6037                                                ))
6038                                            },
6039                                        ),
6040                                    )
6041                                }),
6042                        )
6043                    }),
6044            )
6045            .when_some(thread_view, |this, thread_view| {
6046                let thread = &thread_view.read(cx).thread;
6047                this.when(is_expanded, |this| {
6048                    this.child(
6049                        self.render_subagent_expanded_content(
6050                            entry_ix, context_ix, thread, window, cx,
6051                        ),
6052                    )
6053                })
6054                .children(
6055                    thread
6056                        .read(cx)
6057                        .first_tool_awaiting_confirmation()
6058                        .and_then(|tc| {
6059                            if let ToolCallStatus::WaitingForConfirmation { options, .. } =
6060                                &tc.status
6061                            {
6062                                Some(self.render_subagent_pending_tool_call(
6063                                    entry_ix,
6064                                    context_ix,
6065                                    thread.clone(),
6066                                    tc,
6067                                    options,
6068                                    window,
6069                                    cx,
6070                                ))
6071                            } else {
6072                                None
6073                            }
6074                        }),
6075                )
6076            })
6077            .into_any_element()
6078    }
6079
6080    fn render_subagent_expanded_content(
6081        &self,
6082        _entry_ix: usize,
6083        _context_ix: usize,
6084        thread: &Entity<AcpThread>,
6085        window: &Window,
6086        cx: &Context<Self>,
6087    ) -> impl IntoElement {
6088        let thread_read = thread.read(cx);
6089        let session_id = thread_read.session_id().clone();
6090        let entries = thread_read.entries();
6091
6092        // Find the most recent agent message with any content (message or thought)
6093        let last_assistant_markdown = entries.iter().rev().find_map(|entry| {
6094            if let AgentThreadEntry::AssistantMessage(msg) = entry {
6095                msg.chunks.iter().find_map(|chunk| match chunk {
6096                    AssistantMessageChunk::Message { block } => block.markdown().cloned(),
6097                    AssistantMessageChunk::Thought { block } => block.markdown().cloned(),
6098                })
6099            } else {
6100                None
6101            }
6102        });
6103
6104        let scroll_handle = self
6105            .subagent_scroll_handles
6106            .borrow_mut()
6107            .entry(session_id.clone())
6108            .or_default()
6109            .clone();
6110
6111        scroll_handle.scroll_to_bottom();
6112        let editor_bg = cx.theme().colors().editor_background;
6113
6114        let gradient_overlay = {
6115            div().absolute().inset_0().bg(linear_gradient(
6116                180.,
6117                linear_color_stop(editor_bg, 0.),
6118                linear_color_stop(editor_bg.opacity(0.), 0.15),
6119            ))
6120        };
6121
6122        div()
6123            .relative()
6124            .w_full()
6125            .max_h_56()
6126            .p_2p5()
6127            .text_ui(cx)
6128            .border_t_1()
6129            .border_color(self.tool_card_border_color(cx))
6130            .bg(editor_bg.opacity(0.4))
6131            .overflow_hidden()
6132            .child(
6133                div()
6134                    .id(format!("subagent-content-{}", session_id))
6135                    .size_full()
6136                    .track_scroll(&scroll_handle)
6137                    .when_some(last_assistant_markdown, |this, markdown| {
6138                        this.child(self.render_markdown(
6139                            markdown,
6140                            MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
6141                        ))
6142                    }),
6143            )
6144            .child(gradient_overlay)
6145    }
6146
6147    fn render_subagent_pending_tool_call(
6148        &self,
6149        entry_ix: usize,
6150        context_ix: usize,
6151        subagent_thread: Entity<AcpThread>,
6152        tool_call: &ToolCall,
6153        options: &PermissionOptions,
6154        window: &Window,
6155        cx: &Context<Self>,
6156    ) -> Div {
6157        let tool_call_id = tool_call.id.clone();
6158        let is_edit =
6159            matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some();
6160        let has_image_content = tool_call.content.iter().any(|c| c.image().is_some());
6161
6162        v_flex()
6163            .w_full()
6164            .border_t_1()
6165            .border_color(self.tool_card_border_color(cx))
6166            .child(
6167                self.render_tool_call_label(
6168                    entry_ix, tool_call, is_edit, false, // has_failed
6169                    false, // has_revealed_diff
6170                    true,  // use_card_layout
6171                    window, cx,
6172                )
6173                .py_1(),
6174            )
6175            .children(
6176                tool_call
6177                    .content
6178                    .iter()
6179                    .enumerate()
6180                    .map(|(content_ix, content)| {
6181                        self.render_tool_call_content(
6182                            entry_ix,
6183                            content,
6184                            content_ix,
6185                            tool_call,
6186                            true, // card_layout
6187                            has_image_content,
6188                            false, // has_failed
6189                            window,
6190                            cx,
6191                        )
6192                    }),
6193            )
6194            .child(self.render_subagent_permission_buttons(
6195                entry_ix,
6196                context_ix,
6197                subagent_thread,
6198                tool_call_id,
6199                options,
6200                cx,
6201            ))
6202    }
6203
6204    fn render_subagent_permission_buttons(
6205        &self,
6206        entry_ix: usize,
6207        context_ix: usize,
6208        subagent_thread: Entity<AcpThread>,
6209        tool_call_id: acp::ToolCallId,
6210        options: &PermissionOptions,
6211        cx: &Context<Self>,
6212    ) -> Div {
6213        match options {
6214            PermissionOptions::Flat(options) => self.render_subagent_permission_buttons_flat(
6215                entry_ix,
6216                context_ix,
6217                subagent_thread,
6218                tool_call_id,
6219                options,
6220                cx,
6221            ),
6222            PermissionOptions::Dropdown(options) => self
6223                .render_subagent_permission_buttons_dropdown(
6224                    entry_ix,
6225                    context_ix,
6226                    subagent_thread,
6227                    tool_call_id,
6228                    options,
6229                    cx,
6230                ),
6231        }
6232    }
6233
6234    fn render_subagent_permission_buttons_flat(
6235        &self,
6236        entry_ix: usize,
6237        context_ix: usize,
6238        subagent_thread: Entity<AcpThread>,
6239        tool_call_id: acp::ToolCallId,
6240        options: &[acp::PermissionOption],
6241        cx: &Context<Self>,
6242    ) -> Div {
6243        div()
6244            .p_1()
6245            .border_t_1()
6246            .border_color(self.tool_card_border_color(cx))
6247            .w_full()
6248            .v_flex()
6249            .gap_0p5()
6250            .children(options.iter().map(move |option| {
6251                let option_id = SharedString::from(format!(
6252                    "subagent-{}-{}-{}",
6253                    entry_ix, context_ix, option.option_id.0
6254                ));
6255                Button::new((option_id, entry_ix), option.name.clone())
6256                    .map(|this| match option.kind {
6257                        acp::PermissionOptionKind::AllowOnce => {
6258                            this.icon(IconName::Check).icon_color(Color::Success)
6259                        }
6260                        acp::PermissionOptionKind::AllowAlways => {
6261                            this.icon(IconName::CheckDouble).icon_color(Color::Success)
6262                        }
6263                        acp::PermissionOptionKind::RejectOnce
6264                        | acp::PermissionOptionKind::RejectAlways
6265                        | _ => this.icon(IconName::Close).icon_color(Color::Error),
6266                    })
6267                    .icon_position(IconPosition::Start)
6268                    .icon_size(IconSize::XSmall)
6269                    .label_size(LabelSize::Small)
6270                    .on_click(cx.listener({
6271                        let subagent_thread = subagent_thread.clone();
6272                        let tool_call_id = tool_call_id.clone();
6273                        let option_id = option.option_id.clone();
6274                        let option_kind = option.kind;
6275                        move |this, _, window, cx| {
6276                            this.authorize_subagent_tool_call(
6277                                subagent_thread.clone(),
6278                                tool_call_id.clone(),
6279                                option_id.clone(),
6280                                option_kind,
6281                                window,
6282                                cx,
6283                            );
6284                        }
6285                    }))
6286            }))
6287    }
6288
6289    fn authorize_subagent_tool_call(
6290        &mut self,
6291        subagent_thread: Entity<AcpThread>,
6292        tool_call_id: acp::ToolCallId,
6293        option_id: acp::PermissionOptionId,
6294        option_kind: acp::PermissionOptionKind,
6295        _window: &mut Window,
6296        cx: &mut Context<Self>,
6297    ) {
6298        subagent_thread.update(cx, |thread, cx| {
6299            thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx);
6300        });
6301    }
6302
6303    fn render_subagent_permission_buttons_dropdown(
6304        &self,
6305        entry_ix: usize,
6306        context_ix: usize,
6307        subagent_thread: Entity<AcpThread>,
6308        tool_call_id: acp::ToolCallId,
6309        choices: &[PermissionOptionChoice],
6310        cx: &Context<Self>,
6311    ) -> Div {
6312        let selected_index = self
6313            .selected_permission_granularity
6314            .get(&tool_call_id)
6315            .copied()
6316            .unwrap_or_else(|| choices.len().saturating_sub(1));
6317
6318        let selected_choice = choices.get(selected_index).or(choices.last());
6319
6320        let dropdown_label: SharedString = selected_choice
6321            .map(|choice| choice.label())
6322            .unwrap_or_else(|| "Only this time".into());
6323
6324        let (allow_option_id, allow_option_kind, deny_option_id, deny_option_kind) =
6325            if let Some(choice) = selected_choice {
6326                (
6327                    choice.allow.option_id.clone(),
6328                    choice.allow.kind,
6329                    choice.deny.option_id.clone(),
6330                    choice.deny.kind,
6331                )
6332            } else {
6333                (
6334                    acp::PermissionOptionId::new("allow"),
6335                    acp::PermissionOptionKind::AllowOnce,
6336                    acp::PermissionOptionId::new("deny"),
6337                    acp::PermissionOptionKind::RejectOnce,
6338                )
6339            };
6340
6341        h_flex()
6342            .w_full()
6343            .p_1()
6344            .gap_2()
6345            .justify_between()
6346            .border_t_1()
6347            .border_color(self.tool_card_border_color(cx))
6348            .child(
6349                h_flex()
6350                    .gap_0p5()
6351                    .child(
6352                        Button::new(
6353                            (
6354                                SharedString::from(format!(
6355                                    "subagent-allow-btn-{}-{}",
6356                                    entry_ix, context_ix
6357                                )),
6358                                entry_ix,
6359                            ),
6360                            "Allow",
6361                        )
6362                        .icon(IconName::Check)
6363                        .icon_color(Color::Success)
6364                        .icon_position(IconPosition::Start)
6365                        .icon_size(IconSize::XSmall)
6366                        .label_size(LabelSize::Small)
6367                        .on_click(cx.listener({
6368                            let subagent_thread = subagent_thread.clone();
6369                            let tool_call_id = tool_call_id.clone();
6370                            let option_id = allow_option_id;
6371                            let option_kind = allow_option_kind;
6372                            move |this, _, window, cx| {
6373                                this.authorize_subagent_tool_call(
6374                                    subagent_thread.clone(),
6375                                    tool_call_id.clone(),
6376                                    option_id.clone(),
6377                                    option_kind,
6378                                    window,
6379                                    cx,
6380                                );
6381                            }
6382                        })),
6383                    )
6384                    .child(
6385                        Button::new(
6386                            (
6387                                SharedString::from(format!(
6388                                    "subagent-deny-btn-{}-{}",
6389                                    entry_ix, context_ix
6390                                )),
6391                                entry_ix,
6392                            ),
6393                            "Deny",
6394                        )
6395                        .icon(IconName::Close)
6396                        .icon_color(Color::Error)
6397                        .icon_position(IconPosition::Start)
6398                        .icon_size(IconSize::XSmall)
6399                        .label_size(LabelSize::Small)
6400                        .on_click(cx.listener({
6401                            let tool_call_id = tool_call_id.clone();
6402                            let option_id = deny_option_id;
6403                            let option_kind = deny_option_kind;
6404                            move |this, _, window, cx| {
6405                                this.authorize_subagent_tool_call(
6406                                    subagent_thread.clone(),
6407                                    tool_call_id.clone(),
6408                                    option_id.clone(),
6409                                    option_kind,
6410                                    window,
6411                                    cx,
6412                                );
6413                            }
6414                        })),
6415                    ),
6416            )
6417            .child(self.render_subagent_permission_granularity_dropdown(
6418                choices,
6419                dropdown_label,
6420                entry_ix,
6421                context_ix,
6422                tool_call_id,
6423                selected_index,
6424                cx,
6425            ))
6426    }
6427
6428    fn render_subagent_permission_granularity_dropdown(
6429        &self,
6430        choices: &[PermissionOptionChoice],
6431        current_label: SharedString,
6432        entry_ix: usize,
6433        context_ix: usize,
6434        tool_call_id: acp::ToolCallId,
6435        selected_index: usize,
6436        _cx: &Context<Self>,
6437    ) -> AnyElement {
6438        let menu_options: Vec<(usize, SharedString)> = choices
6439            .iter()
6440            .enumerate()
6441            .map(|(i, choice)| (i, choice.label()))
6442            .collect();
6443
6444        let permission_dropdown_handle = self.permission_dropdown_handle.clone();
6445
6446        PopoverMenu::new((
6447            SharedString::from(format!(
6448                "subagent-permission-granularity-{}-{}",
6449                entry_ix, context_ix
6450            )),
6451            entry_ix,
6452        ))
6453        .with_handle(permission_dropdown_handle)
6454        .trigger(
6455            Button::new(
6456                (
6457                    SharedString::from(format!(
6458                        "subagent-granularity-trigger-{}-{}",
6459                        entry_ix, context_ix
6460                    )),
6461                    entry_ix,
6462                ),
6463                current_label,
6464            )
6465            .icon(IconName::ChevronDown)
6466            .icon_size(IconSize::XSmall)
6467            .icon_color(Color::Muted)
6468            .label_size(LabelSize::Small),
6469        )
6470        .menu(move |window, cx| {
6471            let tool_call_id = tool_call_id.clone();
6472            let options = menu_options.clone();
6473
6474            Some(ContextMenu::build(window, cx, move |mut menu, _, _| {
6475                for (index, display_name) in options.iter() {
6476                    let display_name = display_name.clone();
6477                    let index = *index;
6478                    let tool_call_id_for_entry = tool_call_id.clone();
6479                    let is_selected = index == selected_index;
6480
6481                    menu = menu.toggleable_entry(
6482                        display_name,
6483                        is_selected,
6484                        IconPosition::End,
6485                        None,
6486                        move |window, cx| {
6487                            window.dispatch_action(
6488                                SelectPermissionGranularity {
6489                                    tool_call_id: tool_call_id_for_entry.0.to_string(),
6490                                    index,
6491                                }
6492                                .boxed_clone(),
6493                                cx,
6494                            );
6495                        },
6496                    );
6497                }
6498
6499                menu
6500            }))
6501        })
6502        .into_any_element()
6503    }
6504
6505    fn render_rules_item(&self, cx: &Context<Self>) -> Option<AnyElement> {
6506        let project_context = self
6507            .as_native_thread(cx)?
6508            .read(cx)
6509            .project_context()
6510            .read(cx);
6511
6512        let user_rules_text = if project_context.user_rules.is_empty() {
6513            None
6514        } else if project_context.user_rules.len() == 1 {
6515            let user_rules = &project_context.user_rules[0];
6516
6517            match user_rules.title.as_ref() {
6518                Some(title) => Some(format!("Using \"{title}\" user rule")),
6519                None => Some("Using user rule".into()),
6520            }
6521        } else {
6522            Some(format!(
6523                "Using {} user rules",
6524                project_context.user_rules.len()
6525            ))
6526        };
6527
6528        let first_user_rules_id = project_context
6529            .user_rules
6530            .first()
6531            .map(|user_rules| user_rules.uuid.0);
6532
6533        let rules_files = project_context
6534            .worktrees
6535            .iter()
6536            .filter_map(|worktree| worktree.rules_file.as_ref())
6537            .collect::<Vec<_>>();
6538
6539        let rules_file_text = match rules_files.as_slice() {
6540            &[] => None,
6541            &[rules_file] => Some(format!(
6542                "Using project {:?} file",
6543                rules_file.path_in_worktree
6544            )),
6545            rules_files => Some(format!("Using {} project rules files", rules_files.len())),
6546        };
6547
6548        if user_rules_text.is_none() && rules_file_text.is_none() {
6549            return None;
6550        }
6551
6552        let has_both = user_rules_text.is_some() && rules_file_text.is_some();
6553
6554        Some(
6555            h_flex()
6556                .px_2p5()
6557                .child(
6558                    Icon::new(IconName::Attach)
6559                        .size(IconSize::XSmall)
6560                        .color(Color::Disabled),
6561                )
6562                .when_some(user_rules_text, |parent, user_rules_text| {
6563                    parent.child(
6564                        h_flex()
6565                            .id("user-rules")
6566                            .ml_1()
6567                            .mr_1p5()
6568                            .child(
6569                                Label::new(user_rules_text)
6570                                    .size(LabelSize::XSmall)
6571                                    .color(Color::Muted)
6572                                    .truncate(),
6573                            )
6574                            .hover(|s| s.bg(cx.theme().colors().element_hover))
6575                            .tooltip(Tooltip::text("View User Rules"))
6576                            .on_click(move |_event, window, cx| {
6577                                window.dispatch_action(
6578                                    Box::new(OpenRulesLibrary {
6579                                        prompt_to_select: first_user_rules_id,
6580                                    }),
6581                                    cx,
6582                                )
6583                            }),
6584                    )
6585                })
6586                .when(has_both, |this| {
6587                    this.child(
6588                        Label::new("")
6589                            .size(LabelSize::XSmall)
6590                            .color(Color::Disabled),
6591                    )
6592                })
6593                .when_some(rules_file_text, |parent, rules_file_text| {
6594                    parent.child(
6595                        h_flex()
6596                            .id("project-rules")
6597                            .ml_1p5()
6598                            .child(
6599                                Label::new(rules_file_text)
6600                                    .size(LabelSize::XSmall)
6601                                    .color(Color::Muted),
6602                            )
6603                            .hover(|s| s.bg(cx.theme().colors().element_hover))
6604                            .tooltip(Tooltip::text("View Project Rules"))
6605                            .on_click(cx.listener(Self::handle_open_rules)),
6606                    )
6607                })
6608                .into_any(),
6609        )
6610    }
6611
6612    fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
6613        cx.theme()
6614            .colors()
6615            .element_background
6616            .blend(cx.theme().colors().editor_foreground.opacity(0.025))
6617    }
6618
6619    fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
6620        cx.theme().colors().border.opacity(0.8)
6621    }
6622
6623    fn tool_name_font_size(&self) -> Rems {
6624        rems_from_px(13.)
6625    }
6626
6627    pub(crate) fn render_thread_error(
6628        &mut self,
6629        window: &mut Window,
6630        cx: &mut Context<Self>,
6631    ) -> Option<Div> {
6632        let content = match self.thread_error.as_ref()? {
6633            ThreadError::Other { message, .. } => {
6634                self.render_any_thread_error(message.clone(), window, cx)
6635            }
6636            ThreadError::Refusal => self.render_refusal_error(cx),
6637            ThreadError::AuthenticationRequired(error) => {
6638                self.render_authentication_required_error(error.clone(), cx)
6639            }
6640            ThreadError::PaymentRequired => self.render_payment_required_error(cx),
6641        };
6642
6643        Some(div().child(content))
6644    }
6645
6646    fn render_refusal_error(&self, cx: &mut Context<'_, Self>) -> Callout {
6647        let model_or_agent_name = self.current_model_name(cx);
6648        let refusal_message = format!(
6649            "{} refused to respond to this prompt. \
6650            This can happen when a model believes the prompt violates its content policy \
6651            or safety guidelines, so rephrasing it can sometimes address the issue.",
6652            model_or_agent_name
6653        );
6654
6655        Callout::new()
6656            .severity(Severity::Error)
6657            .title("Request Refused")
6658            .icon(IconName::XCircle)
6659            .description(refusal_message.clone())
6660            .actions_slot(self.create_copy_button(&refusal_message))
6661            .dismiss_action(self.dismiss_error_button(cx))
6662    }
6663
6664    fn render_authentication_required_error(
6665        &self,
6666        error: SharedString,
6667        cx: &mut Context<Self>,
6668    ) -> Callout {
6669        Callout::new()
6670            .severity(Severity::Error)
6671            .title("Authentication Required")
6672            .icon(IconName::XCircle)
6673            .description(error.clone())
6674            .actions_slot(
6675                h_flex()
6676                    .gap_0p5()
6677                    .child(self.authenticate_button(cx))
6678                    .child(self.create_copy_button(error)),
6679            )
6680            .dismiss_action(self.dismiss_error_button(cx))
6681    }
6682
6683    fn render_payment_required_error(&self, cx: &mut Context<Self>) -> Callout {
6684        const ERROR_MESSAGE: &str =
6685            "You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
6686
6687        Callout::new()
6688            .severity(Severity::Error)
6689            .icon(IconName::XCircle)
6690            .title("Free Usage Exceeded")
6691            .description(ERROR_MESSAGE)
6692            .actions_slot(
6693                h_flex()
6694                    .gap_0p5()
6695                    .child(self.upgrade_button(cx))
6696                    .child(self.create_copy_button(ERROR_MESSAGE)),
6697            )
6698            .dismiss_action(self.dismiss_error_button(cx))
6699    }
6700
6701    fn upgrade_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
6702        Button::new("upgrade", "Upgrade")
6703            .label_size(LabelSize::Small)
6704            .style(ButtonStyle::Tinted(ui::TintColor::Accent))
6705            .on_click(cx.listener({
6706                move |this, _, _, cx| {
6707                    this.clear_thread_error(cx);
6708                    cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx));
6709                }
6710            }))
6711    }
6712
6713    fn authenticate_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
6714        Button::new("authenticate", "Authenticate")
6715            .label_size(LabelSize::Small)
6716            .style(ButtonStyle::Filled)
6717            .on_click(cx.listener({
6718                move |this, _, window, cx| {
6719                    let server_view = this.server_view.clone();
6720                    let agent_name = this.agent_name.clone();
6721
6722                    this.clear_thread_error(cx);
6723                    if let Some(message) = this.in_flight_prompt.take() {
6724                        this.message_editor.update(cx, |editor, cx| {
6725                            editor.set_message(message, window, cx);
6726                        });
6727                    }
6728                    let connection = this.thread.read(cx).connection().clone();
6729                    window.defer(cx, |window, cx| {
6730                        AcpServerView::handle_auth_required(
6731                            server_view,
6732                            AuthRequired::new(),
6733                            agent_name,
6734                            connection,
6735                            window,
6736                            cx,
6737                        );
6738                    })
6739                }
6740            }))
6741    }
6742
6743    fn current_model_name(&self, cx: &App) -> SharedString {
6744        // For native agent (Zed Agent), use the specific model name (e.g., "Claude 3.5 Sonnet")
6745        // For ACP agents, use the agent name (e.g., "Claude Code", "Gemini CLI")
6746        // This provides better clarity about what refused the request
6747        if self.as_native_connection(cx).is_some() {
6748            self.model_selector
6749                .clone()
6750                .and_then(|selector| selector.read(cx).active_model(cx))
6751                .map(|model| model.name.clone())
6752                .unwrap_or_else(|| SharedString::from("The model"))
6753        } else {
6754            // ACP agent - use the agent name (e.g., "Claude Code", "Gemini CLI")
6755            self.agent_name.clone()
6756        }
6757    }
6758
6759    fn render_any_thread_error(
6760        &mut self,
6761        error: SharedString,
6762        window: &mut Window,
6763        cx: &mut Context<'_, Self>,
6764    ) -> Callout {
6765        let can_resume = self.thread.read(cx).can_retry(cx);
6766
6767        let markdown = if let Some(markdown) = &self.thread_error_markdown {
6768            markdown.clone()
6769        } else {
6770            let markdown = cx.new(|cx| Markdown::new(error.clone(), None, None, cx));
6771            self.thread_error_markdown = Some(markdown.clone());
6772            markdown
6773        };
6774
6775        let markdown_style =
6776            MarkdownStyle::themed(MarkdownFont::Agent, window, cx).with_muted_text(cx);
6777        let description = self
6778            .render_markdown(markdown, markdown_style)
6779            .into_any_element();
6780
6781        Callout::new()
6782            .severity(Severity::Error)
6783            .icon(IconName::XCircle)
6784            .title("An Error Happened")
6785            .description_slot(description)
6786            .actions_slot(
6787                h_flex()
6788                    .gap_0p5()
6789                    .when(can_resume, |this| {
6790                        this.child(
6791                            IconButton::new("retry", IconName::RotateCw)
6792                                .icon_size(IconSize::Small)
6793                                .tooltip(Tooltip::text("Retry Generation"))
6794                                .on_click(cx.listener(|this, _, _window, cx| {
6795                                    this.retry_generation(cx);
6796                                })),
6797                        )
6798                    })
6799                    .child(self.create_copy_button(error.to_string())),
6800            )
6801            .dismiss_action(self.dismiss_error_button(cx))
6802    }
6803
6804    fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
6805        let workspace = self.workspace.clone();
6806        MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
6807            open_link(text, &workspace, window, cx);
6808        })
6809    }
6810
6811    fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
6812        let message = message.into();
6813
6814        CopyButton::new("copy-error-message", message).tooltip_label("Copy Error Message")
6815    }
6816
6817    fn dismiss_error_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
6818        IconButton::new("dismiss", IconName::Close)
6819            .icon_size(IconSize::Small)
6820            .tooltip(Tooltip::text("Dismiss"))
6821            .on_click(cx.listener({
6822                move |this, _, _, cx| {
6823                    this.clear_thread_error(cx);
6824                    cx.notify();
6825                }
6826            }))
6827    }
6828
6829    fn render_resume_notice(_cx: &Context<Self>) -> AnyElement {
6830        let description = "This agent does not support viewing previous messages. However, your session will still continue from where you last left off.";
6831
6832        div()
6833            .px_2()
6834            .pt_2()
6835            .pb_3()
6836            .w_full()
6837            .child(
6838                Callout::new()
6839                    .severity(Severity::Info)
6840                    .icon(IconName::Info)
6841                    .title("Resumed Session")
6842                    .description(description),
6843            )
6844            .into_any_element()
6845    }
6846
6847    fn update_recent_history_from_cache(
6848        &mut self,
6849        history: &Entity<AcpThreadHistory>,
6850        cx: &mut Context<Self>,
6851    ) {
6852        self.recent_history_entries = history.read(cx).get_recent_sessions(3);
6853        self.hovered_recent_history_item = None;
6854        cx.notify();
6855    }
6856
6857    fn render_empty_state_section_header(
6858        &self,
6859        label: impl Into<SharedString>,
6860        action_slot: Option<AnyElement>,
6861        cx: &mut Context<Self>,
6862    ) -> impl IntoElement {
6863        div().pl_1().pr_1p5().child(
6864            h_flex()
6865                .mt_2()
6866                .pl_1p5()
6867                .pb_1()
6868                .w_full()
6869                .justify_between()
6870                .border_b_1()
6871                .border_color(cx.theme().colors().border_variant)
6872                .child(
6873                    Label::new(label.into())
6874                        .size(LabelSize::Small)
6875                        .color(Color::Muted),
6876                )
6877                .children(action_slot),
6878        )
6879    }
6880
6881    fn render_recent_history(&self, cx: &mut Context<Self>) -> AnyElement {
6882        let render_history = !self.recent_history_entries.is_empty();
6883
6884        v_flex()
6885            .size_full()
6886            .when(render_history, |this| {
6887                let recent_history = self.recent_history_entries.clone();
6888                this.justify_end().child(
6889                    v_flex()
6890                        .child(
6891                            self.render_empty_state_section_header(
6892                                "Recent",
6893                                Some(
6894                                    Button::new("view-history", "View All")
6895                                        .style(ButtonStyle::Subtle)
6896                                        .label_size(LabelSize::Small)
6897                                        .key_binding(
6898                                            KeyBinding::for_action_in(
6899                                                &OpenHistory,
6900                                                &self.focus_handle(cx),
6901                                                cx,
6902                                            )
6903                                            .map(|kb| kb.size(rems_from_px(12.))),
6904                                        )
6905                                        .on_click(move |_event, window, cx| {
6906                                            window.dispatch_action(OpenHistory.boxed_clone(), cx);
6907                                        })
6908                                        .into_any_element(),
6909                                ),
6910                                cx,
6911                            ),
6912                        )
6913                        .child(v_flex().p_1().pr_1p5().gap_1().children({
6914                            let supports_delete = self.history.read(cx).supports_delete();
6915                            recent_history
6916                                .into_iter()
6917                                .enumerate()
6918                                .map(move |(index, entry)| {
6919                                    // TODO: Add keyboard navigation.
6920                                    let is_hovered =
6921                                        self.hovered_recent_history_item == Some(index);
6922                                    crate::acp::thread_history::AcpHistoryEntryElement::new(
6923                                        entry,
6924                                        self.server_view.clone(),
6925                                    )
6926                                    .hovered(is_hovered)
6927                                    .supports_delete(supports_delete)
6928                                    .on_hover(cx.listener(move |this, is_hovered, _window, cx| {
6929                                        if *is_hovered {
6930                                            this.hovered_recent_history_item = Some(index);
6931                                        } else if this.hovered_recent_history_item == Some(index) {
6932                                            this.hovered_recent_history_item = None;
6933                                        }
6934                                        cx.notify();
6935                                    }))
6936                                    .into_any_element()
6937                                })
6938                        })),
6939                )
6940            })
6941            .into_any()
6942    }
6943
6944    fn render_codex_windows_warning(&self, cx: &mut Context<Self>) -> Callout {
6945        Callout::new()
6946            .icon(IconName::Warning)
6947            .severity(Severity::Warning)
6948            .title("Codex on Windows")
6949            .description("For best performance, run Codex in Windows Subsystem for Linux (WSL2)")
6950            .actions_slot(
6951                Button::new("open-wsl-modal", "Open in WSL")
6952                    .icon_size(IconSize::Small)
6953                    .icon_color(Color::Muted)
6954                    .on_click(cx.listener({
6955                        move |_, _, _window, cx| {
6956                            #[cfg(windows)]
6957                            _window.dispatch_action(
6958                                zed_actions::wsl_actions::OpenWsl::default().boxed_clone(),
6959                                cx,
6960                            );
6961                            cx.notify();
6962                        }
6963                    })),
6964            )
6965            .dismiss_action(
6966                IconButton::new("dismiss", IconName::Close)
6967                    .icon_size(IconSize::Small)
6968                    .icon_color(Color::Muted)
6969                    .tooltip(Tooltip::text("Dismiss Warning"))
6970                    .on_click(cx.listener({
6971                        move |this, _, _, cx| {
6972                            this.show_codex_windows_warning = false;
6973                            cx.notify();
6974                        }
6975                    })),
6976            )
6977    }
6978
6979    fn render_new_version_callout(&self, version: &SharedString, cx: &mut Context<Self>) -> Div {
6980        let server_view = self.server_view.clone();
6981        v_flex().w_full().justify_end().child(
6982            h_flex()
6983                .p_2()
6984                .pr_3()
6985                .w_full()
6986                .gap_1p5()
6987                .border_t_1()
6988                .border_color(cx.theme().colors().border)
6989                .bg(cx.theme().colors().element_background)
6990                .child(
6991                    h_flex()
6992                        .flex_1()
6993                        .gap_1p5()
6994                        .child(
6995                            Icon::new(IconName::Download)
6996                                .color(Color::Accent)
6997                                .size(IconSize::Small),
6998                        )
6999                        .child(Label::new("New version available").size(LabelSize::Small)),
7000                )
7001                .child(
7002                    Button::new("update-button", format!("Update to v{}", version))
7003                        .label_size(LabelSize::Small)
7004                        .style(ButtonStyle::Tinted(TintColor::Accent))
7005                        .on_click(move |_, window, cx| {
7006                            server_view
7007                                .update(cx, |view, cx| view.reset(window, cx))
7008                                .ok();
7009                        }),
7010                ),
7011        )
7012    }
7013
7014    fn render_token_limit_callout(&self, cx: &mut Context<Self>) -> Option<Callout> {
7015        if self.token_limit_callout_dismissed {
7016            return None;
7017        }
7018
7019        let token_usage = self.thread.read(cx).token_usage()?;
7020        let ratio = token_usage.ratio();
7021
7022        let (severity, icon, title) = match ratio {
7023            acp_thread::TokenUsageRatio::Normal => return None,
7024            acp_thread::TokenUsageRatio::Warning => (
7025                Severity::Warning,
7026                IconName::Warning,
7027                "Thread reaching the token limit soon",
7028            ),
7029            acp_thread::TokenUsageRatio::Exceeded => (
7030                Severity::Error,
7031                IconName::XCircle,
7032                "Thread reached the token limit",
7033            ),
7034        };
7035
7036        let description = "To continue, start a new thread from a summary.";
7037
7038        Some(
7039            Callout::new()
7040                .severity(severity)
7041                .icon(icon)
7042                .title(title)
7043                .description(description)
7044                .actions_slot(
7045                    h_flex().gap_0p5().child(
7046                        Button::new("start-new-thread", "Start New Thread")
7047                            .label_size(LabelSize::Small)
7048                            .on_click(cx.listener(|this, _, window, cx| {
7049                                let session_id = this.thread.read(cx).session_id().clone();
7050                                window.dispatch_action(
7051                                    crate::NewNativeAgentThreadFromSummary {
7052                                        from_session_id: session_id,
7053                                    }
7054                                    .boxed_clone(),
7055                                    cx,
7056                                );
7057                            })),
7058                    ),
7059                )
7060                .dismiss_action(self.dismiss_error_button(cx)),
7061        )
7062    }
7063
7064    fn open_permission_dropdown(
7065        &mut self,
7066        _: &crate::OpenPermissionDropdown,
7067        window: &mut Window,
7068        cx: &mut Context<Self>,
7069    ) {
7070        self.permission_dropdown_handle.clone().toggle(window, cx);
7071    }
7072
7073    fn open_add_context_menu(
7074        &mut self,
7075        _action: &OpenAddContextMenu,
7076        window: &mut Window,
7077        cx: &mut Context<Self>,
7078    ) {
7079        let menu_handle = self.add_context_menu_handle.clone();
7080        window.defer(cx, move |window, cx| {
7081            menu_handle.toggle(window, cx);
7082        });
7083    }
7084
7085    fn cycle_thinking_effort(&mut self, cx: &mut Context<Self>) {
7086        if !cx.has_flag::<CloudThinkingEffortFeatureFlag>() {
7087            return;
7088        }
7089
7090        let Some(thread) = self.as_native_thread(cx) else {
7091            return;
7092        };
7093
7094        let (effort_levels, current_effort) = {
7095            let thread_ref = thread.read(cx);
7096            let Some(model) = thread_ref.model() else {
7097                return;
7098            };
7099            if !model.supports_thinking() || !thread_ref.thinking_enabled() {
7100                return;
7101            }
7102            let effort_levels = model.supported_effort_levels();
7103            if effort_levels.is_empty() {
7104                return;
7105            }
7106            let current_effort = thread_ref.thinking_effort().cloned();
7107            (effort_levels, current_effort)
7108        };
7109
7110        let current_index = current_effort.and_then(|current| {
7111            effort_levels
7112                .iter()
7113                .position(|level| level.value == current)
7114        });
7115        let next_index = match current_index {
7116            Some(index) => (index + 1) % effort_levels.len(),
7117            None => 0,
7118        };
7119        let next_effort = effort_levels[next_index].value.to_string();
7120
7121        thread.update(cx, |thread, cx| {
7122            thread.set_thinking_effort(Some(next_effort.clone()), cx);
7123
7124            let fs = thread.project().read(cx).fs().clone();
7125            update_settings_file(fs, cx, move |settings, _| {
7126                if let Some(agent) = settings.agent.as_mut()
7127                    && let Some(default_model) = agent.default_model.as_mut()
7128                {
7129                    default_model.effort = Some(next_effort);
7130                }
7131            });
7132        });
7133    }
7134
7135    fn toggle_thinking_effort_menu(
7136        &mut self,
7137        _action: &ToggleThinkingEffortMenu,
7138        window: &mut Window,
7139        cx: &mut Context<Self>,
7140    ) {
7141        let menu_handle = self.thinking_effort_menu_handle.clone();
7142        window.defer(cx, move |window, cx| {
7143            menu_handle.toggle(window, cx);
7144        });
7145    }
7146}
7147
7148impl Render for AcpThreadView {
7149    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
7150        let has_messages = self.list_state.item_count() > 0;
7151
7152        let conversation = v_flex().flex_1().map(|this| {
7153            let this = this.when(self.resumed_without_history, |this| {
7154                this.child(Self::render_resume_notice(cx))
7155            });
7156            if has_messages {
7157                let list_state = self.list_state.clone();
7158                this.child(self.render_entries(cx))
7159                    .vertical_scrollbar_for(&list_state, window, cx)
7160                    .into_any()
7161            } else {
7162                this.child(self.render_recent_history(cx)).into_any()
7163            }
7164        });
7165
7166        v_flex()
7167            .key_context("AcpThread")
7168            .track_focus(&self.focus_handle)
7169            .on_action(cx.listener(|this, _: &menu::Cancel, _, cx| {
7170                if this.parent_id.is_none() {
7171                    this.cancel_generation(cx);
7172                }
7173            }))
7174            .on_action(cx.listener(|this, _: &workspace::GoBack, window, cx| {
7175                if let Some(parent_session_id) = this.parent_id.clone() {
7176                    this.server_view
7177                        .update(cx, |view, cx| {
7178                            view.navigate_to_session(parent_session_id, window, cx);
7179                        })
7180                        .ok();
7181                }
7182            }))
7183            .on_action(cx.listener(Self::keep_all))
7184            .on_action(cx.listener(Self::reject_all))
7185            .on_action(cx.listener(Self::allow_always))
7186            .on_action(cx.listener(Self::allow_once))
7187            .on_action(cx.listener(Self::reject_once))
7188            .on_action(cx.listener(Self::handle_authorize_tool_call))
7189            .on_action(cx.listener(Self::handle_select_permission_granularity))
7190            .on_action(cx.listener(Self::open_permission_dropdown))
7191            .on_action(cx.listener(Self::open_add_context_menu))
7192            .on_action(cx.listener(|this, _: &ToggleThinkingMode, _window, cx| {
7193                if let Some(thread) = this.as_native_thread(cx) {
7194                    thread.update(cx, |thread, cx| {
7195                        thread.set_thinking_enabled(!thread.thinking_enabled(), cx);
7196                    });
7197                }
7198            }))
7199            .on_action(cx.listener(|this, _: &CycleThinkingEffort, _window, cx| {
7200                this.cycle_thinking_effort(cx);
7201            }))
7202            .on_action(cx.listener(Self::toggle_thinking_effort_menu))
7203            .on_action(cx.listener(|this, _: &SendNextQueuedMessage, window, cx| {
7204                this.send_queued_message_at_index(0, true, window, cx);
7205            }))
7206            .on_action(cx.listener(|this, _: &RemoveFirstQueuedMessage, _, cx| {
7207                this.remove_from_queue(0, cx);
7208                cx.notify();
7209            }))
7210            .on_action(cx.listener(|this, _: &EditFirstQueuedMessage, window, cx| {
7211                if let Some(editor) = this.queued_message_editors.first() {
7212                    window.focus(&editor.focus_handle(cx), cx);
7213                }
7214            }))
7215            .on_action(cx.listener(|this, _: &ClearMessageQueue, _, cx| {
7216                this.local_queued_messages.clear();
7217                this.sync_queue_flag_to_native_thread(cx);
7218                this.can_fast_track_queue = false;
7219                cx.notify();
7220            }))
7221            .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
7222                if let Some(config_options_view) = this.config_options_view.clone() {
7223                    let handled = config_options_view.update(cx, |view, cx| {
7224                        view.toggle_category_picker(
7225                            acp::SessionConfigOptionCategory::Mode,
7226                            window,
7227                            cx,
7228                        )
7229                    });
7230                    if handled {
7231                        return;
7232                    }
7233                }
7234
7235                if let Some(profile_selector) = this.profile_selector.clone() {
7236                    profile_selector.read(cx).menu_handle().toggle(window, cx);
7237                } else if let Some(mode_selector) = this.mode_selector.clone() {
7238                    mode_selector.read(cx).menu_handle().toggle(window, cx);
7239                }
7240            }))
7241            .on_action(cx.listener(|this, _: &CycleModeSelector, window, cx| {
7242                if let Some(config_options_view) = this.config_options_view.clone() {
7243                    let handled = config_options_view.update(cx, |view, cx| {
7244                        view.cycle_category_option(
7245                            acp::SessionConfigOptionCategory::Mode,
7246                            false,
7247                            cx,
7248                        )
7249                    });
7250                    if handled {
7251                        return;
7252                    }
7253                }
7254
7255                if let Some(profile_selector) = this.profile_selector.clone() {
7256                    profile_selector.update(cx, |profile_selector, cx| {
7257                        profile_selector.cycle_profile(cx);
7258                    });
7259                } else if let Some(mode_selector) = this.mode_selector.clone() {
7260                    mode_selector.update(cx, |mode_selector, cx| {
7261                        mode_selector.cycle_mode(window, cx);
7262                    });
7263                }
7264            }))
7265            .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
7266                if let Some(config_options_view) = this.config_options_view.clone() {
7267                    let handled = config_options_view.update(cx, |view, cx| {
7268                        view.toggle_category_picker(
7269                            acp::SessionConfigOptionCategory::Model,
7270                            window,
7271                            cx,
7272                        )
7273                    });
7274                    if handled {
7275                        return;
7276                    }
7277                }
7278
7279                if let Some(model_selector) = this.model_selector.clone() {
7280                    model_selector
7281                        .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
7282                }
7283            }))
7284            .on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
7285                if let Some(config_options_view) = this.config_options_view.clone() {
7286                    let handled = config_options_view.update(cx, |view, cx| {
7287                        view.cycle_category_option(
7288                            acp::SessionConfigOptionCategory::Model,
7289                            true,
7290                            cx,
7291                        )
7292                    });
7293                    if handled {
7294                        return;
7295                    }
7296                }
7297
7298                if let Some(model_selector) = this.model_selector.clone() {
7299                    model_selector.update(cx, |model_selector, cx| {
7300                        model_selector.cycle_favorite_models(window, cx);
7301                    });
7302                }
7303            }))
7304            .size_full()
7305            .children(self.render_subagent_titlebar(cx))
7306            .child(conversation)
7307            .children(self.render_activity_bar(window, cx))
7308            .when(self.show_codex_windows_warning, |this| {
7309                this.child(self.render_codex_windows_warning(cx))
7310            })
7311            .children(self.render_thread_retry_status_callout())
7312            .children(self.render_thread_error(window, cx))
7313            .when_some(
7314                match has_messages {
7315                    true => None,
7316                    false => self.new_server_version_available.clone(),
7317                },
7318                |this, version| this.child(self.render_new_version_callout(&version, cx)),
7319            )
7320            .children(self.render_token_limit_callout(cx))
7321            .child(self.render_message_editor(window, cx))
7322    }
7323}
7324
7325pub(crate) fn open_link(
7326    url: SharedString,
7327    workspace: &WeakEntity<Workspace>,
7328    window: &mut Window,
7329    cx: &mut App,
7330) {
7331    let Some(workspace) = workspace.upgrade() else {
7332        cx.open_url(&url);
7333        return;
7334    };
7335
7336    if let Some(mention) = MentionUri::parse(&url, workspace.read(cx).path_style(cx)).log_err() {
7337        workspace.update(cx, |workspace, cx| match mention {
7338            MentionUri::File { abs_path } => {
7339                let project = workspace.project();
7340                let Some(path) =
7341                    project.update(cx, |project, cx| project.find_project_path(abs_path, cx))
7342                else {
7343                    return;
7344                };
7345
7346                workspace
7347                    .open_path(path, None, true, window, cx)
7348                    .detach_and_log_err(cx);
7349            }
7350            MentionUri::PastedImage => {}
7351            MentionUri::Directory { abs_path } => {
7352                let project = workspace.project();
7353                let Some(entry_id) = project.update(cx, |project, cx| {
7354                    let path = project.find_project_path(abs_path, cx)?;
7355                    project.entry_for_path(&path, cx).map(|entry| entry.id)
7356                }) else {
7357                    return;
7358                };
7359
7360                project.update(cx, |_, cx| {
7361                    cx.emit(project::Event::RevealInProjectPanel(entry_id));
7362                });
7363            }
7364            MentionUri::Symbol {
7365                abs_path: path,
7366                line_range,
7367                ..
7368            }
7369            | MentionUri::Selection {
7370                abs_path: Some(path),
7371                line_range,
7372            } => {
7373                let project = workspace.project();
7374                let Some(path) =
7375                    project.update(cx, |project, cx| project.find_project_path(path, cx))
7376                else {
7377                    return;
7378                };
7379
7380                let item = workspace.open_path(path, None, true, window, cx);
7381                window
7382                    .spawn(cx, async move |cx| {
7383                        let Some(editor) = item.await?.downcast::<Editor>() else {
7384                            return Ok(());
7385                        };
7386                        let range =
7387                            Point::new(*line_range.start(), 0)..Point::new(*line_range.start(), 0);
7388                        editor
7389                            .update_in(cx, |editor, window, cx| {
7390                                editor.change_selections(
7391                                    SelectionEffects::scroll(Autoscroll::center()),
7392                                    window,
7393                                    cx,
7394                                    |s| s.select_ranges(vec![range]),
7395                                );
7396                            })
7397                            .ok();
7398                        anyhow::Ok(())
7399                    })
7400                    .detach_and_log_err(cx);
7401            }
7402            MentionUri::Selection { abs_path: None, .. } => {}
7403            MentionUri::Thread { id, name } => {
7404                if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
7405                    panel.update(cx, |panel, cx| {
7406                        panel.open_thread(
7407                            AgentSessionInfo {
7408                                session_id: id,
7409                                cwd: None,
7410                                title: Some(name.into()),
7411                                updated_at: None,
7412                                meta: None,
7413                            },
7414                            window,
7415                            cx,
7416                        )
7417                    });
7418                }
7419            }
7420            MentionUri::TextThread { path, .. } => {
7421                if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
7422                    panel.update(cx, |panel, cx| {
7423                        panel
7424                            .open_saved_text_thread(path.as_path().into(), window, cx)
7425                            .detach_and_log_err(cx);
7426                    });
7427                }
7428            }
7429            MentionUri::Rule { id, .. } => {
7430                let PromptId::User { uuid } = id else {
7431                    return;
7432                };
7433                window.dispatch_action(
7434                    Box::new(OpenRulesLibrary {
7435                        prompt_to_select: Some(uuid.0),
7436                    }),
7437                    cx,
7438                )
7439            }
7440            MentionUri::Fetch { url } => {
7441                cx.open_url(url.as_str());
7442            }
7443            MentionUri::Diagnostics { .. } => {}
7444            MentionUri::TerminalSelection { .. } => {}
7445        })
7446    } else {
7447        cx.open_url(&url);
7448    }
7449}