Standardize agent previews (#29790)

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
This commit is contained in:
Nate Butler 2025-05-02 09:32:59 -04:00 committed by GitHub
parent 225deb6785
commit 3bd7ae6e5b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 114 additions and 105 deletions

View File

@ -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>,

View File

@ -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,
)
};
};

View File

@ -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)]

View File

@ -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,9 +593,8 @@ impl ComponentPreview {
let description = component.description();
v_flex()
.py_2()
.child(
// Build the content container
let mut preview_container = v_flex().py_2().child(
v_flex()
.border_1()
.border_color(cx.theme().colors().border)
@ -651,12 +608,13 @@ impl ComponentPreview {
v_flex()
.gap_1()
.child(
h_flex().gap_1().text_xl().child(div().child(name)).when(
!matches!(scope, ComponentScope::None),
|this| {
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(
@ -667,12 +625,33 @@ impl ComponentPreview {
.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")