active_thread.rs

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