logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled

Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree
with a 3-file patch applied (no upstream history).

Patch (crates/gpui_linux/src/linux/wayland/):
  - serial.rs: add SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; add latest_serial_of()
  - window.rs: activate() uses keyboard-enter serial (Mutter focus gate)

Mutter honors window activation only when the token carries the keyboard-
focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial.
See docs/tray-window-focus-wayland.md in logiguard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 02:22:17 +03:30
commit b72a46db68
3984 changed files with 1583326 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
[package]
name = "ai_onboarding"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/ai_onboarding.rs"
[features]
default = []
[dependencies]
client.workspace = true
cloud_api_types.workspace = true
component.workspace = true
gpui.workspace = true
language_model.workspace = true
serde.workspace = true
smallvec.workspace = true
telemetry.workspace = true
ui.workspace = true
zed_actions.workspace = true

View File

@@ -0,0 +1 @@
../../LICENSE-GPL

View File

@@ -0,0 +1,149 @@
use gpui::{Action, IntoElement, ParentElement, RenderOnce, point};
use language_model::{IconOrSvg, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
use ui::{Divider, List, ListBulletItem, prelude::*};
pub struct ApiKeysWithProviders {
configured_providers: Vec<(IconOrSvg, SharedString)>,
}
impl ApiKeysWithProviders {
pub fn new(cx: &mut Context<Self>) -> Self {
cx.subscribe(
&LanguageModelRegistry::global(cx),
|this: &mut Self, _registry, event: &language_model::Event, cx| match event {
language_model::Event::ProviderStateChanged(_)
| language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_)
| language_model::Event::ProvidersChanged => {
this.configured_providers = Self::compute_configured_providers(cx)
}
_ => {}
},
)
.detach();
Self {
configured_providers: Self::compute_configured_providers(cx),
}
}
fn compute_configured_providers(cx: &App) -> Vec<(IconOrSvg, SharedString)> {
LanguageModelRegistry::read_global(cx)
.visible_providers()
.iter()
.filter(|provider| {
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
})
.map(|provider| (provider.icon(), provider.name().0))
.collect()
}
}
impl Render for ApiKeysWithProviders {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let configured_providers_list =
self.configured_providers
.iter()
.cloned()
.map(|(icon, name)| {
h_flex()
.gap_1p5()
.child(
match icon {
IconOrSvg::Icon(icon_name) => Icon::new(icon_name),
IconOrSvg::Svg(icon_path) => Icon::from_external_svg(icon_path),
}
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(Label::new(name))
});
div()
.mx_2p5()
.p_1()
.pb_0()
.gap_2()
.rounded_t_lg()
.border_t_1()
.border_x_1()
.border_color(cx.theme().colors().border.opacity(0.5))
.bg(cx.theme().colors().background.alpha(0.5))
.shadow(vec![gpui::BoxShadow {
color: gpui::black().opacity(0.15),
offset: point(px(1.), px(-1.)),
blur_radius: px(3.),
spread_radius: px(0.),
}])
.child(
h_flex()
.px_2p5()
.py_1p5()
.gap_2()
.flex_wrap()
.rounded_t(px(5.))
.overflow_hidden()
.border_t_1()
.border_x_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().panel_background)
.child(
h_flex()
.min_w_0()
.gap_2()
.child(
Icon::new(IconName::Info)
.size(IconSize::XSmall)
.color(Color::Muted)
)
.child(
div()
.w_full()
.child(
Label::new("Start now using API keys from your environment for the following providers:")
.color(Color::Muted)
)
)
)
.children(configured_providers_list)
)
}
}
#[derive(IntoElement)]
pub struct ApiKeysWithoutProviders;
impl ApiKeysWithoutProviders {
pub fn new() -> Self {
Self
}
}
impl RenderOnce for ApiKeysWithoutProviders {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
v_flex()
.mt_2()
.gap_1()
.child(
h_flex()
.gap_2()
.child(
Label::new("API Keys")
.size(LabelSize::Small)
.color(Color::Muted)
.buffer_font(cx),
)
.child(Divider::horizontal()),
)
.child(List::new().child(ListBulletItem::new(
"Add your own keys to use AI without signing in.",
)))
.child(
Button::new("configure-providers", "Configure Providers")
.full_width()
.style(ButtonStyle::Outlined)
.on_click(move |_, window, cx| {
window.dispatch_action(zed_actions::agent::OpenSettings.boxed_clone(), cx);
}),
)
}
}

View File

