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