1mod agent_api_keys_onboarding;
2mod agent_panel_onboarding_card;
3mod agent_panel_onboarding_content;
4mod edit_prediction_onboarding_content;
5mod plan_definitions;
6mod young_account_banner;
7
8pub use agent_api_keys_onboarding::{ApiKeysWithProviders, ApiKeysWithoutProviders};
9pub use agent_panel_onboarding_card::AgentPanelOnboardingCard;
10pub use agent_panel_onboarding_content::AgentPanelOnboarding;
11use cloud_api_types::Plan;
12pub use edit_prediction_onboarding_content::EditPredictionOnboarding;
13pub use plan_definitions::PlanDefinitions;
14pub use young_account_banner::YoungAccountBanner;
15
16use std::sync::Arc;
17
18use client::{Client, UserStore, zed_urls};
19use gpui::{AnyElement, Entity, IntoElement, ParentElement};
20use ui::{
21 Divider, List, ListBulletItem, RegisterComponent, Tooltip, Vector, VectorName, prelude::*,
22};
23
24#[derive(PartialEq)]
25pub enum SignInStatus {
26 SignedIn,
27 SigningIn,
28 SignedOut,
29}
30
31impl From<client::Status> for SignInStatus {
32 fn from(status: client::Status) -> Self {
33 if status.is_signing_in() {
34 Self::SigningIn
35 } else if status.is_signed_out() {
36 Self::SignedOut
37 } else {
38 Self::SignedIn
39 }
40 }
41}
42
43#[derive(RegisterComponent, IntoElement)]
44pub struct ZedAiOnboarding {
45 pub sign_in_status: SignInStatus,
46 pub plan: Option<Plan>,
47 pub account_too_young: bool,
48 pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
49 pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
50 pub dismiss_onboarding: Option<Arc<dyn Fn(&mut Window, &mut App)>>,
51}
52
53impl ZedAiOnboarding {
54 pub fn new(
55 client: Arc<Client>,
56 user_store: &Entity<UserStore>,
57 continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
58 cx: &mut App,
59 ) -> Self {
60 let store = user_store.read(cx);
61 let status = *client.status().borrow();
62
63 Self {
64 sign_in_status: status.into(),
65 plan: store.plan(),
66 account_too_young: store.account_too_young(),
67 continue_with_zed_ai,
68 sign_in: Arc::new(move |_window, cx| {
69 cx.spawn({
70 let client = client.clone();
71 async move |cx| client.sign_in_with_optional_connect(true, cx).await
72 })
73 .detach_and_log_err(cx);
74 }),
75 dismiss_onboarding: None,
76 }
77 }
78
79 pub fn with_dismiss(
80 mut self,
81 dismiss_callback: impl Fn(&mut Window, &mut App) + 'static,
82 ) -> Self {
83 self.dismiss_onboarding = Some(Arc::new(dismiss_callback));
84 self
85 }
86
87 fn certified_user_stamp(cx: &App) -> impl IntoElement {
88 div().absolute().bottom_1().right_1().child(
89 Vector::new(
90 VectorName::ProUserStamp,
91 rems_from_px(156.),
92 rems_from_px(60.),
93 )
94 .color(Color::Custom(cx.theme().colors().text_accent.alpha(0.8))),
95 )
96 }
97
98 fn pro_trial_stamp(cx: &App) -> impl IntoElement {
99 div().absolute().bottom_1().right_1().child(
100 Vector::new(
101 VectorName::ProTrialStamp,
102 rems_from_px(156.),
103 rems_from_px(60.),
104 )
105 .color(Color::Custom(cx.theme().colors().text.alpha(0.8))),
106 )
107 }
108
109 fn business_stamp(cx: &App) -> impl IntoElement {
110 div().absolute().bottom_1().right_1().child(
111 Vector::new(
112 VectorName::BusinessStamp,
113 rems_from_px(156.),
114 rems_from_px(60.),
115 )
116 .color(Color::Custom(cx.theme().colors().text_accent.alpha(0.8))),
117 )
118 }
119
120 fn student_stamp(cx: &App) -> impl IntoElement {
121 div().absolute().bottom_1().right_1().child(
122 Vector::new(
123 VectorName::StudentStamp,
124 rems_from_px(156.),
125 rems_from_px(60.),
126 )
127 .color(Color::Custom(cx.theme().colors().text.alpha(0.8))),
128 )
129 }
130
131 fn render_dismiss_button(&self) -> Option<AnyElement> {
132 self.dismiss_onboarding.as_ref().map(|dismiss_callback| {
133 let callback = dismiss_callback.clone();
134
135 h_flex()
136 .absolute()
137 .top_0()
138 .right_0()
139 .child(
140 IconButton::new("dismiss_onboarding", IconName::Close)
141 .icon_size(IconSize::Small)
142 .tooltip(Tooltip::text("Dismiss"))
143 .on_click(move |_, window, cx| {
144 telemetry::event!("Banner Dismissed", source = "AI Onboarding",);
145 callback(window, cx)
146 }),
147 )
148 .into_any_element()
149 })
150 }
151
152 fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
153 let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
154
155 v_flex()
156 .w_full()
157 .relative()
158 .gap_1()
159 .child(Headline::new("Welcome to Zed AI"))
160 .child(
161 Label::new("Sign in to try Zed Pro for 14 days, no credit card required.")
162 .color(Color::Muted)
163 .mb_2(),
164 )
165 .child(PlanDefinitions.pro_plan())
166 .child(
167 Button::new("sign_in", "Try Zed Pro for Free")
168 .disabled(signing_in)
169 .full_width()
170 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
171 .on_click({
172 let callback = self.sign_in.clone();
173 move |_, window, cx| {
174 telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
175 callback(window, cx)
176 }
177 }),
178 )
179 .children(self.render_dismiss_button())
180 .into_any_element()
181 }
182
183 fn render_free_plan_state(&self, cx: &mut App) -> AnyElement {
184 if self.account_too_young {
185 v_flex()
186 .relative()
187 .min_w_0()
188 .gap_1()
189 .child(Headline::new("Welcome to Zed AI"))
190 .child(YoungAccountBanner)
191 .child(
192 v_flex()
193 .mt_2()
194 .gap_1()
195 .child(
196 h_flex()
197 .gap_2()
198 .child(
199 Label::new("Pro")
200 .size(LabelSize::Small)
201 .color(Color::Accent)
202 .buffer_font(cx),
203 )
204 .child(Divider::horizontal()),
205 )
206 .child(PlanDefinitions.pro_plan())
207 .child(
208 Button::new("pro", "Get Started")
209 .full_width()
210 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
211 .on_click(move |_, _window, cx| {
212 telemetry::event!(
213 "Upgrade To Pro Clicked",
214 state = "young-account"
215 );
216 cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
217 }),
218 ),
219 )
220 .into_any_element()
221 } else {
222 v_flex()
223 .w_full()
224 .relative()
225 .gap_1()
226 .child(Headline::new("Welcome to Zed AI"))
227 .child(
228 v_flex()
229 .mt_2()
230 .gap_1()
231 .child(
232 h_flex()
233 .gap_2()
234 .child(
235 Label::new("Free")
236 .size(LabelSize::Small)
237 .color(Color::Muted)
238 .buffer_font(cx),
239 )
240 .child(
241 Label::new("(Current Plan)")
242 .size(LabelSize::Small)
243 .color(Color::Custom(
244 cx.theme().colors().text_muted.opacity(0.6),
245 ))
246 .buffer_font(cx),
247 )
248 .child(Divider::horizontal()),
249 )
250 .child(PlanDefinitions.free_plan()),
251 )
252 .children(self.render_dismiss_button())
253 .child(
254 v_flex()
255 .mt_2()
256 .gap_1()
257 .child(
258 h_flex()
259 .gap_2()
260 .child(
261 Label::new("Pro Trial")
262 .size(LabelSize::Small)
263 .color(Color::Accent)
264 .buffer_font(cx),
265 )
266 .child(Divider::horizontal()),
267 )
268 .child(PlanDefinitions.pro_trial(true))
269 .child(
270 Button::new("pro", "Start Free Trial")
271 .full_width()
272 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
273 .on_click(move |_, _window, cx| {
274 telemetry::event!(
275 "Start Trial Clicked",
276 state = "post-sign-in"
277 );
278 cx.open_url(&zed_urls::start_trial_url(cx))
279 }),
280 ),
281 )
282 .into_any_element()
283 }
284 }
285
286 fn render_trial_state(&self, cx: &mut App) -> AnyElement {
287 v_flex()
288 .w_full()
289 .relative()
290 .gap_1()
291 .child(Self::pro_trial_stamp(cx))
292 .child(Headline::new("Welcome to the Zed Pro Trial"))
293 .child(
294 Label::new("Here's what you get for the next 14 days:")
295 .color(Color::Muted)
296 .mb_2(),
297 )
298 .child(PlanDefinitions.pro_trial(false))
299 .children(self.render_dismiss_button())
300 .into_any_element()
301 }
302
303 fn render_pro_plan_state(&self, cx: &mut App) -> AnyElement {
304 v_flex()
305 .w_full()
306 .relative()
307 .gap_1()
308 .child(Self::certified_user_stamp(cx))
309 .child(Headline::new("Welcome to Zed Pro"))
310 .child(
311 Label::new("Here's what you get:")
312 .color(Color::Muted)
313 .mb_2(),
314 )
315 .child(PlanDefinitions.pro_plan())
316 .children(self.render_dismiss_button())
317 .into_any_element()
318 }
319
320 fn render_business_plan_state(&self, cx: &mut App) -> AnyElement {
321 v_flex()
322 .w_full()
323 .relative()
324 .gap_1()
325 .child(Self::business_stamp(cx))
326 .child(Headline::new("Welcome to Zed Business"))
327 .child(
328 Label::new("Here's what you get:")
329 .color(Color::Muted)
330 .mb_2(),
331 )
332 .child(PlanDefinitions.business_plan())
333 .children(self.render_dismiss_button())
334 .into_any_element()
335 }
336
337 fn render_student_plan_state(&self, cx: &mut App) -> AnyElement {
338 v_flex()
339 .w_full()
340 .relative()
341 .gap_1()
342 .child(Self::student_stamp(cx))
343 .child(Headline::new("Welcome to Zed Student"))
344 .child(
345 Label::new("Here's what you get:")
346 .color(Color::Muted)
347 .mb_2(),
348 )
349 .child(PlanDefinitions.student_plan())
350 .children(self.render_dismiss_button())
351 .into_any_element()
352 }
353}
354
355impl RenderOnce for ZedAiOnboarding {
356 fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
357 if matches!(self.sign_in_status, SignInStatus::SignedIn) {
358 match self.plan {
359 None => self.render_free_plan_state(cx),
360 Some(Plan::ZedFree) => self.render_free_plan_state(cx),
361 Some(Plan::ZedProTrial) => self.render_trial_state(cx),
362 Some(Plan::ZedPro) => self.render_pro_plan_state(cx),
363 Some(Plan::ZedBusiness) => self.render_business_plan_state(cx),
364 Some(Plan::ZedStudent) => self.render_student_plan_state(cx),
365 }
366 } else {
367 self.render_sign_in_disclaimer(cx)
368 }
369 }
370}
371
372impl Component for ZedAiOnboarding {
373 fn scope() -> ComponentScope {
374 ComponentScope::Onboarding
375 }
376
377 fn name() -> &'static str {
378 "Agent New User Onboarding"
379 }
380
381 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
382 fn onboarding(
383 sign_in_status: SignInStatus,
384 plan: Option<Plan>,
385 account_too_young: bool,
386 ) -> AnyElement {
387 div()
388 .w_full()
389 .min_w_40()
390 .max_w(px(1100.))
391 .child(
392 AgentPanelOnboardingCard::new().child(
393 ZedAiOnboarding {
394 sign_in_status,
395 plan,
396 account_too_young,
397 continue_with_zed_ai: Arc::new(|_, _| {}),
398 sign_in: Arc::new(|_, _| {}),
399 dismiss_onboarding: None,
400 }
401 .into_any_element(),
402 ),
403 )
404 .into_any_element()
405 }
406
407 Some(
408 v_flex()
409 .min_w_0()
410 .gap_4()
411 .children(vec![
412 single_example(
413 "Not Signed-in",
414 onboarding(SignInStatus::SignedOut, None, false),
415 ),
416 single_example(
417 "Young Account",
418 onboarding(SignInStatus::SignedIn, None, true),
419 ),
420 single_example(
421 "Free Plan",
422 onboarding(SignInStatus::SignedIn, Some(Plan::ZedFree), false),
423 ),
424 single_example(
425 "Pro Trial",
426 onboarding(SignInStatus::SignedIn, Some(Plan::ZedProTrial), false),
427 ),
428 single_example(
429 "Pro Plan",
430 onboarding(SignInStatus::SignedIn, Some(Plan::ZedPro), false),
431 ),
432 single_example(
433 "Business Plan",
434 onboarding(SignInStatus::SignedIn, Some(Plan::ZedBusiness), false),
435 ),
436 single_example(
437 "Student Plan",
438 onboarding(SignInStatus::SignedIn, Some(Plan::ZedStudent), false),
439 ),
440 ])
441 .into_any_element(),
442 )
443 }
444}
445
446#[derive(RegisterComponent)]
447pub struct AgentLayoutOnboarding {
448 pub use_agent_layout: Arc<dyn Fn(&mut Window, &mut App)>,
449 pub revert_to_editor_layout: Arc<dyn Fn(&mut Window, &mut App)>,
450 pub dismissed: Arc<dyn Fn(&mut Window, &mut App)>,
451 pub is_agent_layout: bool,
452}
453
454impl Render for AgentLayoutOnboarding {
455 fn render(&mut self, _window: &mut ui::Window, _cx: &mut Context<Self>) -> impl IntoElement {
456 let description = "The new threads sidebar, positioned in the far left of your workspace, allows you to manage agents across many projects. Your agent thread lives alongside it, and all other panels live on the right.";
457
458 let dismiss_button = div().absolute().top_1().right_1().child(
459 IconButton::new("dismiss", IconName::Close)
460 .icon_size(IconSize::Small)
461 .on_click({
462 let dismiss = self.dismissed.clone();
463 move |_, window, cx| {
464 telemetry::event!("Agentic Layout Onboarding Dismissed");
465 dismiss(window, cx)
466 }
467 }),
468 );
469
470 let primary_button = if self.is_agent_layout {
471 Button::new("revert", "Use Previous Layout")
472 .label_size(LabelSize::Small)
473 .style(ButtonStyle::Outlined)
474 .on_click({
475 let revert = self.revert_to_editor_layout.clone();
476 let dismiss = self.dismissed.clone();
477 move |_, window, cx| {
478 telemetry::event!("Clicked to Use Previous Layout");
479 revert(window, cx);
480 dismiss(window, cx);
481 }
482 })
483 } else {
484 Button::new("start", "Use New Layout")
485 .label_size(LabelSize::Small)
486 .style(ButtonStyle::Outlined)
487 .on_click({
488 let use_layout = self.use_agent_layout.clone();
489 let dismiss = self.dismissed.clone();
490 move |_, window, cx| {
491 telemetry::event!("Clicked to Use New Layout");
492 use_layout(window, cx);
493 dismiss(window, cx);
494 }
495 })
496 };
497
498 let content = v_flex()
499 .min_w_0()
500 .w_full()
501 .relative()
502 .gap_1()
503 .child(Label::new("A new workspace layout for agentic work"))
504 .child(Label::new(description).color(Color::Muted).mb_2())
505 .child(
506 List::new()
507 .child(ListBulletItem::new("Use your favorite agents in parallel"))
508 .child(ListBulletItem::new("Isolate agents using worktrees"))
509 .child(ListBulletItem::new(
510 "Combine multiple projects in one window",
511 )),
512 )
513 .child(
514 h_flex()
515 .w_full()
516 .gap_1()
517 .flex_wrap()
518 .justify_end()
519 .child(primary_button),
520 )
521 .child(dismiss_button);
522
523 AgentPanelOnboardingCard::new().child(content)
524 }
525}
526
527impl Component for AgentLayoutOnboarding {
528 fn scope() -> ComponentScope {
529 ComponentScope::Onboarding
530 }
531
532 fn name() -> &'static str {
533 "Agent Layout Onboarding"
534 }
535
536 fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
537 let onboarding = cx.new(|_cx| AgentLayoutOnboarding {
538 use_agent_layout: Arc::new(|_, _| {}),
539 revert_to_editor_layout: Arc::new(|_, _| {}),
540 dismissed: Arc::new(|_, _| {}),
541 is_agent_layout: false,
542 });
543
544 Some(
545 v_flex()
546 .min_w_0()
547 .gap_4()
548 .child(single_example(
549 "Agent Layout Onboarding",
550 div()
551 .w_full()
552 .min_w_40()
553 .max_w(px(1100.))
554 .child(onboarding)
555 .into_any_element(),
556 ))
557 .into_any_element(),
558 )
559 }
560}