@@ -0,0 +1,65 @@
use gpui::{AnyElement, IntoElement, ParentElement, linear_color_stop, linear_gradient};
use smallvec::SmallVec;
use ui::prelude::*;
#[derive(IntoElement)]
pub struct AgentPanelOnboardingCard {
children: SmallVec<[AnyElement; 2]>,
}
impl AgentPanelOnboardingCard {
pub fn new() -> Self {
Self {
children: SmallVec::new(),
}
}
}
impl ParentElement for AgentPanelOnboardingCard {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}
impl RenderOnce for AgentPanelOnboardingCard {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let color = cx.theme().colors();
div().min_w_0().p_2p5().bg(color.editor_background).child(
div()
.min_w_0()
.p(px(3.))
.rounded_lg()
.elevation_2(cx)
.bg(color.background.opacity(0.5))
.child(
v_flex()
.relative()
.size_full()
.min_w_0()
.px_4()
.py_3()
.gap_2()
.border_1()
.rounded(px(5.))
.border_color(color.text.opacity(0.1))
.bg(color.panel_background)
.overflow_hidden()
.child(
div()
.absolute()
.inset_0()
.size_full()
.rounded_md()
.overflow_hidden()
.bg(linear_gradient(
360.,
linear_color_stop(color.panel_background, 1.0),
linear_color_stop(color.editor_background, 0.45),
)),
)
.children(self.children),
),
)
}
}

View File

@@ -0,0 +1,90 @@
use std::sync::Arc;
use client::{Client, UserStore};
use cloud_api_types::Plan;
use gpui::{Entity, IntoElement, ParentElement};
use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
use ui::prelude::*;
use crate::{AgentPanelOnboardingCard, ApiKeysWithoutProviders, ZedAiOnboarding};
pub struct AgentPanelOnboarding {
user_store: Entity<UserStore>,
client: Arc<Client>,
has_configured_providers: bool,
continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
}
impl AgentPanelOnboarding {
pub fn new(
user_store: Entity<UserStore>,
client: Arc<Client>,
continue_with_zed_ai: impl Fn(&mut Window, &mut App) + 'static,
cx: &mut Context<Self>,
) -> Self {
cx.subscribe(
&LanguageModelRegistry::global(cx),
|this: &mut Self, _registry, event: &language_model::Event, cx| match event {
language_model::Event::ProviderStateChanged(_)
| language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_)
| language_model::Event::ProvidersChanged => {
this.has_configured_providers = Self::has_configured_providers(cx)
}
_ => {}
},
)
.detach();
Self {
user_store,
client,
has_configured_providers: Self::has_configured_providers(cx),
continue_with_zed_ai: Arc::new(continue_with_zed_ai),
}
}
fn has_configured_providers(cx: &App) -> bool {
LanguageModelRegistry::read_global(cx)
.visible_providers()
.iter()
.any(|provider| provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID)
}
}
impl Render for AgentPanelOnboarding {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let enrolled_in_trial = self
.user_store
.read(cx)
.plan()
.is_some_and(|plan| plan == Plan::ZedProTrial);
let is_pro_user = self
.user_store
.read(cx)
.plan()
.is_some_and(|plan| plan == Plan::ZedPro);
let onboarding = ZedAiOnboarding::new(
self.client.clone(),
&self.user_store,
self.continue_with_zed_ai.clone(),
cx,
)
.with_dismiss({
let callback = self.continue_with_zed_ai.clone();
move |window, cx| callback(window, cx)
});
AgentPanelOnboardingCard::new()
.child(onboarding)
.map(|this| {
if enrolled_in_trial || is_pro_user || self.has_configured_providers {
this
} else {
this.child(ApiKeysWithoutProviders::new())
}
})
}
}

View File

