Merge branch 'main' into message-editor

Conrad Irwin created

Change summary

Cargo.lock                                  |  35 
Cargo.toml                                  |   2 
assets/keymaps/default-linux.json           |   1 
assets/keymaps/default-macos.json           |   1 
crates/agent_ui/src/agent_panel.rs          | 812 ++++++++++++++--------
crates/agent_ui/src/agent_ui.rs             |   2 
crates/agent_ui/src/ui.rs                   |   4 
crates/agent_ui/src/ui/new_thread_button.rs |   6 
crates/copilot/src/copilot.rs               |  12 
crates/file_icons/src/file_icons.rs         |  16 
crates/languages/src/css.rs                 |   8 
crates/languages/src/json.rs                |   8 
crates/languages/src/python.rs              |   1 
crates/languages/src/tailwind.rs            |   8 
crates/languages/src/typescript.rs          |   1 
crates/languages/src/vtsls.rs               |   2 
crates/languages/src/yaml.rs                |   8 
crates/node_runtime/src/node_runtime.rs     |  15 
crates/onboarding/Cargo.toml                |   3 
crates/onboarding/src/ai_setup_page.rs      |  24 
crates/onboarding/src/base_keymap_picker.rs |   2 
crates/onboarding/src/editing_page.rs       |  59 +
crates/onboarding/src/multibuffer_hint.rs   |   0 
crates/onboarding/src/onboarding.rs         |  98 +-
crates/onboarding/src/welcome.rs            | 108 +++
crates/welcome/Cargo.toml                   |  40 -
crates/welcome/LICENSE-GPL                  |   1 
crates/welcome/src/welcome.rs               | 446 ------------
crates/workspace/src/workspace.rs           |   2 
crates/zed/Cargo.toml                       |   3 
crates/zed/src/main.rs                      |   5 
crates/zed/src/zed.rs                       |   5 
crates/zed/src/zed/app_menus.rs             |   2 
crates/zed/src/zed/component_preview.rs     |   2 
crates/zed/src/zed/open_listener.rs         |   5 
docs/src/SUMMARY.md                         |   1 
docs/src/configuring-zed.md                 |   8 
docs/src/helix.md                           |  11 
docs/src/key-bindings.md                    |   4 
docs/src/telemetry.md                       |   5 
extensions/emmet/Cargo.toml                 |   2 
extensions/emmet/extension.toml             |   2 
extensions/emmet/src/emmet.rs               |   2 
43 files changed, 850 insertions(+), 932 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -11152,12 +11152,10 @@ dependencies = [
  "ai_onboarding",
  "anyhow",
  "client",
- "command_palette_hooks",
  "component",
  "db",
  "documented",
  "editor",
- "feature_flags",
  "fs",
  "fuzzy",
  "git",
@@ -11172,6 +11170,7 @@ dependencies = [
  "schemars",
  "serde",
  "settings",
+ "telemetry",
  "theme",
  "ui",
  "util",
@@ -18887,33 +18886,6 @@ version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
 
-[[package]]
-name = "welcome"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "client",
- "component",
- "db",
- "documented",
- "editor",
- "fuzzy",
- "gpui",
- "install_cli",
- "language",
- "picker",
- "project",
- "serde",
- "settings",
- "telemetry",
- "ui",
- "util",
- "vim_mode_setting",
- "workspace",
- "workspace-hack",
- "zed_actions",
-]
-
 [[package]]
 name = "which"
 version = "4.4.2"
@@ -20528,7 +20500,7 @@ dependencies = [
 
 [[package]]
 name = "zed"
-version = "0.200.0"
+version = "0.201.0"
 dependencies = [
  "activity_indicator",
  "agent",
@@ -20668,7 +20640,6 @@ dependencies = [
  "watch",
  "web_search",
  "web_search_providers",
- "welcome",
  "windows 0.61.1",
  "winresource",
  "workspace",
@@ -20692,7 +20663,7 @@ dependencies = [
 
 [[package]]
 name = "zed_emmet"
-version = "0.0.5"
+version = "0.0.6"
 dependencies = [
  "zed_extension_api 0.1.0",
 ]

Cargo.toml πŸ”—

@@ -185,7 +185,6 @@ members = [
     "crates/watch",
     "crates/web_search",
     "crates/web_search_providers",
-    "crates/welcome",
     "crates/workspace",
     "crates/worktree",
     "crates/x_ai",
@@ -412,7 +411,6 @@ vim_mode_setting = { path = "crates/vim_mode_setting" }
 watch = { path = "crates/watch" }
 web_search = { path = "crates/web_search" }
 web_search_providers = { path = "crates/web_search_providers" }
-welcome = { path = "crates/welcome" }
 workspace = { path = "crates/workspace" }
 worktree = { path = "crates/worktree" }
 x_ai = { path = "crates/x_ai" }

assets/keymaps/default-linux.json πŸ”—

@@ -239,6 +239,7 @@
       "ctrl-shift-a": "agent::ToggleContextPicker",
       "ctrl-shift-j": "agent::ToggleNavigationMenu",
       "ctrl-shift-i": "agent::ToggleOptionsMenu",
+      "ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
       "shift-alt-escape": "agent::ExpandMessageEditor",
       "ctrl->": "assistant::QuoteSelection",
       "ctrl-alt-e": "agent::RemoveAllContext",

assets/keymaps/default-macos.json πŸ”—

@@ -279,6 +279,7 @@
       "cmd-shift-a": "agent::ToggleContextPicker",
       "cmd-shift-j": "agent::ToggleNavigationMenu",
       "cmd-shift-i": "agent::ToggleOptionsMenu",
+      "cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
       "shift-alt-escape": "agent::ExpandMessageEditor",
       "cmd->": "assistant::QuoteSelection",
       "cmd-alt-e": "agent::RemoveAllContext",

crates/agent_ui/src/agent_panel.rs πŸ”—

@@ -11,12 +11,12 @@ use serde::{Deserialize, Serialize};
 
 use crate::NewExternalAgentThread;
 use crate::agent_diff::AgentDiffThread;
-use crate::ui::NewThreadButton;
 use crate::{
     AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
     DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
     NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell,
-    ResetTrialUpsell, ToggleBurnMode, ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu,
+    ResetTrialUpsell, ToggleBurnMode, ToggleContextPicker, ToggleNavigationMenu,
+    ToggleNewThreadMenu, ToggleOptionsMenu,
     acp::AcpThreadView,
     active_thread::{self, ActiveThread, ActiveThreadEvent},
     agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
@@ -66,8 +66,8 @@ use theme::ThemeSettings;
 use time::UtcOffset;
 use ui::utils::WithRemSize;
 use ui::{
-    Banner, Callout, ContextMenu, ContextMenuEntry, ElevationIndex, KeyBinding, PopoverMenu,
-    PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*,
+    Banner, ButtonLike, Callout, ContextMenu, ContextMenuEntry, ElevationIndex, KeyBinding,
+    PopoverMenu, PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*,
 };
 use util::ResultExt as _;
 use workspace::{
@@ -85,6 +85,7 @@ const AGENT_PANEL_KEY: &str = "agent_panel";
 #[derive(Serialize, Deserialize)]
 struct SerializedAgentPanel {
     width: Option<Pixels>,
+    selected_agent: Option<AgentType>,
 }
 
 pub fn init(cx: &mut App) {
@@ -178,6 +179,14 @@ pub fn init(cx: &mut App) {
                         });
                     }
                 })
+                .register_action(|workspace, _: &ToggleNewThreadMenu, window, cx| {
+                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
+                        workspace.focus_panel::<AgentPanel>(window, cx);
+                        panel.update(cx, |panel, cx| {
+                            panel.toggle_new_thread_menu(&ToggleNewThreadMenu, window, cx);
+                        });
+                    }
+                })
                 .register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
                     AgentOnboardingModal::toggle(workspace, window, cx)
                 })
@@ -222,6 +231,36 @@ enum WhichFontSize {
     None,
 }
 
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+pub enum AgentType {
+    #[default]
+    Zed,
+    TextThread,
+    Gemini,
+    ClaudeCode,
+    NativeAgent,
+}
+
+impl AgentType {
+    fn label(self) -> impl Into<SharedString> {
+        match self {
+            Self::Zed | Self::TextThread => "Zed",
+            Self::NativeAgent => "Agent 2",
+            Self::Gemini => "Gemini",
+            Self::ClaudeCode => "Claude Code",
+        }
+    }
+
+    fn icon(self) -> IconName {
+        match self {
+            Self::Zed | Self::TextThread => IconName::AiZed,
+            Self::NativeAgent => IconName::ZedAssistant,
+            Self::Gemini => IconName::AiGemini,
+            Self::ClaudeCode => IconName::AiClaude,
+        }
+    }
+}
+
 impl ActiveView {
     pub fn which_font_size_used(&self) -> WhichFontSize {
         match self {
@@ -452,16 +491,21 @@ pub struct AgentPanel {
     zoomed: bool,
     pending_serialization: Option<Task<Result<()>>>,
     onboarding: Entity<AgentPanelOnboarding>,
+    selected_agent: AgentType,
 }
 
 impl AgentPanel {
     fn serialize(&mut self, cx: &mut Context<Self>) {
         let width = self.width;
+        let selected_agent = self.selected_agent;
         self.pending_serialization = Some(cx.background_spawn(async move {
             KEY_VALUE_STORE
                 .write_kvp(
                     AGENT_PANEL_KEY.into(),
-                    serde_json::to_string(&SerializedAgentPanel { width })?,
+                    serde_json::to_string(&SerializedAgentPanel {
+                        width,
+                        selected_agent: Some(selected_agent),
+                    })?,
                 )
                 .await?;
             anyhow::Ok(())
@@ -530,6 +574,9 @@ impl AgentPanel {
                 if let Some(serialized_panel) = serialized_panel {
                     panel.update(cx, |panel, cx| {
                         panel.width = serialized_panel.width.map(|w| w.round());
+                        if let Some(selected_agent) = serialized_panel.selected_agent {
+                            panel.selected_agent = selected_agent;
+                        }
                         cx.notify();
                     });
                 }
@@ -731,6 +778,7 @@ impl AgentPanel {
             zoomed: false,
             pending_serialization: None,
             onboarding,
+            selected_agent: AgentType::default(),
         }
     }
 
@@ -1171,6 +1219,15 @@ impl AgentPanel {
         self.agent_panel_menu_handle.toggle(window, cx);
     }
 
+    pub fn toggle_new_thread_menu(
+        &mut self,
+        _: &ToggleNewThreadMenu,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.new_thread_menu_handle.toggle(window, cx);
+    }
+
     pub fn increase_font_size(
         &mut self,
         action: &IncreaseBufferFontSize,
@@ -1578,6 +1635,17 @@ impl AgentPanel {
 
         menu
     }
+
+    pub fn set_selected_agent(&mut self, agent: AgentType, cx: &mut Context<Self>) {
+        if self.selected_agent != agent {
+            self.selected_agent = agent;
+            self.serialize(cx);
+        }
+    }
+
+    pub fn selected_agent(&self) -> AgentType {
+        self.selected_agent
+    }
 }
 
 impl Focusable for AgentPanel {
@@ -1808,75 +1876,170 @@ impl AgentPanel {
             .into_any()
     }
 
-    fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+    fn render_panel_options_menu(
+        &self,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> impl IntoElement {
         let user_store = self.user_store.read(cx);
         let usage = user_store.model_request_usage();
-
         let account_url = zed_urls::account_url(cx);
 
         let focus_handle = self.focus_handle(cx);
 
-        let go_back_button = div().child(
-            IconButton::new("go-back", IconName::ArrowLeft)
-                .icon_size(IconSize::Small)
-                .on_click(cx.listener(|this, _, window, cx| {
-                    this.go_back(&workspace::GoBack, window, cx);
-                }))
-                .tooltip({
+        let full_screen_label = if self.is_zoomed(window, cx) {
+            "Disable Full Screen"
+        } else {
+            "Enable Full Screen"
+        };
+
+        PopoverMenu::new("agent-options-menu")
+            .trigger_with_tooltip(
+                IconButton::new("agent-options-menu", IconName::Ellipsis)
+                    .icon_size(IconSize::Small),
+                {
                     let focus_handle = focus_handle.clone();
                     move |window, cx| {
                         Tooltip::for_action_in(
-                            "Go Back",
-                            &workspace::GoBack,
+                            "Toggle Agent Menu",
+                            &ToggleOptionsMenu,
                             &focus_handle,
                             window,
                             cx,
                         )
                     }
-                }),
-        );
+                },
+            )
+            .anchor(Corner::TopRight)
+            .with_handle(self.agent_panel_menu_handle.clone())
+            .menu({
+                let focus_handle = focus_handle.clone();
+                move |window, cx| {
+                    Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
+                        menu = menu.context(focus_handle.clone());
+                        if let Some(usage) = usage {
+                            menu = menu
+                                .header_with_link("Prompt Usage", "Manage", account_url.clone())
+                                .custom_entry(
+                                    move |_window, cx| {
+                                        let used_percentage = match usage.limit {
+                                            UsageLimit::Limited(limit) => {
+                                                Some((usage.amount as f32 / limit as f32) * 100.)
+                                            }
+                                            UsageLimit::Unlimited => None,
+                                        };
 
-        let recent_entries_menu = div().child(
-            PopoverMenu::new("agent-nav-menu")
-                .trigger_with_tooltip(
-                    IconButton::new("agent-nav-menu", IconName::MenuAlt)
-                        .icon_size(IconSize::Small)
-                        .style(ui::ButtonStyle::Subtle),
-                    {
-                        let focus_handle = focus_handle.clone();
-                        move |window, cx| {
-                            Tooltip::for_action_in(
-                                "Toggle Panel Menu",
-                                &ToggleNavigationMenu,
-                                &focus_handle,
-                                window,
-                                cx,
-                            )
+                                        h_flex()
+                                            .flex_1()
+                                            .gap_1p5()
+                                            .children(used_percentage.map(|percent| {
+                                                ProgressBar::new("usage", percent, 100., cx)
+                                            }))
+                                            .child(
+                                                Label::new(match usage.limit {
+                                                    UsageLimit::Limited(limit) => {
+                                                        format!("{} / {limit}", usage.amount)
+                                                    }
+                                                    UsageLimit::Unlimited => {
+                                                        format!("{} / ∞", usage.amount)
+                                                    }
+                                                })
+                                                .size(LabelSize::Small)
+                                                .color(Color::Muted),
+                                            )
+                                            .into_any_element()
+                                    },
+                                    move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
+                                )
+                                .separator()
                         }
-                    },
-                )
-                .anchor(Corner::TopLeft)
-                .with_handle(self.assistant_navigation_menu_handle.clone())
-                .menu({
-                    let menu = self.assistant_navigation_menu.clone();
+
+                        menu = menu
+                            .header("MCP Servers")
+                            .action(
+                                "View Server Extensions",
+                                Box::new(zed_actions::Extensions {
+                                    category_filter: Some(
+                                        zed_actions::ExtensionCategoryFilter::ContextServers,
+                                    ),
+                                    id: None,
+                                }),
+                            )
+                            .action("Add Custom Server…", Box::new(AddContextServer))
+                            .separator();
+
+                        menu = menu
+                            .action("Rules…", Box::new(OpenRulesLibrary::default()))
+                            .action("Settings", Box::new(OpenSettings))
+                            .separator()
+                            .action(full_screen_label, Box::new(ToggleZoom));
+                        menu
+                    }))
+                }
+            })
+    }
+
+    fn render_recent_entries_menu(
+        &self,
+        icon: IconName,
+        cx: &mut Context<Self>,
+    ) -> impl IntoElement {
+        let focus_handle = self.focus_handle(cx);
+
+        PopoverMenu::new("agent-nav-menu")
+            .trigger_with_tooltip(
+                IconButton::new("agent-nav-menu", icon)
+                    .icon_size(IconSize::Small)
+                    .style(ui::ButtonStyle::Subtle),
+                {
+                    let focus_handle = focus_handle.clone();
                     move |window, cx| {
-                        if let Some(menu) = menu.as_ref() {
-                            menu.update(cx, |_, cx| {
-                                cx.defer_in(window, |menu, window, cx| {
-                                    menu.rebuild(window, cx);
-                                });
-                            })
-                        }
-                        menu.clone()
+                        Tooltip::for_action_in(
+                            "Toggle Panel Menu",
+                            &ToggleNavigationMenu,
+                            &focus_handle,
+                            window,
+                            cx,
+                        )
                     }
-                }),
-        );
+                },
+            )
+            .anchor(Corner::TopLeft)
+            .with_handle(self.assistant_navigation_menu_handle.clone())
+            .menu({
+                let menu = self.assistant_navigation_menu.clone();
+                move |window, cx| {
+                    if let Some(menu) = menu.as_ref() {
+                        menu.update(cx, |_, cx| {
+                            cx.defer_in(window, |menu, window, cx| {
+                                menu.rebuild(window, cx);
+                            });
+                        })
+                    }
+                    menu.clone()
+                }
+            })
+    }
 
-        let full_screen_label = if self.is_zoomed(window, cx) {
-            "Disable Full Screen"
-        } else {
-            "Enable Full Screen"
-        };
+    fn render_toolbar_back_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
+        let focus_handle = self.focus_handle(cx);
+
+        IconButton::new("go-back", IconName::ArrowLeft)
+            .icon_size(IconSize::Small)
+            .on_click(cx.listener(|this, _, window, cx| {
+                this.go_back(&workspace::GoBack, window, cx);
+            }))
+            .tooltip({
+                let focus_handle = focus_handle.clone();
+
+                move |window, cx| {
+                    Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, window, cx)
+                }
+            })
+    }
+
+    fn render_toolbar_old(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let focus_handle = self.focus_handle(cx);
 
         let active_thread = match &self.active_view {
             ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
@@ -1900,9 +2063,6 @@ impl AgentPanel {
                     Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
                         menu = menu
                             .context(focus_handle.clone())
-                            .when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
-                                this.header("Zed Agent")
-                            })
                             .when_some(active_thread, |this, active_thread| {
                                 let thread = active_thread.read(cx);
 
@@ -1945,72 +2105,101 @@ impl AgentPanel {
                                     .handler(move |window, cx| {
                                         window.dispatch_action(NewTextThread.boxed_clone(), cx);
                                     }),
-                            )
-                            .when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
-                                this.separator()
-                                    .header("External Agents")
-                                    .item(
-                                        ContextMenuEntry::new("New Gemini Thread")
-                                            .icon(IconName::AiGemini)
-                                            .icon_color(Color::Muted)
-                                            .handler(move |window, cx| {
-                                                window.dispatch_action(
-                                                    NewExternalAgentThread {
-                                                        agent: Some(crate::ExternalAgent::Gemini),
-                                                    }
-                                                    .boxed_clone(),
-                                                    cx,
-                                                );
-                                            }),
-                                    )
-                                    .item(
-                                        ContextMenuEntry::new("New Claude Code Thread")
-                                            .icon(IconName::AiClaude)
-                                            .icon_color(Color::Muted)
-                                            .handler(move |window, cx| {
-                                                window.dispatch_action(
-                                                    NewExternalAgentThread {
-                                                        agent: Some(
-                                                            crate::ExternalAgent::ClaudeCode,
-                                                        ),
-                                                    }
-                                                    .boxed_clone(),
-                                                    cx,
-                                                );
-                                            }),
-                                    )
-                                    .item(
-                                        ContextMenuEntry::new("New Native Agent Thread")
-                                            .icon(IconName::ZedAssistant)
-                                            .icon_color(Color::Muted)
-                                            .handler(move |window, cx| {
-                                                window.dispatch_action(
-                                                    NewExternalAgentThread {
-                                                        agent: Some(
-                                                            crate::ExternalAgent::NativeAgent,
-                                                        ),
-                                                    }
-                                                    .boxed_clone(),
-                                                    cx,
-                                                );
-                                            }),
-                                    )
-                            });
+                            );
                         menu
                     }))
                 }
             });
 
-        let agent_panel_menu = PopoverMenu::new("agent-options-menu")
+        h_flex()
+            .id("assistant-toolbar")
+            .h(Tab::container_height(cx))
+            .max_w_full()
+            .flex_none()
+            .justify_between()
+            .gap_2()
+            .bg(cx.theme().colors().tab_bar_background)
+            .border_b_1()
+            .border_color(cx.theme().colors().border)
+            .child(
+                h_flex()
+                    .size_full()
+                    .pl_1()
+                    .gap_1()
+                    .child(match &self.active_view {
+                        ActiveView::History | ActiveView::Configuration => {
+                            self.render_toolbar_back_button(cx).into_any_element()
+                        }
+                        _ => self
+                            .render_recent_entries_menu(IconName::MenuAlt, cx)
+                            .into_any_element(),
+                    })
+                    .child(self.render_title_view(window, cx)),
+            )
+            .child(
+                h_flex()
+                    .h_full()
+                    .gap_2()
+                    .children(self.render_token_count(cx))
+                    .child(
+                        h_flex()
+                            .h_full()
+                            .gap(DynamicSpacing::Base02.rems(cx))
+                            .px(DynamicSpacing::Base08.rems(cx))
+                            .border_l_1()
+                            .border_color(cx.theme().colors().border)
+                            .child(new_thread_menu)
+                            .child(self.render_panel_options_menu(window, cx)),
+                    ),
+            )
+    }
+
+    fn render_toolbar_new(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let focus_handle = self.focus_handle(cx);
+
+        let active_thread = match &self.active_view {
+            ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
+            ActiveView::ExternalAgentThread { .. }
+            | ActiveView::TextThread { .. }
+            | ActiveView::History
+            | ActiveView::Configuration => None,
+        };
+
+        let new_thread_menu = PopoverMenu::new("new_thread_menu")
             .trigger_with_tooltip(
-                IconButton::new("agent-options-menu", IconName::Ellipsis)
-                    .icon_size(IconSize::Small),
+                ButtonLike::new("new_thread_menu_btn").child(
+                    h_flex()
+                        .group("agent-selector")
+                        .gap_1p5()
+                        .child(
+                            h_flex()
+                                .relative()
+                                .size_4()
+                                .justify_center()
+                                .child(
+                                    h_flex()
+                                        .group_hover("agent-selector", |s| s.invisible())
+                                        .child(
+                                            Icon::new(self.selected_agent.icon())
+                                                .color(Color::Muted),
+                                        ),
+                                )
+                                .child(
+                                    h_flex()
+                                        .absolute()
+                                        .invisible()
+                                        .group_hover("agent-selector", |s| s.visible())
+                                        .child(Icon::new(IconName::Plus)),
+                                ),
+                        )
+                        .child(Label::new(self.selected_agent.label())),
+                ),
                 {
                     let focus_handle = focus_handle.clone();
                     move |window, cx| {
                         Tooltip::for_action_in(
-                            "Toggle Agent Menu",
-                            &ToggleOptionsMenu,
+                            "New…",
+                            &ToggleNewThreadMenu,
                             &focus_handle,
                             window,
                             cx,
@@ -2018,76 +2207,197 @@ impl AgentPanel {
                     }
                 },
             )
-            .anchor(Corner::TopRight)
-            .with_handle(self.agent_panel_menu_handle.clone())
+            .anchor(Corner::TopLeft)
+            .with_handle(self.new_thread_menu_handle.clone())
             .menu({
                 let focus_handle = focus_handle.clone();
+                let workspace = self.workspace.clone();
+
                 move |window, cx| {
-                    Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
-                        menu = menu.context(focus_handle.clone());
-                        if let Some(usage) = usage {
-                            menu = menu
-                                .header_with_link("Prompt Usage", "Manage", account_url.clone())
-                                .custom_entry(
-                                    move |_window, cx| {
-                                        let used_percentage = match usage.limit {
-                                            UsageLimit::Limited(limit) => {
-                                                Some((usage.amount as f32 / limit as f32) * 100.)
-                                            }
-                                            UsageLimit::Unlimited => None,
-                                        };
+                    let active_thread = active_thread.clone();
+                    Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
+                        menu = menu
+                            .context(focus_handle.clone())
+                            .header("Zed Agent")
+                            .when_some(active_thread, |this, active_thread| {
+                                let thread = active_thread.read(cx);
 
-                                        h_flex()
-                                            .flex_1()
-                                            .gap_1p5()
-                                            .children(used_percentage.map(|percent| {
-                                                ProgressBar::new("usage", percent, 100., cx)
-                                            }))
-                                            .child(
-                                                Label::new(match usage.limit {
-                                                    UsageLimit::Limited(limit) => {
-                                                        format!("{} / {limit}", usage.amount)
+                                if !thread.is_empty() {
+                                    let thread_id = thread.id().clone();
+                                    this.item(
+                                        ContextMenuEntry::new("New From Summary")
+                                            .icon(IconName::ThreadFromSummary)
+                                            .icon_color(Color::Muted)
+                                            .handler(move |window, cx| {
+                                                window.dispatch_action(
+                                                    Box::new(NewThread {
+                                                        from_thread_id: Some(thread_id.clone()),
+                                                    }),
+                                                    cx,
+                                                );
+                                            }),
+                                    )
+                                } else {
+                                    this
+                                }
+                            })
+                            .item(
+                                ContextMenuEntry::new("New Thread")
+                                    .icon(IconName::Thread)
+                                    .icon_color(Color::Muted)
+                                    .action(NewThread::default().boxed_clone())
+                                    .handler({
+                                        let workspace = workspace.clone();
+                                        move |window, cx| {
+                                            if let Some(workspace) = workspace.upgrade() {
+                                                workspace.update(cx, |workspace, cx| {
+                                                    if let Some(panel) =
+                                                        workspace.panel::<AgentPanel>(cx)
+                                                    {
+                                                        panel.update(cx, |panel, cx| {
+                                                            panel.set_selected_agent(
+                                                                AgentType::Zed,
+                                                                cx,
+                                                            );
+                                                        });
                                                     }
-                                                    UsageLimit::Unlimited => {
-                                                        format!("{} / ∞", usage.amount)
+                                                });
+                                            }
+                                            window.dispatch_action(
+                                                NewThread::default().boxed_clone(),
+                                                cx,
+                                            );
+                                        }
+                                    }),
+                            )
+                            .item(
+                                ContextMenuEntry::new("New Text Thread")
+                                    .icon(IconName::TextThread)
+                                    .icon_color(Color::Muted)
+                                    .action(NewTextThread.boxed_clone())
+                                    .handler({
+                                        let workspace = workspace.clone();
+                                        move |window, cx| {
+                                            if let Some(workspace) = workspace.upgrade() {
+                                                workspace.update(cx, |workspace, cx| {
+                                                    if let Some(panel) =
+                                                        workspace.panel::<AgentPanel>(cx)
+                                                    {
+                                                        panel.update(cx, |panel, cx| {
+                                                            panel.set_selected_agent(
+                                                                AgentType::TextThread,
+                                                                cx,
+                                                            );
+                                                        });
                                                     }
-                                                })
-                                                .size(LabelSize::Small)
-                                                .color(Color::Muted),
-                                            )
-                                            .into_any_element()
-                                    },
-                                    move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
-                                )
-                                .separator()
-                        }
-
-                        menu = menu
-                            .header("MCP Servers")
-                            .action(
-                                "View Server Extensions",
-                                Box::new(zed_actions::Extensions {
-                                    category_filter: Some(
-                                        zed_actions::ExtensionCategoryFilter::ContextServers,
-                                    ),
-                                    id: None,
-                                }),
+                                                });
+                                            }
+                                            window.dispatch_action(NewTextThread.boxed_clone(), cx);
+                                        }
+                                    }),
+                            )
+                            .item(
+                                ContextMenuEntry::new("New Native Agent Thread")
+                                    .icon(IconName::ZedAssistant)
+                                    .icon_color(Color::Muted)
+                                    .handler({
+                                        let workspace = workspace.clone();
+                                        move |window, cx| {
+                                            if let Some(workspace) = workspace.upgrade() {
+                                                workspace.update(cx, |workspace, cx| {
+                                                    if let Some(panel) =
+                                                        workspace.panel::<AgentPanel>(cx)
+                                                    {
+                                                        panel.update(cx, |panel, cx| {
+                                                            panel.set_selected_agent(
+                                                                AgentType::NativeAgent,
+                                                                cx,
+                                                            );
+                                                        });
+                                                    }
+                                                });
+                                            }
+                                            window.dispatch_action(
+                                                NewExternalAgentThread {
+                                                    agent: Some(crate::ExternalAgent::NativeAgent),
+                                                }
+                                                .boxed_clone(),
+                                                cx,
+                                            );
+                                        }
+                                    }),
                             )
-                            .action("Add Custom Server…", Box::new(AddContextServer))
-                            .separator();
-
-                        menu = menu
-                            .action("Rules…", Box::new(OpenRulesLibrary::default()))
-                            .action("Settings", Box::new(OpenSettings))
                             .separator()
-                            .action(full_screen_label, Box::new(ToggleZoom));
+                            .header("External Agents")
+                            .item(
+                                ContextMenuEntry::new("New Gemini Thread")
+                                    .icon(IconName::AiGemini)
+                                    .icon_color(Color::Muted)
+                                    .handler({
+                                        let workspace = workspace.clone();
+                                        move |window, cx| {
+                                            if let Some(workspace) = workspace.upgrade() {
+                                                workspace.update(cx, |workspace, cx| {
+                                                    if let Some(panel) =
+                                                        workspace.panel::<AgentPanel>(cx)
+                                                    {
+                                                        panel.update(cx, |panel, cx| {
+                                                            panel.set_selected_agent(
+                                                                AgentType::Gemini,
+                                                                cx,
+                                                            );
+                                                        });
+                                                    }
+                                                });
+                                            }
+                                            window.dispatch_action(
+                                                NewExternalAgentThread {
+                                                    agent: Some(crate::ExternalAgent::Gemini),
+                                                }
+                                                .boxed_clone(),
+                                                cx,
+                                            );
+                                        }
+                                    }),
+                            )
+                            .item(
+                                ContextMenuEntry::new("New Claude Code Thread")
+                                    .icon(IconName::AiClaude)
+                                    .icon_color(Color::Muted)
+                                    .handler({
+                                        let workspace = workspace.clone();
+                                        move |window, cx| {
+                                            if let Some(workspace) = workspace.upgrade() {
+                                                workspace.update(cx, |workspace, cx| {
+                                                    if let Some(panel) =
+                                                        workspace.panel::<AgentPanel>(cx)
+                                                    {
+                                                        panel.update(cx, |panel, cx| {
+                                                            panel.set_selected_agent(
+                                                                AgentType::ClaudeCode,
+                                                                cx,
+                                                            );
+                                                        });
+                                                    }
+                                                });
+                                            }
+                                            window.dispatch_action(
+                                                NewExternalAgentThread {
+                                                    agent: Some(crate::ExternalAgent::ClaudeCode),
+                                                }
+                                                .boxed_clone(),
+                                                cx,
+                                            );
+                                        }
+                                    }),
+                            );
                         menu
                     }))
                 }
             });
 
         h_flex()
-            .id("assistant-toolbar")
+            .id("agent-panel-toolbar")
             .h(Tab::container_height(cx))
             .max_w_full()
             .flex_none()
@@ -2099,11 +2409,18 @@ impl AgentPanel {
             .child(
                 h_flex()
                     .size_full()
-                    .pl_1()
-                    .gap_1()
+                    .gap(DynamicSpacing::Base08.rems(cx))
                     .child(match &self.active_view {
-                        ActiveView::History | ActiveView::Configuration => go_back_button,
-                        _ => recent_entries_menu,
+                        ActiveView::History | ActiveView::Configuration => {
+                            self.render_toolbar_back_button(cx).into_any_element()
+                        }
+                        _ => h_flex()
+                            .h_full()
+                            .px(DynamicSpacing::Base04.rems(cx))
+                            .border_r_1()
+                            .border_color(cx.theme().colors().border)
+                            .child(new_thread_menu)
+                            .into_any_element(),
                     })
                     .child(self.render_title_view(window, cx)),
             )
@@ -2116,15 +2433,24 @@ impl AgentPanel {
                         h_flex()
                             .h_full()
                             .gap(DynamicSpacing::Base02.rems(cx))
-                            .px(DynamicSpacing::Base08.rems(cx))
+                            .pl(DynamicSpacing::Base04.rems(cx))
+                            .pr(DynamicSpacing::Base06.rems(cx))
                             .border_l_1()
                             .border_color(cx.theme().colors().border)
-                            .child(new_thread_menu)
-                            .child(agent_panel_menu),
+                            .child(self.render_recent_entries_menu(IconName::HistoryRerun, cx))
+                            .child(self.render_panel_options_menu(window, cx)),
                     ),
             )
     }
 
+    fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        if cx.has_flag::<feature_flags::AcpFeatureFlag>() {
+            self.render_toolbar_new(window, cx).into_any_element()
+        } else {
+            self.render_toolbar_old(window, cx).into_any_element()
+        }
+    }
+
     fn render_token_count(&self, cx: &App) -> Option<AnyElement> {
         match &self.active_view {
             ActiveView::Thread {
@@ -2573,138 +2899,6 @@ impl AgentPanel {
                                 },
                             )),
                     )
-                    .child(self.render_empty_state_section_header("Start", None, cx))
-                    .child(
-                        v_flex()
-                            .p_1()
-                            .gap_2()
-                            .child(
-                                h_flex()
-                                    .w_full()
-                                    .gap_2()
-                                    .child(
-                                        NewThreadButton::new(
-                                            "new-thread-btn",
-                                            "New Thread",
-                                            IconName::Thread,
-                                        )
-                                        .keybinding(KeyBinding::for_action_in(
-                                            &NewThread::default(),
-                                            &self.focus_handle(cx),
-                                            window,
-                                            cx,
-                                        ))
-                                        .on_click(
-                                            |window, cx| {
-                                                window.dispatch_action(
-                                                    NewThread::default().boxed_clone(),
-                                                    cx,
-                                                )
-                                            },
-                                        ),
-                                    )
-                                    .child(
-                                        NewThreadButton::new(
-                                            "new-text-thread-btn",
-                                            "New Text Thread",
-                                            IconName::TextThread,
-                                        )
-                                        .keybinding(KeyBinding::for_action_in(
-                                            &NewTextThread,
-                                            &self.focus_handle(cx),
-                                            window,
-                                            cx,
-                                        ))
-                                        .on_click(
-                                            |window, cx| {
-                                                window.dispatch_action(Box::new(NewTextThread), cx)
-                                            },
-                                        ),
-                                    ),
-                            )
-                            .when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
-                                this.child(
-                                    h_flex()
-                                        .w_full()
-                                        .gap_2()
-                                        .child(
-                                            NewThreadButton::new(
-                                                "new-gemini-thread-btn",
-                                                "New Gemini Thread",
-                                                IconName::AiGemini,
-                                            )
-                                            // .keybinding(KeyBinding::for_action_in(
-                                            //     &OpenHistory,
-                                            //     &self.focus_handle(cx),
-                                            //     window,
-                                            //     cx,
-                                            // ))
-                                            .on_click(
-                                                |window, cx| {
-                                                    window.dispatch_action(
-                                                        Box::new(NewExternalAgentThread {
-                                                            agent: Some(
-                                                                crate::ExternalAgent::Gemini,
-                                                            ),
-                                                        }),
-                                                        cx,
-                                                    )
-                                                },
-                                            ),
-                                        )
-                                        .child(
-                                            NewThreadButton::new(
-                                                "new-claude-thread-btn",
-                                                "New Claude Code Thread",
-                                                IconName::AiClaude,
-                                            )
-                                            // .keybinding(KeyBinding::for_action_in(
-                                            //     &OpenHistory,
-                                            //     &self.focus_handle(cx),
-                                            //     window,
-                                            //     cx,
-                                            // ))
-                                            .on_click(
-                                                |window, cx| {
-                                                    window.dispatch_action(
-                                                        Box::new(NewExternalAgentThread {
-                                                            agent: Some(
-                                                                crate::ExternalAgent::ClaudeCode,
-                                                            ),
-                                                        }),
-                                                        cx,
-                                                    )
-                                                },
-                                            ),
-                                        )
-                                        .child(
-                                            NewThreadButton::new(
-                                                "new-native-agent-thread-btn",
-                                                "New Native Agent Thread",
-                                                IconName::ZedAssistant,
-                                            )
-                                            // .keybinding(KeyBinding::for_action_in(
-                                            //     &OpenHistory,
-                                            //     &self.focus_handle(cx),
-                                            //     window,
-                                            //     cx,
-                                            // ))
-                                            .on_click(
-                                                |window, cx| {
-                                                    window.dispatch_action(
-                                                        Box::new(NewExternalAgentThread {
-                                                            agent: Some(
-                                                                crate::ExternalAgent::NativeAgent,
-                                                            ),
-                                                        }),
-                                                        cx,
-                                                    )
-                                                },
-                                            ),
-                                        ),
-                                )
-                            }),
-                    )
                     .when_some(configuration_error.as_ref(), |this, err| {
                         this.child(self.render_configuration_error(err, &focus_handle, window, cx))
                     })

