Skip to repository content812 lines · 29.4 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:37:21.490Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
basics_page.rs
1use std::sync::Arc;
2
3use client::{TelemetrySettings, UserStore};
4use collections::HashMap;
5use fs::Fs;
6use gpui::{Action, App, Entity, IntoElement};
7use project::AgentRegistryStore;
8use project::agent_server_store::AllAgentServersSettings;
9use project::project_settings::ProjectSettings;
10use settings::{
11 BaseKeymap, CustomAgentServerSettings, Settings, SettingsStore, update_settings_file,
12};
13use theme::{Appearance, SystemAppearance, ThemeRegistry};
14use theme_settings::{ThemeAppearanceMode, ThemeName, ThemeSelection, ThemeSettings};
15use ui::{
16 AgentSetupButton, Divider, StatefulInteractiveElement, SwitchField, TintColor,
17 ToggleButtonGroup, ToggleButtonGroupSize, ToggleButtonSimple, ToggleButtonWithIcon, Tooltip,
18 prelude::*,
19};
20use vim_mode_setting::VimModeSetting;
21
22use crate::{
23 ImportCursorSettings, ImportVsCodeSettings, SettingsImportState,
24 identity_section::{IdentitySection, IdentitySectionPresentation, render_identity_section},
25 theme_preview::{ThemePreviewStyle, ThemePreviewTile},
26};
27
28// Aiur is dark-only (omega#70). Selecting the Aiur family gives Aiur in
29// either appearance rather than substituting a light theme the owner did
30// not choose.
31const LIGHT_THEMES: [&str; 3] = ["Aiur", "Ayu Light", "Gruvbox Light"];
32const DARK_THEMES: [&str; 3] = ["Aiur", "Ayu Dark", "Gruvbox Dark"];
33const FAMILY_NAMES: [SharedString; 3] = [
34 SharedString::new_static("Aiur"),
35 SharedString::new_static("Ayu"),
36 SharedString::new_static("Gruvbox"),
37];
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub(crate) enum BasicsPageMode {
41 FirstRun,
42 EditorSetup,
43}
44
45impl BasicsPageMode {
46 fn identity_presentation(self) -> IdentitySectionPresentation {
47 match self {
48 Self::FirstRun => IdentitySectionPresentation::Full,
49 Self::EditorSetup => IdentitySectionPresentation::Compact,
50 }
51 }
52}
53
54fn get_theme_family_themes(theme_name: &str) -> Option<(&'static str, &'static str)> {
55 for i in 0..LIGHT_THEMES.len() {
56 if LIGHT_THEMES[i] == theme_name || DARK_THEMES[i] == theme_name {
57 return Some((LIGHT_THEMES[i], DARK_THEMES[i]));
58 }
59 }
60 None
61}
62
63fn render_theme_section(tab_index: &mut isize, compact: bool, cx: &mut App) -> impl IntoElement {
64 let theme_selection = ThemeSettings::get_global(cx).theme.clone();
65 let system_appearance = theme::SystemAppearance::global(cx);
66
67 let theme_mode = theme_selection
68 .mode()
69 .unwrap_or_else(|| match *system_appearance {
70 Appearance::Light => ThemeAppearanceMode::Light,
71 Appearance::Dark => ThemeAppearanceMode::Dark,
72 });
73
74 return v_flex()
75 .gap_2()
76 .child(
77 div()
78 .flex()
79 .gap_2()
80 .when_else(
81 compact,
82 |this| this.flex_col().items_stretch(),
83 |this| this.flex_row().items_center().justify_between(),
84 )
85 .child(Label::new("Theme"))
86 .child(
87 ToggleButtonGroup::single_row(
88 "theme-selector-onboarding-dark-light",
89 [
90 ThemeAppearanceMode::Light,
91 ThemeAppearanceMode::Dark,
92 ThemeAppearanceMode::System,
93 ]
94 .map(|mode| {
95 const MODE_NAMES: [SharedString; 3] = [
96 SharedString::new_static("Light"),
97 SharedString::new_static("Dark"),
98 SharedString::new_static("System"),
99 ];
100 ToggleButtonSimple::new(
101 MODE_NAMES[mode as usize].clone(),
102 move |_, _, cx| {
103 write_mode_change(mode, cx);
104
105 telemetry::event!(
106 "Welcome Theme mode Changed",
107 from = theme_mode,
108 to = mode
109 );
110 },
111 )
112 }),
113 )
114 .size(ToggleButtonGroupSize::Medium)
115 .tab_index(tab_index)
116 .selected_index(theme_mode as usize)
117 .style(ui::ToggleButtonGroupStyle::Outlined)
118 .when_else(
119 compact,
120 |this| this.full_width(),
121 |this| this.width(rems_from_px(3. * 64.)),
122 ),
123 ),
124 )
125 .child(
126 div()
127 .flex()
128 .gap_2()
129 .when_else(
130 compact,
131 |this| this.flex_col(),
132 |this| this.flex_row().justify_between(),
133 )
134 .children(render_theme_previews(tab_index, &theme_selection, cx)),
135 );
136
137 fn render_theme_previews(
138 tab_index: &mut isize,
139 theme_selection: &ThemeSelection,
140 cx: &mut App,
141 ) -> [impl IntoElement; 3] {
142 let system_appearance = SystemAppearance::global(cx);
143 let theme_registry = ThemeRegistry::global(cx);
144
145 let theme_seed = 0xBEEF as f32;
146 let theme_mode = theme_selection
147 .mode()
148 .unwrap_or_else(|| match *system_appearance {
149 Appearance::Light => ThemeAppearanceMode::Light,
150 Appearance::Dark => ThemeAppearanceMode::Dark,
151 });
152 let appearance = match theme_mode {
153 ThemeAppearanceMode::Light => Appearance::Light,
154 ThemeAppearanceMode::Dark => Appearance::Dark,
155 ThemeAppearanceMode::System => *system_appearance,
156 };
157 let current_theme_name: SharedString = theme_selection.name(appearance).0.into();
158
159 let theme_names = match appearance {
160 Appearance::Light => LIGHT_THEMES,
161 Appearance::Dark => DARK_THEMES,
162 };
163
164 let themes = theme_names.map(|theme| theme_registry.get(theme).unwrap());
165
166 [0, 1, 2].map(|index| {
167 let theme = &themes[index];
168 let is_selected = theme.name == current_theme_name;
169 let name = theme.name.clone();
170 let colors = cx.theme().colors();
171
172 v_flex()
173 .w_full()
174 .items_center()
175 .gap_1()
176 .child(
177 h_flex()
178 .id(name)
179 .role(gpui::Role::RadioButton)
180 .aria_label(format!("{} theme", FAMILY_NAMES[index]))
181 .aria_selected(is_selected)
182 .relative()
183 .w_full()
184 .border_2()
185 .border_color(colors.border_transparent)
186 .rounded(ThemePreviewTile::ROOT_RADIUS)
187 .map(|this| {
188 if is_selected {
189 this.border_color(colors.border_selected)
190 } else {
191 this.opacity(0.8).hover(|s| s.border_color(colors.border))
192 }
193 })
194 .tab_index({
195 *tab_index += 1;
196 *tab_index - 1
197 })
198 .focus(|mut style| {
199 style.border_color = Some(colors.border_focused);
200 style
201 })
202 .on_click({
203 let theme_name = theme.name.clone();
204 let current_theme_name = current_theme_name.clone();
205
206 move |_, _, cx| {
207 write_theme_change(theme_name.clone(), theme_mode, cx);
208 telemetry::event!(
209 "Welcome Theme Changed",
210 from = current_theme_name,
211 to = theme_name
212 );
213 }
214 })
215 .map(|this| {
216 if theme_mode == ThemeAppearanceMode::System {
217 let (light, dark) = (
218 theme_registry.get(LIGHT_THEMES[index]).unwrap(),
219 theme_registry.get(DARK_THEMES[index]).unwrap(),
220 );
221 this.child(
222 ThemePreviewTile::new(light, theme_seed)
223 .style(ThemePreviewStyle::SideBySide(dark)),
224 )
225 } else {
226 this.child(
227 ThemePreviewTile::new(theme.clone(), theme_seed)
228 .style(ThemePreviewStyle::Bordered),
229 )
230 }
231 }),
232 )
233 .child(
234 Label::new(FAMILY_NAMES[index].clone())
235 .color(Color::Muted)
236 .size(LabelSize::Small),
237 )
238 })
239 }
240
241 fn write_mode_change(mode: ThemeAppearanceMode, cx: &mut App) {
242 let fs = <dyn Fs>::global(cx);
243 update_settings_file(fs, cx, move |settings, _cx| {
244 theme_settings::set_mode(settings, mode);
245 });
246 }
247
248 fn write_theme_change(
249 theme: impl Into<Arc<str>>,
250 theme_mode: ThemeAppearanceMode,
251 cx: &mut App,
252 ) {
253 let fs = <dyn Fs>::global(cx);
254 let theme = theme.into();
255 update_settings_file(fs, cx, move |settings, cx| match theme_mode {
256 ThemeAppearanceMode::System => {
257 let (light_theme, dark_theme) =
258 get_theme_family_themes(&theme).unwrap_or((theme.as_ref(), theme.as_ref()));
259
260 settings.theme.theme = Some(settings::ThemeSelection::Dynamic {
261 mode: ThemeAppearanceMode::System,
262 light: ThemeName(light_theme.into()),
263 dark: ThemeName(dark_theme.into()),
264 });
265 }
266 ThemeAppearanceMode::Light => theme_settings::set_theme(
267 settings,
268 theme,
269 Appearance::Light,
270 *SystemAppearance::global(cx),
271 ),
272 ThemeAppearanceMode::Dark => theme_settings::set_theme(
273 settings,
274 theme,
275 Appearance::Dark,
276 *SystemAppearance::global(cx),
277 ),
278 });
279 }
280}
281
282fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement {
283 let fs = <dyn Fs>::global(cx);
284
285 v_flex()
286 .gap_4()
287 .child(
288 SwitchField::new(
289 "onboarding-telemetry-metrics",
290 None::<&str>,
291 Some("Help improve Omega by sending anonymous usage data".into()),
292 if TelemetrySettings::get_global(cx).metrics {
293 ui::ToggleState::Selected
294 } else {
295 ui::ToggleState::Unselected
296 },
297 {
298 let fs = fs.clone();
299 move |selection, _, cx| {
300 let enabled = match selection {
301 ToggleState::Selected => true,
302 ToggleState::Unselected => false,
303 ToggleState::Indeterminate => {
304 return;
305 }
306 };
307
308 update_settings_file(fs.clone(), cx, move |setting, _| {
309 setting.telemetry.get_or_insert_default().metrics = Some(enabled);
310 });
311
312 // This telemetry event shouldn't fire when it's off. If it does we'll be alerted
313 // and can fix it in a timely manner to respect a user's choice.
314 telemetry::event!(
315 "Welcome Page Telemetry Metrics Toggled",
316 options = if enabled { "on" } else { "off" }
317 );
318 }
319 },
320 )
321 .tab_index({
322 *tab_index += 1;
323 *tab_index
324 }),
325 )
326 .child(
327 SwitchField::new(
328 "onboarding-telemetry-crash-reports",
329 None::<&str>,
330 Some(
331 "Help fix Omega by sending crash reports so we can fix critical issues fast"
332 .into(),
333 ),
334 if TelemetrySettings::get_global(cx).diagnostics {
335 ui::ToggleState::Selected
336 } else {
337 ui::ToggleState::Unselected
338 },
339 {
340 let fs = fs.clone();
341 move |selection, _, cx| {
342 let enabled = match selection {
343 ToggleState::Selected => true,
344 ToggleState::Unselected => false,
345 ToggleState::Indeterminate => {
346 return;
347 }
348 };
349
350 update_settings_file(fs.clone(), cx, move |setting, _| {
351 setting.telemetry.get_or_insert_default().diagnostics = Some(enabled);
352 });
353
354 // This telemetry event shouldn't fire when it's off. If it does we'll be alerted
355 // and can fix it in a timely manner to respect a user's choice.
356 telemetry::event!(
357 "Welcome Page Telemetry Diagnostics Toggled",
358 options = if enabled { "on" } else { "off" }
359 );
360 }
361 },
362 )
363 .tab_index({
364 *tab_index += 1;
365 *tab_index
366 }),
367 )
368}
369
370fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
371 let base_keymap = match BaseKeymap::get_global(cx) {
372 BaseKeymap::Zed => Some(0),
373 BaseKeymap::VSCode => Some(1),
374 BaseKeymap::JetBrains => Some(2),
375 BaseKeymap::SublimeText => Some(3),
376 BaseKeymap::Atom => Some(4),
377 BaseKeymap::Emacs => Some(5),
378 BaseKeymap::Cursor => Some(6),
379 BaseKeymap::TextMate => Some(7),
380 BaseKeymap::None => None,
381 };
382
383 return v_flex().gap_2().child(Label::new("Base Keymap")).child(
384 ToggleButtonGroup::two_rows(
385 "base_keymap_selection",
386 [
387 ToggleButtonWithIcon::new("Zed", IconName::AiZed, |_, _, cx| {
388 write_keymap_base(BaseKeymap::Zed, cx);
389 }),
390 ToggleButtonWithIcon::new("VS Code", IconName::EditorVsCode, |_, _, cx| {
391 write_keymap_base(BaseKeymap::VSCode, cx);
392 }),
393 ToggleButtonWithIcon::new("JetBrains", IconName::EditorJetBrains, |_, _, cx| {
394 write_keymap_base(BaseKeymap::JetBrains, cx);
395 }),
396 ToggleButtonWithIcon::new("Sublime Text", IconName::EditorSublime, |_, _, cx| {
397 write_keymap_base(BaseKeymap::SublimeText, cx);
398 }),
399 ],
400 [
401 ToggleButtonWithIcon::new("Atom", IconName::EditorAtom, |_, _, cx| {
402 write_keymap_base(BaseKeymap::Atom, cx);
403 }),
404 ToggleButtonWithIcon::new("Emacs", IconName::EditorEmacs, |_, _, cx| {
405 write_keymap_base(BaseKeymap::Emacs, cx);
406 }),
407 ToggleButtonWithIcon::new("Cursor", IconName::EditorCursor, |_, _, cx| {
408 write_keymap_base(BaseKeymap::Cursor, cx);
409 }),
410 ToggleButtonWithIcon::new("TextMate", IconName::Keyboard, |_, _, cx| {
411 write_keymap_base(BaseKeymap::TextMate, cx);
412 }),
413 ],
414 )
415 .when_some(base_keymap, |this, base_keymap| {
416 this.selected_index(base_keymap)
417 })
418 .full_width()
419 .tab_index(tab_index)
420 .size(ui::ToggleButtonGroupSize::Medium)
421 .style(ui::ToggleButtonGroupStyle::Outlined),
422 );
423
424 fn write_keymap_base(keymap_base: BaseKeymap, cx: &App) {
425 let fs = <dyn Fs>::global(cx);
426
427 update_settings_file(fs, cx, move |setting, _| {
428 setting.base_keymap = Some(keymap_base.into());
429 });
430
431 telemetry::event!("Welcome Keymap Changed", keymap = keymap_base);
432 }
433}
434
435fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
436 let toggle_state = if VimModeSetting::get_global(cx).0 {
437 ui::ToggleState::Selected
438 } else {
439 ui::ToggleState::Unselected
440 };
441 SwitchField::new(
442 "onboarding-vim-mode",
443 Some("Vim Mode"),
444 Some("Coming from Neovim? Use our first-class implementation of Vim Mode".into()),
445 toggle_state,
446 {
447 let fs = <dyn Fs>::global(cx);
448 move |&selection, _, cx| {
449 let vim_mode = match selection {
450 ToggleState::Selected => true,
451 ToggleState::Unselected => false,
452 ToggleState::Indeterminate => {
453 return;
454 }
455 };
456 update_settings_file(fs.clone(), cx, move |setting, _| {
457 setting.vim_mode = Some(vim_mode);
458 });
459
460 telemetry::event!(
461 "Welcome Vim Mode Toggled",
462 options = if vim_mode { "on" } else { "off" },
463 );
464 }
465 },
466 )
467 .tab_index({
468 *tab_index += 1;
469 *tab_index - 1
470 })
471}
472
473fn render_worktree_auto_trust_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
474 let toggle_state = if ProjectSettings::get_global(cx).session.trust_all_worktrees {
475 ui::ToggleState::Selected
476 } else {
477 ui::ToggleState::Unselected
478 };
479
480 let tooltip_description = "Omega can only allow services like language servers, project settings, and MCP servers to run after you mark a new project as trusted.";
481
482 SwitchField::new(
483 "onboarding-auto-trust-worktrees",
484 Some("Trust All Projects By Default"),
485 Some(
486 "Automatically mark all new projects as trusted to unlock all Omega's features".into(),
487 ),
488 toggle_state,
489 {
490 let fs = <dyn Fs>::global(cx);
491 move |&selection, _, cx| {
492 let trust = match selection {
493 ToggleState::Selected => true,
494 ToggleState::Unselected => false,
495 ToggleState::Indeterminate => {
496 return;
497 }
498 };
499 update_settings_file(fs.clone(), cx, move |setting, _| {
500 setting.session.get_or_insert_default().trust_all_worktrees = Some(trust);
501 });
502
503 telemetry::event!(
504 "Welcome Page Worktree Auto Trust Toggled",
505 options = if trust { "on" } else { "off" }
506 );
507 }
508 },
509 )
510 .tab_index({
511 *tab_index += 1;
512 *tab_index - 1
513 })
514 .tooltip(Tooltip::text(tooltip_description))
515}
516
517fn render_setting_import_button(
518 tab_index: isize,
519 label: SharedString,
520 action: &dyn Action,
521 imported: bool,
522) -> impl IntoElement + 'static {
523 let action = action.boxed_clone();
524
525 Button::new(label.clone(), label.clone())
526 .style(ButtonStyle::OutlinedGhost)
527 .size(ButtonSize::Medium)
528 .label_size(LabelSize::Small)
529 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
530 .toggle_state(imported)
531 .tab_index(tab_index)
532 .when(imported, |this| {
533 this.end_icon(Icon::new(IconName::Check).size(IconSize::Small))
534 .color(Color::Success)
535 })
536 .on_click(move |_, window, cx| {
537 telemetry::event!("Welcome Import Settings", import_source = label,);
538 window.dispatch_action(action.boxed_clone(), cx);
539 })
540}
541
542fn render_import_settings_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
543 let import_state = SettingsImportState::global(cx);
544 let imports: [(SharedString, &dyn Action, bool); 2] = [
545 (
546 "VS Code".into(),
547 &ImportVsCodeSettings { skip_prompt: false },
548 import_state.vscode,
549 ),
550 (
551 "Cursor".into(),
552 &ImportCursorSettings { skip_prompt: false },
553 import_state.cursor,
554 ),
555 ];
556
557 let [vscode, cursor] = imports.map(|(label, action, imported)| {
558 *tab_index += 1;
559 render_setting_import_button(*tab_index - 1, label, action, imported)
560 });
561
562 h_flex()
563 .gap_2()
564 .flex_wrap()
565 .justify_between()
566 .child(
567 v_flex()
568 .gap_0p5()
569 .max_w_5_6()
570 .child(Label::new("Import Settings"))
571 .child(
572 Label::new("Automatically pull your settings from other editors")
573 .color(Color::Muted),
574 ),
575 )
576 .child(h_flex().gap_1().child(vscode).child(cursor))
577}
578
579#[derive(Clone, Copy, Debug, PartialEq, Eq)]
580pub(crate) struct FeaturedAgent {
581 pub(crate) id: &'static str,
582 name: &'static str,
583}
584
585pub(crate) const FEATURED_AGENTS: &[FeaturedAgent] = &[
586 FeaturedAgent {
587 id: "claude-acp",
588 name: "Claude",
589 },
590 FeaturedAgent {
591 id: "codex-acp",
592 name: "Codex",
593 },
594 FeaturedAgent {
595 id: "github-copilot-cli",
596 name: "GitHub Copilot",
597 },
598 FeaturedAgent {
599 id: "cursor",
600 name: "Cursor",
601 },
602];
603const AGENT_SETUP_DESCRIPTION: &str = "Install external agents to start a thread. Codex uses its own login and configuration; Omega never copies its credentials.";
604
605fn render_registry_agent_button(
606 agent_id: &'static str,
607 name: SharedString,
608 icon_path: Option<SharedString>,
609 installed: bool,
610 tab_index: isize,
611 cx: &mut App,
612) -> impl IntoElement {
613 let element_id = format!("{}-onboarding", agent_id);
614
615 let icon = match icon_path {
616 Some(icon_path) => Icon::from_external_svg(icon_path),
617 None => Icon::new(IconName::Sparkle),
618 }
619 .size(IconSize::XSmall)
620 .color(Color::Muted);
621
622 let fs = <dyn Fs>::global(cx);
623
624 let state_element = if installed {
625 Icon::new(IconName::Check)
626 .size(IconSize::Small)
627 .color(Color::Success)
628 .into_any_element()
629 } else {
630 Label::new("Install")
631 .size(LabelSize::XSmall)
632 .color(Color::Muted)
633 .into_any_element()
634 };
635
636 let aria_label = if installed {
637 format!("{name} installed")
638 } else {
639 format!("Install {name}")
640 };
641
642 AgentSetupButton::new(element_id)
643 .icon(icon)
644 .name(name)
645 .state(state_element)
646 .aria_label(aria_label)
647 .tab_index(tab_index)
648 .disabled(installed)
649 .on_click(move |_, window, cx| {
650 telemetry::event!("Welcome Agent Install Clicked", agent = agent_id);
651 update_settings_file(fs.clone(), cx, {
652 move |settings, _| {
653 let agent_servers = settings.agent_servers.get_or_insert_default();
654 agent_servers
655 .entry(agent_id.to_string())
656 .or_insert_with(|| CustomAgentServerSettings::Registry {
657 env: Default::default(),
658 default_mode: None,
659 default_config_options: HashMap::default(),
660 favorite_config_option_values: HashMap::default(),
661 });
662 }
663 });
664 window.dispatch_action(
665 Box::new(zed_actions::agent::SelectAgent {
666 agent: agent_id.to_string(),
667 }),
668 cx,
669 );
670 })
671}
672
673fn render_ai_section(tab_index: &mut isize, compact: bool, cx: &mut App) -> impl IntoElement {
674 let registry_agents = AgentRegistryStore::try_global(cx)
675 .map(|store| store.read(cx).agents().to_vec())
676 .unwrap_or_default();
677
678 let installed_agents = cx
679 .global::<SettingsStore>()
680 .get::<AllAgentServersSettings>(None)
681 .clone();
682
683 let column_count = if compact {
684 1
685 } else {
686 FEATURED_AGENTS.len() as u16
687 };
688
689 let grid = FEATURED_AGENTS.iter().fold(
690 div()
691 .w_full()
692 .mt_1p5()
693 .grid()
694 .grid_cols(column_count)
695 .gap_2(),
696 |grid, featured_agent| {
697 let agent_tab_index = *tab_index;
698 *tab_index += 1;
699 let registry_agent = registry_agents
700 .iter()
701 .find(|agent| agent.id().as_ref() == featured_agent.id);
702 let name = registry_agent
703 .map(|agent| agent.name().clone())
704 .unwrap_or_else(|| featured_agent.name.into());
705 let icon_path = registry_agent.and_then(|agent| agent.icon_path().cloned());
706 let is_installed = installed_agents.contains_key(featured_agent.id);
707 grid.child(render_registry_agent_button(
708 featured_agent.id,
709 name,
710 icon_path,
711 is_installed,
712 agent_tab_index,
713 cx,
714 ))
715 },
716 );
717
718 v_flex()
719 .gap_0p5()
720 .child(Label::new("Agent Setup"))
721 .child(Label::new(AGENT_SETUP_DESCRIPTION).color(Color::Muted))
722 .child(grid)
723}
724
725pub(crate) fn render_basics_page(
726 _user_store: &Entity<UserStore>,
727 identity_section: &Entity<IdentitySection>,
728 mode: BasicsPageMode,
729 compact: bool,
730 cx: &mut App,
731) -> impl IntoElement {
732 let mut tab_index = 0;
733
734 v_flex()
735 .id("basics-page")
736 .gap_6()
737 .child(render_identity_section(
738 &mut tab_index,
739 identity_section,
740 mode.identity_presentation(),
741 cx,
742 ))
743 .child(render_theme_section(&mut tab_index, compact, cx))
744 .child(render_base_keymap_section(&mut tab_index, cx))
745 .child(render_ai_section(&mut tab_index, compact, cx))
746 .child(render_import_settings_section(&mut tab_index, cx))
747 .child(render_vim_mode_switch(&mut tab_index, cx))
748 .child(render_worktree_auto_trust_switch(&mut tab_index, cx))
749 .child(Divider::horizontal().color(ui::DividerColor::BorderVariant))
750 .child(render_telemetry_section(&mut tab_index, cx))
751}
752
753#[cfg(test)]
754mod tests {
755 use super::*;
756
757 #[test]
758 fn onboarding_mode_selects_only_the_identity_presentation() {
759 assert_eq!(
760 BasicsPageMode::FirstRun.identity_presentation(),
761 IdentitySectionPresentation::Full
762 );
763 assert_eq!(
764 BasicsPageMode::EditorSetup.identity_presentation(),
765 IdentitySectionPresentation::Compact
766 );
767 }
768
769 #[test]
770 fn omega_onboarding_preserves_theme_families() {
771 assert_eq!(LIGHT_THEMES, ["Aiur", "Ayu Light", "Gruvbox Light"]);
772 assert_eq!(DARK_THEMES, ["Aiur", "Ayu Dark", "Gruvbox Dark"]);
773 assert_eq!(
774 FAMILY_NAMES.map(|name| name.to_string()),
775 ["Aiur", "Ayu", "Gruvbox"]
776 );
777 assert_eq!(
778 get_theme_family_themes("Ayu Dark"),
779 Some(("Ayu Light", "Ayu Dark"))
780 );
781 // Aiur is dark-only, so both appearances resolve to the same theme.
782 assert_eq!(get_theme_family_themes("Aiur"), Some(("Aiur", "Aiur")));
783 }
784
785 #[test]
786 fn omega_onboarding_preserves_featured_registry_agents() {
787 assert_eq!(
788 FEATURED_AGENTS,
789 [
790 FeaturedAgent {
791 id: "claude-acp",
792 name: "Claude",
793 },
794 FeaturedAgent {
795 id: "codex-acp",
796 name: "Codex",
797 },
798 FeaturedAgent {
799 id: "github-copilot-cli",
800 name: "GitHub Copilot",
801 },
802 FeaturedAgent {
803 id: "cursor",
804 name: "Cursor",
805 },
806 ]
807 );
808 assert!(AGENT_SETUP_DESCRIPTION.contains("Codex uses its own login and configuration"));
809 assert!(AGENT_SETUP_DESCRIPTION.contains("Omega never copies its credentials"));
810 }
811}
812