zeta: Revised data-collection onboarding experience (#24031)

Release Notes:

- N/A

---------

Co-authored-by: Danilo <danilo@zed.dev>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: João Marcos <marcospb19@hotmail.com>
This commit is contained in:
Agus Zubiaga 2025-02-04 04:06:09 -03:00 committed by GitHub
parent 29e559d60c
commit 93f8ccaaee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 760 additions and 601 deletions

31
Cargo.lock generated
View file

@ -4059,7 +4059,7 @@ dependencies = [
"util", "util",
"uuid", "uuid",
"workspace", "workspace",
"zed_predict_onboarding", "zed_actions",
] ]
[[package]] [[package]]
@ -6454,7 +6454,6 @@ dependencies = [
"ui", "ui",
"workspace", "workspace",
"zed_actions", "zed_actions",
"zed_predict_onboarding",
"zeta", "zeta",
] ]
@ -13590,7 +13589,7 @@ dependencies = [
"windows 0.58.0", "windows 0.58.0",
"workspace", "workspace",
"zed_actions", "zed_actions",
"zed_predict_onboarding", "zeta",
] ]
[[package]] [[package]]
@ -16588,7 +16587,6 @@ dependencies = [
"winresource", "winresource",
"workspace", "workspace",
"zed_actions", "zed_actions",
"zed_predict_onboarding",
"zeta", "zeta",
] ]
@ -16702,25 +16700,6 @@ dependencies = [
"zed_extension_api 0.1.0", "zed_extension_api 0.1.0",
] ]
[[package]]
name = "zed_predict_onboarding"
version = "0.1.0"
dependencies = [
"chrono",
"client",
"db",
"feature_flags",
"fs",
"gpui",
"language",
"menu",
"settings",
"theme",
"ui",
"util",
"workspace",
]
[[package]] [[package]]
name = "zed_proto" name = "zed_proto"
version = "0.2.1" version = "0.2.1"
@ -16906,6 +16885,7 @@ dependencies = [
"anyhow", "anyhow",
"arrayvec", "arrayvec",
"call", "call",
"chrono",
"client", "client",
"clock", "clock",
"collections", "collections",
@ -16915,6 +16895,7 @@ dependencies = [
"editor", "editor",
"env_logger 0.11.6", "env_logger 0.11.6",
"feature_flags", "feature_flags",
"fs",
"futures 0.3.31", "futures 0.3.31",
"gpui", "gpui",
"http_client", "http_client",
@ -16924,6 +16905,8 @@ dependencies = [
"language_models", "language_models",
"log", "log",
"menu", "menu",
"postage",
"regex",
"reqwest_client", "reqwest_client",
"rpc", "rpc",
"serde", "serde",
@ -16936,10 +16919,12 @@ dependencies = [
"tree-sitter-go", "tree-sitter-go",
"tree-sitter-rust", "tree-sitter-rust",
"ui", "ui",
"unindent",
"util", "util",
"uuid", "uuid",
"workspace", "workspace",
"worktree", "worktree",
"zed_actions",
] ]
[[package]] [[package]]

View file

@ -152,7 +152,6 @@ members = [
"crates/worktree", "crates/worktree",
"crates/zed", "crates/zed",
"crates/zed_actions", "crates/zed_actions",
"crates/zed_predict_onboarding",
"crates/zeta", "crates/zeta",
# #
@ -348,7 +347,6 @@ workspace = { path = "crates/workspace" }
worktree = { path = "crates/worktree" } worktree = { path = "crates/worktree" }
zed = { path = "crates/zed" } zed = { path = "crates/zed" }
zed_actions = { path = "crates/zed_actions" } zed_actions = { path = "crates/zed_actions" }
zed_predict_onboarding = { path = "crates/zed_predict_onboarding" }
zeta = { path = "crates/zeta" } zeta = { path = "crates/zeta" }
# #

View file

@ -1,4 +1,4 @@
<svg width="420" height="128" xmlns="http://www.w3.org/2000/svg"> <svg width="440" height="128" xmlns="http://www.w3.org/2000/svg">
<defs> <defs>
<pattern id="tilePattern" width="22" height="22" patternUnits="userSpaceOnUse"> <pattern id="tilePattern" width="22" height="22" patternUnits="userSpaceOnUse">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">

Before

Width:  |  Height:  |  Size: 971 B

After

Width:  |  Height:  |  Size: 971 B

Before After
Before After

View file

@ -87,7 +87,7 @@ url.workspace = true
util.workspace = true util.workspace = true
uuid.workspace = true uuid.workspace = true
workspace.workspace = true workspace.workspace = true
zed_predict_onboarding.workspace = true zed_actions.workspace = true
[dev-dependencies] [dev-dependencies]
ctor.workspace = true ctor.workspace = true

View file

@ -69,7 +69,6 @@ pub use element::{
}; };
use futures::{future, FutureExt}; use futures::{future, FutureExt};
use fuzzy::StringMatchCandidate; use fuzzy::StringMatchCandidate;
use zed_predict_onboarding::ZedPredictModal;
use code_context_menus::{ use code_context_menus::{
AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu, AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
@ -617,7 +616,8 @@ pub struct Editor {
active_diagnostics: Option<ActiveDiagnosticGroup>, active_diagnostics: Option<ActiveDiagnosticGroup>,
soft_wrap_mode_override: Option<language_settings::SoftWrap>, soft_wrap_mode_override: Option<language_settings::SoftWrap>,
project: Option<Entity<Project>>, // TODO: make this a access method
pub project: Option<Entity<Project>>,
semantics_provider: Option<Rc<dyn SemanticsProvider>>, semantics_provider: Option<Rc<dyn SemanticsProvider>>,
completion_provider: Option<Box<dyn CompletionProvider>>, completion_provider: Option<Box<dyn CompletionProvider>>,
collaboration_hub: Option<Box<dyn CollaborationHub>>, collaboration_hub: Option<Box<dyn CollaborationHub>>,
@ -3944,20 +3944,7 @@ impl Editor {
} }
fn toggle_zed_predict_onboarding(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn toggle_zed_predict_onboarding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let (Some(workspace), Some(project)) = (self.workspace(), self.project.as_ref()) else { window.dispatch_action(zed_actions::OpenZedPredictOnboarding.boxed_clone(), cx);
return;
};
let project = project.read(cx);
ZedPredictModal::toggle(
workspace,
project.user_store().clone(),
project.client().clone(),
project.fs().clone(),
window,
cx,
);
} }
fn do_completion( fn do_completion(

View file

@ -21,8 +21,6 @@ pub struct InlineCompletion {
pub enum DataCollectionState { pub enum DataCollectionState {
/// The provider doesn't support data collection. /// The provider doesn't support data collection.
Unsupported, Unsupported,
/// When there's a file not saved yet. In this case, we can't tell to which project it belongs.
Unknown,
/// Data collection is enabled /// Data collection is enabled
Enabled, Enabled,
/// Data collection is disabled or unanswered. /// Data collection is disabled or unanswered.
@ -34,10 +32,6 @@ impl DataCollectionState {
!matches!(self, DataCollectionState::Unsupported) !matches!(self, DataCollectionState::Unsupported)
} }
pub fn is_unknown(&self) -> bool {
matches!(self, DataCollectionState::Unknown)
}
pub fn is_enabled(&self) -> bool { pub fn is_enabled(&self) -> bool {
matches!(self, DataCollectionState::Enabled) matches!(self, DataCollectionState::Enabled)
} }

View file

@ -29,7 +29,6 @@ workspace.workspace = true
zed_actions.workspace = true zed_actions.workspace = true
zeta.workspace = true zeta.workspace = true
client.workspace = true client.workspace = true
zed_predict_onboarding.workspace = true
[dev-dependencies] [dev-dependencies]
copilot = { workspace = true, features = ["test-support"] } copilot = { workspace = true, features = ["test-support"] }

View file

@ -1,5 +1,5 @@
use anyhow::Result; use anyhow::Result;
use client::{Client, UserStore}; use client::UserStore;
use copilot::{Copilot, Status}; use copilot::{Copilot, Status};
use editor::{actions::ShowInlineCompletion, scroll::Autoscroll, Editor}; use editor::{actions::ShowInlineCompletion, scroll::Autoscroll, Editor};
use feature_flags::{ use feature_flags::{
@ -21,15 +21,14 @@ use settings::{update_settings_file, Settings, SettingsStore};
use std::{path::Path, sync::Arc, time::Duration}; use std::{path::Path, sync::Arc, time::Duration};
use supermaven::{AccountStatus, Supermaven}; use supermaven::{AccountStatus, Supermaven};
use ui::{ use ui::{
prelude::*, ButtonLike, Clickable, ContextMenu, ContextMenuEntry, IconButton, prelude::*, Clickable, ContextMenu, ContextMenuEntry, IconButton, IconButtonShape, PopoverMenu,
IconWithIndicator, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, PopoverMenuHandle, Tooltip,
}; };
use workspace::{ use workspace::{
create_and_open_local_file, item::ItemHandle, notifications::NotificationId, StatusItemView, create_and_open_local_file, item::ItemHandle, notifications::NotificationId, StatusItemView,
Toast, Workspace, Toast, Workspace,
}; };
use zed_actions::OpenBrowser; use zed_actions::OpenBrowser;
use zed_predict_onboarding::ZedPredictModal;
use zeta::RateCompletionModal; use zeta::RateCompletionModal;
actions!(zeta, [RateCompletions]); actions!(zeta, [RateCompletions]);
@ -46,7 +45,6 @@ pub struct InlineCompletionButton {
language: Option<Arc<Language>>, language: Option<Arc<Language>>,
file: Option<Arc<dyn File>>, file: Option<Arc<dyn File>>,
inline_completion_provider: Option<Arc<dyn inline_completion::InlineCompletionProviderHandle>>, inline_completion_provider: Option<Arc<dyn inline_completion::InlineCompletionProviderHandle>>,
client: Arc<Client>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
user_store: Entity<UserStore>, user_store: Entity<UserStore>,
@ -230,71 +228,49 @@ impl Render for InlineCompletionButton {
return div(); return div();
} }
fn icon_button() -> IconButton {
IconButton::new("zed-predict-pending-button", IconName::ZedPredict)
.shape(IconButtonShape::Square)
}
let current_user_terms_accepted = let current_user_terms_accepted =
self.user_store.read(cx).current_user_has_accepted_terms(); self.user_store.read(cx).current_user_has_accepted_terms();
if !current_user_terms_accepted.unwrap_or(false) { if !current_user_terms_accepted.unwrap_or(false) {
let workspace = self.workspace.clone();
let user_store = self.user_store.clone();
let client = self.client.clone();
let fs = self.fs.clone();
let signed_in = current_user_terms_accepted.is_some(); let signed_in = current_user_terms_accepted.is_some();
let tooltip_meta = if signed_in {
"Read Terms of Service"
} else {
"Sign in to use"
};
return div().child( return div().child(
ButtonLike::new("zeta-pending-tos-icon") icon_button()
.child(
IconWithIndicator::new(
Icon::new(IconName::ZedPredict),
Some(Indicator::dot().color(Color::Error)),
)
.indicator_border_color(Some(
cx.theme().colors().status_bar_background,
))
.into_any_element(),
)
.tooltip(move |window, cx| { .tooltip(move |window, cx| {
Tooltip::with_meta( Tooltip::with_meta(
"Edit Predictions", "Edit Predictions",
None, None,
if signed_in { tooltip_meta,
"Read Terms of Service"
} else {
"Sign in to use"
},
window, window,
cx, cx,
) )
}) })
.on_click(cx.listener(move |_, _, window, cx| { .on_click(cx.listener(move |_, _, window, cx| {
if let Some(workspace) = workspace.upgrade() { window.dispatch_action(
ZedPredictModal::toggle( zed_actions::OpenZedPredictOnboarding.boxed_clone(),
workspace, cx,
user_store.clone(), );
client.clone(),
fs.clone(),
window,
cx,
);
}
})), })),
); );
} }
let this = cx.entity().clone(); let this = cx.entity().clone();
let button = IconButton::new("zeta", IconName::ZedPredict).when(
!self.popover_menu_handle.is_deployed(),
|button| {
button.tooltip(|window, cx| {
Tooltip::for_action("Edit Prediction", &ToggleMenu, window, cx)
})
},
);
let is_refreshing = self if !self.popover_menu_handle.is_deployed() {
.inline_completion_provider icon_button().tooltip(|window, cx| {
.as_ref() Tooltip::for_action("Edit Prediction", &ToggleMenu, window, cx)
.map_or(false, |provider| provider.is_refreshing(cx)); });
}
let mut popover_menu = PopoverMenu::new("zeta") let mut popover_menu = PopoverMenu::new("zeta")
.menu(move |window, cx| { .menu(move |window, cx| {
@ -303,9 +279,14 @@ impl Render for InlineCompletionButton {
.anchor(Corner::BottomRight) .anchor(Corner::BottomRight)
.with_handle(self.popover_menu_handle.clone()); .with_handle(self.popover_menu_handle.clone());
let is_refreshing = self
.inline_completion_provider
.as_ref()
.map_or(false, |provider| provider.is_refreshing(cx));
if is_refreshing { if is_refreshing {
popover_menu = popover_menu.trigger( popover_menu = popover_menu.trigger(
button.with_animation( icon_button().with_animation(
"pulsating-label", "pulsating-label",
Animation::new(Duration::from_secs(2)) Animation::new(Duration::from_secs(2))
.repeat() .repeat()
@ -314,7 +295,7 @@ impl Render for InlineCompletionButton {
), ),
); );
} else { } else {
popover_menu = popover_menu.trigger(button); popover_menu = popover_menu.trigger(icon_button());
} }
div().child(popover_menu.into_any_element()) div().child(popover_menu.into_any_element())
@ -328,7 +309,6 @@ impl InlineCompletionButton {
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
user_store: Entity<UserStore>, user_store: Entity<UserStore>,
client: Arc<Client>,
popover_menu_handle: PopoverMenuHandle<ContextMenu>, popover_menu_handle: PopoverMenuHandle<ContextMenu>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
@ -348,7 +328,6 @@ impl InlineCompletionButton {
inline_completion_provider: None, inline_completion_provider: None,
popover_menu_handle, popover_menu_handle,
workspace, workspace,
client,
fs, fs,
user_store, user_store,
} }
@ -447,10 +426,15 @@ impl InlineCompletionButton {
if data_collection.is_supported() { if data_collection.is_supported() {
let provider = provider.clone(); let provider = provider.clone();
menu = menu.separator().item( menu = menu
ContextMenuEntry::new("Data Collection") .separator()
.header("Help Improve The Model")
.header("For OSS Projects Only");
menu = menu.item(
// TODO: We want to add something later that communicates whether
// the current project is open-source.
ContextMenuEntry::new("Share Training Data")
.toggleable(IconPosition::Start, data_collection.is_enabled()) .toggleable(IconPosition::Start, data_collection.is_enabled())
.disabled(data_collection.is_unknown())
.handler(move |_, cx| { .handler(move |_, cx| {
provider.toggle_data_collection(cx); provider.toggle_data_collection(cx);
}), }),

View file

@ -41,7 +41,7 @@ pub struct PredictEditsParams {
pub input_excerpt: String, pub input_excerpt: String,
/// Whether the user provided consent for sampling this interaction. /// Whether the user provided consent for sampling this interaction.
#[serde(default)] #[serde(default)]
pub can_collect_data: bool, pub data_collection_permission: bool,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]

View file

@ -41,13 +41,13 @@ serde.workspace = true
settings.workspace = true settings.workspace = true
smallvec.workspace = true smallvec.workspace = true
story = { workspace = true, optional = true } story = { workspace = true, optional = true }
telemetry.workspace = true
theme.workspace = true theme.workspace = true
ui.workspace = true ui.workspace = true
util.workspace = true util.workspace = true
telemetry.workspace = true
workspace.workspace = true workspace.workspace = true
zed_actions.workspace = true zed_actions.workspace = true
zed_predict_onboarding.workspace = true zeta.workspace = true
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
windows.workspace = true windows.workspace = true

View file

@ -34,7 +34,7 @@ use ui::{
use util::ResultExt; use util::ResultExt;
use workspace::{notifications::NotifyResultExt, Workspace}; use workspace::{notifications::NotifyResultExt, Workspace};
use zed_actions::{OpenBrowser, OpenRecent, OpenRemote}; use zed_actions::{OpenBrowser, OpenRecent, OpenRemote};
use zed_predict_onboarding::ZedPredictBanner; use zeta::ZedPredictBanner;
#[cfg(feature = "stories")] #[cfg(feature = "stories")]
pub use stories::*; pub use stories::*;
@ -162,6 +162,7 @@ impl Render for TitleBar {
.id("titlebar-content") .id("titlebar-content")
.flex() .flex()
.flex_row() .flex_row()
.items_center()
.justify_between() .justify_between()
.w_full() .w_full()
// Note: On Windows the title bar behavior is handled by the platform implementation. // Note: On Windows the title bar behavior is handled by the platform implementation.
@ -268,7 +269,6 @@ impl TitleBar {
let project = workspace.project().clone(); let project = workspace.project().clone();
let user_store = workspace.app_state().user_store.clone(); let user_store = workspace.app_state().user_store.clone();
let client = workspace.app_state().client.clone(); let client = workspace.app_state().client.clone();
let fs = workspace.app_state().fs.clone();
let active_call = ActiveCall::global(cx); let active_call = ActiveCall::global(cx);
let platform_style = PlatformStyle::platform(); let platform_style = PlatformStyle::platform();
@ -296,15 +296,7 @@ impl TitleBar {
subscriptions.push(cx.observe_window_activation(window, Self::window_activation_changed)); subscriptions.push(cx.observe_window_activation(window, Self::window_activation_changed));
subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify())); subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
let zed_predict_banner = cx.new(|cx| { let zed_predict_banner = cx.new(ZedPredictBanner::new);
ZedPredictBanner::new(
workspace.weak_handle(),
user_store.clone(),
client.clone(),
fs.clone(),
cx,
)
});
Self { Self {
platform_style, platform_style,

View file

@ -385,6 +385,11 @@ impl ButtonLike {
Self::new(id).rounding(ButtonLikeRounding::Right) Self::new(id).rounding(ButtonLikeRounding::Right)
} }
pub fn opacity(mut self, opacity: f32) -> Self {
self.base = self.base.opacity(opacity);
self
}
pub(crate) fn height(mut self, height: DefiniteLength) -> Self { pub(crate) fn height(mut self, height: DefiniteLength) -> Self {
self.height = Some(height); self.height = Some(height);
self self

View file

@ -57,12 +57,19 @@ impl<M> Default for PopoverMenuHandle<M> {
struct PopoverMenuHandleState<M> { struct PopoverMenuHandleState<M> {
menu_builder: Rc<dyn Fn(&mut Window, &mut App) -> Option<Entity<M>>>, menu_builder: Rc<dyn Fn(&mut Window, &mut App) -> Option<Entity<M>>>,
menu: Rc<RefCell<Option<Entity<M>>>>, menu: Rc<RefCell<Option<Entity<M>>>>,
on_open: Option<Rc<dyn Fn(&mut Window, &mut App)>>,
} }
impl<M: ManagedView> PopoverMenuHandle<M> { impl<M: ManagedView> PopoverMenuHandle<M> {
pub fn show(&self, window: &mut Window, cx: &mut App) { pub fn show(&self, window: &mut Window, cx: &mut App) {
if let Some(state) = self.0.borrow().as_ref() { if let Some(state) = self.0.borrow().as_ref() {
show_menu(&state.menu_builder, &state.menu, window, cx); show_menu(
&state.menu_builder,
&state.menu,
state.on_open.clone(),
window,
cx,
);
} }
} }
@ -118,6 +125,7 @@ pub struct PopoverMenu<M: ManagedView> {
attach: Option<Corner>, attach: Option<Corner>,
offset: Option<Point<Pixels>>, offset: Option<Point<Pixels>>,
trigger_handle: Option<PopoverMenuHandle<M>>, trigger_handle: Option<PopoverMenuHandle<M>>,
on_open: Option<Rc<dyn Fn(&mut Window, &mut App)>>,
full_width: bool, full_width: bool,
} }
@ -132,6 +140,7 @@ impl<M: ManagedView> PopoverMenu<M> {
attach: None, attach: None,
offset: None, offset: None,
trigger_handle: None, trigger_handle: None,
on_open: None,
full_width: false, full_width: false,
} }
} }
@ -155,11 +164,14 @@ impl<M: ManagedView> PopoverMenu<M> {
} }
pub fn trigger<T: PopoverTrigger>(mut self, t: T) -> Self { pub fn trigger<T: PopoverTrigger>(mut self, t: T) -> Self {
self.child_builder = Some(Box::new(|menu, builder| { let on_open = self.on_open.clone();
self.child_builder = Some(Box::new(move |menu, builder| {
let open = menu.borrow().is_some(); let open = menu.borrow().is_some();
t.toggle_state(open) t.toggle_state(open)
.when_some(builder, |el, builder| { .when_some(builder, |el, builder| {
el.on_click(move |_event, window, cx| show_menu(&builder, &menu, window, cx)) el.on_click(move |_event, window, cx| {
show_menu(&builder, &menu, on_open.clone(), window, cx)
})
}) })
.into_any_element() .into_any_element()
})); }));
@ -185,6 +197,12 @@ impl<M: ManagedView> PopoverMenu<M> {
self self
} }
/// attach something upon opening the menu
pub fn on_open(mut self, on_open: Rc<dyn Fn(&mut Window, &mut App)>) -> Self {
self.on_open = Some(on_open);
self
}
fn resolved_attach(&self) -> Corner { fn resolved_attach(&self) -> Corner {
self.attach.unwrap_or(match self.anchor { self.attach.unwrap_or(match self.anchor {
Corner::TopLeft => Corner::BottomLeft, Corner::TopLeft => Corner::BottomLeft,
@ -209,6 +227,7 @@ impl<M: ManagedView> PopoverMenu<M> {
fn show_menu<M: ManagedView>( fn show_menu<M: ManagedView>(
builder: &Rc<dyn Fn(&mut Window, &mut App) -> Option<Entity<M>>>, builder: &Rc<dyn Fn(&mut Window, &mut App) -> Option<Entity<M>>>,
menu: &Rc<RefCell<Option<Entity<M>>>>, menu: &Rc<RefCell<Option<Entity<M>>>>,
on_open: Option<Rc<dyn Fn(&mut Window, &mut App)>>,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) { ) {
@ -232,6 +251,10 @@ fn show_menu<M: ManagedView>(
window.focus(&new_menu.focus_handle(cx)); window.focus(&new_menu.focus_handle(cx));
*menu.borrow_mut() = Some(new_menu); *menu.borrow_mut() = Some(new_menu);
window.refresh(); window.refresh();
if let Some(on_open) = on_open {
on_open(window, cx);
}
} }
pub struct PopoverMenuElementState<M> { pub struct PopoverMenuElementState<M> {
@ -311,6 +334,7 @@ impl<M: ManagedView> Element for PopoverMenu<M> {
*trigger_handle.0.borrow_mut() = Some(PopoverMenuHandleState { *trigger_handle.0.borrow_mut() = Some(PopoverMenuHandleState {
menu_builder, menu_builder,
menu: element_state.menu.clone(), menu: element_state.menu.clone(),
on_open: self.on_open.clone(),
}); });
} }
} }

View file

@ -1,4 +1,6 @@
use gpui::{div, hsla, prelude::*, AnyView, ElementId, Hsla, IntoElement, Styled, Window}; use gpui::{
div, hsla, prelude::*, AnyView, CursorStyle, ElementId, Hsla, IntoElement, Styled, Window,
};
use std::sync::Arc; use std::sync::Arc;
use crate::utils::is_light; use crate::utils::is_light;
@ -45,6 +47,7 @@ pub struct Checkbox {
filled: bool, filled: bool,
style: ToggleStyle, style: ToggleStyle,
tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView>>, tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView>>,
label: Option<SharedString>,
} }
impl Checkbox { impl Checkbox {
@ -58,6 +61,7 @@ impl Checkbox {
filled: false, filled: false,
style: ToggleStyle::default(), style: ToggleStyle::default(),
tooltip: None, tooltip: None,
label: None,
} }
} }
@ -99,6 +103,12 @@ impl Checkbox {
self.tooltip = Some(Box::new(tooltip)); self.tooltip = Some(Box::new(tooltip));
self self
} }
/// Set the label for the checkbox.
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
self.label = Some(label.into());
self
}
} }
impl Checkbox { impl Checkbox {
@ -116,11 +126,11 @@ impl Checkbox {
fn border_color(&self, cx: &App) -> Hsla { fn border_color(&self, cx: &App) -> Hsla {
if self.disabled { if self.disabled {
return cx.theme().colors().border_disabled; return cx.theme().colors().border_variant;
} }
match self.style.clone() { match self.style.clone() {
ToggleStyle::Ghost => cx.theme().colors().border_variant, ToggleStyle::Ghost => cx.theme().colors().border,
ToggleStyle::ElevationBased(elevation) => elevation.on_elevation_bg(cx), ToggleStyle::ElevationBased(elevation) => elevation.on_elevation_bg(cx),
ToggleStyle::Custom(color) => color.opacity(0.3), ToggleStyle::Custom(color) => color.opacity(0.3),
} }
@ -153,10 +163,8 @@ impl RenderOnce for Checkbox {
let bg_color = self.bg_color(cx); let bg_color = self.bg_color(cx);
let border_color = self.border_color(cx); let border_color = self.border_color(cx);
h_flex() let checkbox = h_flex()
.id(self.id)
.justify_center() .justify_center()
.items_center()
.size(DynamicSpacing::Base20.rems(cx)) .size(DynamicSpacing::Base20.rems(cx))
.group(group_id.clone()) .group(group_id.clone())
.child( .child(
@ -171,13 +179,24 @@ impl RenderOnce for Checkbox {
.bg(bg_color) .bg(bg_color)
.border_1() .border_1()
.border_color(border_color) .border_color(border_color)
.when(self.disabled, |this| {
this.cursor(CursorStyle::OperationNotAllowed)
})
.when(self.disabled, |this| {
this.bg(cx.theme().colors().element_disabled.opacity(0.6))
})
.when(!self.disabled, |this| { .when(!self.disabled, |this| {
this.group_hover(group_id.clone(), |el| { this.group_hover(group_id.clone(), |el| {
el.bg(cx.theme().colors().element_hover) el.bg(cx.theme().colors().element_hover)
}) })
}) })
.children(icon), .children(icon),
) );
h_flex()
.id(self.id)
.gap(DynamicSpacing::Base06.rems(cx))
.child(checkbox)
.when_some( .when_some(
self.on_click.filter(|_| !self.disabled), self.on_click.filter(|_| !self.disabled),
|this, on_click| { |this, on_click| {
@ -186,6 +205,11 @@ impl RenderOnce for Checkbox {
}) })
}, },
) )
// TODO: Allow label size to be different from default.
// TODO: Allow label color to be different from muted.
.when_some(self.label, |this, label| {
this.child(Label::new(label).color(Color::Muted))
})
.when_some(self.tooltip, |this, tooltip| { .when_some(self.tooltip, |this, tooltip| {
this.tooltip(move |window, cx| tooltip(window, cx)) this.tooltip(move |window, cx| tooltip(window, cx))
}) })
@ -203,6 +227,7 @@ pub struct CheckboxWithLabel {
style: ToggleStyle, style: ToggleStyle,
} }
// TODO: Remove `CheckboxWithLabel` now that `label` is a method of `Checkbox`.
impl CheckboxWithLabel { impl CheckboxWithLabel {
/// Creates a checkbox with an attached label. /// Creates a checkbox with an attached label.
pub fn new( pub fn new(

View file

@ -87,7 +87,7 @@ pub const FS_WATCH_LATENCY: Duration = Duration::from_millis(100);
/// May correspond to a directory or a single file. /// May correspond to a directory or a single file.
/// Possible examples: /// Possible examples:
/// * a drag and dropped file — may be added as an invisible, "ephemeral" entry to the current worktree /// * a drag and dropped file — may be added as an invisible, "ephemeral" entry to the current worktree
/// * a directory opened in Zed — may be added as a visible entry to the current worktree /// * a directory opened in Zed — may be added as a visible entry to the current worktree
/// ///
/// Uses [`Entry`] to track the state of each file/directory, can look up absolute paths for entries. /// Uses [`Entry`] to track the state of each file/directory, can look up absolute paths for entries.
pub enum Worktree { pub enum Worktree {

View file

@ -16,7 +16,6 @@ path = "src/main.rs"
[dependencies] [dependencies]
activity_indicator.workspace = true activity_indicator.workspace = true
zed_predict_onboarding.workspace = true
anyhow.workspace = true anyhow.workspace = true
assets.workspace = true assets.workspace = true
assistant.workspace = true assistant.workspace = true

View file

@ -439,7 +439,6 @@ fn main() {
inline_completion_registry::init( inline_completion_registry::init(
app_state.client.clone(), app_state.client.clone(),
app_state.user_store.clone(), app_state.user_store.clone(),
app_state.fs.clone(),
cx, cx,
); );
let prompt_builder = PromptBuilder::load(app_state.fs.clone(), stdout_is_a_pty(), cx); let prompt_builder = PromptBuilder::load(app_state.fs.clone(), stdout_is_a_pty(), cx);

View file

@ -176,7 +176,6 @@ pub fn initialize_workspace(
workspace.weak_handle(), workspace.weak_handle(),
app_state.fs.clone(), app_state.fs.clone(),
app_state.user_store.clone(), app_state.user_store.clone(),
app_state.client.clone(),
popover_menu_handle.clone(), popover_menu_handle.clone(),
cx, cx,
) )

View file

@ -1,21 +1,17 @@
use std::{cell::RefCell, rc::Rc, sync::Arc};
use client::{Client, UserStore}; use client::{Client, UserStore};
use collections::HashMap; use collections::HashMap;
use copilot::{Copilot, CopilotCompletionProvider}; use copilot::{Copilot, CopilotCompletionProvider};
use editor::{Editor, EditorMode}; use editor::{Editor, EditorMode};
use feature_flags::{FeatureFlagAppExt, PredictEditsFeatureFlag}; use feature_flags::{FeatureFlagAppExt, PredictEditsFeatureFlag};
use fs::Fs;
use gpui::{AnyWindowHandle, App, AppContext, Context, Entity, WeakEntity}; use gpui::{AnyWindowHandle, App, AppContext, Context, Entity, WeakEntity};
use language::language_settings::{all_language_settings, InlineCompletionProvider}; use language::language_settings::{all_language_settings, InlineCompletionProvider};
use settings::SettingsStore; use settings::SettingsStore;
use std::{cell::RefCell, rc::Rc, sync::Arc};
use supermaven::{Supermaven, SupermavenCompletionProvider}; use supermaven::{Supermaven, SupermavenCompletionProvider};
use ui::Window; use ui::Window;
use workspace::Workspace;
use zed_predict_onboarding::ZedPredictModal;
use zeta::ProviderDataCollection; use zeta::ProviderDataCollection;
pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, fs: Arc<dyn Fs>, cx: &mut App) { pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
let editors: Rc<RefCell<HashMap<WeakEntity<Editor>, AnyWindowHandle>>> = Rc::default(); let editors: Rc<RefCell<HashMap<WeakEntity<Editor>, AnyWindowHandle>>> = Rc::default();
cx.observe_new({ cx.observe_new({
let editors = editors.clone(); let editors = editors.clone();
@ -96,7 +92,6 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, fs: Arc<dyn Fs>,
let editors = editors.clone(); let editors = editors.clone();
let client = client.clone(); let client = client.clone();
let user_store = user_store.clone(); let user_store = user_store.clone();
let fs = fs.clone();
move |cx| { move |cx| {
let new_provider = all_language_settings(None, cx).inline_completions.provider; let new_provider = all_language_settings(None, cx).inline_completions.provider;
if new_provider != provider { if new_provider != provider {
@ -120,21 +115,10 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, fs: Arc<dyn Fs>,
return; return;
}; };
let Some(Some(workspace)) = window
.update(cx, |_, window, _| window.root().flatten())
.ok()
else {
return;
};
window window
.update(cx, |_, window, cx| { .update(cx, |_, window, cx| {
ZedPredictModal::toggle( window.dispatch_action(
workspace, Box::new(zed_actions::OpenZedPredictOnboarding),
user_store.clone(),
client.clone(),
fs.clone(),
window,
cx, cx,
); );
}) })
@ -228,6 +212,7 @@ fn assign_inline_completion_provider(
window: &mut Window, window: &mut Window,
cx: &mut Context<Editor>, cx: &mut Context<Editor>,
) { ) {
// TODO: Do we really want to collect data only for singleton buffers?
let singleton_buffer = editor.buffer().read(cx).as_singleton(); let singleton_buffer = editor.buffer().read(cx).as_singleton();
match provider { match provider {
@ -255,7 +240,23 @@ fn assign_inline_completion_provider(
if cx.has_flag::<PredictEditsFeatureFlag>() if cx.has_flag::<PredictEditsFeatureFlag>()
|| (cfg!(debug_assertions) && client.status().borrow().is_connected()) || (cfg!(debug_assertions) && client.status().borrow().is_connected())
{ {
let zeta = zeta::Zeta::register(client.clone(), user_store, cx); let mut worktree = None;
if let Some(buffer) = &singleton_buffer {
if let Some(file) = buffer.read(cx).file() {
let id = file.worktree_id(cx);
if let Some(inner_worktree) = editor
.project
.as_ref()
.and_then(|project| project.read(cx).worktree_for_id(id, cx))
{
worktree = Some(inner_worktree);
}
}
}
let zeta = zeta::Zeta::register(worktree, client.clone(), user_store, cx);
if let Some(buffer) = &singleton_buffer { if let Some(buffer) = &singleton_buffer {
if buffer.read(cx).file().is_some() { if buffer.read(cx).file().is_some() {
zeta.update(cx, |zeta, cx| { zeta.update(cx, |zeta, cx| {
@ -264,12 +265,8 @@ fn assign_inline_completion_provider(
} }
} }
let data_collection = ProviderDataCollection::new( let data_collection =
zeta.clone(), ProviderDataCollection::new(zeta.clone(), singleton_buffer, cx);
window.root::<Workspace>().flatten(),
singleton_buffer,
cx,
);
let provider = let provider =
cx.new(|_| zeta::ZetaInlineCompletionProvider::new(zeta, data_collection)); cx.new(|_| zeta::ZetaInlineCompletionProvider::new(zeta, data_collection));

View file

@ -186,3 +186,5 @@ pub mod outline {
/// A pointer to outline::toggle function, exposed here to sewer the breadcrumbs <-> outline dependency. /// A pointer to outline::toggle function, exposed here to sewer the breadcrumbs <-> outline dependency.
pub static TOGGLE_OUTLINE: OnceLock<fn(AnyView, &mut Window, &mut App)> = OnceLock::new(); pub static TOGGLE_OUTLINE: OnceLock<fn(AnyView, &mut Window, &mut App)> = OnceLock::new();
} }
actions!(zed_predict_onboarding, [OpenZedPredictOnboarding]);

View file

@ -1,31 +0,0 @@
[package]
name = "zed_predict_onboarding"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/lib.rs"
doctest = false
[features]
test-support = []
[dependencies]
chrono.workspace = true
client.workspace = true
db.workspace = true
feature_flags.workspace = true
fs.workspace = true
gpui.workspace = true
language.workspace = true
menu.workspace = true
settings.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true

View file

@ -1 +0,0 @@
../../LICENSE-GPL

View file

@ -1,5 +0,0 @@
mod banner;
mod modal;
pub use banner::ZedPredictBanner;
pub use modal::ZedPredictModal;

View file

@ -19,12 +19,14 @@ test-support = []
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
arrayvec.workspace = true arrayvec.workspace = true
chrono.workspace = true
client.workspace = true client.workspace = true
collections.workspace = true collections.workspace = true
command_palette_hooks.workspace = true command_palette_hooks.workspace = true
db.workspace = true db.workspace = true
editor.workspace = true editor.workspace = true
feature_flags.workspace = true feature_flags.workspace = true
fs.workspace = true
futures.workspace = true futures.workspace = true
gpui.workspace = true gpui.workspace = true
http_client.workspace = true http_client.workspace = true
@ -34,6 +36,8 @@ language.workspace = true
language_models.workspace = true language_models.workspace = true
log.workspace = true log.workspace = true
menu.workspace = true menu.workspace = true
postage.workspace = true
regex.workspace = true
rpc.workspace = true rpc.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
@ -46,6 +50,8 @@ ui.workspace = true
util.workspace = true util.workspace = true
uuid.workspace = true uuid.workspace = true
workspace.workspace = true workspace.workspace = true
worktree.workspace = true
zed_actions.workspace = true
[dev-dependencies] [dev-dependencies]
collections = { workspace = true, features = ["test-support"] } collections = { workspace = true, features = ["test-support"] }
@ -64,6 +70,7 @@ settings = { workspace = true, features = ["test-support"] }
theme = { workspace = true, features = ["test-support"] } theme = { workspace = true, features = ["test-support"] }
tree-sitter-go.workspace = true tree-sitter-go.workspace = true
tree-sitter-rust.workspace = true tree-sitter-rust.workspace = true
unindent.workspace = true
workspace = { workspace = true, features = ["test-support"] } workspace = { workspace = true, features = ["test-support"] }
worktree = { workspace = true, features = ["test-support"] } worktree = { workspace = true, features = ["test-support"] }
call = { workspace = true, features = ["test-support"] } call = { workspace = true, features = ["test-support"] }

60
crates/zeta/src/init.rs Normal file
View file

@ -0,0 +1,60 @@
use std::any::{Any, TypeId};
use command_palette_hooks::CommandPaletteFilter;
use feature_flags::{
FeatureFlagAppExt as _, PredictEditsFeatureFlag, PredictEditsRateCompletionsFeatureFlag,
};
use ui::App;
use workspace::Workspace;
use crate::{onboarding_modal::ZedPredictModal, RateCompletionModal, RateCompletions};
pub fn init(cx: &mut App) {
cx.observe_new(move |workspace: &mut Workspace, _, _cx| {
workspace.register_action(|workspace, _: &RateCompletions, window, cx| {
if cx.has_flag::<PredictEditsRateCompletionsFeatureFlag>() {
RateCompletionModal::toggle(workspace, window, cx);
}
});
workspace.register_action(
move |workspace, _: &zed_actions::OpenZedPredictOnboarding, window, cx| {
if cx.has_flag::<PredictEditsFeatureFlag>() {
ZedPredictModal::toggle(
workspace,
workspace.user_store().clone(),
workspace.client().clone(),
workspace.app_state().fs.clone(),
window,
cx,
)
}
},
);
})
.detach();
feature_gate_predict_edits_rating_actions(cx);
}
fn feature_gate_predict_edits_rating_actions(cx: &mut App) {
let rate_completion_action_types = [TypeId::of::<RateCompletions>()];
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_action_types(&rate_completion_action_types);
filter.hide_action_types(&[zed_actions::OpenZedPredictOnboarding.type_id()]);
});
cx.observe_flag::<PredictEditsRateCompletionsFeatureFlag, _>(move |is_enabled, cx| {
if is_enabled {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.show_action_types(rate_completion_action_types.iter());
});
} else {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_action_types(&rate_completion_action_types);
});
}
})
.detach();
}

View file

@ -0,0 +1,210 @@
use regex::Regex;
pub fn is_license_eligible_for_data_collection(license: &str) -> bool {
// TODO: Include more licenses later (namely, Apache)
for pattern in [MIT_LICENSE_REGEX, ISC_LICENSE_REGEX] {
let regex = Regex::new(pattern.trim()).unwrap();
if regex.is_match(license.trim()) {
return true;
}
}
false
}
const MIT_LICENSE_REGEX: &str = r#"
^.*MIT License.*
Copyright.*?
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files \(the "Software"\), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software\.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE\.$
"#;
const ISC_LICENSE_REGEX: &str = r#"
^ISC License
Copyright.*?
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies\.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS\. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE\.$
"#;
#[cfg(test)]
mod tests {
use unindent::unindent;
use crate::is_license_eligible_for_data_collection;
#[test]
fn test_mit_positive_detection() {
let example_license = unindent(
r#"
MIT License
Copyright (c) 2024 John Doe
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"#
.trim(),
);
assert!(is_license_eligible_for_data_collection(&example_license));
let example_license = unindent(
r#"
The MIT License (MIT)
Copyright (c) 2019 John Doe
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"#
.trim(),
);
assert!(is_license_eligible_for_data_collection(&example_license));
}
#[test]
fn test_mit_negative_detection() {
let example_license = unindent(
r#"
MIT License
Copyright (c) 2024 John Doe
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This project is dual licensed under the MIT License and the Apache License, Version 2.0.
"#
.trim(),
);
assert!(!is_license_eligible_for_data_collection(&example_license));
}
#[test]
fn test_isc_positive_detection() {
let example_license = unindent(
r#"
ISC License
Copyright (c) 2024, John Doe
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
"#
.trim(),
);
assert!(is_license_eligible_for_data_collection(&example_license));
}
#[test]
fn test_isc_negative_detection() {
let example_license = unindent(
r#"
ISC License
Copyright (c) 2024, John Doe
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
This project is dual licensed under the ISC License and the MIT License.
"#
.trim(),
);
assert!(!is_license_eligible_for_data_collection(&example_license));
}
}

View file

@ -1,40 +1,20 @@
use std::sync::Arc;
use crate::ZedPredictModal;
use chrono::Utc; use chrono::Utc;
use client::{Client, UserStore};
use feature_flags::{FeatureFlagAppExt as _, PredictEditsFeatureFlag}; use feature_flags::{FeatureFlagAppExt as _, PredictEditsFeatureFlag};
use fs::Fs; use gpui::Subscription;
use gpui::{Entity, Subscription, WeakEntity};
use language::language_settings::{all_language_settings, InlineCompletionProvider}; use language::language_settings::{all_language_settings, InlineCompletionProvider};
use settings::SettingsStore; use settings::SettingsStore;
use ui::{prelude::*, ButtonLike, Tooltip}; use ui::{prelude::*, ButtonLike, Tooltip};
use util::ResultExt; use util::ResultExt;
use workspace::Workspace;
/// Prompts user to try AI inline prediction feature /// Prompts the user to try Zed's Edit Prediction feature
pub struct ZedPredictBanner { pub struct ZedPredictBanner {
workspace: WeakEntity<Workspace>,
user_store: Entity<UserStore>,
client: Arc<Client>,
fs: Arc<dyn Fs>,
dismissed: bool, dismissed: bool,
_subscription: Subscription, _subscription: Subscription,
} }
impl ZedPredictBanner { impl ZedPredictBanner {
pub fn new( pub fn new(cx: &mut Context<Self>) -> Self {
workspace: WeakEntity<Workspace>,
user_store: Entity<UserStore>,
client: Arc<Client>,
fs: Arc<dyn Fs>,
cx: &mut Context<Self>,
) -> Self {
Self { Self {
workspace,
user_store,
client,
fs,
dismissed: get_dismissed(), dismissed: get_dismissed(),
_subscription: cx.observe_global::<SettingsStore>(Self::handle_settings_changed), _subscription: cx.observe_global::<SettingsStore>(Self::handle_settings_changed),
} }
@ -126,24 +106,8 @@ impl Render for ZedPredictBanner {
.child(Label::new("Edit Prediction").size(LabelSize::Small)), .child(Label::new("Edit Prediction").size(LabelSize::Small)),
), ),
) )
.on_click({ .on_click(|_, window, cx| {
let workspace = self.workspace.clone(); window.dispatch_action(Box::new(zed_actions::OpenZedPredictOnboarding), cx)
let user_store = self.user_store.clone();
let client = self.client.clone();
let fs = self.fs.clone();
move |_, window, cx| {
let Some(workspace) = workspace.upgrade() else {
return;
};
ZedPredictModal::toggle(
workspace,
user_store.clone(),
client.clone(),
fs.clone(),
window,
cx,
);
}
}), }),
) )
.child( .child(
@ -163,6 +127,6 @@ impl Render for ZedPredictBanner {
), ),
); );
div().pr_1().child(banner) div().pr_2().child(banner)
} }
} }

View file

@ -1,6 +1,8 @@
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
use crate::{Zeta, ZED_PREDICT_DATA_COLLECTION_CHOICE};
use client::{Client, UserStore}; use client::{Client, UserStore};
use db::kvp::KEY_VALUE_STORE;
use feature_flags::FeatureFlagAppExt as _; use feature_flags::FeatureFlagAppExt as _;
use fs::Fs; use fs::Fs;
use gpui::{ use gpui::{
@ -9,10 +11,12 @@ use gpui::{
}; };
use language::language_settings::{AllLanguageSettings, InlineCompletionProvider}; use language::language_settings::{AllLanguageSettings, InlineCompletionProvider};
use settings::{update_settings_file, Settings}; use settings::{update_settings_file, Settings};
use ui::{prelude::*, CheckboxWithLabel, TintColor}; use ui::{prelude::*, Checkbox, TintColor, Tooltip};
use util::ResultExt;
use workspace::{notifications::NotifyTaskExt, ModalView, Workspace}; use workspace::{notifications::NotifyTaskExt, ModalView, Workspace};
use worktree::Worktree;
/// Introduces user to AI inline prediction feature and terms of service /// Introduces user to Zed's Edit Prediction feature and terms of service
pub struct ZedPredictModal { pub struct ZedPredictModal {
user_store: Entity<UserStore>, user_store: Entity<UserStore>,
client: Arc<Client>, client: Arc<Client>,
@ -20,6 +24,9 @@ pub struct ZedPredictModal {
focus_handle: FocusHandle, focus_handle: FocusHandle,
sign_in_status: SignInStatus, sign_in_status: SignInStatus,
terms_of_service: bool, terms_of_service: bool,
data_collection_expanded: bool,
data_collection_opted_in: bool,
worktrees: Vec<Entity<Worktree>>,
} }
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq)]
@ -33,34 +40,26 @@ enum SignInStatus {
} }
impl ZedPredictModal { impl ZedPredictModal {
fn new( pub fn toggle(
workspace: &mut Workspace,
user_store: Entity<UserStore>, user_store: Entity<UserStore>,
client: Arc<Client>, client: Arc<Client>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
cx: &mut Context<Self>, window: &mut Window,
) -> Self { cx: &mut Context<Workspace>,
ZedPredictModal { ) {
let worktrees = workspace.visible_worktrees(cx).collect();
workspace.toggle_modal(window, cx, |_window, cx| Self {
user_store, user_store,
client, client,
fs, fs,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
sign_in_status: SignInStatus::Idle, sign_in_status: SignInStatus::Idle,
terms_of_service: false, terms_of_service: false,
} data_collection_expanded: false,
} data_collection_opted_in: false,
worktrees,
pub fn toggle(
workspace: Entity<Workspace>,
user_store: Entity<UserStore>,
client: Arc<Client>,
fs: Arc<dyn Fs>,
window: &mut Window,
cx: &mut App,
) {
workspace.update(cx, |this, cx| {
this.toggle_modal(window, cx, |_window, cx| {
ZedPredictModal::new(user_store, client, fs, cx)
});
}); });
} }
@ -74,6 +73,11 @@ impl ZedPredictModal {
cx.notify(); cx.notify();
} }
fn inline_completions_doc(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
cx.open_url("https://zed.dev/docs/configuring-zed#inline-completions");
cx.notify();
}
fn accept_and_enable(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) { fn accept_and_enable(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
let task = self let task = self
.user_store .user_store
@ -82,6 +86,20 @@ impl ZedPredictModal {
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
task.await?; task.await?;
let mut data_collection_opted_in = false;
this.update(&mut cx, |this, _cx| {
data_collection_opted_in = this.data_collection_opted_in;
})
.ok();
KEY_VALUE_STORE
.write_kvp(
ZED_PREDICT_DATA_COLLECTION_CHOICE.into(),
data_collection_opted_in.to_string(),
)
.await
.log_err();
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
update_settings_file::<AllLanguageSettings>(this.fs.clone(), cx, move |file, _| { update_settings_file::<AllLanguageSettings>(this.fs.clone(), cx, move |file, _| {
file.features file.features
@ -89,6 +107,13 @@ impl ZedPredictModal {
.inline_completion_provider = Some(InlineCompletionProvider::Zed); .inline_completion_provider = Some(InlineCompletionProvider::Zed);
}); });
if this.worktrees.is_empty() {
cx.emit(DismissEvent);
return;
}
Zeta::register(None, this.client.clone(), this.user_store.clone(), cx);
cx.emit(DismissEvent); cx.emit(DismissEvent);
}) })
}) })
@ -135,16 +160,16 @@ impl ModalView for ZedPredictModal {}
impl Render for ZedPredictModal { impl Render for ZedPredictModal {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let base = v_flex() let base = v_flex()
.w(px(420.)) .id("zed predict tos")
.key_context("ZedPredictModal")
.w(px(440.))
.p_4() .p_4()
.relative() .relative()
.gap_2() .gap_2()
.overflow_hidden() .overflow_hidden()
.elevation_3(cx) .elevation_3(cx)
.id("zed predict tos")
.track_focus(&self.focus_handle(cx)) .track_focus(&self.focus_handle(cx))
.on_action(cx.listener(Self::cancel)) .on_action(cx.listener(Self::cancel))
.key_context("ZedPredictModal")
.on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| { .on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
cx.emit(DismissEvent); cx.emit(DismissEvent);
})) }))
@ -155,15 +180,15 @@ impl Render for ZedPredictModal {
div() div()
.p_1p5() .p_1p5()
.absolute() .absolute()
.top_0() .top_1()
.left_0() .left_1p5()
.right_0() .right_0()
.h(px(200.)) .h(px(200.))
.child( .child(
svg() svg()
.path("icons/zed_predict_bg.svg") .path("icons/zed_predict_bg.svg")
.text_color(cx.theme().colors().icon_disabled) .text_color(cx.theme().colors().icon_disabled)
.w(px(416.)) .w(px(418.))
.h(px(128.)) .h(px(128.))
.overflow_hidden(), .overflow_hidden(),
), ),
@ -249,24 +274,49 @@ impl Render for ZedPredictModal {
if self.user_store.read(cx).current_user().is_some() { if self.user_store.read(cx).current_user().is_some() {
let copy = match self.sign_in_status { let copy = match self.sign_in_status {
SignInStatus::Idle => "Get accurate and helpful edit predictions at every keystroke. To set Zed as your inline completions provider, ensure you:", SignInStatus::Idle => "Get accurate and instant edit predictions at every keystroke. Before setting Zed as your inline completions provider:",
SignInStatus::SignedIn => "Almost there! Ensure you:", SignInStatus::SignedIn => "Almost there! Ensure you:",
SignInStatus::Waiting => unreachable!(), SignInStatus::Waiting => unreachable!(),
}; };
let accordion_icons = if self.data_collection_expanded {
(IconName::ChevronUp, IconName::ChevronDown)
} else {
(IconName::ChevronDown, IconName::ChevronUp)
};
fn label_item(label_text: impl Into<SharedString>) -> impl Element {
Label::new(label_text).color(Color::Muted).into_element()
}
fn info_item(label_text: impl Into<SharedString>) -> impl Element {
h_flex()
.gap_2()
.child(Icon::new(IconName::Check).size(IconSize::XSmall))
.child(label_item(label_text))
}
fn multiline_info_item<E1: Into<SharedString>, E2: IntoElement>(
first_line: E1,
second_line: E2,
) -> impl Element {
v_flex()
.child(info_item(first_line))
.child(div().pl_5().child(second_line))
}
base.child(Label::new(copy).color(Color::Muted)) base.child(Label::new(copy).color(Color::Muted))
.child( .child(
h_flex() h_flex()
.gap_0p5() .child(
.child(CheckboxWithLabel::new( Checkbox::new("tos-checkbox", self.terms_of_service.into())
"tos-checkbox", .fill()
Label::new("Have read and accepted the").color(Color::Muted), .label("Read and accept the")
self.terms_of_service.into(), .on_click(cx.listener(move |this, state, _window, cx| {
cx.listener(move |this, state, _window, cx| { this.terms_of_service = *state == ToggleState::Selected;
this.terms_of_service = *state == ToggleState::Selected; cx.notify()
cx.notify() })),
}), )
))
.child( .child(
Button::new("view-tos", "Terms of Service") Button::new("view-tos", "Terms of Service")
.icon(IconName::ArrowUpRight) .icon(IconName::ArrowUpRight)
@ -275,6 +325,88 @@ impl Render for ZedPredictModal {
.on_click(cx.listener(Self::view_terms)), .on_click(cx.listener(Self::view_terms)),
), ),
) )
.child(
v_flex()
.child(
h_flex()
.child(
Checkbox::new(
"training-data-checkbox",
self.data_collection_opted_in.into(),
)
.label("Optionally share training data (OSS-only).")
.fill()
.when(self.worktrees.is_empty(), |element| {
element.disabled(true).tooltip(move |window, cx| {
Tooltip::with_meta(
"No Project Open",
None,
"Open a project to enable this option.",
window,
cx,
)
})
})
.on_click(cx.listener(
move |this, state, _window, cx| {
this.data_collection_opted_in =
*state == ToggleState::Selected;
cx.notify()
},
)),
)
// TODO: show each worktree if more than 1
.child(
Button::new("learn-more", "Learn More")
.icon(accordion_icons.0)
.icon_size(IconSize::Indicator)
.icon_color(Color::Muted)
.on_click(cx.listener(|this, _, _, cx| {
this.data_collection_expanded =
!this.data_collection_expanded;
cx.notify()
})),
),
)
.when(self.data_collection_expanded, |element| {
element.child(
v_flex()
.mt_2()
.p_2()
.rounded_md()
.bg(cx.theme().colors().editor_background.opacity(0.5))
.border_1()
.border_color(cx.theme().colors().border_variant)
.child(
div().child(
Label::new("To improve edit predictions, help fine-tune Zed's model by sharing data from the open-source projects you work on.")
.mb_1()
)
)
.child(info_item(
"We ask this exclusively for open-source projects.",
))
.child(info_item(
"Zed automatically detects if your project is open-source.",
))
.child(info_item(
"This setting is valid for all OSS projects you open in Zed.",
))
.child(info_item("Toggle it anytime via the status bar menu."))
.child(multiline_info_item(
"Files that can contain sensitive data, like `.env`, are",
h_flex()
.child(label_item("excluded by default via the"))
.child(
Button::new("doc-link", "disabled_globs").on_click(
cx.listener(Self::inline_completions_doc),
),
)
.child(label_item("setting.")),
)),
)
}),
)
.child( .child(
v_flex() v_flex()
.mt_2() .mt_2()

View file

@ -1,48 +0,0 @@
use std::path::{Path, PathBuf};
use workspace::WorkspaceDb;
use db::sqlez_macros::sql;
use db::{define_connection, query};
define_connection!(
pub static ref DB: ZetaDb<WorkspaceDb> = &[
sql! (
CREATE TABLE zeta_preferences(
worktree_path BLOB NOT NULL PRIMARY KEY,
accepted_data_collection INTEGER
) STRICT;
),
];
);
impl ZetaDb {
query! {
pub fn get_all_data_collection_preferences() -> Result<Vec<(PathBuf, bool)>> {
SELECT worktree_path, accepted_data_collection FROM zeta_preferences
}
}
query! {
pub fn get_accepted_data_collection(worktree_path: &Path) -> Result<Option<bool>> {
SELECT accepted_data_collection FROM zeta_preferences
WHERE worktree_path = ?
}
}
query! {
pub async fn save_data_collection_choice(worktree_path: PathBuf, accepted_data_collection: bool) -> Result<()> {
INSERT INTO zeta_preferences
(worktree_path, accepted_data_collection)
VALUES
(?1, ?2)
ON CONFLICT (worktree_path) DO UPDATE SET
accepted_data_collection = ?2
}
}
query! {
pub async fn clear_all_zeta_preferences() -> Result<()> {
DELETE FROM zeta_preferences
}
}
}

View file

@ -1,10 +1,8 @@
use crate::{CompletionDiffElement, InlineCompletion, InlineCompletionRating, Zeta}; use crate::{CompletionDiffElement, InlineCompletion, InlineCompletionRating, Zeta};
use command_palette_hooks::CommandPaletteFilter;
use editor::Editor; use editor::Editor;
use feature_flags::{FeatureFlagAppExt as _, PredictEditsRateCompletionsFeatureFlag};
use gpui::{actions, prelude::*, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable}; use gpui::{actions, prelude::*, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable};
use language::language_settings; use language::language_settings;
use std::{any::TypeId, time::Duration}; use std::time::Duration;
use ui::{prelude::*, KeyBinding, List, ListItem, ListItemSpacing, Tooltip}; use ui::{prelude::*, KeyBinding, List, ListItem, ListItemSpacing, Tooltip};
use workspace::{ModalView, Workspace}; use workspace::{ModalView, Workspace};
@ -21,40 +19,6 @@ actions!(
] ]
); );
pub fn init(cx: &mut App) {
cx.observe_new(move |workspace: &mut Workspace, _, _cx| {
workspace.register_action(|workspace, _: &RateCompletions, window, cx| {
if cx.has_flag::<PredictEditsRateCompletionsFeatureFlag>() {
RateCompletionModal::toggle(workspace, window, cx);
}
});
})
.detach();
feature_gate_predict_edits_rating_actions(cx);
}
fn feature_gate_predict_edits_rating_actions(cx: &mut App) {
let rate_completion_action_types = [TypeId::of::<RateCompletions>()];
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_action_types(&rate_completion_action_types);
});
cx.observe_flag::<PredictEditsRateCompletionsFeatureFlag, _>(move |is_enabled, cx| {
if is_enabled {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.show_action_types(rate_completion_action_types.iter());
});
} else {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_action_types(&rate_completion_action_types);
});
}
})
.detach();
}
pub struct RateCompletionModal { pub struct RateCompletionModal {
zeta: Entity<Zeta>, zeta: Entity<Zeta>,
active_completion: Option<ActiveCompletion>, active_completion: Option<ActiveCompletion>,

View file

@ -1,22 +1,26 @@
mod completion_diff_element; mod completion_diff_element;
mod persistence; mod init;
mod license_detection;
mod onboarding_banner;
mod onboarding_modal;
mod rate_completion_modal; mod rate_completion_modal;
pub(crate) use completion_diff_element::*; pub(crate) use completion_diff_element::*;
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
pub use init::*;
use inline_completion::DataCollectionState; use inline_completion::DataCollectionState;
pub use license_detection::is_license_eligible_for_data_collection;
pub use onboarding_banner::*;
pub use rate_completion_modal::*; pub use rate_completion_modal::*;
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
use arrayvec::ArrayVec; use arrayvec::ArrayVec;
use client::{Client, UserStore}; use client::{Client, UserStore};
use collections::hash_map::Entry;
use collections::{HashMap, HashSet, VecDeque}; use collections::{HashMap, HashSet, VecDeque};
use feature_flags::FeatureFlagAppExt as _; use feature_flags::FeatureFlagAppExt as _;
use futures::AsyncReadExt; use futures::AsyncReadExt;
use gpui::{ use gpui::{
actions, App, AppContext as _, AsyncApp, Context, Entity, EntityId, Global, Subscription, Task, actions, App, AppContext as _, AsyncApp, Context, Entity, EntityId, Global, Subscription, Task,
WeakEntity,
}; };
use http_client::{HttpClient, Method}; use http_client::{HttpClient, Method};
use language::{ use language::{
@ -24,33 +28,32 @@ use language::{
OffsetRangeExt, Point, ToOffset, ToPoint, OffsetRangeExt, Point, ToOffset, ToPoint,
}; };
use language_models::LlmApiToken; use language_models::LlmApiToken;
use postage::watch;
use rpc::{PredictEditsParams, PredictEditsResponse, EXPIRED_LLM_TOKEN_HEADER_NAME}; use rpc::{PredictEditsParams, PredictEditsResponse, EXPIRED_LLM_TOKEN_HEADER_NAME};
use settings::WorktreeId;
use std::{ use std::{
borrow::Cow, borrow::Cow,
cmp, env, cmp,
fmt::Write, fmt::Write,
future::Future, future::Future,
mem, mem,
ops::Range, ops::Range,
path::{Path, PathBuf}, path::Path,
rc::Rc,
sync::Arc, sync::Arc,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use telemetry_events::InlineCompletionRating; use telemetry_events::InlineCompletionRating;
use util::ResultExt; use util::ResultExt;
use uuid::Uuid; use uuid::Uuid;
use workspace::{ use worktree::Worktree;
notifications::{simple_message_notification::MessageNotification, NotificationId},
Workspace,
};
const CURSOR_MARKER: &'static str = "<|user_cursor_is_here|>"; const CURSOR_MARKER: &'static str = "<|user_cursor_is_here|>";
const START_OF_FILE_MARKER: &'static str = "<|start_of_file|>"; const START_OF_FILE_MARKER: &'static str = "<|start_of_file|>";
const EDITABLE_REGION_START_MARKER: &'static str = "<|editable_region_start|>"; const EDITABLE_REGION_START_MARKER: &'static str = "<|editable_region_start|>";
const EDITABLE_REGION_END_MARKER: &'static str = "<|editable_region_end|>"; const EDITABLE_REGION_END_MARKER: &'static str = "<|editable_region_end|>";
const BUFFER_CHANGE_GROUPING_INTERVAL: Duration = Duration::from_secs(1); const BUFFER_CHANGE_GROUPING_INTERVAL: Duration = Duration::from_secs(1);
const ZED_PREDICT_DATA_COLLECTION_NEVER_ASK_AGAIN_KEY: &'static str = const ZED_PREDICT_DATA_COLLECTION_CHOICE: &str = "zed_predict_data_collection_choice";
"zed_predict_data_collection_never_ask_again";
// TODO(mgsloan): more systematic way to choose or tune these fairly arbitrary constants? // TODO(mgsloan): more systematic way to choose or tune these fairly arbitrary constants?
@ -206,11 +209,12 @@ pub struct Zeta {
registered_buffers: HashMap<gpui::EntityId, RegisteredBuffer>, registered_buffers: HashMap<gpui::EntityId, RegisteredBuffer>,
shown_completions: VecDeque<InlineCompletion>, shown_completions: VecDeque<InlineCompletion>,
rated_completions: HashSet<InlineCompletionId>, rated_completions: HashSet<InlineCompletionId>,
data_collection_preferences: DataCollectionPreferences, data_collection_choice: Entity<DataCollectionChoice>,
llm_token: LlmApiToken, llm_token: LlmApiToken,
_llm_token_subscription: Subscription, _llm_token_subscription: Subscription,
tos_accepted: bool, // Terms of service accepted tos_accepted: bool, // Terms of service accepted
_user_store_subscription: Subscription, _user_store_subscription: Subscription,
license_detection_watchers: HashMap<WorktreeId, Rc<LicenseDetectionWatcher>>,
} }
impl Zeta { impl Zeta {
@ -219,15 +223,28 @@ impl Zeta {
} }
pub fn register( pub fn register(
worktree: Option<Entity<Worktree>>,
client: Arc<Client>, client: Arc<Client>,
user_store: Entity<UserStore>, user_store: Entity<UserStore>,
cx: &mut App, cx: &mut App,
) -> Entity<Self> { ) -> Entity<Self> {
Self::global(cx).unwrap_or_else(|| { let this = Self::global(cx).unwrap_or_else(|| {
let model = cx.new(|cx| Self::new(client, user_store, cx)); let model = cx.new(|cx| Self::new(client, user_store, cx));
cx.set_global(ZetaGlobal(model.clone())); cx.set_global(ZetaGlobal(model.clone()));
model model
}) });
this.update(cx, move |this, cx| {
if let Some(worktree) = worktree {
worktree.update(cx, |worktree, cx| {
this.license_detection_watchers
.entry(worktree.id())
.or_insert_with(|| Rc::new(LicenseDetectionWatcher::new(worktree, cx)));
});
}
});
this
} }
pub fn clear_history(&mut self) { pub fn clear_history(&mut self) {
@ -236,13 +253,17 @@ impl Zeta {
fn new(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut Context<Self>) -> Self { fn new(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut Context<Self>) -> Self {
let refresh_llm_token_listener = language_models::RefreshLlmTokenListener::global(cx); let refresh_llm_token_listener = language_models::RefreshLlmTokenListener::global(cx);
let data_collection_choice = Self::load_data_collection_choices();
let data_collection_choice = cx.new(|_| data_collection_choice);
Self { Self {
client, client,
events: VecDeque::new(), events: VecDeque::new(),
shown_completions: VecDeque::new(), shown_completions: VecDeque::new(),
rated_completions: HashSet::default(), rated_completions: HashSet::default(),
registered_buffers: HashMap::default(), registered_buffers: HashMap::default(),
data_collection_preferences: Self::load_data_collection_preferences(cx), data_collection_choice,
llm_token: LlmApiToken::default(), llm_token: LlmApiToken::default(),
_llm_token_subscription: cx.subscribe( _llm_token_subscription: cx.subscribe(
&refresh_llm_token_listener, &refresh_llm_token_listener,
@ -271,6 +292,7 @@ impl Zeta {
_ => {} _ => {}
} }
}), }),
license_detection_watchers: HashMap::default(),
} }
} }
@ -342,7 +364,7 @@ impl Zeta {
&mut self, &mut self,
buffer: &Entity<Buffer>, buffer: &Entity<Buffer>,
cursor: language::Anchor, cursor: language::Anchor,
can_collect_data: bool, data_collection_permission: bool,
cx: &mut Context<Self>, cx: &mut Context<Self>,
perform_predict_edits: F, perform_predict_edits: F,
) -> Task<Result<Option<InlineCompletion>>> ) -> Task<Result<Option<InlineCompletion>>>
@ -407,7 +429,7 @@ impl Zeta {
input_events: input_events.clone(), input_events: input_events.clone(),
input_excerpt: input_excerpt.clone(), input_excerpt: input_excerpt.clone(),
outline: Some(input_outline.clone()), outline: Some(input_outline.clone()),
can_collect_data, data_collection_permission,
}; };
let response = perform_predict_edits(client, llm_token, is_staff, body).await?; let response = perform_predict_edits(client, llm_token, is_staff, body).await?;
@ -587,13 +609,13 @@ and then another
&mut self, &mut self,
buffer: &Entity<Buffer>, buffer: &Entity<Buffer>,
position: language::Anchor, position: language::Anchor,
can_collect_data: bool, data_collection_permission: bool,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<Result<Option<InlineCompletion>>> { ) -> Task<Result<Option<InlineCompletion>>> {
self.request_completion_impl( self.request_completion_impl(
buffer, buffer,
position, position,
can_collect_data, data_collection_permission,
cx, cx,
Self::perform_predict_edits, Self::perform_predict_edits,
) )
@ -903,84 +925,55 @@ and then another
new_snapshot new_snapshot
} }
/// Creates a `Entity<DataCollectionChoice>` for each unique worktree abs path it sees. fn load_data_collection_choices() -> DataCollectionChoice {
pub fn data_collection_choice_at( let choice = KEY_VALUE_STORE
&mut self, .read_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE)
worktree_abs_path: PathBuf, .log_err()
cx: &mut Context<Self>, .flatten();
) -> Entity<DataCollectionChoice> {
match self match choice.as_deref() {
.data_collection_preferences Some("true") => DataCollectionChoice::Enabled,
.per_worktree Some("false") => DataCollectionChoice::Disabled,
.entry(worktree_abs_path) Some(_) => {
{ log::error!("unknown value in '{ZED_PREDICT_DATA_COLLECTION_CHOICE}'");
Entry::Vacant(entry) => { DataCollectionChoice::NotAnswered
let choice = cx.new(|_| DataCollectionChoice::NotAnswered);
entry.insert(choice.clone());
choice
} }
Entry::Occupied(entry) => entry.get().clone(), None => DataCollectionChoice::NotAnswered,
}
}
fn set_never_ask_again_for_data_collection(&mut self, cx: &mut Context<Self>) {
self.data_collection_preferences.never_ask_again = true;
// persist choice
db::write_and_log(cx, move || {
KEY_VALUE_STORE.write_kvp(
ZED_PREDICT_DATA_COLLECTION_NEVER_ASK_AGAIN_KEY.into(),
"true".to_string(),
)
});
}
fn load_data_collection_preferences(cx: &mut Context<Self>) -> DataCollectionPreferences {
if env::var("ZED_PREDICT_CLEAR_DATA_COLLECTION_PREFERENCES").is_ok() {
db::write_and_log(cx, move || async move {
KEY_VALUE_STORE
.delete_kvp(ZED_PREDICT_DATA_COLLECTION_NEVER_ASK_AGAIN_KEY.into())
.await
.log_err();
persistence::DB.clear_all_zeta_preferences().await
});
return DataCollectionPreferences::default();
}
let never_ask_again = KEY_VALUE_STORE
.read_kvp(ZED_PREDICT_DATA_COLLECTION_NEVER_ASK_AGAIN_KEY)
.log_err()
.flatten()
.map(|value| value == "true")
.unwrap_or(false);
let preferences_per_worktree = persistence::DB
.get_all_data_collection_preferences()
.log_err()
.into_iter()
.flatten()
.map(|(path, choice)| {
let choice = cx.new(|_| DataCollectionChoice::from(choice));
(path, choice)
})
.collect();
DataCollectionPreferences {
never_ask_again,
per_worktree: preferences_per_worktree,
} }
} }
} }
#[derive(Default, Debug)] struct LicenseDetectionWatcher {
struct DataCollectionPreferences { is_open_source_rx: watch::Receiver<bool>,
/// Set when a user clicks on "Never Ask Again", can never be unset. _is_open_source_task: Task<()>,
never_ask_again: bool, }
/// The choices for each worktree.
/// impl LicenseDetectionWatcher {
/// This is filled when loading from database, or when querying if no matching path is found. pub fn new(worktree: &Worktree, cx: &mut Context<Worktree>) -> Self {
per_worktree: HashMap<PathBuf, Entity<DataCollectionChoice>>, let (mut is_open_source_tx, is_open_source_rx) = watch::channel_with::<bool>(false);
let loaded_file_fut = worktree.load_file(Path::new("LICENSE"), false, cx);
Self {
is_open_source_rx,
_is_open_source_task: cx.spawn(|_, _| async move {
// TODO: Don't display error if file not found
let Some(loaded_file) = loaded_file_fut.await.log_err() else {
return;
};
let is_loaded_file_open_source_thing: bool =
is_license_eligible_for_data_collection(&loaded_file.text);
*is_open_source_tx.borrow_mut() = is_loaded_file_open_source_thing;
}),
}
}
/// Answers false until we find out it's open source
pub fn is_open_source(&self) -> bool {
*self.is_open_source_rx.borrow()
}
} }
fn common_prefix<T1: Iterator<Item = char>, T2: Iterator<Item = char>>(a: T1, b: T2) -> usize { fn common_prefix<T1: Iterator<Item = char>, T2: Iterator<Item = char>>(a: T1, b: T2) -> usize {
@ -1308,7 +1301,7 @@ impl DataCollectionChoice {
} }
} }
pub fn toggle(self) -> DataCollectionChoice { pub fn toggle(&self) -> DataCollectionChoice {
match self { match self {
Self::Enabled => Self::Disabled, Self::Enabled => Self::Disabled,
Self::Disabled => Self::Enabled, Self::Disabled => Self::Enabled,
@ -1326,87 +1319,93 @@ impl From<bool> for DataCollectionChoice {
} }
} }
pub struct ZetaInlineCompletionProvider {
zeta: Entity<Zeta>,
pending_completions: ArrayVec<PendingCompletion, 2>,
next_pending_completion_id: usize,
current_completion: Option<CurrentInlineCompletion>,
data_collection: Option<ProviderDataCollection>,
}
pub struct ProviderDataCollection { pub struct ProviderDataCollection {
workspace: WeakEntity<Workspace>, /// When set to None, data collection is not possible in the provider buffer
worktree_root_path: PathBuf, choice: Option<Entity<DataCollectionChoice>>,
choice: Entity<DataCollectionChoice>, license_detection_watcher: Option<Rc<LicenseDetectionWatcher>>,
} }
impl ProviderDataCollection { impl ProviderDataCollection {
pub fn new( pub fn new(zeta: Entity<Zeta>, buffer: Option<Entity<Buffer>>, cx: &mut App) -> Self {
zeta: Entity<Zeta>, let choice_and_watcher = buffer.and_then(|buffer| {
workspace: Option<Entity<Workspace>>, let file = buffer.read(cx).file()?;
buffer: Option<Entity<Buffer>>,
cx: &mut App,
) -> Option<ProviderDataCollection> {
let workspace = workspace?;
let worktree_root_path = buffer?.update(cx, |buffer, cx| {
let file = buffer.file()?;
if !file.is_local() || file.is_private() { if !file.is_local() || file.is_private() {
return None; return None;
} }
workspace.update(cx, |workspace, cx| { let zeta = zeta.read(cx);
Some( let choice = zeta.data_collection_choice.clone();
workspace
.absolute_path_of_worktree(file.worktree_id(cx), cx)? // Unwrap safety: there should be a watcher for each worktree
.to_path_buf(), let license_detection_watcher = zeta
.license_detection_watchers
.get(&file.worktree_id(cx))
.cloned()?;
Some((choice, license_detection_watcher))
});
if let Some((choice, watcher)) = choice_and_watcher {
ProviderDataCollection {
choice: Some(choice),
license_detection_watcher: Some(watcher),
}
} else {
ProviderDataCollection {
choice: None,
license_detection_watcher: None,
}
}
}
pub fn data_collection_permission(&self, cx: &App) -> bool {
self.choice
.as_ref()
.is_some_and(|choice| choice.read(cx).is_enabled())
&& self
.license_detection_watcher
.as_ref()
.is_some_and(|watcher| watcher.is_open_source())
}
pub fn toggle(&mut self, cx: &mut App) {
if let Some(choice) = self.choice.as_mut() {
let new_choice = choice.update(cx, |choice, _cx| {
let new_choice = choice.toggle();
*choice = new_choice;
new_choice
});
db::write_and_log(cx, move || {
KEY_VALUE_STORE.write_kvp(
ZED_PREDICT_DATA_COLLECTION_CHOICE.into(),
new_choice.is_enabled().to_string(),
) )
}) });
})?; }
let choice = zeta.update(cx, |zeta, cx| {
zeta.data_collection_choice_at(worktree_root_path.clone(), cx)
});
Some(ProviderDataCollection {
workspace: workspace.downgrade(),
worktree_root_path,
choice,
})
} }
}
fn set_choice(&mut self, choice: DataCollectionChoice, cx: &mut App) { pub struct ZetaInlineCompletionProvider {
self.choice.update(cx, |this, _| *this = choice); zeta: Entity<Zeta>,
pending_completions: ArrayVec<PendingCompletion, 2>,
let worktree_root_path = self.worktree_root_path.clone(); next_pending_completion_id: usize,
current_completion: Option<CurrentInlineCompletion>,
db::write_and_log(cx, move || { /// None if this is entirely disabled for this provider
persistence::DB.save_data_collection_choice(worktree_root_path, choice.is_enabled()) provider_data_collection: ProviderDataCollection,
});
}
fn toggle_choice(&mut self, cx: &mut App) {
self.set_choice(self.choice.read(cx).toggle(), cx);
}
} }
impl ZetaInlineCompletionProvider { impl ZetaInlineCompletionProvider {
pub const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(8); pub const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(8);
pub fn new(zeta: Entity<Zeta>, data_collection: Option<ProviderDataCollection>) -> Self { pub fn new(zeta: Entity<Zeta>, provider_data_collection: ProviderDataCollection) -> Self {
Self { Self {
zeta, zeta,
pending_completions: ArrayVec::new(), pending_completions: ArrayVec::new(),
next_pending_completion_id: 0, next_pending_completion_id: 0,
current_completion: None, current_completion: None,
data_collection, provider_data_collection,
}
}
fn set_data_collection_choice(&mut self, choice: DataCollectionChoice, cx: &mut App) {
if let Some(data_collection) = self.data_collection.as_mut() {
data_collection.set_choice(choice, cx);
} }
} }
} }
@ -1433,11 +1432,7 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
} }
fn data_collection_state(&self, cx: &App) -> DataCollectionState { fn data_collection_state(&self, cx: &App) -> DataCollectionState {
let Some(data_collection) = self.data_collection.as_ref() else { if self.provider_data_collection.data_collection_permission(cx) {
return DataCollectionState::Unknown;
};
if data_collection.choice.read(cx).is_enabled() {
DataCollectionState::Enabled DataCollectionState::Enabled
} else { } else {
DataCollectionState::Disabled DataCollectionState::Disabled
@ -1445,9 +1440,7 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
} }
fn toggle_data_collection(&mut self, cx: &mut App) { fn toggle_data_collection(&mut self, cx: &mut App) {
if let Some(data_collection) = self.data_collection.as_mut() { self.provider_data_collection.toggle(cx);
data_collection.toggle_choice(cx);
}
} }
fn is_enabled( fn is_enabled(
@ -1495,12 +1488,8 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
let pending_completion_id = self.next_pending_completion_id; let pending_completion_id = self.next_pending_completion_id;
self.next_pending_completion_id += 1; self.next_pending_completion_id += 1;
let can_collect_data = self let data_collection_permission =
.data_collection self.provider_data_collection.data_collection_permission(cx);
.as_ref()
.map_or(false, |data_collection| {
data_collection.choice.read(cx).is_enabled()
});
let task = cx.spawn(|this, mut cx| async move { let task = cx.spawn(|this, mut cx| async move {
if debounce { if debounce {
@ -1509,7 +1498,7 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
let completion_request = this.update(&mut cx, |this, cx| { let completion_request = this.update(&mut cx, |this, cx| {
this.zeta.update(cx, |zeta, cx| { this.zeta.update(cx, |zeta, cx| {
zeta.request_completion(&buffer, position, can_collect_data, cx) zeta.request_completion(&buffer, position, data_collection_permission, cx)
}) })
}); });
@ -1596,79 +1585,8 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
// Right now we don't support cycling. // Right now we don't support cycling.
} }
fn accept(&mut self, cx: &mut Context<Self>) { fn accept(&mut self, _cx: &mut Context<Self>) {
self.pending_completions.clear(); self.pending_completions.clear();
let Some(data_collection) = self.data_collection.as_mut() else {
return;
};
if data_collection.choice.read(cx).is_answered()
|| self
.zeta
.read(cx)
.data_collection_preferences
.never_ask_again
{
return;
}
struct ZetaDataCollectionNotification;
let notification_id = NotificationId::unique::<ZetaDataCollectionNotification>();
const DATA_COLLECTION_INFO_URL: &str = "https://zed.dev/terms-of-service"; // TODO: Replace for a link that's dedicated to Edit Predictions data collection
let this = cx.entity();
data_collection
.workspace
.update(cx, |workspace, cx| {
workspace.show_notification(notification_id, cx, |cx| {
let zeta = self.zeta.clone();
cx.new(move |_cx| {
let message =
"To allow Zed to suggest better edits, turn on data collection. You \
can turn off at any time via the status bar menu.";
MessageNotification::new(message)
.with_title("Per-Project Data Collection Program")
.show_close_button(false)
.with_click_message("Turn On")
.on_click({
let this = this.clone();
move |_window, cx| {
this.update(cx, |this, cx| {
this.set_data_collection_choice(
DataCollectionChoice::Enabled,
cx,
)
});
}
})
.with_secondary_click_message("Turn Off")
.on_secondary_click({
move |_window, cx| {
this.update(cx, |this, cx| {
this.set_data_collection_choice(
DataCollectionChoice::Disabled,
cx,
)
});
}
})
.with_tertiary_click_message("Never Ask Again")
.on_tertiary_click({
move |_window, cx| {
zeta.update(cx, |zeta, cx| {
zeta.set_never_ask_again_for_data_collection(cx);
});
}
})
.more_info_message("Learn More")
.more_info_url(DATA_COLLECTION_INFO_URL)
})
});
})
.log_err();
} }
fn discard(&mut self, _cx: &mut Context<Self>) { fn discard(&mut self, _cx: &mut Context<Self>) {