active_thread.rs

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