Detailed changes
@@ -9,6 +9,7 @@ mod context_picker;
mod context_server_configuration;
mod context_store;
mod context_strip;
+mod debug;
mod history_store;
mod inline_assistant;
mod inline_prompt_editor;
@@ -47,7 +48,7 @@ pub use crate::thread::{Message, MessageSegment, Thread, ThreadEvent};
pub use crate::thread_store::ThreadStore;
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
pub use context_store::ContextStore;
-pub use ui::{all_agent_previews, get_agent_preview};
+pub use ui::preview::{all_agent_previews, get_agent_preview};
actions!(
agent,
@@ -50,7 +50,6 @@ use crate::message_editor::{MessageEditor, MessageEditorEvent};
use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
use crate::thread_history::{PastContext, PastThread, ThreadHistory};
use crate::thread_store::ThreadStore;
-use crate::ui::UsageBanner;
use crate::{
AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, ExpandMessageEditor, Follow,
InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
@@ -432,6 +431,7 @@ impl AssistantPanel {
MessageEditor::new(
fs.clone(),
workspace.clone(),
+ user_store.clone(),
message_editor_context_store.clone(),
prompt_store.clone(),
thread_store.downgrade(),
@@ -735,6 +735,7 @@ impl AssistantPanel {
MessageEditor::new(
self.fs.clone(),
self.workspace.clone(),
+ self.user_store.clone(),
context_store,
self.prompt_store.clone(),
self.thread_store.downgrade(),
@@ -933,6 +934,7 @@ impl AssistantPanel {
MessageEditor::new(
self.fs.clone(),
self.workspace.clone(),
+ self.user_store.clone(),
context_store,
self.prompt_store.clone(),
self.thread_store.downgrade(),
@@ -1944,22 +1946,6 @@ impl AssistantPanel {
})
}
- fn render_usage_banner(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
- let plan = self
- .user_store
- .read(cx)
- .current_plan()
- .map(|plan| match plan {
- Plan::Free => zed_llm_client::Plan::Free,
- Plan::ZedPro => zed_llm_client::Plan::ZedPro,
- Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
- })
- .unwrap_or(zed_llm_client::Plan::Free);
- let usage = self.thread.read(cx).last_usage()?;
-
- Some(UsageBanner::new(plan, usage).into_any_element())
- }
-
fn render_tool_use_limit_reached(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
let tool_use_limit_reached = self
.thread
@@ -2277,7 +2263,6 @@ impl Render for AssistantPanel {
ActiveView::Thread { .. } => parent
.child(self.render_active_thread_or_empty_state(window, cx))
.children(self.render_tool_use_limit_reached(cx))
- .children(self.render_usage_banner(cx))
.child(h_flex().child(self.message_editor.clone()))
.children(self.render_last_error(cx)),
ActiveView::History => parent.child(self.history.clone()),
@@ -0,0 +1,124 @@
+#![allow(unused, dead_code)]
+
+use gpui::Global;
+use language_model::RequestUsage;
+use std::ops::{Deref, DerefMut};
+use ui::prelude::*;
+use zed_llm_client::{Plan, UsageLimit};
+
+/// Debug only: Used for testing various account states
+///
+/// Use this by initializing it with
+/// `cx.set_global(DebugAccountState::default());` somewhere
+///
+/// Then call `cx.debug_account()` to get access
+#[derive(Clone, Debug)]
+pub struct DebugAccountState {
+ pub enabled: bool,
+ pub trial_expired: bool,
+ pub plan: Plan,
+ pub custom_prompt_usage: RequestUsage,
+ pub usage_based_billing_enabled: bool,
+ pub monthly_spending_cap: i32,
+ pub custom_edit_prediction_usage: UsageLimit,
+}
+
+impl DebugAccountState {
+ pub fn enabled(&self) -> bool {
+ self.enabled
+ }
+
+ pub fn set_enabled(&mut self, enabled: bool) -> &mut Self {
+ self.enabled = enabled;
+ self
+ }
+
+ pub fn set_trial_expired(&mut self, trial_expired: bool) -> &mut Self {
+ self.trial_expired = trial_expired;
+ self
+ }
+
+ pub fn set_plan(&mut self, plan: Plan) -> &mut Self {
+ self.plan = plan;
+ self
+ }
+
+ pub fn set_custom_prompt_usage(&mut self, custom_prompt_usage: RequestUsage) -> &mut Self {
+ self.custom_prompt_usage = custom_prompt_usage;
+ self
+ }
+
+ pub fn set_usage_based_billing_enabled(
+ &mut self,
+ usage_based_billing_enabled: bool,
+ ) -> &mut Self {
+ self.usage_based_billing_enabled = usage_based_billing_enabled;
+ self
+ }
+
+ pub fn set_monthly_spending_cap(&mut self, monthly_spending_cap: i32) -> &mut Self {
+ self.monthly_spending_cap = monthly_spending_cap;
+ self
+ }
+
+ pub fn set_custom_edit_prediction_usage(
+ &mut self,
+ custom_edit_prediction_usage: UsageLimit,
+ ) -> &mut Self {
+ self.custom_edit_prediction_usage = custom_edit_prediction_usage;
+ self
+ }
+}
+
+impl Default for DebugAccountState {
+ fn default() -> Self {
+ Self {
+ enabled: false,
+ trial_expired: false,
+ plan: Plan::Free,
+ custom_prompt_usage: RequestUsage {
+ limit: UsageLimit::Unlimited,
+ amount: 0,
+ },
+ usage_based_billing_enabled: false,
+ // $50.00
+ monthly_spending_cap: 5000,
+ custom_edit_prediction_usage: UsageLimit::Unlimited,
+ }
+ }
+}
+
+impl DebugAccountState {
+ pub fn get_global(cx: &App) -> &Self {
+ &cx.global::<GlobalDebugAccountState>().0
+ }
+}
+
+#[derive(Clone, Debug)]
+pub struct GlobalDebugAccountState(pub DebugAccountState);
+
+impl Global for GlobalDebugAccountState {}
+
+impl Deref for GlobalDebugAccountState {
+ type Target = DebugAccountState;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl DerefMut for GlobalDebugAccountState {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+pub trait DebugAccount {
+ fn debug_account(&self) -> &DebugAccountState;
+}
+
+impl DebugAccount for App {
+ fn debug_account(&self) -> &DebugAccountState {
+ &self.global::<GlobalDebugAccountState>().0
+ }
+}
@@ -4,8 +4,12 @@ use std::sync::Arc;
use crate::assistant_model_selector::{AssistantModelSelector, ModelType};
use crate::context::{AgentContextKey, ContextCreasesAddon, ContextLoadResult, load_context};
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
-use crate::ui::{AgentPreview, AnimatedLabel, MaxModeTooltip};
+use crate::ui::{
+ AnimatedLabel, MaxModeTooltip,
+ preview::{AgentPreview, UsageCallout},
+};
use buffer_diff::BufferDiff;
+use client::UserStore;
use collections::{HashMap, HashSet};
use editor::actions::{MoveUp, Paste};
use editor::{
@@ -27,6 +31,7 @@ use language_model_selector::ToggleModelSelector;
use multi_buffer;
use project::Project;
use prompt_store::PromptStore;
+use proto::Plan;
use settings::Settings;
use std::time::Duration;
use theme::ThemeSettings;
@@ -53,6 +58,7 @@ pub struct MessageEditor {
editor: Entity<Editor>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
+ user_store: Entity<UserStore>,
context_store: Entity<ContextStore>,
prompt_store: Option<Entity<PromptStore>>,
context_strip: Entity<ContextStrip>,
@@ -126,6 +132,7 @@ impl MessageEditor {
pub fn new(
fs: Arc<dyn Fs>,
workspace: WeakEntity<Workspace>,
+ user_store: Entity<UserStore>,
context_store: Entity<ContextStore>,
prompt_store: Option<Entity<PromptStore>>,
thread_store: WeakEntity<ThreadStore>,
@@ -188,6 +195,7 @@ impl MessageEditor {
Self {
editor: editor.clone(),
project: thread.read(cx).project().clone(),
+ user_store,
thread,
incompatible_tools_state: incompatible_tools.clone(),
workspace,
@@ -1030,79 +1038,72 @@ impl MessageEditor {
})
}
+ fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context<Self>) -> Option<Div> {
+ if !cx.has_flag::<NewBillingFeatureFlag>() {
+ return None;
+ }
+
+ let plan = self
+ .user_store
+ .read(cx)
+ .current_plan()
+ .map(|plan| match plan {
+ Plan::Free => zed_llm_client::Plan::Free,
+ Plan::ZedPro => zed_llm_client::Plan::ZedPro,
+ Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
+ })
+ .unwrap_or(zed_llm_client::Plan::Free);
+ let usage = self.thread.read(cx).last_usage()?;
+
+ Some(
+ div()
+ .child(UsageCallout::new(plan, usage))
+ .line_height(line_height),
+ )
+ }
+
fn render_token_limit_callout(
&self,
line_height: Pixels,
token_usage_ratio: TokenUsageRatio,
cx: &mut Context<Self>,
- ) -> Div {
- let heading = if token_usage_ratio == TokenUsageRatio::Exceeded {
+ ) -> Option<Div> {
+ if !cx.has_flag::<NewBillingFeatureFlag>() {
+ return None;
+ }
+
+ let title = if token_usage_ratio == TokenUsageRatio::Exceeded {
"Thread reached the token limit"
} else {
"Thread reaching the token limit soon"
};
- h_flex()
- .p_2()
- .gap_2()
- .flex_wrap()
- .justify_between()
- .bg(
- if token_usage_ratio == TokenUsageRatio::Exceeded {
- cx.theme().status().error_background.opacity(0.1)
- } else {
- cx.theme().status().warning_background.opacity(0.1)
- })
- .border_t_1()
- .border_color(cx.theme().colors().border)
- .child(
- h_flex()
- .gap_2()
- .items_start()
- .child(
- h_flex()
- .h(line_height)
- .justify_center()
- .child(
- if token_usage_ratio == TokenUsageRatio::Exceeded {
- Icon::new(IconName::X)
- .color(Color::Error)
- .size(IconSize::XSmall)
- } else {
- Icon::new(IconName::Warning)
- .color(Color::Warning)
- .size(IconSize::XSmall)
- }
- ),
- )
- .child(
- v_flex()
- .mr_auto()
- .child(Label::new(heading).size(LabelSize::Small))
- .child(
- Label::new(
- "Start a new thread from a summary to continue the conversation.",
- )
- .size(LabelSize::Small)
- .color(Color::Muted),
- ),
- ),
- )
- .child(
- Button::new("new-thread", "Start New Thread")
- .on_click(cx.listener(|this, _, window, cx| {
- let from_thread_id = Some(this.thread.read(cx).id().clone());
+ let message = "Start a new thread from a summary to continue the conversation.";
- window.dispatch_action(Box::new(NewThread {
- from_thread_id
- }), cx);
- }))
- .icon(IconName::Plus)
- .icon_position(IconPosition::Start)
- .icon_size(IconSize::Small)
- .style(ButtonStyle::Tinted(ui::TintColor::Accent))
- .label_size(LabelSize::Small),
- )
+ let icon = if token_usage_ratio == TokenUsageRatio::Exceeded {
+ Icon::new(IconName::X)
+ .color(Color::Error)
+ .size(IconSize::XSmall)
+ } else {
+ Icon::new(IconName::Warning)
+ .color(Color::Warning)
+ .size(IconSize::XSmall)
+ };
+
+ Some(
+ div()
+ .child(ui::Callout::multi_line(
+ title.into(),
+ message.into(),
+ icon,
+ "Start New Thread".into(),
+ Box::new(cx.listener(|this, _, window, cx| {
+ let from_thread_id = Some(this.thread.read(cx).id().clone());
+ window.dispatch_action(Box::new(NewThread { from_thread_id }), cx);
+ })),
+ ))
+ .line_height(line_height),
+ )
}
pub fn last_estimated_token_count(&self) -> Option<usize> {
@@ -1307,8 +1308,16 @@ impl Render for MessageEditor {
parent.child(self.render_changed_buffers(&changed_buffers, window, cx))
})
.child(self.render_editor(font_size, line_height, window, cx))
- .when(token_usage_ratio != TokenUsageRatio::Normal, |parent| {
- parent.child(self.render_token_limit_callout(line_height, token_usage_ratio, cx))
+ .children({
+ let usage_callout = self.render_usage_callout(line_height, cx);
+
+ if usage_callout.is_some() {
+ usage_callout
+ } else if token_usage_ratio != TokenUsageRatio::Normal {
+ self.render_token_limit_callout(line_height, token_usage_ratio, cx)
+ } else {
+ None
+ }
})
}
}
@@ -1354,6 +1363,12 @@ impl Component for MessageEditor {
fn scope() -> ComponentScope {
ComponentScope::Agent
}
+
+ fn description() -> Option<&'static str> {
+ Some(
+ "The composer experience of the Agent Panel. This interface handles context, composing messages, switching profiles, models and more.",
+ )
+ }
}
impl AgentPreview for MessageEditor {
@@ -1364,16 +1379,18 @@ impl AgentPreview for MessageEditor {
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();
+ if let Some(workspace) = workspace.upgrade() {
+ let fs = workspace.read(cx).app_state().fs.clone();
+ let user_store = workspace.read(cx).app_state().user_store.clone();
+ let weak_project = workspace.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| {
+ let default_message_editor = cx.new(|cx| {
MessageEditor::new(
fs,
- workspace,
+ workspace.downgrade(),
+ user_store,
context_store,
None,
thread_store,
@@ -1387,8 +1404,15 @@ impl AgentPreview for MessageEditor {
v_flex()
.gap_4()
.children(vec![single_example(
- "Default",
- example_message_editor.clone().into_any_element(),
+ "Default Message Editor",
+ div()
+ .w(px(540.))
+ .pt_12()
+ .bg(cx.theme().colors().panel_background)
+ .border_1()
+ .border_color(cx.theme().colors().border)
+ .child(default_message_editor.clone())
+ .into_any_element(),
)])
.into_any_element(),
)
@@ -355,6 +355,7 @@ pub struct Thread {
request_token_usage: Vec<TokenUsage>,
cumulative_token_usage: TokenUsage,
exceeded_window_error: Option<ExceededWindowError>,
+ last_usage: Option<RequestUsage>,
tool_use_limit_reached: bool,
feedback: Option<ThreadFeedback>,
message_feedback: HashMap<MessageId, ThreadFeedback>,
@@ -418,6 +419,7 @@ impl Thread {
request_token_usage: Vec::new(),
cumulative_token_usage: TokenUsage::default(),
exceeded_window_error: None,
+ last_usage: None,
tool_use_limit_reached: false,
feedback: None,
message_feedback: HashMap::default(),
@@ -526,6 +528,7 @@ impl Thread {
request_token_usage: serialized.request_token_usage,
cumulative_token_usage: serialized.cumulative_token_usage,
exceeded_window_error: None,
+ last_usage: None,
tool_use_limit_reached: false,
feedback: None,
message_feedback: HashMap::default(),
@@ -817,6 +820,10 @@ impl Thread {
.unwrap_or(false)
}
+ pub fn last_usage(&self) -> Option<RequestUsage> {
+ self.last_usage
+ }
+
pub fn tool_use_limit_reached(&self) -> bool {
self.tool_use_limit_reached
}
@@ -1535,7 +1542,10 @@ impl Thread {
CompletionRequestStatus::UsageUpdated {
amount, limit
} => {
- cx.emit(ThreadEvent::UsageUpdated(RequestUsage { limit, amount: amount as i32 }));
+ let usage = RequestUsage { limit, amount: amount as i32 };
+
+ thread.last_usage = Some(usage);
+ cx.emit(ThreadEvent::UsageUpdated(usage));
}
CompletionRequestStatus::ToolUseLimitReached => {
thread.tool_use_limit_reached = true;
@@ -1,14 +1,12 @@
mod agent_notification;
-pub mod agent_preview;
mod animated_label;
mod context_pill;
mod max_mode_tooltip;
+pub mod preview;
mod upsell;
mod usage_banner;
pub use agent_notification::*;
-pub use agent_preview::*;
pub use animated_label::*;
pub use context_pill::*;
pub use max_mode_tooltip::*;
-pub use usage_banner::*;
@@ -0,0 +1,5 @@
+mod agent_preview;
+mod usage_callouts;
+
+pub use agent_preview::*;
+pub use usage_callouts::*;
@@ -42,14 +42,14 @@ pub trait AgentPreview: Component + Sized {
#[macro_export]
macro_rules! register_agent_preview {
($type:ty) => {
- #[linkme::distributed_slice($crate::ui::agent_preview::__ALL_AGENT_PREVIEWS)]
+ #[linkme::distributed_slice($crate::ui::preview::__ALL_AGENT_PREVIEWS)]
static __REGISTER_AGENT_PREVIEW: fn() -> (
component::ComponentId,
- $crate::ui::agent_preview::PreviewFn,
+ $crate::ui::preview::PreviewFn,
) = || {
(
<$type as component::Component>::id(),
- <$type as $crate::ui::agent_preview::AgentPreview>::agent_preview,
+ <$type as $crate::ui::preview::AgentPreview>::agent_preview,
)
};
};
@@ -0,0 +1,204 @@
+use component::{empty_example, example_group_with_title, single_example};
+use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
+use language_model::RequestUsage;
+use ui::{Callout, Color, Icon, IconName, IconSize, prelude::*};
+use zed_llm_client::{Plan, UsageLimit};
+
+#[derive(IntoElement, RegisterComponent)]
+pub struct UsageCallout {
+ plan: Plan,
+ usage: RequestUsage,
+}
+
+impl UsageCallout {
+ pub fn new(plan: Plan, usage: RequestUsage) -> Self {
+ Self { plan, usage }
+ }
+}
+
+impl RenderOnce for UsageCallout {
+ fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
+ let (is_limit_reached, is_approaching_limit, remaining) = match self.usage.limit {
+ UsageLimit::Limited(limit) => {
+ let percentage = self.usage.amount as f32 / limit as f32;
+ let is_limit_reached = percentage >= 1.0;
+ let is_near_limit = percentage >= 0.9 && percentage < 1.0;
+ (
+ is_limit_reached,
+ is_near_limit,
+ limit.saturating_sub(self.usage.amount),
+ )
+ }
+ UsageLimit::Unlimited => (false, false, 0),
+ };
+
+ if !is_limit_reached && !is_approaching_limit {
+ return div().into_any_element();
+ }
+
+ let (title, message, button_text, url) = if is_limit_reached {
+ match self.plan {
+ Plan::Free => (
+ "Out of free prompts",
+ "Upgrade to continue, wait for the next reset, or switch to API key."
+ .to_string(),
+ "Upgrade",
+ "https://zed.dev/pricing",
+ ),
+ Plan::ZedProTrial => (
+ "Out of trial prompts",
+ "Upgrade to Zed Pro to continue, or switch to API key.".to_string(),
+ "Upgrade",
+ "https://zed.dev/pricing",
+ ),
+ Plan::ZedPro => (
+ "Out of included prompts",
+ "Enable usage based billing to continue.".to_string(),
+ "Manage",
+ "https://zed.dev/account",
+ ),
+ }
+ } else {
+ match self.plan {
+ Plan::Free => (
+ "Reaching Free tier limit soon",
+ format!(
+ "{} remaining - Upgrade to increase limit, or switch providers",
+ remaining
+ ),
+ "Upgrade",
+ "https://zed.dev/pricing",
+ ),
+ Plan::ZedProTrial => (
+ "Reaching Trial limit soon",
+ format!(
+ "{} remaining - Upgrade to increase limit, or switch providers",
+ remaining
+ ),
+ "Upgrade",
+ "https://zed.dev/pricing",
+ ),
+ _ => return div().into_any_element(),
+ }
+ };
+
+ let icon = if is_limit_reached {
+ Icon::new(IconName::X)
+ .color(Color::Error)
+ .size(IconSize::XSmall)
+ } else {
+ Icon::new(IconName::Warning)
+ .color(Color::Warning)
+ .size(IconSize::XSmall)
+ };
+
+ Callout::multi_line(
+ title.into(),
+ message.into(),
+ icon,
+ button_text.into(),
+ Box::new(move |_, _, cx| {
+ cx.open_url(url);
+ }),
+ )
+ .into_any_element()
+ }
+}
+
+impl Component for UsageCallout {
+ fn scope() -> ComponentScope {
+ ComponentScope::Agent
+ }
+
+ fn sort_name() -> &'static str {
+ "AgentUsageCallout"
+ }
+
+ fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
+ let free_examples = example_group_with_title(
+ "Free Plan",
+ vec![
+ single_example(
+ "Approaching limit (90%)",
+ UsageCallout::new(
+ Plan::Free,
+ RequestUsage {
+ limit: UsageLimit::Limited(50),
+ amount: 45, // 90% of limit
+ },
+ )
+ .into_any_element(),
+ ),
+ single_example(
+ "Limit reached (100%)",
+ UsageCallout::new(
+ Plan::Free,
+ RequestUsage {
+ limit: UsageLimit::Limited(50),
+ amount: 50, // 100% of limit
+ },
+ )
+ .into_any_element(),
+ ),
+ ],
+ );
+
+ let trial_examples = example_group_with_title(
+ "Zed Pro Trial",
+ vec![
+ single_example(
+ "Approaching limit (90%)",
+ UsageCallout::new(
+ Plan::ZedProTrial,
+ RequestUsage {
+ limit: UsageLimit::Limited(150),
+ amount: 135, // 90% of limit
+ },
+ )
+ .into_any_element(),
+ ),
+ single_example(
+ "Limit reached (100%)",
+ UsageCallout::new(
+ Plan::ZedProTrial,
+ RequestUsage {
+ limit: UsageLimit::Limited(150),
+ amount: 150, // 100% of limit
+ },
+ )
+ .into_any_element(),
+ ),
+ ],
+ );
+
+ let pro_examples = example_group_with_title(
+ "Zed Pro",
+ vec![
+ single_example(
+ "Limit reached (100%)",
+ UsageCallout::new(
+ Plan::ZedPro,
+ RequestUsage {
+ limit: UsageLimit::Limited(500),
+ amount: 500, // 100% of limit
+ },
+ )
+ .into_any_element(),
+ ),
+ empty_example("Unlimited plan (no callout shown)"),
+ ],
+ );
+
+ Some(
+ div()
+ .p_4()
+ .flex()
+ .flex_col()
+ .gap_4()
+ .child(free_examples)
+ .child(trial_examples)
+ .child(pro_examples)
+ .into_any_element(),
+ )
+ }
+}
@@ -4,8 +4,8 @@ use std::sync::LazyLock;
use collections::HashMap;
use gpui::{
- AnyElement, App, IntoElement, RenderOnce, SharedString, Window, div, pattern_slash, prelude::*,
- px, rems,
+ AnyElement, App, IntoElement, Pixels, RenderOnce, SharedString, Window, div, pattern_slash,
+ prelude::*, px, rems,
};
use linkme::distributed_slice;
use parking_lot::RwLock;
@@ -249,13 +249,20 @@ pub struct ComponentExample {
pub variant_name: SharedString,
pub description: Option<SharedString>,
pub element: AnyElement,
+ pub width: Option<Pixels>,
}
impl RenderOnce for ComponentExample {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
div()
.pt_2()
- .w_full()
+ .map(|this| {
+ if let Some(width) = self.width {
+ this.w(width)
+ } else {
+ this.w_full()
+ }
+ })
.flex()
.flex_col()
.gap_3()
@@ -306,6 +313,7 @@ impl ComponentExample {
variant_name: variant_name.into(),
element,
description: None,
+ width: None,
}
}
@@ -313,6 +321,11 @@ impl ComponentExample {
self.description = Some(description.into());
self
}
+
+ pub fn width(mut self, width: Pixels) -> Self {
+ self.width = Some(width);
+ self
+ }
}
/// A group of component examples.
@@ -320,6 +333,7 @@ impl ComponentExample {
pub struct ComponentExampleGroup {
pub title: Option<SharedString>,
pub examples: Vec<ComponentExample>,
+ pub width: Option<Pixels>,
pub grow: bool,
pub vertical: bool,
}
@@ -330,7 +344,13 @@ impl RenderOnce for ComponentExampleGroup {
.flex_col()
.text_sm()
.text_color(cx.theme().colors().text_muted)
- .w_full()
+ .map(|this| {
+ if let Some(width) = self.width {
+ this.w(width)
+ } else {
+ this.w_full()
+ }
+ })
.when_some(self.title, |this, title| {
this.gap_4().child(
div()
@@ -373,6 +393,7 @@ impl ComponentExampleGroup {
Self {
title: None,
examples,
+ width: None,
grow: false,
vertical: false,
}
@@ -381,10 +402,15 @@ impl ComponentExampleGroup {
Self {
title: Some(title.into()),
examples,
+ width: None,
grow: false,
vertical: false,
}
}
+ pub fn width(mut self, width: Pixels) -> Self {
+ self.width = Some(width);
+ self
+ }
pub fn grow(mut self) -> Self {
self.grow = true;
self
@@ -402,6 +428,10 @@ pub fn single_example(
ComponentExample::new(variant_name, example)
}
+pub fn empty_example(variant_name: impl Into<SharedString>) -> ComponentExample {
+ ComponentExample::new(variant_name, div().w_full().text_center().items_center().text_xs().opacity(0.4).child("This space is intentionally left blank. It indicates a case that should render nothing.").into_any_element())
+}
+
pub fn example_group(examples: Vec<ComponentExample>) -> ComponentExampleGroup {
ComponentExampleGroup::new(examples)
}
@@ -110,7 +110,6 @@ struct ComponentPreview {
active_page: PreviewPage,
components: Vec<ComponentMetadata>,
component_list: ListState,
- agent_previews: Vec<ComponentId>,
cursor_index: usize,
language_registry: Arc<LanguageRegistry>,
workspace: WeakEntity<Workspace>,
@@ -179,9 +178,6 @@ impl ComponentPreview {
},
);
- // Initialize agent previews
- let agent_previews = agent::all_agent_previews();
-
let mut component_preview = Self {
workspace_id: None,
focus_handle: cx.focus_handle(),
@@ -195,7 +191,6 @@ impl ComponentPreview {
component_map: components().0,
components: sorted_components,
component_list,
- agent_previews,
cursor_index: selected_index,
filter_editor,
filter_text: String::new(),
@@ -707,38 +702,22 @@ impl ComponentPreview {
}
}
- fn render_active_thread(
- &self,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> impl IntoElement {
+ fn render_active_thread(&self, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.id("render-active-thread")
.size_full()
.child(
- 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(),
- ) {
- agent::get_agent_preview(
- component_id,
- self.workspace.clone(),
- active_thread,
- thread_store,
- window,
- cx,
- )
- .map(|element| div().child(element))
- } else {
- None
- }
- })),
+ div()
+ .mx_auto()
+ .w(px(640.))
+ .h_full()
+ .py_8()
+ .bg(cx.theme().colors().panel_background)
+ .children(self.active_thread.clone().map(|thread| thread.clone()))
+ .when_none(&self.active_thread.clone(), |this| {
+ this.child("No active thread")
+ }),
)
- .children(self.active_thread.clone().map(|thread| thread.clone()))
- .when_none(&self.active_thread.clone(), |this| {
- this.child("No active thread")
- })
.into_any_element()
}
@@ -852,7 +831,7 @@ impl Render for ComponentPreview {
.render_component_page(&id, window, cx)
.into_any_element(),
PreviewPage::ActiveThread => {
- self.render_active_thread(window, cx).into_any_element()
+ self.render_active_thread(cx).into_any_element()
}
}),
)
@@ -1,6 +1,7 @@
mod avatar;
mod banner;
mod button;
+mod callout;
mod content_group;
mod context_menu;
mod disclosure;
@@ -41,6 +42,7 @@ mod stories;
pub use avatar::*;
pub use banner::*;
pub use button::*;
+pub use callout::*;
pub use content_group::*;
pub use context_menu::*;
pub use disclosure::*;
@@ -0,0 +1,162 @@
+use crate::prelude::*;
+use gpui::ClickEvent;
+
+#[derive(IntoElement, RegisterComponent)]
+pub struct Callout {
+ title: SharedString,
+ message: Option<SharedString>,
+ icon: Icon,
+ cta_label: SharedString,
+ cta_action: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
+ line_height: Option<Pixels>,
+}
+
+impl Callout {
+ pub fn single_line(
+ title: SharedString,
+ icon: Icon,
+ cta_label: SharedString,
+ cta_action: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
+ ) -> Self {
+ Self {
+ title,
+ message: None,
+ icon,
+ cta_label,
+ cta_action,
+ line_height: None,
+ }
+ }
+
+ pub fn multi_line(
+ title: SharedString,
+ message: SharedString,
+ icon: Icon,
+ cta_label: SharedString,
+ cta_action: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
+ ) -> Self {
+ Self {
+ title,
+ message: Some(message),
+ icon,
+ cta_label,
+ cta_action,
+ line_height: None,
+ }
+ }
+
+ pub fn line_height(mut self, line_height: Pixels) -> Self {
+ self.line_height = Some(line_height);
+ self
+ }
+}
+
+impl RenderOnce for Callout {
+ fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
+ let line_height = self.line_height.unwrap_or(window.line_height());
+
+ h_flex()
+ .p_2()
+ .gap_2()
+ .w_full()
+ .items_center()
+ .justify_between()
+ .bg(cx.theme().colors().panel_background)
+ .border_t_1()
+ .border_color(cx.theme().colors().border)
+ .overflow_x_hidden()
+ .child(
+ h_flex()
+ .flex_shrink()
+ .overflow_hidden()
+ .gap_2()
+ .items_start()
+ .child(
+ h_flex()
+ .h(line_height)
+ .items_center()
+ .justify_center()
+ .child(self.icon),
+ )
+ .child(
+ v_flex()
+ .flex_shrink()
+ .overflow_hidden()
+ .child(
+ h_flex()
+ .h(line_height)
+ .items_center()
+ .child(Label::new(self.title).size(LabelSize::Small)),
+ )
+ .when_some(self.message, |this, message| {
+ this.child(
+ div()
+ .w_full()
+ .flex_1()
+ .child(message)
+ .text_ui_sm(cx)
+ .text_color(cx.theme().colors().text_muted),
+ )
+ }),
+ ),
+ )
+ .child(
+ div().flex_none().child(
+ Button::new("cta", self.cta_label)
+ .on_click(self.cta_action)
+ .style(ButtonStyle::Filled)
+ .label_size(LabelSize::Small),
+ ),
+ )
+ }
+}
+
+impl Component for Callout {
+ fn scope() -> ComponentScope {
+ ComponentScope::Notification
+ }
+
+ fn description() -> Option<&'static str> {
+ Some(
+ "Used to display a callout for situations where the user needs to know some information, and likely make a decision. This might be a thread running out of tokens, or running out of prompts on a plan and needing to upgrade.",
+ )
+ }
+
+ fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
+ let callout_examples = vec![
+ single_example(
+ "Single Line",
+ Callout::single_line(
+ "Your settings contain deprecated values, please update them.".into(),
+ Icon::new(IconName::Warning)
+ .color(Color::Warning)
+ .size(IconSize::Small),
+ "Backup & Update".into(),
+ Box::new(|_, _, _| {}),
+ )
+ .into_any_element(),
+ )
+ .width(px(580.)),
+ single_example(
+ "Multi Line",
+ Callout::multi_line(
+ "Thread reached the token limit".into(),
+ "Start a new thread from a summary to continue the conversation.".into(),
+ Icon::new(IconName::X)
+ .color(Color::Error)
+ .size(IconSize::Small),
+ "Start New Thread".into(),
+ Box::new(|_, _, _| {}),
+ )
+ .into_any_element(),
+ )
+ .width(px(580.)),
+ ];
+
+ Some(
+ example_group(callout_examples)
+ .vertical()
+ .into_any_element(),
+ )
+ }
+}