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 = "With the new Threads Sidebar, you can manage multiple agents across several projects, all in one window.";
457
458 let dismiss_button = div().absolute().top_0().right_0().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 workflows"))
504 .child(Label::new(description).color(Color::Muted).mb_2())
505 .child(
506 List::new()
507 .child(ListBulletItem::new(
508 "The Sidebar and Agent Panel are on the left by default",
509 ))
510 .child(ListBulletItem::new(
511 "The Project Panel and all other panels shift to the right",
512 ))
513 .child(ListBulletItem::new(
514 "You can always customize your workspace layout in your Settings",
515 )),
516 )
517 .child(
518 h_flex()
519 .w_full()
520 .gap_1()
521 .flex_wrap()
522 .justify_end()
523 .child(
524 Button::new("learn", "Learn More")
525 .label_size(LabelSize::Small)
526 .style(ButtonStyle::OutlinedGhost)
527 .on_click(move |_, _, cx| {
528 cx.open_url(&zed_urls::parallel_agents_blog(cx))
529 }),
530 )
531 .child(primary_button),
532 )
533 .child(dismiss_button);
534
535 AgentPanelOnboardingCard::new().child(content)
536 }
537}
538
539impl Component for AgentLayoutOnboarding {
540 fn scope() -> ComponentScope {
541 ComponentScope::Onboarding
542 }
543
544 fn name() -> &'static str {
545 "Agent Layout Onboarding"
546 }
547
548 fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
549 let onboarding = cx.new(|_cx| AgentLayoutOnboarding {
550 use_agent_layout: Arc::new(|_, _| {}),
551 revert_to_editor_layout: Arc::new(|_, _| {}),
552 dismissed: Arc::new(|_, _| {}),
553 is_agent_layout: false,
554 });
555
556 Some(
557 v_flex()
558 .min_w_0()
559 .gap_4()
560 .child(single_example(
561 "Agent Layout Onboarding",
562 div()
563 .w_full()
564 .min_w_40()
565 .max_w(px(1100.))
566 .child(onboarding)
567 .into_any_element(),
568 ))
569 .into_any_element(),
570 )
571 }
572}