From 6c1f19571a5647bff02ef98b7de159754a884504 Mon Sep 17 00:00:00 2001 From: Gilmar Sales Date: Wed, 13 Aug 2025 12:59:59 -0300 Subject: [PATCH 01/10] Enhance icon detection for files with custom suffixes (#34170) Fixes custom file suffixes (module.ts) of some icon themes like: - **Symbols Icon Theme** image - **Bearded Icon Theme** image Release Notes: - Fixed icon detection for files with custom suffixes like `module.ts` that are overwritten by the language's icon `.ts` --- crates/file_icons/src/file_icons.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/crates/file_icons/src/file_icons.rs b/crates/file_icons/src/file_icons.rs index 2f159771b1..82a8e05d85 100644 --- a/crates/file_icons/src/file_icons.rs +++ b/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 From a7442d8880df3019fd47479849cd4b9653bae364 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Wed, 13 Aug 2025 12:02:14 -0400 Subject: [PATCH 02/10] onboarding: Add more telemetry (#36121) 1. Welcome Page Open 2. Welcome Nav clicked 3. Skip clicked 4. Font changed 5. Import settings clicked 6. Inlay Hints 7. Git Blame 8. Format on Save 9. Font Ligature 10. Ai Enabled 11. Ai Provider Modal open Release Notes: - N/A --------- Co-authored-by: Marshall Bowers --- Cargo.lock | 1 + crates/onboarding/Cargo.toml | 1 + crates/onboarding/src/ai_setup_page.rs | 24 ++++++++--- crates/onboarding/src/editing_page.rs | 59 +++++++++++++++++++++++--- crates/onboarding/src/onboarding.rs | 45 +++++++++++++++++--- 5 files changed, 111 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9078b32f7a..b67188c53b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11172,6 +11172,7 @@ dependencies = [ "schemars", "serde", "settings", + "telemetry", "theme", "ui", "util", diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index cb07bb5dab..8aed1e3287 100644 --- a/crates/onboarding/Cargo.toml +++ b/crates/onboarding/Cargo.toml @@ -38,6 +38,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 diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index 8203f96479..bb1932bdf2 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/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 = ::global(cx); update_settings_file::( fs, cx, move |ai_settings: &mut Option, _| { - *ai_settings = match toggle_state { - ToggleState::Indeterminate => None, - ToggleState::Unselected => Some(true), - ToggleState::Selected => Some(false), - }; + *ai_settings = Some(enabled); }, ); }, diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index c69bd3852e..aa7f4eee74 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/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::(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 = ::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 = ::global(cx); update_settings_file::(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 = ::global(cx); update_settings_file::(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({ diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index c86871c919..3fb6f9b520 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -214,6 +214,7 @@ pub fn init(cx: &mut App) { } pub fn show_onboarding_view(app_state: Arc, cx: &mut App) -> Task> { + telemetry::event!("Onboarding Page Opened"); open_new( Default::default(), app_state, @@ -242,6 +243,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, focus_handle: FocusHandle, @@ -261,7 +272,21 @@ impl Onboarding { }) } - fn set_page(&mut self, page: SelectedPage, cx: &mut Context) { + fn set_page( + &mut self, + page: SelectedPage, + clicked: Option<&'static str>, + cx: &mut Context, + ) { + 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 +350,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 +505,7 @@ impl Onboarding { } fn on_finish(_: &Finish, _: &mut Window, cx: &mut App) { + telemetry::event!("Welcome Skip Clicked"); go_to_welcome_page(cx); } @@ -532,13 +563,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 +837,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 From d9a94a54966cf4e1ebd3aa292ed24b55d7927afb Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 13 Aug 2025 13:18:24 -0300 Subject: [PATCH 03/10] onboarding: Remove feature flag and old welcome crate (#36110) Release Notes: - N/A --------- Co-authored-by: MrSubidubi Co-authored-by: Anthony --- Cargo.lock | 30 -- Cargo.toml | 2 - crates/onboarding/Cargo.toml | 2 - .../src/base_keymap_picker.rs | 2 +- .../src/multibuffer_hint.rs | 0 crates/onboarding/src/onboarding.rs | 52 +- 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 | 1 - crates/zed/src/main.rs | 5 +- crates/zed/src/zed.rs | 5 +- crates/zed/src/zed/app_menus.rs | 2 +- crates/zed/src/zed/open_listener.rs | 5 +- docs/src/telemetry.md | 5 +- 16 files changed, 26 insertions(+), 574 deletions(-) rename crates/{welcome => onboarding}/src/base_keymap_picker.rs (99%) rename crates/{welcome => onboarding}/src/multibuffer_hint.rs (100%) delete mode 100644 crates/welcome/Cargo.toml delete mode 120000 crates/welcome/LICENSE-GPL delete mode 100644 crates/welcome/src/welcome.rs diff --git a/Cargo.lock b/Cargo.lock index b67188c53b..8458c4af4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11152,12 +11152,10 @@ dependencies = [ "ai_onboarding", "anyhow", "client", - "command_palette_hooks", "component", "db", "documented", "editor", - "feature_flags", "fs", "fuzzy", "git", @@ -18888,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" @@ -20669,7 +20640,6 @@ dependencies = [ "watch", "web_search", "web_search_providers", - "welcome", "windows 0.61.1", "winresource", "workspace", diff --git a/Cargo.toml b/Cargo.toml index 8cb3c34a8a..1baa6d3d74 100644 --- a/Cargo.toml +++ b/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" } diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index 8aed1e3287..4157be3172 100644 --- a/crates/onboarding/Cargo.toml +++ b/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 diff --git a/crates/welcome/src/base_keymap_picker.rs b/crates/onboarding/src/base_keymap_picker.rs similarity index 99% rename from crates/welcome/src/base_keymap_picker.rs rename to crates/onboarding/src/base_keymap_picker.rs index 92317ca711..0ac07d9a9d 100644 --- a/crates/welcome/src/base_keymap_picker.rs +++ b/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 diff --git a/crates/welcome/src/multibuffer_hint.rs b/crates/onboarding/src/multibuffer_hint.rs similarity index 100% rename from crates/welcome/src/multibuffer_hint.rs rename to crates/onboarding/src/multibuffer_hint.rs diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 3fb6f9b520..2444e5d44a 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/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,34 +186,8 @@ pub fn init(cx: &mut App) { }) .detach(); - cx.observe_new::(|_, window, cx| { - let Some(window) = window else { - return; - }; + base_keymap_picker::init(cx); - let onboarding_actions = [ - std::any::TypeId::of::(), - std::any::TypeId::of::(), - ]; - - CommandPaletteFilter::update_global(cx, |filter, _cx| { - filter.hide_action_types(&onboarding_actions); - }); - - cx.observe_flag::(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::(cx); } diff --git a/crates/welcome/Cargo.toml b/crates/welcome/Cargo.toml deleted file mode 100644 index acb3fe0f84..0000000000 --- a/crates/welcome/Cargo.toml +++ /dev/null @@ -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"] } diff --git a/crates/welcome/LICENSE-GPL b/crates/welcome/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/welcome/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs deleted file mode 100644 index b0a1c316f4..0000000000 --- a/crates/welcome/src/welcome.rs +++ /dev/null @@ -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, cx: &mut App) -> Task> { - 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, - focus_handle: FocusHandle, - telemetry: Arc, - _settings_subscription: Subscription, -} - -impl Render for WelcomePage { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> 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::( - 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::(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::(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) -> Entity { - 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::(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( - &mut self, - selection: &ToggleState, - cx: &mut Context, - 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::(fs, cx, move |settings, _| { - let value = match selection { - ToggleState::Unselected => false, - ToggleState::Selected => true, - _ => return, - }; - - callback(settings, value) - }); - } - } -} - -impl EventEmitter 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, - _: &mut Window, - cx: &mut Context, - ) -> Option> { - Some(cx.new(|cx| WelcomePage { - focus_handle: cx.focus_handle(), - workspace: self.workspace.clone(), - telemetry: self.telemetry.clone(), - _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), - })) - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { - f(*event) - } -} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 98794e54cd..fb78c62f9e 100644 --- a/crates/workspace/src/workspace.rs +++ b/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. diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 5997e43864..bdbb39698c 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -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 diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 3084bfddad..fd987ef6c5 100644 --- a/crates/zed/src/main.rs +++ b/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, 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( diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 8c89a7d85a..23020d3a9b 100644 --- a/crates/zed/src/zed.rs +++ b/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", diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index 53eec42ba0..9df55a2fb1 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -249,7 +249,7 @@ pub fn app_menus() -> Vec { ), 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( diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 2fd9b0a68c..82d3795e94 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/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 diff --git a/docs/src/telemetry.md b/docs/src/telemetry.md index 107aef5a96..46c39a88ae 100644 --- a/docs/src/telemetry.md +++ b/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: From 2da80e46418c9e59c83207e0e1b7d44c6ebc0462 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 13 Aug 2025 12:34:18 -0400 Subject: [PATCH 04/10] emmet: Use `index.js` directly to launch language server (#36126) This PR updates the Emmet extension to use the `index.js` file directly to launch the language server. This provides better cross-platform support, as we're not relying on platform-specific `.bin` wrappers. Release Notes: - N/A --- extensions/emmet/src/emmet.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/emmet/src/emmet.rs b/extensions/emmet/src/emmet.rs index e4fb3cf814..1434e16e88 100644 --- a/extensions/emmet/src/emmet.rs +++ b/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 { From 0b9c9f5f2da008336c3e1a7648489764f4e87cca Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Wed, 13 Aug 2025 12:42:09 -0400 Subject: [PATCH 05/10] onboarding: Make Welcome page persistent (#36127) Release Notes: - N/A --- crates/onboarding/src/onboarding.rs | 1 + crates/onboarding/src/welcome.rs | 108 +++++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 2444e5d44a..e07a8dc9fb 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -189,6 +189,7 @@ pub fn init(cx: &mut App) { base_keymap_picker::init(cx); register_serializable_item::(cx); + register_serializable_item::(cx); } pub fn show_onboarding_view(app_state: Arc, cx: &mut App) -> Task> { diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs index 65baad03a0..ba0053a3b6 100644 --- a/crates/onboarding/src/welcome.rs +++ b/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, + _window: &mut Window, + cx: &mut App, + ) -> Task> { + workspace::delete_unloaded_items( + alive_items, + workspace_id, + "welcome_pages", + &persistence::WELCOME_PAGES, + cx, + ) + } + + fn deserialize( + _project: Entity, + _workspace: gpui::WeakEntity, + workspace_id: workspace::WorkspaceId, + item_id: workspace::ItemId, + window: &mut Window, + cx: &mut App, + ) -> Task>> { + 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, + ) -> Option>> { + 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 = + &[ + 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 { + SELECT is_open + FROM welcome_pages + WHERE item_id = ? AND workspace_id = ? + } + } + } +} From 4238e640fa9a99acfa7edd08f1f7b27b5f5a649f Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 13 Aug 2025 12:55:02 -0400 Subject: [PATCH 06/10] emmet: Bump to v0.0.6 (#36129) This PR bumps the Emmet extension to v0.0.6. Changes: - https://github.com/zed-industries/zed/pull/36126 Release Notes: - N/A --- Cargo.lock | 2 +- extensions/emmet/Cargo.toml | 2 +- extensions/emmet/extension.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8458c4af4b..ac1a56d53f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20663,7 +20663,7 @@ dependencies = [ [[package]] name = "zed_emmet" -version = "0.0.5" +version = "0.0.6" dependencies = [ "zed_extension_api 0.1.0", ] diff --git a/extensions/emmet/Cargo.toml b/extensions/emmet/Cargo.toml index ff9debdea9..2fbdf2a7e5 100644 --- a/extensions/emmet/Cargo.toml +++ b/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" diff --git a/extensions/emmet/extension.toml b/extensions/emmet/extension.toml index 0ebb801f9d..a1848400b8 100644 --- a/extensions/emmet/extension.toml +++ b/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 "] repository = "https://github.com/zed-industries/zed" From 9a375f14192c9bc9bec54777ec74d1444e11e593 Mon Sep 17 00:00:00 2001 From: ponychicken Date: Wed, 13 Aug 2025 19:36:18 +0200 Subject: [PATCH 07/10] Add some documentation for Helix mode (#35641) Because there is literally no mention of it in the docs Release Notes: - N/A --------- Co-authored-by: ponychicken <183302+ponychicken@users.noreply.github.com> Co-authored-by: Ben Kunkle --- docs/src/SUMMARY.md | 1 + docs/src/configuring-zed.md | 8 +++++++- docs/src/helix.md | 11 +++++++++++ docs/src/key-bindings.md | 4 ++-- 4 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 docs/src/helix.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index fc936d6bd0..c7af36f431 100644 --- a/docs/src/SUMMARY.md +++ b/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) diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 1996e1c4ee..5d11dfe833 100644 --- a/docs/src/configuring-zed.md +++ b/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 diff --git a/docs/src/helix.md b/docs/src/helix.md new file mode 100644 index 0000000000..ddf997d3f0 --- /dev/null +++ b/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). diff --git a/docs/src/key-bindings.md b/docs/src/key-bindings.md index feed912787..9fc94840b7 100644 --- a/docs/src/key-bindings.md +++ b/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 From cb0bc463f103bd5a00d01e0229b9059ea6036d8b Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 13 Aug 2025 14:45:37 -0300 Subject: [PATCH 08/10] agent2: Add new "new thread" selector in the toolbar (#36133) Release Notes: - N/A --- assets/keymaps/default-linux.json | 1 + assets/keymaps/default-macos.json | 1 + crates/agent_ui/src/agent_panel.rs | 838 ++++++++++++-------- 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/zed/src/zed/component_preview.rs | 2 +- 7 files changed, 526 insertions(+), 328 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 708432393c..dda26f406b 100644 --- a/assets/keymaps/default-linux.json +++ b/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", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index abb741af29..3966efd8df 100644 --- a/assets/keymaps/default-macos.json +++ b/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", diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index d07581da93..a641d62296 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -12,12 +12,12 @@ use serde::{Deserialize, Serialize}; use crate::NewExternalAgentThread; use crate::agent_diff::AgentDiffThread; use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES}; -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}, @@ -67,8 +67,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::{ @@ -86,6 +86,7 @@ const AGENT_PANEL_KEY: &str = "agent_panel"; #[derive(Serialize, Deserialize)] struct SerializedAgentPanel { width: Option, + selected_agent: Option, } pub fn init(cx: &mut App) { @@ -179,6 +180,14 @@ pub fn init(cx: &mut App) { }); } }) + .register_action(|workspace, _: &ToggleNewThreadMenu, window, cx| { + if let Some(panel) = workspace.panel::(cx) { + workspace.focus_panel::(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) }) @@ -223,6 +232,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 { + 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 { @@ -453,16 +492,21 @@ pub struct AgentPanel { zoomed: bool, pending_serialization: Option>>, onboarding: Entity, + selected_agent: AgentType, } impl AgentPanel { fn serialize(&mut self, cx: &mut Context) { 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(()) @@ -531,6 +575,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(); }); } @@ -732,6 +779,7 @@ impl AgentPanel { zoomed: false, pending_serialization: None, onboarding, + selected_agent: AgentType::default(), } } @@ -1174,6 +1222,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.new_thread_menu_handle.toggle(window, cx); + } + pub fn increase_font_size( &mut self, action: &IncreaseBufferFontSize, @@ -1581,6 +1638,17 @@ impl AgentPanel { menu } + + pub fn set_selected_agent(&mut self, agent: AgentType, cx: &mut Context) { + 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 { @@ -1811,200 +1879,24 @@ impl AgentPanel { .into_any() } - fn render_toolbar(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render_panel_options_menu( + &self, + window: &mut Window, + cx: &mut Context, + ) -> 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 focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Go Back", - &workspace::GoBack, - &focus_handle, - window, - cx, - ) - } - }), - ); - - 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, - ) - } - }, - ) - .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" }; - 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("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small), - Tooltip::text("New Thread…"), - ) - .anchor(Corner::TopRight) - .with_handle(self.new_thread_menu_handle.clone()) - .menu({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - let active_thread = active_thread.clone(); - Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { - menu = menu - .context(focus_handle.clone()) - .when(cx.has_flag::(), |this| { - this.header("Zed Agent") - }) - .when_some(active_thread, |this, active_thread| { - let thread = active_thread.read(cx); - - 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(move |window, cx| { - 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(move |window, cx| { - window.dispatch_action(NewTextThread.boxed_clone(), cx); - }), - ) - .when(cx.has_flag::(), |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") + PopoverMenu::new("agent-options-menu") .trigger_with_tooltip( IconButton::new("agent-options-menu", IconName::Ellipsis) .icon_size(IconSize::Small), @@ -2087,6 +1979,139 @@ impl AgentPanel { menu })) } + }) + } + + fn render_recent_entries_menu( + &self, + icon: IconName, + cx: &mut Context, + ) -> 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| { + 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() + } + }) + } + + fn render_toolbar_back_button(&self, cx: &mut Context) -> 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) -> 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("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small), + Tooltip::text("New Thread…"), + ) + .anchor(Corner::TopRight) + .with_handle(self.new_thread_menu_handle.clone()) + .menu({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + let active_thread = active_thread.clone(); + Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { + menu = menu + .context(focus_handle.clone()) + .when_some(active_thread, |this, active_thread| { + let thread = active_thread.read(cx); + + 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(move |window, cx| { + 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(move |window, cx| { + window.dispatch_action(NewTextThread.boxed_clone(), cx); + }), + ); + menu + })) + } }); h_flex() @@ -2105,8 +2130,12 @@ impl AgentPanel { .pl_1() .gap_1() .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() + } + _ => self + .render_recent_entries_menu(IconName::MenuAlt, cx) + .into_any_element(), }) .child(self.render_title_view(window, cx)), ) @@ -2123,11 +2152,308 @@ impl AgentPanel { .border_l_1() .border_color(cx.theme().colors().border) .child(new_thread_menu) - .child(agent_panel_menu), + .child(self.render_panel_options_menu(window, cx)), ), ) } + fn render_toolbar_new(&self, window: &mut Window, cx: &mut Context) -> 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( + 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( + "New…", + &ToggleNewThreadMenu, + &focus_handle, + window, + cx, + ) + } + }, + ) + .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| { + 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); + + 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::(cx) + { + panel.update(cx, |panel, cx| { + panel.set_selected_agent( + AgentType::Zed, + cx, + ); + }); + } + }); + } + 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::(cx) + { + panel.update(cx, |panel, cx| { + panel.set_selected_agent( + AgentType::TextThread, + cx, + ); + }); + } + }); + } + 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::(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, + ); + } + }), + ) + .separator() + .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::(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::(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("agent-panel-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() + .gap(DynamicSpacing::Base08.rems(cx)) + .child(match &self.active_view { + 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)), + ) + .child( + h_flex() + .h_full() + .gap_2() + .children(self.render_token_count(cx)) + .child( + h_flex() + .h_full() + .gap(DynamicSpacing::Base02.rems(cx)) + .pl(DynamicSpacing::Base04.rems(cx)) + .pr(DynamicSpacing::Base06.rems(cx)) + .border_l_1() + .border_color(cx.theme().colors().border) + .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) -> impl IntoElement { + if cx.has_flag::() { + 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 { match &self.active_view { ActiveView::Thread { @@ -2576,138 +2902,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::(), |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)) }) diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index b776c0830b..231b9cfb38 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/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. diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index b477a8c385..beeaf0c43b 100644 --- a/crates/agent_ui/src/ui.rs +++ b/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::*; diff --git a/crates/agent_ui/src/ui/new_thread_button.rs b/crates/agent_ui/src/ui/new_thread_button.rs index 7764144150..347d6adcaf 100644 --- a/crates/agent_ui/src/ui/new_thread_button.rs +++ b/crates/agent_ui/src/ui/new_thread_button.rs @@ -11,7 +11,7 @@ pub struct NewThreadButton { } impl NewThreadButton { - pub fn new(id: impl Into, label: impl Into, icon: IconName) -> Self { + fn new(id: impl Into, label: impl Into, icon: IconName) -> Self { Self { id: id.into(), label: label.into(), @@ -21,12 +21,12 @@ impl NewThreadButton { } } - pub fn keybinding(mut self, keybinding: Option) -> Self { + fn keybinding(mut self, keybinding: Option) -> Self { self.keybinding = keybinding; self } - pub fn on_click(mut self, handler: F) -> Self + fn on_click(mut self, handler: F) -> Self where F: Fn(&mut Window, &mut App) + 'static, { diff --git a/crates/zed/src/zed/component_preview.rs b/crates/zed/src/zed/component_preview.rs index ac889a7ad9..4609ecce9b 100644 --- a/crates/zed/src/zed/component_preview.rs +++ b/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(), ) From e52f1483049aa6c0b155c04fb4808aeb9a4bcd1a Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 13 Aug 2025 13:56:51 -0400 Subject: [PATCH 09/10] Bump Zed to v0.201 (#36132) Release Notes: -N/A --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ac1a56d53f..3b1337eece 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20500,7 +20500,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.200.0" +version = "0.201.0" dependencies = [ "activity_indicator", "agent", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index bdbb39698c..4335f2d5a1 100644 --- a/crates/zed/Cargo.toml +++ b/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 "] From 4a3549882905e07b2d4ef259bff7da30d70aa922 Mon Sep 17 00:00:00 2001 From: smit Date: Thu, 14 Aug 2025 00:19:37 +0530 Subject: [PATCH 10/10] copilot: Fix Copilot fails to sign in (#36138) Closes #36093 Pin copilot version to 1.354 for now until further investigation. Release Notes: - Fixes issue where Copilot failed to sign in. Co-authored-by: MrSubidubi --- crates/copilot/src/copilot.rs | 12 ++++++------ 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 ++++++++++++++- 9 files changed, 52 insertions(+), 11 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 49ae2b9d9c..166a582c70 100644 --- a/crates/copilot/src/copilot.rs +++ b/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, 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, 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?; } diff --git a/crates/languages/src/css.rs b/crates/languages/src/css.rs index 7725e079be..19329fcc6e 100644 --- a/crates/languages/src/css.rs +++ b/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 { diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index ca82bb2431..019b45d396 100644 --- a/crates/languages/src/json.rs +++ b/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 { diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 0524c02fd5..5513324487 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -206,6 +206,7 @@ impl LspAdapter for PythonLspAdapter { &server_path, &container_dir, &version, + Default::default(), ) .await; diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index a7edbb148c..6f03eeda8d 100644 --- a/crates/languages/src/tailwind.rs +++ b/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 { diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index f976b62614..a8ba880889 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -589,6 +589,7 @@ impl LspAdapter for TypeScriptLspAdapter { &server_path, &container_dir, version.typescript_version.as_str(), + Default::default(), ) .await; diff --git a/crates/languages/src/vtsls.rs b/crates/languages/src/vtsls.rs index 33751f733e..73498fc579 100644 --- a/crates/languages/src/vtsls.rs +++ b/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 { diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index 815605d524..28be2cc1a4 100644 --- a/crates/languages/src/yaml.rs +++ b/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 { diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 08698a1d6c..6fcc3a728a 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/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>); @@ -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, + } } }