@@ -0,0 +1,442 @@
mod agent_api_keys_onboarding;
mod agent_panel_onboarding_card;
mod agent_panel_onboarding_content;
mod edit_prediction_onboarding_content;
mod plan_definitions;
mod young_account_banner;
pub use agent_api_keys_onboarding::{ApiKeysWithProviders, ApiKeysWithoutProviders};
pub use agent_panel_onboarding_card::AgentPanelOnboardingCard;
pub use agent_panel_onboarding_content::AgentPanelOnboarding;
use cloud_api_types::Plan;
pub use edit_prediction_onboarding_content::EditPredictionOnboarding;
pub use plan_definitions::PlanDefinitions;
pub use young_account_banner::YoungAccountBanner;
use std::sync::Arc;
use client::{Client, UserStore, zed_urls};
use gpui::{AnyElement, Entity, IntoElement, ParentElement, TaskExt};
use ui::{Divider, RegisterComponent, Tooltip, Vector, VectorName, prelude::*};
#[derive(PartialEq)]
pub enum SignInStatus {
SignedIn,
SigningIn,
SignedOut,
}
impl From<client::Status> for SignInStatus {
fn from(status: client::Status) -> Self {
if status.is_signing_in() {
Self::SigningIn
} else if status.is_signed_out() {
Self::SignedOut
} else {
Self::SignedIn
}
}
}
#[derive(RegisterComponent, IntoElement)]
pub struct ZedAiOnboarding {
pub sign_in_status: SignInStatus,
pub plan: Option<Plan>,
pub account_too_young: bool,
pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
pub dismiss_onboarding: Option<Arc<dyn Fn(&mut Window, &mut App)>>,
}
impl ZedAiOnboarding {
pub fn new(
client: Arc<Client>,
user_store: &Entity<UserStore>,
continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
cx: &mut App,
) -> Self {
let store = user_store.read(cx);
let status = *client.status().borrow();
Self {
sign_in_status: status.into(),
plan: store.plan(),
account_too_young: store.account_too_young(),
continue_with_zed_ai,
sign_in: Arc::new(move |_window, cx| {
cx.spawn({
let client = client.clone();
async move |cx| client.sign_in_with_optional_connect(true, cx).await
})
.detach_and_log_err(cx);
}),
dismiss_onboarding: None,
}
}
pub fn with_dismiss(
mut self,
dismiss_callback: impl Fn(&mut Window, &mut App) + 'static,
) -> Self {
self.dismiss_onboarding = Some(Arc::new(dismiss_callback));
self
}
fn certified_user_stamp(cx: &App) -> impl IntoElement {
div().absolute().bottom_1().right_1().child(
Vector::new(
VectorName::ProUserStamp,
rems_from_px(156.),
rems_from_px(60.),
)
.color(Color::Custom(cx.theme().colors().text_accent.alpha(0.8))),
)
}
fn pro_trial_stamp(cx: &App) -> impl IntoElement {
div().absolute().bottom_1().right_1().child(
Vector::new(
VectorName::ProTrialStamp,
rems_from_px(156.),
rems_from_px(60.),
)
.color(Color::Custom(cx.theme().colors().text.alpha(0.8))),
)
}
fn business_stamp(cx: &App) -> impl IntoElement {
div().absolute().bottom_1().right_1().child(
Vector::new(
VectorName::BusinessStamp,
rems_from_px(156.),
rems_from_px(60.),
)
.color(Color::Custom(cx.theme().colors().text_accent.alpha(0.8))),
)
}
fn student_stamp(cx: &App) -> impl IntoElement {
div().absolute().bottom_1().right_1().child(
Vector::new(
VectorName::StudentStamp,
rems_from_px(156.),
rems_from_px(60.),
)
.color(Color::Custom(cx.theme().colors().text.alpha(0.8))),
)
}
fn render_dismiss_button(&self) -> Option<AnyElement> {
self.dismiss_onboarding.as_ref().map(|dismiss_callback| {
let callback = dismiss_callback.clone();
h_flex()
.absolute()
.top_0()
.right_0()
.child(
IconButton::new("dismiss_onboarding", IconName::Close)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Dismiss"))
.on_click(move |_, window, cx| {
telemetry::event!("Banner Dismissed", source = "AI Onboarding",);
callback(window, cx)
}),
)
.into_any_element()
})
}
fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
v_flex()
.w_full()
.relative()
.gap_1()
.child(Headline::new("Welcome to Zed AI"))
.child(
Label::new("Sign in to try Zed Pro free for 14 days.")
.color(Color::Muted)
.mb_2(),
)
.child(PlanDefinitions.sign_in_upsell())
.child(
Button::new("sign_in", "Try Zed Pro for Free")
.disabled(signing_in)
.full_width()
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click({
let callback = self.sign_in.clone();
move |_, window, cx| {
telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
callback(window, cx)
}
}),
)
.children(self.render_dismiss_button())
.into_any_element()
}
fn render_free_plan_state(&self, cx: &mut App) -> AnyElement {
if self.account_too_young {
v_flex()
.relative()
.min_w_0()
.gap_1()
.child(Headline::new("Welcome to Zed AI"))
.child(YoungAccountBanner)
.child(
v_flex()
.mt_2()
.gap_1()
.child(
h_flex()
.gap_2()
.child(
Label::new("Pro")
.size(LabelSize::Small)
.color(Color::Accent)
.buffer_font(cx),
)
.child(Divider::horizontal()),
)
.child(PlanDefinitions.pro_plan())
.child(
Button::new("pro", "Get Started")
.full_width()
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click(move |_, _window, cx| {
telemetry::event!(
"Upgrade To Pro Clicked",
state = "young-account"
);
cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
}),
),
)
.into_any_element()
} else {
v_flex()
.w_full()
.relative()
.gap_1()
.child(Headline::new("Welcome to Zed AI"))
.child(
v_flex()
.mt_2()
.gap_1()
.child(
h_flex()
.gap_2()
.child(
Label::new("Free")
.size(LabelSize::Small)
.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(PlanDefinitions.free_plan()),
)
.children(self.render_dismiss_button())
.child(
v_flex()
.mt_2()
.gap_1()
.child(
h_flex()
.gap_2()
.child(
Label::new("Pro Trial")
.size(LabelSize::Small)
.color(Color::Accent)
.buffer_font(cx),
)
.child(Divider::horizontal()),
)
.child(PlanDefinitions.pro_trial(true))
.child(
Button::new("pro", "Start Free Trial")
.full_width()
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click(move |_, _window, cx| {
telemetry::event!(
"Start Trial Clicked",
state = "post-sign-in"
);
cx.open_url(&zed_urls::start_trial_url(cx))
}),
),
)
.into_any_element()
}
}
fn render_trial_state(&self, cx: &mut App) -> AnyElement {
v_flex()
.w_full()
.relative()
.gap_1()
.child(Self::pro_trial_stamp(cx))
.child(Headline::new("Welcome to the Zed Pro Trial"))
.child(
Label::new("Here's what you get for the next 14 days:")
.color(Color::Muted)
.mb_2(),
)
.child(PlanDefinitions.pro_trial(false))
.children(self.render_dismiss_button())
.into_any_element()
}
fn render_pro_plan_state(&self, cx: &mut App) -> AnyElement {
v_flex()
.w_full()
.relative()
.gap_1()
.child(Self::certified_user_stamp(cx))
.child(Headline::new("Welcome to Zed Pro"))
.child(
Label::new("Here's what you get:")
.color(Color::Muted)
.mb_2(),
)
.child(PlanDefinitions.pro_plan())
.children(self.render_dismiss_button())
.into_any_element()
}
fn render_business_plan_state(&self, cx: &mut App) -> AnyElement {
v_flex()
.w_full()
.relative()
.gap_1()
.child(Self::business_stamp(cx))
.child(Headline::new("Welcome to Zed Business"))
.child(
Label::new("Here's what you get:")
.color(Color::Muted)
.mb_2(),
)
.child(PlanDefinitions.business_plan())
.children(self.render_dismiss_button())
.into_any_element()
}
fn render_student_plan_state(&self, cx: &mut App) -> AnyElement {
v_flex()
.w_full()
.relative()
.gap_1()
.child(Self::student_stamp(cx))
.child(Headline::new("Welcome to Zed Student"))
.child(
Label::new("Here's what you get:")
.color(Color::Muted)
.mb_2(),
)
.child(PlanDefinitions.student_plan())
.children(self.render_dismiss_button())
.into_any_element()
}
}
impl RenderOnce for ZedAiOnboarding {
fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
if matches!(self.sign_in_status, SignInStatus::SignedIn) {
match self.plan {
None => self.render_free_plan_state(cx),
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::ZedBusiness) => self.render_business_plan_state(cx),
Some(Plan::ZedStudent) => self.render_student_plan_state(cx),
}
} else {
self.render_sign_in_disclaimer(cx)
}
}
}
impl Component for ZedAiOnboarding {
fn scope() -> ComponentScope {
ComponentScope::Onboarding
}
fn name() -> &'static str {
"Agent New User Onboarding"
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn onboarding(
sign_in_status: SignInStatus,
plan: Option<Plan>,
account_too_young: bool,
) -> AnyElement {
div()
.w_full()
.min_w_40()
.max_w(px(1100.))
.child(
AgentPanelOnboardingCard::new().child(
ZedAiOnboarding {
sign_in_status,
plan,
account_too_young,
continue_with_zed_ai: Arc::new(|_, _| {}),
sign_in: Arc::new(|_, _| {}),
dismiss_onboarding: None,
}
.into_any_element(),
),
)
.into_any_element()
}
Some(
v_flex()
.min_w_0()
.gap_4()
.children(vec![
single_example(
"Not Signed-in",
onboarding(SignInStatus::SignedOut, None, false),
),
single_example(
"Young Account",
onboarding(SignInStatus::SignedIn, None, true),
),
single_example(
"Free Plan",
onboarding(SignInStatus::SignedIn, Some(Plan::ZedFree), false),
),
single_example(
"Pro Trial",
onboarding(SignInStatus::SignedIn, Some(Plan::ZedProTrial), false),
),
single_example(
"Pro Plan",
onboarding(SignInStatus::SignedIn, Some(Plan::ZedPro), false),
),
single_example(
"Business Plan",
onboarding(SignInStatus::SignedIn, Some(Plan::ZedBusiness), false),
),
single_example(
"Student Plan",
onboarding(SignInStatus::SignedIn, Some(Plan::ZedStudent), false),
),
])
.into_any_element(),
)
}
}

