active_thread.rs

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