diff --git a/Cargo.lock b/Cargo.lock index d001c3371b..35ac944cd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3959,6 +3959,7 @@ dependencies = [ "util", "uuid", "workspace", + "zed_predict_tos", ] [[package]] @@ -6304,6 +6305,7 @@ name = "inline_completion_button" version = "0.1.0" dependencies = [ "anyhow", + "client", "copilot", "editor", "feature_flags", @@ -6323,6 +6325,7 @@ dependencies = [ "ui", "workspace", "zed_actions", + "zed_predict_tos", "zeta", ] @@ -16337,6 +16340,7 @@ dependencies = [ "winresource", "workspace", "zed_actions", + "zed_predict_tos", "zeta", ] @@ -16450,6 +16454,17 @@ dependencies = [ "zed_extension_api 0.1.0", ] +[[package]] +name = "zed_predict_tos" +version = "0.1.0" +dependencies = [ + "client", + "gpui", + "menu", + "ui", + "workspace", +] + [[package]] name = "zed_prisma" version = "0.0.4" diff --git a/Cargo.toml b/Cargo.toml index 8be87f0584..8ee5988c17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ "crates/activity_indicator", + "crates/zed_predict_tos", "crates/anthropic", "crates/assets", "crates/assistant", @@ -198,6 +199,7 @@ edition = "2021" activity_indicator = { path = "crates/activity_indicator" } ai = { path = "crates/ai" } +zed_predict_tos = { path = "crates/zed_predict_tos" } anthropic = { path = "crates/anthropic" } assets = { path = "crates/assets" } assistant = { path = "crates/assistant" } diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 57d43bbf3b..c5cd705438 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -875,5 +875,12 @@ "cmd-shift-enter": "zeta::ThumbsUpActiveCompletion", "cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion" } + }, + { + "context": "ZedPredictTos", + "use_key_equivalents": true, + "bindings": { + "escape": "menu::Cancel" + } } ] diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 17da726954..2427bc4c40 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -122,6 +122,9 @@ pub enum Event { }, ShowContacts, ParticipantIndicesChanged, + TermsStatusUpdated { + accepted: bool, + }, } #[derive(Clone, Copy)] @@ -210,10 +213,24 @@ impl UserStore { staff, ); - this.update(cx, |this, _| { - this.set_current_user_accepted_tos_at( - info.accepted_tos_at, - ); + this.update(cx, |this, cx| { + let accepted_tos_at = { + #[cfg(debug_assertions)] + if std::env::var("ZED_IGNORE_ACCEPTED_TOS").is_ok() + { + None + } else { + info.accepted_tos_at + } + + #[cfg(not(debug_assertions))] + info.accepted_tos_at + }; + + this.set_current_user_accepted_tos_at(accepted_tos_at); + cx.emit(Event::TermsStatusUpdated { + accepted: accepted_tos_at.is_some(), + }); }) } else { anyhow::Ok(()) @@ -704,8 +721,9 @@ impl UserStore { .await .context("error accepting tos")?; - this.update(&mut cx, |this, _| { - this.set_current_user_accepted_tos_at(Some(response.accepted_tos_at)) + this.update(&mut cx, |this, cx| { + this.set_current_user_accepted_tos_at(Some(response.accepted_tos_at)); + cx.emit(Event::TermsStatusUpdated { accepted: true }); }) } else { Err(anyhow!("client not found")) diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 6968a2e24b..95afdc4c1a 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -88,6 +88,7 @@ url.workspace = true util.workspace = true uuid.workspace = true workspace.workspace = true +zed_predict_tos.workspace = true [dev-dependencies] ctor.workspace = true diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index f498d24615..7feda719ea 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -616,6 +616,25 @@ impl CompletionsMenu { ) })), ), + CompletionEntry::InlineCompletionHint( + hint @ InlineCompletionMenuHint::PendingTermsAcceptance, + ) => div().min_w(px(250.)).max_w(px(500.)).child( + ListItem::new("inline-completion") + .inset(true) + .toggle_state(item_ix == selected_item) + .start_slot(Icon::new(IconName::ZedPredict)) + .child( + base_label.child( + StyledText::new(hint.label()) + .with_highlights(&style.text, None), + ), + ) + .on_click(cx.listener(move |editor, _event, cx| { + cx.stop_propagation(); + editor.toggle_zed_predict_tos(cx); + })), + ), + CompletionEntry::InlineCompletionHint( hint @ InlineCompletionMenuHint::Loaded { .. }, ) => div().min_w(px(250.)).max_w(px(500.)).child( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 70039c6725..77c9ba67fe 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -70,6 +70,7 @@ pub use element::{ }; use futures::{future, FutureExt}; use fuzzy::StringMatchCandidate; +use zed_predict_tos::ZedPredictTos; use code_context_menus::{ AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu, @@ -459,6 +460,7 @@ type CompletionId = usize; enum InlineCompletionMenuHint { Loading, Loaded { text: InlineCompletionText }, + PendingTermsAcceptance, None, } @@ -468,6 +470,7 @@ impl InlineCompletionMenuHint { InlineCompletionMenuHint::Loading | InlineCompletionMenuHint::Loaded { .. } => { "Edit Prediction" } + InlineCompletionMenuHint::PendingTermsAcceptance => "Accept Terms of Service", InlineCompletionMenuHint::None => "No Prediction", } } @@ -3828,6 +3831,14 @@ impl Editor { self.do_completion(action.item_ix, CompletionIntent::Compose, cx) } + fn toggle_zed_predict_tos(&mut self, cx: &mut ViewContext) { + let (Some(workspace), Some(project)) = (self.workspace(), self.project.as_ref()) else { + return; + }; + + ZedPredictTos::toggle(workspace, project.read(cx).user_store().clone(), cx); + } + fn do_completion( &mut self, item_ix: Option, @@ -3851,6 +3862,14 @@ impl Editor { self.context_menu_next(&Default::default(), cx); return Some(Task::ready(Ok(()))); } + Some(CompletionEntry::InlineCompletionHint( + InlineCompletionMenuHint::PendingTermsAcceptance, + )) => { + drop(entries); + drop(context_menu); + self.toggle_zed_predict_tos(cx); + return Some(Task::ready(Ok(()))); + } _ => {} } } @@ -4974,6 +4993,8 @@ impl Editor { Some(InlineCompletionMenuHint::Loaded { text }) } else if provider.is_refreshing(cx) { Some(InlineCompletionMenuHint::Loading) + } else if provider.needs_terms_acceptance(cx) { + Some(InlineCompletionMenuHint::PendingTermsAcceptance) } else { Some(InlineCompletionMenuHint::None) } diff --git a/crates/inline_completion/src/inline_completion.rs b/crates/inline_completion/src/inline_completion.rs index 17b77ca4bf..4b467906bb 100644 --- a/crates/inline_completion/src/inline_completion.rs +++ b/crates/inline_completion/src/inline_completion.rs @@ -36,6 +36,9 @@ pub trait InlineCompletionProvider: 'static + Sized { debounce: bool, cx: &mut ModelContext, ); + fn needs_terms_acceptance(&self, _cx: &AppContext) -> bool { + false + } fn cycle( &mut self, buffer: Model, @@ -64,6 +67,7 @@ pub trait InlineCompletionProviderHandle { ) -> bool; fn show_completions_in_menu(&self) -> bool; fn show_completions_in_normal_mode(&self) -> bool; + fn needs_terms_acceptance(&self, cx: &AppContext) -> bool; fn is_refreshing(&self, cx: &AppContext) -> bool; fn refresh( &self, @@ -118,6 +122,10 @@ where self.read(cx).is_enabled(buffer, cursor_position, cx) } + fn needs_terms_acceptance(&self, cx: &AppContext) -> bool { + self.read(cx).needs_terms_acceptance(cx) + } + fn is_refreshing(&self, cx: &AppContext) -> bool { self.read(cx).is_refreshing() } diff --git a/crates/inline_completion_button/Cargo.toml b/crates/inline_completion_button/Cargo.toml index ae1bbd0941..27aaf093a6 100644 --- a/crates/inline_completion_button/Cargo.toml +++ b/crates/inline_completion_button/Cargo.toml @@ -28,6 +28,8 @@ ui.workspace = true workspace.workspace = true zed_actions.workspace = true zeta.workspace = true +client.workspace = true +zed_predict_tos.workspace = true [dev-dependencies] copilot = { workspace = true, features = ["test-support"] } diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index b210a163e7..39bff55709 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -1,12 +1,13 @@ use anyhow::Result; +use client::UserStore; use copilot::{Copilot, Status}; use editor::{scroll::Autoscroll, Editor}; use feature_flags::{FeatureFlagAppExt, PredictEditsFeatureFlag}; use fs::Fs; use gpui::{ actions, div, pulsating_between, Action, Animation, AnimationExt, AppContext, - AsyncWindowContext, Corner, Entity, IntoElement, ParentElement, Render, Subscription, View, - ViewContext, WeakView, WindowContext, + AsyncWindowContext, Corner, Entity, IntoElement, Model, ParentElement, Render, Subscription, + View, ViewContext, WeakView, WindowContext, }; use language::{ language_settings::{ @@ -17,6 +18,7 @@ use language::{ use settings::{update_settings_file, Settings, SettingsStore}; use std::{path::Path, sync::Arc, time::Duration}; use supermaven::{AccountStatus, Supermaven}; +use ui::{ActiveTheme as _, ButtonLike, Color, Icon, IconWithIndicator, Indicator}; use workspace::{ create_and_open_local_file, item::ItemHandle, @@ -27,6 +29,7 @@ use workspace::{ StatusItemView, Toast, Workspace, }; use zed_actions::OpenBrowser; +use zed_predict_tos::ZedPredictTos; use zeta::RateCompletionModal; actions!(zeta, [RateCompletions]); @@ -43,6 +46,7 @@ pub struct InlineCompletionButton { inline_completion_provider: Option>, fs: Arc, workspace: WeakView, + user_store: Model, } enum SupermavenButtonStatus { @@ -206,6 +210,45 @@ impl Render for InlineCompletionButton { return div(); } + if !self + .user_store + .read(cx) + .current_user_has_accepted_terms() + .unwrap_or(false) + { + let workspace = self.workspace.clone(); + let user_store = self.user_store.clone(); + + return div().child( + ButtonLike::new("zeta-pending-tos-icon") + .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(|cx| { + Tooltip::with_meta( + "Edit Predictions", + None, + "Read Terms of Service", + cx, + ) + }) + .on_click(cx.listener(move |_, _, cx| { + let user_store = user_store.clone(); + + if let Some(workspace) = workspace.upgrade() { + ZedPredictTos::toggle(workspace, user_store, cx); + } + })), + ); + } + let this = cx.view().clone(); let button = IconButton::new("zeta", IconName::ZedPredict) .tooltip(|cx| Tooltip::text("Edit Prediction", cx)); @@ -244,6 +287,7 @@ impl InlineCompletionButton { pub fn new( workspace: WeakView, fs: Arc, + user_store: Model, cx: &mut ViewContext, ) -> Self { if let Some(copilot) = Copilot::global(cx) { @@ -261,6 +305,7 @@ impl InlineCompletionButton { inline_completion_provider: None, workspace, fs, + user_store, } } diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 6325744925..9ab196d7c1 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -68,6 +68,8 @@ pub enum IconSize { #[default] /// 16px Medium, + /// 48px + XLarge, } impl IconSize { @@ -77,6 +79,7 @@ impl IconSize { IconSize::XSmall => rems_from_px(12.), IconSize::Small => rems_from_px(14.), IconSize::Medium => rems_from_px(16.), + IconSize::XLarge => rems_from_px(48.), } } @@ -92,6 +95,7 @@ impl IconSize { IconSize::XSmall => DynamicSpacing::Base02.px(cx), IconSize::Small => DynamicSpacing::Base02.px(cx), IconSize::Medium => DynamicSpacing::Base02.px(cx), + IconSize::XLarge => DynamicSpacing::Base02.px(cx), }; (icon_size, padding) diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index f5e40e5fa0..d493e1e700 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -16,6 +16,7 @@ path = "src/main.rs" [dependencies] activity_indicator.workspace = true +zed_predict_tos.workspace = true anyhow.workspace = true assets.workspace = true assistant.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 52d8808d1a..9c2a495e49 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -438,7 +438,11 @@ fn main() { cx, ); snippet_provider::init(cx); - inline_completion_registry::init(app_state.client.clone(), cx); + inline_completion_registry::init( + app_state.client.clone(), + app_state.user_store.clone(), + cx, + ); let prompt_builder = assistant::init( app_state.fs.clone(), app_state.client.clone(), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index bef0da0a9b..5e0e4a9f9f 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -168,6 +168,7 @@ pub fn initialize_workspace( inline_completion_button::InlineCompletionButton::new( workspace.weak_handle(), app_state.fs.clone(), + app_state.user_store.clone(), cx, ) }); diff --git a/crates/zed/src/zed/inline_completion_registry.rs b/crates/zed/src/zed/inline_completion_registry.rs index 475e871d09..49e03d0436 100644 --- a/crates/zed/src/zed/inline_completion_registry.rs +++ b/crates/zed/src/zed/inline_completion_registry.rs @@ -1,20 +1,23 @@ use std::{cell::RefCell, rc::Rc, sync::Arc}; -use client::Client; +use client::{Client, UserStore}; use collections::HashMap; use copilot::{Copilot, CopilotCompletionProvider}; use editor::{Editor, EditorMode}; use feature_flags::{FeatureFlagAppExt, PredictEditsFeatureFlag}; -use gpui::{AnyWindowHandle, AppContext, Context, ViewContext, WeakView}; +use gpui::{AnyWindowHandle, AppContext, Context, Model, ViewContext, WeakView}; use language::language_settings::{all_language_settings, InlineCompletionProvider}; use settings::SettingsStore; use supermaven::{Supermaven, SupermavenCompletionProvider}; +use workspace::Workspace; +use zed_predict_tos::ZedPredictTos; -pub fn init(client: Arc, cx: &mut AppContext) { +pub fn init(client: Arc, user_store: Model, cx: &mut AppContext) { let editors: Rc, AnyWindowHandle>>> = Rc::default(); cx.observe_new_views({ let editors = editors.clone(); let client = client.clone(); + let user_store = user_store.clone(); move |editor: &mut Editor, cx: &mut ViewContext| { if editor.mode() != EditorMode::Full { return; @@ -35,7 +38,7 @@ pub fn init(client: Arc, cx: &mut AppContext) { .borrow_mut() .insert(editor_handle, cx.window_handle()); let provider = all_language_settings(None, cx).inline_completions.provider; - assign_inline_completion_provider(editor, provider, &client, cx); + assign_inline_completion_provider(editor, provider, &client, user_store.clone(), cx); } }) .detach(); @@ -44,7 +47,13 @@ pub fn init(client: Arc, cx: &mut AppContext) { for (editor, window) in editors.borrow().iter() { _ = window.update(cx, |_window, cx| { _ = editor.update(cx, |editor, cx| { - assign_inline_completion_provider(editor, provider, &client, cx); + assign_inline_completion_provider( + editor, + provider, + &client, + user_store.clone(), + cx, + ); }) }); } @@ -56,9 +65,10 @@ pub fn init(client: Arc, cx: &mut AppContext) { cx.observe_flag::({ let editors = editors.clone(); let client = client.clone(); + let user_store = user_store.clone(); move |active, cx| { let provider = all_language_settings(None, cx).inline_completions.provider; - assign_inline_completion_providers(&editors, provider, &client, cx); + assign_inline_completion_providers(&editors, provider, &client, user_store.clone(), cx); if active && !cx.is_action_available(&zeta::ClearHistory) { cx.on_action(clear_zeta_edit_history); } @@ -69,11 +79,48 @@ pub fn init(client: Arc, cx: &mut AppContext) { cx.observe_global::({ let editors = editors.clone(); let client = client.clone(); + let user_store = user_store.clone(); move |cx| { let new_provider = all_language_settings(None, cx).inline_completions.provider; if new_provider != provider { provider = new_provider; - assign_inline_completion_providers(&editors, provider, &client, cx) + assign_inline_completion_providers( + &editors, + provider, + &client, + user_store.clone(), + cx, + ); + + if !user_store + .read(cx) + .current_user_has_accepted_terms() + .unwrap_or(false) + { + match provider { + InlineCompletionProvider::Zed => { + let Some(window) = cx.active_window() else { + return; + }; + + let Some(workspace) = window + .downcast::() + .and_then(|w| w.root_view(cx).ok()) + else { + return; + }; + + window + .update(cx, |_, cx| { + ZedPredictTos::toggle(workspace, user_store.clone(), cx); + }) + .ok(); + } + InlineCompletionProvider::None + | InlineCompletionProvider::Copilot + | InlineCompletionProvider::Supermaven => {} + } + } } } }) @@ -90,12 +137,19 @@ fn assign_inline_completion_providers( editors: &Rc, AnyWindowHandle>>>, provider: InlineCompletionProvider, client: &Arc, + user_store: Model, cx: &mut AppContext, ) { for (editor, window) in editors.borrow().iter() { _ = window.update(cx, |_window, cx| { _ = editor.update(cx, |editor, cx| { - assign_inline_completion_provider(editor, provider, &client, cx); + assign_inline_completion_provider( + editor, + provider, + &client, + user_store.clone(), + cx, + ); }) }); } @@ -141,6 +195,7 @@ fn assign_inline_completion_provider( editor: &mut Editor, provider: language::language_settings::InlineCompletionProvider, client: &Arc, + user_store: Model, cx: &mut ViewContext, ) { match provider { @@ -169,7 +224,7 @@ fn assign_inline_completion_provider( if cx.has_flag::() || (cfg!(debug_assertions) && client.status().borrow().is_connected()) { - let zeta = zeta::Zeta::register(client.clone(), cx); + let zeta = zeta::Zeta::register(client.clone(), user_store, cx); if let Some(buffer) = editor.buffer().read(cx).as_singleton() { if buffer.read(cx).file().is_some() { zeta.update(cx, |zeta, cx| { diff --git a/crates/zed_predict_tos/Cargo.toml b/crates/zed_predict_tos/Cargo.toml new file mode 100644 index 0000000000..657cf4b1b0 --- /dev/null +++ b/crates/zed_predict_tos/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "zed_predict_tos" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/zed_predict_tos.rs" +doctest = false + +[features] +test-support = [] + +[dependencies] +client.workspace = true +gpui.workspace = true +ui.workspace = true +workspace.workspace = true +menu.workspace = true diff --git a/crates/zed_predict_tos/LICENSE-GPL b/crates/zed_predict_tos/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/zed_predict_tos/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/zed_predict_tos/src/zed_predict_tos.rs b/crates/zed_predict_tos/src/zed_predict_tos.rs new file mode 100644 index 0000000000..86166c6d7f --- /dev/null +++ b/crates/zed_predict_tos/src/zed_predict_tos.rs @@ -0,0 +1,152 @@ +//! AI service Terms of Service acceptance modal. + +use client::UserStore; +use gpui::{ + AppContext, ClickEvent, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, + MouseDownEvent, Render, View, +}; +use ui::{prelude::*, TintColor}; +use workspace::{ModalView, Workspace}; + +/// Terms of acceptance for AI inline prediction. +pub struct ZedPredictTos { + focus_handle: FocusHandle, + user_store: Model, + workspace: View, + viewed: bool, +} + +impl ZedPredictTos { + fn new( + workspace: View, + user_store: Model, + cx: &mut ViewContext, + ) -> Self { + ZedPredictTos { + viewed: false, + focus_handle: cx.focus_handle(), + user_store, + workspace, + } + } + pub fn toggle( + workspace: View, + user_store: Model, + cx: &mut WindowContext, + ) { + workspace.update(cx, |this, cx| { + let workspace = cx.view().clone(); + this.toggle_modal(cx, |cx| ZedPredictTos::new(workspace, user_store, cx)); + }); + } + + fn view_terms(&mut self, _: &ClickEvent, cx: &mut ViewContext) { + self.viewed = true; + cx.open_url("https://zed.dev/terms-of-service"); + cx.notify(); + } + + fn accept_terms(&mut self, _: &ClickEvent, cx: &mut ViewContext) { + let task = self + .user_store + .update(cx, |this, cx| this.accept_terms_of_service(cx)); + + let workspace = self.workspace.clone(); + + cx.spawn(|this, mut cx| async move { + match task.await { + Ok(_) => this.update(&mut cx, |_, cx| { + cx.emit(DismissEvent); + }), + Err(err) => workspace.update(&mut cx, |this, cx| { + this.show_error(&err, cx); + }), + } + }) + .detach_and_log_err(cx); + } + + fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + cx.emit(DismissEvent); + } +} + +impl EventEmitter for ZedPredictTos {} + +impl FocusableView for ZedPredictTos { + fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl ModalView for ZedPredictTos {} + +impl Render for ZedPredictTos { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + v_flex() + .id("zed predict tos") + .track_focus(&self.focus_handle(cx)) + .on_action(cx.listener(Self::cancel)) + .key_context("ZedPredictTos") + .elevation_3(cx) + .w_96() + .items_center() + .p_4() + .gap_2() + .on_action(cx.listener(|_, _: &menu::Cancel, cx| { + cx.emit(DismissEvent); + })) + .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, cx| { + cx.focus(&this.focus_handle); + })) + .child( + h_flex() + .w_full() + .justify_between() + .child( + v_flex() + .gap_0p5() + .child( + Label::new("Zed AI") + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child(Headline::new("Edit Prediction")), + ) + .child(Icon::new(IconName::ZedPredict).size(IconSize::XLarge)), + ) + .child( + Label::new( + "To use Zed AI's Edit Prediction feature, please read and accept our Terms of Service.", + ) + .color(Color::Muted), + ) + .child( + v_flex() + .mt_2() + .gap_0p5() + .w_full() + .child(if self.viewed { + Button::new("accept-tos", "I've Read and Accept the Terms of Service") + .style(ButtonStyle::Tinted(TintColor::Accent)) + .full_width() + .on_click(cx.listener(Self::accept_terms)) + } else { + Button::new("view-tos", "Read Terms of Service") + .style(ButtonStyle::Tinted(TintColor::Accent)) + .icon(IconName::ArrowUpRight) + .icon_size(IconSize::XSmall) + .icon_position(IconPosition::End) + .full_width() + .on_click(cx.listener(Self::view_terms)) + }) + .child( + Button::new("cancel", "Cancel") + .full_width() + .on_click(cx.listener(|_, _: &ClickEvent, cx| { + cx.emit(DismissEvent); + })), + ), + ) + } +} diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 2ee589775f..9a12ce4259 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -6,7 +6,7 @@ pub use rate_completion_modal::*; use anyhow::{anyhow, Context as _, Result}; use arrayvec::ArrayVec; -use client::Client; +use client::{Client, UserStore}; use collections::{HashMap, HashSet, VecDeque}; use futures::AsyncReadExt; use gpui::{ @@ -162,6 +162,8 @@ pub struct Zeta { rated_completions: HashSet, llm_token: LlmApiToken, _llm_token_subscription: Subscription, + tos_accepted: bool, // Terms of service accepted + _user_store_subscription: Subscription, } impl Zeta { @@ -169,9 +171,13 @@ impl Zeta { cx.try_global::().map(|global| global.0.clone()) } - pub fn register(client: Arc, cx: &mut AppContext) -> Model { + pub fn register( + client: Arc, + user_store: Model, + cx: &mut AppContext, + ) -> Model { Self::global(cx).unwrap_or_else(|| { - let model = cx.new_model(|cx| Self::new(client, cx)); + let model = cx.new_model(|cx| Self::new(client, user_store, cx)); cx.set_global(ZetaGlobal(model.clone())); model }) @@ -181,7 +187,7 @@ impl Zeta { self.events.clear(); } - fn new(client: Arc, cx: &mut ModelContext) -> Self { + fn new(client: Arc, user_store: Model, cx: &mut ModelContext) -> Self { let refresh_llm_token_listener = language_models::RefreshLlmTokenListener::global(cx); Self { @@ -203,6 +209,16 @@ impl Zeta { .detach_and_log_err(cx); }, ), + tos_accepted: user_store + .read(cx) + .current_user_has_accepted_terms() + .unwrap_or(false), + _user_store_subscription: cx.subscribe(&user_store, |this, _, event, _| match event { + client::user::Event::TermsStatusUpdated { accepted } => { + this.tos_accepted = *accepted; + } + _ => {} + }), } } @@ -1021,6 +1037,10 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx) } + fn needs_terms_acceptance(&self, cx: &AppContext) -> bool { + !self.zeta.read(cx).tos_accepted + } + fn is_refreshing(&self) -> bool { !self.pending_completions.is_empty() } @@ -1032,6 +1052,10 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide debounce: bool, cx: &mut ModelContext, ) { + if !self.zeta.read(cx).tos_accepted { + return; + } + let pending_completion_id = self.next_pending_completion_id; self.next_pending_completion_id += 1; @@ -1337,8 +1361,9 @@ mod tests { RefreshLlmTokenListener::register(client.clone(), cx); }); let server = FakeServer::for_client(42, &client, cx).await; + let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx)); + let zeta = cx.new_model(|cx| Zeta::new(client, user_store, cx)); - let zeta = cx.new_model(|cx| Zeta::new(client, cx)); let buffer = cx.new_model(|cx| Buffer::local(buffer_content, cx)); let cursor = buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(1, 0))); let completion_task =