View File

@@ -0,0 +1,81 @@
use std::sync::Arc;
use client::{Client, UserStore};
use cloud_api_types::Plan;
use gpui::{Entity, IntoElement, ParentElement};
use ui::prelude::*;
use crate::ZedAiOnboarding;
pub struct EditPredictionOnboarding {
user_store: Entity<UserStore>,
client: Arc<Client>,
copilot_is_configured: bool,
continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
continue_with_copilot: Arc<dyn Fn(&mut Window, &mut App)>,
}
impl EditPredictionOnboarding {
pub fn new(
user_store: Entity<UserStore>,
client: Arc<Client>,
copilot_is_configured: bool,
continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
continue_with_copilot: Arc<dyn Fn(&mut Window, &mut App)>,
_cx: &mut Context<Self>,
) -> Self {
Self {
user_store,
copilot_is_configured,
client,
continue_with_zed_ai,
continue_with_copilot,
}
}
}
impl Render for EditPredictionOnboarding {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_free_plan = self
.user_store
.read(cx)
.plan()
.is_some_and(|plan| plan == Plan::ZedFree);
let github_copilot = v_flex()
.gap_1()
.child(Label::new(if self.copilot_is_configured {
"Alternatively, you can continue to use GitHub Copilot as that's already set up."
} else {
"Alternatively, you can use GitHub Copilot as your edit prediction provider."
}))
.child(
Button::new(
"configure-copilot",
if self.copilot_is_configured {
"Use Copilot"
} else {
"Configure Copilot"
},
)
.full_width()
.style(ButtonStyle::Outlined)
.on_click({
let callback = self.continue_with_copilot.clone();
move |_, window, cx| callback(window, cx)
}),
);
v_flex()
.gap_2()
.child(ZedAiOnboarding::new(
self.client.clone(),
&self.user_store,
self.continue_with_zed_ai.clone(),
cx,
))
.when(is_free_plan, |this| {
this.child(ui::Divider::horizontal()).child(github_copilot)
})
}
}

