Add Agent Preview trait (#29760)

Nate Butler created

Like the title says

Release Notes:

- N/A

Change summary

Cargo.lock                                                    |   3 
crates/agent/src/assistant.rs                                 |   2 
crates/agent/src/message_editor.rs                            |  57 +
crates/agent/src/ui.rs                                        |   3 
crates/agent/src/ui/agent_preview.rs                          |  99 ++
crates/agent/src/ui/upsell.rs                                 | 163 ++++
crates/agent/src/ui/usage_banner.rs                           |   4 
crates/component_preview/Cargo.toml                           |  11 
crates/component_preview/src/component_preview.rs             | 203 ++++
crates/component_preview/src/preview_support.rs               |   1 
crates/component_preview/src/preview_support/active_thread.rs |  69 +
11 files changed, 595 insertions(+), 20 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3239,7 +3239,9 @@ dependencies = [
 name = "component_preview"
 version = "0.1.0"
 dependencies = [
+ "agent",
  "anyhow",
+ "assistant_tool",
  "client",
  "collections",
  "component",
@@ -3249,6 +3251,7 @@ dependencies = [
  "log",
  "notifications",
  "project",
+ "prompt_store",
  "serde",
  "ui",
  "ui_input",

crates/agent/src/assistant.rs 🔗

@@ -46,6 +46,8 @@ pub use crate::inline_assistant::InlineAssistant;
 pub use crate::thread::{Message, MessageSegment, Thread, ThreadEvent};
 pub use crate::thread_store::ThreadStore;
 pub use agent_diff::{AgentDiff, AgentDiffToolbar};
+pub use context_store::ContextStore;
+pub use ui::{all_agent_previews, get_agent_preview};
 
 actions!(
     agent,

crates/agent/src/message_editor.rs 🔗

@@ -4,7 +4,7 @@ use std::sync::Arc;
 use crate::assistant_model_selector::{AssistantModelSelector, ModelType};
 use crate::context::{ContextLoadResult, load_context};
 use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
-use crate::ui::AnimatedLabel;
+use crate::ui::{AgentPreview, AnimatedLabel};
 use buffer_diff::BufferDiff;
 use collections::HashSet;
 use editor::actions::{MoveUp, Paste};
@@ -42,10 +42,11 @@ use crate::profile_selector::ProfileSelector;
 use crate::thread::{Thread, TokenUsageRatio};
 use crate::thread_store::ThreadStore;
 use crate::{
-    AgentDiff, Chat, ExpandMessageEditor, NewThread, OpenAgentDiff, RemoveAllContext,
-    ToggleContextPicker, ToggleProfileSelector,
+    ActiveThread, AgentDiff, Chat, ExpandMessageEditor, NewThread, OpenAgentDiff, RemoveAllContext,
+    ToggleContextPicker, ToggleProfileSelector, register_agent_preview,
 };
 
+#[derive(RegisterComponent)]
 pub struct MessageEditor {
     thread: Entity<Thread>,
     incompatible_tools_state: Entity<IncompatibleToolsState>,
@@ -1202,3 +1203,53 @@ impl Render for MessageEditor {
             })
     }
 }
+
+impl Component for MessageEditor {
+    fn scope() -> ComponentScope {
+        ComponentScope::Agent
+    }
+}
+
+impl AgentPreview for MessageEditor {
+    fn create_preview(
+        workspace: WeakEntity<Workspace>,
+        active_thread: Entity<ActiveThread>,
+        thread_store: WeakEntity<ThreadStore>,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> Option<AnyElement> {
+        if let Some(workspace_entity) = workspace.upgrade() {
+            let fs = workspace_entity.read(cx).app_state().fs.clone();
+            let weak_project = workspace_entity.read(cx).project().clone().downgrade();
+            let context_store = cx.new(|_cx| ContextStore::new(weak_project, None));
+            let thread = active_thread.read(cx).thread().clone();
+
+            let example_message_editor = cx.new(|cx| {
+                MessageEditor::new(
+                    fs,
+                    workspace,
+                    context_store,
+                    None,
+                    thread_store,
+                    thread,
+                    window,
+                    cx,
+                )
+            });
+
+            Some(
+                v_flex()
+                    .gap_4()
+                    .children(vec![single_example(
+                        "Default",
+                        example_message_editor.clone().into_any_element(),
+                    )])
+                    .into_any_element(),
+            )
+        } else {
+            None
+        }
+    }
+}
+
+register_agent_preview!(MessageEditor);

crates/agent/src/ui.rs 🔗

@@ -1,9 +1,12 @@
 mod agent_notification;
+pub mod agent_preview;
 mod animated_label;
 mod context_pill;
+mod upsell;
 mod usage_banner;
 
 pub use agent_notification::*;
+pub use agent_preview::*;
 pub use animated_label::*;
 pub use context_pill::*;
 pub use usage_banner::*;

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

@@ -0,0 +1,99 @@
+use collections::HashMap;
+use component::ComponentId;
+use gpui::{App, Entity, WeakEntity};
+use linkme::distributed_slice;
+use std::sync::OnceLock;
+use ui::{AnyElement, Component, Window};
+use workspace::Workspace;
+
+use crate::{ActiveThread, ThreadStore};
+
+/// Function type for creating agent component previews
+pub type PreviewFn = fn(
+    WeakEntity<Workspace>,
+    Entity<ActiveThread>,
+    WeakEntity<ThreadStore>,
+    &mut Window,
+    &mut App,
+) -> Option<AnyElement>;
+
+/// Distributed slice for preview registration functions
+#[distributed_slice]
+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())
+    }
+
+    /// Static method to create a preview for this component type
+    fn create_preview(
+        workspace: WeakEntity<Workspace>,
+        active_thread: Entity<ActiveThread>,
+        thread_store: WeakEntity<ThreadStore>,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> Option<AnyElement>
+    where
+        Self: Sized;
+}
+
+/// Register an agent preview for the given component type
+#[macro_export]
+macro_rules! register_agent_preview {
+    ($type:ty) => {
+        #[linkme::distributed_slice($crate::ui::agent_preview::__ALL_AGENT_PREVIEWS)]
+        static __REGISTER_AGENT_PREVIEW: fn() -> (
+            component::ComponentId,
+            $crate::ui::agent_preview::PreviewFn,
+        ) = || {
+            (
+                <$type as $crate::ui::agent_preview::AgentPreview>::id(),
+                <$type as $crate::ui::agent_preview::AgentPreview>::create_preview,
+            )
+        };
+    };
+}
+
+/// Lazy initialized registry of preview functions
+static AGENT_PREVIEW_REGISTRY: OnceLock<HashMap<ComponentId, PreviewFn>> = OnceLock::new();
+
+/// Initialize the agent preview registry if needed
+fn get_or_init_registry() -> &'static HashMap<ComponentId, PreviewFn> {
+    AGENT_PREVIEW_REGISTRY.get_or_init(|| {
+        let mut map = HashMap::default();
+        for register_fn in __ALL_AGENT_PREVIEWS.iter() {
+            let (id, preview_fn) = register_fn();
+            map.insert(id, preview_fn);
+        }
+        map
+    })
+}
+
+/// Get a specific agent preview by component ID.
+pub fn get_agent_preview(
+    id: &ComponentId,
+    workspace: WeakEntity<Workspace>,
+    active_thread: Entity<ActiveThread>,
+    thread_store: WeakEntity<ThreadStore>,
+    window: &mut Window,
+    cx: &mut App,
+) -> Option<AnyElement> {
+    let registry = get_or_init_registry();
+    registry
+        .get(id)
+        .and_then(|preview_fn| preview_fn(workspace, active_thread, thread_store, window, cx))
+}
+
+/// Get all registered agent previews.
+pub fn all_agent_previews() -> Vec<ComponentId> {
+    let registry = get_or_init_registry();
+    registry.keys().cloned().collect()
+}

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

@@ -0,0 +1,163 @@
+use component::{Component, ComponentScope, single_example};
+use gpui::{
+    AnyElement, App, ClickEvent, IntoElement, ParentElement, RenderOnce, SharedString, Styled,
+    Window,
+};
+use theme::ActiveTheme;
+use ui::{
+    Button, ButtonCommon, ButtonStyle, Checkbox, Clickable, Color, Label, LabelCommon,
+    RegisterComponent, ToggleState, h_flex, v_flex,
+};
+
+/// A component that displays an upsell message with a call-to-action button
+///
+/// # Example
+/// ```
+/// let upsell = Upsell::new(
+///     "Upgrade to Zed Pro",
+///     "Get unlimited access to AI features and more",
+///     "Upgrade Now",
+///     Box::new(|_, _window, cx| {
+///         cx.open_url("https://zed.dev/pricing");
+///     }),
+///     Box::new(|_, _window, cx| {
+///         // Handle dismiss
+///     }),
+///     Box::new(|checked, window, cx| {
+///         // Handle don't show again
+///     }),
+/// );
+/// ```
+#[derive(IntoElement, RegisterComponent)]
+pub struct Upsell {
+    title: SharedString,
+    message: SharedString,
+    cta_text: SharedString,
+    on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>,
+    on_dismiss: Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>,
+    on_dont_show_again: Box<dyn Fn(bool, &mut Window, &mut App)>,
+}
+
+impl Upsell {
+    /// Create a new upsell component
+    pub fn new(
+        title: impl Into<SharedString>,
+        message: impl Into<SharedString>,
+        cta_text: impl Into<SharedString>,
+        on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>,
+        on_dismiss: Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>,
+        on_dont_show_again: Box<dyn Fn(bool, &mut Window, &mut App)>,
+    ) -> Self {
+        Self {
+            title: title.into(),
+            message: message.into(),
+            cta_text: cta_text.into(),
+            on_click,
+            on_dismiss,
+            on_dont_show_again,
+        }
+    }
+}
+
+impl RenderOnce for Upsell {
+    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+        v_flex()
+            .w_full()
+            .p_4()
+            .gap_3()
+            .bg(cx.theme().colors().surface_background)
+            .rounded_md()
+            .border_1()
+            .border_color(cx.theme().colors().border)
+            .child(
+                v_flex()
+                    .gap_1()
+                    .child(
+                        Label::new(self.title)
+                            .size(ui::LabelSize::Large)
+                            .weight(gpui::FontWeight::BOLD),
+                    )
+                    .child(Label::new(self.message).color(Color::Muted)),
+            )
+            .child(
+                h_flex()
+                    .w_full()
+                    .justify_between()
+                    .items_center()
+                    .child(
+                        h_flex()
+                            .items_center()
+                            .gap_1()
+                            .child(
+                                Checkbox::new("dont-show-again", ToggleState::Unselected).on_click(
+                                    move |_, window, cx| {
+                                        (self.on_dont_show_again)(true, window, cx);
+                                    },
+                                ),
+                            )
+                            .child(
+                                Label::new("Don't show again")
+                                    .color(Color::Muted)
+                                    .size(ui::LabelSize::Small),
+                            ),
+                    )
+                    .child(
+                        h_flex()
+                            .gap_2()
+                            .child(
+                                Button::new("dismiss-button", "Dismiss")
+                                    .style(ButtonStyle::Subtle)
+                                    .on_click(self.on_dismiss),
+                            )
+                            .child(
+                                Button::new("cta-button", self.cta_text)
+                                    .style(ButtonStyle::Filled)
+                                    .on_click(self.on_click),
+                            ),
+                    ),
+            )
+    }
+}
+
+impl Component for Upsell {
+    fn scope() -> ComponentScope {
+        ComponentScope::Agent
+    }
+
+    fn name() -> &'static str {
+        "Upsell"
+    }
+
+    fn description() -> Option<&'static str> {
+        Some("A promotional component that displays a message with a call-to-action.")
+    }
+
+    fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
+        let examples = vec![
+            single_example(
+                "Default",
+                Upsell::new(
+                    "Upgrade to Zed Pro",
+                    "Get unlimited access to AI features and more with Zed Pro. Unlock advanced AI capabilities and other premium features.",
+                    "Upgrade Now",
+                    Box::new(|_, _, _| {}),
+                    Box::new(|_, _, _| {}),
+                    Box::new(|_, _, _| {}),
+                ).render(window, cx).into_any_element(),
+            ),
+            single_example(
+                "Short Message",
+                Upsell::new(
+                    "Try Zed Pro for free",
+                    "Start your 7-day trial today.",
+                    "Start Trial",
+                    Box::new(|_, _, _| {}),
+                    Box::new(|_, _, _| {}),
+                    Box::new(|_, _, _| {}),
+                ).render(window, cx).into_any_element(),
+            ),
+        ];
+
+        Some(v_flex().gap_4().children(examples).into_any_element())
+    }
+}

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

