Detailed changes
@@ -41,6 +41,9 @@ use std::{
};
use util::ResultExt as _;
+pub static ZED_STATELESS: std::sync::LazyLock<bool> =
+ std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()));
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DataType {
#[serde(rename = "json")]
@@ -874,7 +877,11 @@ impl ThreadsDatabase {
let needs_migration_from_heed = mdb_path.exists();
- let connection = Connection::open_file(&sqlite_path.to_string_lossy());
+ let connection = if *ZED_STATELESS {
+ Connection::open_memory(Some("THREAD_FALLBACK_DB"))
+ } else {
+ Connection::open_file(&sqlite_path.to_string_lossy())
+ };
connection.exec(indoc! {"
CREATE TABLE IF NOT EXISTS threads (
@@ -185,6 +185,13 @@ impl AgentConfiguration {
None
};
+ let is_signed_in = self
+ .workspace
+ .read_with(cx, |workspace, _| {
+ workspace.client().status().borrow().is_connected()
+ })
+ .unwrap_or(false);
+
v_flex()
.w_full()
.when(is_expanded, |this| this.mb_2())
@@ -233,8 +240,8 @@ impl AgentConfiguration {
.size(LabelSize::Large),
)
.map(|this| {
- if is_zed_provider {
- this.gap_2().child(
+ if is_zed_provider && is_signed_in {
+ this.child(
self.render_zed_plan_info(current_plan, cx),
)
} else {
@@ -564,6 +564,17 @@ impl AgentPanel {
let inline_assist_context_store =
cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade())));
+ let thread_id = thread.read(cx).id().clone();
+
+ let history_store = cx.new(|cx| {
+ HistoryStore::new(
+ thread_store.clone(),
+ context_store.clone(),
+ [HistoryEntryId::Thread(thread_id)],
+ cx,
+ )
+ });
+
let message_editor = cx.new(|cx| {
MessageEditor::new(
fs.clone(),
@@ -573,22 +584,13 @@ impl AgentPanel {
prompt_store.clone(),
thread_store.downgrade(),
context_store.downgrade(),
+ Some(history_store.downgrade()),
thread.clone(),
window,
cx,
)
});
- let thread_id = thread.read(cx).id().clone();
- let history_store = cx.new(|cx| {
- HistoryStore::new(
- thread_store.clone(),
- context_store.clone(),
- [HistoryEntryId::Thread(thread_id)],
- cx,
- )
- });
-
cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
let active_thread = cx.new(|cx| {
@@ -851,6 +853,7 @@ impl AgentPanel {
self.prompt_store.clone(),
self.thread_store.downgrade(),
self.context_store.downgrade(),
+ Some(self.history_store.downgrade()),
thread.clone(),
window,
cx,
@@ -1124,6 +1127,7 @@ impl AgentPanel {
self.prompt_store.clone(),
self.thread_store.downgrade(),
self.context_store.downgrade(),
+ Some(self.history_store.downgrade()),
thread.clone(),
window,
cx,
@@ -2283,20 +2287,21 @@ impl AgentPanel {
}
match &self.active_view {
- ActiveView::Thread { thread, .. } => thread
- .read(cx)
- .thread()
- .read(cx)
- .configured_model()
- .map_or(true, |model| {
- model.provider.id() == language_model::ZED_CLOUD_PROVIDER_ID
- }),
- ActiveView::TextThread { .. } => LanguageModelRegistry::global(cx)
- .read(cx)
- .default_model()
- .map_or(true, |model| {
- model.provider.id() == language_model::ZED_CLOUD_PROVIDER_ID
- }),
+ ActiveView::Thread { .. } | ActiveView::TextThread { .. } => {
+ let history_is_empty = self
+ .history_store
+ .update(cx, |store, cx| store.recent_entries(1, cx).is_empty());
+
+ let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
+ .providers()
+ .iter()
+ .any(|provider| {
+ provider.is_authenticated(cx)
+ && provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
+ });
+
+ history_is_empty || !has_configured_non_zed_providers
+ }
ActiveView::ExternalAgentThread { .. }
| ActiveView::History
| ActiveView::Configuration => false,
@@ -2317,9 +2322,8 @@ impl AgentPanel {
Some(
div()
- .size_full()
.when(thread_view, |this| {
- this.bg(cx.theme().colors().panel_background)
+ this.size_full().bg(cx.theme().colors().panel_background)
})
.when(text_thread_view, |this| {
this.bg(cx.theme().colors().editor_background)
@@ -9,6 +9,7 @@ use crate::ui::{
MaxModeTooltip,
preview::{AgentPreview, UsageCallout},
};
+use agent::history_store::HistoryStore;
use agent::{
context::{AgentContextKey, ContextLoadResult, load_context},
context_store::ContextStoreEvent,
@@ -29,8 +30,9 @@ use fs::Fs;
use futures::future::Shared;
use futures::{FutureExt as _, future};
use gpui::{
- Animation, AnimationExt, App, Entity, EventEmitter, Focusable, KeyContext, Subscription, Task,
- TextStyle, WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
+ Animation, AnimationExt, App, Entity, EventEmitter, Focusable, IntoElement, KeyContext,
+ Subscription, Task, TextStyle, WeakEntity, linear_color_stop, linear_gradient, point,
+ pulsating_between,
};
use language::{Buffer, Language, Point};
use language_model::{
@@ -80,6 +82,7 @@ pub struct MessageEditor {
user_store: Entity<UserStore>,
context_store: Entity<ContextStore>,
prompt_store: Option<Entity<PromptStore>>,
+ history_store: Option<WeakEntity<HistoryStore>>,
context_strip: Entity<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: Entity<AgentModelSelector>,
@@ -161,6 +164,7 @@ impl MessageEditor {
prompt_store: Option<Entity<PromptStore>>,
thread_store: WeakEntity<ThreadStore>,
text_thread_store: WeakEntity<TextThreadStore>,
+ history_store: Option<WeakEntity<HistoryStore>>,
thread: Entity<Thread>,
window: &mut Window,
cx: &mut Context<Self>,
@@ -233,6 +237,7 @@ impl MessageEditor {
workspace,
context_store,
prompt_store,
+ history_store,
context_strip,
context_picker_menu_handle,
load_context_task: None,
@@ -1661,32 +1666,36 @@ impl Render for MessageEditor {
let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5;
- let in_pro_trial = matches!(
- self.user_store.read(cx).current_plan(),
- Some(proto::Plan::ZedProTrial)
- );
+ let has_configured_providers = LanguageModelRegistry::read_global(cx)
+ .providers()
+ .iter()
+ .filter(|provider| {
+ provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
+ })
+ .count()
+ > 0;
- let pro_user = matches!(
- self.user_store.read(cx).current_plan(),
- Some(proto::Plan::ZedPro)
- );
+ let is_signed_out = self
+ .workspace
+ .read_with(cx, |workspace, _| {
+ workspace.client().status().borrow().is_signed_out()
+ })
+ .unwrap_or(true);
- let configured_providers: Vec<(IconName, SharedString)> =
- LanguageModelRegistry::read_global(cx)
- .providers()
- .iter()
- .filter(|provider| {
- provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
- })
- .map(|provider| (provider.icon(), provider.name().0.clone()))
- .collect();
- let has_existing_providers = configured_providers.len() > 0;
+ let has_history = self
+ .history_store
+ .as_ref()
+ .and_then(|hs| hs.update(cx, |hs, cx| hs.entries(cx).len() > 0).ok())
+ .unwrap_or(false)
+ || self
+ .thread
+ .read_with(cx, |thread, _| thread.messages().len() > 0);
v_flex()
.size_full()
.bg(cx.theme().colors().panel_background)
.when(
- has_existing_providers && !in_pro_trial && !pro_user,
+ !has_history && is_signed_out && has_configured_providers,
|this| this.child(cx.new(ApiKeysWithProviders::new)),
)
.when(changed_buffers.len() > 0, |parent| {
@@ -1778,6 +1787,7 @@ impl AgentPreview for MessageEditor {
None,
thread_store.downgrade(),
text_thread_store.downgrade(),
+ None,
thread,
window,
cx,
@@ -5,7 +5,6 @@ mod end_trial_upsell;
mod new_thread_button;
mod onboarding_modal;
pub mod preview;
-mod upsell;
pub use agent_notification::*;
pub use burn_mode_tooltip::*;
@@ -3,7 +3,7 @@ use std::sync::Arc;
use ai_onboarding::{AgentPanelOnboardingCard, BulletItem};
use client::zed_urls;
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
-use ui::{Divider, List, prelude::*};
+use ui::{Divider, List, Tooltip, prelude::*};
#[derive(IntoElement, RegisterComponent)]
pub struct EndTrialUpsell {
@@ -33,14 +33,19 @@ impl RenderOnce for EndTrialUpsell {
)
.child(
List::new()
- .child(BulletItem::new("500 prompts per month with Claude models"))
- .child(BulletItem::new("Unlimited edit predictions")),
+ .child(BulletItem::new("500 prompts with Claude models"))
+ .child(BulletItem::new(
+ "Unlimited edit predictions with Zeta, our open-source model",
+ )),
)
.child(
Button::new("cta-button", "Upgrade to Zed Pro")
.full_width()
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
- .on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))),
+ .on_click(move |_, _window, cx| {
+ telemetry::event!("Upgrade To Pro Clicked", state = "end-of-trial");
+ cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
+ }),
);
let free_section = v_flex()
@@ -55,37 +60,43 @@ impl RenderOnce for EndTrialUpsell {
.color(Color::Muted)
.buffer_font(cx),
)
+ .child(
+ Label::new("(Current Plan)")
+ .size(LabelSize::Small)
+ .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.6)))
+ .buffer_font(cx),
+ )
.child(Divider::horizontal()),
)
.child(
List::new()
- .child(BulletItem::new(
- "50 prompts per month with the Claude models",
- ))
- .child(BulletItem::new(
- "2000 accepted edit predictions using our open-source Zeta model",
- )),
- )
- .child(
- Button::new("dismiss-button", "Stay on Free")
- .full_width()
- .style(ButtonStyle::Outlined)
- .on_click({
- let callback = self.dismiss_upsell.clone();
- move |_, window, cx| callback(window, cx)
- }),
+ .child(BulletItem::new("50 prompts with the Claude models"))
+ .child(BulletItem::new("2,000 accepted edit predictions")),
);
AgentPanelOnboardingCard::new()
- .child(Headline::new("Your Zed Pro trial has expired."))
+ .child(Headline::new("Your Zed Pro Trial has expired"))
.child(
Label::new("You've been automatically reset to the Free plan.")
- .size(LabelSize::Small)
.color(Color::Muted)
- .mb_1(),
+ .mb_2(),
)
.child(pro_section)
.child(free_section)
+ .child(
+ h_flex().absolute().top_4().right_4().child(
+ IconButton::new("dismiss_onboarding", IconName::Close)
+ .icon_size(IconSize::Small)
+ .tooltip(Tooltip::text("Dismiss"))
+ .on_click({
+ let callback = self.dismiss_upsell.clone();
+ move |_, window, cx| {
+ telemetry::event!("Banner Dismissed", source = "AI Onboarding");
+ callback(window, cx)
+ }
+ }),
+ ),
+ )
}
}
@@ -1,163 +0,0 @@
-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 access to advanced 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) + 'static>,
- on_dismiss: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
- on_dont_show_again: Box<dyn Fn(bool, &mut Window, &mut App) + 'static>,
-}
-
-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) + 'static>,
- on_dismiss: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
- on_dont_show_again: Box<dyn Fn(bool, &mut Window, &mut App) + 'static>,
- ) -> 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", "No Thanks")
- .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())
- }
-}
@@ -61,6 +61,11 @@ impl Render for AgentPanelOnboarding {
Some(proto::Plan::ZedProTrial)
);
+ let is_pro_user = matches!(
+ self.user_store.read(cx).current_plan(),
+ Some(proto::Plan::ZedPro)
+ );
+
AgentPanelOnboardingCard::new()
.child(
ZedAiOnboarding::new(
@@ -75,7 +80,7 @@ impl Render for AgentPanelOnboarding {
}),
)
.map(|this| {
- if enrolled_in_trial || self.configured_providers.len() >= 1 {
+ if enrolled_in_trial || is_pro_user || self.configured_providers.len() >= 1 {
this
} else {
this.child(ApiKeysWithoutProviders::new())
@@ -16,6 +16,7 @@ use client::{Client, UserStore, zed_urls};
use gpui::{AnyElement, Entity, IntoElement, ParentElement, SharedString};
use ui::{Divider, List, ListItem, RegisterComponent, TintColor, Tooltip, prelude::*};
+#[derive(IntoElement)]
pub struct BulletItem {
label: SharedString,
}
@@ -28,18 +29,27 @@ impl BulletItem {
}
}
-impl IntoElement for BulletItem {
- type Element = AnyElement;
+impl RenderOnce for BulletItem {
+ fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
+ let line_height = 0.85 * window.line_height();
- fn into_element(self) -> Self::Element {
ListItem::new("list-item")
.selectable(false)
- .start_slot(
- Icon::new(IconName::Dash)
- .size(IconSize::XSmall)
- .color(Color::Hidden),
+ .child(
+ h_flex()
+ .w_full()
+ .min_w_0()
+ .gap_1()
+ .items_start()
+ .child(
+ h_flex().h(line_height).justify_center().child(
+ Icon::new(IconName::Dash)
+ .size(IconSize::XSmall)
+ .color(Color::Hidden),
+ ),
+ )
+ .child(div().w_full().min_w_0().child(Label::new(self.label))),
)
- .child(div().w_full().child(Label::new(self.label)))
.into_any_element()
}
}
@@ -373,7 +383,9 @@ impl ZedAiOnboarding {
.child(
List::new()
.child(BulletItem::new("500 prompts with Claude models"))
- .child(BulletItem::new("Unlimited edit predictions")),
+ .child(BulletItem::new(
+ "Unlimited edit predictions with Zeta, our open-source model",
+ )),
)
.child(
Button::new("pro", "Continue with Zed Pro")
@@ -767,6 +767,11 @@ impl ContextStore {
fn reload(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
let fs = self.fs.clone();
cx.spawn(async move |this, cx| {
+ pub static ZED_STATELESS: LazyLock<bool> =
+ LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()));
+ if *ZED_STATELESS {
+ return Ok(());
+ }
fs.create_dir(contexts_dir()).await?;
let mut paths = fs.read_dir(contexts_dir()).await?;
@@ -765,12 +765,14 @@ impl UserStore {
pub fn current_plan(&self) -> Option<proto::Plan> {
#[cfg(debug_assertions)]
- if let Ok(plan) = std::env::var("ZED_SIMULATE_ZED_PRO_PLAN").as_ref() {
+ if let Ok(plan) = std::env::var("ZED_SIMULATE_PLAN").as_ref() {
return match plan.as_str() {
"free" => Some(proto::Plan::Free),
"trial" => Some(proto::Plan::ZedProTrial),
"pro" => Some(proto::Plan::ZedPro),
- _ => None,
+ _ => {
+ panic!("ZED_SIMULATE_PLAN must be one of 'free', 'trial', or 'pro'");
+ }
};
}
@@ -1159,19 +1159,20 @@ impl RenderOnce for ZedAiConfiguration {
let manage_subscription_buttons = if is_pro {
Button::new("manage_settings", "Manage Subscription")
+ .full_width()
.style(ButtonStyle::Tinted(TintColor::Accent))
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx)))
.into_any_element()
} else if self.plan.is_none() || self.eligible_for_trial {
Button::new("start_trial", "Start 14-day Free Pro Trial")
- .style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
.full_width()
+ .style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click(|_, _, cx| cx.open_url(&zed_urls::start_trial_url(cx)))
.into_any_element()
} else {
Button::new("upgrade", "Upgrade to Pro")
- .style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
.full_width()
+ .style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)))
.into_any_element()
};