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