crates/agent_ui/src/agent_ui.rs πŸ”—

@@ -64,6 +64,8 @@ actions!(
         NewTextThread,
         /// Toggles the context picker interface for adding files, symbols, or other context.
         ToggleContextPicker,
+        /// Toggles the menu to create new agent threads.
+        ToggleNewThreadMenu,
         /// Toggles the navigation menu for switching between threads and views.
         ToggleNavigationMenu,
         /// Toggles the options menu for agent settings and preferences.

crates/agent_ui/src/ui.rs πŸ”—

@@ -2,7 +2,7 @@ mod agent_notification;
 mod burn_mode_tooltip;
 mod context_pill;
 mod end_trial_upsell;
-mod new_thread_button;
+// mod new_thread_button;
 mod onboarding_modal;
 pub mod preview;
 
@@ -10,5 +10,5 @@ pub use agent_notification::*;
 pub use burn_mode_tooltip::*;
 pub use context_pill::*;
 pub use end_trial_upsell::*;
-pub use new_thread_button::*;
+// pub use new_thread_button::*;
 pub use onboarding_modal::*;

crates/agent_ui/src/ui/new_thread_button.rs πŸ”—

@@ -11,7 +11,7 @@ pub struct NewThreadButton {
 }
 
 impl NewThreadButton {
-    pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>, icon: IconName) -> Self {
+    fn new(id: impl Into<ElementId>, label: impl Into<SharedString>, icon: IconName) -> Self {
         Self {
             id: id.into(),
             label: label.into(),
@@ -21,12 +21,12 @@ impl NewThreadButton {
         }
     }
 
-    pub fn keybinding(mut self, keybinding: Option<ui::KeyBinding>) -> Self {
+    fn keybinding(mut self, keybinding: Option<ui::KeyBinding>) -> Self {
         self.keybinding = keybinding;
         self
     }
 
-    pub fn on_click<F>(mut self, handler: F) -> Self
+    fn on_click<F>(mut self, handler: F) -> Self
     where
         F: Fn(&mut Window, &mut App) + 'static,
     {

crates/copilot/src/copilot.rs πŸ”—

@@ -21,7 +21,7 @@ use language::{
     point_from_lsp, point_to_lsp,
 };
 use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName};
-use node_runtime::NodeRuntime;
+use node_runtime::{NodeRuntime, VersionCheck};
 use parking_lot::Mutex;
 use project::DisableAiSettings;
 use request::StatusNotification;
@@ -1169,9 +1169,8 @@ async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::
     const SERVER_PATH: &str =
         "node_modules/@github/copilot-language-server/dist/language-server.js";
 
-    let latest_version = node_runtime
-        .npm_package_latest_version(PACKAGE_NAME)
-        .await?;
+    // pinning it: https://github.com/zed-industries/zed/issues/36093
+    const PINNED_VERSION: &str = "1.354";
     let server_path = paths::copilot_dir().join(SERVER_PATH);
 
     fs.create_dir(paths::copilot_dir()).await?;
@@ -1181,12 +1180,13 @@ async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::
             PACKAGE_NAME,
             &server_path,
             paths::copilot_dir(),
-            &latest_version,
+            &PINNED_VERSION,
+            VersionCheck::VersionMismatch,
         )
         .await;
     if should_install {
         node_runtime
-            .npm_install_packages(paths::copilot_dir(), &[(PACKAGE_NAME, &latest_version)])
+            .npm_install_packages(paths::copilot_dir(), &[(PACKAGE_NAME, &PINNED_VERSION)])
             .await?;
     }
 

crates/file_icons/src/file_icons.rs πŸ”—

@@ -33,13 +33,23 @@ impl FileIcons {
         // TODO: Associate a type with the languages and have the file's language
         //       override these associations
 
-        // check if file name is in suffixes
-        // e.g. catch file named `eslint.config.js` instead of `.eslint.config.js`
-        if let Some(typ) = path.file_name().and_then(|typ| typ.to_str()) {
+        if let Some(mut typ) = path.file_name().and_then(|typ| typ.to_str()) {
+            // check if file name is in suffixes
+            // e.g. catch file named `eslint.config.js` instead of `.eslint.config.js`
             let maybe_path = get_icon_from_suffix(typ);
             if maybe_path.is_some() {
                 return maybe_path;
             }
+
+            // check if suffix based on first dot is in suffixes
+            // e.g. consider `module.js` as suffix to angular's module file named `auth.module.js`
+            while let Some((_, suffix)) = typ.split_once('.') {
+                let maybe_path = get_icon_from_suffix(suffix);
+                if maybe_path.is_some() {
+                    return maybe_path;
+                }
+                typ = suffix;
+            }
         }
 
         // primary case: check if the files extension or the hidden file name

crates/languages/src/css.rs πŸ”—

@@ -103,7 +103,13 @@ impl LspAdapter for CssLspAdapter {
 
         let should_install_language_server = self
             .node
-            .should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version)
+            .should_install_npm_package(
+                Self::PACKAGE_NAME,
+                &server_path,
+                &container_dir,
+                &version,
+                Default::default(),
+            )
             .await;
 
         if should_install_language_server {

crates/languages/src/json.rs πŸ”—

@@ -340,7 +340,13 @@ impl LspAdapter for JsonLspAdapter {
 
         let should_install_language_server = self
             .node
-            .should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version)
+            .should_install_npm_package(
+                Self::PACKAGE_NAME,
+                &server_path,
+                &container_dir,
+                &version,
+                Default::default(),
+            )
             .await;
 
         if should_install_language_server {

crates/languages/src/tailwind.rs πŸ”—

@@ -108,7 +108,13 @@ impl LspAdapter for TailwindLspAdapter {
 
         let should_install_language_server = self
             .node
-            .should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version)
+            .should_install_npm_package(
+                Self::PACKAGE_NAME,
+                &server_path,
+                &container_dir,
+                &version,
+                Default::default(),
+            )
             .await;
 
         if should_install_language_server {

crates/languages/src/vtsls.rs πŸ”—

@@ -116,6 +116,7 @@ impl LspAdapter for VtslsLspAdapter {
                 &server_path,
                 &container_dir,
                 &latest_version.server_version,
+                Default::default(),
             )
             .await
         {
@@ -129,6 +130,7 @@ impl LspAdapter for VtslsLspAdapter {
                 &container_dir.join(Self::TYPESCRIPT_TSDK_PATH),
                 &container_dir,
                 &latest_version.typescript_version,
+                Default::default(),
             )
             .await
         {

crates/languages/src/yaml.rs πŸ”—

@@ -104,7 +104,13 @@ impl LspAdapter for YamlLspAdapter {
 
         let should_install_language_server = self
             .node
-            .should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version)
+            .should_install_npm_package(
+                Self::PACKAGE_NAME,
+                &server_path,
+                &container_dir,
+                &version,
+                Default::default(),
+            )
             .await;
 
         if should_install_language_server {

crates/node_runtime/src/node_runtime.rs πŸ”—

@@ -29,6 +29,15 @@ pub struct NodeBinaryOptions {
     pub use_paths: Option<(PathBuf, PathBuf)>,
 }
 
+#[derive(Default)]
+pub enum VersionCheck {
+    /// Check whether the installed and requested version have a mismatch
+    VersionMismatch,
+    /// Only check whether the currently installed version is older than the newest one
+    #[default]
+    OlderVersion,
+}
+
 #[derive(Clone)]
 pub struct NodeRuntime(Arc<Mutex<NodeRuntimeState>>);
 
@@ -287,6 +296,7 @@ impl NodeRuntime {
         local_executable_path: &Path,
         local_package_directory: &Path,
         latest_version: &str,
+        version_check: VersionCheck,
     ) -> bool {
         // In the case of the local system not having the package installed,
         // or in the instances where we fail to parse package.json data,
@@ -311,7 +321,10 @@ impl NodeRuntime {
             return true;
         };
 
-        installed_version < latest_version
+        match version_check {
+            VersionCheck::VersionMismatch => installed_version != latest_version,
+            VersionCheck::OlderVersion => installed_version < latest_version,
+        }
     }
 }
 

crates/onboarding/Cargo.toml πŸ”—

@@ -18,12 +18,10 @@ default = []
 ai_onboarding.workspace = true
 anyhow.workspace = true
 client.workspace = true
-command_palette_hooks.workspace = true
 component.workspace = true
 db.workspace = true
 documented.workspace = true
 editor.workspace = true
-feature_flags.workspace = true
 fs.workspace = true
 fuzzy.workspace = true
 git.workspace = true
@@ -38,6 +36,7 @@ project.workspace = true
 schemars.workspace = true
 serde.workspace = true
 settings.workspace = true
+telemetry.workspace = true
 theme.workspace = true
 ui.workspace = true
 util.workspace = true

crates/onboarding/src/ai_setup_page.rs πŸ”—

@@ -188,6 +188,11 @@ fn render_llm_provider_card(
                                 workspace
                                     .update(cx, |workspace, cx| {
                                         workspace.toggle_modal(window, cx, |window, cx| {
+                                            telemetry::event!(
+                                                "Welcome AI Modal Opened",
+                                                provider = provider.name().0,
+                                            );
+
                                             let modal = AiConfigurationModal::new(
                                                 provider.clone(),
                                                 window,
@@ -245,16 +250,25 @@ pub(crate) fn render_ai_setup_page(
                     ToggleState::Selected
                 },
                 |&toggle_state, _, cx| {
+                    let enabled = match toggle_state {
+                        ToggleState::Indeterminate => {
+                            return;
+                        }
+                        ToggleState::Unselected => true,
+                        ToggleState::Selected => false,
+                    };
+
+                    telemetry::event!(
+                        "Welcome AI Enabled",
+                        toggle = if enabled { "on" } else { "off" },
+                    );
+
                     let fs = <dyn Fs>::global(cx);
                     update_settings_file::<DisableAiSettings>(
                         fs,
                         cx,
                         move |ai_settings: &mut Option<bool>, _| {
-                            *ai_settings = match toggle_state {
-                                ToggleState::Indeterminate => None,
-                                ToggleState::Unselected => Some(true),
-                                ToggleState::Selected => Some(false),
-                            };
+                            *ai_settings = Some(enabled);
                         },
                     );
                 },

crates/welcome/src/base_keymap_picker.rs β†’ crates/onboarding/src/base_keymap_picker.rs πŸ”—

@@ -12,7 +12,7 @@ use util::ResultExt;
 use workspace::{ModalView, Workspace, ui::HighlightedLabel};
 
 actions!(
-    welcome,
+    zed,
     [
         /// Toggles the base keymap selector modal.
         ToggleBaseKeymapSelector

crates/onboarding/src/editing_page.rs πŸ”—

@@ -35,6 +35,11 @@ fn write_show_mini_map(show: ShowMinimap, cx: &mut App) {
     EditorSettings::override_global(curr_settings, cx);
 
     update_settings_file::<EditorSettings>(fs, cx, move |editor_settings, _| {
+        telemetry::event!(
+            "Welcome Minimap Clicked",
+            from = editor_settings.minimap.unwrap_or_default(),
+            to = show
+        );
         editor_settings.minimap.get_or_insert_default().show = Some(show);
     });
 }
@@ -71,7 +76,7 @@ fn read_git_blame(cx: &App) -> bool {
     ProjectSettings::get_global(cx).git.inline_blame_enabled()
 }
 
-fn set_git_blame(enabled: bool, cx: &mut App) {
+fn write_git_blame(enabled: bool, cx: &mut App) {
     let fs = <dyn Fs>::global(cx);
 
     let mut curr_settings = ProjectSettings::get_global(cx).clone();
@@ -95,6 +100,12 @@ fn write_ui_font_family(font: SharedString, cx: &mut App) {
     let fs = <dyn Fs>::global(cx);
 
     update_settings_file::<ThemeSettings>(fs, cx, move |theme_settings, _| {
+        telemetry::event!(
+            "Welcome Font Changed",
+            type = "ui font",
+            old = theme_settings.ui_font_family,
+            new = font.clone()
+        );
         theme_settings.ui_font_family = Some(FontFamilyName(font.into()));
     });
 }
@@ -119,6 +130,13 @@ fn write_buffer_font_family(font_family: SharedString, cx: &mut App) {
     let fs = <dyn Fs>::global(cx);
 
     update_settings_file::<ThemeSettings>(fs, cx, move |theme_settings, _| {
+        telemetry::event!(
+            "Welcome Font Changed",
+            type = "editor font",
+            old = theme_settings.buffer_font_family,
+            new = font_family.clone()
+        );
+
         theme_settings.buffer_font_family = Some(FontFamilyName(font_family.into()));
     });
 }
@@ -197,7 +215,7 @@ fn render_setting_import_button(
                                     .color(Color::Muted)
                                     .size(IconSize::XSmall),
                             )
-                            .child(Label::new(label)),
+                            .child(Label::new(label.clone())),
                     )
                     .when(imported, |this| {
                         this.child(
@@ -212,7 +230,10 @@ fn render_setting_import_button(
                         )
                     }),
             )
-            .on_click(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)),
+            .on_click(move |_, window, cx| {
+                telemetry::event!("Welcome Import Settings", import_source = label,);
+                window.dispatch_action(action.boxed_clone(), cx);
+            }),
     )
 }
 
@@ -605,7 +626,13 @@ fn render_popular_settings_section(
                     ui::ToggleState::Unselected
                 },
                 |toggle_state, _, cx| {
-                    write_font_ligatures(toggle_state == &ToggleState::Selected, cx);
+                    let enabled = toggle_state == &ToggleState::Selected;
+                    telemetry::event!(
+                        "Welcome Font Ligature",
+                        options = if enabled { "on" } else { "off" },
+                    );
+
+                    write_font_ligatures(enabled, cx);
                 },
             )
             .tab_index({
@@ -625,7 +652,13 @@ fn render_popular_settings_section(
                     ui::ToggleState::Unselected
                 },
                 |toggle_state, _, cx| {
-                    write_format_on_save(toggle_state == &ToggleState::Selected, cx);
+                    let enabled = toggle_state == &ToggleState::Selected;
+                    telemetry::event!(
+                        "Welcome Format On Save Changed",
+                        options = if enabled { "on" } else { "off" },
+                    );
+
+                    write_format_on_save(enabled, cx);
                 },
             )
             .tab_index({
@@ -644,7 +677,13 @@ fn render_popular_settings_section(
                     ui::ToggleState::Unselected
                 },
                 |toggle_state, _, cx| {
-                    write_inlay_hints(toggle_state == &ToggleState::Selected, cx);
+                    let enabled = toggle_state == &ToggleState::Selected;
+                    telemetry::event!(
+                        "Welcome Inlay Hints Changed",
+                        options = if enabled { "on" } else { "off" },
+                    );
+
+                    write_inlay_hints(enabled, cx);
                 },
             )
             .tab_index({
@@ -663,7 +702,13 @@ fn render_popular_settings_section(
                     ui::ToggleState::Unselected
                 },
                 |toggle_state, _, cx| {
-                    set_git_blame(toggle_state == &ToggleState::Selected, cx);
+                    let enabled = toggle_state == &ToggleState::Selected;
+                    telemetry::event!(
+                        "Welcome Git Blame Changed",
+                        options = if enabled { "on" } else { "off" },
+                    );
+
+                    write_git_blame(enabled, cx);
                 },
             )
             .tab_index({

crates/onboarding/src/onboarding.rs πŸ”—

@@ -1,8 +1,7 @@
-use crate::welcome::{ShowWelcome, WelcomePage};
+pub use crate::welcome::ShowWelcome;
+use crate::{multibuffer_hint::MultibufferHint, welcome::WelcomePage};
 use client::{Client, UserStore, zed_urls};
-use command_palette_hooks::CommandPaletteFilter;
 use db::kvp::KEY_VALUE_STORE;
-use feature_flags::{FeatureFlag, FeatureFlagViewExt as _};
 use fs::Fs;
 use gpui::{
     Action, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity, EventEmitter,
@@ -27,17 +26,13 @@ use workspace::{
 };
 
 mod ai_setup_page;
+mod base_keymap_picker;
 mod basics_page;
 mod editing_page;
+pub mod multibuffer_hint;
 mod theme_preview;
 mod welcome;
 
-pub struct OnBoardingFeatureFlag {}
-
-impl FeatureFlag for OnBoardingFeatureFlag {
-    const NAME: &'static str = "onboarding";
-}
-
 /// Imports settings from Visual Studio Code.
 #[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
 #[action(namespace = zed)]
@@ -57,6 +52,7 @@ pub struct ImportCursorSettings {
 }
 
 pub const FIRST_OPEN: &str = "first_open";
+pub const DOCS_URL: &str = "https://zed.dev/docs/";
 
 actions!(
     zed,
@@ -80,11 +76,19 @@ actions!(
         /// Sign in while in the onboarding flow.
         SignIn,
         /// Open the user account in zed.dev while in the onboarding flow.
-        OpenAccount
+        OpenAccount,
+        /// Resets the welcome screen hints to their initial state.
+        ResetHints
     ]
 );
 
 pub fn init(cx: &mut App) {
+    cx.observe_new(|workspace: &mut Workspace, _, _cx| {
+        workspace
+            .register_action(|_workspace, _: &ResetHints, _, cx| MultibufferHint::set_count(0, cx));
+    })
+    .detach();
+
     cx.on_action(|_: &OpenOnboarding, cx| {
         with_active_or_new_workspace(cx, |workspace, window, cx| {
             workspace
@@ -182,38 +186,14 @@ pub fn init(cx: &mut App) {
     })
     .detach();
 
-    cx.observe_new::<Workspace>(|_, window, cx| {
-        let Some(window) = window else {
-            return;
-        };
-
-        let onboarding_actions = [
-            std::any::TypeId::of::<OpenOnboarding>(),
-            std::any::TypeId::of::<ShowWelcome>(),
-        ];
+    base_keymap_picker::init(cx);
 
-        CommandPaletteFilter::update_global(cx, |filter, _cx| {
-            filter.hide_action_types(&onboarding_actions);
-        });
-
-        cx.observe_flag::<OnBoardingFeatureFlag, _>(window, move |is_enabled, _, _, cx| {
-            if is_enabled {
-                CommandPaletteFilter::update_global(cx, |filter, _cx| {
-                    filter.show_action_types(onboarding_actions.iter());
-                });
-            } else {
-                CommandPaletteFilter::update_global(cx, |filter, _cx| {
-                    filter.hide_action_types(&onboarding_actions);
-                });
-            }
-        })
-        .detach();
-    })
-    .detach();
     register_serializable_item::<Onboarding>(cx);
+    register_serializable_item::<WelcomePage>(cx);
 }
 
 pub fn show_onboarding_view(app_state: Arc<AppState>, cx: &mut App) -> Task<anyhow::Result<()>> {
+    telemetry::event!("Onboarding Page Opened");
     open_new(
         Default::default(),
         app_state,
@@ -242,6 +222,16 @@ enum SelectedPage {
     AiSetup,
 }
 
+impl SelectedPage {
+    fn name(&self) -> &'static str {
+        match self {
+            SelectedPage::Basics => "Basics",
+            SelectedPage::Editing => "Editing",
+            SelectedPage::AiSetup => "AI Setup",
+        }
+    }
+}
+
 struct Onboarding {
     workspace: WeakEntity<Workspace>,
     focus_handle: FocusHandle,
@@ -261,7 +251,21 @@ impl Onboarding {
         })
     }
 
-    fn set_page(&mut self, page: SelectedPage, cx: &mut Context<Self>) {
+    fn set_page(
+        &mut self,
+        page: SelectedPage,
+        clicked: Option<&'static str>,
+        cx: &mut Context<Self>,
+    ) {
+        if let Some(click) = clicked {
+            telemetry::event!(
+                "Welcome Tab Clicked",
+                from = self.selected_page.name(),
+                to = page.name(),
+                clicked = click,
+            );
+        }
+
         self.selected_page = page;
         cx.notify();
         cx.emit(ItemEvent::UpdateTab);
@@ -325,8 +329,13 @@ impl Onboarding {
                     gpui::Empty.into_any_element(),
                     IntoElement::into_any_element,
                 ))
-                .on_click(cx.listener(move |this, _, _, cx| {
-                    this.set_page(page, cx);
+                .on_click(cx.listener(move |this, click_event, _, cx| {
+                    let click = match click_event {
+                        gpui::ClickEvent::Mouse(_) => "mouse",
+                        gpui::ClickEvent::Keyboard(_) => "keyboard",
+                    };
+
+                    this.set_page(page, Some(click), cx);
                 }))
         })
     }
@@ -475,6 +484,7 @@ impl Onboarding {
     }
 
     fn on_finish(_: &Finish, _: &mut Window, cx: &mut App) {
+        telemetry::event!("Welcome Skip Clicked");
         go_to_welcome_page(cx);
     }
 
@@ -532,13 +542,13 @@ impl Render for Onboarding {
             .on_action(Self::handle_sign_in)
             .on_action(Self::handle_open_account)
             .on_action(cx.listener(|this, _: &ActivateBasicsPage, _, cx| {
-                this.set_page(SelectedPage::Basics, cx);
+                this.set_page(SelectedPage::Basics, Some("action"), cx);
             }))
             .on_action(cx.listener(|this, _: &ActivateEditingPage, _, cx| {
-                this.set_page(SelectedPage::Editing, cx);
+                this.set_page(SelectedPage::Editing, Some("action"), cx);
             }))
             .on_action(cx.listener(|this, _: &ActivateAISetupPage, _, cx| {
-                this.set_page(SelectedPage::AiSetup, cx);
+                this.set_page(SelectedPage::AiSetup, Some("action"), cx);
             }))
             .on_action(cx.listener(|_, _: &menu::SelectNext, window, cx| {
                 window.focus_next();
@@ -806,7 +816,7 @@ impl workspace::SerializableItem for Onboarding {
                     if let Some(page) = page {
                         zlog::info!("Onboarding page {page:?} loaded");
                         onboarding_page.update(cx, |onboarding_page, cx| {
-                            onboarding_page.set_page(page, cx);
+                            onboarding_page.set_page(page, None, cx);
                         })
                     }
                     onboarding_page

crates/onboarding/src/welcome.rs πŸ”—

@@ -1,6 +1,6 @@
 use gpui::{
     Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
-    ParentElement, Render, Styled, Window, actions,
+    ParentElement, Render, Styled, Task, Window, actions,
 };
 use menu::{SelectNext, SelectPrevious};
 use ui::{ButtonLike, Divider, DividerColor, KeyBinding, Vector, VectorName, prelude::*};
@@ -352,3 +352,109 @@ impl Item for WelcomePage {
         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::{define_connection, query, sqlez_macros::sql};
+    use workspace::WorkspaceDb;
+
+    define_connection! {
+        pub static ref WELCOME_PAGES: WelcomePagesDb<WorkspaceDb> =
+            &[
+                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;
+                ),
+            ];
+    }
+
+    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/welcome/Cargo.toml πŸ”—

@@ -1,40 +0,0 @@
-[package]
-name = "welcome"
-version = "0.1.0"
-edition.workspace = true
-publish.workspace = true
-license = "GPL-3.0-or-later"
-
-[lints]
-workspace = true
-
-[lib]
-path = "src/welcome.rs"
-
-[features]
-test-support = []
-
-[dependencies]
-anyhow.workspace = true
-client.workspace = true
-component.workspace = true
-db.workspace = true
-documented.workspace = true
-fuzzy.workspace = true
-gpui.workspace = true
-install_cli.workspace = true
-language.workspace = true
-picker.workspace = true
-project.workspace = true
-serde.workspace = true
-settings.workspace = true
-telemetry.workspace = true
-ui.workspace = true
-util.workspace = true
-vim_mode_setting.workspace = true
-workspace-hack.workspace = true
-workspace.workspace = true
-zed_actions.workspace = true
-
-[dev-dependencies]
-editor = { workspace = true, features = ["test-support"] }

crates/welcome/src/welcome.rs πŸ”—

@@ -1,446 +0,0 @@
-use client::{TelemetrySettings, telemetry::Telemetry};
-use db::kvp::KEY_VALUE_STORE;
-use gpui::{
-    Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
-    ParentElement, Render, Styled, Subscription, Task, WeakEntity, Window, actions, svg,
-};
-use language::language_settings::{EditPredictionProvider, all_language_settings};
-use project::DisableAiSettings;
-use settings::{Settings, SettingsStore};
-use std::sync::Arc;
-use ui::{CheckboxWithLabel, ElevationIndex, Tooltip, prelude::*};
-use util::ResultExt;
-use vim_mode_setting::VimModeSetting;
-use workspace::{
-    AppState, Welcome, Workspace, WorkspaceId,
-    dock::DockPosition,
-    item::{Item, ItemEvent},
-    open_new,
-};
-
-pub use multibuffer_hint::*;
-
-mod base_keymap_picker;
-mod multibuffer_hint;
-
-actions!(
-    welcome,
-    [
-        /// Resets the welcome screen hints to their initial state.
-        ResetHints
-    ]
-);
-
-pub const FIRST_OPEN: &str = "first_open";
-pub const DOCS_URL: &str = "https://zed.dev/docs/";
-
-pub fn init(cx: &mut App) {
-    cx.observe_new(|workspace: &mut Workspace, _, _cx| {
-        workspace.register_action(|workspace, _: &Welcome, window, cx| {
-            let welcome_page = WelcomePage::new(workspace, cx);
-            workspace.add_item_to_active_pane(Box::new(welcome_page), None, true, window, cx)
-        });
-        workspace
-            .register_action(|_workspace, _: &ResetHints, _, cx| MultibufferHint::set_count(0, cx));
-    })
-    .detach();
-
-    base_keymap_picker::init(cx);
-}
-
-pub fn show_welcome_view(app_state: Arc<AppState>, cx: &mut App) -> Task<anyhow::Result<()>> {
-    open_new(
-        Default::default(),
-        app_state,
-        cx,
-        |workspace, window, cx| {
-            workspace.toggle_dock(DockPosition::Left, window, cx);
-            let welcome_page = WelcomePage::new(workspace, cx);
-            workspace.add_item_to_center(Box::new(welcome_page.clone()), window, cx);
-
-            window.focus(&welcome_page.focus_handle(cx));
-
-            cx.notify();
-
-            db::write_and_log(cx, || {
-                KEY_VALUE_STORE.write_kvp(FIRST_OPEN.to_string(), "false".to_string())
-            });
-        },
-    )
-}
-
-pub struct WelcomePage {
-    workspace: WeakEntity<Workspace>,
-    focus_handle: FocusHandle,
-    telemetry: Arc<Telemetry>,
-    _settings_subscription: Subscription,
-}
-
-impl Render for WelcomePage {
-    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let edit_prediction_provider_is_zed =
-            all_language_settings(None, cx).edit_predictions.provider
-                == EditPredictionProvider::Zed;
-
-        let edit_prediction_label = if edit_prediction_provider_is_zed {
-            "Edit Prediction Enabled"
-        } else {
-            "Try Edit Prediction"
-        };
-
-        h_flex()
-            .size_full()
-            .bg(cx.theme().colors().editor_background)
-            .key_context("Welcome")
-            .track_focus(&self.focus_handle(cx))
-            .child(
-                v_flex()
-                    .gap_8()
-                    .mx_auto()
-                    .child(
-                        v_flex()
-                            .w_full()
-                            .child(
-                                svg()
-                                    .path("icons/logo_96.svg")
-                                    .text_color(cx.theme().colors().icon_disabled)
-                                    .w(px(40.))
-                                    .h(px(40.))
-                                    .mx_auto()
-                                    .mb_4(),
-                            )
-                            .child(
-                                h_flex()
-                                    .w_full()
-                                    .justify_center()
-                                    .child(Headline::new("Welcome to Zed")),
-                            )
-                            .child(
-                                h_flex().w_full().justify_center().child(
-                                    Label::new("The editor for what's next")
-                                        .color(Color::Muted)
-                                        .italic(),
-                                ),
-                            ),
-                    )
-                    .child(
-                        h_flex()
-                            .items_start()
-                            .gap_8()
-                            .child(
-                                v_flex()
-                                    .gap_2()
-                                    .pr_8()
-                                    .border_r_1()
-                                    .border_color(cx.theme().colors().border_variant)
-                                    .child(
-                                        self.section_label( cx).child(
-                                            Label::new("Get Started")
-                                                .size(LabelSize::XSmall)
-                                                .color(Color::Muted),
-                                        ),
-                                    )
-                                    .child(
-                                        Button::new("choose-theme", "Choose a Theme")
-                                            .icon(IconName::SwatchBook)
-                                            .icon_size(IconSize::XSmall)
-                                            .icon_color(Color::Muted)
-                                            .icon_position(IconPosition::Start)
-                                            .on_click(cx.listener(|this, _, window, cx| {
-                                                telemetry::event!("Welcome Theme Changed");
-                                                this.workspace
-                                                    .update(cx, |_workspace, cx| {
-                                                        window.dispatch_action(zed_actions::theme_selector::Toggle::default().boxed_clone(), cx);
-                                                    })
-                                                    .ok();
-                                            })),
-                                    )
-                                    .child(
-                                        Button::new("choose-keymap", "Choose a Keymap")
-                                            .icon(IconName::Keyboard)
-                                            .icon_size(IconSize::XSmall)
-                                            .icon_color(Color::Muted)
-                                            .icon_position(IconPosition::Start)
-                                            .on_click(cx.listener(|this, _, window, cx| {
-                                                telemetry::event!("Welcome Keymap Changed");
-                                                this.workspace
-                                                    .update(cx, |workspace, cx| {
-                                                        base_keymap_picker::toggle(
-                                                            workspace,
-                                                            &Default::default(),
-                                                            window, cx,
-                                                        )
-                                                    })
-                                                    .ok();
-                                            })),
-                                    )
-                                    .when(!DisableAiSettings::get_global(cx).disable_ai, |parent| {
-                                        parent.child(
-                                            Button::new(
-                                                "edit_prediction_onboarding",
-                                                edit_prediction_label,
-                                            )
-                                            .disabled(edit_prediction_provider_is_zed)
-                                            .icon(IconName::ZedPredict)
-                                            .icon_size(IconSize::XSmall)
-                                            .icon_color(Color::Muted)
-                                            .icon_position(IconPosition::Start)
-                                            .on_click(
-                                                cx.listener(|_, _, window, cx| {
-                                                    telemetry::event!("Welcome Screen Try Edit Prediction clicked");
-                                                    window.dispatch_action(zed_actions::OpenZedPredictOnboarding.boxed_clone(), cx);
-                                                }),
-                                            ),
-                                        )
-                                    })
-                                    .child(
-                                        Button::new("edit settings", "Edit Settings")
-                                            .icon(IconName::Settings)
-                                            .icon_size(IconSize::XSmall)
-                                            .icon_color(Color::Muted)
-                                            .icon_position(IconPosition::Start)
-                                            .on_click(cx.listener(|_, _, window, cx| {
-                                                telemetry::event!("Welcome Settings Edited");
-                                                window.dispatch_action(Box::new(
-                                                    zed_actions::OpenSettings,
-                                                ), cx);
-                                            })),
-                                    )
-
-                            )
-                            .child(
-                                v_flex()
-                                    .gap_2()
-                                    .child(
-                                        self.section_label(cx).child(
-                                            Label::new("Resources")
-                                                .size(LabelSize::XSmall)
-                                                .color(Color::Muted),
-                                        ),
-                                    )
-                                    .when(cfg!(target_os = "macos"), |el| {
-                                        el.child(
-                                            Button::new("install-cli", "Install the CLI")
-                                                .icon(IconName::Terminal)
-                                                .icon_size(IconSize::XSmall)
-                                                .icon_color(Color::Muted)
-                                                .icon_position(IconPosition::Start)
-                                                .on_click(cx.listener(|this, _, window, cx| {
-                                                    telemetry::event!("Welcome CLI Installed");
-                                                    this.workspace.update(cx, |_, cx|{
-                                                        install_cli::install_cli(window, cx);
-                                                    }).log_err();
-                                                })),
-                                        )
-                                    })
-                                    .child(
-                                        Button::new("view-docs", "View Documentation")
-                                            .icon(IconName::FileCode)
-                                            .icon_size(IconSize::XSmall)
-                                            .icon_color(Color::Muted)
-                                            .icon_position(IconPosition::Start)
-                                            .on_click(cx.listener(|_, _, _, cx| {
-                                                telemetry::event!("Welcome Documentation Viewed");
-                                                cx.open_url(DOCS_URL);
-                                            })),
-                                    )
-                                    .child(
-                                        Button::new("explore-extensions", "Explore Extensions")
-                                            .icon(IconName::Blocks)
-                                            .icon_size(IconSize::XSmall)
-                                            .icon_color(Color::Muted)
-                                            .icon_position(IconPosition::Start)
-                                            .on_click(cx.listener(|_, _, window, cx| {
-                                                telemetry::event!("Welcome Extensions Page Opened");
-                                                window.dispatch_action(Box::new(
-                                                    zed_actions::Extensions::default(),
-                                                ), cx);
-                                            })),
-                                    )
-                            ),
-                    )
-                    .child(
-                        v_container()
-                            .px_2()
-                            .gap_2()
-                            .child(
-                                h_flex()
-                                    .justify_between()
-                                    .child(
-                                        CheckboxWithLabel::new(
-                                            "enable-vim",
-                                            Label::new("Enable Vim Mode"),
-                                            if VimModeSetting::get_global(cx).0 {
-                                                ui::ToggleState::Selected
-                                            } else {
-                                                ui::ToggleState::Unselected
-                                            },
-                                            cx.listener(move |this, selection, _window, cx| {
-                                                telemetry::event!("Welcome Vim Mode Toggled");
-                                                this.update_settings::<VimModeSetting>(
-                                                    selection,
-                                                    cx,
-                                                    |setting, value| *setting = Some(value),
-                                                );
-                                            }),
-                                        )
-                                        .fill()
-                                        .elevation(ElevationIndex::ElevatedSurface),
-                                    )
-                                    .child(
-                                        IconButton::new("vim-mode", IconName::Info)
-                                            .icon_size(IconSize::XSmall)
-                                            .icon_color(Color::Muted)
-                                            .tooltip(
-                                                Tooltip::text(
-                                                    "You can also toggle Vim Mode via the command palette or Editor Controls menu.")
-                                            ),
-                                    ),
-                            )
-                            .child(
-                                CheckboxWithLabel::new(
-                                    "enable-crash",
-                                    Label::new("Send Crash Reports"),
-                                    if TelemetrySettings::get_global(cx).diagnostics {
-                                        ui::ToggleState::Selected
-                                    } else {
-                                        ui::ToggleState::Unselected
-                                    },
-                                    cx.listener(move |this, selection, _window, cx| {
-                                        telemetry::event!("Welcome Diagnostic Telemetry Toggled");
-                                        this.update_settings::<TelemetrySettings>(selection, cx, {
-                                            move |settings, value| {
-                                                settings.diagnostics = Some(value);
-                                                telemetry::event!(
-                                                    "Settings Changed",
-                                                    setting = "diagnostic telemetry",
-                                                    value
-                                                );
-                                            }
-                                        });
-                                    }),
-                                )
-                                .fill()
-                                .elevation(ElevationIndex::ElevatedSurface),
-                            )
-                            .child(
-                                CheckboxWithLabel::new(
-                                    "enable-telemetry",
-                                    Label::new("Send Telemetry"),
-                                    if TelemetrySettings::get_global(cx).metrics {
-                                        ui::ToggleState::Selected
-                                    } else {
-                                        ui::ToggleState::Unselected
-                                    },
-                                    cx.listener(move |this, selection, _window, cx| {
-                                        telemetry::event!("Welcome Metric Telemetry Toggled");
-                                        this.update_settings::<TelemetrySettings>(selection, cx, {
-                                            move |settings, value| {
-                                                settings.metrics = Some(value);
-                                                telemetry::event!(
-                                                    "Settings Changed",
-                                                    setting = "metric telemetry",
-                                                    value
-                                                );
-                                            }
-                                        });
-                                    }),
-                                )
-                                .fill()
-                                .elevation(ElevationIndex::ElevatedSurface),
-                            ),
-                    ),
-            )
-    }
-}
-
-impl WelcomePage {
-    pub fn new(workspace: &Workspace, cx: &mut Context<Workspace>) -> Entity<Self> {
-        let this = cx.new(|cx| {
-            cx.on_release(|_: &mut Self, _| {
-                telemetry::event!("Welcome Page Closed");
-            })
-            .detach();
-
-            WelcomePage {
-                focus_handle: cx.focus_handle(),
-                workspace: workspace.weak_handle(),
-                telemetry: workspace.client().telemetry().clone(),
-                _settings_subscription: cx
-                    .observe_global::<SettingsStore>(move |_, cx| cx.notify()),
-            }
-        });
-
-        this
-    }
-
-    fn section_label(&self, cx: &mut App) -> Div {
-        div()
-            .pl_1()
-            .font_buffer(cx)
-            .text_color(Color::Muted.color(cx))
-    }
-
-    fn update_settings<T: Settings>(
-        &mut self,
-        selection: &ToggleState,
-        cx: &mut Context<Self>,
-        callback: impl 'static + Send + Fn(&mut T::FileContent, bool),
-    ) {
-        if let Some(workspace) = self.workspace.upgrade() {
-            let fs = workspace.read(cx).app_state().fs.clone();
-            let selection = *selection;
-            settings::update_settings_file::<T>(fs, cx, move |settings, _| {
-                let value = match selection {
-                    ToggleState::Unselected => false,
-                    ToggleState::Selected => true,
-                    _ => return,
-                };
-
-                callback(settings, value)
-            });
-        }
-    }
-}
-
-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("Welcome Page Opened")
-    }
-
-    fn show_toolbar(&self) -> bool {
-        false
-    }
-
-    fn clone_on_split(
-        &self,
-        _workspace_id: Option<WorkspaceId>,
-        _: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>> {
-        Some(cx.new(|cx| WelcomePage {
-            focus_handle: cx.focus_handle(),
-            workspace: self.workspace.clone(),
-            telemetry: self.telemetry.clone(),
-            _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
-        }))
-    }
-
-    fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
-        f(*event)
-    }
-}

crates/workspace/src/workspace.rs πŸ”—

@@ -248,8 +248,6 @@ actions!(
         ToggleZoom,
         /// Stops following a collaborator.
         Unfollow,
-        /// Shows the welcome screen.
-        Welcome,
         /// Restores the banner.
         RestoreBanner,
         /// Toggles expansion of the selected item.

crates/zed/Cargo.toml πŸ”—

@@ -2,7 +2,7 @@
 description = "The fast, collaborative code editor."
 edition.workspace = true
 name = "zed"
-version = "0.200.0"
+version = "0.201.0"
 publish.workspace = true
 license = "GPL-3.0-or-later"
 authors = ["Zed Team <hi@zed.dev>"]
@@ -157,7 +157,6 @@ vim_mode_setting.workspace = true
 watch.workspace = true
 web_search.workspace = true
 web_search_providers.workspace = true
-welcome.workspace = true
 workspace-hack.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true

crates/zed/src/main.rs πŸ”—

@@ -20,6 +20,7 @@ use gpui::{App, AppContext as _, Application, AsyncApp, Focusable as _, UpdateGl
 use gpui_tokio::Tokio;
 use http_client::{Url, read_proxy_from_env};
 use language::LanguageRegistry;
+use onboarding::{FIRST_OPEN, show_onboarding_view};
 use prompt_store::PromptBuilder;
 use reqwest_client::ReqwestClient;
 
@@ -44,7 +45,6 @@ use theme::{
 };
 use util::{ResultExt, TryFutureExt, maybe};
 use uuid::Uuid;
-use welcome::{FIRST_OPEN, show_welcome_view};
 use workspace::{
     AppState, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceSettings, WorkspaceStore,
     notifications::NotificationId,
@@ -623,7 +623,6 @@ pub fn main() {
         feedback::init(cx);
         markdown_preview::init(cx);
         svg_preview::init(cx);
-        welcome::init(cx);
         onboarding::init(cx);
         settings_ui::init(cx);
         extensions_ui::init(cx);
@@ -1044,7 +1043,7 @@ async fn restore_or_create_workspace(app_state: Arc<AppState>, cx: &mut AsyncApp
             }
         }
     } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
-        cx.update(|cx| show_welcome_view(app_state, cx))?.await?;
+        cx.update(|cx| show_onboarding_view(app_state, cx))?.await?;
     } else {
         cx.update(|cx| {
             workspace::open_new(

crates/zed/src/zed.rs πŸ”—

@@ -34,6 +34,8 @@ use image_viewer::ImageInfo;
 use language_tools::lsp_tool::{self, LspTool};
 use migrate::{MigrationBanner, MigrationEvent, MigrationNotification, MigrationType};
 use migrator::{migrate_keymap, migrate_settings};
+use onboarding::DOCS_URL;
+use onboarding::multibuffer_hint::MultibufferHint;
 pub use open_listener::*;
 use outline_panel::OutlinePanel;
 use paths::{
@@ -67,7 +69,6 @@ use util::markdown::MarkdownString;
 use util::{ResultExt, asset_str};
 use uuid::Uuid;
 use vim_mode_setting::VimModeSetting;
-use welcome::{DOCS_URL, MultibufferHint};
 use workspace::notifications::{NotificationId, dismiss_app_notification, show_app_notification};
 use workspace::{
     AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings,
@@ -3975,7 +3976,6 @@ mod tests {
             client::init(&app_state.client, cx);
             language::init(cx);
             workspace::init(app_state.clone(), cx);
-            welcome::init(cx);
             onboarding::init(cx);
             Project::init_settings(cx);
             app_state
@@ -4380,7 +4380,6 @@ mod tests {
                 "toolchain",
                 "variable_list",
                 "vim",
-                "welcome",
                 "workspace",
                 "zed",
                 "zed_predict_onboarding",

crates/zed/src/zed/app_menus.rs πŸ”—

@@ -249,7 +249,7 @@ pub fn app_menus() -> Vec<Menu> {
                 ),
                 MenuItem::action("View Telemetry", zed_actions::OpenTelemetryLog),
                 MenuItem::action("View Dependency Licenses", zed_actions::OpenLicenses),
-                MenuItem::action("Show Welcome", workspace::Welcome),
+                MenuItem::action("Show Welcome", onboarding::ShowWelcome),
                 MenuItem::action("Give Feedback...", zed_actions::feedback::GiveFeedback),
                 MenuItem::separator(),
                 MenuItem::action(

crates/zed/src/zed/component_preview.rs πŸ”—

@@ -761,7 +761,7 @@ impl Render for ComponentPreview {
                         )
                         .track_scroll(self.nav_scroll_handle.clone())
                         .p_2p5()
-                        .w(px(229.))
+                        .w(px(231.)) // Matches perfectly with the size of the "Component Preview" tab, if that's the first one in the pane
                         .h_full()
                         .flex_1(),
                     )

crates/zed/src/zed/open_listener.rs πŸ”—

@@ -15,6 +15,8 @@ use futures::{FutureExt, SinkExt, StreamExt};
 use git_ui::file_diff_view::FileDiffView;
 use gpui::{App, AsyncApp, Global, WindowHandle};
 use language::Point;
+use onboarding::FIRST_OPEN;
+use onboarding::show_onboarding_view;
 use recent_projects::{SshSettings, open_ssh_project};
 use remote::SshConnectionOptions;
 use settings::Settings;
@@ -24,7 +26,6 @@ use std::thread;
 use std::time::Duration;
 use util::ResultExt;
 use util::paths::PathWithPosition;
-use welcome::{FIRST_OPEN, show_welcome_view};
 use workspace::item::ItemHandle;
 use workspace::{AppState, OpenOptions, SerializedWorkspaceLocation, Workspace};
 
@@ -378,7 +379,7 @@ async fn open_workspaces(
     if grouped_locations.is_empty() {
         // If we have no paths to open, show the welcome screen if this is the first launch
         if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
-            cx.update(|cx| show_welcome_view(app_state, cx).detach())
+            cx.update(|cx| show_onboarding_view(app_state, cx).detach())
                 .log_err();
         }
         // If not the first launch, show an empty window with empty editor

docs/src/SUMMARY.md πŸ”—

@@ -21,6 +21,7 @@
 - [Icon Themes](./icon-themes.md)
 - [Visual Customization](./visual-customization.md)
 - [Vim Mode](./vim.md)
+- [Helix Mode](./helix.md)
 
 <!-- - [Globs](./globs.md) -->
 <!-- - [Fonts](./fonts.md) -->

docs/src/configuring-zed.md πŸ”—

@@ -3195,10 +3195,16 @@ Run the `theme selector: toggle` action in the command palette to see a current
 
 ## Vim
 
-- Description: Whether or not to enable vim mode (work in progress).
+- Description: Whether or not to enable vim mode. See the [Vim documentation](./vim.md) for more details on configuration.
 - Setting: `vim_mode`
 - Default: `false`
 
+## Helix Mode
+
+- Description: Whether or not to enable Helix mode. Enabling `helix_mode` also enables `vim_mode`. See the [Helix documentation](./helix.md) for more details.
+- Setting: `helix_mode`
+- Default: `false`
+
 ## Project Panel
 
 - Description: Customize project panel

docs/src/helix.md πŸ”—

@@ -0,0 +1,11 @@
+# Helix Mode
+
+_Work in progress! Not all Helix keybindings are implemented yet._
+
+Zed's Helix mode is an emulation layer that brings Helix-style keybindings and modal editing to Zed. It builds upon Zed's [Vim mode](./vim.md), so much of the core functionality is shared. Enabling `helix_mode` will also enable `vim_mode`.
+
+For a guide on Vim-related features that are also available in Helix mode, please refer to our [Vim mode documentation](./vim.md).
+
+To check the current status of Helix mode, or to request a missing Helix feature, checkout out the ["Are we Helix yet?" discussion](https://github.com/zed-industries/zed/discussions/33580).
+
+For a detailed list of Helix's default keybindings, please visit the [official Helix documentation](https://docs.helix-editor.com/keymap.html).

docs/src/key-bindings.md πŸ”—

@@ -14,7 +14,7 @@ If you're used to a specific editor's defaults you can set a `base_keymap` in yo
 - TextMate
 - None (disables _all_ key bindings)
 
-You can also enable `vim_mode`, which adds vim bindings too.
+You can also enable `vim_mode` or `helix_mode`, which add modal bindings. For more information, see the documentation for [Vim mode](./vim.md) and [Helix mode](./helix.md).
 
 ## User keymaps
 
@@ -119,7 +119,7 @@ It's worth noting that attributes are only available on the node they are define
 
 Note: Before Zed v0.197.x, the ! operator only looked at one node at a time, and `>` meant "parent" not "ancestor". This meant that `!Editor` would match the context `Workspace > Pane > Editor`, because (confusingly) the Pane matches `!Editor`, and that `os=macos > Editor` did not match the context `Workspace > Pane > Editor` because of the intermediate `Pane` node.
 
-If you're using Vim mode, we have information on how [vim modes influence the context](./vim.md#contexts)
+If you're using Vim mode, we have information on how [vim modes influence the context](./vim.md#contexts). Helix mode is built on top of Vim mode and uses the same contexts.
 
 ### Actions
 

docs/src/telemetry.md πŸ”—

@@ -4,7 +4,8 @@ Zed collects anonymous telemetry data to help the team understand how people are
 
 ## Configuring Telemetry Settings
 
-You have full control over what data is sent out by Zed. To enable or disable some or all telemetry types, open your `settings.json` file via {#action zed::OpenSettings}({#kb zed::OpenSettings}) from the command palette.
+You have full control over what data is sent out by Zed.
+To enable or disable some or all telemetry types, open your `settings.json` file via {#action zed::OpenSettings}({#kb zed::OpenSettings}) from the command palette.
 
 Insert and tweak the following:
 
@@ -15,8 +16,6 @@ Insert and tweak the following:
 },
 ```
 
-The telemetry settings can also be configured via the welcome screen, which can be invoked via the {#action workspace::Welcome} action in the command palette.
-
 ## Dataflow
 
 Telemetry is sent from the application to our servers. Data is proxied through our servers to enable us to easily switch analytics services. We currently use:

extensions/emmet/Cargo.toml πŸ”—

@@ -1,6 +1,6 @@
 [package]
 name = "zed_emmet"
-version = "0.0.5"
+version = "0.0.6"
 edition.workspace = true
 publish.workspace = true
 license = "Apache-2.0"

extensions/emmet/extension.toml πŸ”—

@@ -1,7 +1,7 @@
 id = "emmet"
 name = "Emmet"
 description = "Emmet support"
-version = "0.0.5"
+version = "0.0.6"
 schema_version = 1
 authors = ["Piotr Osiewicz <piotr@zed.dev>"]
 repository = "https://github.com/zed-industries/zed"

extensions/emmet/src/emmet.rs πŸ”—

@@ -5,7 +5,7 @@ struct EmmetExtension {
     did_find_server: bool,
 }
 
-const SERVER_PATH: &str = "node_modules/.bin/emmet-language-server";
+const SERVER_PATH: &str = "node_modules/@olrtg/emmet-language-server/dist/index.js";
 const PACKAGE_NAME: &str = "@olrtg/emmet-language-server";
 
 impl EmmetExtension {