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