workspace: Display a launchpad page when in an empty window & add it as a `restore_on_startup` value (#44048)

Simon Pham and Danilo Leal created

Hi,

This PR fixes nothing. I just miss the option to open recent projects
quickly upon opening Zed, so I made this. Hope I can see it soon in
Preview channel.
If there is any suggestion, just comment. I will take it seriously.

Thank you!

|ui|before|after|
|-|-|-|
|empty pane|<img width="1571" height="941" alt="Screenshot 2025-12-03 at
12 39 25"
src="https://github.com/user-attachments/assets/753cbbc5-ddca-4143-aed8-0832ca59b8e7"
/>|<img width="1604" height="952" alt="Screenshot 2025-12-03 at 12 34
03"
src="https://github.com/user-attachments/assets/2f591d48-ef86-4886-a220-0f78a0bcad92"
/>|
|new window|<img width="1571" height="941" alt="Screenshot 2025-12-03 at
12 39 21"
src="https://github.com/user-attachments/assets/a3a1b110-a278-4f8b-980e-75f5bc96b609"
/>|<img width="1604" height="952" alt="Screenshot 2025-12-04 at 10 43
17"
src="https://github.com/user-attachments/assets/74a00d91-50da-41a2-8fc2-24511d548063"
/>|

---

Release Notes:

- Added a new value to the `restore_on_startup` setting called
`launchpad`. This value makes Zed open with a variant of the welcome
screen ("the launchpad") upon startup. Additionally, this same page
variant is now also what is displayed if you close all tabs in an
existing window that doesn't contain any folders open. The launchpad
page shows you up to 5 recent projects, making it easy to open something
you were working recently.

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>

Change summary

Cargo.lock                                              |   2 
assets/keymaps/default-linux.json                       |   5 
assets/keymaps/default-macos.json                       |   5 
assets/keymaps/default-windows.json                     |   5 
crates/editor/src/editor.rs                             |   9 
crates/migrator/src/migrations.rs                       |   6 
crates/migrator/src/migrations/m_2025_12_15/settings.rs |  52 +
crates/migrator/src/migrator.rs                         |   8 
crates/onboarding/Cargo.toml                            |   1 
crates/onboarding/src/onboarding.rs                     |  22 
crates/onboarding/src/welcome.rs                        | 443 --------
crates/settings/src/settings_content/workspace.rs       |   9 
crates/title_bar/src/title_bar.rs                       |   2 
crates/workspace/Cargo.toml                             |   1 
crates/workspace/src/pane.rs                            |  26 
crates/workspace/src/welcome.rs                         | 568 +++++++++++
crates/workspace/src/workspace.rs                       |   1 
crates/zed/src/main.rs                                  |   8 
crates/zed/src/zed.rs                                   |   1 
crates/zed_actions/src/lib.rs                           |   2 
docs/src/configuring-zed.md                             |  10 
21 files changed, 711 insertions(+), 475 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -10843,7 +10843,6 @@ dependencies = [
  "documented",
  "fs",
  "fuzzy",
- "git",
  "gpui",
  "menu",
  "notifications",
@@ -20096,6 +20095,7 @@ dependencies = [
  "feature_flags",
  "fs",
  "futures 0.3.31",
+ "git",
  "gpui",
  "http_client",
  "itertools 0.14.0",

assets/keymaps/default-linux.json 🔗

@@ -1263,6 +1263,11 @@
       "ctrl-+": ["zed::IncreaseUiFontSize", { "persist": false }],
       "ctrl--": ["zed::DecreaseUiFontSize", { "persist": false }],
       "ctrl-0": ["zed::ResetUiFontSize", { "persist": false }],
+      "ctrl-1": ["welcome::OpenRecentProject", 0],
+      "ctrl-2": ["welcome::OpenRecentProject", 1],
+      "ctrl-3": ["welcome::OpenRecentProject", 2],
+      "ctrl-4": ["welcome::OpenRecentProject", 3],
+      "ctrl-5": ["welcome::OpenRecentProject", 4],
     },
   },
   {

assets/keymaps/default-macos.json 🔗

@@ -1366,6 +1366,11 @@
       "cmd-+": ["zed::IncreaseUiFontSize", { "persist": false }],
       "cmd--": ["zed::DecreaseUiFontSize", { "persist": false }],
       "cmd-0": ["zed::ResetUiFontSize", { "persist": false }],
+      "cmd-1": ["welcome::OpenRecentProject", 0],
+      "cmd-2": ["welcome::OpenRecentProject", 1],
+      "cmd-3": ["welcome::OpenRecentProject", 2],
+      "cmd-4": ["welcome::OpenRecentProject", 3],
+      "cmd-5": ["welcome::OpenRecentProject", 4],
     },
   },
   {

assets/keymaps/default-windows.json 🔗

@@ -1295,6 +1295,11 @@
       "ctrl-+": ["zed::IncreaseUiFontSize", { "persist": false }],
       "ctrl--": ["zed::DecreaseUiFontSize", { "persist": false }],
       "ctrl-0": ["zed::ResetUiFontSize", { "persist": false }],
+      "ctrl-1": ["welcome::OpenRecentProject", 0],
+      "ctrl-2": ["welcome::OpenRecentProject", 1],
+      "ctrl-3": ["welcome::OpenRecentProject", 2],
+      "ctrl-4": ["welcome::OpenRecentProject", 3],
+      "ctrl-5": ["welcome::OpenRecentProject", 4],
     },
   },
   {

crates/editor/src/editor.rs 🔗

@@ -3427,7 +3427,8 @@ impl Editor {
                 data.selections = inmemory_selections;
             });
 