View File

@@ -0,0 +1,56 @@
use gpui::{IntoElement, ParentElement};
use ui::{List, ListBulletItem, prelude::*};
/// Centralized definitions for Zed AI plans
pub struct PlanDefinitions;
impl PlanDefinitions {
pub fn free_plan(&self) -> impl IntoElement {
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"))
}
pub fn sign_in_upsell(&self) -> impl IntoElement {
List::new()
.child(ListBulletItem::new("Unlimited edit predictions"))
.child(ListBulletItem::new("$20 of tokens in Zed agent"))
.child(ListBulletItem::new("No credit card required"))
}
pub fn pro_trial(&self, period: bool) -> impl IntoElement {
List::new()
.child(ListBulletItem::new("$20 of tokens in Zed agent"))
.child(ListBulletItem::new("Unlimited edit predictions"))
.when(period, |this| {
this.child(ListBulletItem::new(
"Try it out for 14 days, no credit card required",
))
})
}
pub fn pro_plan(&self) -> impl IntoElement {
List::new()
.child(ListBulletItem::new("$5 of tokens in Zed agent"))
.child(ListBulletItem::new("Usage-based billing beyond $5"))
.child(ListBulletItem::new("Unlimited edit predictions"))
}
pub fn business_plan(&self) -> impl IntoElement {
List::new()
.child(ListBulletItem::new("Unlimited edit predictions"))
.child(ListBulletItem::new("Usage-based billing"))
}
pub fn student_plan(&self) -> impl IntoElement {
List::new()
.child(ListBulletItem::new("Unlimited edit predictions"))
.child(ListBulletItem::new("$10 of tokens in Zed agent"))
.child(ListBulletItem::new(
"Optional credit packs for additional usage",
))
}
}

View File

@@ -0,0 +1,22 @@
use gpui::{IntoElement, ParentElement};
use ui::{Banner, prelude::*};
#[derive(IntoElement)]
pub struct YoungAccountBanner;
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 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);
div()
.max_w_full()
.my_1()
.child(Banner::new().severity(Severity::Warning).child(label))
}
}