thread_view.rs

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