active_thread.rs

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