Add Agent Preview trait (#29760)
Like the title says Release Notes: - N/A
This commit is contained in:
parent
93cc4946d8
commit
672a1dd553
11 changed files with 595 additions and 20 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -3239,7 +3239,9 @@ dependencies = [
|
||||||
name = "component_preview"
|
name = "component_preview"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"agent",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"assistant_tool",
|
||||||
"client",
|
"client",
|
||||||
"collections",
|
"collections",
|
||||||
"component",
|
"component",
|
||||||
|
@ -3249,6 +3251,7 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"notifications",
|
"notifications",
|
||||||
"project",
|
"project",
|
||||||
|
"prompt_store",
|
||||||
"serde",
|
"serde",
|
||||||
"ui",
|
"ui",
|
||||||
"ui_input",
|
"ui_input",
|
||||||
|
|
|
@ -46,6 +46,8 @@ pub use crate::inline_assistant::InlineAssistant;
|
||||||
pub use crate::thread::{Message, MessageSegment, Thread, ThreadEvent};
|
pub use crate::thread::{Message, MessageSegment, Thread, ThreadEvent};
|
||||||
pub use crate::thread_store::ThreadStore;
|
pub use crate::thread_store::ThreadStore;
|
||||||
pub use agent_diff::{AgentDiff, AgentDiffToolbar};
|
pub use agent_diff::{AgentDiff, AgentDiffToolbar};
|
||||||
|
pub use context_store::ContextStore;
|
||||||
|
pub use ui::{all_agent_previews, get_agent_preview};
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
agent,
|
agent,
|
||||||
|
|
|
@ -4,7 +4,7 @@ use std::sync::Arc;
|
||||||
use crate::assistant_model_selector::{AssistantModelSelector, ModelType};
|
use crate::assistant_model_selector::{AssistantModelSelector, ModelType};
|
||||||
use crate::context::{ContextLoadResult, load_context};
|
use crate::context::{ContextLoadResult, load_context};
|
||||||
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
|
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
|
||||||
use crate::ui::AnimatedLabel;
|
use crate::ui::{AgentPreview, AnimatedLabel};
|
||||||
use buffer_diff::BufferDiff;
|
use buffer_diff::BufferDiff;
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use editor::actions::{MoveUp, Paste};
|
use editor::actions::{MoveUp, Paste};
|
||||||
|
@ -42,10 +42,11 @@ use crate::profile_selector::ProfileSelector;
|
||||||
use crate::thread::{Thread, TokenUsageRatio};
|
use crate::thread::{Thread, TokenUsageRatio};
|
||||||
use crate::thread_store::ThreadStore;
|
use crate::thread_store::ThreadStore;
|
||||||
use crate::{
|
use crate::{
|
||||||
AgentDiff, Chat, ExpandMessageEditor, NewThread, OpenAgentDiff, RemoveAllContext,
|
ActiveThread, AgentDiff, Chat, ExpandMessageEditor, NewThread, OpenAgentDiff, RemoveAllContext,
|
||||||
ToggleContextPicker, ToggleProfileSelector,
|
ToggleContextPicker, ToggleProfileSelector, register_agent_preview,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(RegisterComponent)]
|
||||||
pub struct MessageEditor {
|
pub struct MessageEditor {
|
||||||
thread: Entity<Thread>,
|
thread: Entity<Thread>,
|
||||||
incompatible_tools_state: Entity<IncompatibleToolsState>,
|
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);
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
mod agent_notification;
|
mod agent_notification;
|
||||||
|
pub mod agent_preview;
|
||||||
mod animated_label;
|
mod animated_label;
|
||||||
mod context_pill;
|
mod context_pill;
|
||||||
|
mod upsell;
|
||||||
mod usage_banner;
|
mod usage_banner;
|
||||||
|
|
||||||
pub use agent_notification::*;
|
pub use agent_notification::*;
|
||||||
|
pub use agent_preview::*;
|
||||||
pub use animated_label::*;
|
pub use animated_label::*;
|
||||||
pub use context_pill::*;
|
pub use context_pill::*;
|
||||||
pub use usage_banner::*;
|
pub use usage_banner::*;
|
||||||
|
|
99
crates/agent/src/ui/agent_preview.rs
Normal file
99
crates/agent/src/ui/agent_preview.rs
Normal 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()
|
||||||
|
}
|
163
crates/agent/src/ui/upsell.rs
Normal file
163
crates/agent/src/ui/upsell.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -98,6 +98,10 @@ impl RenderOnce for UsageBanner {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for UsageBanner {
|
impl Component for UsageBanner {
|
||||||
|
fn scope() -> ComponentScope {
|
||||||
|
ComponentScope::Agent
|
||||||
|
}
|
||||||
|
|
||||||
fn sort_name() -> &'static str {
|
fn sort_name() -> &'static str {
|
||||||
"AgentUsageBanner"
|
"AgentUsageBanner"
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,18 +15,21 @@ path = "src/component_preview.rs"
|
||||||
default = []
|
default = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
agent.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
component.workspace = true
|
component.workspace = true
|
||||||
|
db.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
languages.workspace = true
|
languages.workspace = true
|
||||||
notifications.workspace = true
|
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
notifications.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
|
prompt_store.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
ui_input.workspace = true
|
ui_input.workspace = true
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
db.workspace = true
|
assistant_tool.workspace = true
|
||||||
anyhow.workspace = true
|
|
||||||
serde.workspace = true
|
|
||||||
|
|
|
@ -3,10 +3,12 @@
|
||||||
//! A view for exploring Zed components.
|
//! A view for exploring Zed components.
|
||||||
|
|
||||||
mod persistence;
|
mod persistence;
|
||||||
|
mod preview_support;
|
||||||
|
|
||||||
use std::iter::Iterator;
|
use std::iter::Iterator;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use agent::{ActiveThread, ThreadStore};
|
||||||
use client::UserStore;
|
use client::UserStore;
|
||||||
use component::{ComponentId, ComponentMetadata, components};
|
use component::{ComponentId, ComponentMetadata, components};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
@ -19,6 +21,7 @@ use gpui::{ListState, ScrollHandle, ScrollStrategy, UniformListScrollHandle};
|
||||||
use languages::LanguageRegistry;
|
use languages::LanguageRegistry;
|
||||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||||
use persistence::COMPONENT_PREVIEW_DB;
|
use persistence::COMPONENT_PREVIEW_DB;
|
||||||
|
use preview_support::active_thread::{load_preview_thread_store, static_active_thread};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use ui::{Divider, HighlightedLabel, ListItem, ListSubHeader, prelude::*};
|
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| {
|
cx.observe_new(move |workspace: &mut Workspace, _window, cx| {
|
||||||
let app_state = app_state.clone();
|
let app_state = app_state.clone();
|
||||||
|
let project = workspace.project().clone();
|
||||||
let weak_workspace = cx.entity().downgrade();
|
let weak_workspace = cx.entity().downgrade();
|
||||||
|
|
||||||
workspace.register_action(
|
workspace.register_action(
|
||||||
|
@ -45,6 +49,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
|
||||||
let component_preview = cx.new(|cx| {
|
let component_preview = cx.new(|cx| {
|
||||||
ComponentPreview::new(
|
ComponentPreview::new(
|
||||||
weak_workspace.clone(),
|
weak_workspace.clone(),
|
||||||
|
project.clone(),
|
||||||
language_registry,
|
language_registry,
|
||||||
user_store,
|
user_store,
|
||||||
None,
|
None,
|
||||||
|
@ -52,6 +57,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
.expect("Failed to create component preview")
|
||||||
});
|
});
|
||||||
|
|
||||||
workspace.add_item_to_active_pane(
|
workspace.add_item_to_active_pane(
|
||||||
|
@ -69,6 +75,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
|
||||||
|
|
||||||
enum PreviewEntry {
|
enum PreviewEntry {
|
||||||
AllComponents,
|
AllComponents,
|
||||||
|
ActiveThread,
|
||||||
Separator,
|
Separator,
|
||||||
Component(ComponentMetadata, Option<Vec<usize>>),
|
Component(ComponentMetadata, Option<Vec<usize>>),
|
||||||
SectionHeader(SharedString),
|
SectionHeader(SharedString),
|
||||||
|
@ -91,6 +98,7 @@ enum PreviewPage {
|
||||||
#[default]
|
#[default]
|
||||||
AllComponents,
|
AllComponents,
|
||||||
Component(ComponentId),
|
Component(ComponentId),
|
||||||
|
ActiveThread,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ComponentPreview {
|
struct ComponentPreview {
|
||||||
|
@ -102,24 +110,63 @@ struct ComponentPreview {
|
||||||
active_page: PreviewPage,
|
active_page: PreviewPage,
|
||||||
components: Vec<ComponentMetadata>,
|
components: Vec<ComponentMetadata>,
|
||||||
component_list: ListState,
|
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,
|
cursor_index: usize,
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
|
project: Entity<Project>,
|
||||||
user_store: Entity<UserStore>,
|
user_store: Entity<UserStore>,
|
||||||
filter_editor: Entity<SingleLineInput>,
|
filter_editor: Entity<SingleLineInput>,
|
||||||
filter_text: String,
|
filter_text: String,
|
||||||
|
|
||||||
|
// preview support
|
||||||
|
thread_store: Option<Entity<ThreadStore>>,
|
||||||
|
active_thread: Option<Entity<ActiveThread>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ComponentPreview {
|
impl ComponentPreview {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
|
project: Entity<Project>,
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
user_store: Entity<UserStore>,
|
user_store: Entity<UserStore>,
|
||||||
selected_index: impl Into<Option<usize>>,
|
selected_index: impl Into<Option<usize>>,
|
||||||
active_page: Option<PreviewPage>,
|
active_page: Option<PreviewPage>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
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 sorted_components = components().all_sorted();
|
||||||
let selected_index = selected_index.into().unwrap_or(0);
|
let selected_index = selected_index.into().unwrap_or(0);
|
||||||
let active_page = active_page.unwrap_or(PreviewPage::AllComponents);
|
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 {
|
let mut component_preview = Self {
|
||||||
workspace_id: None,
|
workspace_id: None,
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
|
@ -151,13 +232,17 @@ impl ComponentPreview {
|
||||||
language_registry,
|
language_registry,
|
||||||
user_store,
|
user_store,
|
||||||
workspace,
|
workspace,
|
||||||
|
project,
|
||||||
active_page,
|
active_page,
|
||||||
component_map: components().0,
|
component_map: components().0,
|
||||||
components: sorted_components,
|
components: sorted_components,
|
||||||
component_list,
|
component_list,
|
||||||
|
agent_previews,
|
||||||
cursor_index: selected_index,
|
cursor_index: selected_index,
|
||||||
filter_editor,
|
filter_editor,
|
||||||
filter_text: String::new(),
|
filter_text: String::new(),
|
||||||
|
thread_store: None,
|
||||||
|
active_thread: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
if component_preview.cursor_index > 0 {
|
if component_preview.cursor_index > 0 {
|
||||||
|
@ -169,13 +254,41 @@ impl ComponentPreview {
|
||||||
let focus_handle = component_preview.filter_editor.read(cx).focus_handle(cx);
|
let focus_handle = component_preview.filter_editor.read(cx).focus_handle(cx);
|
||||||
window.focus(&focus_handle);
|
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 {
|
pub fn active_page_id(&self, _cx: &App) -> ActivePageId {
|
||||||
match &self.active_page {
|
match &self.active_page {
|
||||||
PreviewPage::AllComponents => ActivePageId::default(),
|
PreviewPage::AllComponents => ActivePageId::default(),
|
||||||
PreviewPage::Component(component_id) => ActivePageId(component_id.0.to_string()),
|
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
|
// Always show all components first
|
||||||
entries.push(PreviewEntry::AllComponents);
|
entries.push(PreviewEntry::AllComponents);
|
||||||
|
entries.push(PreviewEntry::ActiveThread);
|
||||||
entries.push(PreviewEntry::Separator);
|
entries.push(PreviewEntry::Separator);
|
||||||
|
|
||||||
let mut scopes: Vec<_> = scope_groups
|
let mut scopes: Vec<_> = scope_groups
|
||||||
|
@ -389,6 +503,19 @@ impl ComponentPreview {
|
||||||
}))
|
}))
|
||||||
.into_any_element()
|
.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)
|
PreviewEntry::Separator => ListItem::new(ix)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
@ -471,6 +598,7 @@ impl ComponentPreview {
|
||||||
.render_scope_header(ix, shared_string.clone(), window, cx)
|
.render_scope_header(ix, shared_string.clone(), window, cx)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
PreviewEntry::AllComponents => div().w_full().h_0().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(),
|
PreviewEntry::Separator => div().w_full().h_0().into_any_element(),
|
||||||
})
|
})
|
||||||
.unwrap()
|
.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>) {
|
fn test_status_toast(&self, cx: &mut Context<Self>) {
|
||||||
if let Some(workspace) = self.workspace.upgrade() {
|
if let Some(workspace) = self.workspace.upgrade() {
|
||||||
workspace.update(cx, |workspace, cx| {
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
@ -704,6 +867,9 @@ impl Render for ComponentPreview {
|
||||||
PreviewPage::Component(id) => self
|
PreviewPage::Component(id) => self
|
||||||
.render_component_page(&id, window, cx)
|
.render_component_page(&id, window, cx)
|
||||||
.into_any_element(),
|
.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 language_registry = self.language_registry.clone();
|
||||||
let user_store = self.user_store.clone();
|
let user_store = self.user_store.clone();
|
||||||
let weak_workspace = self.workspace.clone();
|
let weak_workspace = self.workspace.clone();
|
||||||
|
let project = self.project.clone();
|
||||||
let selected_index = self.cursor_index;
|
let selected_index = self.cursor_index;
|
||||||
let active_page = self.active_page.clone();
|
let active_page = self.active_page.clone();
|
||||||
|
|
||||||
Some(cx.new(|cx| {
|
let self_result = Self::new(
|
||||||
Self::new(
|
weak_workspace,
|
||||||
weak_workspace,
|
project,
|
||||||
language_registry,
|
language_registry,
|
||||||
user_store,
|
user_store,
|
||||||
selected_index,
|
selected_index,
|
||||||
Some(active_page),
|
Some(active_page),
|
||||||
window,
|
window,
|
||||||
cx,
|
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)) {
|
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 user_store = user_store.clone();
|
||||||
let language_registry = language_registry.clone();
|
let language_registry = language_registry.clone();
|
||||||
let weak_workspace = workspace.clone();
|
let weak_workspace = workspace.clone();
|
||||||
|
let project = project.clone();
|
||||||
cx.update(move |window, cx| {
|
cx.update(move |window, cx| {
|
||||||
Ok(cx.new(|cx| {
|
Ok(cx.new(|cx| {
|
||||||
ComponentPreview::new(
|
ComponentPreview::new(
|
||||||
weak_workspace,
|
weak_workspace,
|
||||||
|
project,
|
||||||
language_registry,
|
language_registry,
|
||||||
user_store,
|
user_store,
|
||||||
None,
|
None,
|
||||||
|
@ -849,6 +1025,7 @@ impl SerializableItem for ComponentPreview {
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
.expect("Failed to create component preview")
|
||||||
}))
|
}))
|
||||||
})?
|
})?
|
||||||
})
|
})
|
||||||
|
|
1
crates/component_preview/src/preview_support.rs
Normal file
1
crates/component_preview/src/preview_support.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod active_thread;
|
|
@ -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,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue