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 confirmation_options = match &tool_call.status {
4483            ToolCallStatus::WaitingForConfirmation { options, .. } => Some(options),
4484            _ => None,
4485        };
4486        let needs_confirmation = confirmation_options.is_some();
4487
4488        let output = terminal_data.output();
4489        let command_finished = output.is_some();
4490        let truncated_output =
4491            output.is_some_and(|output| output.original_content_len > output.content.len());
4492        let output_line_count = output.map(|output| output.content_line_count).unwrap_or(0);
4493
4494        let command_failed = command_finished
4495            && output.is_some_and(|o| o.exit_status.is_some_and(|status| !status.success()));
4496
4497        let time_elapsed = if let Some(output) = output {
4498            output.ended_at.duration_since(started_at)
4499        } else {
4500            started_at.elapsed()
4501        };
4502
4503        let header_id =
4504            SharedString::from(format!("terminal-tool-header-{}", terminal.entity_id()));
4505        let header_group = SharedString::from(format!(
4506            "terminal-tool-header-group-{}",
4507            terminal.entity_id()
4508        ));
4509        let header_bg = cx
4510            .theme()
4511            .colors()
4512            .element_background
4513            .blend(cx.theme().colors().editor_foreground.opacity(0.025));
4514        let border_color = cx.theme().colors().border.opacity(0.6);
4515
4516        let working_dir = working_dir
4517            .as_ref()
4518            .map(|path| path.display().to_string())
4519            .unwrap_or_else(|| "current directory".to_string());
4520
4521        // Since the command's source is wrapped in a markdown code block
4522        // (```\n...\n```), we need to strip that so we're left with only the
4523        // command's content.
4524        let command_source = command.read(cx).source();
4525        let command_content = command_source
4526            .strip_prefix("```\n")
4527            .and_then(|s| s.strip_suffix("\n```"))
4528            .unwrap_or(&command_source);
4529
4530        let command_element =
4531            self.render_collapsible_command(false, command_content, &tool_call.id, cx);
4532
4533        let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
4534
4535        let header = h_flex()
4536            .id(header_id)
4537            .px_1p5()
4538            .pt_1()
4539            .flex_none()
4540            .gap_1()
4541            .justify_between()
4542            .rounded_t_md()
4543            .child(
4544                div()
4545                    .id(("command-target-path", terminal.entity_id()))
4546                    .w_full()
4547                    .max_w_full()
4548                    .overflow_x_scroll()
4549                    .child(
4550                        Label::new(working_dir)
4551                            .buffer_font(cx)
4552                            .size(LabelSize::XSmall)
4553                            .color(Color::Muted),
4554                    ),
4555            )
4556            .when(!command_finished && !needs_confirmation, |header| {
4557                header
4558                    .gap_1p5()
4559                    .child(
4560                        Button::new(
4561                            SharedString::from(format!("stop-terminal-{}", terminal.entity_id())),
4562                            "Stop",
4563                        )
4564                        .icon(IconName::Stop)
4565                        .icon_position(IconPosition::Start)
4566                        .icon_size(IconSize::Small)
4567                        .icon_color(Color::Error)
4568                        .label_size(LabelSize::Small)
4569                        .tooltip(move |_window, cx| {
4570                            Tooltip::with_meta(
4571                                "Stop This Command",
4572                                None,
4573                                "Also possible by placing your cursor inside the terminal and using regular terminal bindings.",
4574                                cx,
4575                            )
4576                        })
4577                        .on_click({
4578                            let terminal = terminal.clone();
4579                            cx.listener(move |this, _event, _window, cx| {
4580                                terminal.update(cx, |terminal, cx| {
4581                                    terminal.stop_by_user(cx);
4582                                });
4583                                if AgentSettings::get_global(cx).cancel_generation_on_terminal_stop {
4584                                    this.cancel_generation(cx);
4585                                }
4586                            })
4587                        }),
4588                    )
4589                    .child(Divider::vertical())
4590                    .child(
4591                        Icon::new(IconName::ArrowCircle)
4592                            .size(IconSize::XSmall)
4593                            .color(Color::Info)
4594                            .with_rotate_animation(2)
4595                    )
4596            })
4597            .when(truncated_output, |header| {
4598                let tooltip = if let Some(output) = output {
4599                    if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
4600                       format!("Output exceeded terminal max lines and was \
4601                            truncated, the model received the first {}.", format_file_size(output.content.len() as u64, true))
4602                    } else {
4603                        format!(
4604                            "Output is {} long, and to avoid unexpected token usage, \
4605                                only {} was sent back to the agent.",
4606                            format_file_size(output.original_content_len as u64, true),
4607                             format_file_size(output.content.len() as u64, true)
4608                        )
4609                    }
4610                } else {
4611                    "Output was truncated".to_string()
4612                };
4613
4614                header.child(
4615                    h_flex()
4616                        .id(("terminal-tool-truncated-label", terminal.entity_id()))
4617                        .gap_1()
4618                        .child(
4619                            Icon::new(IconName::Info)
4620                                .size(IconSize::XSmall)
4621                                .color(Color::Ignored),
4622                        )
4623                        .child(
4624                            Label::new("Truncated")
4625                                .color(Color::Muted)
4626                                .size(LabelSize::XSmall),
4627                        )
4628                        .tooltip(Tooltip::text(tooltip)),
4629                )
4630            })
4631            .when(time_elapsed > Duration::from_secs(10), |header| {
4632                header.child(
4633                    Label::new(format!("({})", duration_alt_display(time_elapsed)))
4634                        .buffer_font(cx)
4635                        .color(Color::Muted)
4636                        .size(LabelSize::XSmall),
4637                )
4638            })
4639            .when(tool_failed || command_failed, |header| {
4640                header.child(
4641                    div()
4642                        .id(("terminal-tool-error-code-indicator", terminal.entity_id()))
4643                        .child(
4644                            Icon::new(IconName::Close)
4645                                .size(IconSize::Small)
4646                                .color(Color::Error),
4647                        )
4648                        .when_some(output.and_then(|o| o.exit_status), |this, status| {
4649                            this.tooltip(Tooltip::text(format!(
4650                                "Exited with code {}",
4651                                status.code().unwrap_or(-1),
4652                            )))
4653                        }),
4654                )
4655            })
4656            .child(
4657                Disclosure::new(
4658                    SharedString::from(format!(
4659                        "terminal-tool-disclosure-{}",
4660                        terminal.entity_id()
4661                    )),
4662                    is_expanded,
4663                )
4664                .opened_icon(IconName::ChevronUp)
4665                .closed_icon(IconName::ChevronDown)
4666                .visible_on_hover(&header_group)
4667                .on_click(cx.listener({
4668                    let id = tool_call.id.clone();
4669                    move |this, _event, _window, cx| {
4670                        if is_expanded {
4671                            this.expanded_tool_calls.remove(&id);
4672                        } else {
4673                            this.expanded_tool_calls.insert(id.clone());
4674                        }
4675                        cx.notify();
4676                    }
4677                })),
4678            );
4679
4680        let terminal_view = self
4681            .entry_view_state
4682            .read(cx)
4683            .entry(entry_ix)
4684            .and_then(|entry| entry.terminal(terminal));
4685
4686        v_flex()
4687            .my_1p5()
4688            .mx_5()
4689            .border_1()
4690            .when(tool_failed || command_failed, |card| card.border_dashed())
4691            .border_color(border_color)
4692            .rounded_md()
4693            .overflow_hidden()
4694            .child(
4695                v_flex()
4696                    .group(&header_group)
4697                    .bg(header_bg)
4698                    .text_xs()
4699                    .child(header)
4700                    .child(command_element),
4701            )
4702            .when(is_expanded && terminal_view.is_some(), |this| {
4703                this.child(
4704                    div()
4705                        .pt_2()
4706                        .border_t_1()
4707                        .when(tool_failed || command_failed, |card| card.border_dashed())
4708                        .border_color(border_color)
4709                        .bg(cx.theme().colors().editor_background)
4710                        .rounded_b_md()
4711                        .text_ui_sm(cx)
4712                        .h_full()
4713                        .children(terminal_view.map(|terminal_view| {
4714                            let element = if terminal_view
4715                                .read(cx)
4716                                .content_mode(window, cx)
4717                                .is_scrollable()
4718                            {
4719                                div().h_72().child(terminal_view).into_any_element()
4720                            } else {
4721                                terminal_view.into_any_element()
4722                            };
4723
4724                            div()
4725                                .on_action(cx.listener(|_this, _: &NewTerminal, window, cx| {
4726                                    window.dispatch_action(NewThread.boxed_clone(), cx);
4727                                    cx.stop_propagation();
4728                                }))
4729                                .child(element)
4730                                .into_any_element()
4731                        })),
4732                )
4733            })
4734            .when_some(confirmation_options, |this, options| {
4735                this.child(self.render_permission_buttons(
4736                    options,
4737                    entry_ix,
4738                    tool_call.id.clone(),
4739                    cx,
4740                ))
4741            })
4742            .into_any()
4743    }
4744
4745    fn render_tool_call(
4746        &self,
4747        entry_ix: usize,
4748        tool_call: &ToolCall,
4749        window: &Window,
4750        cx: &Context<Self>,
4751    ) -> Div {
4752        let has_location = tool_call.locations.len() == 1;
4753        let card_header_id = SharedString::from("inner-tool-call-header");
4754
4755        let failed_or_canceled = match &tool_call.status {
4756            ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => true,
4757            _ => false,
4758        };
4759
4760        let needs_confirmation = matches!(
4761            tool_call.status,
4762            ToolCallStatus::WaitingForConfirmation { .. }
4763        );
4764        let is_terminal_tool = matches!(tool_call.kind, acp::ToolKind::Execute);
4765
4766        let is_edit =
4767            matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some();
4768
4769        // For subagent tool calls, render the subagent cards directly without wrapper
4770        if tool_call.is_subagent() {
4771            return self.render_subagent_tool_call(
4772                entry_ix,
4773                tool_call,
4774                tool_call.subagent_session_id.clone(),
4775                window,
4776                cx,
4777            );
4778        }
4779
4780        let is_cancelled_edit = is_edit && matches!(tool_call.status, ToolCallStatus::Canceled);
4781        let has_revealed_diff = tool_call.diffs().next().is_some_and(|diff| {
4782            self.entry_view_state
4783                .read(cx)
4784                .entry(entry_ix)
4785                .and_then(|entry| entry.editor_for_diff(diff))
4786                .is_some()
4787                && diff.read(cx).has_revealed_range(cx)
4788        });
4789
4790        let use_card_layout = needs_confirmation || is_edit || is_terminal_tool;
4791
4792        let has_image_content = tool_call.content.iter().any(|c| c.image().is_some());
4793        let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
4794        let mut is_open = self.expanded_tool_calls.contains(&tool_call.id);
4795
4796        is_open |= needs_confirmation;
4797
4798        let should_show_raw_input = !is_terminal_tool && !is_edit && !has_image_content;
4799
4800        let input_output_header = |label: SharedString| {
4801            Label::new(label)
4802                .size(LabelSize::XSmall)
4803                .color(Color::Muted)
4804                .buffer_font(cx)
4805        };
4806
4807        let tool_output_display = if is_open {
4808            match &tool_call.status {
4809                ToolCallStatus::WaitingForConfirmation { options, .. } => v_flex()
4810                    .w_full()
4811                    .children(
4812                        tool_call
4813                            .content
4814                            .iter()
4815                            .enumerate()
4816                            .map(|(content_ix, content)| {
4817                                div()
4818                                    .child(self.render_tool_call_content(
4819                                        entry_ix,
4820                                        content,
4821                                        content_ix,
4822                                        tool_call,
4823                                        use_card_layout,
4824                                        has_image_content,
4825                                        failed_or_canceled,
4826                                        window,
4827                                        cx,
4828                                    ))
4829                                    .into_any_element()
4830                            }),
4831                    )
4832                    .when(should_show_raw_input, |this| {
4833                        let is_raw_input_expanded =
4834                            self.expanded_tool_call_raw_inputs.contains(&tool_call.id);
4835
4836                        let input_header = if is_raw_input_expanded {
4837                            "Raw Input:"
4838                        } else {
4839                            "View Raw Input"
4840                        };
4841
4842                        this.child(
4843                            v_flex()
4844                                .p_2()
4845                                .gap_1()
4846                                .border_t_1()
4847                                .border_color(self.tool_card_border_color(cx))
4848                                .child(
4849                                    h_flex()
4850                                        .id("disclosure_container")
4851                                        .pl_0p5()
4852                                        .gap_1()
4853                                        .justify_between()
4854                                        .rounded_xs()
4855                                        .hover(|s| s.bg(cx.theme().colors().element_hover))
4856                                        .child(input_output_header(input_header.into()))
4857                                        .child(
4858                                            Disclosure::new(
4859                                                ("raw-input-disclosure", entry_ix),
4860                                                is_raw_input_expanded,
4861                                            )
4862                                            .opened_icon(IconName::ChevronUp)
4863                                            .closed_icon(IconName::ChevronDown),
4864                                        )
4865                                        .on_click(cx.listener({
4866                                            let id = tool_call.id.clone();
4867
4868                                            move |this: &mut Self, _, _, cx| {
4869                                                if this.expanded_tool_call_raw_inputs.contains(&id)
4870                                                {
4871                                                    this.expanded_tool_call_raw_inputs.remove(&id);
4872                                                } else {
4873                                                    this.expanded_tool_call_raw_inputs
4874                                                        .insert(id.clone());
4875                                                }
4876                                                cx.notify();
4877                                            }
4878                                        })),
4879                                )
4880                                .when(is_raw_input_expanded, |this| {
4881                                    this.children(tool_call.raw_input_markdown.clone().map(
4882                                        |input| {
4883                                            self.render_markdown(
4884                                                input,
4885                                                MarkdownStyle::themed(
4886                                                    MarkdownFont::Agent,
4887                                                    window,
4888                                                    cx,
4889                                                ),
4890                                            )
4891                                        },
4892                                    ))
4893                                }),
4894                        )
4895                    })
4896                    .child(self.render_permission_buttons(
4897                        options,
4898                        entry_ix,
4899                        tool_call.id.clone(),
4900                        cx,
4901                    ))
4902                    .into_any(),
4903                ToolCallStatus::Pending | ToolCallStatus::InProgress
4904                    if is_edit
4905                        && tool_call.content.is_empty()
4906                        && self.as_native_connection(cx).is_some() =>
4907                {
4908                    self.render_diff_loading(cx)
4909                }
4910                ToolCallStatus::Pending
4911                | ToolCallStatus::InProgress
4912                | ToolCallStatus::Completed
4913                | ToolCallStatus::Failed
4914                | ToolCallStatus::Canceled => v_flex()
4915                    .when(should_show_raw_input, |this| {
4916                        this.mt_1p5().w_full().child(
4917                            v_flex()
4918                                .ml(rems(0.4))
4919                                .px_3p5()
4920                                .pb_1()
4921                                .gap_1()
4922                                .border_l_1()
4923                                .border_color(self.tool_card_border_color(cx))
4924                                .child(input_output_header("Raw Input:".into()))
4925                                .children(tool_call.raw_input_markdown.clone().map(|input| {
4926                                    div().id(("tool-call-raw-input-markdown", entry_ix)).child(
4927                                        self.render_markdown(
4928                                            input,
4929                                            MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
4930                                        ),
4931                                    )
4932                                }))
4933                                .child(input_output_header("Output:".into())),
4934                        )
4935                    })
4936                    .children(
4937                        tool_call
4938                            .content
4939                            .iter()
4940                            .enumerate()
4941                            .map(|(content_ix, content)| {
4942                                div().id(("tool-call-output", entry_ix)).child(
4943                                    self.render_tool_call_content(
4944                                        entry_ix,
4945                                        content,
4946                                        content_ix,
4947                                        tool_call,
4948                                        use_card_layout,
4949                                        has_image_content,
4950                                        failed_or_canceled,
4951                                        window,
4952                                        cx,
4953                                    ),
4954                                )
4955                            }),
4956                    )
4957                    .into_any(),
4958                ToolCallStatus::Rejected => Empty.into_any(),
4959            }
4960            .into()
4961        } else {
4962            None
4963        };
4964
4965        v_flex()
4966            .map(|this| {
4967                if use_card_layout {
4968                    this.my_1p5()
4969                        .rounded_md()
4970                        .border_1()
4971                        .when(failed_or_canceled, |this| this.border_dashed())
4972                        .border_color(self.tool_card_border_color(cx))
4973                        .bg(cx.theme().colors().editor_background)
4974                        .overflow_hidden()
4975                } else {
4976                    this.my_1()
4977                }
4978            })
4979            .map(|this| {
4980                if has_location && !use_card_layout {
4981                    this.ml_4()
4982                } else {
4983                    this.ml_5()
4984                }
4985            })
4986            .mr_5()
4987            .map(|this| {
4988                if is_terminal_tool {
4989                    let label_source = tool_call.label.read(cx).source();
4990                    this.child(self.render_collapsible_command(true, label_source, &tool_call.id, cx))
4991                } else {
4992                    this.child(
4993                        h_flex()
4994                            .group(&card_header_id)
4995                            .relative()
4996                            .w_full()
4997                            .gap_1()
4998                            .justify_between()
4999                            .when(use_card_layout, |this| {
5000                                this.p_0p5()
5001                                    .rounded_t(rems_from_px(5.))
5002                                    .bg(self.tool_card_header_bg(cx))
5003                            })
5004                            .child(self.render_tool_call_label(
5005                                entry_ix,
5006                                tool_call,
5007                                is_edit,
5008                                is_cancelled_edit,
5009                                has_revealed_diff,
5010                                use_card_layout,
5011                                window,
5012                                cx,
5013                            ))
5014                            .when(is_collapsible || failed_or_canceled, |this| {
5015                                let diff_for_discard =
5016                                    if has_revealed_diff && is_cancelled_edit && cx.has_flag::<AgentV2FeatureFlag>() {
5017                                        tool_call.diffs().next().cloned()
5018                                    } else {
5019                                        None
5020                                    };
5021                                this.child(
5022                                    h_flex()
5023                                        .px_1()
5024                                        .when_some(diff_for_discard.clone(), |this, _| this.pr_0p5())
5025                                        .gap_1()
5026                                        .when(is_collapsible, |this| {
5027                                            this.child(
5028                                            Disclosure::new(("expand-output", entry_ix), is_open)
5029                                                .opened_icon(IconName::ChevronUp)
5030                                                .closed_icon(IconName::ChevronDown)
5031                                                .visible_on_hover(&card_header_id)
5032                                                .on_click(cx.listener({
5033                                                    let id = tool_call.id.clone();
5034                                                    move |this: &mut Self, _, _, cx: &mut Context<Self>| {
5035                                                                if is_open {
5036                                                                    this
5037                                                                        .expanded_tool_calls.remove(&id);
5038                                                                } else {
5039                                                                    this.expanded_tool_calls.insert(id.clone());
5040                                                                }
5041                                                            cx.notify();
5042                                                    }
5043                                                })),
5044                                        )
5045                                        })
5046                                        .when(failed_or_canceled, |this| {
5047                                            if is_cancelled_edit && !has_revealed_diff {
5048                                                this.child(
5049                                                    div()
5050                                                        .id(entry_ix)
5051                                                        .tooltip(Tooltip::text(
5052                                                            "Interrupted Edit",
5053                                                        ))
5054                                                        .child(
5055                                                            Icon::new(IconName::XCircle)
5056                                                                .color(Color::Muted)
5057                                                                .size(IconSize::Small),
5058                                                        ),
5059                                                )
5060                                            } else if is_cancelled_edit {
5061                                                this
5062                                            } else {
5063                                                this.child(
5064                                                    Icon::new(IconName::Close)
5065                                                        .color(Color::Error)
5066                                                        .size(IconSize::Small),
5067                                                )
5068                                            }
5069                                        })
5070                                        .when_some(diff_for_discard, |this, diff| {
5071                                            let tool_call_id = tool_call.id.clone();
5072                                            let is_discarded = self.discarded_partial_edits.contains(&tool_call_id);
5073                                            this.when(!is_discarded, |this| {
5074                                                this.child(
5075                                                    IconButton::new(
5076                                                        ("discard-partial-edit", entry_ix),
5077                                                        IconName::Undo,
5078                                                    )
5079                                                    .icon_size(IconSize::Small)
5080                                                    .tooltip(move |_, cx| Tooltip::with_meta(
5081                                                        "Discard Interrupted Edit",
5082                                                        None,
5083                                                        "You can discard this interrupted partial edit and restore the original file content.",
5084                                                        cx
5085                                                    ))
5086                                                    .on_click(cx.listener({
5087                                                        let tool_call_id = tool_call_id.clone();
5088                                                        move |this, _, _window, cx| {
5089                                                            let diff_data = diff.read(cx);
5090                                                            let base_text = diff_data.base_text().clone();
5091                                                            let buffer = diff_data.buffer().clone();
5092                                                            buffer.update(cx, |buffer, cx| {
5093                                                                buffer.set_text(base_text.as_ref(), cx);
5094                                                            });
5095                                                            this.discarded_partial_edits.insert(tool_call_id.clone());
5096                                                            cx.notify();
5097                                                        }
5098                                                    })),
5099                                                )
5100                                            })
5101                                        })
5102
5103                                )
5104                            }),
5105                    )
5106                }
5107            })
5108            .children(tool_output_display)
5109    }
5110
5111    fn render_permission_buttons(
5112        &self,
5113        options: &PermissionOptions,
5114        entry_ix: usize,
5115        tool_call_id: acp::ToolCallId,
5116        cx: &Context<Self>,
5117    ) -> Div {
5118        match options {
5119            PermissionOptions::Flat(options) => {
5120                self.render_permission_buttons_flat(options, entry_ix, tool_call_id, cx)
5121            }
5122            PermissionOptions::Dropdown(options) => {
5123                self.render_permission_buttons_dropdown(options, entry_ix, tool_call_id, cx)
5124            }
5125        }
5126    }
5127
5128    fn render_permission_buttons_dropdown(
5129        &self,
5130        choices: &[PermissionOptionChoice],
5131        entry_ix: usize,
5132        tool_call_id: acp::ToolCallId,
5133        cx: &Context<Self>,
5134    ) -> Div {
5135        let is_first = self
5136            .thread
5137            .read(cx)
5138            .first_tool_awaiting_confirmation()
5139            .is_some_and(|call| call.id == tool_call_id);
5140
5141        // Get the selected granularity index, defaulting to the last option ("Only this time")
5142        let selected_index = self
5143            .selected_permission_granularity
5144            .get(&tool_call_id)
5145            .copied()
5146            .unwrap_or_else(|| choices.len().saturating_sub(1));
5147
5148        let selected_choice = choices.get(selected_index).or(choices.last());
5149
5150        let dropdown_label: SharedString = selected_choice
5151            .map(|choice| choice.label())
5152            .unwrap_or_else(|| "Only this time".into());
5153
5154        let (allow_option_id, allow_option_kind, deny_option_id, deny_option_kind) =
5155            if let Some(choice) = selected_choice {
5156                (
5157                    choice.allow.option_id.clone(),
5158                    choice.allow.kind,
5159                    choice.deny.option_id.clone(),
5160                    choice.deny.kind,
5161                )
5162            } else {
5163                (
5164                    acp::PermissionOptionId::new("allow"),
5165                    acp::PermissionOptionKind::AllowOnce,
5166                    acp::PermissionOptionId::new("deny"),
5167                    acp::PermissionOptionKind::RejectOnce,
5168                )
5169            };
5170
5171        h_flex()
5172            .w_full()
5173            .p_1()
5174            .gap_2()
5175            .justify_between()
5176            .border_t_1()
5177            .border_color(self.tool_card_border_color(cx))
5178            .child(
5179                h_flex()
5180                    .gap_0p5()
5181                    .child(
5182                        Button::new(("allow-btn", entry_ix), "Allow")
5183                            .icon(IconName::Check)
5184                            .icon_color(Color::Success)
5185                            .icon_position(IconPosition::Start)
5186                            .icon_size(IconSize::XSmall)
5187                            .label_size(LabelSize::Small)
5188                            .when(is_first, |this| {
5189                                this.key_binding(
5190                                    KeyBinding::for_action_in(
5191                                        &AllowOnce as &dyn Action,
5192                                        &self.focus_handle(cx),
5193                                        cx,
5194                                    )
5195                                    .map(|kb| kb.size(rems_from_px(10.))),
5196                                )
5197                            })
5198                            .on_click(cx.listener({
5199                                let tool_call_id = tool_call_id.clone();
5200                                let option_id = allow_option_id;
5201                                let option_kind = allow_option_kind;
5202                                move |this, _, window, cx| {
5203                                    this.authorize_tool_call(
5204                                        tool_call_id.clone(),
5205                                        option_id.clone(),
5206                                        option_kind,
5207                                        window,
5208                                        cx,
5209                                    );
5210                                }
5211                            })),
5212                    )
5213                    .child(
5214                        Button::new(("deny-btn", entry_ix), "Deny")
5215                            .icon(IconName::Close)
5216                            .icon_color(Color::Error)
5217                            .icon_position(IconPosition::Start)
5218                            .icon_size(IconSize::XSmall)
5219                            .label_size(LabelSize::Small)
5220                            .when(is_first, |this| {
5221                                this.key_binding(
5222                                    KeyBinding::for_action_in(
5223                                        &RejectOnce as &dyn Action,
5224                                        &self.focus_handle(cx),
5225                                        cx,
5226                                    )
5227                                    .map(|kb| kb.size(rems_from_px(10.))),
5228                                )
5229                            })
5230                            .on_click(cx.listener({
5231                                let tool_call_id = tool_call_id.clone();
5232                                let option_id = deny_option_id;
5233                                let option_kind = deny_option_kind;
5234                                move |this, _, window, cx| {
5235                                    this.authorize_tool_call(
5236                                        tool_call_id.clone(),
5237                                        option_id.clone(),
5238                                        option_kind,
5239                                        window,
5240                                        cx,
5241                                    );
5242                                }
5243                            })),
5244                    ),
5245            )
5246            .child(self.render_permission_granularity_dropdown(
5247                choices,
5248                dropdown_label,
5249                entry_ix,
5250                tool_call_id,
5251                selected_index,
5252                is_first,
5253                cx,
5254            ))
5255    }
5256
5257    fn render_permission_granularity_dropdown(
5258        &self,
5259        choices: &[PermissionOptionChoice],
5260        current_label: SharedString,
5261        entry_ix: usize,
5262        tool_call_id: acp::ToolCallId,
5263        selected_index: usize,
5264        is_first: bool,
5265        cx: &Context<Self>,
5266    ) -> AnyElement {
5267        let menu_options: Vec<(usize, SharedString)> = choices
5268            .iter()
5269            .enumerate()
5270            .map(|(i, choice)| (i, choice.label()))
5271            .collect();
5272
5273        let permission_dropdown_handle = self.permission_dropdown_handle.clone();
5274
5275        PopoverMenu::new(("permission-granularity", entry_ix))
5276            .with_handle(permission_dropdown_handle)
5277            .trigger(
5278                Button::new(("granularity-trigger", entry_ix), current_label)
5279                    .icon(IconName::ChevronDown)
5280                    .icon_size(IconSize::XSmall)
5281                    .icon_color(Color::Muted)
5282                    .label_size(LabelSize::Small)
5283                    .when(is_first, |this| {
5284                        this.key_binding(
5285                            KeyBinding::for_action_in(
5286                                &crate::OpenPermissionDropdown as &dyn Action,
5287                                &self.focus_handle(cx),
5288                                cx,
5289                            )
5290                            .map(|kb| kb.size(rems_from_px(10.))),
5291                        )
5292                    }),
5293            )
5294            .menu(move |window, cx| {
5295                let tool_call_id = tool_call_id.clone();
5296                let options = menu_options.clone();
5297
5298                Some(ContextMenu::build(window, cx, move |mut menu, _, _| {
5299                    for (index, display_name) in options.iter() {
5300                        let display_name = display_name.clone();
5301                        let index = *index;
5302                        let tool_call_id_for_entry = tool_call_id.clone();
5303                        let is_selected = index == selected_index;
5304
5305                        menu = menu.toggleable_entry(
5306                            display_name,
5307                            is_selected,
5308                            IconPosition::End,
5309                            None,
5310                            move |window, cx| {
5311                                window.dispatch_action(
5312                                    SelectPermissionGranularity {
5313                                        tool_call_id: tool_call_id_for_entry.0.to_string(),
5314                                        index,
5315                                    }
5316                                    .boxed_clone(),
5317                                    cx,
5318                                );
5319                            },
5320                        );
5321                    }
5322
5323                    menu
5324                }))
5325            })
5326            .into_any_element()
5327    }
5328
5329    fn render_permission_buttons_flat(
5330        &self,
5331        options: &[acp::PermissionOption],
5332        entry_ix: usize,
5333        tool_call_id: acp::ToolCallId,
5334        cx: &Context<Self>,
5335    ) -> Div {
5336        let is_first = self
5337            .thread
5338            .read(cx)
5339            .first_tool_awaiting_confirmation()
5340            .is_some_and(|call| call.id == tool_call_id);
5341        let mut seen_kinds: ArrayVec<acp::PermissionOptionKind, 3> = ArrayVec::new();
5342
5343        div()
5344            .p_1()
5345            .border_t_1()
5346            .border_color(self.tool_card_border_color(cx))
5347            .w_full()
5348            .v_flex()
5349            .gap_0p5()
5350            .children(options.iter().map(move |option| {
5351                let option_id = SharedString::from(option.option_id.0.clone());
5352                Button::new((option_id, entry_ix), option.name.clone())
5353                    .map(|this| {
5354                        let (this, action) = match option.kind {
5355                            acp::PermissionOptionKind::AllowOnce => (
5356                                this.icon(IconName::Check).icon_color(Color::Success),
5357                                Some(&AllowOnce as &dyn Action),
5358                            ),
5359                            acp::PermissionOptionKind::AllowAlways => (
5360                                this.icon(IconName::CheckDouble).icon_color(Color::Success),
5361                                Some(&AllowAlways as &dyn Action),
5362                            ),
5363                            acp::PermissionOptionKind::RejectOnce => (
5364                                this.icon(IconName::Close).icon_color(Color::Error),
5365                                Some(&RejectOnce as &dyn Action),
5366                            ),
5367                            acp::PermissionOptionKind::RejectAlways | _ => {
5368                                (this.icon(IconName::Close).icon_color(Color::Error), None)
5369                            }
5370                        };
5371
5372                        let Some(action) = action else {
5373                            return this;
5374                        };
5375
5376                        if !is_first || seen_kinds.contains(&option.kind) {
5377                            return this;
5378                        }
5379
5380                        seen_kinds.push(option.kind);
5381
5382                        this.key_binding(
5383                            KeyBinding::for_action_in(action, &self.focus_handle(cx), cx)
5384                                .map(|kb| kb.size(rems_from_px(10.))),
5385                        )
5386                    })
5387                    .icon_position(IconPosition::Start)
5388                    .icon_size(IconSize::XSmall)
5389                    .label_size(LabelSize::Small)
5390                    .on_click(cx.listener({
5391                        let tool_call_id = tool_call_id.clone();
5392                        let option_id = option.option_id.clone();
5393                        let option_kind = option.kind;
5394                        move |this, _, window, cx| {
5395                            this.authorize_tool_call(
5396                                tool_call_id.clone(),
5397                                option_id.clone(),
5398                                option_kind,
5399                                window,
5400                                cx,
5401                            );
5402                        }
5403                    }))
5404            }))
5405    }
5406
5407    fn render_diff_loading(&self, cx: &Context<Self>) -> AnyElement {
5408        let bar = |n: u64, width_class: &str| {
5409            let bg_color = cx.theme().colors().element_active;
5410            let base = h_flex().h_1().rounded_full();
5411
5412            let modified = match width_class {
5413                "w_4_5" => base.w_3_4(),
5414                "w_1_4" => base.w_1_4(),
5415                "w_2_4" => base.w_2_4(),
5416                "w_3_5" => base.w_3_5(),
5417                "w_2_5" => base.w_2_5(),
5418                _ => base.w_1_2(),
5419            };
5420
5421            modified.with_animation(
5422                ElementId::Integer(n),
5423                Animation::new(Duration::from_secs(2)).repeat(),
5424                move |tab, delta| {
5425                    let delta = (delta - 0.15 * n as f32) / 0.7;
5426                    let delta = 1.0 - (0.5 - delta).abs() * 2.;
5427                    let delta = ease_in_out(delta.clamp(0., 1.));
5428                    let delta = 0.1 + 0.9 * delta;
5429
5430                    tab.bg(bg_color.opacity(delta))
5431                },
5432            )
5433        };
5434
5435        v_flex()
5436            .p_3()
5437            .gap_1()
5438            .rounded_b_md()
5439            .bg(cx.theme().colors().editor_background)
5440            .child(bar(0, "w_4_5"))
5441            .child(bar(1, "w_1_4"))
5442            .child(bar(2, "w_2_4"))
5443            .child(bar(3, "w_3_5"))
5444            .child(bar(4, "w_2_5"))
5445            .into_any_element()
5446    }
5447
5448    fn render_tool_call_label(
5449        &self,
5450        entry_ix: usize,
5451        tool_call: &ToolCall,
5452        is_edit: bool,
5453        has_failed: bool,
5454        has_revealed_diff: bool,
5455        use_card_layout: bool,
5456        window: &Window,
5457        cx: &Context<Self>,
5458    ) -> Div {
5459        let has_location = tool_call.locations.len() == 1;
5460        let is_file = tool_call.kind == acp::ToolKind::Edit && has_location;
5461        let is_subagent_tool_call = tool_call.is_subagent();
5462
5463        let file_icon = if has_location {
5464            FileIcons::get_icon(&tool_call.locations[0].path, cx)
5465                .map(Icon::from_path)
5466                .unwrap_or(Icon::new(IconName::ToolPencil))
5467        } else {
5468            Icon::new(IconName::ToolPencil)
5469        };
5470
5471        let tool_icon = if is_file && has_failed && has_revealed_diff {
5472            div()
5473                .id(entry_ix)
5474                .tooltip(Tooltip::text("Interrupted Edit"))
5475                .child(DecoratedIcon::new(
5476                    file_icon,
5477                    Some(
5478                        IconDecoration::new(
5479                            IconDecorationKind::Triangle,
5480                            self.tool_card_header_bg(cx),
5481                            cx,
5482                        )
5483                        .color(cx.theme().status().warning)
5484                        .position(gpui::Point {
5485                            x: px(-2.),
5486                            y: px(-2.),
5487                        }),
5488                    ),
5489                ))
5490                .into_any_element()
5491        } else if is_file {
5492            div().child(file_icon).into_any_element()
5493        } else if is_subagent_tool_call {
5494            Icon::new(self.agent_icon)
5495                .size(IconSize::Small)
5496                .color(Color::Muted)
5497                .into_any_element()
5498        } else {
5499            Icon::new(match tool_call.kind {
5500                acp::ToolKind::Read => IconName::ToolSearch,
5501                acp::ToolKind::Edit => IconName::ToolPencil,
5502                acp::ToolKind::Delete => IconName::ToolDeleteFile,
5503                acp::ToolKind::Move => IconName::ArrowRightLeft,
5504                acp::ToolKind::Search => IconName::ToolSearch,
5505                acp::ToolKind::Execute => IconName::ToolTerminal,
5506                acp::ToolKind::Think => IconName::ToolThink,
5507                acp::ToolKind::Fetch => IconName::ToolWeb,
5508                acp::ToolKind::SwitchMode => IconName::ArrowRightLeft,
5509                acp::ToolKind::Other | _ => IconName::ToolHammer,
5510            })
5511            .size(IconSize::Small)
5512            .color(Color::Muted)
5513            .into_any_element()
5514        };
5515
5516        let gradient_overlay = {
5517            div()
5518                .absolute()
5519                .top_0()
5520                .right_0()
5521                .w_12()
5522                .h_full()
5523                .map(|this| {
5524                    if use_card_layout {
5525                        this.bg(linear_gradient(
5526                            90.,
5527                            linear_color_stop(self.tool_card_header_bg(cx), 1.),
5528                            linear_color_stop(self.tool_card_header_bg(cx).opacity(0.2), 0.),
5529                        ))
5530                    } else {
5531                        this.bg(linear_gradient(
5532                            90.,
5533                            linear_color_stop(cx.theme().colors().panel_background, 1.),
5534                            linear_color_stop(
5535                                cx.theme().colors().panel_background.opacity(0.2),
5536                                0.,
5537                            ),
5538                        ))
5539                    }
5540                })
5541        };
5542
5543        h_flex()
5544            .relative()
5545            .w_full()
5546            .h(window.line_height() - px(2.))
5547            .text_size(self.tool_name_font_size())
5548            .gap_1p5()
5549            .when(has_location || use_card_layout, |this| this.px_1())
5550            .when(has_location, |this| {
5551                this.cursor(CursorStyle::PointingHand)
5552                    .rounded(rems_from_px(3.)) // Concentric border radius
5553                    .hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.5)))
5554            })
5555            .overflow_hidden()
5556            .child(tool_icon)
5557            .child(if has_location {
5558                h_flex()
5559                    .id(("open-tool-call-location", entry_ix))
5560                    .w_full()
5561                    .map(|this| {
5562                        if use_card_layout {
5563                            this.text_color(cx.theme().colors().text)
5564                        } else {
5565                            this.text_color(cx.theme().colors().text_muted)
5566                        }
5567                    })
5568                    .child(
5569                        self.render_markdown(
5570                            tool_call.label.clone(),
5571                            MarkdownStyle {
5572                                prevent_mouse_interaction: true,
5573                                ..MarkdownStyle::themed(MarkdownFont::Agent, window, cx)
5574                                    .with_muted_text(cx)
5575                            },
5576                        ),
5577                    )
5578                    .tooltip(Tooltip::text("Go to File"))
5579                    .on_click(cx.listener(move |this, _, window, cx| {
5580                        this.open_tool_call_location(entry_ix, 0, window, cx);
5581                    }))
5582                    .into_any_element()
5583            } else {
5584                h_flex()
5585                    .w_full()
5586                    .child(self.render_markdown(
5587                        tool_call.label.clone(),
5588                        MarkdownStyle::themed(MarkdownFont::Agent, window, cx).with_muted_text(cx),
5589                    ))
5590                    .into_any()
5591            })
5592            .when(!is_edit, |this| this.child(gradient_overlay))
5593    }
5594
5595    fn open_tool_call_location(
5596        &self,
5597        entry_ix: usize,
5598        location_ix: usize,
5599        window: &mut Window,
5600        cx: &mut Context<Self>,
5601    ) -> Option<()> {
5602        let (tool_call_location, agent_location) = self
5603            .thread
5604            .read(cx)
5605            .entries()
5606            .get(entry_ix)?
5607            .location(location_ix)?;
5608
5609        let project_path = self
5610            .project
5611            .upgrade()?
5612            .read(cx)
5613            .find_project_path(&tool_call_location.path, cx)?;
5614
5615        let open_task = self
5616            .workspace
5617            .update(cx, |workspace, cx| {
5618                workspace.open_path(project_path, None, true, window, cx)
5619            })
5620            .log_err()?;
5621        window
5622            .spawn(cx, async move |cx| {
5623                let item = open_task.await?;
5624
5625                let Some(active_editor) = item.downcast::<Editor>() else {
5626                    return anyhow::Ok(());
5627                };
5628
5629                active_editor.update_in(cx, |editor, window, cx| {
5630                    let multibuffer = editor.buffer().read(cx);
5631                    let buffer = multibuffer.as_singleton();
5632                    if agent_location.buffer.upgrade() == buffer {
5633                        let excerpt_id = multibuffer.excerpt_ids().first().cloned();
5634                        let anchor =
5635                            editor::Anchor::in_buffer(excerpt_id.unwrap(), agent_location.position);
5636                        editor.change_selections(Default::default(), window, cx, |selections| {
5637                            selections.select_anchor_ranges([anchor..anchor]);
5638                        })
5639                    } else {
5640                        let row = tool_call_location.line.unwrap_or_default();
5641                        editor.change_selections(Default::default(), window, cx, |selections| {
5642                            selections.select_ranges([Point::new(row, 0)..Point::new(row, 0)]);
5643                        })
5644                    }
5645                })?;
5646
5647                anyhow::Ok(())
5648            })
5649            .detach_and_log_err(cx);
5650
5651        None
5652    }
5653
5654    fn render_tool_call_content(
5655        &self,
5656        entry_ix: usize,
5657        content: &ToolCallContent,
5658        context_ix: usize,
5659        tool_call: &ToolCall,
5660        card_layout: bool,
5661        is_image_tool_call: bool,
5662        has_failed: bool,
5663        window: &Window,
5664        cx: &Context<Self>,
5665    ) -> AnyElement {
5666        match content {
5667            ToolCallContent::ContentBlock(content) => {
5668                if let Some(resource_link) = content.resource_link() {
5669                    self.render_resource_link(resource_link, cx)
5670                } else if let Some(markdown) = content.markdown() {
5671                    self.render_markdown_output(
5672                        markdown.clone(),
5673                        tool_call.id.clone(),
5674                        context_ix,
5675                        card_layout,
5676                        window,
5677                        cx,
5678                    )
5679                } else if let Some(image) = content.image() {
5680                    let location = tool_call.locations.first().cloned();
5681                    self.render_image_output(
5682                        entry_ix,
5683                        image.clone(),
5684                        location,
5685                        card_layout,
5686                        is_image_tool_call,
5687                        cx,
5688                    )
5689                } else {
5690                    Empty.into_any_element()
5691                }
5692            }
5693            ToolCallContent::Diff(diff) => {
5694                self.render_diff_editor(entry_ix, diff, tool_call, has_failed, cx)
5695            }
5696            ToolCallContent::Terminal(terminal) => {
5697                self.render_terminal_tool_call(entry_ix, terminal, tool_call, window, cx)
5698            }
5699        }
5700    }
5701
5702    fn render_resource_link(
5703        &self,
5704        resource_link: &acp::ResourceLink,
5705        cx: &Context<Self>,
5706    ) -> AnyElement {
5707        let uri: SharedString = resource_link.uri.clone().into();
5708        let is_file = resource_link.uri.strip_prefix("file://");
5709
5710        let Some(project) = self.project.upgrade() else {
5711            return Empty.into_any_element();
5712        };
5713
5714        let label: SharedString = if let Some(abs_path) = is_file {
5715            if let Some(project_path) = project
5716                .read(cx)
5717                .project_path_for_absolute_path(&Path::new(abs_path), cx)
5718                && let Some(worktree) = project
5719                    .read(cx)
5720                    .worktree_for_id(project_path.worktree_id, cx)
5721            {
5722                worktree
5723                    .read(cx)
5724                    .full_path(&project_path.path)
5725                    .to_string_lossy()
5726                    .to_string()
5727                    .into()
5728            } else {
5729                abs_path.to_string().into()
5730            }
5731        } else {
5732            uri.clone()
5733        };
5734
5735        let button_id = SharedString::from(format!("item-{}", uri));
5736
5737        div()
5738            .ml(rems(0.4))
5739            .pl_2p5()
5740            .border_l_1()
5741            .border_color(self.tool_card_border_color(cx))
5742            .overflow_hidden()
5743            .child(
5744                Button::new(button_id, label)
5745                    .label_size(LabelSize::Small)
5746                    .color(Color::Muted)
5747                    .truncate(true)
5748                    .when(is_file.is_none(), |this| {
5749                        this.icon(IconName::ArrowUpRight)
5750                            .icon_size(IconSize::XSmall)
5751                            .icon_color(Color::Muted)
5752                    })
5753                    .on_click(cx.listener({
5754                        let workspace = self.workspace.clone();
5755                        move |_, _, window, cx: &mut Context<Self>| {
5756                            open_link(uri.clone(), &workspace, window, cx);
5757                        }
5758                    })),
5759            )
5760            .into_any_element()
5761    }
5762
5763    fn render_diff_editor(
5764        &self,
5765        entry_ix: usize,
5766        diff: &Entity<acp_thread::Diff>,
5767        tool_call: &ToolCall,
5768        has_failed: bool,
5769        cx: &Context<Self>,
5770    ) -> AnyElement {
5771        let tool_progress = matches!(
5772            &tool_call.status,
5773            ToolCallStatus::InProgress | ToolCallStatus::Pending
5774        );
5775
5776        let revealed_diff_editor = if let Some(entry) =
5777            self.entry_view_state.read(cx).entry(entry_ix)
5778            && let Some(editor) = entry.editor_for_diff(diff)
5779            && diff.read(cx).has_revealed_range(cx)
5780        {
5781            Some(editor)
5782        } else {
5783            None
5784        };
5785
5786        let show_top_border = !has_failed || revealed_diff_editor.is_some();
5787
5788        v_flex()
5789            .h_full()
5790            .when(show_top_border, |this| {
5791                this.border_t_1()
5792                    .when(has_failed, |this| this.border_dashed())
5793                    .border_color(self.tool_card_border_color(cx))
5794            })
5795            .child(if let Some(editor) = revealed_diff_editor {
5796                editor.into_any_element()
5797            } else if tool_progress && self.as_native_connection(cx).is_some() {
5798                self.render_diff_loading(cx)
5799            } else {
5800                Empty.into_any()
5801            })
5802            .into_any()
5803    }
5804
5805    fn render_markdown_output(
5806        &self,
5807        markdown: Entity<Markdown>,
5808        tool_call_id: acp::ToolCallId,
5809        context_ix: usize,
5810        card_layout: bool,
5811        window: &Window,
5812        cx: &Context<Self>,
5813    ) -> AnyElement {
5814        let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id));
5815
5816        v_flex()
5817            .gap_2()
5818            .map(|this| {
5819                if card_layout {
5820                    this.when(context_ix > 0, |this| {
5821                        this.pt_2()
5822                            .border_t_1()
5823                            .border_color(self.tool_card_border_color(cx))
5824                    })
5825                } else {
5826                    this.ml(rems(0.4))
5827                        .px_3p5()
5828                        .border_l_1()
5829                        .border_color(self.tool_card_border_color(cx))
5830                }
5831            })
5832            .text_xs()
5833            .text_color(cx.theme().colors().text_muted)
5834            .child(self.render_markdown(
5835                markdown,
5836                MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
5837            ))
5838            .when(!card_layout, |this| {
5839                this.child(
5840                    IconButton::new(button_id, IconName::ChevronUp)
5841                        .full_width()
5842                        .style(ButtonStyle::Outlined)
5843                        .icon_color(Color::Muted)
5844                        .on_click(cx.listener({
5845                            move |this: &mut Self, _, _, cx: &mut Context<Self>| {
5846                                this.expanded_tool_calls.remove(&tool_call_id);
5847                                cx.notify();
5848                            }
5849                        })),
5850                )
5851            })
5852            .into_any_element()
5853    }
5854
5855    fn render_image_output(
5856        &self,
5857        entry_ix: usize,
5858        image: Arc<gpui::Image>,
5859        location: Option<acp::ToolCallLocation>,
5860        card_layout: bool,
5861        show_dimensions: bool,
5862        cx: &Context<Self>,
5863    ) -> AnyElement {
5864        let dimensions_label = if show_dimensions {
5865            let format_name = match image.format() {
5866                gpui::ImageFormat::Png => "PNG",
5867                gpui::ImageFormat::Jpeg => "JPEG",
5868                gpui::ImageFormat::Webp => "WebP",
5869                gpui::ImageFormat::Gif => "GIF",
5870                gpui::ImageFormat::Svg => "SVG",
5871                gpui::ImageFormat::Bmp => "BMP",
5872                gpui::ImageFormat::Tiff => "TIFF",
5873                gpui::ImageFormat::Ico => "ICO",
5874            };
5875            let dimensions = image::ImageReader::new(std::io::Cursor::new(image.bytes()))
5876                .with_guessed_format()
5877                .ok()
5878                .and_then(|reader| reader.into_dimensions().ok());
5879            dimensions.map(|(w, h)| format!("{}×{} {}", w, h, format_name))
5880        } else {
5881            None
5882        };
5883
5884        v_flex()
5885            .gap_2()
5886            .map(|this| {
5887                if card_layout {
5888                    this
5889                } else {
5890                    this.ml(rems(0.4))
5891                        .px_3p5()
5892                        .border_l_1()
5893                        .border_color(self.tool_card_border_color(cx))
5894                }
5895            })
5896            .when(dimensions_label.is_some() || location.is_some(), |this| {
5897                this.child(
5898                    h_flex()
5899                        .w_full()
5900                        .justify_between()
5901                        .items_center()
5902                        .children(dimensions_label.map(|label| {
5903                            Label::new(label)
5904                                .size(LabelSize::XSmall)
5905                                .color(Color::Muted)
5906                                .buffer_font(cx)
5907                        }))
5908                        .when_some(location, |this, _loc| {
5909                            this.child(
5910                                Button::new(("go-to-file", entry_ix), "Go to File")
5911                                    .label_size(LabelSize::Small)
5912                                    .on_click(cx.listener(move |this, _, window, cx| {
5913                                        this.open_tool_call_location(entry_ix, 0, window, cx);
5914                                    })),
5915                            )
5916                        }),
5917                )
5918            })
5919            .child(
5920                img(image)
5921                    .max_w_96()
5922                    .max_h_96()
5923                    .object_fit(ObjectFit::ScaleDown),
5924            )
5925            .into_any_element()
5926    }
5927
5928    fn render_subagent_tool_call(
5929        &self,
5930        entry_ix: usize,
5931        tool_call: &ToolCall,
5932        subagent_session_id: Option<acp::SessionId>,
5933        window: &Window,
5934        cx: &Context<Self>,
5935    ) -> Div {
5936        let tool_call_status = &tool_call.status;
5937
5938        let subagent_thread_view = subagent_session_id.and_then(|id| {
5939            self.server_view
5940                .upgrade()
5941                .and_then(|server_view| server_view.read(cx).as_connected())
5942                .and_then(|connected| connected.threads.get(&id))
5943        });
5944
5945        let content = self.render_subagent_card(
5946            entry_ix,
5947            0,
5948            subagent_thread_view,
5949            tool_call_status,
5950            window,
5951            cx,
5952        );
5953
5954        v_flex().mx_5().my_1p5().gap_3().child(content)
5955    }
5956
5957    fn render_subagent_card(
5958        &self,
5959        entry_ix: usize,
5960        context_ix: usize,
5961        thread_view: Option<&Entity<AcpThreadView>>,
5962        tool_call_status: &ToolCallStatus,
5963        window: &Window,
5964        cx: &Context<Self>,
5965    ) -> AnyElement {
5966        let thread = thread_view
5967            .as_ref()
5968            .map(|view| view.read(cx).thread.clone());
5969        let session_id = thread
5970            .as_ref()
5971            .map(|thread| thread.read(cx).session_id().clone());
5972        let action_log = thread.as_ref().map(|thread| thread.read(cx).action_log());
5973        let changed_buffers = action_log
5974            .map(|log| log.read(cx).changed_buffers(cx))
5975            .unwrap_or_default();
5976
5977        let is_expanded = if let Some(session_id) = &session_id {
5978            self.expanded_subagents.contains(session_id)
5979        } else {
5980            false
5981        };
5982        let files_changed = changed_buffers.len();
5983        let diff_stats = DiffStats::all_files(&changed_buffers, cx);
5984
5985        let is_running = matches!(
5986            tool_call_status,
5987            ToolCallStatus::Pending | ToolCallStatus::InProgress
5988        );
5989        let is_canceled_or_failed = matches!(
5990            tool_call_status,
5991            ToolCallStatus::Canceled | ToolCallStatus::Failed | ToolCallStatus::Rejected
5992        );
5993
5994        let title = thread
5995            .as_ref()
5996            .map(|t| t.read(cx).title())
5997            .unwrap_or_else(|| {
5998                if is_canceled_or_failed {
5999                    "Subagent Canceled"
6000                } else {
6001                    "Creating Subagent…"
6002                }
6003                .into()
6004            });
6005
6006        let card_header_id = format!("subagent-header-{}-{}", entry_ix, context_ix);
6007        let diff_stat_id = format!("subagent-diff-{}-{}", entry_ix, context_ix);
6008
6009        let icon = h_flex().w_4().justify_center().child(if is_running {
6010            SpinnerLabel::new()
6011                .size(LabelSize::Small)
6012                .into_any_element()
6013        } else if is_canceled_or_failed {
6014            Icon::new(IconName::Close)
6015                .size(IconSize::Small)
6016                .color(Color::Error)
6017                .into_any_element()
6018        } else {
6019            Icon::new(IconName::Check)
6020                .size(IconSize::Small)
6021                .color(Color::Success)
6022                .into_any_element()
6023        });
6024
6025        let has_expandable_content = thread.as_ref().map_or(false, |thread| {
6026            thread.read(cx).entries().iter().rev().any(|entry| {
6027                if let AgentThreadEntry::AssistantMessage(msg) = entry {
6028                    msg.chunks.iter().any(|chunk| match chunk {
6029                        AssistantMessageChunk::Message { block } => block.markdown().is_some(),
6030                        AssistantMessageChunk::Thought { block } => block.markdown().is_some(),
6031                    })
6032                } else {
6033                    false
6034                }
6035            })
6036        });
6037
6038        v_flex()
6039            .w_full()
6040            .rounded_md()
6041            .border_1()
6042            .border_color(self.tool_card_border_color(cx))
6043            .overflow_hidden()
6044            .child(
6045                h_flex()
6046                    .group(&card_header_id)
6047                    .p_1()
6048                    .pl_1p5()
6049                    .w_full()
6050                    .gap_1()
6051                    .justify_between()
6052                    .bg(self.tool_card_header_bg(cx))
6053                    .child(
6054                        h_flex()
6055                            .gap_1p5()
6056                            .child(icon)
6057                            .child(Label::new(title.to_string()).size(LabelSize::Small))
6058                            .when(files_changed > 0, |this| {
6059                                this.child(
6060                                    h_flex()
6061                                        .gap_1()
6062                                        .child(
6063                                            Label::new(format!(
6064                                                "{} {} changed",
6065                                                files_changed,
6066                                                if files_changed == 1 { "file" } else { "files" }
6067                                            ))
6068                                            .size(LabelSize::Small)
6069                                            .color(Color::Muted),
6070                                        )
6071                                        .child(DiffStat::new(
6072                                            diff_stat_id.clone(),
6073                                            diff_stats.lines_added as usize,
6074                                            diff_stats.lines_removed as usize,
6075                                        )),
6076                                )
6077                            }),
6078                    )
6079                    .when_some(session_id, |this, session_id| {
6080                        this.child(
6081                            h_flex()
6082                                .when(has_expandable_content, |this| {
6083                                    this.child(
6084                                        IconButton::new(
6085                                            format!(
6086                                                "subagent-disclosure-{}-{}",
6087                                                entry_ix, context_ix
6088                                            ),
6089                                            if is_expanded {
6090                                                IconName::ChevronUp
6091                                            } else {
6092                                                IconName::ChevronDown
6093                                            },
6094                                        )
6095                                        .icon_color(Color::Muted)
6096                                        .icon_size(IconSize::Small)
6097                                        .disabled(!has_expandable_content)
6098                                        .visible_on_hover(card_header_id.clone())
6099                                        .on_click(
6100                                            cx.listener({
6101                                                let session_id = session_id.clone();
6102                                                move |this, _, _, cx| {
6103                                                    if this.expanded_subagents.contains(&session_id)
6104                                                    {
6105                                                        this.expanded_subagents.remove(&session_id);
6106                                                    } else {
6107                                                        this.expanded_subagents
6108                                                            .insert(session_id.clone());
6109                                                    }
6110                                                    cx.notify();
6111                                                }
6112                                            }),
6113                                        ),
6114                                    )
6115                                })
6116                                .child(
6117                                    IconButton::new(
6118                                        format!("expand-subagent-{}-{}", entry_ix, context_ix),
6119                                        IconName::Maximize,
6120                                    )
6121                                    .icon_color(Color::Muted)
6122                                    .icon_size(IconSize::Small)
6123                                    .tooltip(Tooltip::text("Expand Subagent"))
6124                                    .visible_on_hover(card_header_id)
6125                                    .on_click(cx.listener(
6126                                        move |this, _event, window, cx| {
6127                                            this.server_view
6128                                                .update(cx, |this, cx| {
6129                                                    this.navigate_to_session(
6130                                                        session_id.clone(),
6131                                                        window,
6132                                                        cx,
6133                                                    );
6134                                                })
6135                                                .ok();
6136                                        },
6137                                    )),
6138                                )
6139                                .when(is_running, |buttons| {
6140                                    buttons.child(
6141                                        IconButton::new(
6142                                            format!("stop-subagent-{}-{}", entry_ix, context_ix),
6143                                            IconName::Stop,
6144                                        )
6145                                        .icon_size(IconSize::Small)
6146                                        .icon_color(Color::Error)
6147                                        .tooltip(Tooltip::text("Stop Subagent"))
6148                                        .when_some(
6149                                            thread_view
6150                                                .as_ref()
6151                                                .map(|view| view.read(cx).thread.clone()),
6152                                            |this, thread| {
6153                                                this.on_click(cx.listener(
6154                                                    move |_this, _event, _window, cx| {
6155                                                        thread.update(cx, |thread, _cx| {
6156                                                            thread.stop_by_user();
6157                                                        });
6158                                                    },
6159                                                ))
6160                                            },
6161                                        ),
6162                                    )
6163                                }),
6164                        )
6165                    }),
6166            )
6167            .when_some(thread_view, |this, thread_view| {
6168                let thread = &thread_view.read(cx).thread;
6169                this.when(is_expanded, |this| {
6170                    this.child(
6171                        self.render_subagent_expanded_content(
6172                            entry_ix, context_ix, thread, window, cx,
6173                        ),
6174                    )
6175                })
6176                .children(
6177                    thread
6178                        .read(cx)
6179                        .first_tool_awaiting_confirmation()
6180                        .and_then(|tc| {
6181                            if let ToolCallStatus::WaitingForConfirmation { options, .. } =
6182                                &tc.status
6183                            {
6184                                Some(self.render_subagent_pending_tool_call(
6185                                    entry_ix,
6186                                    context_ix,
6187                                    thread.clone(),
6188                                    tc,
6189                                    options,
6190                                    window,
6191                                    cx,
6192                                ))
6193                            } else {
6194                                None
6195                            }
6196                        }),
6197                )
6198            })
6199            .into_any_element()
6200    }
6201
6202    fn render_subagent_expanded_content(
6203        &self,
6204        _entry_ix: usize,
6205        _context_ix: usize,
6206        thread: &Entity<AcpThread>,
6207        window: &Window,
6208        cx: &Context<Self>,
6209    ) -> impl IntoElement {
6210        let thread_read = thread.read(cx);
6211        let session_id = thread_read.session_id().clone();
6212        let entries = thread_read.entries();
6213
6214        // Find the most recent agent message with any content (message or thought)
6215        let last_assistant_markdown = entries.iter().rev().find_map(|entry| {
6216            if let AgentThreadEntry::AssistantMessage(msg) = entry {
6217                msg.chunks.iter().find_map(|chunk| match chunk {
6218                    AssistantMessageChunk::Message { block } => block.markdown().cloned(),
6219                    AssistantMessageChunk::Thought { block } => block.markdown().cloned(),
6220                })
6221            } else {
6222                None
6223            }
6224        });
6225
6226        let scroll_handle = self
6227            .subagent_scroll_handles
6228            .borrow_mut()
6229            .entry(session_id.clone())
6230            .or_default()
6231            .clone();
6232
6233        scroll_handle.scroll_to_bottom();
6234        let editor_bg = cx.theme().colors().editor_background;
6235
6236        let gradient_overlay = {
6237            div().absolute().inset_0().bg(linear_gradient(
6238                180.,
6239                linear_color_stop(editor_bg, 0.),
6240                linear_color_stop(editor_bg.opacity(0.), 0.15),
6241            ))
6242        };
6243
6244        div()
6245            .relative()
6246            .w_full()
6247            .max_h_56()
6248            .p_2p5()
6249            .text_ui(cx)
6250            .border_t_1()
6251            .border_color(self.tool_card_border_color(cx))
6252            .bg(editor_bg.opacity(0.4))
6253            .overflow_hidden()
6254            .child(
6255                div()
6256                    .id(format!("subagent-content-{}", session_id))
6257                    .size_full()
6258                    .track_scroll(&scroll_handle)
6259                    .when_some(last_assistant_markdown, |this, markdown| {
6260                        this.child(self.render_markdown(
6261                            markdown,
6262                            MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
6263                        ))
6264                    }),
6265            )
6266            .child(gradient_overlay)
6267    }
6268
6269    fn render_subagent_pending_tool_call(
6270        &self,
6271        entry_ix: usize,
6272        context_ix: usize,
6273        subagent_thread: Entity<AcpThread>,
6274        tool_call: &ToolCall,
6275        options: &PermissionOptions,
6276        window: &Window,
6277        cx: &Context<Self>,
6278    ) -> Div {
6279        let tool_call_id = tool_call.id.clone();
6280        let is_edit =
6281            matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some();
6282        let has_image_content = tool_call.content.iter().any(|c| c.image().is_some());
6283
6284        v_flex()
6285            .w_full()
6286            .border_t_1()
6287            .border_color(self.tool_card_border_color(cx))
6288            .child(
6289                self.render_tool_call_label(
6290                    entry_ix, tool_call, is_edit, false, // has_failed
6291                    false, // has_revealed_diff
6292                    true,  // use_card_layout
6293                    window, cx,
6294                )
6295                .py_1(),
6296            )
6297            .children(
6298                tool_call
6299                    .content
6300                    .iter()
6301                    .enumerate()
6302                    .map(|(content_ix, content)| {
6303                        self.render_tool_call_content(
6304                            entry_ix,
6305                            content,
6306                            content_ix,
6307                            tool_call,
6308                            true, // card_layout
6309                            has_image_content,
6310                            false, // has_failed
6311                            window,
6312                            cx,
6313                        )
6314                    }),
6315            )
6316            .child(self.render_subagent_permission_buttons(
6317                entry_ix,
6318                context_ix,
6319                subagent_thread,
6320                tool_call_id,
6321                options,
6322                cx,
6323            ))
6324    }
6325
6326    fn render_subagent_permission_buttons(
6327        &self,
6328        entry_ix: usize,
6329        context_ix: usize,
6330        subagent_thread: Entity<AcpThread>,
6331        tool_call_id: acp::ToolCallId,
6332        options: &PermissionOptions,
6333        cx: &Context<Self>,
6334    ) -> Div {
6335        match options {
6336            PermissionOptions::Flat(options) => self.render_subagent_permission_buttons_flat(
6337                entry_ix,
6338                context_ix,
6339                subagent_thread,
6340                tool_call_id,
6341                options,
6342                cx,
6343            ),
6344            PermissionOptions::Dropdown(options) => self
6345                .render_subagent_permission_buttons_dropdown(
6346                    entry_ix,
6347                    context_ix,
6348                    subagent_thread,
6349                    tool_call_id,
6350                    options,
6351                    cx,
6352                ),
6353        }
6354    }
6355
6356    fn render_subagent_permission_buttons_flat(
6357        &self,
6358        entry_ix: usize,
6359        context_ix: usize,
6360        subagent_thread: Entity<AcpThread>,
6361        tool_call_id: acp::ToolCallId,
6362        options: &[acp::PermissionOption],
6363        cx: &Context<Self>,
6364    ) -> Div {
6365        div()
6366            .p_1()
6367            .border_t_1()
6368            .border_color(self.tool_card_border_color(cx))
6369            .w_full()
6370            .v_flex()
6371            .gap_0p5()
6372            .children(options.iter().map(move |option| {
6373                let option_id = SharedString::from(format!(
6374                    "subagent-{}-{}-{}",
6375                    entry_ix, context_ix, option.option_id.0
6376                ));
6377                Button::new((option_id, entry_ix), option.name.clone())
6378                    .map(|this| match option.kind {
6379                        acp::PermissionOptionKind::AllowOnce => {
6380                            this.icon(IconName::Check).icon_color(Color::Success)
6381                        }
6382                        acp::PermissionOptionKind::AllowAlways => {
6383                            this.icon(IconName::CheckDouble).icon_color(Color::Success)
6384                        }
6385                        acp::PermissionOptionKind::RejectOnce
6386                        | acp::PermissionOptionKind::RejectAlways
6387                        | _ => this.icon(IconName::Close).icon_color(Color::Error),
6388                    })
6389                    .icon_position(IconPosition::Start)
6390                    .icon_size(IconSize::XSmall)
6391                    .label_size(LabelSize::Small)
6392                    .on_click(cx.listener({
6393                        let subagent_thread = subagent_thread.clone();
6394                        let tool_call_id = tool_call_id.clone();
6395                        let option_id = option.option_id.clone();
6396                        let option_kind = option.kind;
6397                        move |this, _, window, cx| {
6398                            this.authorize_subagent_tool_call(
6399                                subagent_thread.clone(),
6400                                tool_call_id.clone(),
6401                                option_id.clone(),
6402                                option_kind,
6403                                window,
6404                                cx,
6405                            );
6406                        }
6407                    }))
6408            }))
6409    }
6410
6411    fn authorize_subagent_tool_call(
6412        &mut self,
6413        subagent_thread: Entity<AcpThread>,
6414        tool_call_id: acp::ToolCallId,
6415        option_id: acp::PermissionOptionId,
6416        option_kind: acp::PermissionOptionKind,
6417        _window: &mut Window,
6418        cx: &mut Context<Self>,
6419    ) {
6420        subagent_thread.update(cx, |thread, cx| {
6421            thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx);
6422        });
6423    }
6424
6425    fn render_subagent_permission_buttons_dropdown(
6426        &self,
6427        entry_ix: usize,
6428        context_ix: usize,
6429        subagent_thread: Entity<AcpThread>,
6430        tool_call_id: acp::ToolCallId,
6431        choices: &[PermissionOptionChoice],
6432        cx: &Context<Self>,
6433    ) -> Div {
6434        let selected_index = self
6435            .selected_permission_granularity
6436            .get(&tool_call_id)
6437            .copied()
6438            .unwrap_or_else(|| choices.len().saturating_sub(1));
6439
6440        let selected_choice = choices.get(selected_index).or(choices.last());
6441
6442        let dropdown_label: SharedString = selected_choice
6443            .map(|choice| choice.label())
6444            .unwrap_or_else(|| "Only this time".into());
6445
6446        let (allow_option_id, allow_option_kind, deny_option_id, deny_option_kind) =
6447            if let Some(choice) = selected_choice {
6448                (
6449                    choice.allow.option_id.clone(),
6450                    choice.allow.kind,
6451                    choice.deny.option_id.clone(),
6452                    choice.deny.kind,
6453                )
6454            } else {
6455                (
6456                    acp::PermissionOptionId::new("allow"),
6457                    acp::PermissionOptionKind::AllowOnce,
6458                    acp::PermissionOptionId::new("deny"),
6459                    acp::PermissionOptionKind::RejectOnce,
6460                )
6461            };
6462
6463        h_flex()
6464            .w_full()
6465            .p_1()
6466            .gap_2()
6467            .justify_between()
6468            .border_t_1()
6469            .border_color(self.tool_card_border_color(cx))
6470            .child(
6471                h_flex()
6472                    .gap_0p5()
6473                    .child(
6474                        Button::new(
6475                            (
6476                                SharedString::from(format!(
6477                                    "subagent-allow-btn-{}-{}",
6478                                    entry_ix, context_ix
6479                                )),
6480                                entry_ix,
6481                            ),
6482                            "Allow",
6483                        )
6484                        .icon(IconName::Check)
6485                        .icon_color(Color::Success)
6486                        .icon_position(IconPosition::Start)
6487                        .icon_size(IconSize::XSmall)
6488                        .label_size(LabelSize::Small)
6489                        .on_click(cx.listener({
6490                            let subagent_thread = subagent_thread.clone();
6491                            let tool_call_id = tool_call_id.clone();
6492                            let option_id = allow_option_id;
6493                            let option_kind = allow_option_kind;
6494                            move |this, _, window, cx| {
6495                                this.authorize_subagent_tool_call(
6496                                    subagent_thread.clone(),
6497                                    tool_call_id.clone(),
6498                                    option_id.clone(),
6499                                    option_kind,
6500                                    window,
6501                                    cx,
6502                                );
6503                            }
6504                        })),
6505                    )
6506                    .child(
6507                        Button::new(
6508                            (
6509                                SharedString::from(format!(
6510                                    "subagent-deny-btn-{}-{}",
6511                                    entry_ix, context_ix
6512                                )),
6513                                entry_ix,
6514                            ),
6515                            "Deny",
6516                        )
6517                        .icon(IconName::Close)
6518                        .icon_color(Color::Error)
6519                        .icon_position(IconPosition::Start)
6520                        .icon_size(IconSize::XSmall)
6521                        .label_size(LabelSize::Small)
6522                        .on_click(cx.listener({
6523                            let tool_call_id = tool_call_id.clone();
6524                            let option_id = deny_option_id;
6525                            let option_kind = deny_option_kind;
6526                            move |this, _, window, cx| {
6527                                this.authorize_subagent_tool_call(
6528                                    subagent_thread.clone(),
6529                                    tool_call_id.clone(),
6530                                    option_id.clone(),
6531                                    option_kind,
6532                                    window,
6533                                    cx,
6534                                );
6535                            }
6536                        })),
6537                    ),
6538            )
6539            .child(self.render_subagent_permission_granularity_dropdown(
6540                choices,
6541                dropdown_label,
6542                entry_ix,
6543                context_ix,
6544                tool_call_id,
6545                selected_index,
6546                cx,
6547            ))
6548    }
6549
6550    fn render_subagent_permission_granularity_dropdown(
6551        &self,
6552        choices: &[PermissionOptionChoice],
6553        current_label: SharedString,
6554        entry_ix: usize,
6555        context_ix: usize,
6556        tool_call_id: acp::ToolCallId,
6557        selected_index: usize,
6558        _cx: &Context<Self>,
6559    ) -> AnyElement {
6560        let menu_options: Vec<(usize, SharedString)> = choices
6561            .iter()
6562            .enumerate()
6563            .map(|(i, choice)| (i, choice.label()))
6564            .collect();
6565
6566        let permission_dropdown_handle = self.permission_dropdown_handle.clone();
6567
6568        PopoverMenu::new((
6569            SharedString::from(format!(
6570                "subagent-permission-granularity-{}-{}",
6571                entry_ix, context_ix
6572            )),
6573            entry_ix,
6574        ))
6575        .with_handle(permission_dropdown_handle)
6576        .trigger(
6577            Button::new(
6578                (
6579                    SharedString::from(format!(
6580                        "subagent-granularity-trigger-{}-{}",
6581                        entry_ix, context_ix
6582                    )),
6583                    entry_ix,
6584                ),
6585                current_label,
6586            )
6587            .icon(IconName::ChevronDown)
6588            .icon_size(IconSize::XSmall)
6589            .icon_color(Color::Muted)
6590            .label_size(LabelSize::Small),
6591        )
6592        .menu(move |window, cx| {
6593            let tool_call_id = tool_call_id.clone();
6594            let options = menu_options.clone();
6595
6596            Some(ContextMenu::build(window, cx, move |mut menu, _, _| {
6597                for (index, display_name) in options.iter() {
6598                    let display_name = display_name.clone();
6599                    let index = *index;
6600                    let tool_call_id_for_entry = tool_call_id.clone();
6601                    let is_selected = index == selected_index;
6602
6603                    menu = menu.toggleable_entry(
6604                        display_name,
6605                        is_selected,
6606                        IconPosition::End,
6607                        None,
6608                        move |window, cx| {
6609                            window.dispatch_action(
6610                                SelectPermissionGranularity {
6611                                    tool_call_id: tool_call_id_for_entry.0.to_string(),
6612                                    index,
6613                                }
6614                                .boxed_clone(),
6615                                cx,
6616                            );
6617                        },
6618                    );
6619                }
6620
6621                menu
6622            }))
6623        })
6624        .into_any_element()
6625    }
6626
6627    fn render_rules_item(&self, cx: &Context<Self>) -> Option<AnyElement> {
6628        let project_context = self
6629            .as_native_thread(cx)?
6630            .read(cx)
6631            .project_context()
6632            .read(cx);
6633
6634        let user_rules_text = if project_context.user_rules.is_empty() {
6635            None
6636        } else if project_context.user_rules.len() == 1 {
6637            let user_rules = &project_context.user_rules[0];
6638
6639            match user_rules.title.as_ref() {
6640                Some(title) => Some(format!("Using \"{title}\" user rule")),
6641                None => Some("Using user rule".into()),
6642            }
6643        } else {
6644            Some(format!(
6645                "Using {} user rules",
6646                project_context.user_rules.len()
6647            ))
6648        };
6649
6650        let first_user_rules_id = project_context
6651            .user_rules
6652            .first()
6653            .map(|user_rules| user_rules.uuid.0);
6654
6655        let rules_files = project_context
6656            .worktrees
6657            .iter()
6658            .filter_map(|worktree| worktree.rules_file.as_ref())
6659            .collect::<Vec<_>>();
6660
6661        let rules_file_text = match rules_files.as_slice() {
6662            &[] => None,
6663            &[rules_file] => Some(format!(
6664                "Using project {:?} file",
6665                rules_file.path_in_worktree
6666            )),
6667            rules_files => Some(format!("Using {} project rules files", rules_files.len())),
6668        };
6669
6670        if user_rules_text.is_none() && rules_file_text.is_none() {
6671            return None;
6672        }
6673
6674        let has_both = user_rules_text.is_some() && rules_file_text.is_some();
6675
6676        Some(
6677            h_flex()
6678                .px_2p5()
6679                .child(
6680                    Icon::new(IconName::Attach)
6681                        .size(IconSize::XSmall)
6682                        .color(Color::Disabled),
6683                )
6684                .when_some(user_rules_text, |parent, user_rules_text| {
6685                    parent.child(
6686                        h_flex()
6687                            .id("user-rules")
6688                            .ml_1()
6689                            .mr_1p5()
6690                            .child(
6691                                Label::new(user_rules_text)
6692                                    .size(LabelSize::XSmall)
6693                                    .color(Color::Muted)
6694                                    .truncate(),
6695                            )
6696                            .hover(|s| s.bg(cx.theme().colors().element_hover))
6697                            .tooltip(Tooltip::text("View User Rules"))
6698                            .on_click(move |_event, window, cx| {
6699                                window.dispatch_action(
6700                                    Box::new(OpenRulesLibrary {
6701                                        prompt_to_select: first_user_rules_id,
6702                                    }),
6703                                    cx,
6704                                )
6705                            }),
6706                    )
6707                })
6708                .when(has_both, |this| {
6709                    this.child(
6710                        Label::new("")
6711                            .size(LabelSize::XSmall)
6712                            .color(Color::Disabled),
6713                    )
6714                })
6715                .when_some(rules_file_text, |parent, rules_file_text| {
6716                    parent.child(
6717                        h_flex()
6718                            .id("project-rules")
6719                            .ml_1p5()
6720                            .child(
6721                                Label::new(rules_file_text)
6722                                    .size(LabelSize::XSmall)
6723                                    .color(Color::Muted),
6724                            )
6725                            .hover(|s| s.bg(cx.theme().colors().element_hover))
6726                            .tooltip(Tooltip::text("View Project Rules"))
6727                            .on_click(cx.listener(Self::handle_open_rules)),
6728                    )
6729                })
6730                .into_any(),
6731        )
6732    }
6733
6734    fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
6735        cx.theme()
6736            .colors()
6737            .element_background
6738            .blend(cx.theme().colors().editor_foreground.opacity(0.025))
6739    }
6740
6741    fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
6742        cx.theme().colors().border.opacity(0.8)
6743    }
6744
6745    fn tool_name_font_size(&self) -> Rems {
6746        rems_from_px(13.)
6747    }
6748
6749    pub(crate) fn render_thread_error(
6750        &mut self,
6751        window: &mut Window,
6752        cx: &mut Context<Self>,
6753    ) -> Option<Div> {
6754        let content = match self.thread_error.as_ref()? {
6755            ThreadError::Other { message, .. } => {
6756                self.render_any_thread_error(message.clone(), window, cx)
6757            }
6758            ThreadError::Refusal => self.render_refusal_error(cx),
6759            ThreadError::AuthenticationRequired(error) => {
6760                self.render_authentication_required_error(error.clone(), cx)
6761            }
6762            ThreadError::PaymentRequired => self.render_payment_required_error(cx),
6763        };
6764
6765        Some(div().child(content))
6766    }
6767
6768    fn render_refusal_error(&self, cx: &mut Context<'_, Self>) -> Callout {
6769        let model_or_agent_name = self.current_model_name(cx);
6770        let refusal_message = format!(
6771            "{} refused to respond to this prompt. \
6772            This can happen when a model believes the prompt violates its content policy \
6773            or safety guidelines, so rephrasing it can sometimes address the issue.",
6774            model_or_agent_name
6775        );
6776
6777        Callout::new()
6778            .severity(Severity::Error)
6779            .title("Request Refused")
6780            .icon(IconName::XCircle)
6781            .description(refusal_message.clone())
6782            .actions_slot(self.create_copy_button(&refusal_message))
6783            .dismiss_action(self.dismiss_error_button(cx))
6784    }
6785
6786    fn render_authentication_required_error(
6787        &self,
6788        error: SharedString,
6789        cx: &mut Context<Self>,
6790    ) -> Callout {
6791        Callout::new()
6792            .severity(Severity::Error)
6793            .title("Authentication Required")
6794            .icon(IconName::XCircle)
6795            .description(error.clone())
6796            .actions_slot(
6797                h_flex()
6798                    .gap_0p5()
6799                    .child(self.authenticate_button(cx))
6800                    .child(self.create_copy_button(error)),
6801            )
6802            .dismiss_action(self.dismiss_error_button(cx))
6803    }
6804
6805    fn render_payment_required_error(&self, cx: &mut Context<Self>) -> Callout {
6806        const ERROR_MESSAGE: &str =
6807            "You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
6808
6809        Callout::new()
6810            .severity(Severity::Error)
6811            .icon(IconName::XCircle)
6812            .title("Free Usage Exceeded")
6813            .description(ERROR_MESSAGE)
6814            .actions_slot(
6815                h_flex()
6816                    .gap_0p5()
6817                    .child(self.upgrade_button(cx))
6818                    .child(self.create_copy_button(ERROR_MESSAGE)),
6819            )
6820            .dismiss_action(self.dismiss_error_button(cx))
6821    }
6822
6823    fn upgrade_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
6824        Button::new("upgrade", "Upgrade")
6825            .label_size(LabelSize::Small)
6826            .style(ButtonStyle::Tinted(ui::TintColor::Accent))
6827            .on_click(cx.listener({
6828                move |this, _, _, cx| {
6829                    this.clear_thread_error(cx);
6830                    cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx));
6831                }
6832            }))
6833    }
6834
6835    fn authenticate_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
6836        Button::new("authenticate", "Authenticate")
6837            .label_size(LabelSize::Small)
6838            .style(ButtonStyle::Filled)
6839            .on_click(cx.listener({
6840                move |this, _, window, cx| {
6841                    let server_view = this.server_view.clone();
6842                    let agent_name = this.agent_name.clone();
6843
6844                    this.clear_thread_error(cx);
6845                    if let Some(message) = this.in_flight_prompt.take() {
6846                        this.message_editor.update(cx, |editor, cx| {
6847                            editor.set_message(message, window, cx);
6848                        });
6849                    }
6850                    let connection = this.thread.read(cx).connection().clone();
6851                    window.defer(cx, |window, cx| {
6852                        AcpServerView::handle_auth_required(
6853                            server_view,
6854                            AuthRequired::new(),
6855                            agent_name,
6856                            connection,
6857                            window,
6858                            cx,
6859                        );
6860                    })
6861                }
6862            }))
6863    }
6864
6865    fn current_model_name(&self, cx: &App) -> SharedString {
6866        // For native agent (Zed Agent), use the specific model name (e.g., "Claude 3.5 Sonnet")
6867        // For ACP agents, use the agent name (e.g., "Claude Code", "Gemini CLI")
6868        // This provides better clarity about what refused the request
6869        if self.as_native_connection(cx).is_some() {
6870            self.model_selector
6871                .clone()
6872                .and_then(|selector| selector.read(cx).active_model(cx))
6873                .map(|model| model.name.clone())
6874                .unwrap_or_else(|| SharedString::from("The model"))
6875        } else {
6876            // ACP agent - use the agent name (e.g., "Claude Code", "Gemini CLI")
6877            self.agent_name.clone()
6878        }
6879    }
6880
6881    fn render_any_thread_error(
6882        &mut self,
6883        error: SharedString,
6884        window: &mut Window,
6885        cx: &mut Context<'_, Self>,
6886    ) -> Callout {
6887        let can_resume = self.thread.read(cx).can_retry(cx);
6888
6889        let markdown = if let Some(markdown) = &self.thread_error_markdown {
6890            markdown.clone()
6891        } else {
6892            let markdown = cx.new(|cx| Markdown::new(error.clone(), None, None, cx));
6893            self.thread_error_markdown = Some(markdown.clone());
6894            markdown
6895        };
6896
6897        let markdown_style =
6898            MarkdownStyle::themed(MarkdownFont::Agent, window, cx).with_muted_text(cx);
6899        let description = self
6900            .render_markdown(markdown, markdown_style)
6901            .into_any_element();
6902
6903        Callout::new()
6904            .severity(Severity::Error)
6905            .icon(IconName::XCircle)
6906            .title("An Error Happened")
6907            .description_slot(description)
6908            .actions_slot(
6909                h_flex()
6910                    .gap_0p5()
6911                    .when(can_resume, |this| {
6912                        this.child(
6913                            IconButton::new("retry", IconName::RotateCw)
6914                                .icon_size(IconSize::Small)
6915                                .tooltip(Tooltip::text("Retry Generation"))
6916                                .on_click(cx.listener(|this, _, _window, cx| {
6917                                    this.retry_generation(cx);
6918                                })),
6919                        )
6920                    })
6921                    .child(self.create_copy_button(error.to_string())),
6922            )
6923            .dismiss_action(self.dismiss_error_button(cx))
6924    }
6925
6926    fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
6927        let workspace = self.workspace.clone();
6928        MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
6929            open_link(text, &workspace, window, cx);
6930        })
6931    }
6932
6933    fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
6934        let message = message.into();
6935
6936        CopyButton::new("copy-error-message", message).tooltip_label("Copy Error Message")
6937    }
6938
6939    fn dismiss_error_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
6940        IconButton::new("dismiss", IconName::Close)
6941            .icon_size(IconSize::Small)
6942            .tooltip(Tooltip::text("Dismiss"))
6943            .on_click(cx.listener({
6944                move |this, _, _, cx| {
6945                    this.clear_thread_error(cx);
6946                    cx.notify();
6947                }
6948            }))
6949    }
6950
6951    fn render_resume_notice(_cx: &Context<Self>) -> AnyElement {
6952        let description = "This agent does not support viewing previous messages. However, your session will still continue from where you last left off.";
6953
6954        div()
6955            .px_2()
6956            .pt_2()
6957            .pb_3()
6958            .w_full()
6959            .child(
6960                Callout::new()
6961                    .severity(Severity::Info)
6962                    .icon(IconName::Info)
6963                    .title("Resumed Session")
6964                    .description(description),
6965            )
6966            .into_any_element()
6967    }
6968
6969    fn update_recent_history_from_cache(
6970        &mut self,
6971        history: &Entity<AcpThreadHistory>,
6972        cx: &mut Context<Self>,
6973    ) {
6974        self.recent_history_entries = history.read(cx).get_recent_sessions(3);
6975        self.hovered_recent_history_item = None;
6976        cx.notify();
6977    }
6978
6979    fn render_empty_state_section_header(
6980        &self,
6981        label: impl Into<SharedString>,
6982        action_slot: Option<AnyElement>,
6983        cx: &mut Context<Self>,
6984    ) -> impl IntoElement {
6985        div().pl_1().pr_1p5().child(
6986            h_flex()
6987                .mt_2()
6988                .pl_1p5()
6989                .pb_1()
6990                .w_full()
6991                .justify_between()
6992                .border_b_1()
6993                .border_color(cx.theme().colors().border_variant)
6994                .child(
6995                    Label::new(label.into())
6996                        .size(LabelSize::Small)
6997                        .color(Color::Muted),
6998                )
6999                .children(action_slot),
7000        )
7001    }
7002
7003    fn render_recent_history(&self, cx: &mut Context<Self>) -> AnyElement {
7004        let render_history = !self.recent_history_entries.is_empty();
7005
7006        v_flex()
7007            .size_full()
7008            .when(render_history, |this| {
7009                let recent_history = self.recent_history_entries.clone();
7010                this.justify_end().child(
7011                    v_flex()
7012                        .child(
7013                            self.render_empty_state_section_header(
7014                                "Recent",
7015                                Some(
7016                                    Button::new("view-history", "View All")
7017                                        .style(ButtonStyle::Subtle)
7018                                        .label_size(LabelSize::Small)
7019                                        .key_binding(
7020                                            KeyBinding::for_action_in(
7021                                                &OpenHistory,
7022                                                &self.focus_handle(cx),
7023                                                cx,
7024                                            )
7025                                            .map(|kb| kb.size(rems_from_px(12.))),
7026                                        )
7027                                        .on_click(move |_event, window, cx| {
7028                                            window.dispatch_action(OpenHistory.boxed_clone(), cx);
7029                                        })
7030                                        .into_any_element(),
7031                                ),
7032                                cx,
7033                            ),
7034                        )
7035                        .child(v_flex().p_1().pr_1p5().gap_1().children({
7036                            let supports_delete = self.history.read(cx).supports_delete();
7037                            recent_history
7038                                .into_iter()
7039                                .enumerate()
7040                                .map(move |(index, entry)| {
7041                                    // TODO: Add keyboard navigation.
7042                                    let is_hovered =
7043                                        self.hovered_recent_history_item == Some(index);
7044                                    crate::acp::thread_history::AcpHistoryEntryElement::new(
7045                                        entry,
7046                                        self.server_view.clone(),
7047                                    )
7048                                    .hovered(is_hovered)
7049                                    .supports_delete(supports_delete)
7050                                    .on_hover(cx.listener(move |this, is_hovered, _window, cx| {
7051                                        if *is_hovered {
7052                                            this.hovered_recent_history_item = Some(index);
7053                                        } else if this.hovered_recent_history_item == Some(index) {
7054                                            this.hovered_recent_history_item = None;
7055                                        }
7056                                        cx.notify();
7057                                    }))
7058                                    .into_any_element()
7059                                })
7060                        })),
7061                )
7062            })
7063            .into_any()
7064    }
7065
7066    fn render_codex_windows_warning(&self, cx: &mut Context<Self>) -> Callout {
7067        Callout::new()
7068            .icon(IconName::Warning)
7069            .severity(Severity::Warning)
7070            .title("Codex on Windows")
7071            .description("For best performance, run Codex in Windows Subsystem for Linux (WSL2)")
7072            .actions_slot(
7073                Button::new("open-wsl-modal", "Open in WSL")
7074                    .icon_size(IconSize::Small)
7075                    .icon_color(Color::Muted)
7076                    .on_click(cx.listener({
7077                        move |_, _, _window, cx| {
7078                            #[cfg(windows)]
7079                            _window.dispatch_action(
7080                                zed_actions::wsl_actions::OpenWsl::default().boxed_clone(),
7081                                cx,
7082                            );
7083                            cx.notify();
7084                        }
7085                    })),
7086            )
7087            .dismiss_action(
7088                IconButton::new("dismiss", IconName::Close)
7089                    .icon_size(IconSize::Small)
7090                    .icon_color(Color::Muted)
7091                    .tooltip(Tooltip::text("Dismiss Warning"))
7092                    .on_click(cx.listener({
7093                        move |this, _, _, cx| {
7094                            this.show_codex_windows_warning = false;
7095                            cx.notify();
7096                        }
7097                    })),
7098            )
7099    }
7100
7101    fn render_new_version_callout(&self, version: &SharedString, cx: &mut Context<Self>) -> Div {
7102        let server_view = self.server_view.clone();
7103        v_flex().w_full().justify_end().child(
7104            h_flex()
7105                .p_2()
7106                .pr_3()
7107                .w_full()
7108                .gap_1p5()
7109                .border_t_1()
7110                .border_color(cx.theme().colors().border)
7111                .bg(cx.theme().colors().element_background)
7112                .child(
7113                    h_flex()
7114                        .flex_1()
7115                        .gap_1p5()
7116                        .child(
7117                            Icon::new(IconName::Download)
7118                                .color(Color::Accent)
7119                                .size(IconSize::Small),
7120                        )
7121                        .child(Label::new("New version available").size(LabelSize::Small)),
7122                )
7123                .child(
7124                    Button::new("update-button", format!("Update to v{}", version))
7125                        .label_size(LabelSize::Small)
7126                        .style(ButtonStyle::Tinted(TintColor::Accent))
7127                        .on_click(move |_, window, cx| {
7128                            server_view
7129                                .update(cx, |view, cx| view.reset(window, cx))
7130                                .ok();
7131                        }),
7132                ),
7133        )
7134    }
7135
7136    fn render_token_limit_callout(&self, cx: &mut Context<Self>) -> Option<Callout> {
7137        if self.token_limit_callout_dismissed {
7138            return None;
7139        }
7140
7141        let token_usage = self.thread.read(cx).token_usage()?;
7142        let ratio = token_usage.ratio();
7143
7144        let (severity, icon, title) = match ratio {
7145            acp_thread::TokenUsageRatio::Normal => return None,
7146            acp_thread::TokenUsageRatio::Warning => (
7147                Severity::Warning,
7148                IconName::Warning,
7149                "Thread reaching the token limit soon",
7150            ),
7151            acp_thread::TokenUsageRatio::Exceeded => (
7152                Severity::Error,
7153                IconName::XCircle,
7154                "Thread reached the token limit",
7155            ),
7156        };
7157
7158        let description = "To continue, start a new thread from a summary.";
7159
7160        Some(
7161            Callout::new()
7162                .severity(severity)
7163                .icon(icon)
7164                .title(title)
7165                .description(description)
7166                .actions_slot(
7167                    h_flex().gap_0p5().child(
7168                        Button::new("start-new-thread", "Start New Thread")
7169                            .label_size(LabelSize::Small)
7170                            .on_click(cx.listener(|this, _, window, cx| {
7171                                let session_id = this.thread.read(cx).session_id().clone();
7172                                window.dispatch_action(
7173                                    crate::NewNativeAgentThreadFromSummary {
7174                                        from_session_id: session_id,
7175                                    }
7176                                    .boxed_clone(),
7177                                    cx,
7178                                );
7179                            })),
7180                    ),
7181                )
7182                .dismiss_action(self.dismiss_error_button(cx)),
7183        )
7184    }
7185
7186    fn open_permission_dropdown(
7187        &mut self,
7188        _: &crate::OpenPermissionDropdown,
7189        window: &mut Window,
7190        cx: &mut Context<Self>,
7191    ) {
7192        self.permission_dropdown_handle.clone().toggle(window, cx);
7193    }
7194
7195    fn open_add_context_menu(
7196        &mut self,
7197        _action: &OpenAddContextMenu,
7198        window: &mut Window,
7199        cx: &mut Context<Self>,
7200    ) {
7201        let menu_handle = self.add_context_menu_handle.clone();
7202        window.defer(cx, move |window, cx| {
7203            menu_handle.toggle(window, cx);
7204        });
7205    }
7206
7207    fn cycle_thinking_effort(&mut self, cx: &mut Context<Self>) {
7208        if !cx.has_flag::<CloudThinkingEffortFeatureFlag>() {
7209            return;
7210        }
7211
7212        let Some(thread) = self.as_native_thread(cx) else {
7213            return;
7214        };
7215
7216        let (effort_levels, current_effort) = {
7217            let thread_ref = thread.read(cx);
7218            let Some(model) = thread_ref.model() else {
7219                return;
7220            };
7221            if !model.supports_thinking() || !thread_ref.thinking_enabled() {
7222                return;
7223            }
7224            let effort_levels = model.supported_effort_levels();
7225            if effort_levels.is_empty() {
7226                return;
7227            }
7228            let current_effort = thread_ref.thinking_effort().cloned();
7229            (effort_levels, current_effort)
7230        };
7231
7232        let current_index = current_effort.and_then(|current| {
7233            effort_levels
7234                .iter()
7235                .position(|level| level.value == current)
7236        });
7237        let next_index = match current_index {
7238            Some(index) => (index + 1) % effort_levels.len(),
7239            None => 0,
7240        };
7241        let next_effort = effort_levels[next_index].value.to_string();
7242
7243        thread.update(cx, |thread, cx| {
7244            thread.set_thinking_effort(Some(next_effort.clone()), cx);
7245
7246            let fs = thread.project().read(cx).fs().clone();
7247            update_settings_file(fs, cx, move |settings, _| {
7248                if let Some(agent) = settings.agent.as_mut()
7249                    && let Some(default_model) = agent.default_model.as_mut()
7250                {
7251                    default_model.effort = Some(next_effort);
7252                }
7253            });
7254        });
7255    }
7256
7257    fn toggle_thinking_effort_menu(
7258        &mut self,
7259        _action: &ToggleThinkingEffortMenu,
7260        window: &mut Window,
7261        cx: &mut Context<Self>,
7262    ) {
7263        let menu_handle = self.thinking_effort_menu_handle.clone();
7264        window.defer(cx, move |window, cx| {
7265            menu_handle.toggle(window, cx);
7266        });
7267    }
7268}
7269
7270impl Render for AcpThreadView {
7271    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
7272        let has_messages = self.list_state.item_count() > 0;
7273
7274        let conversation = v_flex().flex_1().map(|this| {
7275            let this = this.when(self.resumed_without_history, |this| {
7276                this.child(Self::render_resume_notice(cx))
7277            });
7278            if has_messages {
7279                let list_state = self.list_state.clone();
7280                this.child(self.render_entries(cx))
7281                    .vertical_scrollbar_for(&list_state, window, cx)
7282                    .into_any()
7283            } else {
7284                this.child(self.render_recent_history(cx)).into_any()
7285            }
7286        });
7287
7288        v_flex()
7289            .key_context("AcpThread")
7290            .track_focus(&self.focus_handle)
7291            .on_action(cx.listener(|this, _: &menu::Cancel, _, cx| {
7292                if this.parent_id.is_none() {
7293                    this.cancel_generation(cx);
7294                }
7295            }))
7296            .on_action(cx.listener(|this, _: &workspace::GoBack, window, cx| {
7297                if let Some(parent_session_id) = this.parent_id.clone() {
7298                    this.server_view
7299                        .update(cx, |view, cx| {
7300                            view.navigate_to_session(parent_session_id, window, cx);
7301                        })
7302                        .ok();
7303                }
7304            }))
7305            .on_action(cx.listener(Self::keep_all))
7306            .on_action(cx.listener(Self::reject_all))
7307            .on_action(cx.listener(Self::allow_always))
7308            .on_action(cx.listener(Self::allow_once))
7309            .on_action(cx.listener(Self::reject_once))
7310            .on_action(cx.listener(Self::handle_authorize_tool_call))
7311            .on_action(cx.listener(Self::handle_select_permission_granularity))
7312            .on_action(cx.listener(Self::open_permission_dropdown))
7313            .on_action(cx.listener(Self::open_add_context_menu))
7314            .on_action(cx.listener(|this, _: &ToggleThinkingMode, _window, cx| {
7315                if let Some(thread) = this.as_native_thread(cx) {
7316                    thread.update(cx, |thread, cx| {
7317                        thread.set_thinking_enabled(!thread.thinking_enabled(), cx);
7318                    });
7319                }
7320            }))
7321            .on_action(cx.listener(|this, _: &CycleThinkingEffort, _window, cx| {
7322                this.cycle_thinking_effort(cx);
7323            }))
7324            .on_action(cx.listener(Self::toggle_thinking_effort_menu))
7325            .on_action(cx.listener(|this, _: &SendNextQueuedMessage, window, cx| {
7326                this.send_queued_message_at_index(0, true, window, cx);
7327            }))
7328            .on_action(cx.listener(|this, _: &RemoveFirstQueuedMessage, _, cx| {
7329                this.remove_from_queue(0, cx);
7330                cx.notify();
7331            }))
7332            .on_action(cx.listener(|this, _: &EditFirstQueuedMessage, window, cx| {
7333                if let Some(editor) = this.queued_message_editors.first() {
7334                    window.focus(&editor.focus_handle(cx), cx);
7335                }
7336            }))
7337            .on_action(cx.listener(|this, _: &ClearMessageQueue, _, cx| {
7338                this.local_queued_messages.clear();
7339                this.sync_queue_flag_to_native_thread(cx);
7340                this.can_fast_track_queue = false;
7341                cx.notify();
7342            }))
7343            .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
7344                if let Some(config_options_view) = this.config_options_view.clone() {
7345                    let handled = config_options_view.update(cx, |view, cx| {
7346                        view.toggle_category_picker(
7347                            acp::SessionConfigOptionCategory::Mode,
7348                            window,
7349                            cx,
7350                        )
7351                    });
7352                    if handled {
7353                        return;
7354                    }
7355                }
7356
7357                if let Some(profile_selector) = this.profile_selector.clone() {
7358                    profile_selector.read(cx).menu_handle().toggle(window, cx);
7359                } else if let Some(mode_selector) = this.mode_selector.clone() {
7360                    mode_selector.read(cx).menu_handle().toggle(window, cx);
7361                }
7362            }))
7363            .on_action(cx.listener(|this, _: &CycleModeSelector, window, cx| {
7364                if let Some(config_options_view) = this.config_options_view.clone() {
7365                    let handled = config_options_view.update(cx, |view, cx| {
7366                        view.cycle_category_option(
7367                            acp::SessionConfigOptionCategory::Mode,
7368                            false,
7369                            cx,
7370                        )
7371                    });
7372                    if handled {
7373                        return;
7374                    }
7375                }
7376
7377                if let Some(profile_selector) = this.profile_selector.clone() {
7378                    profile_selector.update(cx, |profile_selector, cx| {
7379                        profile_selector.cycle_profile(cx);
7380                    });
7381                } else if let Some(mode_selector) = this.mode_selector.clone() {
7382                    mode_selector.update(cx, |mode_selector, cx| {
7383                        mode_selector.cycle_mode(window, cx);
7384                    });
7385                }
7386            }))
7387            .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
7388                if let Some(config_options_view) = this.config_options_view.clone() {
7389                    let handled = config_options_view.update(cx, |view, cx| {
7390                        view.toggle_category_picker(
7391                            acp::SessionConfigOptionCategory::Model,
7392                            window,
7393                            cx,
7394                        )
7395                    });
7396                    if handled {
7397                        return;
7398                    }
7399                }
7400
7401                if let Some(model_selector) = this.model_selector.clone() {
7402                    model_selector
7403                        .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
7404                }
7405            }))
7406            .on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
7407                if let Some(config_options_view) = this.config_options_view.clone() {
7408                    let handled = config_options_view.update(cx, |view, cx| {
7409                        view.cycle_category_option(
7410                            acp::SessionConfigOptionCategory::Model,
7411                            true,
7412                            cx,
7413                        )
7414                    });
7415                    if handled {
7416                        return;
7417                    }
7418                }
7419
7420                if let Some(model_selector) = this.model_selector.clone() {
7421                    model_selector.update(cx, |model_selector, cx| {
7422                        model_selector.cycle_favorite_models(window, cx);
7423                    });
7424                }
7425            }))
7426            .size_full()
7427            .children(self.render_subagent_titlebar(cx))
7428            .child(conversation)
7429            .children(self.render_activity_bar(window, cx))
7430            .when(self.show_codex_windows_warning, |this| {
7431                this.child(self.render_codex_windows_warning(cx))
7432            })
7433            .children(self.render_thread_retry_status_callout())
7434            .children(self.render_thread_error(window, cx))
7435            .when_some(
7436                match has_messages {
7437                    true => None,
7438                    false => self.new_server_version_available.clone(),
7439                },
7440                |this, version| this.child(self.render_new_version_callout(&version, cx)),
7441            )
7442            .children(self.render_token_limit_callout(cx))
7443            .child(self.render_message_editor(window, cx))
7444    }
7445}
7446
7447pub(crate) fn open_link(
7448    url: SharedString,
7449    workspace: &WeakEntity<Workspace>,
7450    window: &mut Window,
7451    cx: &mut App,
7452) {
7453    let Some(workspace) = workspace.upgrade() else {
7454        cx.open_url(&url);
7455        return;
7456    };
7457
7458    if let Some(mention) = MentionUri::parse(&url, workspace.read(cx).path_style(cx)).log_err() {
7459        workspace.update(cx, |workspace, cx| match mention {
7460            MentionUri::File { abs_path } => {
7461                let project = workspace.project();
7462                let Some(path) =
7463                    project.update(cx, |project, cx| project.find_project_path(abs_path, cx))
7464                else {
7465                    return;
7466                };
7467
7468                workspace
7469                    .open_path(path, None, true, window, cx)
7470                    .detach_and_log_err(cx);
7471            }
7472            MentionUri::PastedImage => {}
7473            MentionUri::Directory { abs_path } => {
7474                let project = workspace.project();
7475                let Some(entry_id) = project.update(cx, |project, cx| {
7476                    let path = project.find_project_path(abs_path, cx)?;
7477                    project.entry_for_path(&path, cx).map(|entry| entry.id)
7478                }) else {
7479                    return;
7480                };
7481
7482                project.update(cx, |_, cx| {
7483                    cx.emit(project::Event::RevealInProjectPanel(entry_id));
7484                });
7485            }
7486            MentionUri::Symbol {
7487                abs_path: path,
7488                line_range,
7489                ..
7490            }
7491            | MentionUri::Selection {
7492                abs_path: Some(path),
7493                line_range,
7494            } => {
7495                let project = workspace.project();
7496                let Some(path) =
7497                    project.update(cx, |project, cx| project.find_project_path(path, cx))
7498                else {
7499                    return;
7500                };
7501
7502                let item = workspace.open_path(path, None, true, window, cx);
7503                window
7504                    .spawn(cx, async move |cx| {
7505                        let Some(editor) = item.await?.downcast::<Editor>() else {
7506                            return Ok(());
7507                        };
7508                        let range =
7509                            Point::new(*line_range.start(), 0)..Point::new(*line_range.start(), 0);
7510                        editor
7511                            .update_in(cx, |editor, window, cx| {
7512                                editor.change_selections(
7513                                    SelectionEffects::scroll(Autoscroll::center()),
7514                                    window,
7515                                    cx,
7516                                    |s| s.select_ranges(vec![range]),
7517                                );
7518                            })
7519                            .ok();
7520                        anyhow::Ok(())
7521                    })
7522                    .detach_and_log_err(cx);
7523            }
7524            MentionUri::Selection { abs_path: None, .. } => {}
7525            MentionUri::Thread { id, name } => {
7526                if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
7527                    panel.update(cx, |panel, cx| {
7528                        panel.open_thread(
7529                            AgentSessionInfo {
7530                                session_id: id,
7531                                cwd: None,
7532                                title: Some(name.into()),
7533                                updated_at: None,
7534                                meta: None,
7535                            },
7536                            window,
7537                            cx,
7538                        )
7539                    });
7540                }
7541            }
7542            MentionUri::TextThread { path, .. } => {
7543                if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
7544                    panel.update(cx, |panel, cx| {
7545                        panel
7546                            .open_saved_text_thread(path.as_path().into(), window, cx)
7547                            .detach_and_log_err(cx);
7548                    });
7549                }
7550            }
7551            MentionUri::Rule { id, .. } => {
7552                let PromptId::User { uuid } = id else {
7553                    return;
7554                };
7555                window.dispatch_action(
7556                    Box::new(OpenRulesLibrary {
7557                        prompt_to_select: Some(uuid.0),
7558                    }),
7559                    cx,
7560                )
7561            }
7562            MentionUri::Fetch { url } => {
7563                cx.open_url(url.as_str());
7564            }
7565            MentionUri::Diagnostics { .. } => {}
7566            MentionUri::TerminalSelection { .. } => {}
7567        })
7568    } else {
7569        cx.open_url(&url);
7570    }
7571}