-            if WorkspaceSettings::get(None, cx).restore_on_startup != RestoreOnStartupBehavior::None
+            if WorkspaceSettings::get(None, cx).restore_on_startup
+                != RestoreOnStartupBehavior::EmptyTab
                 && let Some(workspace_id) = self.workspace_serialization_id(cx)
             {
                 let snapshot = self.buffer().read(cx).snapshot(cx);
@@ -3467,7 +3468,8 @@ impl Editor {
         use text::ToPoint as _;
 
         if self.mode.is_minimap()
-            || WorkspaceSettings::get(None, cx).restore_on_startup == RestoreOnStartupBehavior::None
+            || WorkspaceSettings::get(None, cx).restore_on_startup
+                == RestoreOnStartupBehavior::EmptyTab
         {
             return;
         }
@@ -23163,7 +23165,8 @@ impl Editor {
     ) {
         if self.buffer_kind(cx) == ItemBufferKind::Singleton
             && !self.mode.is_minimap()
-            && WorkspaceSettings::get(None, cx).restore_on_startup != RestoreOnStartupBehavior::None
+            && WorkspaceSettings::get(None, cx).restore_on_startup
+                != RestoreOnStartupBehavior::EmptyTab
         {
             let buffer_snapshot = OnceCell::new();
 

crates/migrator/src/migrations.rs 🔗

@@ -165,3 +165,9 @@ pub(crate) mod m_2025_12_08 {
 
     pub(crate) use keymap::KEYMAP_PATTERNS;
 }
+
+pub(crate) mod m_2025_12_15 {
+    mod settings;
+
+    pub(crate) use settings::SETTINGS_PATTERNS;
+}

crates/migrator/src/migrations/m_2025_12_15/settings.rs 🔗

@@ -0,0 +1,52 @@
+use std::ops::Range;
+use tree_sitter::{Query, QueryMatch};
+
+use crate::MigrationPatterns;
+use crate::patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN;
+
+pub const SETTINGS_PATTERNS: MigrationPatterns = &[(
+    SETTINGS_NESTED_KEY_VALUE_PATTERN,
+    rename_restore_on_startup_values,
+)];
+
+fn rename_restore_on_startup_values(
+    contents: &str,
+    mat: &QueryMatch,
+    query: &Query,
+) -> Option<(Range<usize>, String)> {
+    if !is_restore_on_startup_setting(contents, mat, query) {
+        return None;
+    }
+
+    let setting_value_ix = query.capture_index_for_name("setting_value")?;
+    let setting_value_range = mat
+        .nodes_for_capture_index(setting_value_ix)
+        .next()?
+        .byte_range();
+    let setting_value = contents.get(setting_value_range.clone())?;
+
+    // The value includes quotes, so we check for the quoted string
+    let new_value = match setting_value.trim() {
+        "\"none\"" => "\"empty_tab\"",
+        "\"welcome\"" => "\"launchpad\"",
+        _ => return None,
+    };
+
+    Some((setting_value_range, new_value.to_string()))
+}
+
+fn is_restore_on_startup_setting(contents: &str, mat: &QueryMatch, query: &Query) -> bool {
+    // Check that the parent key is "workspace" (since restore_on_startup is under workspace settings)
+    // Actually, restore_on_startup can be at the root level too, so we need to handle both cases
+    // The SETTINGS_NESTED_KEY_VALUE_PATTERN captures parent_key and setting_name
+
+    let setting_name_ix = match query.capture_index_for_name("setting_name") {
+        Some(ix) => ix,
+        None => return false,
+    };
+    let setting_name_range = match mat.nodes_for_capture_index(setting_name_ix).next() {
+        Some(node) => node.byte_range(),
+        None => return false,
+    };
+    contents.get(setting_name_range) == Some("restore_on_startup")
+}

crates/migrator/src/migrator.rs 🔗

@@ -232,6 +232,10 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
             &SETTINGS_QUERY_2025_11_20,
         ),
         MigrationType::Json(migrations::m_2025_11_25::remove_context_server_source),
+        MigrationType::TreeSitter(
+            migrations::m_2025_12_15::SETTINGS_PATTERNS,
+            &SETTINGS_QUERY_2025_12_15,
+        ),
     ];
     run_migrations(text, migrations)
 }
@@ -366,6 +370,10 @@ define_query!(
     KEYMAP_QUERY_2025_12_08,
     migrations::m_2025_12_08::KEYMAP_PATTERNS
 );
+define_query!(
+    SETTINGS_QUERY_2025_12_15,
+    migrations::m_2025_12_15::SETTINGS_PATTERNS
+);
 
 // custom query
 static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {

crates/onboarding/Cargo.toml 🔗

@@ -22,7 +22,6 @@ db.workspace = true
 documented.workspace = true
 fs.workspace = true
 fuzzy.workspace = true
-git.workspace = true
 gpui.workspace = true
 menu.workspace = true
 notifications.workspace = true

crates/onboarding/src/onboarding.rs 🔗

@@ -1,5 +1,4 @@
-pub use crate::welcome::ShowWelcome;
-use crate::{multibuffer_hint::MultibufferHint, welcome::WelcomePage};
+use crate::multibuffer_hint::MultibufferHint;
 use client::{Client, UserStore, zed_urls};
 use db::kvp::KEY_VALUE_STORE;
 use fs::Fs;
@@ -17,6 +16,8 @@ use ui::{
     Divider, KeyBinding, ParentElement as _, StatefulInteractiveElement, Vector, VectorName,
     WithScrollbar as _, prelude::*, rems_from_px,
 };
+pub use workspace::welcome::ShowWelcome;
+use workspace::welcome::WelcomePage;
 use workspace::{
     AppState, Workspace, WorkspaceId,
     dock::DockPosition,
@@ -24,12 +25,12 @@ use workspace::{
     notifications::NotifyResultExt as _,
     open_new, register_serializable_item, with_active_or_new_workspace,
 };
+use zed_actions::OpenOnboarding;
 
 mod base_keymap_picker;
 mod basics_page;
 pub mod multibuffer_hint;
 mod theme_preview;
-mod welcome;
 
 /// Imports settings from Visual Studio Code.
 #[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
@@ -52,14 +53,6 @@ pub struct ImportCursorSettings {
 pub const FIRST_OPEN: &str = "first_open";
 pub const DOCS_URL: &str = "https://zed.dev/docs/";
 
-actions!(
-    zed,
-    [
-        /// Opens the onboarding view.
-        OpenOnboarding
-    ]
-);
-
 actions!(
     onboarding,
     [
@@ -121,7 +114,8 @@ pub fn init(cx: &mut App) {
                     if let Some(existing) = existing {
                         workspace.activate_item(&existing, true, true, window, cx);
                     } else {
-                        let settings_page = WelcomePage::new(window, cx);
+                        let settings_page = cx
+                            .new(|cx| WelcomePage::new(workspace.weak_handle(), false, window, cx));
                         workspace.add_item_to_active_pane(
                             Box::new(settings_page),
                             None,
@@ -427,7 +421,9 @@ fn go_to_welcome_page(cx: &mut App) {
             if let Some(idx) = idx {
                 pane.activate_item(idx, true, true, window, cx);
             } else {
-                let item = Box::new(WelcomePage::new(window, cx));
+                let item = Box::new(
+                    cx.new(|cx| WelcomePage::new(workspace.weak_handle(), false, window, cx)),
+                );
                 pane.add_item(item, true, true, Some(onboarding_idx), window, cx);
             }
 

crates/onboarding/src/welcome.rs 🔗

@@ -1,443 +0,0 @@
-use gpui::{
-    Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
-    ParentElement, Render, Styled, Task, Window, actions,
-};
-use menu::{SelectNext, SelectPrevious};
-use ui::{ButtonLike, Divider, DividerColor, KeyBinding, Vector, VectorName, prelude::*};
-use workspace::{
-    NewFile, Open,
-    item::{Item, ItemEvent},
-    with_active_or_new_workspace,
-};
-use zed_actions::{Extensions, OpenSettings, agent, command_palette};
-
-use crate::{Onboarding, OpenOnboarding};
-
-actions!(
-    zed,
-    [
-        /// Show the Zed welcome screen
-        ShowWelcome
-    ]
-);
-
-const CONTENT: (Section<4>, Section<3>) = (
-    Section {
-        title: "Get Started",
-        entries: [
-            SectionEntry {
-                icon: IconName::Plus,
-                title: "New File",
-                action: &NewFile,
-            },
-            SectionEntry {
-                icon: IconName::FolderOpen,
-                title: "Open Project",
-                action: &Open,
-            },
-            SectionEntry {
-                icon: IconName::CloudDownload,
-                title: "Clone Repository",
-                action: &git::Clone,
-            },
-            SectionEntry {
-                icon: IconName::ListCollapse,
-                title: "Open Command Palette",
-                action: &command_palette::Toggle,
-            },
-        ],
-    },
-    Section {
-        title: "Configure",
-        entries: [
-            SectionEntry {
-                icon: IconName::Settings,
-                title: "Open Settings",
-                action: &OpenSettings,
-            },
-            SectionEntry {
-                icon: IconName::ZedAssistant,
-                title: "View AI Settings",
-                action: &agent::OpenSettings,
-            },
-            SectionEntry {
-                icon: IconName::Blocks,
-                title: "Explore Extensions",
-                action: &Extensions {
-                    category_filter: None,
-                    id: None,
-                },
-            },
-        ],
-    },
-);
-
-struct Section<const COLS: usize> {
-    title: &'static str,
-    entries: [SectionEntry; COLS],
-}
-
-impl<const COLS: usize> Section<COLS> {
-    fn render(self, index_offset: usize, focus: &FocusHandle, cx: &mut App) -> impl IntoElement {
-        v_flex()
-            .min_w_full()
-            .child(
-                h_flex()
-                    .px_1()
-                    .mb_2()
-                    .gap_2()
-                    .child(
-                        Label::new(self.title.to_ascii_uppercase())
-                            .buffer_font(cx)
-                            .color(Color::Muted)
-                            .size(LabelSize::XSmall),
-                    )
-                    .child(Divider::horizontal().color(DividerColor::BorderVariant)),
-            )
-            .children(
-                self.entries
-                    .iter()
-                    .enumerate()
-                    .map(|(index, entry)| entry.render(index_offset + index, focus, cx)),
-            )
-    }
-}
-
-struct SectionEntry {
-    icon: IconName,
-    title: &'static str,
-    action: &'static dyn Action,
-}
-
-impl SectionEntry {
-    fn render(&self, button_index: usize, focus: &FocusHandle, cx: &App) -> impl IntoElement {
-        ButtonLike::new(("onboarding-button-id", button_index))
-            .tab_index(button_index as isize)
-            .full_width()
-            .size(ButtonSize::Medium)
-            .child(
-                h_flex()
-                    .w_full()
-                    .justify_between()
-                    .child(
-                        h_flex()
-                            .gap_2()
-                            .child(
-                                Icon::new(self.icon)
-                                    .color(Color::Muted)
-                                    .size(IconSize::XSmall),
-                            )
-                            .child(Label::new(self.title)),
-                    )
-                    .child(
-                        KeyBinding::for_action_in(self.action, focus, cx).size(rems_from_px(12.)),
-                    ),
-            )
-            .on_click(|_, window, cx| window.dispatch_action(self.action.boxed_clone(), cx))
-    }
-}
-
-pub struct WelcomePage {
-    focus_handle: FocusHandle,
-}
-
-impl WelcomePage {
-    fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
-        window.focus_next();
-        cx.notify();
-    }
-
-    fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
-        window.focus_prev();
-        cx.notify();
-    }
-}
-
-impl Render for WelcomePage {
-    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let (first_section, second_section) = CONTENT;
-        let first_section_entries = first_section.entries.len();
-        let last_index = first_section_entries + second_section.entries.len();
-
-        h_flex()
-            .size_full()
-            .justify_center()
-            .overflow_hidden()
-            .bg(cx.theme().colors().editor_background)
-            .key_context("Welcome")
-            .track_focus(&self.focus_handle(cx))
-            .on_action(cx.listener(Self::select_previous))
-            .on_action(cx.listener(Self::select_next))
-            .child(
-                h_flex()
-                    .px_12()
-                    .py_40()
-                    .size_full()
-                    .relative()
-                    .max_w(px(1100.))
-                    .child(
-                        div()
-                            .size_full()
-                            .max_w_128()
-                            .mx_auto()
-                            .child(
-                                h_flex()
-                                    .w_full()
-                                    .justify_center()
-                                    .gap_4()
-                                    .child(Vector::square(VectorName::ZedLogo, rems(2.)))
-                                    .child(
-                                        div().child(Headline::new("Welcome to Zed")).child(
-                                            Label::new("The editor for what's next")
-                                                .size(LabelSize::Small)
-                                                .color(Color::Muted)
-                                                .italic(),
-                                        ),
-                                    ),
-                            )
-                            .child(
-                                v_flex()
-                                    .mt_10()
-                                    .gap_6()
-                                    .child(first_section.render(
-                                        Default::default(),
-                                        &self.focus_handle,
-                                        cx,
-                                    ))
-                                    .child(second_section.render(
-                                        first_section_entries,
-                                        &self.focus_handle,
-                                        cx,
-                                    ))
-                                    .child(
-                                        h_flex()
-                                            .w_full()
-                                            .pt_4()
-                                            .justify_center()
-                                            // We call this a hack
-                                            .rounded_b_xs()
-                                            .border_t_1()
-                                            .border_color(cx.theme().colors().border.opacity(0.6))
-                                            .border_dashed()
-                                            .child(
-                                                    Button::new("welcome-exit", "Return to Setup")
-                                                        .tab_index(last_index as isize)
-                                                        .full_width()
-                                                        .label_size(LabelSize::XSmall)
-                                                        .on_click(|_, window, cx| {
-                                                            window.dispatch_action(
-                                                                OpenOnboarding.boxed_clone(),
-                                                                cx,
-                                                            );
-
-                                                            with_active_or_new_workspace(cx, |workspace, window, cx| {
-                                                                let Some((welcome_id, welcome_idx)) = workspace
-                                                                    .active_pane()
-                                                                    .read(cx)
-                                                                    .items()
-                                                                    .enumerate()
-                                                                    .find_map(|(idx, item)| {
-                                                                        let _ = item.downcast::<WelcomePage>()?;
-                                                                        Some((item.item_id(), idx))
-                                                                    })
-                                                                else {
-                                                                    return;
-                                                                };
-
-                                                                workspace.active_pane().update(cx, |pane, cx| {
-                                                                    // Get the index here to get around the borrow checker
-                                                                    let idx = pane.items().enumerate().find_map(
-                                                                        |(idx, item)| {
-                                                                            let _ =
-                                                                                item.downcast::<Onboarding>()?;
-                                                                            Some(idx)
-                                                                        },
-                                                                    );
-
-                                                                    if let Some(idx) = idx {
-                                                                        pane.activate_item(
-                                                                            idx, true, true, window, cx,
-                                                                        );
-                                                                    } else {
-                                                                        let item =
-                                                                            Box::new(Onboarding::new(workspace, cx));
-                                                                        pane.add_item(
-                                                                            item,
-                                                                            true,
-                                                                            true,
-                                                                            Some(welcome_idx),
-                                                                            window,
-                                                                            cx,
-                                                                        );
-                                                                    }
-
-                                                                    pane.remove_item(
-                                                                        welcome_id,
-                                                                        false,
-                                                                        false,
-                                                                        window,
-                                                                        cx,
-                                                                    );
-                                                                });
-                                                            });
-                                                        }),
-                                                ),
-                                    ),
-                            ),
-                    ),
-            )
-    }
-}
-
-impl WelcomePage {
-    pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
-        cx.new(|cx| {
-            let focus_handle = cx.focus_handle();
-            cx.on_focus(&focus_handle, window, |_, _, cx| cx.notify())
-                .detach();
-
-            WelcomePage { focus_handle }
-        })
-    }
-}
-
-impl EventEmitter<ItemEvent> for WelcomePage {}
-
-impl Focusable for WelcomePage {
-    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
-        self.focus_handle.clone()
-    }
-}
-
-impl Item for WelcomePage {
-    type Event = ItemEvent;
-
-    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
-        "Welcome".into()
-    }
-
-    fn telemetry_event_text(&self) -> Option<&'static str> {
-        Some("New Welcome Page Opened")
-    }
-
-    fn show_toolbar(&self) -> bool {
-        false
-    }
-
-    fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
-        f(*event)
-    }
-}
-
-impl workspace::SerializableItem for WelcomePage {
-    fn serialized_item_kind() -> &'static str {
-        "WelcomePage"
-    }
-
-    fn cleanup(
-        workspace_id: workspace::WorkspaceId,
-        alive_items: Vec<workspace::ItemId>,
-        _window: &mut Window,
-        cx: &mut App,
-    ) -> Task<gpui::Result<()>> {
-        workspace::delete_unloaded_items(
-            alive_items,
-            workspace_id,
-            "welcome_pages",
-            &persistence::WELCOME_PAGES,
-            cx,
-        )
-    }
-
-    fn deserialize(
-        _project: Entity<project::Project>,
-        _workspace: gpui::WeakEntity<workspace::Workspace>,
-        workspace_id: workspace::WorkspaceId,
-        item_id: workspace::ItemId,
-        window: &mut Window,
-        cx: &mut App,
-    ) -> Task<gpui::Result<Entity<Self>>> {
-        if persistence::WELCOME_PAGES
-            .get_welcome_page(item_id, workspace_id)
-            .ok()
-            .is_some_and(|is_open| is_open)
-        {
-            window.spawn(cx, async move |cx| cx.update(WelcomePage::new))
-        } else {
-            Task::ready(Err(anyhow::anyhow!("No welcome page to deserialize")))
-        }
-    }
-
-    fn serialize(
-        &mut self,
-        workspace: &mut workspace::Workspace,
-        item_id: workspace::ItemId,
-        _closing: bool,
-        _window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Option<Task<gpui::Result<()>>> {
-        let workspace_id = workspace.database_id()?;
-        Some(cx.background_spawn(async move {
-            persistence::WELCOME_PAGES
-                .save_welcome_page(item_id, workspace_id, true)
-                .await
-        }))
-    }
-
-    fn should_serialize(&self, event: &Self::Event) -> bool {
-        event == &ItemEvent::UpdateTab
-    }
-}
-
-mod persistence {
-    use db::{
-        query,
-        sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
-        sqlez_macros::sql,
-    };
-    use workspace::WorkspaceDb;
-
-    pub struct WelcomePagesDb(ThreadSafeConnection);
-
-    impl Domain for WelcomePagesDb {
-        const NAME: &str = stringify!(WelcomePagesDb);
-
-        const MIGRATIONS: &[&str] = (&[sql!(
-                    CREATE TABLE welcome_pages (
-                        workspace_id INTEGER,
-                        item_id INTEGER UNIQUE,
-                        is_open INTEGER DEFAULT FALSE,
-
-                        PRIMARY KEY(workspace_id, item_id),
-                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
-                        ON DELETE CASCADE
-                    ) STRICT;
-        )]);
-    }
-
-    db::static_connection!(WELCOME_PAGES, WelcomePagesDb, [WorkspaceDb]);
-
-    impl WelcomePagesDb {
-        query! {
-            pub async fn save_welcome_page(
-                item_id: workspace::ItemId,
-                workspace_id: workspace::WorkspaceId,
-                is_open: bool
-            ) -> Result<()> {
-                INSERT OR REPLACE INTO welcome_pages(item_id, workspace_id, is_open)
-                VALUES (?, ?, ?)
-            }
-        }
-
-        query! {
-            pub fn get_welcome_page(
-                item_id: workspace::ItemId,
-                workspace_id: workspace::WorkspaceId
-            ) -> Result<bool> {
-                SELECT is_open
-                FROM welcome_pages
-                WHERE item_id = ? AND workspace_id = ?
-            }
-        }
-    }
-}

crates/settings/src/settings_content/workspace.rs 🔗

@@ -42,7 +42,7 @@ pub struct WorkspaceSettingsContent {
     /// Default: off
     pub autosave: Option<AutosaveSetting>,
     /// Controls previous session restoration in freshly launched Zed instance.
-    /// Values: none, last_workspace, last_session
+    /// Values: empty_tab, last_workspace, last_session, launchpad
     /// Default: last_session
     pub restore_on_startup: Option<RestoreOnStartupBehavior>,
     /// Whether to attempt to restore previous file's state when opening it again.
@@ -382,13 +382,16 @@ impl CloseWindowWhenNoItems {
 )]
 #[serde(rename_all = "snake_case")]
 pub enum RestoreOnStartupBehavior {
-    /// Always start with an empty editor
-    None,
+    /// Always start with an empty editor tab
+    #[serde(alias = "none")]
+    EmptyTab,
     /// Restore the workspace that was closed last.
     LastWorkspace,
     /// Restore all workspaces that were open when quitting Zed.
     #[default]
     LastSession,
+    /// Show the launchpad with recent projects (no tabs).
+    Launchpad,
 }
 
 #[with_fallible_options]

crates/title_bar/src/title_bar.rs 🔗

@@ -479,7 +479,7 @@ impl TitleBar {
         let name = if let Some(name) = name {
             util::truncate_and_trailoff(&name, MAX_PROJECT_NAME_LENGTH)
         } else {
-            "Open recent project".to_string()
+            "Open Recent Project".to_string()
         };
 
         Button::new("project_name_trigger", name)

crates/workspace/Cargo.toml 🔗

@@ -38,6 +38,7 @@ db.workspace = true
 feature_flags.workspace = true
 fs.workspace = true
 futures.workspace = true
+git.workspace = true
 gpui.workspace = true
 http_client.workspace = true
 itertools.workspace = true

crates/workspace/src/pane.rs 🔗

@@ -47,10 +47,9 @@ use std::{
 };
 use theme::ThemeSettings;
 use ui::{
-    ButtonSize, Color, ContextMenu, ContextMenuEntry, ContextMenuItem, DecoratedIcon, IconButton,
-    IconButtonShape, IconDecoration, IconDecorationKind, IconName, IconSize, Indicator, Label,
-    PopoverMenu, PopoverMenuHandle, Tab, TabBar, TabPosition, Tooltip, prelude::*,
-    right_click_menu,
+    ContextMenu, ContextMenuEntry, ContextMenuItem, DecoratedIcon, IconButtonShape, IconDecoration,
+    IconDecorationKind, Indicator, PopoverMenu, PopoverMenuHandle, Tab, TabBar, TabPosition,
+    Tooltip, prelude::*, right_click_menu,
 };
 use util::{ResultExt, debug_panic, maybe, paths::PathStyle, truncate_and_remove_front};
 
@@ -398,6 +397,7 @@ pub struct Pane {
     diagnostic_summary_update: Task<()>,
     /// If a certain project item wants to get recreated with specific data, it can persist its data before the recreation here.
     pub project_item_restoration_data: HashMap<ProjectItemKind, Box<dyn Any + Send>>,
+    welcome_page: Option<Entity<crate::welcome::WelcomePage>>,
 
     pub in_center_group: bool,
     pub is_upper_left: bool,
@@ -546,6 +546,7 @@ impl Pane {
             zoom_out_on_close: true,
             diagnostic_summary_update: Task::ready(()),
             project_item_restoration_data: HashMap::default(),
+            welcome_page: None,
             in_center_group: false,
             is_upper_left: false,
             is_upper_right: false,
@@ -635,6 +636,10 @@ impl Pane {
                 self.last_focus_handle_by_item
                     .insert(active_item.item_id(), focused.downgrade());
             }
+        } else if let Some(welcome_page) = self.welcome_page.as_ref() {
+            if self.focus_handle.is_focused(window) {
+                welcome_page.read(cx).focus_handle(cx).focus(window);
+            }
         }
     }
 
@@ -4061,10 +4066,15 @@ impl Render for Pane {
                             if has_worktrees {
                                 placeholder
                             } else {
-                                placeholder.child(
-                                    Label::new("Open a file or project to get started.")
-                                        .color(Color::Muted),
-                                )
+                                if self.welcome_page.is_none() {
+                                    let workspace = self.workspace.clone();
+                                    self.welcome_page = Some(cx.new(|cx| {
+                                        crate::welcome::WelcomePage::new(
+                                            workspace, true, window, cx,
+                                        )
+                                    }));
+                                }
+                                placeholder.child(self.welcome_page.clone().unwrap())
                             }
                         }
                     })

crates/workspace/src/welcome.rs 🔗

@@ -0,0 +1,568 @@
+use crate::{
+    NewFile, Open, PathList, SerializedWorkspaceLocation, WORKSPACE_DB, Workspace, WorkspaceId,
+    item::{Item, ItemEvent},
+};
+use git::Clone as GitClone;
+use gpui::WeakEntity;
+use gpui::{
+    Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
+    ParentElement, Render, Styled, Task, Window, actions,
+};
+use menu::{SelectNext, SelectPrevious};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use ui::{ButtonLike, Divider, DividerColor, KeyBinding, Vector, VectorName, prelude::*};
+use util::ResultExt;
+use zed_actions::{Extensions, OpenOnboarding, OpenSettings, agent, command_palette};
+
+#[derive(PartialEq, Clone, Debug, Deserialize, Serialize, JsonSchema, Action)]
+#[action(namespace = welcome)]
+#[serde(transparent)]
+pub struct OpenRecentProject {
+    pub index: usize,
+}
+
+actions!(
+    zed,
+    [
+        /// Show the Zed welcome screen
+        ShowWelcome
+    ]
+);
+
+#[derive(IntoElement)]
+struct SectionHeader {
+    title: SharedString,
+}
+
+impl SectionHeader {
+    fn new(title: impl Into<SharedString>) -> Self {
+        Self {
+            title: title.into(),
+        }
+    }
+}
+
+impl RenderOnce for SectionHeader {
+    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+        h_flex()
+            .px_1()
+            .mb_2()
+            .gap_2()
+            .child(
+                Label::new(self.title.to_ascii_uppercase())
+                    .buffer_font(cx)
+                    .color(Color::Muted)
+                    .size(LabelSize::XSmall),
+            )
+            .child(Divider::horizontal().color(DividerColor::BorderVariant))
+    }
+}
+
+#[derive(IntoElement)]
+struct SectionButton {
+    label: SharedString,
+    icon: IconName,
+    action: Box<dyn Action>,
+    tab_index: usize,
+    focus_handle: FocusHandle,
+}
+
+impl SectionButton {
+    fn new(
+        label: impl Into<SharedString>,
+        icon: IconName,
+        action: &dyn Action,
+        tab_index: usize,
+        focus_handle: FocusHandle,
+    ) -> Self {
+        Self {
+            label: label.into(),
+            icon,
+            action: action.boxed_clone(),
+            tab_index,
+            focus_handle,
+        }
+    }
+}
+
+impl RenderOnce for SectionButton {
+    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+        let id = format!("onb-button-{}", self.label);
+        let action_ref: &dyn Action = &*self.action;
+
+        ButtonLike::new(id)
+            .tab_index(self.tab_index as isize)
+            .full_width()
+            .size(ButtonSize::Medium)
+            .child(
+                h_flex()
+                    .w_full()
+                    .justify_between()
+                    .child(
+                        h_flex()
+                            .gap_2()
+                            .child(
+                                Icon::new(self.icon)
+                                    .color(Color::Muted)
+                                    .size(IconSize::Small),
+                            )
+                            .child(Label::new(self.label)),
+                    )
+                    .child(
+                        KeyBinding::for_action_in(action_ref, &self.focus_handle, cx)
+                            .size(rems_from_px(12.)),
+                    ),
+            )
+            .on_click(move |_, window, cx| window.dispatch_action(self.action.boxed_clone(), cx))
+    }
+}
+
+struct SectionEntry {
+    icon: IconName,
+    title: &'static str,
+    action: &'static dyn Action,
+}
+
+impl SectionEntry {
+    fn render(&self, button_index: usize, focus: &FocusHandle, _cx: &App) -> impl IntoElement {
+        SectionButton::new(
+            self.title,
+            self.icon,
+            self.action,
+            button_index,
+            focus.clone(),
+        )
+    }
+}
+
+const CONTENT: (Section<4>, Section<3>) = (
+    Section {
+        title: "Get Started",
+        entries: [
+            SectionEntry {
+                icon: IconName::Plus,
+                title: "New File",
+                action: &NewFile,
+            },
+            SectionEntry {
+                icon: IconName::FolderOpen,
+                title: "Open Project",
+                action: &Open,
+            },
+            SectionEntry {
+                icon: IconName::CloudDownload,
+                title: "Clone Repository",
+                action: &GitClone,
+            },
+            SectionEntry {
+                icon: IconName::ListCollapse,
+                title: "Open Command Palette",
+                action: &command_palette::Toggle,
+            },
+        ],
+    },
+    Section {
+        title: "Configure",
+        entries: [
+            SectionEntry {
+                icon: IconName::Settings,
+                title: "Open Settings",
+                action: &OpenSettings,
+            },
+            SectionEntry {
+                icon: IconName::ZedAssistant,
+                title: "View AI Settings",
+                action: &agent::OpenSettings,
+            },
+            SectionEntry {
+                icon: IconName::Blocks,
+                title: "Explore Extensions",
+                action: &Extensions {
+                    category_filter: None,
+                    id: None,
+                },
+            },
+        ],
+    },
+);
+
+struct Section<const COLS: usize> {
+    title: &'static str,
+    entries: [SectionEntry; COLS],
+}
+
+impl<const COLS: usize> Section<COLS> {
+    fn render(self, index_offset: usize, focus: &FocusHandle, cx: &App) -> impl IntoElement {
+        v_flex()
+            .min_w_full()
+            .child(SectionHeader::new(self.title))
+            .children(
+                self.entries
+                    .iter()
+                    .enumerate()
+                    .map(|(index, entry)| entry.render(index_offset + index, focus, cx)),
+            )
+    }
+}
+
+pub struct WelcomePage {
+    workspace: WeakEntity<Workspace>,
+    focus_handle: FocusHandle,
+    fallback_to_recent_projects: bool,
+    recent_workspaces: Option<Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>>,
+}
+
+impl WelcomePage {
+    pub fn new(
+        workspace: WeakEntity<Workspace>,
+        fallback_to_recent_projects: bool,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Self {
+        let focus_handle = cx.focus_handle();
+        cx.on_focus(&focus_handle, window, |_, _, cx| cx.notify())
+            .detach();
+
+        if fallback_to_recent_projects {
+            cx.spawn_in(window, async move |this: WeakEntity<Self>, cx| {
+                let workspaces = WORKSPACE_DB
+                    .recent_workspaces_on_disk()
+                    .await
+                    .log_err()
+                    .unwrap_or_default();
+
+                this.update(cx, |this, cx| {
+                    this.recent_workspaces = Some(workspaces);
+                    cx.notify();
+                })
+                .ok();
+            })
+            .detach();
+        }
+
+        WelcomePage {
+            workspace,
+            focus_handle,
+            fallback_to_recent_projects,
+            recent_workspaces: None,
+        }
+    }
+
+    fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
+        window.focus_next();
+        cx.notify();
+    }
+
+    fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
+        window.focus_prev();
+        cx.notify();
+    }
+
+    fn open_recent_project(
+        &mut self,
+        action: &OpenRecentProject,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        if let Some(recent_workspaces) = &self.recent_workspaces {
+            if let Some((_workspace_id, location, paths)) = recent_workspaces.get(action.index) {
+                let paths = paths.clone();
+                let location = location.clone();
+                let is_local = matches!(location, SerializedWorkspaceLocation::Local);
+                let workspace = self.workspace.clone();
+
+                if is_local {
+                    let paths = paths.paths().to_vec();
+                    cx.spawn_in(window, async move |_, cx| {
+                        let _ = workspace.update_in(cx, |workspace, window, cx| {
+                            workspace
+                                .open_workspace_for_paths(true, paths, window, cx)
+                                .detach();
+                        });
+                    })
+                    .detach();
+                } else {
+                    use zed_actions::OpenRecent;
+                    window.dispatch_action(OpenRecent::default().boxed_clone(), cx);
+                }
+            }
+        }
+    }
+
+    fn render_recent_project_section(
+        &self,
+        recent_projects: Vec<impl IntoElement>,
+    ) -> impl IntoElement {
+        v_flex()
+            .w_full()
+            .child(SectionHeader::new("Recent Projects"))
+            .children(recent_projects)
+    }
+
+    fn render_recent_project(
+        &self,
+        index: usize,
+        location: &SerializedWorkspaceLocation,
+        paths: &PathList,
+    ) -> impl IntoElement {
+        let (icon, title) = match location {
+            SerializedWorkspaceLocation::Local => {
+                let path = paths.paths().first().map(|p| p.as_path());
+                let name = path
+                    .and_then(|p| p.file_name())
+                    .map(|n| n.to_string_lossy().to_string())
+                    .unwrap_or_else(|| "Untitled".to_string());
+                (IconName::Folder, name)
+            }
+            SerializedWorkspaceLocation::Remote(_) => {
+                (IconName::Server, "Remote Project".to_string())
+            }
+        };
+
+        SectionButton::new(
+            title,
+            icon,
+            &OpenRecentProject { index },
+            10,
+            self.focus_handle.clone(),
+        )
+    }
+}
+
+impl Render for WelcomePage {
+    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let (first_section, second_section) = CONTENT;
+        let first_section_entries = first_section.entries.len();
+        let last_index = first_section_entries + second_section.entries.len();
+
+        let recent_projects = self
+            .recent_workspaces
+            .as_ref()
+            .into_iter()
+            .flatten()
+            .take(5)
+            .enumerate()
+            .map(|(index, (_, loc, paths))| self.render_recent_project(index, loc, paths))
+            .collect::<Vec<_>>();
+
+        let second_section = if self.fallback_to_recent_projects && !recent_projects.is_empty() {
+            self.render_recent_project_section(recent_projects)
+                .into_any_element()
+        } else {
+            second_section
+                .render(first_section_entries, &self.focus_handle, cx)
+                .into_any_element()
+        };
+
+        let welcome_label = if self.fallback_to_recent_projects {
+            "Welcome back to Zed"
+        } else {
+            "Welcome to Zed"
+        };
+
+        h_flex()
+            .key_context("Welcome")
+            .track_focus(&self.focus_handle(cx))
+            .on_action(cx.listener(Self::select_previous))
+            .on_action(cx.listener(Self::select_next))
+            .on_action(cx.listener(Self::open_recent_project))
+            .size_full()
+            .justify_center()
+            .overflow_hidden()
+            .bg(cx.theme().colors().editor_background)
+            .child(
+                h_flex()
+                    .relative()
+                    .size_full()
+                    .px_12()
+                    .py_40()
+                    .max_w(px(1100.))
+                    .child(
+                        v_flex()
+                            .size_full()
+                            .max_w_128()
+                            .mx_auto()
+                            .gap_6()
+                            .overflow_x_hidden()
+                            .child(
+                                h_flex()
+                                    .w_full()
+                                    .justify_center()
+                                    .mb_4()
+                                    .gap_4()
+                                    .child(Vector::square(VectorName::ZedLogo, rems_from_px(45.)))
+                                    .child(
+                                        v_flex().child(Headline::new(welcome_label)).child(
+                                            Label::new("The editor for what's next")
+                                                .size(LabelSize::Small)
+                                                .color(Color::Muted)
+                                                .italic(),
+                                        ),
+                                    ),
+                            )
+                            .child(first_section.render(Default::default(), &self.focus_handle, cx))
+                            .child(second_section)
+                            .when(!self.fallback_to_recent_projects, |this| {
+                                this.child(
+                                    v_flex().gap_1().child(Divider::horizontal()).child(
+                                        Button::new("welcome-exit", "Return to Onboarding")
+                                            .tab_index(last_index as isize)
+                                            .full_width()
+                                            .label_size(LabelSize::XSmall)
+                                            .on_click(|_, window, cx| {
+                                                window.dispatch_action(
+                                                    OpenOnboarding.boxed_clone(),
+                                                    cx,
+                                                );
+                                            }),
+                                    ),
+                                )
+                            }),
+                    ),
+            )
+    }
+}
+
+impl EventEmitter<ItemEvent> for WelcomePage {}
+
+impl Focusable for WelcomePage {
+    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl Item for WelcomePage {
+    type Event = ItemEvent;
+
+    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
+        "Welcome".into()
+    }
+
+    fn telemetry_event_text(&self) -> Option<&'static str> {
+        Some("New Welcome Page Opened")
+    }
+
+    fn show_toolbar(&self) -> bool {
+        false
+    }
+
+    fn to_item_events(event: &Self::Event, mut f: impl FnMut(crate::item::ItemEvent)) {
+        f(*event)
+    }
+}
+
+impl crate::SerializableItem for WelcomePage {
+    fn serialized_item_kind() -> &'static str {
+        "WelcomePage"
+    }
+
+    fn cleanup(
+        workspace_id: crate::WorkspaceId,
+        alive_items: Vec<crate::ItemId>,
+        _window: &mut Window,
+        cx: &mut App,
+    ) -> Task<gpui::Result<()>> {
+        crate::delete_unloaded_items(
+            alive_items,
+            workspace_id,
+            "welcome_pages",
+            &persistence::WELCOME_PAGES,
+            cx,
+        )
+    }
+
+    fn deserialize(
+        _project: Entity<project::Project>,
+        workspace: gpui::WeakEntity<Workspace>,
+        workspace_id: crate::WorkspaceId,
+        item_id: crate::ItemId,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> Task<gpui::Result<Entity<Self>>> {
+        if persistence::WELCOME_PAGES
+            .get_welcome_page(item_id, workspace_id)
+            .ok()
+            .is_some_and(|is_open| is_open)
+        {
+            Task::ready(Ok(
+                cx.new(|cx| WelcomePage::new(workspace, false, window, cx))
+            ))
+        } else {
+            Task::ready(Err(anyhow::anyhow!("No welcome page to deserialize")))
+        }
+    }
+
+    fn serialize(
+        &mut self,
+        workspace: &mut Workspace,
+        item_id: crate::ItemId,
+        _closing: bool,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Option<Task<gpui::Result<()>>> {
+        let workspace_id = workspace.database_id()?;
+        Some(cx.background_spawn(async move {
+            persistence::WELCOME_PAGES
+                .save_welcome_page(item_id, workspace_id, true)
+                .await
+        }))
+    }
+
+    fn should_serialize(&self, event: &Self::Event) -> bool {
+        event == &ItemEvent::UpdateTab
+    }
+}
+
+mod persistence {
+    use crate::WorkspaceDb;
+    use db::{
+        query,
+        sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
+        sqlez_macros::sql,
+    };
+
+    pub struct WelcomePagesDb(ThreadSafeConnection);
+
+    impl Domain for WelcomePagesDb {
+        const NAME: &str = stringify!(WelcomePagesDb);
+
+        const MIGRATIONS: &[&str] = (&[sql!(
+                    CREATE TABLE welcome_pages (
+                        workspace_id INTEGER,
+                        item_id INTEGER UNIQUE,
+                        is_open INTEGER DEFAULT FALSE,
+
+                        PRIMARY KEY(workspace_id, item_id),
+                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
+                        ON DELETE CASCADE
+                    ) STRICT;
+        )]);
+    }
+
+    db::static_connection!(WELCOME_PAGES, WelcomePagesDb, [WorkspaceDb]);
+
+    impl WelcomePagesDb {
+        query! {
+            pub async fn save_welcome_page(
+                item_id: crate::ItemId,
+                workspace_id: crate::WorkspaceId,
+                is_open: bool
+            ) -> Result<()> {
+                INSERT OR REPLACE INTO welcome_pages(item_id, workspace_id, is_open)
+                VALUES (?, ?, ?)
+            }
+        }
+
+        query! {
+            pub fn get_welcome_page(
+                item_id: crate::ItemId,
+                workspace_id: crate::WorkspaceId
+            ) -> Result<bool> {
+                SELECT is_open
+                FROM welcome_pages
+                WHERE item_id = ? AND workspace_id = ?
+            }
+        }
+    }
+}

crates/workspace/src/workspace.rs 🔗

@@ -16,6 +16,7 @@ mod theme_preview;
 mod toast_layer;
 mod toolbar;
 pub mod utility_pane;
+pub mod welcome;
 mod workspace_settings;
 
 pub use crate::notifications::NotificationFrame;

crates/zed/src/main.rs 🔗

@@ -1157,7 +1157,13 @@ async fn restore_or_create_workspace(app_state: Arc<AppState>, cx: &mut AsyncApp
                 app_state,
                 cx,
                 |workspace, window, cx| {
-                    Editor::new_file(workspace, &Default::default(), window, cx)
+                    let restore_on_startup = WorkspaceSettings::get_global(cx).restore_on_startup;
+                    match restore_on_startup {
+                        workspace::RestoreOnStartupBehavior::Launchpad => {}
+                        _ => {
+                            Editor::new_file(workspace, &Default::default(), window, cx);
+                        }
+                    }
                 },
             )
         })?

crates/zed/src/zed.rs 🔗

@@ -4801,6 +4801,7 @@ mod tests {
                 "keymap_editor",
                 "keystroke_input",
                 "language_selector",
+                "welcome",
                 "line_ending_selector",
                 "lsp_tool",
                 "markdown",

crates/zed_actions/src/lib.rs 🔗

@@ -70,6 +70,8 @@ actions!(
         OpenTelemetryLog,
         /// Opens the performance profiler.
         OpenPerformanceProfiler,
+        /// Opens the onboarding view.
+        OpenOnboarding,
     ]
 );
 

docs/src/configuring-zed.md 🔗

@@ -3142,7 +3142,15 @@ List of strings containing any combination of:
 
 ```json [settings]
 {
-  "restore_on_startup": "none"
+  "restore_on_startup": "empty_tab"
+}
+```
+
+4. Always start with the welcome launchpad:
+
+```json [settings]
+{
+  "restore_on_startup": "launchpad"
 }
 ```