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