@@ -98,6 +98,10 @@ impl RenderOnce for UsageBanner {
 }
 
 impl Component for UsageBanner {
+    fn scope() -> ComponentScope {
+        ComponentScope::Agent
+    }
+
     fn sort_name() -> &'static str {
         "AgentUsageBanner"
     }

crates/component_preview/Cargo.toml 🔗

@@ -15,18 +15,21 @@ path = "src/component_preview.rs"
 default = []
 
 [dependencies]
+agent.workspace = true
+anyhow.workspace = true
 client.workspace = true
 collections.workspace = true
 component.workspace = true
+db.workspace = true
 gpui.workspace = true
 languages.workspace = true
-notifications.workspace = true
 log.workspace = true
+notifications.workspace = true
 project.workspace = true
+prompt_store.workspace = true
+serde.workspace = true
 ui.workspace = true
 ui_input.workspace = true
 workspace-hack.workspace = true
 workspace.workspace = true
-db.workspace = true
-anyhow.workspace = true
-serde.workspace = true
+assistant_tool.workspace = true

crates/component_preview/src/component_preview.rs 🔗

@@ -3,10 +3,12 @@
 //! A view for exploring Zed components.
 
 mod persistence;
+mod preview_support;
 
 use std::iter::Iterator;
 use std::sync::Arc;
 
