active_thread.rs

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