From 5f0212de5fb76c91c4b8991f1bc8ed1464a2e527 Mon Sep 17 00:00:00 2001
From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Date: Thu, 27 Nov 2025 13:10:55 -0300
Subject: [PATCH] Better promote edit prediction when signed out (#43665)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Introducing this little popover here that's aimed at better
communicating what Zed's built-in edit prediction feature is and how
much people can get of it for free by purely just signing in.
Release Notes:
- N/A
---
Cargo.lock | 1 +
crates/client/src/zed_urls.rs | 8 +
crates/edit_prediction_button/Cargo.toml | 1 +
.../src/edit_prediction_button.rs | 145 +++++++++++++++++-
4 files changed, 149 insertions(+), 6 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 8d4baa2e5221c23ff57a227a94dae4ae3859ec83..082f420f11f12968dd5c6bec46c3fab4f2b37a7f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5308,6 +5308,7 @@ dependencies = [
"telemetry",
"theme",
"ui",
+ "util",
"workspace",
"zed_actions",
"zeta",
diff --git a/crates/client/src/zed_urls.rs b/crates/client/src/zed_urls.rs
index 957d6c68f773db025b4ee604666f5b3d8101148b..2fe47251695446b54d6766c9a52bbd2da366d34e 100644
--- a/crates/client/src/zed_urls.rs
+++ b/crates/client/src/zed_urls.rs
@@ -59,3 +59,11 @@ pub fn agent_server_docs(cx: &App) -> String {
server_url = server_url(cx)
)
}
+
+/// Returns the URL to Zed's edit prediction documentation.
+pub fn edit_prediction_docs(cx: &App) -> String {
+ format!(
+ "{server_url}/docs/ai/edit-prediction",
+ server_url = server_url(cx)
+ )
+}
diff --git a/crates/edit_prediction_button/Cargo.toml b/crates/edit_prediction_button/Cargo.toml
index 9062aca3c56f527385aecb000ebcd625f588eb9a..b7ec07e1e2b24d1d1b851913195afdbf58376da5 100644
--- a/crates/edit_prediction_button/Cargo.toml
+++ b/crates/edit_prediction_button/Cargo.toml
@@ -32,6 +32,7 @@ settings.workspace = true
supermaven.workspace = true
telemetry.workspace = true
ui.workspace = true
+util.workspace = true
workspace.workspace = true
zed_actions.workspace = true
zeta.workspace = true
diff --git a/crates/edit_prediction_button/src/edit_prediction_button.rs b/crates/edit_prediction_button/src/edit_prediction_button.rs
index 254caa698aa05214f73a749e540233952db4978b..ba371b53aebe8c8f2db01501e01391125341a457 100644
--- a/crates/edit_prediction_button/src/edit_prediction_button.rs
+++ b/crates/edit_prediction_button/src/edit_prediction_button.rs
@@ -11,7 +11,7 @@ use fs::Fs;
use gpui::{
Action, Animation, AnimationExt, App, AsyncWindowContext, Corner, Entity, FocusHandle,
Focusable, IntoElement, ParentElement, Render, Subscription, WeakEntity, actions, div,
- pulsating_between,
+ ease_in_out, pulsating_between,
};
use indoc::indoc;
use language::{
@@ -34,6 +34,7 @@ use ui::{
Clickable, ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, IconButton,
IconButtonShape, Indicator, PopoverMenu, PopoverMenuHandle, ProgressBar, Tooltip, prelude::*,
};
+use util::ResultExt as _;
use workspace::{
StatusItemView, Toast, Workspace, create_and_open_local_file, item::ItemHandle,
notifications::NotificationId,
@@ -322,7 +323,7 @@ impl Render for EditPredictionButton {
let tooltip_meta = if self.user_store.read(cx).current_user().is_some() {
"Choose a Plan"
} else {
- "Sign In"
+ "Sign In To Use"
};
return div().child(
@@ -357,6 +358,7 @@ impl Render for EditPredictionButton {
}
let show_editor_predictions = self.editor_show_predictions;
+ let user = self.user_store.read(cx).current_user();
let icon_button = IconButton::new("zed-predict-pending-button", zeta_icon)
.shape(IconButtonShape::Square)
@@ -372,10 +374,18 @@ impl Render for EditPredictionButton {
},
)
.when(!self.popover_menu_handle.is_deployed(), |element| {
+ let user = user.clone();
element.tooltip(move |_window, cx| {
if enabled {
if show_editor_predictions {
Tooltip::for_action("Edit Prediction", &ToggleMenu, cx)
+ } else if user.is_none() {
+ Tooltip::with_meta(
+ "Edit Prediction",
+ Some(&ToggleMenu),
+ "Sign In To Use",
+ cx,
+ )
} else {
Tooltip::with_meta(
"Edit Prediction",
@@ -398,11 +408,25 @@ impl Render for EditPredictionButton {
let this = cx.weak_entity();
let mut popover_menu = PopoverMenu::new("zeta")
- .menu(move |window, cx| {
- this.update(cx, |this, cx| {
- this.build_zeta_context_menu(provider, window, cx)
+ .when(user.is_some(), |popover_menu| {
+ let this = this.clone();
+
+ popover_menu.menu(move |window, cx| {
+ this.update(cx, |this, cx| {
+ this.build_zeta_context_menu(provider, window, cx)
+ })
+ .ok()
+ })
+ })
+ .when(user.is_none(), |popover_menu| {
+ let this = this.clone();
+
+ popover_menu.menu(move |window, cx| {
+ this.update(cx, |this, cx| {
+ this.build_zeta_upsell_context_menu(window, cx)
+ })
+ .ok()
})
- .ok()
})
.anchor(Corner::BottomRight)
.with_handle(self.popover_menu_handle.clone());
@@ -1045,6 +1069,55 @@ impl EditPredictionButton {
})
}
+ fn build_zeta_upsell_context_menu(
+ &self,
+ window: &mut Window,
+ cx: &mut Context,
+ ) -> Entity {
+ ContextMenu::build(window, cx, |mut menu, _window, cx| {
+ menu = menu
+ .custom_row(move |_window, cx| {
+ let description = indoc! {
+ "Sign in for 2,000 worth of accepted suggestions at every keystroke, \
+ powered by Zeta, our open-source, open-data model."
+ };
+
+ v_flex()
+ .max_w_64()
+ .h(rems_from_px(148.))
+ .child(render_zeta_tab_animation(cx))
+ .child(Label::new("Edit Prediction"))
+ .child(
+ Label::new(description)
+ .color(Color::Muted)
+ .size(LabelSize::Small),
+ )
+ .into_any_element()
+ })
+ .separator()
+ .entry("Sign In & Start Using", None, |window, cx| {
+ let client = Client::global(cx);
+ window
+ .spawn(cx, async move |cx| {
+ client
+ .sign_in_with_optional_connect(true, &cx)
+ .await
+ .log_err();
+ })
+ .detach();
+ })
+ .link(
+ "Learn More",
+ OpenBrowser {
+ url: zed_urls::edit_prediction_docs(cx),
+ }
+ .boxed_clone(),
+ );
+
+ menu
+ })
+ }
+
pub fn update_enabled(&mut self, editor: Entity, cx: &mut Context) {
let editor = editor.read(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
@@ -1248,6 +1321,66 @@ fn toggle_edit_prediction_mode(fs: Arc, mode: EditPredictionsMode, cx: &
}
}
+fn render_zeta_tab_animation(cx: &App) -> impl IntoElement {
+ let tab = |n: u64, inverted: bool| {
+ let text_color = cx.theme().colors().text;
+
+ h_flex().child(
+ h_flex()
+ .text_size(TextSize::XSmall.rems(cx))
+ .text_color(text_color)
+ .child("tab")
+ .with_animation(
+ ElementId::Integer(n),
+ Animation::new(Duration::from_secs(4)).repeat(),
+ move |tab, delta| {
+ let n_f32 = n as f32;
+
+ let delta = if inverted {
+ (delta - 0.15 * (5.0 - n_f32)) / 0.7
+ } else {
+ (delta - 0.15 * n_f32) / 0.7
+ };
+
+ let delta = 1.0 - (0.5 - delta).abs() * 2.;
+ let delta = ease_in_out(delta.clamp(0., 1.));
+ let delta = 0.1 + 0.5 * delta;
+
+ tab.text_color(text_color.opacity(delta))
+ },
+ ),
+ )
+ };
+
+ let tab_sequence = |inverted: bool| {
+ h_flex()
+ .gap_1()
+ .child(tab(0, inverted))
+ .child(tab(1, inverted))
+ .child(tab(2, inverted))
+ .child(tab(3, inverted))
+ .child(tab(4, inverted))
+ };
+
+ h_flex()
+ .my_1p5()
+ .p_4()
+ .justify_center()
+ .gap_2()
+ .rounded_xs()
+ .border_1()
+ .border_dashed()
+ .border_color(cx.theme().colors().border)
+ .bg(gpui::pattern_slash(
+ cx.theme().colors().border.opacity(0.5),
+ 1.,
+ 8.,
+ ))
+ .child(tab_sequence(true))
+ .child(Icon::new(IconName::ZedPredict))
+ .child(tab_sequence(false))
+}
+
fn copilot_settings_url(enterprise_uri: Option<&str>) -> String {
match enterprise_uri {
Some(uri) => {