Require accepting ToS when enabling zeta (#23255)

Note: Design hasn't been reviewed yet, but the logic is done

When the user switches the inline completion provider to `zed`, we'll
show a modal prompting them to accept terms if they haven't done so:


https://github.com/user-attachments/assets/3fc6d368-c00a-4dcb-9484-fbbbb5eb859e

If they dismiss the modal, they'll be able to get to it again from the
inline completion button:


https://github.com/user-attachments/assets/cf842778-5538-4e06-9ed8-21579981cc47

This also stops zeta sending requests that will fail immediately when
ToS are not accepted.

Release Notes:

- N/A

---------

Co-authored-by: Richard <richard@zed.dev>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Joao <joao@zed.dev>
This commit is contained in:
Agus Zubiaga 2025-01-20 11:48:49 -03:00 committed by GitHub
parent 5bb696e6d7
commit 919803a4f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 427 additions and 23 deletions

15
Cargo.lock generated
View file

@ -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"

View file

@ -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" }

View file

@ -875,5 +875,12 @@
"cmd-shift-enter": "zeta::ThumbsUpActiveCompletion",
"cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion"
}
},
{
"context": "ZedPredictTos",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel"
}
}
]

View file

@ -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"))

View file

@ -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

View file

@ -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(

View file

@ -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<Self>) {
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<usize>,
@ -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)
}

View file

@ -36,6 +36,9 @@ pub trait InlineCompletionProvider: 'static + Sized {
debounce: bool,
cx: &mut ModelContext<Self>,
);
fn needs_terms_acceptance(&self, _cx: &AppContext) -> bool {
false
}
fn cycle(
&mut self,
buffer: Model<Buffer>,
@ -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()
}

View file

@ -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"] }

View file

@ -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<Arc<dyn inline_completion::InlineCompletionProviderHandle>>,
fs: Arc<dyn Fs>,
workspace: WeakView<Workspace>,
user_store: Model<UserStore>,
}
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<Workspace>,
fs: Arc<dyn Fs>,
user_store: Model<UserStore>,
cx: &mut ViewContext<Self>,
) -> Self {
if let Some(copilot) = Copilot::global(cx) {
@ -261,6 +305,7 @@ impl InlineCompletionButton {
inline_completion_provider: None,
workspace,
fs,
user_store,
}
}

View file

@ -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)

View file

@ -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

View file

@ -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(),

View file

@ -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,
)
});

View file

@ -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<Client>, cx: &mut AppContext) {
pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
let editors: Rc<RefCell<HashMap<WeakView<Editor>, 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<Editor>| {
if editor.mode() != EditorMode::Full {
return;
@ -35,7 +38,7 @@ pub fn init(client: Arc<Client>, 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<Client>, 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<Client>, cx: &mut AppContext) {
cx.observe_flag::<PredictEditsFeatureFlag, _>({
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<Client>, cx: &mut AppContext) {
cx.observe_global::<SettingsStore>({
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::<Workspace>()
.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<RefCell<HashMap<WeakView<Editor>, AnyWindowHandle>>>,
provider: InlineCompletionProvider,
client: &Arc<Client>,
user_store: Model<UserStore>,
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<Client>,
user_store: Model<UserStore>,
cx: &mut ViewContext<Editor>,
) {
match provider {
@ -169,7 +224,7 @@ fn assign_inline_completion_provider(
if cx.has_flag::<PredictEditsFeatureFlag>()
|| (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| {

View file

@ -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

View file

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

View file

@ -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<UserStore>,
workspace: View<Workspace>,
viewed: bool,
}
impl ZedPredictTos {
fn new(
workspace: View<Workspace>,
user_store: Model<UserStore>,
cx: &mut ViewContext<Self>,
) -> Self {
ZedPredictTos {
viewed: false,
focus_handle: cx.focus_handle(),
user_store,
workspace,
}
}
pub fn toggle(
workspace: View<Workspace>,
user_store: Model<UserStore>,
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>) {
self.viewed = true;
cx.open_url("https://zed.dev/terms-of-service");
cx.notify();
}
fn accept_terms(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
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<Self>) {
cx.emit(DismissEvent);
}
}
impl EventEmitter<DismissEvent> 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<Self>) -> 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);
})),
),
)
}
}

View file

@ -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<InlineCompletionId>,
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::<ZetaGlobal>().map(|global| global.0.clone())
}
pub fn register(client: Arc<Client>, cx: &mut AppContext) -> Model<Self> {
pub fn register(
client: Arc<Client>,
user_store: Model<UserStore>,
cx: &mut AppContext,
) -> Model<Self> {
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<Client>, cx: &mut ModelContext<Self>) -> Self {
fn new(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut ModelContext<Self>) -> 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<Self>,
) {
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 =