+use agent::{ActiveThread, ThreadStore};
 use client::UserStore;
 use component::{ComponentId, ComponentMetadata, components};
 use gpui::{
@@ -19,6 +21,7 @@ use gpui::{ListState, ScrollHandle, ScrollStrategy, UniformListScrollHandle};
 use languages::LanguageRegistry;
 use notifications::status_toast::{StatusToast, ToastIcon};
 use persistence::COMPONENT_PREVIEW_DB;
+use preview_support::active_thread::{load_preview_thread_store, static_active_thread};
 use project::Project;
 use ui::{Divider, HighlightedLabel, ListItem, ListSubHeader, prelude::*};
 
@@ -33,6 +36,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
 
     cx.observe_new(move |workspace: &mut Workspace, _window, cx| {
         let app_state = app_state.clone();
+        let project = workspace.project().clone();
         let weak_workspace = cx.entity().downgrade();
 
         workspace.register_action(
@@ -45,6 +49,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
                 let component_preview = cx.new(|cx| {
                     ComponentPreview::new(
                         weak_workspace.clone(),
+                        project.clone(),
                         language_registry,
                         user_store,
                         None,
@@ -52,6 +57,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
                         window,
                         cx,
                     )
+                    .expect("Failed to create component preview")
                 });
 
                 workspace.add_item_to_active_pane(
@@ -69,6 +75,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
 
 enum PreviewEntry {
     AllComponents,
+    ActiveThread,
     Separator,
     Component(ComponentMetadata, Option<Vec<usize>>),
     SectionHeader(SharedString),
@@ -91,6 +98,7 @@ enum PreviewPage {
     #[default]
     AllComponents,
     Component(ComponentId),
+    ActiveThread,
 }
 
 struct ComponentPreview {
@@ -102,24 +110,63 @@ 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>,
+        >,
+    >,
     cursor_index: usize,
     language_registry: Arc<LanguageRegistry>,
     workspace: WeakEntity<Workspace>,
+    project: Entity<Project>,
     user_store: Entity<UserStore>,
     filter_editor: Entity<SingleLineInput>,
     filter_text: String,
+
+    // preview support
+    thread_store: Option<Entity<ThreadStore>>,
+    active_thread: Option<Entity<ActiveThread>>,
 }
 
 impl ComponentPreview {
     pub fn new(
         workspace: WeakEntity<Workspace>,
+        project: Entity<Project>,
         language_registry: Arc<LanguageRegistry>,
         user_store: Entity<UserStore>,
         selected_index: impl Into<Option<usize>>,
         active_page: Option<PreviewPage>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Self {
+    ) -> anyhow::Result<Self> {
+        let workspace_clone = workspace.clone();
+        let project_clone = project.clone();
+
+        let entity = cx.weak_entity();
+        window
+            .spawn(cx, async move |cx| {
+                let thread_store_task =
+                    load_preview_thread_store(workspace_clone.clone(), project_clone.clone(), cx)
+                        .await;
+
+                if let Ok(thread_store) = thread_store_task.await {
+                    entity
+                        .update_in(cx, |this, window, cx| {
+                            this.thread_store = Some(thread_store.clone());
+                            this.create_active_thread(window, cx);
+                        })
+                        .ok();
+                }
+            })
+            .detach();
+
         let sorted_components = components().all_sorted();
         let selected_index = selected_index.into().unwrap_or(0);
         let active_page = active_page.unwrap_or(PreviewPage::AllComponents);
@@ -143,6 +190,40 @@ 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 mut component_preview = Self {
             workspace_id: None,
             focus_handle: cx.focus_handle(),
@@ -151,13 +232,17 @@ impl ComponentPreview {
             language_registry,
             user_store,
             workspace,
+            project,
             active_page,
             component_map: components().0,
             components: sorted_components,
             component_list,
+            agent_previews,
             cursor_index: selected_index,
             filter_editor,
             filter_text: String::new(),
+            thread_store: None,
+            active_thread: None,
         };
 
         if component_preview.cursor_index > 0 {
@@ -169,13 +254,41 @@ impl ComponentPreview {
         let focus_handle = component_preview.filter_editor.read(cx).focus_handle(cx);
         window.focus(&focus_handle);
 
-        component_preview
+        Ok(component_preview)
+    }
+
+    pub fn create_active_thread(
+        &mut self,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> &mut Self {
+        let workspace = self.workspace.clone();
+        let language_registry = self.language_registry.clone();
+        let weak_handle = self.workspace.clone();
+        if let Some(workspace) = workspace.upgrade() {
+            let project = workspace.read(cx).project().clone();
+            if let Some(thread_store) = self.thread_store.clone() {
+                let active_thread = static_active_thread(
+                    weak_handle,
+                    project,
+                    language_registry,
+                    thread_store,
+                    window,
+                    cx,
+                );
+                self.active_thread = Some(active_thread);
+                cx.notify();
+            }
+        }
+
+        self
     }
 
     pub fn active_page_id(&self, _cx: &App) -> ActivePageId {
         match &self.active_page {
             PreviewPage::AllComponents => ActivePageId::default(),
             PreviewPage::Component(component_id) => ActivePageId(component_id.0.to_string()),
+            PreviewPage::ActiveThread => ActivePageId("active_thread".to_string()),
         }
     }
 
@@ -289,6 +402,7 @@ impl ComponentPreview {
 
         // Always show all components first
         entries.push(PreviewEntry::AllComponents);
+        entries.push(PreviewEntry::ActiveThread);
         entries.push(PreviewEntry::Separator);
 
         let mut scopes: Vec<_> = scope_groups
@@ -389,6 +503,19 @@ impl ComponentPreview {
                     }))
                     .into_any_element()
             }
+            PreviewEntry::ActiveThread => {
+                let selected = self.active_page == PreviewPage::ActiveThread;
+
+                ListItem::new(ix)
+                    .child(Label::new("Active Thread").color(Color::Default))
+                    .selectable(true)
+                    .toggle_state(selected)
+                    .inset(true)
+                    .on_click(cx.listener(move |this, _, _, cx| {
+                        this.set_active_page(PreviewPage::ActiveThread, cx);
+                    }))
+                    .into_any_element()
+            }
             PreviewEntry::Separator => ListItem::new(ix)
                 .child(
                     h_flex()
@@ -471,6 +598,7 @@ impl ComponentPreview {
                             .render_scope_header(ix, shared_string.clone(), window, cx)
                             .into_any_element(),
                         PreviewEntry::AllComponents => div().w_full().h_0().into_any_element(),
+                        PreviewEntry::ActiveThread => div().w_full().h_0().into_any_element(),
                         PreviewEntry::Separator => div().w_full().h_0().into_any_element(),
                     })
                     .unwrap()
@@ -595,6 +723,41 @@ impl ComponentPreview {
         }
     }
 
+    fn render_active_thread(
+        &self,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> impl IntoElement {
+        v_flex()
+            .id("render-active-thread")
+            .size_full()
+            .child(
+                v_flex().children(self.agent_previews.iter().filter_map(|preview_fn| {
+                    if let (Some(thread_store), Some(active_thread)) = (
+                        self.thread_store.as_ref().map(|ts| ts.downgrade()),
+                        self.active_thread.clone(),
+                    ) {
+                        preview_fn(
+                            self,
+                            self.workspace.clone(),
+                            active_thread,
+                            thread_store,
+                            window,
+                            cx,
+                        )
+                        .map(|element| div().child(element))
+                    } else {
+                        None
+                    }
+                })),
+            )
+            .children(self.active_thread.clone().map(|thread| thread.clone()))
+            .when_none(&self.active_thread.clone(), |this| {
+                this.child("No active thread")
+            })
+            .into_any_element()
+    }
+
     fn test_status_toast(&self, cx: &mut Context<Self>) {
         if let Some(workspace) = self.workspace.upgrade() {
             workspace.update(cx, |workspace, cx| {
@@ -704,6 +867,9 @@ impl Render for ComponentPreview {
                         PreviewPage::Component(id) => self
                             .render_component_page(&id, window, cx)
                             .into_any_element(),
+                        PreviewPage::ActiveThread => {
+                            self.render_active_thread(window, cx).into_any_element()
+                        }
                     }),
             )
     }
@@ -759,20 +925,28 @@ impl Item for ComponentPreview {
         let language_registry = self.language_registry.clone();
         let user_store = self.user_store.clone();
         let weak_workspace = self.workspace.clone();
+        let project = self.project.clone();
         let selected_index = self.cursor_index;
         let active_page = self.active_page.clone();
 
-        Some(cx.new(|cx| {
-            Self::new(
-                weak_workspace,
-                language_registry,
-                user_store,
-                selected_index,
-                Some(active_page),
-                window,
-                cx,
-            )
-        }))
+        let self_result = Self::new(
+            weak_workspace,
+            project,
+            language_registry,
+            user_store,
+            selected_index,
+            Some(active_page),
+            window,
+            cx,
+        );
+
+        match self_result {
+            Ok(preview) => Some(cx.new(|_cx| preview)),
+            Err(e) => {
+                log::error!("Failed to clone component preview: {}", e);
+                None
+            }
+        }
     }
 
     fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
@@ -838,10 +1012,12 @@ impl SerializableItem for ComponentPreview {
             let user_store = user_store.clone();
             let language_registry = language_registry.clone();
             let weak_workspace = workspace.clone();
+            let project = project.clone();
             cx.update(move |window, cx| {
                 Ok(cx.new(|cx| {
                     ComponentPreview::new(
                         weak_workspace,
+                        project,
                         language_registry,
                         user_store,
                         None,
@@ -849,6 +1025,7 @@ impl SerializableItem for ComponentPreview {
                         window,
                         cx,
                     )
+                    .expect("Failed to create component preview")
                 }))
             })?
         })

crates/component_preview/src/preview_support/active_thread.rs 🔗

@@ -0,0 +1,69 @@
+use languages::LanguageRegistry;
+use project::Project;
+use std::sync::Arc;
+
+use agent::{ActiveThread, ContextStore, MessageSegment, ThreadStore};
+use assistant_tool::ToolWorkingSet;
+use gpui::{AppContext, AsyncApp, Entity, Task, WeakEntity};
+use prompt_store::PromptBuilder;
+use ui::{App, Window};
+use workspace::Workspace;
+
+pub async fn load_preview_thread_store(
+    workspace: WeakEntity<Workspace>,
+    project: Entity<Project>,
+    cx: &mut AsyncApp,
+) -> Task<anyhow::Result<Entity<ThreadStore>>> {
+    cx.spawn(async move |cx| {
+        workspace
+            .update(cx, |_, cx| {
+                ThreadStore::load(
+                    project.clone(),
+                    cx.new(|_| ToolWorkingSet::default()),
+                    None,
+                    Arc::new(PromptBuilder::new(None).unwrap()),
+                    cx,
+                )
+            })?
+            .await
+    })
+}
+
+pub fn static_active_thread(
+    workspace: WeakEntity<Workspace>,
+    project: Entity<Project>,
+    language_registry: Arc<LanguageRegistry>,
+    thread_store: Entity<ThreadStore>,
+    window: &mut Window,
+    cx: &mut App,
+) -> Entity<ActiveThread> {
+    let context_store =
+        cx.new(|_| ContextStore::new(project.downgrade(), Some(thread_store.downgrade())));
+
+    let thread = thread_store.update(cx, |thread_store, cx| thread_store.create_thread(cx));
+    thread.update(cx, |thread, cx| {
+        thread.insert_assistant_message(vec![
+            MessageSegment::Text("I'll help you fix the lifetime error in your `cx.spawn` call. When working with async operations in GPUI, there are specific patterns to follow for proper lifetime management.".to_string()),
+            MessageSegment::Text("\n\nLet's look at what's happening in your code:".to_string()),
+            MessageSegment::Text("\n\n---\n\nLet's check the current state of the active_thread.rs file to understand what might have changed:".to_string()),
+            MessageSegment::Text("\n\n---\n\nLooking at the implementation of `load_preview_thread_store` and understanding GPUI's async patterns, here's the issue:".to_string()),
+            MessageSegment::Text("\n\n1. `load_preview_thread_store` returns a `Task<anyhow::Result<Entity<ThreadStore>>>`, which means it's already a task".to_string()),
+            MessageSegment::Text("\n2. When you call this function inside another `spawn` call, you're nesting tasks incorrectly".to_string()),
+            MessageSegment::Text("\n3. The `this` parameter you're trying to use in your closure has the wrong context".to_string()),
+            MessageSegment::Text("\n\nHere's the correct way to implement this:".to_string()),
+            MessageSegment::Text("\n\n---\n\nThe problem is in how you're setting up the async closure and trying to reference variables like `window` and `language_registry` that aren't accessible in that scope.".to_string()),
+            MessageSegment::Text("\n\nHere's how to fix it:".to_string()),
+        ], cx);
+    });
+    cx.new(|cx| {
+        ActiveThread::new(
+            thread,
+            thread_store,
+            context_store,
+            language_registry,
+            workspace.clone(),
+            window,
+            cx,
+        )
+    })
+}