Add Agent Preview trait (#29760)

Like the title says

Release Notes:

- N/A
This commit is contained in:
Nate Butler 2025-05-01 23:03:06 -04:00 committed by GitHub
parent 93cc4946d8
commit 672a1dd553
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 595 additions and 20 deletions

3
Cargo.lock generated
View file

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

View file

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

View file

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

View file

@ -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::*;

View file

@ -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()
}

View file

@ -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())
}
}

View file

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

View file

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

View file

@ -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")
}))
})?
})

View file

@ -0,0 +1 @@
pub mod active_thread;

View file

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