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
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:
27
crates/ai_onboarding/Cargo.toml
Normal file
27
crates/ai_onboarding/Cargo.toml
Normal 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
|
||||
1
crates/ai_onboarding/LICENSE-GPL
Symbolic link
1
crates/ai_onboarding/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
149
crates/ai_onboarding/src/agent_api_keys_onboarding.rs
Normal file
149
crates/ai_onboarding/src/agent_api_keys_onboarding.rs
Normal 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);
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
65
crates/ai_onboarding/src/agent_panel_onboarding_card.rs
Normal file
65
crates/ai_onboarding/src/agent_panel_onboarding_card.rs
Normal 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),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
90
crates/ai_onboarding/src/agent_panel_onboarding_content.rs
Normal file
90
crates/ai_onboarding/src/agent_panel_onboarding_content.rs
Normal 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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
442
crates/ai_onboarding/src/ai_onboarding.rs
Normal file
442
crates/ai_onboarding/src/ai_onboarding.rs
Normal 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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
56
crates/ai_onboarding/src/plan_definitions.rs
Normal file
56
crates/ai_onboarding/src/plan_definitions.rs
Normal 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",
|
||||
))
|
||||
}
|
||||
}
|
||||
22
crates/ai_onboarding/src/young_account_banner.rs
Normal file
22
crates/ai_onboarding/src/young_account_banner.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user