From ea4073e50e3f21f46bc64c2f3864398b3946e22a Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 8 Sep 2025 20:18:43 -0400 Subject: [PATCH] cloud_llm_client: Add new `Plan` variants (#37810) This PR adds new variants to the `Plan` enum. Release Notes: - N/A --- Cargo.lock | 1 + crates/agent_ui/src/acp/thread_view.rs | 1 + crates/agent_ui/src/agent_configuration.rs | 6 ++- crates/agent_ui/src/agent_panel.rs | 1 + crates/agent_ui/src/ui/end_trial_upsell.rs | 7 ++-- .../agent_ui/src/ui/preview/usage_callouts.rs | 4 +- crates/ai_onboarding/Cargo.toml | 1 + crates/ai_onboarding/src/ai_onboarding.rs | 39 +++++++++---------- crates/ai_onboarding/src/ai_upsell_card.rs | 23 +++++------ crates/ai_onboarding/src/plan_definitions.rs | 6 +-- .../cloud_llm_client/src/cloud_llm_client.rs | 8 ++++ crates/feature_flags/src/feature_flags.rs | 6 +++ .../language_model/src/model/cloud_model.rs | 1 + crates/title_bar/src/title_bar.rs | 8 +++- 14 files changed, 68 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 47281f51fa7c80d5d04b0ff1e1ea40f3dd2ad4f3..eee80226fa69e8fa076246f34d7009f135352e4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -482,6 +482,7 @@ dependencies = [ "client", "cloud_llm_client", "component", + "feature_flags", "gpui", "language_model", "serde", diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 8e8d5493cfd3408ba6fe0338b97f16bb8dbf15ba..e9794160a2c6facc4f5a9aacf700aae8b1a1eb72 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -5005,6 +5005,7 @@ impl AcpThreadView { cloud_llm_client::Plan::ZedProTrial | cloud_llm_client::Plan::ZedFree => { "Upgrade to Zed Pro for more prompts." } + cloud_llm_client::Plan::ZedProV2 | cloud_llm_client::Plan::ZedProTrialV2 => "", }; Callout::new() diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 8ae21841fcfd8ac834541f1060ac1fc5dc04b7c2..bc7dd1d55296075a375eb6f98662d9a626636ef7 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -516,8 +516,10 @@ impl AgentConfiguration { let (plan_name, label_color, bg_color) = match plan { Plan::ZedFree => ("Free", Color::Default, free_chip_bg), - Plan::ZedProTrial => ("Pro Trial", Color::Accent, pro_chip_bg), - Plan::ZedPro => ("Pro", Color::Accent, pro_chip_bg), + Plan::ZedProTrial | Plan::ZedProTrialV2 => { + ("Pro Trial", Color::Accent, pro_chip_bg) + } + Plan::ZedPro | Plan::ZedProV2 => ("Pro", Color::Accent, pro_chip_bg), }; Chip::new(plan_name.to_string()) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index e1ac67f1c8ba48ea60eee4be50ffedd14c3cf9c3..67a0988f7fb4a49a5f2453fd57beb52c3e2dd16b 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -3518,6 +3518,7 @@ impl AgentPanel { let error_message = match plan { Plan::ZedPro => "Upgrade to usage-based billing for more prompts.", Plan::ZedProTrial | Plan::ZedFree => "Upgrade to Zed Pro for more prompts.", + Plan::ZedProV2 | Plan::ZedProTrialV2 => "", }; Callout::new() diff --git a/crates/agent_ui/src/ui/end_trial_upsell.rs b/crates/agent_ui/src/ui/end_trial_upsell.rs index 3a8a119800543ad033efd563d7896ccc80add373..55164aef716aa2d8d64195c69b765c6e429e8ce5 100644 --- a/crates/agent_ui/src/ui/end_trial_upsell.rs +++ b/crates/agent_ui/src/ui/end_trial_upsell.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use ai_onboarding::{AgentPanelOnboardingCard, PlanDefinitions}; use client::zed_urls; +use feature_flags::{BillingV2FeatureFlag, FeatureFlagAppExt as _}; use gpui::{AnyElement, App, IntoElement, RenderOnce, Window}; use ui::{Divider, Tooltip, prelude::*}; @@ -18,8 +19,6 @@ impl EndTrialUpsell { impl RenderOnce for EndTrialUpsell { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let plan_definitions = PlanDefinitions; - let pro_section = v_flex() .gap_1() .child( @@ -33,7 +32,7 @@ impl RenderOnce for EndTrialUpsell { ) .child(Divider::horizontal()), ) - .child(plan_definitions.pro_plan(false)) + .child(PlanDefinitions.pro_plan(cx.has_flag::(), false)) .child( Button::new("cta-button", "Upgrade to Zed Pro") .full_width() @@ -64,7 +63,7 @@ impl RenderOnce for EndTrialUpsell { ) .child(Divider::horizontal()), ) - .child(plan_definitions.free_plan()); + .child(PlanDefinitions.free_plan(cx.has_flag::())); AgentPanelOnboardingCard::new() .child(Headline::new("Your Zed Pro Trial has expired")) diff --git a/crates/agent_ui/src/ui/preview/usage_callouts.rs b/crates/agent_ui/src/ui/preview/usage_callouts.rs index d4d037b9765e5bd20bbcd547f5cc906285d26711..7c080f075a5101b7082b550d5ffbd9fa8ec92525 100644 --- a/crates/agent_ui/src/ui/preview/usage_callouts.rs +++ b/crates/agent_ui/src/ui/preview/usage_callouts.rs @@ -45,13 +45,13 @@ impl RenderOnce for UsageCallout { "Upgrade", zed_urls::account_url(cx), ), - Plan::ZedProTrial => ( + Plan::ZedProTrial | Plan::ZedProTrialV2 => ( "Out of trial prompts", "Upgrade to Zed Pro to continue, or switch to API key.".to_string(), "Upgrade", zed_urls::account_url(cx), ), - Plan::ZedPro => ( + Plan::ZedPro | Plan::ZedProV2 => ( "Out of included prompts", "Enable usage-based billing to continue.".to_string(), "Manage", diff --git a/crates/ai_onboarding/Cargo.toml b/crates/ai_onboarding/Cargo.toml index 95a45b1a6fbe103f02532d33c21af707f2f51d45..cf3e6e9cd66eff0ce412436d4dc1d2b4b01c0041 100644 --- a/crates/ai_onboarding/Cargo.toml +++ b/crates/ai_onboarding/Cargo.toml @@ -18,6 +18,7 @@ default = [] client.workspace = true cloud_llm_client.workspace = true component.workspace = true +feature_flags.workspace = true gpui.workspace = true language_model.workspace = true serde.workspace = true diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs index 6d8ac6472563ac0abd79d59f44b36a924eee1757..60b8fa89ffe5b1c4779083ff0b5641dd9bf9bcc8 100644 --- a/crates/ai_onboarding/src/ai_onboarding.rs +++ b/crates/ai_onboarding/src/ai_onboarding.rs @@ -18,6 +18,7 @@ pub use young_account_banner::YoungAccountBanner; use std::sync::Arc; use client::{Client, UserStore, zed_urls}; +use feature_flags::{BillingV2FeatureFlag, FeatureFlagAppExt as _}; use gpui::{AnyElement, Entity, IntoElement, ParentElement}; use ui::{Divider, RegisterComponent, Tooltip, prelude::*}; @@ -84,9 +85,8 @@ impl ZedAiOnboarding { self } - fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement { + fn render_sign_in_disclaimer(&self, cx: &mut App) -> AnyElement { let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn); - let plan_definitions = PlanDefinitions; v_flex() .gap_1() @@ -96,7 +96,7 @@ impl ZedAiOnboarding { .color(Color::Muted) .mb_2(), ) - .child(plan_definitions.pro_plan(false)) + .child(PlanDefinitions.pro_plan(cx.has_flag::(), false)) .child( Button::new("sign_in", "Try Zed Pro for Free") .disabled(signing_in) @@ -114,16 +114,13 @@ impl ZedAiOnboarding { } fn render_free_plan_state(&self, cx: &mut App) -> AnyElement { - let young_account_banner = YoungAccountBanner; - let plan_definitions = PlanDefinitions; - if self.account_too_young { v_flex() .relative() .max_w_full() .gap_1() .child(Headline::new("Welcome to Zed AI")) - .child(young_account_banner) + .child(YoungAccountBanner) .child( v_flex() .mt_2() @@ -139,7 +136,9 @@ impl ZedAiOnboarding { ) .child(Divider::horizontal()), ) - .child(plan_definitions.pro_plan(true)) + .child( + PlanDefinitions.pro_plan(cx.has_flag::(), true), + ) .child( Button::new("pro", "Get Started") .full_width() @@ -182,7 +181,7 @@ impl ZedAiOnboarding { ) .child(Divider::horizontal()), ) - .child(plan_definitions.free_plan()), + .child(PlanDefinitions.free_plan(cx.has_flag::())), ) .when_some( self.dismiss_onboarding.as_ref(), @@ -220,7 +219,9 @@ impl ZedAiOnboarding { ) .child(Divider::horizontal()), ) - .child(plan_definitions.pro_trial(true)) + .child( + PlanDefinitions.pro_trial(cx.has_flag::(), true), + ) .child( Button::new("pro", "Start Free Trial") .full_width() @@ -238,9 +239,7 @@ impl ZedAiOnboarding { } } - fn render_trial_state(&self, _cx: &mut App) -> AnyElement { - let plan_definitions = PlanDefinitions; - + fn render_trial_state(&self, is_v2: bool, _cx: &mut App) -> AnyElement { v_flex() .relative() .gap_1() @@ -250,7 +249,7 @@ impl ZedAiOnboarding { .color(Color::Muted) .mb_2(), ) - .child(plan_definitions.pro_trial(false)) + .child(PlanDefinitions.pro_trial(is_v2, false)) .when_some( self.dismiss_onboarding.as_ref(), |this, dismiss_callback| { @@ -274,9 +273,7 @@ impl ZedAiOnboarding { .into_any_element() } - fn render_pro_plan_state(&self, _cx: &mut App) -> AnyElement { - let plan_definitions = PlanDefinitions; - + fn render_pro_plan_state(&self, is_v2: bool, _cx: &mut App) -> AnyElement { v_flex() .gap_1() .child(Headline::new("Welcome to Zed Pro")) @@ -285,7 +282,7 @@ impl ZedAiOnboarding { .color(Color::Muted) .mb_2(), ) - .child(plan_definitions.pro_plan(false)) + .child(PlanDefinitions.pro_plan(is_v2, false)) .when_some( self.dismiss_onboarding.as_ref(), |this, dismiss_callback| { @@ -315,8 +312,10 @@ impl RenderOnce for ZedAiOnboarding { if matches!(self.sign_in_status, SignInStatus::SignedIn) { match self.plan { None | Some(Plan::ZedFree) => self.render_free_plan_state(cx), - Some(Plan::ZedProTrial) => self.render_trial_state(cx), - Some(Plan::ZedPro) => self.render_pro_plan_state(cx), + Some(Plan::ZedProTrial) => self.render_trial_state(false, cx), + Some(Plan::ZedProTrialV2) => self.render_trial_state(true, cx), + Some(Plan::ZedPro) => self.render_pro_plan_state(false, cx), + Some(Plan::ZedProV2) => self.render_pro_plan_state(true, cx), } } else { self.render_sign_in_disclaimer(cx) diff --git a/crates/ai_onboarding/src/ai_upsell_card.rs b/crates/ai_onboarding/src/ai_upsell_card.rs index d6e7a0bbad321148495b37292c3d17f4321c0a6e..6a797d84379ca5d108c7b69806c7432ead3beeff 100644 --- a/crates/ai_onboarding/src/ai_upsell_card.rs +++ b/crates/ai_onboarding/src/ai_upsell_card.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use client::{Client, UserStore, zed_urls}; use cloud_llm_client::Plan; +use feature_flags::{BillingV2FeatureFlag, FeatureFlagAppExt}; use gpui::{AnyElement, App, Entity, IntoElement, RenderOnce, Window}; use ui::{CommonAnimationExt, Divider, Vector, VectorName, prelude::*}; @@ -49,9 +50,6 @@ impl AiUpsellCard { impl RenderOnce for AiUpsellCard { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let plan_definitions = PlanDefinitions; - let young_account_banner = YoungAccountBanner; - let pro_section = v_flex() .flex_grow() .w_full() @@ -67,7 +65,7 @@ impl RenderOnce for AiUpsellCard { ) .child(Divider::horizontal()), ) - .child(plan_definitions.pro_plan(false)); + .child(PlanDefinitions.pro_plan(cx.has_flag::(), false)); let free_section = v_flex() .flex_grow() @@ -84,7 +82,7 @@ impl RenderOnce for AiUpsellCard { ) .child(Divider::horizontal()), ) - .child(plan_definitions.free_plan()); + .child(PlanDefinitions.free_plan(cx.has_flag::())); let grid_bg = h_flex() .absolute() @@ -173,7 +171,7 @@ impl RenderOnce for AiUpsellCard { .child(Label::new("Try Zed AI").size(LabelSize::Large)) .map(|this| { if self.account_too_young { - this.child(young_account_banner).child( + this.child(YoungAccountBanner).child( v_flex() .mt_2() .gap_1() @@ -188,7 +186,10 @@ impl RenderOnce for AiUpsellCard { ) .child(Divider::horizontal()), ) - .child(plan_definitions.pro_plan(true)) + .child( + PlanDefinitions + .pro_plan(cx.has_flag::(), true), + ) .child( Button::new("pro", "Get Started") .full_width() @@ -235,7 +236,7 @@ impl RenderOnce for AiUpsellCard { ) } }), - Some(Plan::ZedProTrial) => card + Some(plan @ Plan::ZedProTrial | plan @ Plan::ZedProTrialV2) => card .child(pro_trial_stamp) .child(Label::new("You're in the Zed Pro Trial").size(LabelSize::Large)) .child( @@ -243,8 +244,8 @@ impl RenderOnce for AiUpsellCard { .color(Color::Muted) .mb_2(), ) - .child(plan_definitions.pro_trial(false)), - Some(Plan::ZedPro) => card + .child(PlanDefinitions.pro_trial(plan == Plan::ZedProTrialV2, false)), + Some(plan @ Plan::ZedPro | plan @ Plan::ZedProV2) => card .child(certified_user_stamp) .child(Label::new("You're in the Zed Pro plan").size(LabelSize::Large)) .child( @@ -252,7 +253,7 @@ impl RenderOnce for AiUpsellCard { .color(Color::Muted) .mb_2(), ) - .child(plan_definitions.pro_plan(false)), + .child(PlanDefinitions.pro_plan(plan == Plan::ZedProV2, false)), }, // Signed Out State _ => card diff --git a/crates/ai_onboarding/src/plan_definitions.rs b/crates/ai_onboarding/src/plan_definitions.rs index 8d66f6c3563c482b2356e081b5786219f5bf1de3..dce67d421006ce918018923b86dbe22012efef01 100644 --- a/crates/ai_onboarding/src/plan_definitions.rs +++ b/crates/ai_onboarding/src/plan_definitions.rs @@ -7,13 +7,13 @@ pub struct PlanDefinitions; impl PlanDefinitions { pub const AI_DESCRIPTION: &'static str = "Zed offers a complete agentic experience, with robust editing and reviewing features to collaborate with AI."; - pub fn free_plan(&self) -> impl IntoElement { + pub fn free_plan(&self, _is_v2: bool) -> impl IntoElement { List::new() .child(ListBulletItem::new("50 prompts with Claude models")) .child(ListBulletItem::new("2,000 accepted edit predictions")) } - pub fn pro_trial(&self, period: bool) -> impl IntoElement { + pub fn pro_trial(&self, _is_v2: bool, period: bool) -> impl IntoElement { List::new() .child(ListBulletItem::new("150 prompts with Claude models")) .child(ListBulletItem::new( @@ -26,7 +26,7 @@ impl PlanDefinitions { }) } - pub fn pro_plan(&self, price: bool) -> impl IntoElement { + pub fn pro_plan(&self, _is_v2: bool, price: bool) -> impl IntoElement { List::new() .child(ListBulletItem::new("500 prompts with Claude models")) .child(ListBulletItem::new( diff --git a/crates/cloud_llm_client/src/cloud_llm_client.rs b/crates/cloud_llm_client/src/cloud_llm_client.rs index d5a21bfe20938cc981db740a674f4016b9328d96..4bc33079dafd244ae109a45bd12cd5f9cb506b2b 100644 --- a/crates/cloud_llm_client/src/cloud_llm_client.rs +++ b/crates/cloud_llm_client/src/cloud_llm_client.rs @@ -82,8 +82,10 @@ pub enum Plan { ZedFree, #[serde(alias = "ZedPro")] ZedPro, + ZedProV2, #[serde(alias = "ZedProTrial")] ZedProTrial, + ZedProTrialV2, } impl FromStr for Plan { @@ -327,6 +329,12 @@ mod tests { let plan = serde_json::from_value::(json!("zed_pro_trial")).unwrap(); assert_eq!(plan, Plan::ZedProTrial); + + let plan = serde_json::from_value::(json!("zed_pro_v2")).unwrap(); + assert_eq!(plan, Plan::ZedProV2); + + let plan = serde_json::from_value::(json!("zed_pro_trial_v2")).unwrap(); + assert_eq!(plan, Plan::ZedProTrialV2); } #[test] diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index 2f609f51ab79f933d8f25505f710db24e6615ef0..8a50b7ec9bcc5149360ad7499e5e97d5731dfaa7 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -66,6 +66,12 @@ impl FeatureFlag for LlmClosedBetaFeatureFlag { const NAME: &'static str = "llm-closed-beta"; } +pub struct BillingV2FeatureFlag {} + +impl FeatureFlag for BillingV2FeatureFlag { + const NAME: &'static str = "billing-v2"; +} + pub struct NotebookFeatureFlag; impl FeatureFlag for NotebookFeatureFlag { diff --git a/crates/language_model/src/model/cloud_model.rs b/crates/language_model/src/model/cloud_model.rs index 8a7f3456fbb54826809e8a25c2c767d387afcd4e..c6e146e6b30d70588399274c322e9bf8296709c4 100644 --- a/crates/language_model/src/model/cloud_model.rs +++ b/crates/language_model/src/model/cloud_model.rs @@ -36,6 +36,7 @@ impl fmt::Display for ModelRequestLimitReachedError { Plan::ZedProTrial => { "Model request limit reached. Upgrade to Zed Pro for more requests." } + Plan::ZedProV2 | Plan::ZedProTrialV2 => "Model request limit reached.", }; write!(f, "{message}") diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index f031b8394afc551c8077419f504104936095a0c3..009092993753f2510cba7da0769eefcf347a499e 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -660,8 +660,12 @@ impl TitleBar { let (plan_name, label_color, bg_color) = match plan { None | Some(Plan::ZedFree) => ("Free", Color::Default, free_chip_bg), - Some(Plan::ZedProTrial) => ("Pro Trial", Color::Accent, pro_chip_bg), - Some(Plan::ZedPro) => ("Pro", Color::Accent, pro_chip_bg), + Some(Plan::ZedProTrial | Plan::ZedProTrialV2) => { + ("Pro Trial", Color::Accent, pro_chip_bg) + } + Some(Plan::ZedPro | Plan::ZedProV2) => { + ("Pro", Color::Accent, pro_chip_bg) + } }; menu.custom_entry(