cloud_llm_client: Add new `Plan` variants (#37810)

Marshall Bowers created

This PR adds new variants to the `Plan` enum.

Release Notes:

- N/A

Change summary

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 +-
crates/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 +-
crates/cloud_llm_client/src/cloud_llm_client.rs  |  8 +++
crates/feature_flags/src/feature_flags.rs        |  6 ++
crates/language_model/src/model/cloud_model.rs   |  1 
crates/title_bar/src/title_bar.rs                |  8 ++
14 files changed, 68 insertions(+), 44 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -482,6 +482,7 @@ dependencies = [
  "client",
  "cloud_llm_client",
  "component",
+ "feature_flags",
  "gpui",
  "language_model",
  "serde",

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()

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())

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()

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::<BillingV2FeatureFlag>(), 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::<BillingV2FeatureFlag>()));
 
         AgentPanelOnboardingCard::new()
             .child(Headline::new("Your Zed Pro Trial has expired"))

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",

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

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::<BillingV2FeatureFlag>(), 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::<BillingV2FeatureFlag>(), 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::<BillingV2FeatureFlag>())),
                 )
                 .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::<BillingV2FeatureFlag>(), 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)

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::<BillingV2FeatureFlag>(), 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::<BillingV2FeatureFlag>()));
 
         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::<BillingV2FeatureFlag>(), 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

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(

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::<Plan>(json!("zed_pro_trial")).unwrap();
         assert_eq!(plan, Plan::ZedProTrial);
+
+        let plan = serde_json::from_value::<Plan>(json!("zed_pro_v2")).unwrap();
+        assert_eq!(plan, Plan::ZedProV2);
+
+        let plan = serde_json::from_value::<Plan>(json!("zed_pro_trial_v2")).unwrap();
+        assert_eq!(plan, Plan::ZedProTrialV2);
     }
 
     #[test]

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 {

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}")

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(