diff --git a/Cargo.lock b/Cargo.lock index 898533b5a5..b34c98fa72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9626,6 +9626,7 @@ dependencies = [ "credentials_provider", "deepseek", "editor", + "feature_flags", "fs", "futures 0.3.31", "google_ai", diff --git a/crates/agent_ui/src/ui/end_trial_upsell.rs b/crates/agent_ui/src/ui/end_trial_upsell.rs index 4db9244469..9c25519659 100644 --- a/crates/agent_ui/src/ui/end_trial_upsell.rs +++ b/crates/agent_ui/src/ui/end_trial_upsell.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use ai_onboarding::{AgentPanelOnboardingCard, PlanDefinitions}; use client::zed_urls; -use cloud_llm_client::{Plan, PlanV1}; +use cloud_llm_client::{Plan, PlanV2}; use gpui::{AnyElement, App, IntoElement, RenderOnce, Window}; use ui::{Divider, Tooltip, prelude::*}; @@ -112,7 +112,7 @@ impl Component for EndTrialUpsell { Some( v_flex() .child(EndTrialUpsell { - plan: Plan::V1(PlanV1::ZedFree), + plan: Plan::V2(PlanV2::ZedFree), dismiss_upsell: Arc::new(|_, _| {}), }) .into_any_element(), diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs index e131a60b12..35e5832d3e 100644 --- a/crates/ai_onboarding/src/ai_onboarding.rs +++ b/crates/ai_onboarding/src/ai_onboarding.rs @@ -120,7 +120,7 @@ impl ZedAiOnboarding { .max_w_full() .gap_1() .child(Headline::new("Welcome to Zed AI")) - .child(YoungAccountBanner) + .child(YoungAccountBanner::new(is_v2)) .child( v_flex() .mt_2() @@ -372,7 +372,7 @@ impl Component for ZedAiOnboarding { "Free Plan", onboarding( SignInStatus::SignedIn, - Some(Plan::V1(PlanV1::ZedFree)), + Some(Plan::V2(PlanV2::ZedFree)), false, ), ), @@ -380,7 +380,7 @@ impl Component for ZedAiOnboarding { "Pro Trial", onboarding( SignInStatus::SignedIn, - Some(Plan::V1(PlanV1::ZedProTrial)), + Some(Plan::V2(PlanV2::ZedProTrial)), false, ), ), @@ -388,7 +388,7 @@ impl Component for ZedAiOnboarding { "Pro Plan", onboarding( SignInStatus::SignedIn, - Some(Plan::V1(PlanV1::ZedPro)), + Some(Plan::V2(PlanV2::ZedPro)), false, ), ), diff --git a/crates/ai_onboarding/src/ai_upsell_card.rs b/crates/ai_onboarding/src/ai_upsell_card.rs index 51758dd9ac..f6a4759666 100644 --- a/crates/ai_onboarding/src/ai_upsell_card.rs +++ b/crates/ai_onboarding/src/ai_upsell_card.rs @@ -175,7 +175,7 @@ impl RenderOnce for AiUpsellCard { .child(Label::new("Try Zed AI").size(LabelSize::Large)) .map(|this| { if self.account_too_young { - this.child(YoungAccountBanner).child( + this.child(YoungAccountBanner::new(is_v2_plan)).child( v_flex() .mt_2() .gap_1() @@ -215,7 +215,7 @@ impl RenderOnce for AiUpsellCard { .child( footer_container .child( - Button::new("start_trial", "Start 14-day Free Pro Trial") + Button::new("start_trial", "Start Pro Trial") .full_width() .style(ButtonStyle::Tinted(ui::TintColor::Accent)) .when_some(self.tab_index, |this, tab_index| { @@ -230,7 +230,7 @@ impl RenderOnce for AiUpsellCard { }), ) .child( - Label::new("No credit card required") + Label::new("14 days, no credit card required") .size(LabelSize::Small) .color(Color::Muted), ), @@ -327,7 +327,7 @@ impl Component for AiUpsellCard { sign_in_status: SignInStatus::SignedIn, sign_in: Arc::new(|_, _| {}), account_too_young: false, - user_plan: Some(Plan::V1(PlanV1::ZedFree)), + user_plan: Some(Plan::V2(PlanV2::ZedFree)), tab_index: Some(1), } .into_any_element(), @@ -338,7 +338,7 @@ impl Component for AiUpsellCard { sign_in_status: SignInStatus::SignedIn, sign_in: Arc::new(|_, _| {}), account_too_young: true, - user_plan: Some(Plan::V1(PlanV1::ZedFree)), + user_plan: Some(Plan::V2(PlanV2::ZedFree)), tab_index: Some(1), } .into_any_element(), @@ -349,7 +349,7 @@ impl Component for AiUpsellCard { sign_in_status: SignInStatus::SignedIn, sign_in: Arc::new(|_, _| {}), account_too_young: false, - user_plan: Some(Plan::V1(PlanV1::ZedProTrial)), + user_plan: Some(Plan::V2(PlanV2::ZedProTrial)), tab_index: Some(1), } .into_any_element(), @@ -360,7 +360,7 @@ impl Component for AiUpsellCard { sign_in_status: SignInStatus::SignedIn, sign_in: Arc::new(|_, _| {}), account_too_young: false, - user_plan: Some(Plan::V1(PlanV1::ZedPro)), + user_plan: Some(Plan::V2(PlanV2::ZedPro)), tab_index: Some(1), } .into_any_element(), diff --git a/crates/ai_onboarding/src/plan_definitions.rs b/crates/ai_onboarding/src/plan_definitions.rs index dce67d4210..11f5631171 100644 --- a/crates/ai_onboarding/src/plan_definitions.rs +++ b/crates/ai_onboarding/src/plan_definitions.rs @@ -7,33 +7,62 @@ 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, _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, _is_v2: bool, period: bool) -> impl IntoElement { - List::new() - .child(ListBulletItem::new("150 prompts with Claude models")) - .child(ListBulletItem::new( - "Unlimited edit predictions with Zeta, our open-source model", - )) - .when(period, |this| { - this.child(ListBulletItem::new( - "Try it out for 14 days for free, no credit card required", + pub fn free_plan(&self, is_v2: bool) -> impl IntoElement { + if is_v2 { + List::new() + .child(ListBulletItem::new("2,000 accepted edit predictions")) + .child(ListBulletItem::new( + "Unlimited prompts with your AI API keys", )) - }) + .child(ListBulletItem::new( + "Unlimited use of external agents like Claude Code", + )) + } else { + List::new() + .child(ListBulletItem::new("50 prompts with Claude models")) + .child(ListBulletItem::new("2,000 accepted edit predictions")) + } } - 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( - "Unlimited edit predictions with Zeta, our open-source model", - )) - .when(price, |this| { - this.child(ListBulletItem::new("$20 USD per month")) - }) + pub fn pro_trial(&self, is_v2: bool, period: bool) -> impl IntoElement { + if is_v2 { + List::new() + .child(ListBulletItem::new("Unlimited edit predictions")) + .child(ListBulletItem::new("$20 of tokens")) + .when(period, |this| { + this.child(ListBulletItem::new( + "Try it out for 14 days, no credit card required", + )) + }) + } else { + List::new() + .child(ListBulletItem::new("150 prompts with Claude models")) + .child(ListBulletItem::new( + "Unlimited edit predictions with Zeta, our open-source model", + )) + .when(period, |this| { + this.child(ListBulletItem::new( + "Try it out for 14 days, no credit card required", + )) + }) + } + } + + pub fn pro_plan(&self, is_v2: bool, price: bool) -> impl IntoElement { + if is_v2 { + List::new() + .child(ListBulletItem::new("Unlimited edit predictions")) + .child(ListBulletItem::new("$5 of tokens")) + .child(ListBulletItem::new("Usage-based billing beyond $5")) + } else { + List::new() + .child(ListBulletItem::new("500 prompts with Claude models")) + .child(ListBulletItem::new( + "Unlimited edit predictions with Zeta, our open-source model", + )) + .when(price, |this| { + this.child(ListBulletItem::new("$20 USD per month")) + }) + } } } diff --git a/crates/ai_onboarding/src/young_account_banner.rs b/crates/ai_onboarding/src/young_account_banner.rs index ae13b95568..fde9a31c49 100644 --- a/crates/ai_onboarding/src/young_account_banner.rs +++ b/crates/ai_onboarding/src/young_account_banner.rs @@ -2,17 +2,30 @@ use gpui::{IntoElement, ParentElement}; use ui::{Banner, prelude::*}; #[derive(IntoElement)] -pub struct YoungAccountBanner; +pub struct YoungAccountBanner { + is_v2: bool, +} + +impl YoungAccountBanner { + pub fn new(is_v2: bool) -> Self { + Self { is_v2 } + } +} impl RenderOnce for YoungAccountBanner { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - const YOUNG_ACCOUNT_DISCLAIMER: &str = "To prevent abuse of our service, GitHub accounts created fewer than 30 days ago are not eligible for free plan usage or Pro plan free trial. To request an exception, reach out to billing-support@zed.dev."; + const YOUNG_ACCOUNT_DISCLAIMER: &str = "To prevent abuse of our service, GitHub accounts created fewer than 30 days ago are not eligible for free plan usage or Pro plan free trial. You can request an exception by reaching out to billing-support@zed.dev"; + const YOUNG_ACCOUNT_DISCLAIMER_V2: &str = "To prevent abuse of our service, GitHub accounts created fewer than 30 days ago are not eligible for the Pro trial. You can request an exception by reaching out to billing-support@zed.dev"; let label = div() .w_full() .text_sm() .text_color(cx.theme().colors().text_muted) - .child(YOUNG_ACCOUNT_DISCLAIMER); + .child(if self.is_v2 { + YOUNG_ACCOUNT_DISCLAIMER_V2 + } else { + YOUNG_ACCOUNT_DISCLAIMER + }); div() .max_w_full() diff --git a/crates/feature_flags/src/flags.rs b/crates/feature_flags/src/flags.rs index eaae9ce425..d8c8bec873 100644 --- a/crates/feature_flags/src/flags.rs +++ b/crates/feature_flags/src/flags.rs @@ -10,6 +10,10 @@ pub struct BillingV2FeatureFlag {} impl FeatureFlag for BillingV2FeatureFlag { const NAME: &'static str = "billing-v2"; + + fn enabled_for_all() -> bool { + true + } } pub struct NotebookFeatureFlag; diff --git a/crates/language_models/Cargo.toml b/crates/language_models/Cargo.toml index 8a2a681c26..d4ae4b26b6 100644 --- a/crates/language_models/Cargo.toml +++ b/crates/language_models/Cargo.toml @@ -29,6 +29,7 @@ copilot.workspace = true credentials_provider.workspace = true deepseek = { workspace = true, features = ["schemars"] } editor.workspace = true +feature_flags.workspace = true fs.workspace = true futures.workspace = true google_ai = { workspace = true, features = ["schemars"] } diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index 40958af775..65fcae0f6a 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -11,6 +11,7 @@ use cloud_llm_client::{ SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME, TOOL_USE_LIMIT_REACHED_HEADER_NAME, ZED_VERSION_HEADER_NAME, }; +use feature_flags::{BillingV2FeatureFlag, FeatureFlagAppExt}; use futures::{ AsyncBufReadExt, FutureExt, Stream, StreamExt, future::BoxFuture, stream::BoxStream, }; @@ -973,12 +974,13 @@ struct ZedAiConfiguration { } impl RenderOnce for ZedAiConfiguration { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let young_account_banner = YoungAccountBanner; - + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let is_pro = self.plan.is_some_and(|plan| { matches!(plan, Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro)) }); + let is_free_v2 = self + .plan + .is_some_and(|plan| plan == Plan::V2(PlanV2::ZedFree)); let subscription_text = match (self.plan, self.subscription_period) { (Some(Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro)), Some(_)) => { "You have access to Zed's hosted models through your Pro subscription." @@ -986,9 +988,16 @@ impl RenderOnce for ZedAiConfiguration { (Some(Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial)), Some(_)) => { "You have access to Zed's hosted models through your Pro trial." } - (Some(Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree)), Some(_)) => { + (Some(Plan::V1(PlanV1::ZedFree)), Some(_)) => { "You have basic access to Zed's hosted models through the Free plan." } + (Some(Plan::V2(PlanV2::ZedFree)), Some(_)) => { + if self.eligible_for_trial { + "Subscribe for access to Zed's hosted models. Start with a 14 day free trial." + } else { + "Subscribe for access to Zed's hosted models." + } + } _ => { if self.eligible_for_trial { "Subscribe for access to Zed's hosted models. Start with a 14 day free trial." @@ -1038,7 +1047,10 @@ impl RenderOnce for ZedAiConfiguration { v_flex().gap_2().w_full().map(|this| { if self.account_too_young { - this.child(young_account_banner).child( + this.child(YoungAccountBanner::new( + is_free_v2 || cx.has_flag::(), + )) + .child( Button::new("upgrade", "Upgrade to Pro") .style(ui::ButtonStyle::Tinted(ui::TintColor::Accent)) .full_width()