1use std::sync::Arc;
2use std::time::Duration;
3
4use client::{Client, TelemetrySettings, UserStore, zed_urls};
5use cloud_api_types::Plan;
6use collections::HashMap;
7use fs::Fs;
8use gpui::{Action, Animation, AnimationExt, App, Entity, IntoElement, pulsating_between};
9use project::agent_server_store::AllAgentServersSettings;
10use project::project_settings::ProjectSettings;
11use project::{AgentRegistryStore, RegistryAgent};
12use settings::{
13 BaseKeymap, CustomAgentServerSettings, Settings, SettingsStore, update_settings_file,
14};
15use theme::{Appearance, SystemAppearance, ThemeRegistry};
16use theme_settings::{ThemeAppearanceMode, ThemeName, ThemeSelection, ThemeSettings};
17use ui::{
18 AgentSetupButton, Divider, StatefulInteractiveElement, SwitchField, TintColor,
19 ToggleButtonGroup, ToggleButtonGroupSize, ToggleButtonSimple, ToggleButtonWithIcon, Tooltip,
20 prelude::*,
21};
22use vim_mode_setting::VimModeSetting;
23
24use crate::{
25 ImportCursorSettings, ImportVsCodeSettings, SettingsImportState,
26 theme_preview::{ThemePreviewStyle, ThemePreviewTile},
27};
28
29const LIGHT_THEMES: [&str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"];
30const DARK_THEMES: [&str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"];
31const FAMILY_NAMES: [SharedString; 3] = [
32 SharedString::new_static("One"),
33 SharedString::new_static("Ayu"),
34 SharedString::new_static("Gruvbox"),
35];
36
37fn get_theme_family_themes(theme_name: &str) -> Option<(&'static str, &'static str)> {
38 for i in 0..LIGHT_THEMES.len() {
39 if LIGHT_THEMES[i] == theme_name || DARK_THEMES[i] == theme_name {
40 return Some((LIGHT_THEMES[i], DARK_THEMES[i]));
41 }
42 }
43 None
44}
45
46fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
47 let theme_selection = ThemeSettings::get_global(cx).theme.clone();
48 let system_appearance = theme::SystemAppearance::global(cx);
49
50 let theme_mode = theme_selection
51 .mode()
52 .unwrap_or_else(|| match *system_appearance {
53 Appearance::Light => ThemeAppearanceMode::Light,
54 Appearance::Dark => ThemeAppearanceMode::Dark,
55 });
56
57 return v_flex()
58 .gap_2()
59 .child(
60 h_flex().justify_between().child(Label::new("Theme")).child(
61 ToggleButtonGroup::single_row(
62 "theme-selector-onboarding-dark-light",
63 [
64 ThemeAppearanceMode::Light,
65 ThemeAppearanceMode::Dark,
66 ThemeAppearanceMode::System,
67 ]
68 .map(|mode| {
69 const MODE_NAMES: [SharedString; 3] = [
70 SharedString::new_static("Light"),
71 SharedString::new_static("Dark"),
72 SharedString::new_static("System"),
73 ];
74 ToggleButtonSimple::new(
75 MODE_NAMES[mode as usize].clone(),
76 move |_, _, cx| {
77 write_mode_change(mode, cx);
78
79 telemetry::event!(
80 "Welcome Theme mode Changed",
81 from = theme_mode,
82 to = mode
83 );
84 },
85 )
86 }),
87 )
88 .size(ToggleButtonGroupSize::Medium)
89 .tab_index(tab_index)
90 .selected_index(theme_mode as usize)
91 .style(ui::ToggleButtonGroupStyle::Outlined)
92 .width(rems_from_px(3. * 64.)),
93 ),
94 )
95 .child(
96 h_flex()
97 .gap_2()
98 .justify_between()
99 .children(render_theme_previews(tab_index, &theme_selection, cx)),
100 );
101
102 fn render_theme_previews(
103 tab_index: &mut isize,
104 theme_selection: &ThemeSelection,
105 cx: &mut App,
106 ) -> [impl IntoElement; 3] {
107 let system_appearance = SystemAppearance::global(cx);
108 let theme_registry = ThemeRegistry::global(cx);
109
110 let theme_seed = 0xBEEF as f32;
111 let theme_mode = theme_selection
112 .mode()
113 .unwrap_or_else(|| match *system_appearance {
114 Appearance::Light => ThemeAppearanceMode::Light,
115 Appearance::Dark => ThemeAppearanceMode::Dark,
116 });
117 let appearance = match theme_mode {
118 ThemeAppearanceMode::Light => Appearance::Light,
119 ThemeAppearanceMode::Dark => Appearance::Dark,
120 ThemeAppearanceMode::System => *system_appearance,
121 };
122 let current_theme_name: SharedString = theme_selection.name(appearance).0.into();
123
124 let theme_names = match appearance {
125 Appearance::Light => LIGHT_THEMES,
126 Appearance::Dark => DARK_THEMES,
127 };
128
129 let themes = theme_names.map(|theme| theme_registry.get(theme).unwrap());
130
131 [0, 1, 2].map(|index| {
132 let theme = &themes[index];
133 let is_selected = theme.name == current_theme_name;
134 let name = theme.name.clone();
135 let colors = cx.theme().colors();
136
137 v_flex()
138 .w_full()
139 .items_center()
140 .gap_1()
141 .child(
142 h_flex()
143 .id(name)
144 .relative()
145 .w_full()
146 .border_2()
147 .border_color(colors.border_transparent)
148 .rounded(ThemePreviewTile::ROOT_RADIUS)
149 .map(|this| {
150 if is_selected {
151 this.border_color(colors.border_selected)
152 } else {
153 this.opacity(0.8).hover(|s| s.border_color(colors.border))
154 }
155 })
156 .tab_index({
157 *tab_index += 1;
158 *tab_index - 1
159 })
160 .focus(|mut style| {
161 style.border_color = Some(colors.border_focused);
162 style
163 })
164 .on_click({
165 let theme_name = theme.name.clone();
166 let current_theme_name = current_theme_name.clone();
167
168 move |_, _, cx| {
169 write_theme_change(theme_name.clone(), theme_mode, cx);
170 telemetry::event!(
171 "Welcome Theme Changed",
172 from = current_theme_name,
173 to = theme_name
174 );
175 }
176 })
177 .map(|this| {
178 if theme_mode == ThemeAppearanceMode::System {
179 let (light, dark) = (
180 theme_registry.get(LIGHT_THEMES[index]).unwrap(),
181 theme_registry.get(DARK_THEMES[index]).unwrap(),
182 );
183 this.child(
184 ThemePreviewTile::new(light, theme_seed)
185 .style(ThemePreviewStyle::SideBySide(dark)),
186 )
187 } else {
188 this.child(
189 ThemePreviewTile::new(theme.clone(), theme_seed)
190 .style(ThemePreviewStyle::Bordered),
191 )
192 }
193 }),
194 )
195 .child(
196 Label::new(FAMILY_NAMES[index].clone())
197 .color(Color::Muted)
198 .size(LabelSize::Small),
199 )
200 })
201 }
202
203 fn write_mode_change(mode: ThemeAppearanceMode, cx: &mut App) {
204 let fs = <dyn Fs>::global(cx);
205 update_settings_file(fs, cx, move |settings, _cx| {
206 theme_settings::set_mode(settings, mode);
207 });
208 }
209
210 fn write_theme_change(
211 theme: impl Into<Arc<str>>,
212 theme_mode: ThemeAppearanceMode,
213 cx: &mut App,
214 ) {
215 let fs = <dyn Fs>::global(cx);
216 let theme = theme.into();
217 update_settings_file(fs, cx, move |settings, cx| match theme_mode {
218 ThemeAppearanceMode::System => {
219 let (light_theme, dark_theme) =
220 get_theme_family_themes(&theme).unwrap_or((theme.as_ref(), theme.as_ref()));
221
222 settings.theme.theme = Some(settings::ThemeSelection::Dynamic {
223 mode: ThemeAppearanceMode::System,
224 light: ThemeName(light_theme.into()),
225 dark: ThemeName(dark_theme.into()),
226 });
227 }
228 ThemeAppearanceMode::Light => theme_settings::set_theme(
229 settings,
230 theme,
231 Appearance::Light,
232 *SystemAppearance::global(cx),
233 ),
234 ThemeAppearanceMode::Dark => theme_settings::set_theme(
235 settings,
236 theme,
237 Appearance::Dark,
238 *SystemAppearance::global(cx),
239 ),
240 });
241 }
242}
243
244fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement {
245 let fs = <dyn Fs>::global(cx);
246
247 v_flex()
248 .gap_4()
249 .child(
250 SwitchField::new(
251 "onboarding-telemetry-metrics",
252 None::<&str>,
253 Some("Help improve Zed by sending anonymous usage data".into()),
254 if TelemetrySettings::get_global(cx).metrics {
255 ui::ToggleState::Selected
256 } else {
257 ui::ToggleState::Unselected
258 },
259 {
260 let fs = fs.clone();
261 move |selection, _, cx| {
262 let enabled = match selection {
263 ToggleState::Selected => true,
264 ToggleState::Unselected => false,
265 ToggleState::Indeterminate => {
266 return;
267 }
268 };
269
270 update_settings_file(fs.clone(), cx, move |setting, _| {
271 setting.telemetry.get_or_insert_default().metrics = Some(enabled);
272 });
273
274 // This telemetry event shouldn't fire when it's off. If it does we'll be alerted
275 // and can fix it in a timely manner to respect a user's choice.
276 telemetry::event!(
277 "Welcome Page Telemetry Metrics Toggled",
278 options = if enabled { "on" } else { "off" }
279 );
280 }
281 },
282 )
283 .tab_index({
284 *tab_index += 1;
285 *tab_index
286 }),
287 )
288 .child(
289 SwitchField::new(
290 "onboarding-telemetry-crash-reports",
291 None::<&str>,
292 Some(
293 "Help fix Zed by sending crash reports so we can fix critical issues fast"
294 .into(),
295 ),
296 if TelemetrySettings::get_global(cx).diagnostics {
297 ui::ToggleState::Selected
298 } else {
299 ui::ToggleState::Unselected
300 },
301 {
302 let fs = fs.clone();
303 move |selection, _, cx| {
304 let enabled = match selection {
305 ToggleState::Selected => true,
306 ToggleState::Unselected => false,
307 ToggleState::Indeterminate => {
308 return;
309 }
310 };
311
312 update_settings_file(fs.clone(), cx, move |setting, _| {
313 setting.telemetry.get_or_insert_default().diagnostics = Some(enabled);
314 });
315
316 // This telemetry event shouldn't fire when it's off. If it does we'll be alerted
317 // and can fix it in a timely manner to respect a user's choice.
318 telemetry::event!(
319 "Welcome Page Telemetry Diagnostics Toggled",
320 options = if enabled { "on" } else { "off" }
321 );
322 }
323 },
324 )
325 .tab_index({
326 *tab_index += 1;
327 *tab_index
328 }),
329 )
330}
331
332fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
333 let base_keymap = match BaseKeymap::get_global(cx) {
334 BaseKeymap::VSCode => Some(0),
335 BaseKeymap::JetBrains => Some(1),
336 BaseKeymap::SublimeText => Some(2),
337 BaseKeymap::Atom => Some(3),
338 BaseKeymap::Emacs => Some(4),
339 BaseKeymap::Cursor => Some(5),
340 BaseKeymap::TextMate | BaseKeymap::None => None,
341 };
342
343 return v_flex().gap_2().child(Label::new("Base Keymap")).child(
344 ToggleButtonGroup::two_rows(
345 "base_keymap_selection",
346 [
347 ToggleButtonWithIcon::new("VS Code", IconName::EditorVsCode, |_, _, cx| {
348 write_keymap_base(BaseKeymap::VSCode, cx);
349 }),
350 ToggleButtonWithIcon::new("JetBrains", IconName::EditorJetBrains, |_, _, cx| {
351 write_keymap_base(BaseKeymap::JetBrains, cx);
352 }),
353 ToggleButtonWithIcon::new("Sublime Text", IconName::EditorSublime, |_, _, cx| {
354 write_keymap_base(BaseKeymap::SublimeText, cx);
355 }),
356 ],
357 [
358 ToggleButtonWithIcon::new("Atom", IconName::EditorAtom, |_, _, cx| {
359 write_keymap_base(BaseKeymap::Atom, cx);
360 }),
361 ToggleButtonWithIcon::new("Emacs", IconName::EditorEmacs, |_, _, cx| {
362 write_keymap_base(BaseKeymap::Emacs, cx);
363 }),
364 ToggleButtonWithIcon::new("Cursor", IconName::EditorCursor, |_, _, cx| {
365 write_keymap_base(BaseKeymap::Cursor, cx);
366 }),
367 ],
368 )
369 .when_some(base_keymap, |this, base_keymap| {
370 this.selected_index(base_keymap)
371 })
372 .full_width()
373 .tab_index(tab_index)
374 .size(ui::ToggleButtonGroupSize::Medium)
375 .style(ui::ToggleButtonGroupStyle::Outlined),
376 );
377
378 fn write_keymap_base(keymap_base: BaseKeymap, cx: &App) {
379 let fs = <dyn Fs>::global(cx);
380
381 update_settings_file(fs, cx, move |setting, _| {
382 setting.base_keymap = Some(keymap_base.into());
383 });
384
385 telemetry::event!("Welcome Keymap Changed", keymap = keymap_base);
386 }
387}
388
389fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
390 let toggle_state = if VimModeSetting::get_global(cx).0 {
391 ui::ToggleState::Selected
392 } else {
393 ui::ToggleState::Unselected
394 };
395 SwitchField::new(
396 "onboarding-vim-mode",
397 Some("Vim Mode"),
398 Some("Coming from Neovim? Use our first-class implementation of Vim Mode".into()),
399 toggle_state,
400 {
401 let fs = <dyn Fs>::global(cx);
402 move |&selection, _, cx| {
403 let vim_mode = match selection {
404 ToggleState::Selected => true,
405 ToggleState::Unselected => false,
406 ToggleState::Indeterminate => {
407 return;
408 }
409 };
410 update_settings_file(fs.clone(), cx, move |setting, _| {
411 setting.vim_mode = Some(vim_mode);
412 });
413
414 telemetry::event!(
415 "Welcome Vim Mode Toggled",
416 options = if vim_mode { "on" } else { "off" },
417 );
418 }
419 },
420 )
421 .tab_index({
422 *tab_index += 1;
423 *tab_index - 1
424 })
425}
426
427fn render_worktree_auto_trust_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
428 let toggle_state = if ProjectSettings::get_global(cx).session.trust_all_worktrees {
429 ui::ToggleState::Selected
430 } else {
431 ui::ToggleState::Unselected
432 };
433
434 let tooltip_description = "Zed can only allow services like language servers, project settings, and MCP servers to run after you mark a new project as trusted.";
435
436 SwitchField::new(
437 "onboarding-auto-trust-worktrees",
438 Some("Trust All Projects By Default"),
439 Some("Automatically mark all new projects as trusted to unlock all Zed's features".into()),
440 toggle_state,
441 {
442 let fs = <dyn Fs>::global(cx);
443 move |&selection, _, cx| {
444 let trust = match selection {
445 ToggleState::Selected => true,
446 ToggleState::Unselected => false,
447 ToggleState::Indeterminate => {
448 return;
449 }
450 };
451 update_settings_file(fs.clone(), cx, move |setting, _| {
452 setting.session.get_or_insert_default().trust_all_worktrees = Some(trust);
453 });
454
455 telemetry::event!(
456 "Welcome Page Worktree Auto Trust Toggled",
457 options = if trust { "on" } else { "off" }
458 );
459 }
460 },
461 )
462 .tab_index({
463 *tab_index += 1;
464 *tab_index - 1
465 })
466 .tooltip(Tooltip::text(tooltip_description))
467}
468
469fn render_setting_import_button(
470 tab_index: isize,
471 label: SharedString,
472 action: &dyn Action,
473 imported: bool,
474) -> impl IntoElement + 'static {
475 let action = action.boxed_clone();
476
477 Button::new(label.clone(), label.clone())
478 .style(ButtonStyle::OutlinedGhost)
479 .size(ButtonSize::Medium)
480 .label_size(LabelSize::Small)
481 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
482 .toggle_state(imported)
483 .tab_index(tab_index)
484 .when(imported, |this| {
485 this.end_icon(Icon::new(IconName::Check).size(IconSize::Small))
486 .color(Color::Success)
487 })
488 .on_click(move |_, window, cx| {
489 telemetry::event!("Welcome Import Settings", import_source = label,);
490 window.dispatch_action(action.boxed_clone(), cx);
491 })
492}
493
494fn render_import_settings_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
495 let import_state = SettingsImportState::global(cx);
496 let imports: [(SharedString, &dyn Action, bool); 2] = [
497 (
498 "VS Code".into(),
499 &ImportVsCodeSettings { skip_prompt: false },
500 import_state.vscode,
501 ),
502 (
503 "Cursor".into(),
504 &ImportCursorSettings { skip_prompt: false },
505 import_state.cursor,
506 ),
507 ];
508
509 let [vscode, cursor] = imports.map(|(label, action, imported)| {
510 *tab_index += 1;
511 render_setting_import_button(*tab_index - 1, label, action, imported)
512 });
513
514 h_flex()
515 .gap_2()
516 .flex_wrap()
517 .justify_between()
518 .child(
519 v_flex()
520 .gap_0p5()
521 .max_w_5_6()
522 .child(Label::new("Import Settings"))
523 .child(
524 Label::new("Automatically pull your settings from other editors")
525 .color(Color::Muted),
526 ),
527 )
528 .child(h_flex().gap_1().child(vscode).child(cursor))
529}
530
531const FEATURED_AGENT_IDS: &[&str] = &["claude-acp", "codex-acp", "github-copilot-cli", "cursor"];
532
533fn render_registry_agent_button(
534 agent: &RegistryAgent,
535 installed: bool,
536 cx: &mut App,
537) -> impl IntoElement {
538 let agent_id = agent.id().to_string();
539 let element_id = format!("{}-onboarding", agent_id);
540
541 let icon = match agent.icon_path() {
542 Some(icon_path) => Icon::from_external_svg(icon_path.clone()),
543 None => Icon::new(IconName::Sparkle),
544 }
545 .size(IconSize::XSmall)
546 .color(Color::Muted);
547
548 let fs = <dyn Fs>::global(cx);
549
550 let state_element = if installed {
551 Icon::new(IconName::Check)
552 .size(IconSize::Small)
553 .color(Color::Success)
554 .into_any_element()
555 } else {
556 Label::new("Install")
557 .size(LabelSize::XSmall)
558 .color(Color::Muted)
559 .into_any_element()
560 };
561
562 AgentSetupButton::new(element_id)
563 .icon(icon)
564 .name(agent.name().clone())
565 .state(state_element)
566 .disabled(installed)
567 .on_click(move |_, _, cx| {
568 let agent_id = agent_id.clone();
569 update_settings_file(fs.clone(), cx, move |settings, _| {
570 let agent_servers = settings.agent_servers.get_or_insert_default();
571 agent_servers.entry(agent_id).or_insert_with(|| {
572 CustomAgentServerSettings::Registry {
573 env: Default::default(),
574 default_mode: None,
575 default_model: None,
576 favorite_models: Vec::new(),
577 default_config_options: HashMap::default(),
578 favorite_config_option_values: HashMap::default(),
579 }
580 });
581 });
582 })
583}
584
585fn render_zed_agent_button(user_store: &Entity<UserStore>, cx: &mut App) -> impl IntoElement {
586 let client = Client::global(cx);
587 let status = *client.status().borrow();
588
589 let plan = user_store.read(cx).plan();
590 let is_free = matches!(plan, Some(Plan::ZedFree) | None);
591 let is_pro = matches!(plan, Some(Plan::ZedPro));
592 let is_trial = matches!(plan, Some(Plan::ZedProTrial));
593
594 let is_signed_out = status.is_signed_out()
595 || matches!(
596 status,
597 client::Status::AuthenticationError | client::Status::ConnectionError
598 );
599 let is_signing_in = status.is_signing_in();
600 let is_signed_in = !is_signed_out;
601
602 let state_element = if is_signed_out {
603 Label::new("Sign In")
604 .size(LabelSize::XSmall)
605 .color(Color::Muted)
606 .into_any_element()
607 } else if is_signing_in {
608 Label::new("Signing Inβ¦")
609 .size(LabelSize::XSmall)
610 .color(Color::Muted)
611 .with_animation(
612 "signing-in",
613 Animation::new(Duration::from_secs(2))
614 .repeat()
615 .with_easing(pulsating_between(0.4, 0.8)),
616 |label, delta| label.alpha(delta),
617 )
618 .into_any_element()
619 } else if is_signed_in && is_free {
620 Label::new("Start Free Trial")
621 .size(LabelSize::XSmall)
622 .color(Color::Muted)
623 .into_any_element()
624 } else {
625 Icon::new(IconName::Check)
626 .size(IconSize::Small)
627 .color(Color::Success)
628 .into_any_element()
629 };
630
631 AgentSetupButton::new("zed-agent-onboarding")
632 .icon(
633 Icon::new(IconName::ZedAgent)
634 .size(IconSize::XSmall)
635 .color(Color::Muted),
636 )
637 .name("Zed Agent")
638 .state(state_element)
639 .disabled(is_trial || is_pro)
640 .map(|this| {
641 if is_signed_in && is_free {
642 this.on_click(move |_, _window, cx| {
643 telemetry::event!("Start Trial Clicked", state = "post-sign-in");
644 cx.open_url(&zed_urls::start_trial_url(cx))
645 })
646 } else {
647 this.on_click(move |_, _, cx| {
648 let client = Client::global(cx);
649 cx.spawn(async move |cx| client.sign_in_with_optional_connect(true, cx).await)
650 .detach_and_log_err(cx);
651 })
652 }
653 })
654}
655
656fn render_ai_section(user_store: &Entity<UserStore>, cx: &mut App) -> impl IntoElement {
657 let registry_agents = AgentRegistryStore::try_global(cx)
658 .map(|store| store.read(cx).agents().to_vec())
659 .unwrap_or_default();
660
661 let installed_agents = cx
662 .global::<SettingsStore>()
663 .get::<AllAgentServersSettings>(None)
664 .clone();
665
666 let column_count = 1 + FEATURED_AGENT_IDS.len() as u16;
667
668 let grid = FEATURED_AGENT_IDS.iter().fold(
669 div()
670 .w_full()
671 .mt_1p5()
672 .grid()
673 .grid_cols(column_count)
674 .gap_2()
675 .child(render_zed_agent_button(user_store, cx)),
676 |grid, agent_id| {
677 let Some(agent) = registry_agents
678 .iter()
679 .find(|a| a.id().as_ref() == *agent_id)
680 else {
681 return grid;
682 };
683 let is_installed = installed_agents.contains_key(*agent_id);
684 grid.child(render_registry_agent_button(agent, is_installed, cx))
685 },
686 );
687
688 v_flex()
689 .gap_0p5()
690 .child(Label::new("Agent Setup"))
691 .child(
692 Label::new("Install your favorite agents and start your first thread.")
693 .color(Color::Muted),
694 )
695 .child(grid)
696}
697
698pub(crate) fn render_basics_page(user_store: &Entity<UserStore>, cx: &mut App) -> impl IntoElement {
699 let mut tab_index = 0;
700
701 v_flex()
702 .id("basics-page")
703 .gap_6()
704 .child(render_theme_section(&mut tab_index, cx))
705 .child(render_base_keymap_section(&mut tab_index, cx))
706 .child(render_ai_section(user_store, cx))
707 .child(render_import_settings_section(&mut tab_index, cx))
708 .child(render_vim_mode_switch(&mut tab_index, cx))
709 .child(render_worktree_auto_trust_switch(&mut tab_index, cx))
710 .child(Divider::horizontal().color(ui::DividerColor::BorderVariant))
711 .child(render_telemetry_section(&mut tab_index, cx))
712}