Standardize agent previews (#29790)

Nate Butler created

This PR makes agent previews render like any other preview in the
component preview list & pages.

Page:

![CleanShot 2025-05-02 at 09 17
12@2x](https://github.com/user-attachments/assets/8b611380-b686-4fd6-9c76-de27e35b0b38)

List:

![CleanShot 2025-05-02 at 09 17
33@2x](https://github.com/user-attachments/assets/ab063649-dc3c-4c95-969b-c3795b2197f2)


Release Notes:

- N/A

Change summary

crates/agent/src/message_editor.rs                |   2 
crates/agent/src/ui/agent_preview.rs              |  25 -
crates/component/src/component.rs                 |   5 
crates/component_preview/src/component_preview.rs | 187 +++++++++-------
4 files changed, 114 insertions(+), 105 deletions(-)

Detailed changes

crates/agent/src/message_editor.rs 🔗

@@ -1211,7 +1211,7 @@ impl Component for MessageEditor {
 }
 
 impl AgentPreview for MessageEditor {
-    fn create_preview(
+    fn agent_preview(
         workspace: WeakEntity<Workspace>,
         active_thread: Entity<ActiveThread>,
         thread_store: WeakEntity<ThreadStore>,

crates/agent/src/ui/agent_preview.rs 🔗

@@ -3,7 +3,7 @@ use component::ComponentId;
 use gpui::{App, Entity, WeakEntity};
 use linkme::distributed_slice;
 use std::sync::OnceLock;
-use ui::{AnyElement, Component, Window};
+use ui::{AnyElement, Component, ComponentScope, Window};
 use workspace::Workspace;
 
 use crate::{ActiveThread, ThreadStore};
@@ -22,27 +22,20 @@ pub type PreviewFn = fn(
 pub static __ALL_AGENT_PREVIEWS: [fn() -> (ComponentId, PreviewFn)] = [..];
 
 /// Trait that must be implemented by components that provide agent previews.
-pub trait AgentPreview: Component {
-    /// Get the ID for this component
-    ///
-    /// Eventually this will move to the component trait.
-    fn id() -> ComponentId
-    where
-        Self: Sized,
-    {
-        ComponentId(Self::name())
+pub trait AgentPreview: Component + Sized {
+    #[allow(unused)] // We can't know this is used due to the distributed slice
+    fn scope(&self) -> ComponentScope {
+        ComponentScope::Agent
     }
 
     /// Static method to create a preview for this component type
-    fn create_preview(
+    fn agent_preview(
         workspace: WeakEntity<Workspace>,
         active_thread: Entity<ActiveThread>,
         thread_store: WeakEntity<ThreadStore>,
         window: &mut Window,
         cx: &mut App,
-    ) -> Option<AnyElement>
-    where
-        Self: Sized;
+    ) -> Option<AnyElement>;
 }
 
 /// Register an agent preview for the given component type
@@ -55,8 +48,8 @@ macro_rules! register_agent_preview {
             $crate::ui::agent_preview::PreviewFn,
         ) = || {
             (
-                <$type as $crate::ui::agent_preview::AgentPreview>::id(),
-                <$type as $crate::ui::agent_preview::AgentPreview>::create_preview,
+                <$type as component::Component>::id(),
+                <$type as $crate::ui::agent_preview::AgentPreview>::agent_preview,
             )
         };
     };

crates/component/src/component.rs 🔗

@@ -18,6 +18,9 @@ pub trait Component {
     fn name() -> &'static str {
         std::any::type_name::<Self>()
     }
+    fn id() -> ComponentId {
+        ComponentId(Self::name())
+    }
     /// Returns a name that the component should be sorted by.
     ///
     /// Implement this if the component should be sorted in an alternate order than its name.
@@ -81,7 +84,7 @@ pub fn register_component<T: Component>() {
     let component_data = (T::scope(), T::name(), T::sort_name(), T::description());
     let mut data = COMPONENT_DATA.write();
     data.components.push(component_data);
-    data.previews.insert(T::name(), T::preview);
+    data.previews.insert(T::id().0, T::preview);
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]

crates/component_preview/src/component_preview.rs 🔗

@@ -110,18 +110,7 @@ struct ComponentPreview {
     active_page: PreviewPage,
     components: Vec<ComponentMetadata>,
     component_list: ListState,
-    agent_previews: Vec<
-        Box<
-            dyn Fn(
-                &Self,
-                WeakEntity<Workspace>,
-                Entity<ActiveThread>,
-                WeakEntity<ThreadStore>,
-                &mut Window,
-                &mut App,
-            ) -> Option<AnyElement>,
-        >,
-    >,
+    agent_previews: Vec<ComponentId>,
     cursor_index: usize,
     language_registry: Arc<LanguageRegistry>,
     workspace: WeakEntity<Workspace>,
@@ -191,38 +180,7 @@ impl ComponentPreview {
         );
 
         // Initialize agent previews
-        let agent_previews = agent::all_agent_previews()
-            .into_iter()
-            .map(|id| {
-                Box::new(
-                    move |_self: &ComponentPreview,
-                          workspace: WeakEntity<Workspace>,
-                          active_thread: Entity<ActiveThread>,
-                          thread_store: WeakEntity<ThreadStore>,
-                          window: &mut Window,
-                          cx: &mut App| {
-                        agent::get_agent_preview(
-                            &id,
-                            workspace,
-                            active_thread,
-                            thread_store,
-                            window,
-                            cx,
-                        )
-                    },
-                )
-                    as Box<
-                        dyn Fn(
-                            &ComponentPreview,
-                            WeakEntity<Workspace>,
-                            Entity<ActiveThread>,
-                            WeakEntity<ThreadStore>,
-                            &mut Window,
-                            &mut App,
-                        ) -> Option<AnyElement>,
-                    >
-            })
-            .collect::<Vec<_>>();
+        let agent_previews = agent::all_agent_previews();
 
         let mut component_preview = Self {
             workspace_id: None,
@@ -635,44 +593,65 @@ impl ComponentPreview {
 
         let description = component.description();
 
-        v_flex()
-            .py_2()
-            .child(
-                v_flex()
-                    .border_1()
-                    .border_color(cx.theme().colors().border)
-                    .rounded_sm()
-                    .w_full()
-                    .gap_4()
-                    .py_4()
-                    .px_6()
-                    .flex_none()
-                    .child(
-                        v_flex()
-                            .gap_1()
-                            .child(
-                                h_flex().gap_1().text_xl().child(div().child(name)).when(
-                                    !matches!(scope, ComponentScope::None),
-                                    |this| {
-                                        this.child(div().opacity(0.5).child(format!("({})", scope)))
-                                    },
-                                ),
+        // Build the content container
+        let mut preview_container = v_flex().py_2().child(
+            v_flex()
+                .border_1()
+                .border_color(cx.theme().colors().border)
+                .rounded_sm()
+                .w_full()
+                .gap_4()
+                .py_4()
+                .px_6()
+                .flex_none()
+                .child(
+                    v_flex()
+                        .gap_1()
+                        .child(
+                            h_flex()
+                                .gap_1()
+                                .text_xl()
+                                .child(div().child(name))
+                                .when(!matches!(scope, ComponentScope::None), |this| {
+                                    this.child(div().opacity(0.5).child(format!("({})", scope)))
+                                }),
+                        )
+                        .when_some(description, |this, description| {
+                            this.child(
+                                div()
+                                    .text_ui_sm(cx)
+                                    .text_color(cx.theme().colors().text_muted)
+                                    .max_w(px(600.0))
+                                    .child(description),
                             )
-                            .when_some(description, |this, description| {
-                                this.child(
-                                    div()
-                                        .text_ui_sm(cx)
-                                        .text_color(cx.theme().colors().text_muted)
-                                        .max_w(px(600.0))
-                                        .child(description),
-                                )
-                            }),
-                    )
-                    .when_some(component.preview(), |this, preview| {
-                        this.children(preview(window, cx))
-                    }),
-            )
-            .into_any_element()
+                        }),
+                ),
+        );
+
+        // Check if the component's scope is Agent
+        if scope == ComponentScope::Agent {
+            if let (Some(thread_store), Some(active_thread)) = (
+                self.thread_store.as_ref().map(|ts| ts.downgrade()),
+                self.active_thread.clone(),
+            ) {
+                if let Some(element) = agent::get_agent_preview(
+                    &component.id(),
+                    self.workspace.clone(),
+                    active_thread,
+                    thread_store,
+                    window,
+                    cx,
+                ) {
+                    preview_container = preview_container.child(element);
+                } else if let Some(preview) = component.preview() {
+                    preview_container = preview_container.children(preview(window, cx));
+                }
+            }
+        } else if let Some(preview) = component.preview() {
+            preview_container = preview_container.children(preview(window, cx));
+        }
+
+        preview_container.into_any_element()
     }
 
     fn render_all_components(&self, cx: &Context<Self>) -> impl IntoElement {
@@ -711,7 +690,12 @@ impl ComponentPreview {
             v_flex()
                 .id("render-component-page")
                 .size_full()
-                .child(ComponentPreviewPage::new(component.clone()))
+                .child(ComponentPreviewPage::new(
+                    component.clone(),
+                    self.workspace.clone(),
+                    self.thread_store.as_ref().map(|ts| ts.downgrade()),
+                    self.active_thread.clone(),
+                ))
                 .into_any_element()
         } else {
             v_flex()
@@ -732,13 +716,13 @@ impl ComponentPreview {
             .id("render-active-thread")
             .size_full()
             .child(
-                v_flex().children(self.agent_previews.iter().filter_map(|preview_fn| {
+                v_flex().children(self.agent_previews.iter().filter_map(|component_id| {
                     if let (Some(thread_store), Some(active_thread)) = (
                         self.thread_store.as_ref().map(|ts| ts.downgrade()),
                         self.active_thread.clone(),
                     ) {
-                        preview_fn(
-                            self,
+                        agent::get_agent_preview(
+                            component_id,
                             self.workspace.clone(),
                             active_thread,
                             thread_store,
@@ -894,7 +878,7 @@ impl Default for ActivePageId {
 
 impl From<ComponentId> for ActivePageId {
     fn from(id: ComponentId) -> Self {
-        ActivePageId(id.0.to_string())
+        Self(id.0.to_string())
     }
 }
 
@@ -1073,16 +1057,25 @@ impl SerializableItem for ComponentPreview {
 pub struct ComponentPreviewPage {
     // languages: Arc<LanguageRegistry>,
     component: ComponentMetadata,
+    workspace: WeakEntity<Workspace>,
+    thread_store: Option<WeakEntity<ThreadStore>>,
+    active_thread: Option<Entity<ActiveThread>>,
 }
 
 impl ComponentPreviewPage {
     pub fn new(
         component: ComponentMetadata,
+        workspace: WeakEntity<Workspace>,
+        thread_store: Option<WeakEntity<ThreadStore>>,
+        active_thread: Option<Entity<ActiveThread>>,
         // languages: Arc<LanguageRegistry>
     ) -> Self {
         Self {
             // languages,
             component,
+            workspace,
+            thread_store,
+            active_thread,
         }
     }
 
@@ -1113,12 +1106,32 @@ impl ComponentPreviewPage {
     }
 
     fn render_preview(&self, window: &mut Window, cx: &mut App) -> impl IntoElement {
+        // Try to get agent preview first if we have an active thread
+        let maybe_agent_preview = if let (Some(thread_store), Some(active_thread)) =
+            (self.thread_store.as_ref(), self.active_thread.as_ref())
+        {
+            agent::get_agent_preview(
+                &self.component.id(),
+                self.workspace.clone(),
+                active_thread.clone(),
+                thread_store.clone(),
+                window,
+                cx,
+            )
+        } else {
+            None
+        };
+
         v_flex()
             .flex_1()
             .px_12()
             .py_6()
             .bg(cx.theme().colors().editor_background)
-            .child(if let Some(preview) = self.component.preview() {
+            .child(if let Some(element) = maybe_agent_preview {
+                // Use agent preview if available
+                element
+            } else if let Some(preview) = self.component.preview() {
+                // Fall back to component preview
                 preview(window, cx).unwrap_or_else(|| {
                     div()
                         .child("Failed to load preview. This path should be unreachable")