thread_view.rs

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