mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-08 07:35:01 +03:00
assistant: Require user to accept TOS for cloud provider (#16111)
This adds the requirement for users to accept the terms of service the first time they send a message with the Cloud provider. Once this is out and in a nightly, we need to add the check to the server side too, to authenticate access to the models. Demo: https://github.com/user-attachments/assets/0edebf74-8120-4fa2-b801-bb76f04e8a17 Release Notes: - N/A
This commit is contained in:
parent
98f314ba21
commit
fbb533b3e0
@ -490,6 +490,7 @@ impl AssistantPanel {
|
|||||||
}
|
}
|
||||||
language_model::Event::ProviderStateChanged => {
|
language_model::Event::ProviderStateChanged => {
|
||||||
this.ensure_authenticated(cx);
|
this.ensure_authenticated(cx);
|
||||||
|
cx.notify()
|
||||||
}
|
}
|
||||||
language_model::Event::AddedProvider(_)
|
language_model::Event::AddedProvider(_)
|
||||||
| language_model::Event::RemovedProvider(_) => {
|
| language_model::Event::RemovedProvider(_) => {
|
||||||
@ -1712,6 +1713,7 @@ pub struct ContextEditor {
|
|||||||
assistant_panel: WeakView<AssistantPanel>,
|
assistant_panel: WeakView<AssistantPanel>,
|
||||||
error_message: Option<SharedString>,
|
error_message: Option<SharedString>,
|
||||||
debug_inspector: Option<ContextInspector>,
|
debug_inspector: Option<ContextInspector>,
|
||||||
|
show_accept_terms: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_TAB_TITLE: &str = "New Context";
|
const DEFAULT_TAB_TITLE: &str = "New Context";
|
||||||
@ -1772,6 +1774,7 @@ impl ContextEditor {
|
|||||||
assistant_panel,
|
assistant_panel,
|
||||||
error_message: None,
|
error_message: None,
|
||||||
debug_inspector: None,
|
debug_inspector: None,
|
||||||
|
show_accept_terms: false,
|
||||||
};
|
};
|
||||||
this.update_message_headers(cx);
|
this.update_message_headers(cx);
|
||||||
this.insert_slash_command_output_sections(sections, cx);
|
this.insert_slash_command_output_sections(sections, cx);
|
||||||
@ -1804,6 +1807,16 @@ impl ContextEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
|
fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
|
||||||
|
let provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||||
|
if provider
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |provider| provider.must_accept_terms(cx))
|
||||||
|
{
|
||||||
|
self.show_accept_terms = true;
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if !self.apply_active_workflow_step(cx) {
|
if !self.apply_active_workflow_step(cx) {
|
||||||
self.error_message = None;
|
self.error_message = None;
|
||||||
self.send_to_model(cx);
|
self.send_to_model(cx);
|
||||||
@ -3388,7 +3401,14 @@ impl ContextEditor {
|
|||||||
None => (ButtonStyle::Filled, None),
|
None => (ButtonStyle::Filled, None),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||||
|
let disabled = self.show_accept_terms
|
||||||
|
&& provider
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |provider| provider.must_accept_terms(cx));
|
||||||
|
|
||||||
ButtonLike::new("send_button")
|
ButtonLike::new("send_button")
|
||||||
|
.disabled(disabled)
|
||||||
.style(style)
|
.style(style)
|
||||||
.when_some(tooltip, |button, tooltip| {
|
.when_some(tooltip, |button, tooltip| {
|
||||||
button.tooltip(move |_| tooltip.clone())
|
button.tooltip(move |_| tooltip.clone())
|
||||||
@ -3437,6 +3457,15 @@ impl EventEmitter<SearchEvent> for ContextEditor {}
|
|||||||
|
|
||||||
impl Render for ContextEditor {
|
impl Render for ContextEditor {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
let provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||||
|
let accept_terms = if self.show_accept_terms {
|
||||||
|
provider
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|provider| provider.render_accept_terms(cx))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.key_context("ContextEditor")
|
.key_context("ContextEditor")
|
||||||
.capture_action(cx.listener(ContextEditor::cancel))
|
.capture_action(cx.listener(ContextEditor::cancel))
|
||||||
@ -3455,6 +3484,21 @@ impl Render for ContextEditor {
|
|||||||
.bg(cx.theme().colors().editor_background)
|
.bg(cx.theme().colors().editor_background)
|
||||||
.child(self.editor.clone()),
|
.child(self.editor.clone()),
|
||||||
)
|
)
|
||||||
|
.when_some(accept_terms, |this, element| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.right_4()
|
||||||
|
.bottom_10()
|
||||||
|
.max_w_96()
|
||||||
|
.py_2()
|
||||||
|
.px_3()
|
||||||
|
.elevation_2(cx)
|
||||||
|
.bg(cx.theme().colors().surface_background)
|
||||||
|
.occlude()
|
||||||
|
.child(element),
|
||||||
|
)
|
||||||
|
})
|
||||||
.child(
|
.child(
|
||||||
h_flex().flex_none().relative().child(
|
h_flex().flex_none().relative().child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore};
|
use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
|
use chrono::Duration;
|
||||||
use futures::{stream::BoxStream, StreamExt};
|
use futures::{stream::BoxStream, StreamExt};
|
||||||
use gpui::{BackgroundExecutor, Context, Model, TestAppContext};
|
use gpui::{BackgroundExecutor, Context, Model, TestAppContext};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
@ -162,6 +163,11 @@ impl FakeServer {
|
|||||||
return Ok(*message.downcast().unwrap());
|
return Ok(*message.downcast().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let accepted_tos_at = chrono::Utc::now()
|
||||||
|
.checked_sub_signed(Duration::hours(5))
|
||||||
|
.expect("failed to build accepted_tos_at")
|
||||||
|
.timestamp() as u64;
|
||||||
|
|
||||||
if message.is::<TypedEnvelope<GetPrivateUserInfo>>() {
|
if message.is::<TypedEnvelope<GetPrivateUserInfo>>() {
|
||||||
self.respond(
|
self.respond(
|
||||||
message
|
message
|
||||||
@ -172,6 +178,7 @@ impl FakeServer {
|
|||||||
metrics_id: "the-metrics-id".into(),
|
metrics_id: "the-metrics-id".into(),
|
||||||
staff: false,
|
staff: false,
|
||||||
flags: Default::default(),
|
flags: Default::default(),
|
||||||
|
accepted_tos_at: Some(accepted_tos_at),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use super::{proto, Client, Status, TypedEnvelope};
|
use super::{proto, Client, Status, TypedEnvelope};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use collections::{hash_map::Entry, HashMap, HashSet};
|
use collections::{hash_map::Entry, HashMap, HashSet};
|
||||||
use feature_flags::FeatureFlagAppExt;
|
use feature_flags::FeatureFlagAppExt;
|
||||||
use futures::{channel::mpsc, Future, StreamExt};
|
use futures::{channel::mpsc, Future, StreamExt};
|
||||||
@ -94,6 +95,7 @@ pub struct UserStore {
|
|||||||
update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
|
update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
|
||||||
current_plan: Option<proto::Plan>,
|
current_plan: Option<proto::Plan>,
|
||||||
current_user: watch::Receiver<Option<Arc<User>>>,
|
current_user: watch::Receiver<Option<Arc<User>>>,
|
||||||
|
accepted_tos_at: Option<Option<DateTime<Utc>>>,
|
||||||
contacts: Vec<Arc<Contact>>,
|
contacts: Vec<Arc<Contact>>,
|
||||||
incoming_contact_requests: Vec<Arc<User>>,
|
incoming_contact_requests: Vec<Arc<User>>,
|
||||||
outgoing_contact_requests: Vec<Arc<User>>,
|
outgoing_contact_requests: Vec<Arc<User>>,
|
||||||
@ -150,6 +152,7 @@ impl UserStore {
|
|||||||
by_github_login: Default::default(),
|
by_github_login: Default::default(),
|
||||||
current_user: current_user_rx,
|
current_user: current_user_rx,
|
||||||
current_plan: None,
|
current_plan: None,
|
||||||
|
accepted_tos_at: None,
|
||||||
contacts: Default::default(),
|
contacts: Default::default(),
|
||||||
incoming_contact_requests: Default::default(),
|
incoming_contact_requests: Default::default(),
|
||||||
participant_indices: Default::default(),
|
participant_indices: Default::default(),
|
||||||
@ -189,9 +192,10 @@ impl UserStore {
|
|||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
let fetch_metrics_id =
|
let fetch_private_user_info =
|
||||||
client.request(proto::GetPrivateUserInfo {}).log_err();
|
client.request(proto::GetPrivateUserInfo {}).log_err();
|
||||||
let (user, info) = futures::join!(fetch_user, fetch_metrics_id);
|
let (user, info) =
|
||||||
|
futures::join!(fetch_user, fetch_private_user_info);
|
||||||
|
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
if let Some(info) = info {
|
if let Some(info) = info {
|
||||||
@ -202,9 +206,17 @@ impl UserStore {
|
|||||||
client.telemetry.set_authenticated_user_info(
|
client.telemetry.set_authenticated_user_info(
|
||||||
Some(info.metrics_id.clone()),
|
Some(info.metrics_id.clone()),
|
||||||
staff,
|
staff,
|
||||||
)
|
);
|
||||||
|
|
||||||
|
this.update(cx, |this, _| {
|
||||||
|
this.set_current_user_accepted_tos_at(
|
||||||
|
info.accepted_tos_at,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
anyhow::Ok(())
|
||||||
}
|
}
|
||||||
})?;
|
})??;
|
||||||
|
|
||||||
current_user_tx.send(user).await.ok();
|
current_user_tx.send(user).await.ok();
|
||||||
|
|
||||||
@ -680,6 +692,39 @@ impl UserStore {
|
|||||||
self.current_user.clone()
|
self.current_user.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn current_user_has_accepted_terms(&self) -> Option<bool> {
|
||||||
|
self.accepted_tos_at
|
||||||
|
.map(|accepted_tos_at| accepted_tos_at.is_some())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn accept_terms_of_service(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||||
|
if self.current_user().is_none() {
|
||||||
|
return Task::ready(Err(anyhow!("no current user")));
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = self.client.clone();
|
||||||
|
cx.spawn(move |this, mut cx| async move {
|
||||||
|
if let Some(client) = client.upgrade() {
|
||||||
|
let response = client
|
||||||
|
.request(proto::AcceptTermsOfService {})
|
||||||
|
.await
|
||||||
|
.context("error accepting tos")?;
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, _| {
|
||||||
|
this.set_current_user_accepted_tos_at(Some(response.accepted_tos_at))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("client not found"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_current_user_accepted_tos_at(&mut self, accepted_tos_at: Option<u64>) {
|
||||||
|
self.accepted_tos_at = Some(
|
||||||
|
accepted_tos_at.and_then(|timestamp| DateTime::from_timestamp(timestamp as i64, 0)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn load_users(
|
fn load_users(
|
||||||
&mut self,
|
&mut self,
|
||||||
request: impl RequestMessage<Response = UsersResponse>,
|
request: impl RequestMessage<Response = UsersResponse>,
|
||||||
|
@ -9,7 +9,8 @@ CREATE TABLE "users" (
|
|||||||
"connected_once" BOOLEAN NOT NULL DEFAULT false,
|
"connected_once" BOOLEAN NOT NULL DEFAULT false,
|
||||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
"metrics_id" TEXT,
|
"metrics_id" TEXT,
|
||||||
"github_user_id" INTEGER
|
"github_user_id" INTEGER,
|
||||||
|
"accepted_tos_at" TIMESTAMP WITHOUT TIME ZONE
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX "index_users_github_login" ON "users" ("github_login");
|
CREATE UNIQUE INDEX "index_users_github_login" ON "users" ("github_login");
|
||||||
CREATE UNIQUE INDEX "index_invite_code_users" ON "users" ("invite_code");
|
CREATE UNIQUE INDEX "index_invite_code_users" ON "users" ("invite_code");
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users ADD accepted_tos_at TIMESTAMP WITHOUT TIME ZONE;
|
@ -225,6 +225,26 @@ impl Database {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets "accepted_tos_at" on the user to the given timestamp.
|
||||||
|
pub async fn set_user_accepted_tos_at(
|
||||||
|
&self,
|
||||||
|
id: UserId,
|
||||||
|
accepted_tos_at: Option<DateTime>,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.transaction(|tx| async move {
|
||||||
|
user::Entity::update_many()
|
||||||
|
.filter(user::Column::Id.eq(id))
|
||||||
|
.set(user::ActiveModel {
|
||||||
|
accepted_tos_at: ActiveValue::set(accepted_tos_at),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.exec(&*tx)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
/// hard delete the user.
|
/// hard delete the user.
|
||||||
pub async fn destroy_user(&self, id: UserId) -> Result<()> {
|
pub async fn destroy_user(&self, id: UserId) -> Result<()> {
|
||||||
self.transaction(|tx| async move {
|
self.transaction(|tx| async move {
|
||||||
|
@ -18,6 +18,7 @@ pub struct Model {
|
|||||||
pub connected_once: bool,
|
pub connected_once: bool,
|
||||||
pub metrics_id: Uuid,
|
pub metrics_id: Uuid,
|
||||||
pub created_at: DateTime,
|
pub created_at: DateTime,
|
||||||
|
pub accepted_tos_at: Option<DateTime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
@ -10,6 +10,7 @@ mod extension_tests;
|
|||||||
mod feature_flag_tests;
|
mod feature_flag_tests;
|
||||||
mod message_tests;
|
mod message_tests;
|
||||||
mod processed_stripe_event_tests;
|
mod processed_stripe_event_tests;
|
||||||
|
mod user_tests;
|
||||||
|
|
||||||
use crate::migrations::run_database_migrations;
|
use crate::migrations::run_database_migrations;
|
||||||
|
|
||||||
|
45
crates/collab/src/db/tests/user_tests.rs
Normal file
45
crates/collab/src/db/tests/user_tests.rs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
db::{Database, NewUserParams},
|
||||||
|
test_both_dbs,
|
||||||
|
};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
test_both_dbs!(
|
||||||
|
test_accepted_tos,
|
||||||
|
test_accepted_tos_postgres,
|
||||||
|
test_accepted_tos_sqlite
|
||||||
|
);
|
||||||
|
|
||||||
|
async fn test_accepted_tos(db: &Arc<Database>) {
|
||||||
|
let user_id = db
|
||||||
|
.create_user(
|
||||||
|
"user1@example.com",
|
||||||
|
false,
|
||||||
|
NewUserParams {
|
||||||
|
github_login: "user1".to_string(),
|
||||||
|
github_user_id: 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.user_id;
|
||||||
|
|
||||||
|
let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
|
||||||
|
assert!(user.accepted_tos_at.is_none());
|
||||||
|
|
||||||
|
let accepted_tos_at = Utc::now().naive_utc();
|
||||||
|
db.set_user_accepted_tos_at(user_id, Some(accepted_tos_at))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
|
||||||
|
assert!(user.accepted_tos_at.is_some());
|
||||||
|
assert_eq!(user.accepted_tos_at, Some(accepted_tos_at));
|
||||||
|
|
||||||
|
db.set_user_accepted_tos_at(user_id, None).await.unwrap();
|
||||||
|
|
||||||
|
let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
|
||||||
|
assert!(user.accepted_tos_at.is_none());
|
||||||
|
}
|
@ -31,6 +31,7 @@ use axum::{
|
|||||||
routing::get,
|
routing::get,
|
||||||
Extension, Router, TypedHeader,
|
Extension, Router, TypedHeader,
|
||||||
};
|
};
|
||||||
|
use chrono::Utc;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
pub use connection_pool::{ConnectionPool, ZedVersion};
|
pub use connection_pool::{ConnectionPool, ZedVersion};
|
||||||
use core::fmt::{self, Debug, Formatter};
|
use core::fmt::{self, Debug, Formatter};
|
||||||
@ -604,6 +605,7 @@ impl Server {
|
|||||||
.add_message_handler(user_message_handler(update_followers))
|
.add_message_handler(user_message_handler(update_followers))
|
||||||
.add_request_handler(user_handler(get_private_user_info))
|
.add_request_handler(user_handler(get_private_user_info))
|
||||||
.add_request_handler(user_handler(get_llm_api_token))
|
.add_request_handler(user_handler(get_llm_api_token))
|
||||||
|
.add_request_handler(user_handler(accept_terms_of_service))
|
||||||
.add_message_handler(user_message_handler(acknowledge_channel_message))
|
.add_message_handler(user_message_handler(acknowledge_channel_message))
|
||||||
.add_message_handler(user_message_handler(acknowledge_buffer_version))
|
.add_message_handler(user_message_handler(acknowledge_buffer_version))
|
||||||
.add_request_handler(user_handler(get_supermaven_api_key))
|
.add_request_handler(user_handler(get_supermaven_api_key))
|
||||||
@ -4882,6 +4884,25 @@ async fn get_private_user_info(
|
|||||||
metrics_id,
|
metrics_id,
|
||||||
staff: user.admin,
|
staff: user.admin,
|
||||||
flags,
|
flags,
|
||||||
|
accepted_tos_at: user.accepted_tos_at.map(|t| t.and_utc().timestamp() as u64),
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Accept the terms of service (tos) on behalf of the current user
|
||||||
|
async fn accept_terms_of_service(
|
||||||
|
_request: proto::AcceptTermsOfService,
|
||||||
|
response: Response<proto::AcceptTermsOfService>,
|
||||||
|
session: UserSession,
|
||||||
|
) -> Result<()> {
|
||||||
|
let db = session.db().await;
|
||||||
|
|
||||||
|
let accepted_tos_at = Utc::now();
|
||||||
|
db.set_user_accepted_tos_at(session.user_id(), Some(accepted_tos_at.naive_utc()))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
response.send(proto::AcceptTermsOfServiceResponse {
|
||||||
|
accepted_tos_at: accepted_tos_at.timestamp() as u64,
|
||||||
})?;
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,9 @@ pub mod settings;
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use client::{Client, UserStore};
|
use client::{Client, UserStore};
|
||||||
use futures::{future::BoxFuture, stream::BoxStream};
|
use futures::{future::BoxFuture, stream::BoxStream};
|
||||||
use gpui::{AnyView, AppContext, AsyncAppContext, Model, SharedString, Task, WindowContext};
|
use gpui::{
|
||||||
|
AnyElement, AnyView, AppContext, AsyncAppContext, Model, SharedString, Task, WindowContext,
|
||||||
|
};
|
||||||
pub use model::*;
|
pub use model::*;
|
||||||
use project::Fs;
|
use project::Fs;
|
||||||
use proto::Plan;
|
use proto::Plan;
|
||||||
@ -114,6 +116,12 @@ pub trait LanguageModelProvider: 'static {
|
|||||||
fn is_authenticated(&self, cx: &AppContext) -> bool;
|
fn is_authenticated(&self, cx: &AppContext) -> bool;
|
||||||
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>>;
|
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>>;
|
||||||
fn configuration_view(&self, cx: &mut WindowContext) -> AnyView;
|
fn configuration_view(&self, cx: &mut WindowContext) -> AnyView;
|
||||||
|
fn must_accept_terms(&self, _cx: &AppContext) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn render_accept_terms(&self, _cx: &mut WindowContext) -> Option<AnyElement> {
|
||||||
|
None
|
||||||
|
}
|
||||||
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>>;
|
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,10 @@ use client::{Client, PerformCompletionParams, UserStore, EXPIRED_LLM_TOKEN_HEADE
|
|||||||
use collections::BTreeMap;
|
use collections::BTreeMap;
|
||||||
use feature_flags::{FeatureFlagAppExt, LanguageModels};
|
use feature_flags::{FeatureFlagAppExt, LanguageModels};
|
||||||
use futures::{future::BoxFuture, stream::BoxStream, AsyncBufReadExt, FutureExt, StreamExt};
|
use futures::{future::BoxFuture, stream::BoxStream, AsyncBufReadExt, FutureExt, StreamExt};
|
||||||
use gpui::{AnyView, AppContext, AsyncAppContext, Model, ModelContext, Subscription, Task};
|
use gpui::{
|
||||||
|
AnyElement, AnyView, AppContext, AsyncAppContext, FontWeight, Model, ModelContext,
|
||||||
|
Subscription, Task,
|
||||||
|
};
|
||||||
use http_client::{AsyncBody, HttpClient, Method, Response};
|
use http_client::{AsyncBody, HttpClient, Method, Response};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -62,6 +65,7 @@ pub struct State {
|
|||||||
client: Arc<Client>,
|
client: Arc<Client>,
|
||||||
user_store: Model<UserStore>,
|
user_store: Model<UserStore>,
|
||||||
status: client::Status,
|
status: client::Status,
|
||||||
|
accept_terms: Option<Task<Result<()>>>,
|
||||||
_subscription: Subscription,
|
_subscription: Subscription,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,6 +81,26 @@ impl State {
|
|||||||
this.update(&mut cx, |_, cx| cx.notify())
|
this.update(&mut cx, |_, cx| cx.notify())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn has_accepted_terms_of_service(&self, cx: &AppContext) -> bool {
|
||||||
|
self.user_store
|
||||||
|
.read(cx)
|
||||||
|
.current_user_has_accepted_terms()
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accept_terms_of_service(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
|
let user_store = self.user_store.clone();
|
||||||
|
self.accept_terms = Some(cx.spawn(move |this, mut cx| async move {
|
||||||
|
let _ = user_store
|
||||||
|
.update(&mut cx, |store, cx| store.accept_terms_of_service(cx))?
|
||||||
|
.await;
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.accept_terms = None;
|
||||||
|
cx.notify()
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CloudLanguageModelProvider {
|
impl CloudLanguageModelProvider {
|
||||||
@ -88,6 +112,7 @@ impl CloudLanguageModelProvider {
|
|||||||
client: client.clone(),
|
client: client.clone(),
|
||||||
user_store,
|
user_store,
|
||||||
status,
|
status,
|
||||||
|
accept_terms: None,
|
||||||
_subscription: cx.observe_global::<SettingsStore>(|_, cx| {
|
_subscription: cx.observe_global::<SettingsStore>(|_, cx| {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}),
|
}),
|
||||||
@ -223,6 +248,57 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn must_accept_terms(&self, cx: &AppContext) -> bool {
|
||||||
|
!self.state.read(cx).has_accepted_terms_of_service(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_accept_terms(&self, cx: &mut WindowContext) -> Option<AnyElement> {
|
||||||
|
let state = self.state.read(cx);
|
||||||
|
|
||||||
|
let terms = [(
|
||||||
|
"anthropic_terms_of_service",
|
||||||
|
"Anthropic Terms of Service",
|
||||||
|
"https://www.anthropic.com/legal/consumer-terms",
|
||||||
|
)]
|
||||||
|
.map(|(id, label, url)| {
|
||||||
|
Button::new(id, label)
|
||||||
|
.style(ButtonStyle::Subtle)
|
||||||
|
.icon(IconName::ExternalLink)
|
||||||
|
.icon_size(IconSize::XSmall)
|
||||||
|
.icon_color(Color::Muted)
|
||||||
|
.on_click(move |_, cx| cx.open_url(url))
|
||||||
|
});
|
||||||
|
|
||||||
|
if state.has_accepted_terms_of_service(cx) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let disabled = state.accept_terms.is_some();
|
||||||
|
Some(
|
||||||
|
v_flex()
|
||||||
|
.child(Label::new("Terms & Conditions").weight(FontWeight::SEMIBOLD))
|
||||||
|
.child("Please read and accept the terms and conditions of Zed AI and our provider partners to continue.")
|
||||||
|
.child(v_flex().m_2().gap_1().children(terms))
|
||||||
|
.child(
|
||||||
|
h_flex().justify_end().mt_1().child(
|
||||||
|
Button::new("accept_terms", "Accept")
|
||||||
|
.disabled(disabled)
|
||||||
|
.on_click({
|
||||||
|
let state = self.state.downgrade();
|
||||||
|
move |_, cx| {
|
||||||
|
state
|
||||||
|
.update(cx, |state, cx| {
|
||||||
|
state.accept_terms_of_service(cx)
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.into_any(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn reset_credentials(&self, _cx: &mut AppContext) -> Task<Result<()>> {
|
fn reset_credentials(&self, _cx: &mut AppContext) -> Task<Result<()>> {
|
||||||
Task::ready(Ok(()))
|
Task::ready(Ok(()))
|
||||||
}
|
}
|
||||||
@ -766,6 +842,7 @@ impl Render for ConfigurationView {
|
|||||||
|
|
||||||
let is_connected = !self.state.read(cx).is_signed_out();
|
let is_connected = !self.state.read(cx).is_signed_out();
|
||||||
let plan = self.state.read(cx).user_store.read(cx).current_plan();
|
let plan = self.state.read(cx).user_store.read(cx).current_plan();
|
||||||
|
let must_accept_terms = !self.state.read(cx).has_accepted_terms_of_service(cx);
|
||||||
|
|
||||||
let is_pro = plan == Some(proto::Plan::ZedPro);
|
let is_pro = plan == Some(proto::Plan::ZedPro);
|
||||||
|
|
||||||
@ -773,6 +850,11 @@ impl Render for ConfigurationView {
|
|||||||
v_flex()
|
v_flex()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
.max_w_4_5()
|
.max_w_4_5()
|
||||||
|
.when(must_accept_terms, |this| {
|
||||||
|
this.child(Label::new(
|
||||||
|
"You must accept the terms of service to use this provider.",
|
||||||
|
))
|
||||||
|
})
|
||||||
.child(Label::new(
|
.child(Label::new(
|
||||||
if is_pro {
|
if is_pro {
|
||||||
"You have full access to Zed's hosted models from Anthropic, OpenAI, Google with faster speeds and higher limits through Zed Pro."
|
"You have full access to Zed's hosted models from Anthropic, OpenAI, Google with faster speeds and higher limits through Zed Pro."
|
||||||
|
@ -49,7 +49,7 @@ message Envelope {
|
|||||||
GetDefinition get_definition = 32;
|
GetDefinition get_definition = 32;
|
||||||
GetDefinitionResponse get_definition_response = 33;
|
GetDefinitionResponse get_definition_response = 33;
|
||||||
GetDeclaration get_declaration = 237;
|
GetDeclaration get_declaration = 237;
|
||||||
GetDeclarationResponse get_declaration_response = 238; // current max
|
GetDeclarationResponse get_declaration_response = 238;
|
||||||
GetTypeDefinition get_type_definition = 34;
|
GetTypeDefinition get_type_definition = 34;
|
||||||
GetTypeDefinitionResponse get_type_definition_response = 35;
|
GetTypeDefinitionResponse get_type_definition_response = 35;
|
||||||
|
|
||||||
@ -130,6 +130,8 @@ message Envelope {
|
|||||||
GetPrivateUserInfoResponse get_private_user_info_response = 103;
|
GetPrivateUserInfoResponse get_private_user_info_response = 103;
|
||||||
UpdateUserPlan update_user_plan = 234;
|
UpdateUserPlan update_user_plan = 234;
|
||||||
UpdateDiffBase update_diff_base = 104;
|
UpdateDiffBase update_diff_base = 104;
|
||||||
|
AcceptTermsOfService accept_terms_of_service = 239;
|
||||||
|
AcceptTermsOfServiceResponse accept_terms_of_service_response = 240; // current max
|
||||||
|
|
||||||
OnTypeFormatting on_type_formatting = 105;
|
OnTypeFormatting on_type_formatting = 105;
|
||||||
OnTypeFormattingResponse on_type_formatting_response = 106;
|
OnTypeFormattingResponse on_type_formatting_response = 106;
|
||||||
@ -270,7 +272,7 @@ message Envelope {
|
|||||||
AddWorktreeResponse add_worktree_response = 223;
|
AddWorktreeResponse add_worktree_response = 223;
|
||||||
|
|
||||||
GetLlmToken get_llm_token = 235;
|
GetLlmToken get_llm_token = 235;
|
||||||
GetLlmTokenResponse get_llm_token_response = 236; // current max
|
GetLlmTokenResponse get_llm_token_response = 236;
|
||||||
}
|
}
|
||||||
|
|
||||||
reserved 158 to 161;
|
reserved 158 to 161;
|
||||||
@ -1692,6 +1694,7 @@ message GetPrivateUserInfoResponse {
|
|||||||
string metrics_id = 1;
|
string metrics_id = 1;
|
||||||
bool staff = 2;
|
bool staff = 2;
|
||||||
repeated string flags = 3;
|
repeated string flags = 3;
|
||||||
|
optional uint64 accepted_tos_at = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Plan {
|
enum Plan {
|
||||||
@ -1703,6 +1706,12 @@ message UpdateUserPlan {
|
|||||||
Plan plan = 1;
|
Plan plan = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message AcceptTermsOfService {}
|
||||||
|
|
||||||
|
message AcceptTermsOfServiceResponse {
|
||||||
|
uint64 accepted_tos_at = 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Entities
|
// Entities
|
||||||
|
|
||||||
message ViewId {
|
message ViewId {
|
||||||
|
@ -187,6 +187,8 @@ impl fmt::Display for PeerId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
messages!(
|
messages!(
|
||||||
|
(AcceptTermsOfService, Foreground),
|
||||||
|
(AcceptTermsOfServiceResponse, Foreground),
|
||||||
(Ack, Foreground),
|
(Ack, Foreground),
|
||||||
(AckBufferOperation, Background),
|
(AckBufferOperation, Background),
|
||||||
(AckChannelMessage, Background),
|
(AckChannelMessage, Background),
|
||||||
@ -409,6 +411,7 @@ messages!(
|
|||||||
);
|
);
|
||||||
|
|
||||||
request_messages!(
|
request_messages!(
|
||||||
|
(AcceptTermsOfService, AcceptTermsOfServiceResponse),
|
||||||
(ApplyCodeAction, ApplyCodeActionResponse),
|
(ApplyCodeAction, ApplyCodeActionResponse),
|
||||||
(
|
(
|
||||||
ApplyCompletionAdditionalEdits,
|
ApplyCompletionAdditionalEdits,
|
||||||
|
Loading…
Reference in New Issue
Block a user