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