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