Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:41:25.713Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

basics_page.rs

812 lines · 29.4 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.