active_thread.rs

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