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