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:
parent
5bb696e6d7
commit
919803a4f4
19 changed files with 427 additions and 23 deletions
15
Cargo.lock
generated
15
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -875,5 +875,12 @@
|
|||
"cmd-shift-enter": "zeta::ThumbsUpActiveCompletion",
|
||||
"cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ZedPredictTos",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
});
|
||||
|
|
|
@ -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| {
|
||||
|
|
23
crates/zed_predict_tos/Cargo.toml
Normal file
23
crates/zed_predict_tos/Cargo.toml
Normal 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
|
1
crates/zed_predict_tos/LICENSE-GPL
Symbolic link
1
crates/zed_predict_tos/LICENSE-GPL
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../LICENSE-GPL
|
152
crates/zed_predict_tos/src/zed_predict_tos.rs
Normal file
152
crates/zed_predict_tos/src/zed_predict_tos.rs
Normal 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);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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 =
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue