basics_page.rs

  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}