From f6f7762f3294c0028797bc6e466a3afcdc76a676 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 24 Jul 2025 11:26:15 -0300 Subject: [PATCH] ai onboarding: Add overall fixes to the whole flow (#34996) Closes https://github.com/zed-industries/zed/issues/34979 Release Notes: - N/A --------- Co-authored-by: Agus Zubiaga Co-authored-by: Ben Kunkle --- crates/agent/src/thread_store.rs | 9 +- crates/agent_ui/src/agent_configuration.rs | 11 +- crates/agent_ui/src/agent_panel.rs | 56 +++--- crates/agent_ui/src/message_editor.rs | 52 +++--- crates/agent_ui/src/ui.rs | 1 - crates/agent_ui/src/ui/end_trial_upsell.rs | 55 +++--- crates/agent_ui/src/ui/upsell.rs | 163 ------------------ .../src/agent_panel_onboarding_content.rs | 7 +- crates/ai_onboarding/src/ai_onboarding.rs | 30 +++- crates/assistant_context/src/context_store.rs | 5 + crates/client/src/user.rs | 6 +- crates/language_models/src/provider/cloud.rs | 5 +- 12 files changed, 150 insertions(+), 250 deletions(-) delete mode 100644 crates/agent_ui/src/ui/upsell.rs diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index 0347156cd4..cc7cb50c91 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -41,6 +41,9 @@ use std::{ }; use util::ResultExt as _; +pub static ZED_STATELESS: std::sync::LazyLock = + 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 ( diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 334c5ee6dc..fabeee2bce 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -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() .when(is_expanded, |this| this.mb_2()) .child( @@ -230,8 +237,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 { diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 6ae2f12b5e..a0250816a0 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -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) diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 62be5629f1..c160f1de04 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -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, context_store: Entity, prompt_store: Option>, + history_store: Option>, context_strip: Entity, context_picker_menu_handle: PopoverMenuHandle, model_selector: Entity, @@ -161,6 +164,7 @@ impl MessageEditor { prompt_store: Option>, thread_store: WeakEntity, text_thread_store: WeakEntity, + history_store: Option>, thread: Entity, window: &mut Window, cx: &mut Context, @@ -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, diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index 15f2e28e58..b477a8c385 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -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::*; diff --git a/crates/agent_ui/src/ui/end_trial_upsell.rs b/crates/agent_ui/src/ui/end_trial_upsell.rs index 9c2dd98d20..36770c2197 100644 --- a/crates/agent_ui/src/ui/end_trial_upsell.rs +++ b/crates/agent_ui/src/ui/end_trial_upsell.rs @@ -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) + } + }), + ), + ) } } diff --git a/crates/agent_ui/src/ui/upsell.rs b/crates/agent_ui/src/ui/upsell.rs deleted file mode 100644 index f311aade22..0000000000 --- a/crates/agent_ui/src/ui/upsell.rs +++ /dev/null @@ -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, - on_dismiss: Box, - on_dont_show_again: Box, -} - -impl Upsell { - /// Create a new upsell component - pub fn new( - title: impl Into, - message: impl Into, - cta_text: impl Into, - on_click: Box, - on_dismiss: Box, - on_dont_show_again: Box, - ) -> 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 { - 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()) - } -} diff --git a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs index 771482abf3..e8a62f7ff2 100644 --- a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs +++ b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs @@ -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()) diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs index f9a91503ae..7fffb60ecc 100644 --- a/crates/ai_onboarding/src/ai_onboarding.rs +++ b/crates/ai_onboarding/src/ai_onboarding.rs @@ -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") diff --git a/crates/assistant_context/src/context_store.rs b/crates/assistant_context/src/context_store.rs index 3400913eb8..3090a7b234 100644 --- a/crates/assistant_context/src/context_store.rs +++ b/crates/assistant_context/src/context_store.rs @@ -767,6 +767,11 @@ impl ContextStore { fn reload(&mut self, cx: &mut Context) -> Task> { let fs = self.fs.clone(); cx.spawn(async move |this, cx| { + pub static ZED_STATELESS: LazyLock = + 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?; diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index f5213fbcb6..5ed258aa8e 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -765,12 +765,14 @@ impl UserStore { pub fn current_plan(&self) -> Option { #[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'"); + } }; } diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index fac8810714..09a2ac6e0a 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -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() };