Enable configuring edit prediction providers through the settings UI (#44505)
- Edit prediction providers can now be configured through the settings UI - Cleaned up the status bar menu to only show _configured_ providers - Added to the status bar icon button tooltip the name of the active provider - Only display the data collection functionality under "Privacy" for the Zed models - Moved the Codestral edit prediction provider out of the Mistral section in the agent panel into the settings UI - Refined and improved UI and states for configuring GitHub Copilot as both an agent and edit prediction provider #### Todos before merge: - [x] UI: Unify with settings UI style and tidy it all up - [x] Unify Copilot modal `impl`s to use separate window - [x] Remove stop light icons from GitHub modal - [x] Make dismiss events work on GitHub modal - [ ] Investigate workarounds to tell if Copilot authenticated even when LSP not running Release Notes: - settings_ui: Added a section for configuring edit prediction providers under AI > Edit Predictions, including Codestral and GitHub Copilot. Once you've updated you can use the following link to open it: zed://settings/edit_predictions.providers --------- Co-authored-by: Ben Kunkle <ben@zed.dev>
This commit is contained in:
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -5111,7 +5111,6 @@ dependencies = [
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"copilot",
|
||||
"credentials_provider",
|
||||
"ctor",
|
||||
"db",
|
||||
"edit_prediction_context",
|
||||
@@ -5275,7 +5274,6 @@ dependencies = [
|
||||
"text",
|
||||
"theme",
|
||||
"ui",
|
||||
"ui_input",
|
||||
"util",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
@@ -8802,6 +8800,7 @@ dependencies = [
|
||||
"cloud_api_types",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"credentials_provider",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"http_client",
|
||||
@@ -8820,6 +8819,7 @@ dependencies = [
|
||||
"telemetry_events",
|
||||
"thiserror 2.0.17",
|
||||
"util",
|
||||
"zed_env_vars",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8876,7 +8876,6 @@ dependencies = [
|
||||
"util",
|
||||
"vercel",
|
||||
"x_ai",
|
||||
"zed_env_vars",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -14778,6 +14777,8 @@ dependencies = [
|
||||
"assets",
|
||||
"bm25",
|
||||
"client",
|
||||
"copilot",
|
||||
"edit_prediction",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
@@ -14786,6 +14787,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"heck 0.5.0",
|
||||
"language",
|
||||
"language_models",
|
||||
"log",
|
||||
"menu",
|
||||
"node_runtime",
|
||||
|
||||
@@ -1410,8 +1410,9 @@
|
||||
"proxy_no_verify": null,
|
||||
},
|
||||
"codestral": {
|
||||
"model": null,
|
||||
"max_tokens": null,
|
||||
"api_url": "https://codestral.mistral.ai",
|
||||
"model": "codestral-latest",
|
||||
"max_tokens": 150,
|
||||
},
|
||||
// Whether edit predictions are enabled when editing text threads in the agent panel.
|
||||
// This setting has no effect if globally disabled.
|
||||
|
||||
@@ -34,9 +34,9 @@ use project::{
|
||||
};
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use ui::{
|
||||
Button, ButtonStyle, Chip, CommonAnimationExt, ContextMenu, ContextMenuEntry, Disclosure,
|
||||
Divider, DividerColor, ElevationIndex, IconName, IconPosition, IconSize, Indicator, LabelSize,
|
||||
PopoverMenu, Switch, Tooltip, WithScrollbar, prelude::*,
|
||||
ButtonStyle, Chip, CommonAnimationExt, ContextMenu, ContextMenuEntry, Disclosure, Divider,
|
||||
DividerColor, ElevationIndex, Indicator, LabelSize, PopoverMenu, Switch, Tooltip,
|
||||
WithScrollbar, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{Workspace, create_and_open_local_file};
|
||||
|
||||
@@ -4,7 +4,7 @@ pub mod copilot_responses;
|
||||
pub mod request;
|
||||
mod sign_in;
|
||||
|
||||
use crate::sign_in::initiate_sign_in_within_workspace;
|
||||
use crate::sign_in::initiate_sign_out;
|
||||
use ::fs::Fs;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use collections::{HashMap, HashSet};
|
||||
@@ -28,12 +28,10 @@ use project::DisableAiSettings;
|
||||
use request::StatusNotification;
|
||||
use semver::Version;
|
||||
use serde_json::json;
|
||||
use settings::Settings;
|
||||
use settings::SettingsStore;
|
||||
use sign_in::{reinstall_and_sign_in_within_workspace, sign_out_within_workspace};
|
||||
use std::collections::hash_map::Entry;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
collections::hash_map::Entry,
|
||||
env,
|
||||
ffi::OsString,
|
||||
mem,
|
||||
@@ -42,12 +40,14 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
use sum_tree::Dimensions;
|
||||
use util::rel_path::RelPath;
|
||||
use util::{ResultExt, fs::remove_matching};
|
||||
use util::{ResultExt, fs::remove_matching, rel_path::RelPath};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub use crate::copilot_edit_prediction_delegate::CopilotEditPredictionDelegate;
|
||||
pub use crate::sign_in::{CopilotCodeVerification, initiate_sign_in, reinstall_and_sign_in};
|
||||
pub use crate::sign_in::{
|
||||
ConfigurationMode, ConfigurationView, CopilotCodeVerification, initiate_sign_in,
|
||||
reinstall_and_sign_in,
|
||||
};
|
||||
|
||||
actions!(
|
||||
copilot,
|
||||
@@ -98,21 +98,14 @@ pub fn init(
|
||||
.detach();
|
||||
|
||||
cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
|
||||
workspace.register_action(|workspace, _: &SignIn, window, cx| {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
let is_reinstall = false;
|
||||
initiate_sign_in_within_workspace(workspace, copilot, is_reinstall, window, cx);
|
||||
}
|
||||
workspace.register_action(|_, _: &SignIn, window, cx| {
|
||||
initiate_sign_in(window, cx);
|
||||
});
|
||||
workspace.register_action(|workspace, _: &Reinstall, window, cx| {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
reinstall_and_sign_in_within_workspace(workspace, copilot, window, cx);
|
||||
}
|
||||
workspace.register_action(|_, _: &Reinstall, window, cx| {
|
||||
reinstall_and_sign_in(window, cx);
|
||||
});
|
||||
workspace.register_action(|workspace, _: &SignOut, _window, cx| {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
sign_out_within_workspace(workspace, copilot, cx);
|
||||
}
|
||||
workspace.register_action(|_, _: &SignOut, window, cx| {
|
||||
initiate_sign_out(window, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
@@ -375,7 +368,7 @@ impl Copilot {
|
||||
}
|
||||
}
|
||||
|
||||
fn start_copilot(
|
||||
pub fn start_copilot(
|
||||
&mut self,
|
||||
check_edit_prediction_provider: bool,
|
||||
awaiting_sign_in_after_start: bool,
|
||||
@@ -563,6 +556,14 @@ impl Copilot {
|
||||
let server = start_language_server.await;
|
||||
this.update(cx, |this, cx| {
|
||||
cx.notify();
|
||||
|
||||
if env::var("ZED_FORCE_COPILOT_ERROR").is_ok() {
|
||||
this.server = CopilotServer::Error(
|
||||
"Forced error for testing (ZED_FORCE_COPILOT_ERROR)".into(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
match server {
|
||||
Ok((server, status)) => {
|
||||
this.server = CopilotServer::Running(RunningCopilotServer {
|
||||
@@ -584,7 +585,17 @@ impl Copilot {
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub(crate) fn sign_in(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
pub fn is_authenticated(&self) -> bool {
|
||||
return matches!(
|
||||
self.server,
|
||||
CopilotServer::Running(RunningCopilotServer {
|
||||
sign_in_status: SignInStatus::Authorized,
|
||||
..
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
pub fn sign_in(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
if let CopilotServer::Running(server) = &mut self.server {
|
||||
let task = match &server.sign_in_status {
|
||||
SignInStatus::Authorized => Task::ready(Ok(())).shared(),
|
||||
|
||||
@@ -1,160 +1,151 @@
|
||||
use crate::{Copilot, Status, request::PromptUserDeviceFlow};
|
||||
use anyhow::Context as _;
|
||||
use gpui::{
|
||||
Animation, AnimationExt, App, ClipboardItem, Context, DismissEvent, Element, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, MouseDownEvent,
|
||||
ParentElement, Render, Styled, Subscription, Transformation, Window, div, percentage, svg,
|
||||
App, ClipboardItem, Context, DismissEvent, Element, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, InteractiveElement, IntoElement, MouseDownEvent, ParentElement, Render, Styled,
|
||||
Subscription, Window, WindowBounds, WindowOptions, div, point,
|
||||
};
|
||||
use std::time::Duration;
|
||||
use ui::{Button, Label, Vector, VectorName, prelude::*};
|
||||
use ui::{ButtonLike, CommonAnimationExt, ConfiguredApiCard, Vector, VectorName, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::{ModalView, Toast, Workspace};
|
||||
use workspace::{Toast, Workspace, notifications::NotificationId};
|
||||
|
||||
const COPILOT_SIGN_UP_URL: &str = "https://github.com/features/copilot";
|
||||
const ERROR_LABEL: &str =
|
||||
"Copilot had issues starting. You can try reinstalling it and signing in again.";
|
||||
|
||||
struct CopilotStatusToast;
|
||||
|
||||
pub fn initiate_sign_in(window: &mut Window, cx: &mut App) {
|
||||
let is_reinstall = false;
|
||||
initiate_sign_in_impl(is_reinstall, window, cx)
|
||||
}
|
||||
|
||||
pub fn initiate_sign_out(window: &mut Window, cx: &mut App) {
|
||||
let Some(copilot) = Copilot::global(cx) else {
|
||||
return;
|
||||
};
|
||||
let Some(workspace) = window.root::<Workspace>().flatten() else {
|
||||
return;
|
||||
};
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let is_reinstall = false;
|
||||
initiate_sign_in_within_workspace(workspace, copilot, is_reinstall, window, cx)
|
||||
});
|
||||
|
||||
copilot_toast(Some("Signing out of Copilot…"), window, cx);
|
||||
|
||||
let sign_out_task = copilot.update(cx, |copilot, cx| copilot.sign_out(cx));
|
||||
window
|
||||
.spawn(cx, async move |cx| match sign_out_task.await {
|
||||
Ok(()) => {
|
||||
cx.update(|window, cx| copilot_toast(Some("Signed out of Copilot"), window, cx))
|
||||
}
|
||||
Err(err) => cx.update(|window, cx| {
|
||||
if let Some(workspace) = window.root::<Workspace>().flatten() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.show_error(&err, cx);
|
||||
})
|
||||
} else {
|
||||
log::error!("{:?}", err);
|
||||
}
|
||||
}),
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn reinstall_and_sign_in(window: &mut Window, cx: &mut App) {
|
||||
let Some(copilot) = Copilot::global(cx) else {
|
||||
return;
|
||||
};
|
||||
let _ = copilot.update(cx, |copilot, cx| copilot.reinstall(cx));
|
||||
let is_reinstall = true;
|
||||
initiate_sign_in_impl(is_reinstall, window, cx);
|
||||
}
|
||||
|
||||
fn open_copilot_code_verification_window(copilot: &Entity<Copilot>, window: &Window, cx: &mut App) {
|
||||
let current_window_center = window.bounds().center();
|
||||
let height = px(450.);
|
||||
let width = px(350.);
|
||||
let window_bounds = WindowBounds::Windowed(gpui::bounds(
|
||||
current_window_center - point(height / 2.0, width / 2.0),
|
||||
gpui::size(height, width),
|
||||
));
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
kind: gpui::WindowKind::PopUp,
|
||||
window_bounds: Some(window_bounds),
|
||||
is_resizable: false,
|
||||
is_movable: true,
|
||||
titlebar: Some(gpui::TitlebarOptions {
|
||||
appears_transparent: true,
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| cx.new(|cx| CopilotCodeVerification::new(&copilot, window, cx)),
|
||||
)
|
||||
.context("Failed to open Copilot code verification window")
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn copilot_toast(message: Option<&'static str>, window: &Window, cx: &mut App) {
|
||||
const NOTIFICATION_ID: NotificationId = NotificationId::unique::<CopilotStatusToast>();
|
||||
|
||||
let Some(workspace) = window.root::<Workspace>().flatten() else {
|
||||
return;
|
||||
};
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
reinstall_and_sign_in_within_workspace(workspace, copilot, window, cx);
|
||||
|
||||
workspace.update(cx, |workspace, cx| match message {
|
||||
Some(message) => workspace.show_toast(Toast::new(NOTIFICATION_ID, message), cx),
|
||||
None => workspace.dismiss_toast(&NOTIFICATION_ID, cx),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn reinstall_and_sign_in_within_workspace(
|
||||
workspace: &mut Workspace,
|
||||
copilot: Entity<Copilot>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
let _ = copilot.update(cx, |copilot, cx| copilot.reinstall(cx));
|
||||
let is_reinstall = true;
|
||||
initiate_sign_in_within_workspace(workspace, copilot, is_reinstall, window, cx);
|
||||
}
|
||||
|
||||
pub fn initiate_sign_in_within_workspace(
|
||||
workspace: &mut Workspace,
|
||||
copilot: Entity<Copilot>,
|
||||
is_reinstall: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
pub fn initiate_sign_in_impl(is_reinstall: bool, window: &mut Window, cx: &mut App) {
|
||||
let Some(copilot) = Copilot::global(cx) else {
|
||||
return;
|
||||
};
|
||||
if matches!(copilot.read(cx).status(), Status::Disabled) {
|
||||
copilot.update(cx, |copilot, cx| copilot.start_copilot(false, true, cx));
|
||||
}
|
||||
match copilot.read(cx).status() {
|
||||
Status::Starting { task } => {
|
||||
workspace.show_toast(
|
||||
Toast::new(
|
||||
NotificationId::unique::<CopilotStatusToast>(),
|
||||
if is_reinstall {
|
||||
"Copilot is reinstalling..."
|
||||
} else {
|
||||
"Copilot is starting..."
|
||||
},
|
||||
),
|
||||
copilot_toast(
|
||||
Some(if is_reinstall {
|
||||
"Copilot is reinstalling…"
|
||||
} else {
|
||||
"Copilot is starting…"
|
||||
}),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.spawn_in(window, async move |workspace, cx| {
|
||||
task.await;
|
||||
if let Some(copilot) = cx.update(|_window, cx| Copilot::global(cx)).ok().flatten() {
|
||||
workspace
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
match copilot.read(cx).status() {
|
||||
Status::Authorized => workspace.show_toast(
|
||||
Toast::new(
|
||||
NotificationId::unique::<CopilotStatusToast>(),
|
||||
"Copilot has started.",
|
||||
),
|
||||
cx,
|
||||
),
|
||||
_ => {
|
||||
workspace.dismiss_toast(
|
||||
&NotificationId::unique::<CopilotStatusToast>(),
|
||||
cx,
|
||||
);
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.detach_and_log_err(cx);
|
||||
workspace.toggle_modal(window, cx, |_, cx| {
|
||||
CopilotCodeVerification::new(&copilot, cx)
|
||||
});
|
||||
}
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
task.await;
|
||||
cx.update(|window, cx| {
|
||||
let Some(copilot) = Copilot::global(cx) else {
|
||||
return;
|
||||
};
|
||||
match copilot.read(cx).status() {
|
||||
Status::Authorized => {
|
||||
copilot_toast(Some("Copilot has started."), window, cx)
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
_ => {
|
||||
copilot_toast(None, window, cx);
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.detach_and_log_err(cx);
|
||||
open_copilot_code_verification_window(&copilot, window, cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
_ => {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.detach();
|
||||
workspace.toggle_modal(window, cx, |_, cx| {
|
||||
CopilotCodeVerification::new(&copilot, cx)
|
||||
});
|
||||
open_copilot_code_verification_window(&copilot, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sign_out_within_workspace(
|
||||
workspace: &mut Workspace,
|
||||
copilot: Entity<Copilot>,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
workspace.show_toast(
|
||||
Toast::new(
|
||||
NotificationId::unique::<CopilotStatusToast>(),
|
||||
"Signing out of Copilot...",
|
||||
),
|
||||
cx,
|
||||
);
|
||||
let sign_out_task = copilot.update(cx, |copilot, cx| copilot.sign_out(cx));
|
||||
cx.spawn(async move |workspace, cx| match sign_out_task.await {
|
||||
Ok(()) => {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.show_toast(
|
||||
Toast::new(
|
||||
NotificationId::unique::<CopilotStatusToast>(),
|
||||
"Signed out of Copilot.",
|
||||
),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(err) => {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.show_error(&err, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub struct CopilotCodeVerification {
|
||||
status: Status,
|
||||
connect_clicked: bool,
|
||||
@@ -170,23 +161,27 @@ impl Focusable for CopilotCodeVerification {
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for CopilotCodeVerification {}
|
||||
impl ModalView for CopilotCodeVerification {
|
||||
fn on_before_dismiss(
|
||||
&mut self,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> workspace::DismissDecision {
|
||||
self.copilot.update(cx, |copilot, cx| {
|
||||
if matches!(copilot.status(), Status::SigningIn { .. }) {
|
||||
copilot.sign_out(cx).detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
workspace::DismissDecision::Dismiss(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl CopilotCodeVerification {
|
||||
pub fn new(copilot: &Entity<Copilot>, cx: &mut Context<Self>) -> Self {
|
||||
pub fn new(copilot: &Entity<Copilot>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
window.on_window_should_close(cx, |window, cx| {
|
||||
if let Some(this) = window.root::<CopilotCodeVerification>().flatten() {
|
||||
this.update(cx, |this, cx| {
|
||||
this.before_dismiss(cx);
|
||||
});
|
||||
}
|
||||
true
|
||||
});
|
||||
cx.subscribe_in(
|
||||
&cx.entity(),
|
||||
window,
|
||||
|this, _, _: &DismissEvent, window, cx| {
|
||||
window.remove_window();
|
||||
this.before_dismiss(cx);
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
let status = copilot.read(cx).status();
|
||||
Self {
|
||||
status,
|
||||
@@ -215,45 +210,45 @@ impl CopilotCodeVerification {
|
||||
.read_from_clipboard()
|
||||
.map(|item| item.text().as_ref() == Some(&data.user_code))
|
||||
.unwrap_or(false);
|
||||
h_flex()
|
||||
.w_full()
|
||||
.p_1()
|
||||
.border_1()
|
||||
.border_muted(cx)
|
||||
.rounded_sm()
|
||||
.cursor_pointer()
|
||||
.justify_between()
|
||||
.on_mouse_down(gpui::MouseButton::Left, {
|
||||
|
||||
ButtonLike::new("copy-button")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.size(ButtonSize::Medium)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.p_1()
|
||||
.justify_between()
|
||||
.child(Label::new(data.user_code.clone()))
|
||||
.child(Label::new(if copied { "Copied!" } else { "Copy" })),
|
||||
)
|
||||
.on_click({
|
||||
let user_code = data.user_code.clone();
|
||||
move |_, window, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(user_code.clone()));
|
||||
window.refresh();
|
||||
}
|
||||
})
|
||||
.child(div().flex_1().child(Label::new(data.user_code.clone())))
|
||||
.child(div().flex_none().px_1().child(Label::new(if copied {
|
||||
"Copied!"
|
||||
} else {
|
||||
"Copy"
|
||||
})))
|
||||
}
|
||||
|
||||
fn render_prompting_modal(
|
||||
connect_clicked: bool,
|
||||
data: &PromptUserDeviceFlow,
|
||||
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl Element {
|
||||
let connect_button_label = if connect_clicked {
|
||||
"Waiting for connection..."
|
||||
"Waiting for connection…"
|
||||
} else {
|
||||
"Connect to GitHub"
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.gap_2()
|
||||
.gap_2p5()
|
||||
.items_center()
|
||||
.child(Headline::new("Use GitHub Copilot in Zed.").size(HeadlineSize::Large))
|
||||
.text_center()
|
||||
.child(Headline::new("Use GitHub Copilot in Zed").size(HeadlineSize::Large))
|
||||
.child(
|
||||
Label::new("Using Copilot requires an active subscription on GitHub.")
|
||||
.color(Color::Muted),
|
||||
@@ -261,83 +256,119 @@ impl CopilotCodeVerification {
|
||||
.child(Self::render_device_code(data, cx))
|
||||
.child(
|
||||
Label::new("Paste this code into GitHub after clicking the button below.")
|
||||
.size(ui::LabelSize::Small),
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Button::new("connect-button", connect_button_label)
|
||||
.on_click({
|
||||
let verification_uri = data.verification_uri.clone();
|
||||
cx.listener(move |this, _, _window, cx| {
|
||||
cx.open_url(&verification_uri);
|
||||
this.connect_clicked = true;
|
||||
})
|
||||
})
|
||||
.full_width()
|
||||
.style(ButtonStyle::Filled),
|
||||
)
|
||||
.child(
|
||||
Button::new("copilot-enable-cancel-button", "Cancel")
|
||||
.full_width()
|
||||
.on_click(cx.listener(|_, _, _, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})),
|
||||
v_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("connect-button", connect_button_label)
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.size(ButtonSize::Medium)
|
||||
.on_click({
|
||||
let verification_uri = data.verification_uri.clone();
|
||||
cx.listener(move |this, _, _window, cx| {
|
||||
cx.open_url(&verification_uri);
|
||||
this.connect_clicked = true;
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("copilot-enable-cancel-button", "Cancel")
|
||||
.full_width()
|
||||
.size(ButtonSize::Medium)
|
||||
.on_click(cx.listener(|_, _, _, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_enabled_modal(cx: &mut Context<Self>) -> impl Element {
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.text_center()
|
||||
.justify_center()
|
||||
.child(Headline::new("Copilot Enabled!").size(HeadlineSize::Large))
|
||||
.child(Label::new(
|
||||
"You can update your settings or sign out from the Copilot menu in the status bar.",
|
||||
))
|
||||
.child(Label::new("You're all set to use GitHub Copilot.").color(Color::Muted))
|
||||
.child(
|
||||
Button::new("copilot-enabled-done-button", "Done")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.size(ButtonSize::Medium)
|
||||
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_unauthorized_modal(cx: &mut Context<Self>) -> impl Element {
|
||||
v_flex()
|
||||
.child(Headline::new("You must have an active GitHub Copilot subscription.").size(HeadlineSize::Large))
|
||||
let description = "Enable Copilot by connecting your existing license once you have subscribed or renewed your subscription.";
|
||||
|
||||
.child(Label::new(
|
||||
"You can enable Copilot by connecting your existing license once you have subscribed or renewed your subscription.",
|
||||
).color(Color::Warning))
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.text_center()
|
||||
.justify_center()
|
||||
.child(
|
||||
Headline::new("You must have an active GitHub Copilot subscription.")
|
||||
.size(HeadlineSize::Large),
|
||||
)
|
||||
.child(Label::new(description).color(Color::Warning))
|
||||
.child(
|
||||
Button::new("copilot-subscribe-button", "Subscribe on GitHub")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.size(ButtonSize::Medium)
|
||||
.on_click(|_, _, cx| cx.open_url(COPILOT_SIGN_UP_URL)),
|
||||
)
|
||||
.child(
|
||||
Button::new("copilot-subscribe-cancel-button", "Cancel")
|
||||
.full_width()
|
||||
.size(ButtonSize::Medium)
|
||||
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_loading(window: &mut Window, _: &mut Context<Self>) -> impl Element {
|
||||
let loading_icon = svg()
|
||||
.size_8()
|
||||
.path(IconName::ArrowCircle.path())
|
||||
.text_color(window.text_style().color)
|
||||
.with_animation(
|
||||
"icon_circle_arrow",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|svg, delta| svg.with_transformation(Transformation::rotate(percentage(delta))),
|
||||
);
|
||||
fn render_error_modal(_cx: &mut Context<Self>) -> impl Element {
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.text_center()
|
||||
.justify_center()
|
||||
.child(Headline::new("An Error Happened").size(HeadlineSize::Large))
|
||||
.child(Label::new(ERROR_LABEL).color(Color::Muted))
|
||||
.child(
|
||||
Button::new("copilot-subscribe-button", "Reinstall Copilot and Sign In")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.size(ButtonSize::Medium)
|
||||
.icon(IconName::Download)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(|_, window, cx| reinstall_and_sign_in(window, cx)),
|
||||
)
|
||||
}
|
||||
|
||||
h_flex().justify_center().child(loading_icon)
|
||||
fn before_dismiss(
|
||||
&mut self,
|
||||
cx: &mut Context<'_, CopilotCodeVerification>,
|
||||
) -> workspace::DismissDecision {
|
||||
self.copilot.update(cx, |copilot, cx| {
|
||||
if matches!(copilot.status(), Status::SigningIn { .. }) {
|
||||
copilot.sign_out(cx).detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
workspace::DismissDecision::Dismiss(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for CopilotCodeVerification {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let prompt = match &self.status {
|
||||
Status::SigningIn { prompt: None } => {
|
||||
Self::render_loading(window, cx).into_any_element()
|
||||
}
|
||||
Status::SigningIn { prompt: None } => Icon::new(IconName::ArrowCircle)
|
||||
.color(Color::Muted)
|
||||
.with_rotate_animation(2)
|
||||
.into_any_element(),
|
||||
Status::SigningIn {
|
||||
prompt: Some(prompt),
|
||||
} => Self::render_prompting_modal(self.connect_clicked, prompt, cx).into_any_element(),
|
||||
@@ -349,17 +380,20 @@ impl Render for CopilotCodeVerification {
|
||||
self.connect_clicked = false;
|
||||
Self::render_enabled_modal(cx).into_any_element()
|
||||
}
|
||||
Status::Error(..) => Self::render_error_modal(cx).into_any_element(),
|
||||
_ => div().into_any_element(),
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.id("copilot code verification")
|
||||
.id("copilot_code_verification")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.elevation_3(cx)
|
||||
.w_96()
|
||||
.items_center()
|
||||
.p_4()
|
||||
.size_full()
|
||||
.px_4()
|
||||
.py_8()
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.elevation_3(cx)
|
||||
.on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
@@ -373,3 +407,243 @@ impl Render for CopilotCodeVerification {
|
||||
.child(prompt)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ConfigurationView {
|
||||
copilot_status: Option<Status>,
|
||||
is_authenticated: fn(cx: &App) -> bool,
|
||||
edit_prediction: bool,
|
||||
_subscription: Option<Subscription>,
|
||||
}
|
||||
|
||||
pub enum ConfigurationMode {
|
||||
Chat,
|
||||
EditPrediction,
|
||||
}
|
||||
|
||||
impl ConfigurationView {
|
||||
pub fn new(
|
||||
is_authenticated: fn(cx: &App) -> bool,
|
||||
mode: ConfigurationMode,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let copilot = Copilot::global(cx);
|
||||
|
||||
Self {
|
||||
copilot_status: copilot.as_ref().map(|copilot| copilot.read(cx).status()),
|
||||
is_authenticated,
|
||||
edit_prediction: matches!(mode, ConfigurationMode::EditPrediction),
|
||||
_subscription: copilot.as_ref().map(|copilot| {
|
||||
cx.observe(copilot, |this, model, cx| {
|
||||
this.copilot_status = Some(model.read(cx).status());
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigurationView {
|
||||
fn is_starting(&self) -> bool {
|
||||
matches!(&self.copilot_status, Some(Status::Starting { .. }))
|
||||
}
|
||||
|
||||
fn is_signing_in(&self) -> bool {
|
||||
matches!(
|
||||
&self.copilot_status,
|
||||
Some(Status::SigningIn { .. })
|
||||
| Some(Status::SignedOut {
|
||||
awaiting_signing_in: true
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
fn is_error(&self) -> bool {
|
||||
matches!(&self.copilot_status, Some(Status::Error(_)))
|
||||
}
|
||||
|
||||
fn has_no_status(&self) -> bool {
|
||||
self.copilot_status.is_none()
|
||||
}
|
||||
|
||||
fn loading_message(&self) -> Option<SharedString> {
|
||||
if self.is_starting() {
|
||||
Some("Starting Copilot…".into())
|
||||
} else if self.is_signing_in() {
|
||||
Some("Signing into Copilot…".into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn render_loading_button(
|
||||
&self,
|
||||
label: impl Into<SharedString>,
|
||||
edit_prediction: bool,
|
||||
) -> impl IntoElement {
|
||||
ButtonLike::new("loading_button")
|
||||
.disabled(true)
|
||||
.style(ButtonStyle::Outlined)
|
||||
.when(edit_prediction, |this| this.size(ButtonSize::Medium))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.justify_center()
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.with_rotate_animation(4),
|
||||
)
|
||||
.child(Label::new(label)),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_sign_in_button(&self, edit_prediction: bool) -> impl IntoElement {
|
||||
let label = if edit_prediction {
|
||||
"Sign in to GitHub"
|
||||
} else {
|
||||
"Sign in to use GitHub Copilot"
|
||||
};
|
||||
|
||||
Button::new("sign_in", label)
|
||||
.map(|this| {
|
||||
if edit_prediction {
|
||||
this.size(ButtonSize::Medium)
|
||||
} else {
|
||||
this.full_width()
|
||||
}
|
||||
})
|
||||
.style(ButtonStyle::Outlined)
|
||||
.icon(IconName::Github)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(|_, window, cx| initiate_sign_in(window, cx))
|
||||
}
|
||||
|
||||
fn render_reinstall_button(&self, edit_prediction: bool) -> impl IntoElement {
|
||||
let label = if edit_prediction {
|
||||
"Reinstall and Sign in"
|
||||
} else {
|
||||
"Reinstall Copilot and Sign in"
|
||||
};
|
||||
|
||||
Button::new("reinstall_and_sign_in", label)
|
||||
.map(|this| {
|
||||
if edit_prediction {
|
||||
this.size(ButtonSize::Medium)
|
||||
} else {
|
||||
this.full_width()
|
||||
}
|
||||
})
|
||||
.style(ButtonStyle::Outlined)
|
||||
.icon(IconName::Download)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(|_, window, cx| reinstall_and_sign_in(window, cx))
|
||||
}
|
||||
|
||||
fn render_for_edit_prediction(&self) -> impl IntoElement {
|
||||
let container = |description: SharedString, action: AnyElement| {
|
||||
h_flex()
|
||||
.pt_2p5()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.max_w_1_2()
|
||||
.child(Label::new("Authenticate To Use"))
|
||||
.child(
|
||||
Label::new(description)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
)
|
||||
.child(action)
|
||||
};
|
||||
|
||||
let start_label = "To use Copilot for edit predictions, you need to be logged in to GitHub. Note that your GitHub account must have an active Copilot subscription.".into();
|
||||
let no_status_label = "Copilot requires an active GitHub Copilot subscription. Please ensure Copilot is configured and try again, or use a different edit predictions provider.".into();
|
||||
|
||||
if let Some(msg) = self.loading_message() {
|
||||
container(
|
||||
start_label,
|
||||
self.render_loading_button(msg, true).into_any_element(),
|
||||
)
|
||||
.into_any_element()
|
||||
} else if self.is_error() {
|
||||
container(
|
||||
ERROR_LABEL.into(),
|
||||
self.render_reinstall_button(true).into_any_element(),
|
||||
)
|
||||
.into_any_element()
|
||||
} else if self.has_no_status() {
|
||||
container(
|
||||
no_status_label,
|
||||
self.render_sign_in_button(true).into_any_element(),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
container(
|
||||
start_label,
|
||||
self.render_sign_in_button(true).into_any_element(),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_for_chat(&self) -> impl IntoElement {
|
||||
let start_label = "To use Zed's agent with GitHub Copilot, you need to be logged in to GitHub. Note that your GitHub account must have an active Copilot Chat subscription.";
|
||||
let no_status_label = "Copilot Chat requires an active GitHub Copilot subscription. Please ensure Copilot is configured and try again, or use a different LLM provider.";
|
||||
|
||||
if let Some(msg) = self.loading_message() {
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(Label::new(start_label))
|
||||
.child(self.render_loading_button(msg, false))
|
||||
.into_any_element()
|
||||
} else if self.is_error() {
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(Label::new(ERROR_LABEL))
|
||||
.child(self.render_reinstall_button(false))
|
||||
.into_any_element()
|
||||
} else if self.has_no_status() {
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(Label::new(no_status_label))
|
||||
.child(self.render_sign_in_button(false))
|
||||
.into_any_element()
|
||||
} else {
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(Label::new(start_label))
|
||||
.child(self.render_sign_in_button(false))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ConfigurationView {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let is_authenticated = self.is_authenticated;
|
||||
|
||||
if is_authenticated(cx) {
|
||||
return ConfiguredApiCard::new("Authorized")
|
||||
.button_label("Sign Out")
|
||||
.on_click(|_, window, cx| {
|
||||
initiate_sign_out(window, cx);
|
||||
})
|
||||
.into_any_element();
|
||||
}
|
||||
|
||||
if self.edit_prediction {
|
||||
self.render_for_edit_prediction().into_any_element()
|
||||
} else {
|
||||
self.render_for_chat().into_any_element()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
copilot.workspace = true
|
||||
credentials_provider.workspace = true
|
||||
db.workspace = true
|
||||
edit_prediction_types.workspace = true
|
||||
edit_prediction_context.workspace = true
|
||||
|
||||
@@ -72,6 +72,7 @@ pub use crate::prediction::EditPrediction;
|
||||
pub use crate::prediction::EditPredictionId;
|
||||
use crate::prediction::EditPredictionResult;
|
||||
pub use crate::sweep_ai::SweepAi;
|
||||
pub use language_model::ApiKeyState;
|
||||
pub use telemetry_events::EditPredictionRating;
|
||||
pub use zed_edit_prediction_delegate::ZedEditPredictionDelegate;
|
||||
|
||||
@@ -536,22 +537,12 @@ impl EditPredictionStore {
|
||||
self.edit_prediction_model = model;
|
||||
}
|
||||
|
||||
pub fn has_sweep_api_token(&self) -> bool {
|
||||
self.sweep_ai
|
||||
.api_token
|
||||
.clone()
|
||||
.now_or_never()
|
||||
.flatten()
|
||||
.is_some()
|
||||
pub fn has_sweep_api_token(&self, cx: &App) -> bool {
|
||||
self.sweep_ai.api_token.read(cx).has_key()
|
||||
}
|
||||
|
||||
pub fn has_mercury_api_token(&self) -> bool {
|
||||
self.mercury
|
||||
.api_token
|
||||
.clone()
|
||||
.now_or_never()
|
||||
.flatten()
|
||||
.is_some()
|
||||
pub fn has_mercury_api_token(&self, cx: &App) -> bool {
|
||||
self.mercury.api_token.read(cx).has_key()
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli-support")]
|
||||
|
||||
@@ -1,40 +1,34 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use credentials_provider::CredentialsProvider;
|
||||
use futures::{AsyncReadExt as _, FutureExt, future::Shared};
|
||||
use gpui::{
|
||||
App, AppContext as _, Task,
|
||||
http_client::{self, AsyncBody, Method},
|
||||
};
|
||||
use language::{OffsetRangeExt as _, ToOffset, ToPoint as _};
|
||||
use std::{mem, ops::Range, path::Path, sync::Arc, time::Instant};
|
||||
use zeta_prompt::ZetaPromptInput;
|
||||
|
||||
use crate::{
|
||||
DebugEvent, EditPredictionFinishedDebugEvent, EditPredictionId, EditPredictionModelInput,
|
||||
EditPredictionStartedDebugEvent, open_ai_response::text_from_response,
|
||||
prediction::EditPredictionResult,
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{
|
||||
App, AppContext as _, Entity, SharedString, Task,
|
||||
http_client::{self, AsyncBody, Method},
|
||||
};
|
||||
use language::{OffsetRangeExt as _, ToOffset, ToPoint as _};
|
||||
use language_model::{ApiKeyState, EnvVar, env_var};
|
||||
use std::{mem, ops::Range, path::Path, sync::Arc, time::Instant};
|
||||
use zeta_prompt::ZetaPromptInput;
|
||||
|
||||
const MERCURY_API_URL: &str = "https://api.inceptionlabs.ai/v1/edit/completions";
|
||||
const MAX_CONTEXT_TOKENS: usize = 150;
|
||||
const MAX_REWRITE_TOKENS: usize = 350;
|
||||
|
||||
pub struct Mercury {
|
||||
pub api_token: Shared<Task<Option<String>>>,
|
||||
pub api_token: Entity<ApiKeyState>,
|
||||
}
|
||||
|
||||
impl Mercury {
|
||||
pub fn new(cx: &App) -> Self {
|
||||
pub fn new(cx: &mut App) -> Self {
|
||||
Mercury {
|
||||
api_token: load_api_token(cx).shared(),
|
||||
api_token: mercury_api_token(cx),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_api_token(&mut self, api_token: Option<String>, cx: &mut App) -> Task<Result<()>> {
|
||||
self.api_token = Task::ready(api_token.clone()).shared();
|
||||
store_api_token_in_keychain(api_token, cx)
|
||||
}
|
||||
|
||||
pub(crate) fn request_prediction(
|
||||
&self,
|
||||
EditPredictionModelInput {
|
||||
@@ -48,7 +42,10 @@ impl Mercury {
|
||||
}: EditPredictionModelInput,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Option<EditPredictionResult>>> {
|
||||
let Some(api_token) = self.api_token.clone().now_or_never().flatten() else {
|
||||
self.api_token.update(cx, |key_state, cx| {
|
||||
_ = key_state.load_if_needed(MERCURY_CREDENTIALS_URL, |s| s, cx);
|
||||
});
|
||||
let Some(api_token) = self.api_token.read(cx).key(&MERCURY_CREDENTIALS_URL) else {
|
||||
return Task::ready(Ok(None));
|
||||
};
|
||||
let full_path: Arc<Path> = snapshot
|
||||
@@ -299,45 +296,16 @@ fn push_delimited(prompt: &mut String, delimiters: Range<&str>, cb: impl FnOnce(
|
||||
prompt.push_str(delimiters.end);
|
||||
}
|
||||
|
||||
pub const MERCURY_CREDENTIALS_URL: &str = "https://api.inceptionlabs.ai/v1/edit/completions";
|
||||
pub const MERCURY_CREDENTIALS_URL: SharedString =
|
||||
SharedString::new_static("https://api.inceptionlabs.ai/v1/edit/completions");
|
||||
pub const MERCURY_CREDENTIALS_USERNAME: &str = "mercury-api-token";
|
||||
pub static MERCURY_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("MERCURY_AI_TOKEN");
|
||||
pub static MERCURY_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
|
||||
|
||||
pub fn load_api_token(cx: &App) -> Task<Option<String>> {
|
||||
if let Some(api_token) = std::env::var("MERCURY_AI_TOKEN")
|
||||
.ok()
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
return Task::ready(Some(api_token));
|
||||
}
|
||||
let credentials_provider = <dyn CredentialsProvider>::global(cx);
|
||||
cx.spawn(async move |cx| {
|
||||
let (_, credentials) = credentials_provider
|
||||
.read_credentials(MERCURY_CREDENTIALS_URL, &cx)
|
||||
.await
|
||||
.ok()??;
|
||||
String::from_utf8(credentials).ok()
|
||||
})
|
||||
}
|
||||
|
||||
fn store_api_token_in_keychain(api_token: Option<String>, cx: &App) -> Task<Result<()>> {
|
||||
let credentials_provider = <dyn CredentialsProvider>::global(cx);
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
if let Some(api_token) = api_token {
|
||||
credentials_provider
|
||||
.write_credentials(
|
||||
MERCURY_CREDENTIALS_URL,
|
||||
MERCURY_CREDENTIALS_USERNAME,
|
||||
api_token.as_bytes(),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.context("Failed to save Mercury API token to system keychain")
|
||||
} else {
|
||||
credentials_provider
|
||||
.delete_credentials(MERCURY_CREDENTIALS_URL, cx)
|
||||
.await
|
||||
.context("Failed to delete Mercury API token from system keychain")
|
||||
}
|
||||
})
|
||||
pub fn mercury_api_token(cx: &mut App) -> Entity<ApiKeyState> {
|
||||
MERCURY_API_KEY
|
||||
.get_or_init(|| {
|
||||
cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()))
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use credentials_provider::CredentialsProvider;
|
||||
use futures::{AsyncReadExt as _, FutureExt, future::Shared};
|
||||
use anyhow::Result;
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{
|
||||
App, AppContext as _, Task,
|
||||
App, AppContext as _, Entity, SharedString, Task,
|
||||
http_client::{self, AsyncBody, Method},
|
||||
};
|
||||
use language::{Point, ToOffset as _};
|
||||
use language_model::{ApiKeyState, EnvVar, env_var};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
@@ -20,30 +20,28 @@ use crate::{EditPredictionId, EditPredictionModelInput, prediction::EditPredicti
|
||||
const SWEEP_API_URL: &str = "https://autocomplete.sweep.dev/backend/next_edit_autocomplete";
|
||||
|
||||
pub struct SweepAi {
|
||||
pub api_token: Shared<Task<Option<String>>>,
|
||||
pub api_token: Entity<ApiKeyState>,
|
||||
pub debug_info: Arc<str>,
|
||||
}
|
||||
|
||||
impl SweepAi {
|
||||
pub fn new(cx: &App) -> Self {
|
||||
pub fn new(cx: &mut App) -> Self {
|
||||
SweepAi {
|
||||
api_token: load_api_token(cx).shared(),
|
||||
api_token: sweep_api_token(cx),
|
||||
debug_info: debug_info(cx),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_api_token(&mut self, api_token: Option<String>, cx: &mut App) -> Task<Result<()>> {
|
||||
self.api_token = Task::ready(api_token.clone()).shared();
|
||||
store_api_token_in_keychain(api_token, cx)
|
||||
}
|
||||
|
||||
pub fn request_prediction_with_sweep(
|
||||
&self,
|
||||
inputs: EditPredictionModelInput,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Option<EditPredictionResult>>> {
|
||||
let debug_info = self.debug_info.clone();
|
||||
let Some(api_token) = self.api_token.clone().now_or_never().flatten() else {
|
||||
self.api_token.update(cx, |key_state, cx| {
|
||||
_ = key_state.load_if_needed(SWEEP_CREDENTIALS_URL, |s| s, cx);
|
||||
});
|
||||
let Some(api_token) = self.api_token.read(cx).key(&SWEEP_CREDENTIALS_URL) else {
|
||||
return Task::ready(Ok(None));
|
||||
};
|
||||
let full_path: Arc<Path> = inputs
|
||||
@@ -270,47 +268,18 @@ impl SweepAi {
|
||||
}
|
||||
}
|
||||
|
||||
pub const SWEEP_CREDENTIALS_URL: &str = "https://autocomplete.sweep.dev";
|
||||
pub const SWEEP_CREDENTIALS_URL: SharedString =
|
||||
SharedString::new_static("https://autocomplete.sweep.dev");
|
||||
pub const SWEEP_CREDENTIALS_USERNAME: &str = "sweep-api-token";
|
||||
pub static SWEEP_AI_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("SWEEP_AI_TOKEN");
|
||||
pub static SWEEP_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
|
||||
|
||||
pub fn load_api_token(cx: &App) -> Task<Option<String>> {
|
||||
if let Some(api_token) = std::env::var("SWEEP_AI_TOKEN")
|
||||
.ok()
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
return Task::ready(Some(api_token));
|
||||
}
|
||||
let credentials_provider = <dyn CredentialsProvider>::global(cx);
|
||||
cx.spawn(async move |cx| {
|
||||
let (_, credentials) = credentials_provider
|
||||
.read_credentials(SWEEP_CREDENTIALS_URL, &cx)
|
||||
.await
|
||||
.ok()??;
|
||||
String::from_utf8(credentials).ok()
|
||||
})
|
||||
}
|
||||
|
||||
fn store_api_token_in_keychain(api_token: Option<String>, cx: &App) -> Task<Result<()>> {
|
||||
let credentials_provider = <dyn CredentialsProvider>::global(cx);
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
if let Some(api_token) = api_token {
|
||||
credentials_provider
|
||||
.write_credentials(
|
||||
SWEEP_CREDENTIALS_URL,
|
||||
SWEEP_CREDENTIALS_USERNAME,
|
||||
api_token.as_bytes(),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.context("Failed to save Sweep API token to system keychain")
|
||||
} else {
|
||||
credentials_provider
|
||||
.delete_credentials(SWEEP_CREDENTIALS_URL, cx)
|
||||
.await
|
||||
.context("Failed to delete Sweep API token from system keychain")
|
||||
}
|
||||
})
|
||||
pub fn sweep_api_token(cx: &mut App) -> Entity<ApiKeyState> {
|
||||
SWEEP_API_KEY
|
||||
.get_or_init(|| {
|
||||
cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()))
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
|
||||
@@ -100,7 +100,7 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate {
|
||||
) -> bool {
|
||||
let store = self.store.read(cx);
|
||||
if store.edit_prediction_model == EditPredictionModel::Sweep {
|
||||
store.has_sweep_api_token()
|
||||
store.has_sweep_api_token(cx)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ cloud_llm_client.workspace = true
|
||||
codestral.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
copilot.workspace = true
|
||||
edit_prediction.workspace = true
|
||||
edit_prediction_types.workspace = true
|
||||
edit_prediction.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
@@ -41,7 +41,6 @@ telemetry.workspace = true
|
||||
text.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
ui_input.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
@@ -3,7 +3,9 @@ use client::{Client, UserStore, zed_urls};
|
||||
use cloud_llm_client::UsageLimit;
|
||||
use codestral::CodestralEditPredictionDelegate;
|
||||
use copilot::{Copilot, Status};
|
||||
use edit_prediction::{MercuryFeatureFlag, SweepFeatureFlag, Zeta2FeatureFlag};
|
||||
use edit_prediction::{
|
||||
EditPredictionStore, MercuryFeatureFlag, SweepFeatureFlag, Zeta2FeatureFlag,
|
||||
};
|
||||
use edit_prediction_types::EditPredictionDelegateHandle;
|
||||
use editor::{
|
||||
Editor, MultiBufferOffset, SelectionEffects, actions::ShowEditPrediction, scroll::Autoscroll,
|
||||
@@ -42,12 +44,9 @@ use workspace::{
|
||||
StatusItemView, Toast, Workspace, create_and_open_local_file, item::ItemHandle,
|
||||
notifications::NotificationId,
|
||||
};
|
||||
use zed_actions::OpenBrowser;
|
||||
use zed_actions::{OpenBrowser, OpenSettingsAt};
|
||||
|
||||
use crate::{
|
||||
ExternalProviderApiKeyModal, RatePredictions,
|
||||
rate_prediction_modal::PredictEditsRatePredictionsFeatureFlag,
|
||||
};
|
||||
use crate::{RatePredictions, rate_prediction_modal::PredictEditsRatePredictionsFeatureFlag};
|
||||
|
||||
actions!(
|
||||
edit_prediction,
|
||||
@@ -248,45 +247,21 @@ impl Render for EditPredictionButton {
|
||||
EditPredictionProvider::Codestral => {
|
||||
let enabled = self.editor_enabled.unwrap_or(true);
|
||||
let has_api_key = CodestralEditPredictionDelegate::has_api_key(cx);
|
||||
let fs = self.fs.clone();
|
||||
let this = cx.weak_entity();
|
||||
|
||||
let tooltip_meta = if has_api_key {
|
||||
"Powered by Codestral"
|
||||
} else {
|
||||
"Missing API key for Codestral"
|
||||
};
|
||||
|
||||
div().child(
|
||||
PopoverMenu::new("codestral")
|
||||
.menu(move |window, cx| {
|
||||
if has_api_key {
|
||||
this.update(cx, |this, cx| {
|
||||
this.build_codestral_context_menu(window, cx)
|
||||
})
|
||||
.ok()
|
||||
} else {
|
||||
Some(ContextMenu::build(window, cx, |menu, _, _| {
|
||||
let fs = fs.clone();
|
||||
|
||||
menu.entry(
|
||||
"Configure Codestral API Key",
|
||||
None,
|
||||
move |window, cx| {
|
||||
window.dispatch_action(
|
||||
zed_actions::agent::OpenSettings.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
},
|
||||
)
|
||||
.separator()
|
||||
.entry(
|
||||
"Use Zed AI instead",
|
||||
None,
|
||||
move |_, cx| {
|
||||
set_completion_provider(
|
||||
fs.clone(),
|
||||
cx,
|
||||
EditPredictionProvider::Zed,
|
||||
)
|
||||
},
|
||||
)
|
||||
}))
|
||||
}
|
||||
this.update(cx, |this, cx| {
|
||||
this.build_codestral_context_menu(window, cx)
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.anchor(Corner::BottomRight)
|
||||
.trigger_with_tooltip(
|
||||
@@ -304,7 +279,14 @@ impl Render for EditPredictionButton {
|
||||
cx.theme().colors().status_bar_background,
|
||||
))
|
||||
}),
|
||||
move |_window, cx| Tooltip::for_action("Codestral", &ToggleMenu, cx),
|
||||
move |_window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Edit Prediction",
|
||||
Some(&ToggleMenu),
|
||||
tooltip_meta,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
.with_handle(self.popover_menu_handle.clone()),
|
||||
)
|
||||
@@ -313,6 +295,7 @@ impl Render for EditPredictionButton {
|
||||
let enabled = self.editor_enabled.unwrap_or(true);
|
||||
|
||||
let ep_icon;
|
||||
let tooltip_meta;
|
||||
let mut missing_token = false;
|
||||
|
||||
match provider {
|
||||
@@ -320,15 +303,25 @@ impl Render for EditPredictionButton {
|
||||
EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME,
|
||||
) => {
|
||||
ep_icon = IconName::SweepAi;
|
||||
tooltip_meta = if missing_token {
|
||||
"Missing API key for Sweep"
|
||||
} else {
|
||||
"Powered by Sweep"
|
||||
};
|
||||
missing_token = edit_prediction::EditPredictionStore::try_global(cx)
|
||||
.is_some_and(|ep_store| !ep_store.read(cx).has_sweep_api_token());
|
||||
.is_some_and(|ep_store| !ep_store.read(cx).has_sweep_api_token(cx));
|
||||
}
|
||||
EditPredictionProvider::Experimental(
|
||||
EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME,
|
||||
) => {
|
||||
ep_icon = IconName::Inception;
|
||||
missing_token = edit_prediction::EditPredictionStore::try_global(cx)
|
||||
.is_some_and(|ep_store| !ep_store.read(cx).has_mercury_api_token());
|
||||
.is_some_and(|ep_store| !ep_store.read(cx).has_mercury_api_token(cx));
|
||||
tooltip_meta = if missing_token {
|
||||
"Missing API key for Mercury"
|
||||
} else {
|
||||
"Powered by Mercury"
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
ep_icon = if enabled {
|
||||
@@ -336,6 +329,7 @@ impl Render for EditPredictionButton {
|
||||
} else {
|
||||
IconName::ZedPredictDisabled
|
||||
};
|
||||
tooltip_meta = "Powered by Zeta"
|
||||
}
|
||||
};
|
||||
|
||||
@@ -400,33 +394,26 @@ impl Render for EditPredictionButton {
|
||||
})
|
||||
.when(!self.popover_menu_handle.is_deployed(), |element| {
|
||||
let user = user.clone();
|
||||
|
||||
element.tooltip(move |_window, cx| {
|
||||
if enabled {
|
||||
let description = if enabled {
|
||||
if show_editor_predictions {
|
||||
Tooltip::for_action("Edit Prediction", &ToggleMenu, cx)
|
||||
tooltip_meta
|
||||
} else if user.is_none() {
|
||||
Tooltip::with_meta(
|
||||
"Edit Prediction",
|
||||
Some(&ToggleMenu),
|
||||
"Sign In To Use",
|
||||
cx,
|
||||
)
|
||||
"Sign In To Use"
|
||||
} else {
|
||||
Tooltip::with_meta(
|
||||
"Edit Prediction",
|
||||
Some(&ToggleMenu),
|
||||
"Hidden For This File",
|
||||
cx,
|
||||
)
|
||||
"Hidden For This File"
|
||||
}
|
||||
} else {
|
||||
Tooltip::with_meta(
|
||||
"Edit Prediction",
|
||||
Some(&ToggleMenu),
|
||||
"Disabled For This File",
|
||||
cx,
|
||||
)
|
||||
}
|
||||
"Disabled For This File"
|
||||
};
|
||||
|
||||
Tooltip::with_meta(
|
||||
"Edit Prediction",
|
||||
Some(&ToggleMenu),
|
||||
description,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
@@ -519,6 +506,12 @@ impl EditPredictionButton {
|
||||
|
||||
providers.push(EditPredictionProvider::Zed);
|
||||
|
||||
if cx.has_flag::<Zeta2FeatureFlag>() {
|
||||
providers.push(EditPredictionProvider::Experimental(
|
||||
EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME,
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
if matches!(copilot.read(cx).status(), Status::Authorized) {
|
||||
providers.push(EditPredictionProvider::Copilot);
|
||||
@@ -537,24 +530,28 @@ impl EditPredictionButton {
|
||||
providers.push(EditPredictionProvider::Codestral);
|
||||
}
|
||||
|
||||
if cx.has_flag::<SweepFeatureFlag>() {
|
||||
let ep_store = EditPredictionStore::try_global(cx);
|
||||
|
||||
if cx.has_flag::<SweepFeatureFlag>()
|
||||
&& ep_store
|
||||
.as_ref()
|
||||
.is_some_and(|ep_store| ep_store.read(cx).has_sweep_api_token(cx))
|
||||
{
|
||||
providers.push(EditPredictionProvider::Experimental(
|
||||
EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME,
|
||||
));
|
||||
}
|
||||
|
||||
if cx.has_flag::<MercuryFeatureFlag>() {
|
||||
if cx.has_flag::<MercuryFeatureFlag>()
|
||||
&& ep_store
|
||||
.as_ref()
|
||||
.is_some_and(|ep_store| ep_store.read(cx).has_mercury_api_token(cx))
|
||||
{
|
||||
providers.push(EditPredictionProvider::Experimental(
|
||||
EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME,
|
||||
));
|
||||
}
|
||||
|
||||
if cx.has_flag::<Zeta2FeatureFlag>() {
|
||||
providers.push(EditPredictionProvider::Experimental(
|
||||
EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME,
|
||||
));
|
||||
}
|
||||
|
||||
providers
|
||||
}
|
||||
|
||||
@@ -562,13 +559,10 @@ impl EditPredictionButton {
|
||||
&self,
|
||||
mut menu: ContextMenu,
|
||||
current_provider: EditPredictionProvider,
|
||||
cx: &App,
|
||||
cx: &mut App,
|
||||
) -> ContextMenu {
|
||||
let available_providers = self.get_available_providers(cx);
|
||||
|
||||
const ZED_AI_CALLOUT: &str =
|
||||
"Zed's edit prediction is powered by Zeta, an open-source, dataset mode.";
|
||||
|
||||
let providers: Vec<_> = available_providers
|
||||
.into_iter()
|
||||
.filter(|p| *p != EditPredictionProvider::None)
|
||||
@@ -581,153 +575,32 @@ impl EditPredictionButton {
|
||||
let is_current = provider == current_provider;
|
||||
let fs = self.fs.clone();
|
||||
|
||||
menu = match provider {
|
||||
EditPredictionProvider::Zed => menu.item(
|
||||
ContextMenuEntry::new("Zed AI")
|
||||
.toggleable(IconPosition::Start, is_current)
|
||||
.documentation_aside(
|
||||
DocumentationSide::Left,
|
||||
DocumentationEdge::Bottom,
|
||||
|_| Label::new(ZED_AI_CALLOUT).into_any_element(),
|
||||
)
|
||||
.handler(move |_, cx| {
|
||||
set_completion_provider(fs.clone(), cx, provider);
|
||||
}),
|
||||
),
|
||||
EditPredictionProvider::Copilot => menu.item(
|
||||
ContextMenuEntry::new("GitHub Copilot")
|
||||
.toggleable(IconPosition::Start, is_current)
|
||||
.handler(move |_, cx| {
|
||||
set_completion_provider(fs.clone(), cx, provider);
|
||||
}),
|
||||
),
|
||||
EditPredictionProvider::Supermaven => menu.item(
|
||||
ContextMenuEntry::new("Supermaven")
|
||||
.toggleable(IconPosition::Start, is_current)
|
||||
.handler(move |_, cx| {
|
||||
set_completion_provider(fs.clone(), cx, provider);
|
||||
}),
|
||||
),
|
||||
EditPredictionProvider::Codestral => menu.item(
|
||||
ContextMenuEntry::new("Codestral")
|
||||
.toggleable(IconPosition::Start, is_current)
|
||||
.handler(move |_, cx| {
|
||||
set_completion_provider(fs.clone(), cx, provider);
|
||||
}),
|
||||
),
|
||||
let name = match provider {
|
||||
EditPredictionProvider::Zed => "Zed AI",
|
||||
EditPredictionProvider::Copilot => "GitHub Copilot",
|
||||
EditPredictionProvider::Supermaven => "Supermaven",
|
||||
EditPredictionProvider::Codestral => "Codestral",
|
||||
EditPredictionProvider::Experimental(
|
||||
EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME,
|
||||
) => {
|
||||
let has_api_token = edit_prediction::EditPredictionStore::try_global(cx)
|
||||
.map_or(false, |ep_store| ep_store.read(cx).has_sweep_api_token());
|
||||
|
||||
let should_open_modal = !has_api_token || is_current;
|
||||
|
||||
let entry = if has_api_token {
|
||||
ContextMenuEntry::new("Sweep")
|
||||
.toggleable(IconPosition::Start, is_current)
|
||||
} else {
|
||||
ContextMenuEntry::new("Sweep")
|
||||
.icon(IconName::XCircle)
|
||||
.icon_color(Color::Error)
|
||||
.documentation_aside(
|
||||
DocumentationSide::Left,
|
||||
DocumentationEdge::Bottom,
|
||||
|_| {
|
||||
Label::new("Click to configure your Sweep API token")
|
||||
.into_any_element()
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
let entry = entry.handler(move |window, cx| {
|
||||
if should_open_modal {
|
||||
if let Some(workspace) = window.root::<Workspace>().flatten() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
ExternalProviderApiKeyModal::new(
|
||||
window,
|
||||
cx,
|
||||
|api_key, store, cx| {
|
||||
store
|
||||
.sweep_ai
|
||||
.set_api_token(api_key, cx)
|
||||
.detach_and_log_err(cx);
|
||||
},
|
||||
)
|
||||
});
|
||||
});
|
||||
};
|
||||
} else {
|
||||
set_completion_provider(fs.clone(), cx, provider);
|
||||
}
|
||||
});
|
||||
|
||||
menu.item(entry)
|
||||
}
|
||||
) => "Sweep",
|
||||
EditPredictionProvider::Experimental(
|
||||
EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME,
|
||||
) => {
|
||||
let has_api_token = edit_prediction::EditPredictionStore::try_global(cx)
|
||||
.map_or(false, |ep_store| ep_store.read(cx).has_mercury_api_token());
|
||||
|
||||
let should_open_modal = !has_api_token || is_current;
|
||||
|
||||
let entry = if has_api_token {
|
||||
ContextMenuEntry::new("Mercury")
|
||||
.toggleable(IconPosition::Start, is_current)
|
||||
} else {
|
||||
ContextMenuEntry::new("Mercury")
|
||||
.icon(IconName::XCircle)
|
||||
.icon_color(Color::Error)
|
||||
.documentation_aside(
|
||||
DocumentationSide::Left,
|
||||
DocumentationEdge::Bottom,
|
||||
|_| {
|
||||
Label::new("Click to configure your Mercury API token")
|
||||
.into_any_element()
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
let entry = entry.handler(move |window, cx| {
|
||||
if should_open_modal {
|
||||
if let Some(workspace) = window.root::<Workspace>().flatten() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
ExternalProviderApiKeyModal::new(
|
||||
window,
|
||||
cx,
|
||||
|api_key, store, cx| {
|
||||
store
|
||||
.mercury
|
||||
.set_api_token(api_key, cx)
|
||||
.detach_and_log_err(cx);
|
||||
},
|
||||
)
|
||||
});
|
||||
});
|
||||
};
|
||||
} else {
|
||||
set_completion_provider(fs.clone(), cx, provider);
|
||||
}
|
||||
});
|
||||
|
||||
menu.item(entry)
|
||||
}
|
||||
) => "Mercury",
|
||||
EditPredictionProvider::Experimental(
|
||||
EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME,
|
||||
) => menu.item(
|
||||
ContextMenuEntry::new("Zeta2")
|
||||
.toggleable(IconPosition::Start, is_current)
|
||||
.handler(move |_, cx| {
|
||||
set_completion_provider(fs.clone(), cx, provider);
|
||||
}),
|
||||
),
|
||||
) => "Zeta2",
|
||||
EditPredictionProvider::None | EditPredictionProvider::Experimental(_) => {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
menu = menu.item(
|
||||
ContextMenuEntry::new(name)
|
||||
.toggleable(IconPosition::Start, is_current)
|
||||
.handler(move |_, cx| {
|
||||
set_completion_provider(fs.clone(), cx, provider);
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -832,14 +705,7 @@ impl EditPredictionButton {
|
||||
let subtle_mode = matches!(current_mode, EditPredictionsMode::Subtle);
|
||||
let eager_mode = matches!(current_mode, EditPredictionsMode::Eager);
|
||||
|
||||
if matches!(
|
||||
provider,
|
||||
EditPredictionProvider::Zed
|
||||
| EditPredictionProvider::Copilot
|
||||
| EditPredictionProvider::Supermaven
|
||||
| EditPredictionProvider::Codestral
|
||||
) {
|
||||
menu = menu
|
||||
menu = menu
|
||||
.separator()
|
||||
.header("Display Modes")
|
||||
.item(
|
||||
@@ -868,104 +734,111 @@ impl EditPredictionButton {
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
menu = menu.separator().header("Privacy");
|
||||
|
||||
if let Some(provider) = &self.edit_prediction_provider {
|
||||
let data_collection = provider.data_collection_state(cx);
|
||||
if matches!(
|
||||
provider,
|
||||
EditPredictionProvider::Zed
|
||||
| EditPredictionProvider::Experimental(
|
||||
EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME,
|
||||
)
|
||||
) {
|
||||
if let Some(provider) = &self.edit_prediction_provider {
|
||||
let data_collection = provider.data_collection_state(cx);
|
||||
|
||||
if data_collection.is_supported() {
|
||||
let provider = provider.clone();
|
||||
let enabled = data_collection.is_enabled();
|
||||
let is_open_source = data_collection.is_project_open_source();
|
||||
let is_collecting = data_collection.is_enabled();
|
||||
let (icon_name, icon_color) = if is_open_source && is_collecting {
|
||||
(IconName::Check, Color::Success)
|
||||
} else {
|
||||
(IconName::Check, Color::Accent)
|
||||
};
|
||||
if data_collection.is_supported() {
|
||||
let provider = provider.clone();
|
||||
let enabled = data_collection.is_enabled();
|
||||
let is_open_source = data_collection.is_project_open_source();
|
||||
let is_collecting = data_collection.is_enabled();
|
||||
let (icon_name, icon_color) = if is_open_source && is_collecting {
|
||||
(IconName::Check, Color::Success)
|
||||
} else {
|
||||
(IconName::Check, Color::Accent)
|
||||
};
|
||||
|
||||
menu = menu.item(
|
||||
ContextMenuEntry::new("Training Data Collection")
|
||||
.toggleable(IconPosition::Start, data_collection.is_enabled())
|
||||
.icon(icon_name)
|
||||
.icon_color(icon_color)
|
||||
.documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, move |cx| {
|
||||
let (msg, label_color, icon_name, icon_color) = match (is_open_source, is_collecting) {
|
||||
(true, true) => (
|
||||
"Project identified as open source, and you're sharing data.",
|
||||
Color::Default,
|
||||
IconName::Check,
|
||||
Color::Success,
|
||||
),
|
||||
(true, false) => (
|
||||
"Project identified as open source, but you're not sharing data.",
|
||||
Color::Muted,
|
||||
IconName::Close,
|
||||
Color::Muted,
|
||||
),
|
||||
(false, true) => (
|
||||
"Project not identified as open source. No data captured.",
|
||||
Color::Muted,
|
||||
IconName::Close,
|
||||
Color::Muted,
|
||||
),
|
||||
(false, false) => (
|
||||
"Project not identified as open source, and setting turned off.",
|
||||
Color::Muted,
|
||||
IconName::Close,
|
||||
Color::Muted,
|
||||
),
|
||||
};
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new(indoc!{
|
||||
"Help us improve our open dataset model by sharing data from open source repositories. \
|
||||
Zed must detect a license file in your repo for this setting to take effect. \
|
||||
Files with sensitive data and secrets are excluded by default."
|
||||
})
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.items_start()
|
||||
.pt_2()
|
||||
.pr_1()
|
||||
.flex_1()
|
||||
.gap_1p5()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(h_flex().flex_shrink_0().h(line_height).child(Icon::new(icon_name).size(IconSize::XSmall).color(icon_color)))
|
||||
.child(div().child(msg).w_full().text_sm().text_color(label_color.color(cx)))
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.handler(move |_, cx| {
|
||||
provider.toggle_data_collection(cx);
|
||||
|
||||
if !enabled {
|
||||
telemetry::event!(
|
||||
"Data Collection Enabled",
|
||||
source = "Edit Prediction Status Menu"
|
||||
);
|
||||
} else {
|
||||
telemetry::event!(
|
||||
"Data Collection Disabled",
|
||||
source = "Edit Prediction Status Menu"
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if is_collecting && !is_open_source {
|
||||
menu = menu.item(
|
||||
ContextMenuEntry::new("No data captured.")
|
||||
.disabled(true)
|
||||
.icon(IconName::Close)
|
||||
.icon_color(Color::Error)
|
||||
.icon_size(IconSize::Small),
|
||||
ContextMenuEntry::new("Training Data Collection")
|
||||
.toggleable(IconPosition::Start, data_collection.is_enabled())
|
||||
.icon(icon_name)
|
||||
.icon_color(icon_color)
|
||||
.documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, move |cx| {
|
||||
let (msg, label_color, icon_name, icon_color) = match (is_open_source, is_collecting) {
|
||||
(true, true) => (
|
||||
"Project identified as open source, and you're sharing data.",
|
||||
Color::Default,
|
||||
IconName::Check,
|
||||
Color::Success,
|
||||
),
|
||||
(true, false) => (
|
||||
"Project identified as open source, but you're not sharing data.",
|
||||
Color::Muted,
|
||||
IconName::Close,
|
||||
Color::Muted,
|
||||
),
|
||||
(false, true) => (
|
||||
"Project not identified as open source. No data captured.",
|
||||
Color::Muted,
|
||||
IconName::Close,
|
||||
Color::Muted,
|
||||
),
|
||||
(false, false) => (
|
||||
"Project not identified as open source, and setting turned off.",
|
||||
Color::Muted,
|
||||
IconName::Close,
|
||||
Color::Muted,
|
||||
),
|
||||
};
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new(indoc!{
|
||||
"Help us improve our open dataset model by sharing data from open source repositories. \
|
||||
Zed must detect a license file in your repo for this setting to take effect. \
|
||||
Files with sensitive data and secrets are excluded by default."
|
||||
})
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.items_start()
|
||||
.pt_2()
|
||||
.pr_1()
|
||||
.flex_1()
|
||||
.gap_1p5()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(h_flex().flex_shrink_0().h(line_height).child(Icon::new(icon_name).size(IconSize::XSmall).color(icon_color)))
|
||||
.child(div().child(msg).w_full().text_sm().text_color(label_color.color(cx)))
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.handler(move |_, cx| {
|
||||
provider.toggle_data_collection(cx);
|
||||
|
||||
if !enabled {
|
||||
telemetry::event!(
|
||||
"Data Collection Enabled",
|
||||
source = "Edit Prediction Status Menu"
|
||||
);
|
||||
} else {
|
||||
telemetry::event!(
|
||||
"Data Collection Disabled",
|
||||
source = "Edit Prediction Status Menu"
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if is_collecting && !is_open_source {
|
||||
menu = menu.item(
|
||||
ContextMenuEntry::new("No data captured.")
|
||||
.disabled(true)
|
||||
.icon(IconName::Close)
|
||||
.icon_color(Color::Error)
|
||||
.icon_size(IconSize::Small),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1087,10 +960,7 @@ impl EditPredictionButton {
|
||||
let menu =
|
||||
self.add_provider_switching_section(menu, EditPredictionProvider::Codestral, cx);
|
||||
|
||||
menu.separator()
|
||||
.entry("Configure Codestral API Key", None, move |window, cx| {
|
||||
window.dispatch_action(zed_actions::agent::OpenSettings.boxed_clone(), cx);
|
||||
})
|
||||
menu
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1210,6 +1080,22 @@ impl EditPredictionButton {
|
||||
}
|
||||
|
||||
menu = self.add_provider_switching_section(menu, provider, cx);
|
||||
menu = menu.separator().item(
|
||||
ContextMenuEntry::new("Configure Providers")
|
||||
.icon(IconName::Settings)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(
|
||||
OpenSettingsAt {
|
||||
path: "edit_predictions.providers".to_string(),
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
menu
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
mod edit_prediction_button;
|
||||
mod edit_prediction_context_view;
|
||||
mod external_provider_api_token_modal;
|
||||
mod rate_prediction_modal;
|
||||
|
||||
use std::any::{Any as _, TypeId};
|
||||
@@ -17,7 +16,6 @@ use ui::{App, prelude::*};
|
||||
use workspace::{SplitDirection, Workspace};
|
||||
|
||||
pub use edit_prediction_button::{EditPredictionButton, ToggleMenu};
|
||||
pub use external_provider_api_token_modal::ExternalProviderApiKeyModal;
|
||||
|
||||
use crate::rate_prediction_modal::PredictEditsRatePredictionsFeatureFlag;
|
||||
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
use edit_prediction::EditPredictionStore;
|
||||
use gpui::{
|
||||
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render,
|
||||
};
|
||||
use ui::{Button, ButtonStyle, Clickable, Headline, HeadlineSize, prelude::*};
|
||||
use ui_input::InputField;
|
||||
use workspace::ModalView;
|
||||
|
||||
pub struct ExternalProviderApiKeyModal {
|
||||
api_key_input: Entity<InputField>,
|
||||
focus_handle: FocusHandle,
|
||||
on_confirm: Box<dyn Fn(Option<String>, &mut EditPredictionStore, &mut App)>,
|
||||
}
|
||||
|
||||
impl ExternalProviderApiKeyModal {
|
||||
pub fn new(
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
on_confirm: impl Fn(Option<String>, &mut EditPredictionStore, &mut App) + 'static,
|
||||
) -> Self {
|
||||
let api_key_input = cx.new(|cx| InputField::new(window, cx, "Enter your API key"));
|
||||
|
||||
Self {
|
||||
api_key_input,
|
||||
focus_handle: cx.focus_handle(),
|
||||
on_confirm: Box::new(on_confirm),
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let api_key = self.api_key_input.read(cx).text(cx);
|
||||
let api_key = (!api_key.trim().is_empty()).then_some(api_key);
|
||||
|
||||
if let Some(ep_store) = EditPredictionStore::try_global(cx) {
|
||||
ep_store.update(cx, |ep_store, cx| (self.on_confirm)(api_key, ep_store, cx))
|
||||
}
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ExternalProviderApiKeyModal {}
|
||||
|
||||
impl ModalView for ExternalProviderApiKeyModal {}
|
||||
|
||||
impl Focusable for ExternalProviderApiKeyModal {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ExternalProviderApiKeyModal {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.key_context("ExternalApiKeyModal")
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.elevation_2(cx)
|
||||
.w(px(400.))
|
||||
.p_4()
|
||||
.gap_3()
|
||||
.child(Headline::new("API Token").size(HeadlineSize::Small))
|
||||
.child(self.api_key_input.clone())
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.gap_2()
|
||||
.child(Button::new("cancel", "Cancel").on_click(cx.listener(
|
||||
|_, _, _window, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
},
|
||||
)))
|
||||
.child(
|
||||
Button::new("save", "Save")
|
||||
.style(ButtonStyle::Filled)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.confirm(&menu::Confirm, window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ test-support = []
|
||||
[dependencies]
|
||||
anthropic = { workspace = true, features = ["schemars"] }
|
||||
anyhow.workspace = true
|
||||
credentials_provider.workspace = true
|
||||
base64.workspace = true
|
||||
client.workspace = true
|
||||
cloud_api_types.workspace = true
|
||||
@@ -41,6 +42,7 @@ smol.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
thiserror.workspace = true
|
||||
util.workspace = true
|
||||
zed_env_vars.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -2,7 +2,6 @@ use anyhow::{Result, anyhow};
|
||||
use credentials_provider::CredentialsProvider;
|
||||
use futures::{FutureExt, future};
|
||||
use gpui::{AsyncApp, Context, SharedString, Task};
|
||||
use language_model::AuthenticateError;
|
||||
use std::{
|
||||
fmt::{Display, Formatter},
|
||||
sync::Arc,
|
||||
@@ -10,13 +9,16 @@ use std::{
|
||||
use util::ResultExt as _;
|
||||
use zed_env_vars::EnvVar;
|
||||
|
||||
use crate::AuthenticateError;
|
||||
|
||||
/// Manages a single API key for a language model provider. API keys either come from environment
|
||||
/// variables or the system keychain.
|
||||
///
|
||||
/// Keys from the system keychain are associated with a provider URL, and this ensures that they are
|
||||
/// only used with that URL.
|
||||
pub struct ApiKeyState {
|
||||
url: SharedString,
|
||||
pub url: SharedString,
|
||||
env_var: EnvVar,
|
||||
load_status: LoadStatus,
|
||||
load_task: Option<future::Shared<Task<()>>>,
|
||||
}
|
||||
@@ -35,9 +37,10 @@ pub struct ApiKey {
|
||||
}
|
||||
|
||||
impl ApiKeyState {
|
||||
pub fn new(url: SharedString) -> Self {
|
||||
pub fn new(url: SharedString, env_var: EnvVar) -> Self {
|
||||
Self {
|
||||
url,
|
||||
env_var,
|
||||
load_status: LoadStatus::NotPresent,
|
||||
load_task: None,
|
||||
}
|
||||
@@ -47,6 +50,10 @@ impl ApiKeyState {
|
||||
matches!(self.load_status, LoadStatus::Loaded { .. })
|
||||
}
|
||||
|
||||
pub fn env_var_name(&self) -> &SharedString {
|
||||
&self.env_var.name
|
||||
}
|
||||
|
||||
pub fn is_from_env_var(&self) -> bool {
|
||||
match &self.load_status {
|
||||
LoadStatus::Loaded(ApiKey {
|
||||
@@ -136,14 +143,13 @@ impl ApiKeyState {
|
||||
pub fn handle_url_change<Ent: 'static>(
|
||||
&mut self,
|
||||
url: SharedString,
|
||||
env_var: &EnvVar,
|
||||
get_this: impl Fn(&mut Ent) -> &mut Self + Clone + 'static,
|
||||
cx: &mut Context<Ent>,
|
||||
) {
|
||||
if url != self.url {
|
||||
if !self.is_from_env_var() {
|
||||
// loading will continue even though this result task is dropped
|
||||
let _task = self.load_if_needed(url, env_var, get_this, cx);
|
||||
let _task = self.load_if_needed(url, get_this, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,7 +162,6 @@ impl ApiKeyState {
|
||||
pub fn load_if_needed<Ent: 'static>(
|
||||
&mut self,
|
||||
url: SharedString,
|
||||
env_var: &EnvVar,
|
||||
get_this: impl Fn(&mut Ent) -> &mut Self + Clone + 'static,
|
||||
cx: &mut Context<Ent>,
|
||||
) -> Task<Result<(), AuthenticateError>> {
|
||||
@@ -166,10 +171,10 @@ impl ApiKeyState {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
|
||||
if let Some(key) = &env_var.value
|
||||
if let Some(key) = &self.env_var.value
|
||||
&& !key.is_empty()
|
||||
{
|
||||
let api_key = ApiKey::from_env(env_var.name.clone(), key);
|
||||
let api_key = ApiKey::from_env(self.env_var.name.clone(), key);
|
||||
self.url = url;
|
||||
self.load_status = LoadStatus::Loaded(api_key);
|
||||
self.load_task = None;
|
||||
@@ -1,3 +1,4 @@
|
||||
mod api_key;
|
||||
mod model;
|
||||
mod rate_limiter;
|
||||
mod registry;
|
||||
@@ -30,6 +31,7 @@ use std::{fmt, io};
|
||||
use thiserror::Error;
|
||||
use util::serde::is_default;
|
||||
|
||||
pub use crate::api_key::{ApiKey, ApiKeyState};
|
||||
pub use crate::model::*;
|
||||
pub use crate::rate_limiter::*;
|
||||
pub use crate::registry::*;
|
||||
@@ -37,6 +39,7 @@ pub use crate::request::*;
|
||||
pub use crate::role::*;
|
||||
pub use crate::telemetry::*;
|
||||
pub use crate::tool_schema::LanguageModelToolSchemaFormat;
|
||||
pub use zed_env_vars::{EnvVar, env_var};
|
||||
|
||||
pub const ANTHROPIC_PROVIDER_ID: LanguageModelProviderId =
|
||||
LanguageModelProviderId::new("anthropic");
|
||||
|
||||
@@ -60,7 +60,6 @@ ui_input.workspace = true
|
||||
util.workspace = true
|
||||
vercel = { workspace = true, features = ["schemars"] }
|
||||
x_ai = { workspace = true, features = ["schemars"] }
|
||||
zed_env_vars.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -7,10 +7,8 @@ use gpui::{App, Context, Entity};
|
||||
use language_model::{LanguageModelProviderId, LanguageModelRegistry};
|
||||
use provider::deepseek::DeepSeekLanguageModelProvider;
|
||||
|
||||
mod api_key;
|
||||
pub mod provider;
|
||||
mod settings;
|
||||
pub mod ui;
|
||||
|
||||
use crate::provider::anthropic::AnthropicLanguageModelProvider;
|
||||
use crate::provider::bedrock::BedrockLanguageModelProvider;
|
||||
|
||||
@@ -8,25 +8,21 @@ use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture, stream::B
|
||||
use gpui::{AnyView, App, AsyncApp, Context, Entity, Task};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
AuthenticateError, ConfigurationViewTargetAgent, LanguageModel,
|
||||
LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelId,
|
||||
LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
|
||||
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
|
||||
LanguageModelToolResultContent, MessageContent, RateLimiter, Role,
|
||||
ApiKeyState, AuthenticateError, ConfigurationViewTargetAgent, EnvVar, LanguageModel,
|
||||
LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
|
||||
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
|
||||
LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, MessageContent,
|
||||
RateLimiter, Role, StopReason, env_var,
|
||||
};
|
||||
use language_model::{LanguageModelCompletionEvent, LanguageModelToolUse, StopReason};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::pin::Pin;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use strum::IntoEnumIterator;
|
||||
use ui::{List, prelude::*};
|
||||
use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
|
||||
use ui_input::InputField;
|
||||
use util::ResultExt;
|
||||
use zed_env_vars::{EnvVar, env_var};
|
||||
|
||||
use crate::api_key::ApiKeyState;
|
||||
use crate::ui::{ConfiguredApiCard, InstructionListItem};
|
||||
|
||||
pub use settings::AnthropicAvailableModel as AvailableModel;
|
||||
|
||||
@@ -65,12 +61,8 @@ impl State {
|
||||
|
||||
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
|
||||
let api_url = AnthropicLanguageModelProvider::api_url(cx);
|
||||
self.api_key_state.load_if_needed(
|
||||
api_url,
|
||||
&API_KEY_ENV_VAR,
|
||||
|this| &mut this.api_key_state,
|
||||
cx,
|
||||
)
|
||||
self.api_key_state
|
||||
.load_if_needed(api_url, |this| &mut this.api_key_state, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,17 +71,13 @@ impl AnthropicLanguageModelProvider {
|
||||
let state = cx.new(|cx| {
|
||||
cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
|
||||
let api_url = Self::api_url(cx);
|
||||
this.api_key_state.handle_url_change(
|
||||
api_url,
|
||||
&API_KEY_ENV_VAR,
|
||||
|this| &mut this.api_key_state,
|
||||
cx,
|
||||
);
|
||||
this.api_key_state
|
||||
.handle_url_change(api_url, |this| &mut this.api_key_state, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
State {
|
||||
api_key_state: ApiKeyState::new(Self::api_url(cx)),
|
||||
api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
|
||||
}
|
||||
});
|
||||
|
||||
@@ -937,14 +925,12 @@ impl Render for ConfigurationView {
|
||||
.child(
|
||||
List::new()
|
||||
.child(
|
||||
InstructionListItem::new(
|
||||
"Create one by visiting",
|
||||
Some("Anthropic's settings"),
|
||||
Some("https://console.anthropic.com/settings/keys")
|
||||
)
|
||||
ListBulletItem::new("")
|
||||
.child(Label::new("Create one by visiting"))
|
||||
.child(ButtonLink::new("Anthropic's settings", "https://console.anthropic.com/settings/keys"))
|
||||
)
|
||||
.child(
|
||||
InstructionListItem::text_only("Paste your API key below and hit enter to start using the agent")
|
||||
ListBulletItem::new("Paste your API key below and hit enter to start using the agent")
|
||||
)
|
||||
)
|
||||
.child(self.api_key_editor.clone())
|
||||
@@ -953,7 +939,8 @@ impl Render for ConfigurationView {
|
||||
format!("You can also assign the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed."),
|
||||
)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
.color(Color::Muted)
|
||||
.mt_0p5(),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::pin::Pin;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::ui::{ConfiguredApiCard, InstructionListItem};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use aws_config::stalled_stream_protection::StalledStreamProtectionConfig;
|
||||
use aws_config::{BehaviorVersion, Region};
|
||||
@@ -44,7 +43,7 @@ use serde_json::Value;
|
||||
use settings::{BedrockAvailableModel as AvailableModel, Settings, SettingsStore};
|
||||
use smol::lock::OnceCell;
|
||||
use strum::{EnumIter, IntoEnumIterator, IntoStaticStr};
|
||||
use ui::{List, prelude::*};
|
||||
use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
|
||||
use ui_input::InputField;
|
||||
use util::ResultExt;
|
||||
|
||||
@@ -1250,18 +1249,14 @@ impl Render for ConfigurationView {
|
||||
.child(
|
||||
List::new()
|
||||
.child(
|
||||
InstructionListItem::new(
|
||||
"Grant permissions to the strategy you'll use according to the:",
|
||||
Some("Prerequisites"),
|
||||
Some("https://docs.aws.amazon.com/bedrock/latest/userguide/inference-prereq.html"),
|
||||
)
|
||||
ListBulletItem::new("")
|
||||
.child(Label::new("Grant permissions to the strategy you'll use according to the:"))
|
||||
.child(ButtonLink::new("Prerequisites", "https://docs.aws.amazon.com/bedrock/latest/userguide/inference-prereq.html"))
|
||||
)
|
||||
.child(
|
||||
InstructionListItem::new(
|
||||
"Select the models you would like access to:",
|
||||
Some("Bedrock Model Catalog"),
|
||||
Some("https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/modelaccess"),
|
||||
)
|
||||
ListBulletItem::new("")
|
||||
.child(Label::new("Select the models you would like access to:"))
|
||||
.child(ButtonLink::new("Bedrock Model Catalog", "https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/modelaccess"))
|
||||
)
|
||||
)
|
||||
.child(self.render_static_credentials_ui())
|
||||
@@ -1302,22 +1297,22 @@ impl ConfigurationView {
|
||||
)
|
||||
.child(
|
||||
List::new()
|
||||
.child(InstructionListItem::new(
|
||||
"Create an IAM user in the AWS console with programmatic access",
|
||||
Some("IAM Console"),
|
||||
Some("https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1#/users"),
|
||||
))
|
||||
.child(InstructionListItem::new(
|
||||
"Attach the necessary Bedrock permissions to this ",
|
||||
Some("user"),
|
||||
Some("https://docs.aws.amazon.com/bedrock/latest/userguide/inference-prereq.html"),
|
||||
))
|
||||
.child(InstructionListItem::text_only(
|
||||
"Copy the access key ID and secret access key when provided",
|
||||
))
|
||||
.child(InstructionListItem::text_only(
|
||||
"Enter these credentials below",
|
||||
)),
|
||||
.child(
|
||||
ListBulletItem::new("")
|
||||
.child(Label::new("Create an IAM user in the AWS console with programmatic access"))
|
||||
.child(ButtonLink::new("IAM Console", "https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1#/users"))
|
||||
)
|
||||
.child(
|
||||
ListBulletItem::new("")
|
||||
.child(Label::new("Attach the necessary Bedrock permissions to this"))
|
||||
.child(ButtonLink::new("user", "https://docs.aws.amazon.com/bedrock/latest/userguide/inference-prereq.html"))
|
||||
)
|
||||
.child(
|
||||
ListBulletItem::new("Copy the access key ID and secret access key when provided")
|
||||
)
|
||||
.child(
|
||||
ListBulletItem::new("Enter these credentials below")
|
||||
)
|
||||
)
|
||||
.child(self.access_key_id_editor.clone())
|
||||
.child(self.secret_access_key_editor.clone())
|
||||
|
||||
@@ -14,7 +14,7 @@ use copilot::{Copilot, Status};
|
||||
use futures::future::BoxFuture;
|
||||
use futures::stream::BoxStream;
|
||||
use futures::{FutureExt, Stream, StreamExt};
|
||||
use gpui::{Action, AnyView, App, AsyncApp, Entity, Render, Subscription, Task, svg};
|
||||
use gpui::{AnyView, App, AsyncApp, Entity, Subscription, Task};
|
||||
use http_client::StatusCode;
|
||||
use language::language_settings::all_language_settings;
|
||||
use language_model::{
|
||||
@@ -26,11 +26,9 @@ use language_model::{
|
||||
StopReason, TokenUsage,
|
||||
};
|
||||
use settings::SettingsStore;
|
||||
use ui::{CommonAnimationExt, prelude::*};
|
||||
use ui::prelude::*;
|
||||
use util::debug_panic;
|
||||
|
||||
use crate::ui::ConfiguredApiCard;
|
||||
|
||||
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("copilot_chat");
|
||||
const PROVIDER_NAME: LanguageModelProviderName =
|
||||
LanguageModelProviderName::new("GitHub Copilot Chat");
|
||||
@@ -179,8 +177,18 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
|
||||
_: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyView {
|
||||
let state = self.state.clone();
|
||||
cx.new(|cx| ConfigurationView::new(state, cx)).into()
|
||||
cx.new(|cx| {
|
||||
copilot::ConfigurationView::new(
|
||||
|cx| {
|
||||
CopilotChat::global(cx)
|
||||
.map(|m| m.read(cx).is_authenticated())
|
||||
.unwrap_or(false)
|
||||
},
|
||||
copilot::ConfigurationMode::Chat,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
fn reset_credentials(&self, _cx: &mut App) -> Task<Result<()>> {
|
||||
@@ -1474,92 +1482,3 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
struct ConfigurationView {
|
||||
copilot_status: Option<copilot::Status>,
|
||||
state: Entity<State>,
|
||||
_subscription: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl ConfigurationView {
|
||||
pub fn new(state: Entity<State>, cx: &mut Context<Self>) -> Self {
|
||||
let copilot = Copilot::global(cx);
|
||||
|
||||
Self {
|
||||
copilot_status: copilot.as_ref().map(|copilot| copilot.read(cx).status()),
|
||||
state,
|
||||
_subscription: copilot.as_ref().map(|copilot| {
|
||||
cx.observe(copilot, |this, model, cx| {
|
||||
this.copilot_status = Some(model.read(cx).status());
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ConfigurationView {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
if self.state.read(cx).is_authenticated(cx) {
|
||||
ConfiguredApiCard::new("Authorized")
|
||||
.button_label("Sign Out")
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(copilot::SignOut.boxed_clone(), cx);
|
||||
})
|
||||
.into_any_element()
|
||||
} else {
|
||||
let loading_icon = Icon::new(IconName::ArrowCircle).with_rotate_animation(4);
|
||||
|
||||
const ERROR_LABEL: &str = "Copilot Chat requires an active GitHub Copilot subscription. Please ensure Copilot is configured and try again, or use a different Assistant provider.";
|
||||
|
||||
match &self.copilot_status {
|
||||
Some(status) => match status {
|
||||
Status::Starting { task: _ } => h_flex()
|
||||
.gap_2()
|
||||
.child(loading_icon)
|
||||
.child(Label::new("Starting Copilot…"))
|
||||
.into_any_element(),
|
||||
Status::SigningIn { prompt: _ }
|
||||
| Status::SignedOut {
|
||||
awaiting_signing_in: true,
|
||||
} => h_flex()
|
||||
.gap_2()
|
||||
.child(loading_icon)
|
||||
.child(Label::new("Signing into Copilot…"))
|
||||
.into_any_element(),
|
||||
Status::Error(_) => {
|
||||
const LABEL: &str = "Copilot had issues starting. Please try restarting it. If the issue persists, try reinstalling Copilot.";
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.child(Label::new(LABEL))
|
||||
.child(svg().size_8().path(IconName::CopilotError.path()))
|
||||
.into_any_element()
|
||||
}
|
||||
_ => {
|
||||
const LABEL: &str = "To use Zed's agent with GitHub Copilot, you need to be logged in to GitHub. Note that your GitHub account must have an active Copilot Chat subscription.";
|
||||
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(Label::new(LABEL))
|
||||
.child(
|
||||
Button::new("sign_in", "Sign in to use GitHub Copilot")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.icon_color(Color::Muted)
|
||||
.icon(IconName::Github)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(|_, window, cx| {
|
||||
copilot::initiate_sign_in(window, cx)
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
},
|
||||
None => v_flex()
|
||||
.gap_6()
|
||||
.child(Label::new(ERROR_LABEL))
|
||||
.into_any_element(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture, stream::BoxStream
|
||||
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
|
||||
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
|
||||
LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, MessageContent,
|
||||
RateLimiter, Role, StopReason, TokenUsage,
|
||||
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
|
||||
LanguageModelToolUse, MessageContent, RateLimiter, Role, StopReason, TokenUsage, env_var,
|
||||
};
|
||||
pub use settings::DeepseekAvailableModel as AvailableModel;
|
||||
use settings::{Settings, SettingsStore};
|
||||
@@ -19,13 +19,9 @@ use std::pin::Pin;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use ui::{List, prelude::*};
|
||||
use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
|
||||
use ui_input::InputField;
|
||||
use util::ResultExt;
|
||||
use zed_env_vars::{EnvVar, env_var};
|
||||
|
||||
use crate::ui::ConfiguredApiCard;
|
||||
use crate::{api_key::ApiKeyState, ui::InstructionListItem};
|
||||
|
||||
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("deepseek");
|
||||
const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("DeepSeek");
|
||||
@@ -67,12 +63,8 @@ impl State {
|
||||
|
||||
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
|
||||
let api_url = DeepSeekLanguageModelProvider::api_url(cx);
|
||||
self.api_key_state.load_if_needed(
|
||||
api_url,
|
||||
&API_KEY_ENV_VAR,
|
||||
|this| &mut this.api_key_state,
|
||||
cx,
|
||||
)
|
||||
self.api_key_state
|
||||
.load_if_needed(api_url, |this| &mut this.api_key_state, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,17 +73,13 @@ impl DeepSeekLanguageModelProvider {
|
||||
let state = cx.new(|cx| {
|
||||
cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
|
||||
let api_url = Self::api_url(cx);
|
||||
this.api_key_state.handle_url_change(
|
||||
api_url,
|
||||
&API_KEY_ENV_VAR,
|
||||
|this| &mut this.api_key_state,
|
||||
cx,
|
||||
);
|
||||
this.api_key_state
|
||||
.handle_url_change(api_url, |this| &mut this.api_key_state, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
State {
|
||||
api_key_state: ApiKeyState::new(Self::api_url(cx)),
|
||||
api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
|
||||
}
|
||||
});
|
||||
|
||||
@@ -632,12 +620,15 @@ impl Render for ConfigurationView {
|
||||
.child(Label::new("To use DeepSeek in Zed, you need an API key:"))
|
||||
.child(
|
||||
List::new()
|
||||
.child(InstructionListItem::new(
|
||||
"Get your API key from the",
|
||||
Some("DeepSeek console"),
|
||||
Some("https://platform.deepseek.com/api_keys"),
|
||||
))
|
||||
.child(InstructionListItem::text_only(
|
||||
.child(
|
||||
ListBulletItem::new("")
|
||||
.child(Label::new("Get your API key from the"))
|
||||
.child(ButtonLink::new(
|
||||
"DeepSeek console",
|
||||
"https://platform.deepseek.com/api_keys",
|
||||
)),
|
||||
)
|
||||
.child(ListBulletItem::new(
|
||||
"Paste your API key below and hit enter to start using the assistant",
|
||||
)),
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ use google_ai::{
|
||||
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
AuthenticateError, ConfigurationViewTargetAgent, LanguageModelCompletionError,
|
||||
AuthenticateError, ConfigurationViewTargetAgent, EnvVar, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelToolChoice, LanguageModelToolSchemaFormat,
|
||||
LanguageModelToolUse, LanguageModelToolUseId, MessageContent, StopReason,
|
||||
};
|
||||
@@ -28,14 +28,11 @@ use std::sync::{
|
||||
atomic::{self, AtomicU64},
|
||||
};
|
||||
use strum::IntoEnumIterator;
|
||||
use ui::{List, prelude::*};
|
||||
use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
|
||||
use ui_input::InputField;
|
||||
use util::ResultExt;
|
||||
use zed_env_vars::EnvVar;
|
||||
|
||||
use crate::api_key::ApiKey;
|
||||
use crate::api_key::ApiKeyState;
|
||||
use crate::ui::{ConfiguredApiCard, InstructionListItem};
|
||||
use language_model::{ApiKey, ApiKeyState};
|
||||
|
||||
const PROVIDER_ID: LanguageModelProviderId = language_model::GOOGLE_PROVIDER_ID;
|
||||
const PROVIDER_NAME: LanguageModelProviderName = language_model::GOOGLE_PROVIDER_NAME;
|
||||
@@ -87,12 +84,8 @@ impl State {
|
||||
|
||||
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
|
||||
let api_url = GoogleLanguageModelProvider::api_url(cx);
|
||||
self.api_key_state.load_if_needed(
|
||||
api_url,
|
||||
&API_KEY_ENV_VAR,
|
||||
|this| &mut this.api_key_state,
|
||||
cx,
|
||||
)
|
||||
self.api_key_state
|
||||
.load_if_needed(api_url, |this| &mut this.api_key_state, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,17 +94,13 @@ impl GoogleLanguageModelProvider {
|
||||
let state = cx.new(|cx| {
|
||||
cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
|
||||
let api_url = Self::api_url(cx);
|
||||
this.api_key_state.handle_url_change(
|
||||
api_url,
|
||||
&API_KEY_ENV_VAR,
|
||||
|this| &mut this.api_key_state,
|
||||
cx,
|
||||
);
|
||||
this.api_key_state
|
||||
.handle_url_change(api_url, |this| &mut this.api_key_state, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
State {
|
||||
api_key_state: ApiKeyState::new(Self::api_url(cx)),
|
||||
api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
|
||||
}
|
||||
});
|
||||
|
||||
@@ -873,14 +862,14 @@ impl Render for ConfigurationView {
|
||||
})))
|
||||
.child(
|
||||
List::new()
|
||||
.child(InstructionListItem::new(
|
||||
"Create one by visiting",
|
||||
Some("Google AI's console"),
|
||||
Some("https://aistudio.google.com/app/apikey"),
|
||||
))
|
||||
.child(InstructionListItem::text_only(
|
||||
"Paste your API key below and hit enter to start using the assistant",
|
||||
)),
|
||||
.child(
|
||||
ListBulletItem::new("")
|
||||
.child(Label::new("Create one by visiting"))
|
||||
.child(ButtonLink::new("Google AI's console", "https://aistudio.google.com/app/apikey"))
|
||||
)
|
||||
.child(
|
||||
ListBulletItem::new("Paste your API key below and hit enter to start using the agent")
|
||||
)
|
||||
)
|
||||
.child(self.api_key_editor.clone())
|
||||
.child(
|
||||
|
||||
@@ -20,11 +20,10 @@ use settings::{Settings, SettingsStore};
|
||||
use std::pin::Pin;
|
||||
use std::str::FromStr;
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
use ui::{ButtonLike, Indicator, List, prelude::*};
|
||||
use ui::{ButtonLike, Indicator, InlineCode, List, ListBulletItem, prelude::*};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::AllLanguageModelSettings;
|
||||
use crate::ui::InstructionListItem;
|
||||
|
||||
const LMSTUDIO_DOWNLOAD_URL: &str = "https://lmstudio.ai/download";
|
||||
const LMSTUDIO_CATALOG_URL: &str = "https://lmstudio.ai/models";
|
||||
@@ -686,12 +685,14 @@ impl Render for ConfigurationView {
|
||||
.child(
|
||||
v_flex().gap_1().child(Label::new(lmstudio_intro)).child(
|
||||
List::new()
|
||||
.child(InstructionListItem::text_only(
|
||||
.child(ListBulletItem::new(
|
||||
"LM Studio needs to be running with at least one model downloaded.",
|
||||
))
|
||||
.child(InstructionListItem::text_only(
|
||||
"To get your first model, try running `lms get qwen2.5-coder-7b`",
|
||||
)),
|
||||
.child(
|
||||
ListBulletItem::new("")
|
||||
.child(Label::new("To get your first model, try running"))
|
||||
.child(InlineCode::new("lms get qwen2.5-coder-7b")),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
|
||||
@@ -1,31 +1,27 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use collections::BTreeMap;
|
||||
use fs::Fs;
|
||||
|
||||
use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture, stream::BoxStream};
|
||||
use gpui::{AnyView, App, AsyncApp, Context, Entity, Global, SharedString, Task, Window};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
|
||||
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
|
||||
LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, MessageContent,
|
||||
RateLimiter, Role, StopReason, TokenUsage,
|
||||
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
|
||||
LanguageModelToolUse, MessageContent, RateLimiter, Role, StopReason, TokenUsage, env_var,
|
||||
};
|
||||
use mistral::{CODESTRAL_API_URL, MISTRAL_API_URL, StreamResponse};
|
||||
pub use mistral::{CODESTRAL_API_URL, MISTRAL_API_URL, StreamResponse};
|
||||
pub use settings::MistralAvailableModel as AvailableModel;
|
||||
use settings::{EditPredictionProvider, Settings, SettingsStore, update_settings_file};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::collections::HashMap;
|
||||
use std::pin::Pin;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use std::sync::{Arc, LazyLock, OnceLock};
|
||||
use strum::IntoEnumIterator;
|
||||
use ui::{List, prelude::*};
|
||||
use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
|
||||
use ui_input::InputField;
|
||||
use util::ResultExt;
|
||||
use zed_env_vars::{EnvVar, env_var};
|
||||
|
||||
use crate::ui::ConfiguredApiCard;
|
||||
use crate::{api_key::ApiKeyState, ui::InstructionListItem};
|
||||
|
||||
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("mistral");
|
||||
const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("Mistral");
|
||||
@@ -35,6 +31,7 @@ static API_KEY_ENV_VAR: LazyLock<EnvVar> = env_var!(API_KEY_ENV_VAR_NAME);
|
||||
|
||||
const CODESTRAL_API_KEY_ENV_VAR_NAME: &str = "CODESTRAL_API_KEY";
|
||||
static CODESTRAL_API_KEY_ENV_VAR: LazyLock<EnvVar> = env_var!(CODESTRAL_API_KEY_ENV_VAR_NAME);
|
||||
static CODESTRAL_API_KEY: OnceLock<Entity<ApiKeyState>> = OnceLock::new();
|
||||
|
||||
#[derive(Default, Clone, Debug, PartialEq)]
|
||||
pub struct MistralSettings {
|
||||
@@ -44,12 +41,22 @@ pub struct MistralSettings {
|
||||
|
||||
pub struct MistralLanguageModelProvider {
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
state: Entity<State>,
|
||||
pub state: Entity<State>,
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
api_key_state: ApiKeyState,
|
||||
codestral_api_key_state: ApiKeyState,
|
||||
codestral_api_key_state: Entity<ApiKeyState>,
|
||||
}
|
||||
|
||||
pub fn codestral_api_key(cx: &mut App) -> Entity<ApiKeyState> {
|
||||
return CODESTRAL_API_KEY
|
||||
.get_or_init(|| {
|
||||
cx.new(|_| {
|
||||
ApiKeyState::new(CODESTRAL_API_URL.into(), CODESTRAL_API_KEY_ENV_VAR.clone())
|
||||
})
|
||||
})
|
||||
.clone();
|
||||
}
|
||||
|
||||
impl State {
|
||||
@@ -63,39 +70,19 @@ impl State {
|
||||
.store(api_url, api_key, |this| &mut this.api_key_state, cx)
|
||||
}
|
||||
|
||||
fn set_codestral_api_key(
|
||||
&mut self,
|
||||
api_key: Option<String>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.codestral_api_key_state.store(
|
||||
CODESTRAL_API_URL.into(),
|
||||
api_key,
|
||||
|this| &mut this.codestral_api_key_state,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
|
||||
let api_url = MistralLanguageModelProvider::api_url(cx);
|
||||
self.api_key_state.load_if_needed(
|
||||
api_url,
|
||||
&API_KEY_ENV_VAR,
|
||||
|this| &mut this.api_key_state,
|
||||
cx,
|
||||
)
|
||||
self.api_key_state
|
||||
.load_if_needed(api_url, |this| &mut this.api_key_state, cx)
|
||||
}
|
||||
|
||||
fn authenticate_codestral(
|
||||
&mut self,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<(), AuthenticateError>> {
|
||||
self.codestral_api_key_state.load_if_needed(
|
||||
CODESTRAL_API_URL.into(),
|
||||
&CODESTRAL_API_KEY_ENV_VAR,
|
||||
|this| &mut this.codestral_api_key_state,
|
||||
cx,
|
||||
)
|
||||
self.codestral_api_key_state.update(cx, |state, cx| {
|
||||
state.load_if_needed(CODESTRAL_API_URL.into(), |state| state, cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,18 +103,14 @@ impl MistralLanguageModelProvider {
|
||||
let state = cx.new(|cx| {
|
||||
cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
|
||||
let api_url = Self::api_url(cx);
|
||||
this.api_key_state.handle_url_change(
|
||||
api_url,
|
||||
&API_KEY_ENV_VAR,
|
||||
|this| &mut this.api_key_state,
|
||||
cx,
|
||||
);
|
||||
this.api_key_state
|
||||
.handle_url_change(api_url, |this| &mut this.api_key_state, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
State {
|
||||
api_key_state: ApiKeyState::new(Self::api_url(cx)),
|
||||
codestral_api_key_state: ApiKeyState::new(CODESTRAL_API_URL.into()),
|
||||
api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
|
||||
codestral_api_key_state: codestral_api_key(cx),
|
||||
}
|
||||
});
|
||||
|
||||
@@ -142,7 +125,11 @@ impl MistralLanguageModelProvider {
|
||||
}
|
||||
|
||||
pub fn codestral_api_key(&self, url: &str, cx: &App) -> Option<Arc<str>> {
|
||||
self.state.read(cx).codestral_api_key_state.key(url)
|
||||
self.state
|
||||
.read(cx)
|
||||
.codestral_api_key_state
|
||||
.read(cx)
|
||||
.key(url)
|
||||
}
|
||||
|
||||
fn create_language_model(&self, model: mistral::Model) -> Arc<dyn LanguageModel> {
|
||||
@@ -159,7 +146,7 @@ impl MistralLanguageModelProvider {
|
||||
&crate::AllLanguageModelSettings::get_global(cx).mistral
|
||||
}
|
||||
|
||||
fn api_url(cx: &App) -> SharedString {
|
||||
pub fn api_url(cx: &App) -> SharedString {
|
||||
let api_url = &Self::settings(cx).api_url;
|
||||
if api_url.is_empty() {
|
||||
mistral::MISTRAL_API_URL.into()
|
||||
@@ -747,7 +734,6 @@ struct RawToolCall {
|
||||
|
||||
struct ConfigurationView {
|
||||
api_key_editor: Entity<InputField>,
|
||||
codestral_api_key_editor: Entity<InputField>,
|
||||
state: Entity<State>,
|
||||
load_credentials_task: Option<Task<()>>,
|
||||
}
|
||||
@@ -756,8 +742,6 @@ impl ConfigurationView {
|
||||
fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let api_key_editor =
|
||||
cx.new(|cx| InputField::new(window, cx, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"));
|
||||
let codestral_api_key_editor =
|
||||
cx.new(|cx| InputField::new(window, cx, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"));
|
||||
|
||||
cx.observe(&state, |_, _, cx| {
|
||||
cx.notify();
|
||||
@@ -774,12 +758,6 @@ impl ConfigurationView {
|
||||
// We don't log an error, because "not signed in" is also an error.
|
||||
let _ = task.await;
|
||||
}
|
||||
if let Some(task) = state
|
||||
.update(cx, |state, cx| state.authenticate_codestral(cx))
|
||||
.log_err()
|
||||
{
|
||||
let _ = task.await;
|
||||
}
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.load_credentials_task = None;
|
||||
@@ -791,7 +769,6 @@ impl ConfigurationView {
|
||||
|
||||
Self {
|
||||
api_key_editor,
|
||||
codestral_api_key_editor,
|
||||
state,
|
||||
load_credentials_task,
|
||||
}
|
||||
@@ -829,110 +806,9 @@ impl ConfigurationView {
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn save_codestral_api_key(
|
||||
&mut self,
|
||||
_: &menu::Confirm,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let api_key = self
|
||||
.codestral_api_key_editor
|
||||
.read(cx)
|
||||
.text(cx)
|
||||
.trim()
|
||||
.to_string();
|
||||
if api_key.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// url changes can cause the editor to be displayed again
|
||||
self.codestral_api_key_editor
|
||||
.update(cx, |editor, cx| editor.set_text("", window, cx));
|
||||
|
||||
let state = self.state.clone();
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
state
|
||||
.update(cx, |state, cx| {
|
||||
state.set_codestral_api_key(Some(api_key), cx)
|
||||
})?
|
||||
.await?;
|
||||
cx.update(|_window, cx| {
|
||||
set_edit_prediction_provider(EditPredictionProvider::Codestral, cx)
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn reset_codestral_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.codestral_api_key_editor
|
||||
.update(cx, |editor, cx| editor.set_text("", window, cx));
|
||||
|
||||
let state = self.state.clone();
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
state
|
||||
.update(cx, |state, cx| state.set_codestral_api_key(None, cx))?
|
||||
.await?;
|
||||
cx.update(|_window, cx| set_edit_prediction_provider(EditPredictionProvider::Zed, cx))
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn should_render_api_key_editor(&self, cx: &mut Context<Self>) -> bool {
|
||||
!self.state.read(cx).is_authenticated()
|
||||
}
|
||||
|
||||
fn render_codestral_api_key_editor(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let key_state = &self.state.read(cx).codestral_api_key_state;
|
||||
let should_show_editor = !key_state.has_key();
|
||||
let env_var_set = key_state.is_from_env_var();
|
||||
let configured_card_label = if env_var_set {
|
||||
format!("API key set in {CODESTRAL_API_KEY_ENV_VAR_NAME} environment variable")
|
||||
} else {
|
||||
"Codestral API key configured".to_string()
|
||||
};
|
||||
|
||||
if should_show_editor {
|
||||
v_flex()
|
||||
.id("codestral")
|
||||
.size_full()
|
||||
.mt_2()
|
||||
.on_action(cx.listener(Self::save_codestral_api_key))
|
||||
.child(Label::new(
|
||||
"To use Codestral as an edit prediction provider, \
|
||||
you need to add a Codestral-specific API key. Follow these steps:",
|
||||
))
|
||||
.child(
|
||||
List::new()
|
||||
.child(InstructionListItem::new(
|
||||
"Create one by visiting",
|
||||
Some("the Codestral section of Mistral's console"),
|
||||
Some("https://console.mistral.ai/codestral"),
|
||||
))
|
||||
.child(InstructionListItem::text_only("Paste your API key below and hit enter")),
|
||||
)
|
||||
.child(self.codestral_api_key_editor.clone())
|
||||
.child(
|
||||
Label::new(
|
||||
format!("You can also assign the {CODESTRAL_API_KEY_ENV_VAR_NAME} environment variable and restart Zed."),
|
||||
)
|
||||
.size(LabelSize::Small).color(Color::Muted),
|
||||
).into_any()
|
||||
} else {
|
||||
ConfiguredApiCard::new(configured_card_label)
|
||||
.disabled(env_var_set)
|
||||
.on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
|
||||
.when(env_var_set, |this| {
|
||||
this.tooltip_label(format!(
|
||||
"To reset your API key, \
|
||||
unset the {CODESTRAL_API_KEY_ENV_VAR_NAME} environment variable."
|
||||
))
|
||||
})
|
||||
.on_click(
|
||||
cx.listener(|this, _, window, cx| this.reset_codestral_api_key(window, cx)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ConfigurationView {
|
||||
@@ -958,17 +834,17 @@ impl Render for ConfigurationView {
|
||||
.child(Label::new("To use Zed's agent with Mistral, you need to add an API key. Follow these steps:"))
|
||||
.child(
|
||||
List::new()
|
||||
.child(InstructionListItem::new(
|
||||
"Create one by visiting",
|
||||
Some("Mistral's console"),
|
||||
Some("https://console.mistral.ai/api-keys"),
|
||||
))
|
||||
.child(InstructionListItem::text_only(
|
||||
"Ensure your Mistral account has credits",
|
||||
))
|
||||
.child(InstructionListItem::text_only(
|
||||
"Paste your API key below and hit enter to start using the assistant",
|
||||
)),
|
||||
.child(
|
||||
ListBulletItem::new("")
|
||||
.child(Label::new("Create one by visiting"))
|
||||
.child(ButtonLink::new("Mistral's console", "https://console.mistral.ai/api-keys"))
|
||||
)
|
||||
.child(
|
||||
ListBulletItem::new("Ensure your Mistral account has credits")
|
||||
)
|
||||
.child(
|
||||
ListBulletItem::new("Paste your API key below and hit enter to start using the assistant")
|
||||
),
|
||||
)
|
||||
.child(self.api_key_editor.clone())
|
||||
.child(
|
||||
@@ -977,7 +853,6 @@ impl Render for ConfigurationView {
|
||||
)
|
||||
.size(LabelSize::Small).color(Color::Muted),
|
||||
)
|
||||
.child(self.render_codestral_api_key_editor(cx))
|
||||
.into_any()
|
||||
} else {
|
||||
v_flex()
|
||||
@@ -994,24 +869,11 @@ impl Render for ConfigurationView {
|
||||
))
|
||||
}),
|
||||
)
|
||||
.child(self.render_codestral_api_key_editor(cx))
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_edit_prediction_provider(provider: EditPredictionProvider, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
settings
|
||||
.project
|
||||
.all_languages
|
||||
.features
|
||||
.get_or_insert_default()
|
||||
.edit_prediction_provider = Some(provider);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -5,11 +5,11 @@ use futures::{Stream, TryFutureExt, stream};
|
||||
use gpui::{AnyView, App, AsyncApp, Context, CursorStyle, Entity, Task};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
|
||||
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
|
||||
LanguageModelRequestTool, LanguageModelToolChoice, LanguageModelToolUse,
|
||||
LanguageModelToolUseId, MessageContent, RateLimiter, Role, StopReason, TokenUsage,
|
||||
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelRequestTool, LanguageModelToolChoice, LanguageModelToolUse,
|
||||
LanguageModelToolUseId, MessageContent, RateLimiter, Role, StopReason, TokenUsage, env_var,
|
||||
};
|
||||
use menu;
|
||||
use ollama::{
|
||||
@@ -22,13 +22,13 @@ use std::pin::Pin;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use ui::{ButtonLike, ElevationIndex, List, Tooltip, prelude::*};
|
||||
use ui::{
|
||||
ButtonLike, ButtonLink, ConfiguredApiCard, ElevationIndex, InlineCode, List, ListBulletItem,
|
||||
Tooltip, prelude::*,
|
||||
};
|
||||
use ui_input::InputField;
|
||||
use zed_env_vars::{EnvVar, env_var};
|
||||
|
||||
use crate::AllLanguageModelSettings;
|
||||
use crate::api_key::ApiKeyState;
|
||||
use crate::ui::{ConfiguredApiCard, InstructionListItem};
|
||||
|
||||
const OLLAMA_DOWNLOAD_URL: &str = "https://ollama.com/download";
|
||||
const OLLAMA_LIBRARY_URL: &str = "https://ollama.com/library";
|
||||
@@ -80,12 +80,9 @@ impl State {
|
||||
|
||||
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
|
||||
let api_url = OllamaLanguageModelProvider::api_url(cx);
|
||||
let task = self.api_key_state.load_if_needed(
|
||||
api_url,
|
||||
&API_KEY_ENV_VAR,
|
||||
|this| &mut this.api_key_state,
|
||||
cx,
|
||||
);
|
||||
let task = self
|
||||
.api_key_state
|
||||
.load_if_needed(api_url, |this| &mut this.api_key_state, cx);
|
||||
|
||||
// Always try to fetch models - if no API key is needed (local Ollama), it will work
|
||||
// If API key is needed and provided, it will work
|
||||
@@ -185,7 +182,7 @@ impl OllamaLanguageModelProvider {
|
||||
http_client,
|
||||
fetched_models: Default::default(),
|
||||
fetch_model_task: None,
|
||||
api_key_state: ApiKeyState::new(Self::api_url(cx)),
|
||||
api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
|
||||
}
|
||||
}),
|
||||
};
|
||||
@@ -733,15 +730,17 @@ impl ConfigurationView {
|
||||
.child(Label::new("To use local Ollama:"))
|
||||
.child(
|
||||
List::new()
|
||||
.child(InstructionListItem::new(
|
||||
"Download and install Ollama from",
|
||||
Some("ollama.com"),
|
||||
Some("https://ollama.com/download"),
|
||||
))
|
||||
.child(InstructionListItem::text_only(
|
||||
"Start Ollama and download a model: `ollama run gpt-oss:20b`",
|
||||
))
|
||||
.child(InstructionListItem::text_only(
|
||||
.child(
|
||||
ListBulletItem::new("")
|
||||
.child(Label::new("Download and install Ollama from"))
|
||||
.child(ButtonLink::new("ollama.com", "https://ollama.com/download")),
|
||||
)
|
||||
.child(
|
||||
ListBulletItem::new("")
|
||||
.child(Label::new("Start Ollama and download a model:"))
|
||||
.child(InlineCode::new("ollama run gpt-oss:20b")),
|
||||
)
|
||||
.child(ListBulletItem::new(
|
||||
"Click 'Connect' below to start using Ollama in Zed",
|
||||
)),
|
||||
)
|
||||
|
||||
@@ -5,11 +5,11 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture};
|
||||
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
|
||||
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
|
||||
LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, MessageContent,
|
||||
RateLimiter, Role, StopReason, TokenUsage,
|
||||
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
|
||||
LanguageModelToolUse, MessageContent, RateLimiter, Role, StopReason, TokenUsage, env_var,
|
||||
};
|
||||
use menu;
|
||||
use open_ai::{
|
||||
@@ -20,13 +20,9 @@ use std::pin::Pin;
|
||||
use std::str::FromStr as _;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use strum::IntoEnumIterator;
|
||||
use ui::{List, prelude::*};
|
||||
use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
|
||||
use ui_input::InputField;
|
||||
use util::ResultExt;
|
||||
use zed_env_vars::{EnvVar, env_var};
|
||||
|
||||
use crate::ui::ConfiguredApiCard;
|
||||
use crate::{api_key::ApiKeyState, ui::InstructionListItem};
|
||||
|
||||
const PROVIDER_ID: LanguageModelProviderId = language_model::OPEN_AI_PROVIDER_ID;
|
||||
const PROVIDER_NAME: LanguageModelProviderName = language_model::OPEN_AI_PROVIDER_NAME;
|
||||
@@ -62,12 +58,8 @@ impl State {
|
||||
|
||||
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
|
||||
let api_url = OpenAiLanguageModelProvider::api_url(cx);
|
||||
self.api_key_state.load_if_needed(
|
||||
api_url,
|
||||
&API_KEY_ENV_VAR,
|
||||
|this| &mut this.api_key_state,
|
||||
cx,
|
||||
)
|
||||
self.api_key_state
|
||||
.load_if_needed(api_url, |this| &mut this.api_key_state, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,17 +68,13 @@ impl OpenAiLanguageModelProvider {
|
||||
let state = cx.new(|cx| {
|
||||
cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
|
||||
let api_url = Self::api_url(cx);
|
||||
this.api_key_state.handle_url_change(
|
||||
api_url,
|
||||
&API_KEY_ENV_VAR,
|
||||
|this| &mut this.api_key_state,
|
||||
cx,
|
||||
);
|
||||
this.api_key_state
|
||||
.handle_url_change(api_url, |this| &mut this.api_key_state, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
State {
|
||||
api_key_state: ApiKeyState::new(Self::api_url(cx)),
|
||||
api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
|
||||
}
|
||||
});
|
||||
|
||||
@@ -790,17 +778,17 @@ impl Render for ConfigurationView {
|
||||
.child(Label::new("To use Zed's agent with OpenAI, you need to add an API key. Follow these steps:"))
|
||||
.child(
|
||||
List::new()
|
||||
.child(InstructionListItem::new(
|
||||
"Create one by visiting",
|
||||
Some("OpenAI's console"),
|
||||
Some("https://platform.openai.com/api-keys"),
|
||||
))
|
||||
.child(InstructionListItem::text_only(
|
||||
"Ensure your OpenAI account has credits",
|
||||
))
|
||||
.child(InstructionListItem::text_only(
|
||||
"Paste your API key below and hit enter to start using the assistant",
|
||||
)),
|
||||
.child(
|
||||
ListBulletItem::new("")
|
||||
.child(Label::new("Create one by visiting"))
|
||||
.child(ButtonLink::new("OpenAI's console", "https://platform.openai.com/api-keys"))
|
||||
)
|
||||
.child(
|
||||
ListBulletItem::new("Ensure your OpenAI account has credits")
|
||||
)
|
||||
.child(
|
||||
ListBulletItem::new("Paste your API key below and hit enter to start using the agent")
|
||||
),
|
||||
)
|
||||
.child(self.api_key_editor.clone())
|
||||
.child(
|
||||
|
||||
@@ -4,10 +4,10 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture};
|
||||
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
|
||||
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
|
||||
LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter,
|
||||
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter,
|
||||
};
|
||||
use menu;
|
||||
use open_ai::{ResponseStreamEvent, stream_completion};
|
||||
@@ -16,9 +16,7 @@ use std::sync::Arc;
|
||||
use ui::{ElevationIndex, Tooltip, prelude::*};
|
||||
use ui_input::InputField;
|
||||
use util::ResultExt;
|
||||
use zed_env_vars::EnvVar;
|
||||
|
||||
use crate::api_key::ApiKeyState;
|
||||
use crate::provider::open_ai::{OpenAiEventMapper, into_open_ai};
|
||||
pub use settings::OpenAiCompatibleAvailableModel as AvailableModel;
|
||||
pub use settings::OpenAiCompatibleModelCapabilities as ModelCapabilities;
|
||||
@@ -38,7 +36,6 @@ pub struct OpenAiCompatibleLanguageModelProvider {
|
||||
|
||||
pub struct State {
|
||||
id: Arc<str>,
|
||||
api_key_env_var: EnvVar,
|
||||
api_key_state: ApiKeyState,
|
||||
settings: OpenAiCompatibleSettings,
|
||||
}
|
||||
@@ -56,12 +53,8 @@ impl State {
|
||||
|
||||
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
|
||||
let api_url = SharedString::new(self.settings.api_url.clone());
|
||||
self.api_key_state.load_if_needed(
|
||||
api_url,
|
||||
&self.api_key_env_var,
|
||||
|this| &mut this.api_key_state,
|
||||
cx,
|
||||
)
|
||||
self.api_key_state
|
||||
.load_if_needed(api_url, |this| &mut this.api_key_state, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +76,6 @@ impl OpenAiCompatibleLanguageModelProvider {
|
||||
let api_url = SharedString::new(settings.api_url.as_str());
|
||||
this.api_key_state.handle_url_change(
|
||||
api_url,
|
||||
&this.api_key_env_var,
|
||||
|this| &mut this.api_key_state,
|
||||
cx,
|
||||
);
|
||||
@@ -95,8 +87,10 @@ impl OpenAiCompatibleLanguageModelProvider {
|
||||
let settings = resolve_settings(&id, cx).cloned().unwrap_or_default();
|
||||
State {
|
||||
id: id.clone(),
|
||||
api_key_env_var: EnvVar::new(api_key_env_var_name),
|
||||
api_key_state: ApiKeyState::new(SharedString::new(settings.api_url.as_str())),
|
||||
api_key_state: ApiKeyState::new(
|
||||
SharedString::new(settings.api_url.as_str()),
|
||||
EnvVar::new(api_key_env_var_name),
|
||||
),
|
||||
settings,
|
||||
}
|
||||
});
|
||||
@@ -437,7 +431,7 @@ impl Render for ConfigurationView {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let state = self.state.read(cx);
|
||||
let env_var_set = state.api_key_state.is_from_env_var();
|
||||
let env_var_name = &state.api_key_env_var.name;
|
||||
let env_var_name = state.api_key_state.env_var_name();
|
||||
|
||||
let api_key_section = if self.should_render_editor(cx) {
|
||||
v_flex()
|
||||
|
||||
@@ -4,11 +4,12 @@ use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture};
|
||||
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
|
||||
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
|
||||
LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolSchemaFormat,
|
||||
LanguageModelToolUse, MessageContent, RateLimiter, Role, StopReason, TokenUsage,
|
||||
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
|
||||
LanguageModelToolSchemaFormat, LanguageModelToolUse, MessageContent, RateLimiter, Role,
|
||||
StopReason, TokenUsage, env_var,
|
||||
};
|
||||
use open_router::{
|
||||
Model, ModelMode as OpenRouterModelMode, OPEN_ROUTER_API_URL, ResponseStreamEvent, list_models,
|
||||
@@ -17,13 +18,9 @@ use settings::{OpenRouterAvailableModel as AvailableModel, Settings, SettingsSto
|
||||
use std::pin::Pin;
|
||||
use std::str::FromStr as _;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use ui::{List, prelude::*};
|
||||
use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
|
||||
use ui_input::InputField;
|
||||
use util::ResultExt;
|
||||
use zed_env_vars::{EnvVar, env_var};
|
||||
|
||||
use crate::ui::ConfiguredApiCard;
|
||||
use crate::{api_key::ApiKeyState, ui::InstructionListItem};
|
||||
|
||||
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("openrouter");
|
||||
const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("OpenRouter");
|
||||
@@ -62,12 +59,9 @@ impl State {
|
||||
|
||||
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
|
||||
let api_url = OpenRouterLanguageModelProvider::api_url(cx);
|
||||
let task = self.api_key_state.load_if_needed(
|
||||
api_url,
|
||||
&API_KEY_ENV_VAR,
|
||||
|this| &mut this.api_key_state,
|
||||
cx,
|
||||
);
|
||||
let task = self
|
||||
.api_key_state
|
||||
.load_if_needed(api_url, |this| &mut this.api_key_state, cx);
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let result = task.await;
|
||||
@@ -135,7 +129,7 @@ impl OpenRouterLanguageModelProvider {
|
||||
})
|
||||
.detach();
|
||||
State {
|
||||
api_key_state: ApiKeyState::new(Self::api_url(cx)),
|
||||
api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
|
||||
http_client: http_client.clone(),
|
||||
available_models: Vec::new(),
|
||||
fetch_models_task: None,
|
||||
@@ -830,17 +824,15 @@ impl Render for ConfigurationView {
|
||||
.child(Label::new("To use Zed's agent with OpenRouter, you need to add an API key. Follow these steps:"))
|
||||
.child(
|
||||
List::new()
|
||||
.child(InstructionListItem::new(
|
||||
"Create an API key by visiting",
|
||||
Some("OpenRouter's console"),
|
||||
Some("https://openrouter.ai/keys"),
|
||||
))
|
||||
.child(InstructionListItem::text_only(
|
||||
"Ensure your OpenRouter account has credits",
|
||||
))
|
||||
.child(InstructionListItem::text_only(
|
||||
"Paste your API key below and hit enter to start using the assistant",
|
||||
)),
|
||||
.child(
|
||||
ListBulletItem::new("")
|
||||
.child(Label::new("Create an API key by visiting"))
|
||||
.child(ButtonLink::new("OpenRouter's console", "https://openrouter.ai/keys"))
|
||||
)
|
||||
.child(ListBulletItem::new("Ensure your OpenRouter account has credits")
|
||||
)
|
||||
.child(ListBulletItem::new("Paste your API key below and hit enter to start using the assistant")
|
||||
),
|
||||
)
|
||||
.child(self.api_key_editor.clone())
|
||||
.child(
|
||||
|
||||
@@ -4,26 +4,20 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture};
|
||||
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
|
||||
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
|
||||
LanguageModelToolChoice, RateLimiter, Role,
|
||||
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelToolChoice, RateLimiter, Role, env_var,
|
||||
};
|
||||
use open_ai::ResponseStreamEvent;
|
||||
pub use settings::VercelAvailableModel as AvailableModel;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use strum::IntoEnumIterator;
|
||||
use ui::{List, prelude::*};
|
||||
use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
|
||||
use ui_input::InputField;
|
||||
use util::ResultExt;
|
||||
use vercel::{Model, VERCEL_API_URL};
|
||||
use zed_env_vars::{EnvVar, env_var};
|
||||
|
||||
use crate::{
|
||||
api_key::ApiKeyState,
|
||||
ui::{ConfiguredApiCard, InstructionListItem},
|
||||
};
|
||||
|
||||
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("vercel");
|
||||
const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("Vercel");
|
||||
@@ -59,12 +53,8 @@ impl State {
|
||||
|
||||
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
|
||||
let api_url = VercelLanguageModelProvider::api_url(cx);
|
||||
self.api_key_state.load_if_needed(
|
||||
api_url,
|
||||
&API_KEY_ENV_VAR,
|
||||
|this| &mut this.api_key_state,
|
||||
cx,
|
||||
)
|
||||
self.api_key_state
|
||||
.load_if_needed(api_url, |this| &mut this.api_key_state, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,17 +63,13 @@ impl VercelLanguageModelProvider {
|
||||
let state = cx.new(|cx| {
|
||||
cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
|
||||
let api_url = Self::api_url(cx);
|
||||
this.api_key_state.handle_url_change(
|
||||
api_url,
|
||||
&API_KEY_ENV_VAR,
|
||||
|this| &mut this.api_key_state,
|
||||
cx,
|
||||
);
|
||||
this.api_key_state
|
||||
.handle_url_change(api_url, |this| &mut this.api_key_state, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
State {
|
||||
api_key_state: ApiKeyState::new(Self::api_url(cx)),
|
||||
api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
|
||||
}
|
||||
});
|
||||
|
||||
@@ -472,14 +458,14 @@ impl Render for ConfigurationView {
|
||||
.child(Label::new("To use Zed's agent with Vercel v0, you need to add an API key. Follow these steps:"))
|
||||
.child(
|
||||
List::new()
|
||||
.child(InstructionListItem::new(
|
||||
"Create one by visiting",
|
||||
Some("Vercel v0's console"),
|
||||
Some("https://v0.dev/chat/settings/keys"),
|
||||
))
|
||||
.child(InstructionListItem::text_only(
|
||||
"Paste your API key below and hit enter to start using the agent",
|
||||
)),
|
||||
.child(
|
||||
ListBulletItem::new("")
|
||||
.child(Label::new("Create one by visiting"))
|
||||
.child(ButtonLink::new("Vercel v0's console", "https://v0.dev/chat/settings/keys"))
|
||||
)
|
||||
.child(
|
||||
ListBulletItem::new("Paste your API key below and hit enter to start using the agent")
|
||||
),
|
||||
)
|
||||
.child(self.api_key_editor.clone())
|
||||
.child(
|
||||
|
||||
@@ -4,26 +4,21 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture};
|
||||
use gpui::{AnyView, App, AsyncApp, Context, Entity, Task, Window};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
|
||||
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
|
||||
LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter, Role,
|
||||
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter,
|
||||
Role, env_var,
|
||||
};
|
||||
use open_ai::ResponseStreamEvent;
|
||||
pub use settings::XaiAvailableModel as AvailableModel;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use strum::IntoEnumIterator;
|
||||
use ui::{List, prelude::*};
|
||||
use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
|
||||
use ui_input::InputField;
|
||||
use util::ResultExt;
|
||||
use x_ai::{Model, XAI_API_URL};
|
||||
use zed_env_vars::{EnvVar, env_var};
|
||||
|
||||
use crate::{
|
||||
api_key::ApiKeyState,
|
||||
ui::{ConfiguredApiCard, InstructionListItem},
|
||||
};
|
||||
|
||||
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("x_ai");
|
||||
const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("xAI");
|
||||
@@ -59,12 +54,8 @@ impl State {
|
||||
|
||||
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
|
||||
let api_url = XAiLanguageModelProvider::api_url(cx);
|
||||
self.api_key_state.load_if_needed(
|
||||
api_url,
|
||||
&API_KEY_ENV_VAR,
|
||||
|this| &mut this.api_key_state,
|
||||
cx,
|
||||
)
|
||||
self.api_key_state
|
||||
.load_if_needed(api_url, |this| &mut this.api_key_state, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,17 +64,13 @@ impl XAiLanguageModelProvider {
|
||||
let state = cx.new(|cx| {
|
||||
cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
|
||||
let api_url = Self::api_url(cx);
|
||||
this.api_key_state.handle_url_change(
|
||||
api_url,
|
||||
&API_KEY_ENV_VAR,
|
||||
|this| &mut this.api_key_state,
|
||||
cx,
|
||||
);
|
||||
this.api_key_state
|
||||
.handle_url_change(api_url, |this| &mut this.api_key_state, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
State {
|
||||
api_key_state: ApiKeyState::new(Self::api_url(cx)),
|
||||
api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
|
||||
}
|
||||
});
|
||||
|
||||
@@ -474,14 +461,14 @@ impl Render for ConfigurationView {
|
||||
.child(Label::new("To use Zed's agent with xAI, you need to add an API key. Follow these steps:"))
|
||||
.child(
|
||||
List::new()
|
||||
.child(InstructionListItem::new(
|
||||
"Create one by visiting",
|
||||
Some("xAI console"),
|
||||
Some("https://console.x.ai/team/default/api-keys"),
|
||||
))
|
||||
.child(InstructionListItem::text_only(
|
||||
"Paste your API key below and hit enter to start using the agent",
|
||||
)),
|
||||
.child(
|
||||
ListBulletItem::new("")
|
||||
.child(Label::new("Create one by visiting"))
|
||||
.child(ButtonLink::new("xAI console", "https://console.x.ai/team/default/api-keys"))
|
||||
)
|
||||
.child(
|
||||
ListBulletItem::new("Paste your API key below and hit enter to start using the agent")
|
||||
),
|
||||
)
|
||||
.child(self.api_key_editor.clone())
|
||||
.child(
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
pub mod configured_api_card;
|
||||
pub mod instruction_list_item;
|
||||
pub use configured_api_card::ConfiguredApiCard;
|
||||
pub use instruction_list_item::InstructionListItem;
|
||||
@@ -1,69 +0,0 @@
|
||||
use gpui::{AnyElement, IntoElement, ParentElement, SharedString};
|
||||
use ui::{ListItem, prelude::*};
|
||||
|
||||
/// A reusable list item component for adding LLM provider configuration instructions
|
||||
pub struct InstructionListItem {
|
||||
label: SharedString,
|
||||
button_label: Option<SharedString>,
|
||||
button_link: Option<String>,
|
||||
}
|
||||
|
||||
impl InstructionListItem {
|
||||
pub fn new(
|
||||
label: impl Into<SharedString>,
|
||||
button_label: Option<impl Into<SharedString>>,
|
||||
button_link: Option<impl Into<String>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
button_label: button_label.map(|l| l.into()),
|
||||
button_link: button_link.map(|l| l.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text_only(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
button_label: None,
|
||||
button_link: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for InstructionListItem {
|
||||
type Element = AnyElement;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
let item_content = if let (Some(button_label), Some(button_link)) =
|
||||
(self.button_label, self.button_link)
|
||||
{
|
||||
let link = button_link;
|
||||
let unique_id = SharedString::from(format!("{}-button", self.label));
|
||||
|
||||
h_flex()
|
||||
.flex_wrap()
|
||||
.child(Label::new(self.label))
|
||||
.child(
|
||||
Button::new(unique_id, button_label)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(move |_, _window, cx| cx.open_url(&link)),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
Label::new(self.label).into_any_element()
|
||||
};
|
||||
|
||||
ListItem::new("list-item")
|
||||
.selectable(false)
|
||||
.start_slot(
|
||||
Icon::new(IconName::Dash)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Hidden),
|
||||
)
|
||||
.child(div().w_full().child(item_content))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
@@ -186,22 +186,20 @@ pub struct CopilotSettingsContent {
|
||||
pub enterprise_uri: Option<String>,
|
||||
}
|
||||
|
||||
#[with_fallible_options]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
|
||||
pub struct CodestralSettingsContent {
|
||||
/// Model to use for completions.
|
||||
///
|
||||
/// Default: "codestral-latest"
|
||||
#[serde(default)]
|
||||
pub model: Option<String>,
|
||||
/// Maximum tokens to generate.
|
||||
///
|
||||
/// Default: 150
|
||||
#[serde(default)]
|
||||
pub max_tokens: Option<u32>,
|
||||
/// Api URL to use for completions.
|
||||
///
|
||||
/// Default: "https://codestral.mistral.ai"
|
||||
#[serde(default)]
|
||||
pub api_url: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@ test-support = []
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
bm25 = "2.3.2"
|
||||
copilot.workspace = true
|
||||
edit_prediction.workspace = true
|
||||
language_models.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
@@ -38,8 +41,8 @@ strum.workspace = true
|
||||
telemetry.workspace = true
|
||||
theme.workspace = true
|
||||
title_bar.workspace = true
|
||||
ui.workspace = true
|
||||
ui_input.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
@@ -2,10 +2,12 @@ mod dropdown;
|
||||
mod font_picker;
|
||||
mod icon_theme_picker;
|
||||
mod input_field;
|
||||
mod section_items;
|
||||
mod theme_picker;
|
||||
|
||||
pub use dropdown::*;
|
||||
pub use font_picker::font_picker;
|
||||
pub use icon_theme_picker::icon_theme_picker;
|
||||
pub use input_field::*;
|
||||
pub use section_items::*;
|
||||
pub use theme_picker::theme_picker;
|
||||
|
||||
@@ -13,6 +13,7 @@ pub struct SettingsInputField {
|
||||
tab_index: Option<isize>,
|
||||
}
|
||||
|
||||
// TODO: Update the `ui_input::InputField` to use `window.use_state` and `RenceOnce` and remove this component
|
||||
impl SettingsInputField {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
||||
56
crates/settings_ui/src/components/section_items.rs
Normal file
56
crates/settings_ui/src/components/section_items.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use gpui::{IntoElement, ParentElement, Styled};
|
||||
use ui::{Divider, DividerColor, prelude::*};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct SettingsSectionHeader {
|
||||
icon: Option<IconName>,
|
||||
label: SharedString,
|
||||
no_padding: bool,
|
||||
}
|
||||
|
||||
impl SettingsSectionHeader {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
icon: None,
|
||||
no_padding: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon(mut self, icon: IconName) -> Self {
|
||||
self.icon = Some(icon);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn no_padding(mut self, no_padding: bool) -> Self {
|
||||
self.no_padding = no_padding;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for SettingsSectionHeader {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let label = Label::new(self.label)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx);
|
||||
|
||||
v_flex()
|
||||
.w_full()
|
||||
.when(!self.no_padding, |this| this.px_8())
|
||||
.gap_1p5()
|
||||
.map(|this| {
|
||||
if self.icon.is_some() {
|
||||
this.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(self.icon.unwrap()).color(Color::Muted))
|
||||
.child(label),
|
||||
)
|
||||
} else {
|
||||
this.child(label)
|
||||
}
|
||||
})
|
||||
.child(Divider::horizontal().color(DividerColor::BorderFaded))
|
||||
}
|
||||
}
|
||||
@@ -2330,8 +2330,12 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
// Note that `crates/json_schema_store` solves the same problem, there is probably a way to unify the two
|
||||
items.push(SettingsPageItem::SectionHeader(LANGUAGES_SECTION_HEADER));
|
||||
items.extend(all_language_names(cx).into_iter().map(|language_name| {
|
||||
let link = format!("languages.{language_name}");
|
||||
SettingsPageItem::SubPageLink(SubPageLink {
|
||||
title: language_name,
|
||||
description: None,
|
||||
json_path: Some(link.leak()),
|
||||
in_json: true,
|
||||
files: USER | PROJECT,
|
||||
render: Arc::new(|this, window, cx| {
|
||||
this.render_sub_page_items(
|
||||
@@ -6013,7 +6017,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "In Text Threads",
|
||||
title: "Display In Text Threads",
|
||||
description: "Whether edit predictions are enabled when editing text threads in the agent panel.",
|
||||
field: Box::new(SettingField {
|
||||
json_path: Some("edit_prediction.in_text_threads"),
|
||||
@@ -6027,42 +6031,6 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Copilot Provider",
|
||||
description: "Use GitHub Copilot as your edit prediction provider.",
|
||||
field: Box::new(
|
||||
SettingField {
|
||||
json_path: Some("edit_prediction.copilot_provider"),
|
||||
pick: |settings_content| {
|
||||
settings_content.project.all_languages.edit_predictions.as_ref()?.copilot.as_ref()
|
||||
},
|
||||
write: |settings_content, value| {
|
||||
settings_content.project.all_languages.edit_predictions.get_or_insert_default().copilot = value;
|
||||
},
|
||||
}
|
||||
.unimplemented(),
|
||||
),
|
||||
metadata: None,
|
||||
files: USER | PROJECT,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Codestral Provider",
|
||||
description: "Use Mistral's Codestral as your edit prediction provider.",
|
||||
field: Box::new(
|
||||
SettingField {
|
||||
json_path: Some("edit_prediction.codestral_provider"),
|
||||
pick: |settings_content| {
|
||||
settings_content.project.all_languages.edit_predictions.as_ref()?.codestral.as_ref()
|
||||
},
|
||||
write: |settings_content, value| {
|
||||
settings_content.project.all_languages.edit_predictions.get_or_insert_default().codestral = value;
|
||||
},
|
||||
}
|
||||
.unimplemented(),
|
||||
),
|
||||
metadata: None,
|
||||
files: USER | PROJECT,
|
||||
}),
|
||||
]
|
||||
);
|
||||
items
|
||||
@@ -7485,9 +7453,23 @@ fn non_editor_language_settings_data() -> Vec<SettingsPageItem> {
|
||||
fn edit_prediction_language_settings_section() -> Vec<SettingsPageItem> {
|
||||
vec![
|
||||
SettingsPageItem::SectionHeader("Edit Predictions"),
|
||||
SettingsPageItem::SubPageLink(SubPageLink {
|
||||
title: "Configure Providers".into(),
|
||||
json_path: Some("edit_predictions.providers"),
|
||||
description: Some("Set up different edit prediction providers in complement to Zed's built-in Zeta model.".into()),
|
||||
in_json: false,
|
||||
files: USER,
|
||||
render: Arc::new(|_, window, cx| {
|
||||
let settings_window = cx.entity();
|
||||
let page = window.use_state(cx, |_, _| {
|
||||
crate::pages::EditPredictionSetupPage::new(settings_window)
|
||||
});
|
||||
page.into_any_element()
|
||||
}),
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Show Edit Predictions",
|
||||
description: "Controls whether edit predictions are shown immediately or manually by triggering `editor::showeditprediction` (false).",
|
||||
description: "Controls whether edit predictions are shown immediately or manually.",
|
||||
field: Box::new(SettingField {
|
||||
json_path: Some("languages.$(language).show_edit_predictions"),
|
||||
pick: |settings_content| {
|
||||
@@ -7505,7 +7487,7 @@ fn edit_prediction_language_settings_section() -> Vec<SettingsPageItem> {
|
||||
files: USER | PROJECT,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Edit Predictions Disabled In",
|
||||
title: "Disable in Language Scopes",
|
||||
description: "Controls whether edit predictions are shown in the given language scopes.",
|
||||
field: Box::new(
|
||||
SettingField {
|
||||
|
||||
2
crates/settings_ui/src/pages.rs
Normal file
2
crates/settings_ui/src/pages.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod edit_prediction_provider_setup;
|
||||
pub use edit_prediction_provider_setup::EditPredictionSetupPage;
|
||||
365
crates/settings_ui/src/pages/edit_prediction_provider_setup.rs
Normal file
365
crates/settings_ui/src/pages/edit_prediction_provider_setup.rs
Normal file
@@ -0,0 +1,365 @@
|
||||
use edit_prediction::{
|
||||
ApiKeyState, Zeta2FeatureFlag,
|
||||
mercury::{MERCURY_CREDENTIALS_URL, mercury_api_token},
|
||||
sweep_ai::{SWEEP_CREDENTIALS_URL, sweep_api_token},
|
||||
};
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use gpui::{Entity, ScrollHandle, prelude::*};
|
||||
use language_models::provider::mistral::{CODESTRAL_API_URL, codestral_api_key};
|
||||
use ui::{ButtonLink, ConfiguredApiCard, WithScrollbar, prelude::*};
|
||||
|
||||
use crate::{
|
||||
SettingField, SettingItem, SettingsFieldMetadata, SettingsPageItem, SettingsWindow, USER,
|
||||
components::{SettingsInputField, SettingsSectionHeader},
|
||||
};
|
||||
|
||||
pub struct EditPredictionSetupPage {
|
||||
settings_window: Entity<SettingsWindow>,
|
||||
scroll_handle: ScrollHandle,
|
||||
}
|
||||
|
||||
impl EditPredictionSetupPage {
|
||||
pub fn new(settings_window: Entity<SettingsWindow>) -> Self {
|
||||
Self {
|
||||
settings_window,
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for EditPredictionSetupPage {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let settings_window = self.settings_window.clone();
|
||||
|
||||
let providers = [
|
||||
Some(render_github_copilot_provider(window, cx).into_any_element()),
|
||||
cx.has_flag::<Zeta2FeatureFlag>().then(|| {
|
||||
render_api_key_provider(
|
||||
IconName::Inception,
|
||||
"Mercury",
|
||||
"https://platform.inceptionlabs.ai/dashboard/api-keys".into(),
|
||||
mercury_api_token(cx),
|
||||
|_cx| MERCURY_CREDENTIALS_URL,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.into_any_element()
|
||||
}),
|
||||
cx.has_flag::<Zeta2FeatureFlag>().then(|| {
|
||||
render_api_key_provider(
|
||||
IconName::SweepAi,
|
||||
"Sweep",
|
||||
"https://app.sweep.dev/".into(),
|
||||
sweep_api_token(cx),
|
||||
|_cx| SWEEP_CREDENTIALS_URL,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.into_any_element()
|
||||
}),
|
||||
Some(
|
||||
render_api_key_provider(
|
||||
IconName::AiMistral,
|
||||
"Codestral",
|
||||
"https://console.mistral.ai/codestral".into(),
|
||||
codestral_api_key(cx),
|
||||
|cx| language_models::MistralLanguageModelProvider::api_url(cx),
|
||||
Some(settings_window.update(cx, |settings_window, cx| {
|
||||
let codestral_settings = codestral_settings();
|
||||
settings_window
|
||||
.render_sub_page_items_section(
|
||||
codestral_settings.iter().enumerate(),
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.into_any_element()
|
||||
})),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
];
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.vertical_scrollbar_for(&self.scroll_handle, window, cx)
|
||||
.child(
|
||||
v_flex()
|
||||
.id("ep-setup-page")
|
||||
.min_w_0()
|
||||
.size_full()
|
||||
.px_8()
|
||||
.pb_16()
|
||||
.overflow_y_scroll()
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.children(providers.into_iter().flatten()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_api_key_provider(
|
||||
icon: IconName,
|
||||
title: &'static str,
|
||||
link: SharedString,
|
||||
api_key_state: Entity<ApiKeyState>,
|
||||
current_url: fn(&mut App) -> SharedString,
|
||||
additional_fields: Option<AnyElement>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<EditPredictionSetupPage>,
|
||||
) -> impl IntoElement {
|
||||
let weak_page = cx.weak_entity();
|
||||
_ = window.use_keyed_state(title, cx, |_, cx| {
|
||||
let task = api_key_state.update(cx, |key_state, cx| {
|
||||
key_state.load_if_needed(current_url(cx), |state| state, cx)
|
||||
});
|
||||
cx.spawn(async move |_, cx| {
|
||||
task.await.ok();
|
||||
weak_page
|
||||
.update(cx, |_, cx| {
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
});
|
||||
|
||||
let (has_key, env_var_name, is_from_env_var) = api_key_state.read_with(cx, |state, _| {
|
||||
(
|
||||
state.has_key(),
|
||||
Some(state.env_var_name().clone()),
|
||||
state.is_from_env_var(),
|
||||
)
|
||||
});
|
||||
|
||||
let write_key = move |api_key: Option<String>, cx: &mut App| {
|
||||
api_key_state
|
||||
.update(cx, |key_state, cx| {
|
||||
let url = current_url(cx);
|
||||
key_state.store(url, api_key, |key_state| key_state, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
};
|
||||
|
||||
let base_container = v_flex().id(title).min_w_0().pt_8().gap_1p5();
|
||||
let header = SettingsSectionHeader::new(title)
|
||||
.icon(icon)
|
||||
.no_padding(true);
|
||||
let button_link_label = format!("{} dashboard", title);
|
||||
let description = h_flex()
|
||||
.min_w_0()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new("Visit the")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
ButtonLink::new(button_link_label, link)
|
||||
.no_icon(true)
|
||||
.label_size(LabelSize::Small)
|
||||
.label_color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new("to generate an API key.")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
);
|
||||
let configured_card_label = if is_from_env_var {
|
||||
"API Key Set in Environment Variable"
|
||||
} else {
|
||||
"API Key Configured"
|
||||
};
|
||||
|
||||
let container = if has_key {
|
||||
base_container.child(header).child(
|
||||
ConfiguredApiCard::new(configured_card_label)
|
||||
.button_label("Reset Key")
|
||||
.button_tab_index(0)
|
||||
.disabled(is_from_env_var)
|
||||
.when_some(env_var_name, |this, env_var_name| {
|
||||
this.when(is_from_env_var, |this| {
|
||||
this.tooltip_label(format!(
|
||||
"To reset your API key, unset the {} environment variable.",
|
||||
env_var_name
|
||||
))
|
||||
})
|
||||
})
|
||||
.on_click(move |_, _, cx| {
|
||||
write_key(None, cx);
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
base_container.child(header).child(
|
||||
h_flex()
|
||||
.pt_2p5()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.max_w_1_2()
|
||||
.child(Label::new("API Key"))
|
||||
.child(description)
|
||||
.when_some(env_var_name, |this, env_var_name| {
|
||||
this.child({
|
||||
let label = format!(
|
||||
"Or set the {} env var and restart Zed.",
|
||||
env_var_name.as_ref()
|
||||
);
|
||||
Label::new(label).size(LabelSize::Small).color(Color::Muted)
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
SettingsInputField::new()
|
||||
.tab_index(0)
|
||||
.with_placeholder("xxxxxxxxxxxxxxxxxxxx")
|
||||
.on_confirm(move |api_key, cx| {
|
||||
write_key(api_key.filter(|key| !key.is_empty()), cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
};
|
||||
|
||||
container.when_some(additional_fields, |this, additional_fields| {
|
||||
this.child(
|
||||
div()
|
||||
.map(|this| if has_key { this.mt_1() } else { this.mt_4() })
|
||||
.px_neg_8()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(additional_fields),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn codestral_settings() -> Box<[SettingsPageItem]> {
|
||||
Box::new([
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "API URL",
|
||||
description: "The API URL to use for Codestral.",
|
||||
field: Box::new(SettingField {
|
||||
pick: |settings| {
|
||||
settings
|
||||
.project
|
||||
.all_languages
|
||||
.edit_predictions
|
||||
.as_ref()?
|
||||
.codestral
|
||||
.as_ref()?
|
||||
.api_url
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings, value| {
|
||||
settings
|
||||
.project
|
||||
.all_languages
|
||||
.edit_predictions
|
||||
.get_or_insert_default()
|
||||
.codestral
|
||||
.get_or_insert_default()
|
||||
.api_url = value;
|
||||
},
|
||||
json_path: Some("edit_predictions.codestral.api_url"),
|
||||
}),
|
||||
metadata: Some(Box::new(SettingsFieldMetadata {
|
||||
placeholder: Some(CODESTRAL_API_URL),
|
||||
..Default::default()
|
||||
})),
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Max Tokens",
|
||||
description: "The maximum number of tokens to generate.",
|
||||
field: Box::new(SettingField {
|
||||
pick: |settings| {
|
||||
settings
|
||||
.project
|
||||
.all_languages
|
||||
.edit_predictions
|
||||
.as_ref()?
|
||||
.codestral
|
||||
.as_ref()?
|
||||
.max_tokens
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings, value| {
|
||||
settings
|
||||
.project
|
||||
.all_languages
|
||||
.edit_predictions
|
||||
.get_or_insert_default()
|
||||
.codestral
|
||||
.get_or_insert_default()
|
||||
.max_tokens = value;
|
||||
},
|
||||
json_path: Some("edit_predictions.codestral.max_tokens"),
|
||||
}),
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Model",
|
||||
description: "The Codestral model id to use.",
|
||||
field: Box::new(SettingField {
|
||||
pick: |settings| {
|
||||
settings
|
||||
.project
|
||||
.all_languages
|
||||
.edit_predictions
|
||||
.as_ref()?
|
||||
.codestral
|
||||
.as_ref()?
|
||||
.model
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings, value| {
|
||||
settings
|
||||
.project
|
||||
.all_languages
|
||||
.edit_predictions
|
||||
.get_or_insert_default()
|
||||
.codestral
|
||||
.get_or_insert_default()
|
||||
.model = value;
|
||||
},
|
||||
json_path: Some("edit_predictions.codestral.model"),
|
||||
}),
|
||||
metadata: Some(Box::new(SettingsFieldMetadata {
|
||||
placeholder: Some("codestral-latest"),
|
||||
..Default::default()
|
||||
})),
|
||||
files: USER,
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
pub(crate) fn render_github_copilot_provider(
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> impl IntoElement {
|
||||
let configuration_view = window.use_state(cx, |_, cx| {
|
||||
copilot::ConfigurationView::new(
|
||||
|cx| {
|
||||
copilot::Copilot::global(cx)
|
||||
.is_some_and(|copilot| copilot.read(cx).is_authenticated())
|
||||
},
|
||||
copilot::ConfigurationMode::EditPrediction,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
v_flex()
|
||||
.id("github-copilot")
|
||||
.min_w_0()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
SettingsSectionHeader::new("GitHub Copilot")
|
||||
.icon(IconName::Copilot)
|
||||
.no_padding(true),
|
||||
)
|
||||
.child(configuration_view)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
mod components;
|
||||
mod page_data;
|
||||
mod pages;
|
||||
|
||||
use anyhow::Result;
|
||||
use editor::{Editor, EditorEvent};
|
||||
@@ -28,9 +29,8 @@ use std::{
|
||||
};
|
||||
use title_bar::platform_title_bar::PlatformTitleBar;
|
||||
use ui::{
|
||||
Banner, ContextMenu, Divider, DividerColor, DropdownMenu, DropdownStyle, IconButtonShape,
|
||||
KeyBinding, KeybindingHint, PopoverMenu, Switch, Tooltip, TreeViewItem, WithScrollbar,
|
||||
prelude::*,
|
||||
Banner, ContextMenu, Divider, DropdownMenu, DropdownStyle, IconButtonShape, KeyBinding,
|
||||
KeybindingHint, PopoverMenu, Switch, Tooltip, TreeViewItem, WithScrollbar, prelude::*,
|
||||
};
|
||||
use ui_input::{NumberField, NumberFieldType};
|
||||
use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
|
||||
@@ -38,7 +38,8 @@ use workspace::{AppState, OpenOptions, OpenVisible, Workspace, client_side_decor
|
||||
use zed_actions::{OpenProjectSettings, OpenSettings, OpenSettingsAt};
|
||||
|
||||
use crate::components::{
|
||||
EnumVariantDropdown, SettingsInputField, font_picker, icon_theme_picker, theme_picker,
|
||||
EnumVariantDropdown, SettingsInputField, SettingsSectionHeader, font_picker, icon_theme_picker,
|
||||
theme_picker,
|
||||
};
|
||||
|
||||
const NAVBAR_CONTAINER_TAB_INDEX: isize = 0;
|
||||
@@ -613,7 +614,10 @@ pub fn open_settings_editor(
|
||||
app_id: Some(app_id.to_owned()),
|
||||
window_decorations: Some(window_decorations),
|
||||
window_min_size: Some(gpui::Size {
|
||||
width: px(360.0),
|
||||
// Don't make the settings window thinner than this,
|
||||
// otherwise, it gets unusable. Users with smaller res monitors
|
||||
// can customize the height, but not the width.
|
||||
width: px(900.0),
|
||||
height: px(240.0),
|
||||
}),
|
||||
window_bounds: Some(WindowBounds::centered(scaled_bounds, cx)),
|
||||
@@ -834,18 +838,9 @@ impl SettingsPageItem {
|
||||
};
|
||||
|
||||
match self {
|
||||
SettingsPageItem::SectionHeader(header) => v_flex()
|
||||
.w_full()
|
||||
.px_8()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Label::new(SharedString::new_static(header))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(Divider::horizontal().color(DividerColor::BorderFaded))
|
||||
.into_any_element(),
|
||||
SettingsPageItem::SectionHeader(header) => {
|
||||
SettingsSectionHeader::new(SharedString::new_static(header)).into_any_element()
|
||||
}
|
||||
SettingsPageItem::SettingItem(setting_item) => {
|
||||
let (field_with_padding, _) =
|
||||
render_setting_item_inner(setting_item, true, false, cx);
|
||||
@@ -869,9 +864,20 @@ impl SettingsPageItem {
|
||||
.map(apply_padding)
|
||||
.child(
|
||||
v_flex()
|
||||
.relative()
|
||||
.w_full()
|
||||
.max_w_1_2()
|
||||
.child(Label::new(sub_page_link.title.clone())),
|
||||
.child(Label::new(sub_page_link.title.clone()))
|
||||
.when_some(
|
||||
sub_page_link.description.as_ref(),
|
||||
|this, description| {
|
||||
this.child(
|
||||
Label::new(description.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new(
|
||||
@@ -909,7 +915,13 @@ impl SettingsPageItem {
|
||||
this.push_sub_page(sub_page_link.clone(), header, cx)
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(render_settings_item_link(
|
||||
sub_page_link.title.clone(),
|
||||
sub_page_link.json_path,
|
||||
false,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.when(!is_last, |this| this.child(Divider::horizontal()))
|
||||
.into_any_element(),
|
||||
@@ -983,20 +995,6 @@ fn render_settings_item(
|
||||
let (found_in_file, _) = setting_item.field.file_set_in(file.clone(), cx);
|
||||
let file_set_in = SettingsUiFile::from_settings(found_in_file.clone());
|
||||
|
||||
let clipboard_has_link = cx
|
||||
.read_from_clipboard()
|
||||
.and_then(|entry| entry.text())
|
||||
.map_or(false, |maybe_url| {
|
||||
setting_item.field.json_path().is_some()
|
||||
&& maybe_url.strip_prefix("zed://settings/") == setting_item.field.json_path()
|
||||
});
|
||||
|
||||
let (link_icon, link_icon_color) = if clipboard_has_link {
|
||||
(IconName::Check, Color::Success)
|
||||
} else {
|
||||
(IconName::Link, Color::Muted)
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.id(setting_item.title)
|
||||
.min_w_0()
|
||||
@@ -1056,42 +1054,62 @@ fn render_settings_item(
|
||||
)
|
||||
.child(control)
|
||||
.when(sub_page_stack().is_empty(), |this| {
|
||||
// Intentionally using the description to make the icon button
|
||||
// unique because some items share the same title (e.g., "Font Size")
|
||||
let icon_button_id =
|
||||
SharedString::new(format!("copy-link-btn-{}", setting_item.description));
|
||||
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top(rems_from_px(18.))
|
||||
.map(|this| {
|
||||
if sub_field {
|
||||
this.visible_on_hover("setting-sub-item")
|
||||
.left(rems_from_px(-8.5))
|
||||
} else {
|
||||
this.visible_on_hover("setting-item")
|
||||
.left(rems_from_px(-22.))
|
||||
}
|
||||
})
|
||||
.child({
|
||||
IconButton::new(icon_button_id, link_icon)
|
||||
.icon_color(link_icon_color)
|
||||
.icon_size(IconSize::Small)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(Tooltip::text("Copy Link"))
|
||||
.when_some(setting_item.field.json_path(), |this, path| {
|
||||
this.on_click(cx.listener(move |_, _, _, cx| {
|
||||
let link = format!("zed://settings/{}", path);
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(link));
|
||||
cx.notify();
|
||||
}))
|
||||
})
|
||||
}),
|
||||
)
|
||||
this.child(render_settings_item_link(
|
||||
setting_item.description,
|
||||
setting_item.field.json_path(),
|
||||
sub_field,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn render_settings_item_link(
|
||||
id: impl Into<ElementId>,
|
||||
json_path: Option<&'static str>,
|
||||
sub_field: bool,
|
||||
cx: &mut Context<'_, SettingsWindow>,
|
||||
) -> impl IntoElement {
|
||||
let clipboard_has_link = cx
|
||||
.read_from_clipboard()
|
||||
.and_then(|entry| entry.text())
|
||||
.map_or(false, |maybe_url| {
|
||||
json_path.is_some() && maybe_url.strip_prefix("zed://settings/") == json_path
|
||||
});
|
||||
|
||||
let (link_icon, link_icon_color) = if clipboard_has_link {
|
||||
(IconName::Check, Color::Success)
|
||||
} else {
|
||||
(IconName::Link, Color::Muted)
|
||||
};
|
||||
|
||||
div()
|
||||
.absolute()
|
||||
.top(rems_from_px(18.))
|
||||
.map(|this| {
|
||||
if sub_field {
|
||||
this.visible_on_hover("setting-sub-item")
|
||||
.left(rems_from_px(-8.5))
|
||||
} else {
|
||||
this.visible_on_hover("setting-item")
|
||||
.left(rems_from_px(-22.))
|
||||
}
|
||||
})
|
||||
.child(
|
||||
IconButton::new((id.into(), "copy-link-btn"), link_icon)
|
||||
.icon_color(link_icon_color)
|
||||
.icon_size(IconSize::Small)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(Tooltip::text("Copy Link"))
|
||||
.when_some(json_path, |this, path| {
|
||||
this.on_click(cx.listener(move |_, _, _, cx| {
|
||||
let link = format!("zed://settings/{}", path);
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(link));
|
||||
cx.notify();
|
||||
}))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
struct SettingItem {
|
||||
title: &'static str,
|
||||
description: &'static str,
|
||||
@@ -1175,6 +1193,12 @@ impl PartialEq for SettingItem {
|
||||
#[derive(Clone)]
|
||||
struct SubPageLink {
|
||||
title: SharedString,
|
||||
description: Option<SharedString>,
|
||||
/// See [`SettingField.json_path`]
|
||||
json_path: Option<&'static str>,
|
||||
/// Whether or not the settings in this sub page are configurable in settings.json
|
||||
/// Removes the "Edit in settings.json" button from the page.
|
||||
in_json: bool,
|
||||
files: FileMask,
|
||||
render: Arc<
|
||||
dyn Fn(&mut SettingsWindow, &mut Window, &mut Context<SettingsWindow>) -> AnyElement
|
||||
@@ -1835,6 +1859,7 @@ impl SettingsWindow {
|
||||
header_str = *header;
|
||||
}
|
||||
SettingsPageItem::SubPageLink(sub_page_link) => {
|
||||
json_path = sub_page_link.json_path;
|
||||
documents.push(bm25::Document {
|
||||
id: key_index,
|
||||
contents: [page.title, header_str, sub_page_link.title.as_ref()]
|
||||
@@ -2758,19 +2783,49 @@ impl SettingsWindow {
|
||||
page_content
|
||||
}
|
||||
|
||||
fn render_sub_page_items<'a, Items: Iterator<Item = (usize, &'a SettingsPageItem)>>(
|
||||
fn render_sub_page_items<'a, Items>(
|
||||
&self,
|
||||
items: Items,
|
||||
page_index: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<SettingsWindow>,
|
||||
) -> impl IntoElement {
|
||||
let mut page_content = v_flex()
|
||||
) -> impl IntoElement
|
||||
where
|
||||
Items: Iterator<Item = (usize, &'a SettingsPageItem)>,
|
||||
{
|
||||
let page_content = v_flex()
|
||||
.id("settings-ui-page")
|
||||
.size_full()
|
||||
.overflow_y_scroll()
|
||||
.track_scroll(&self.sub_page_scroll_handle);
|
||||
self.render_sub_page_items_in(page_content, items, page_index, window, cx)
|
||||
}
|
||||
|
||||
fn render_sub_page_items_section<'a, Items>(
|
||||
&self,
|
||||
items: Items,
|
||||
page_index: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<SettingsWindow>,
|
||||
) -> impl IntoElement
|
||||
where
|
||||
Items: Iterator<Item = (usize, &'a SettingsPageItem)>,
|
||||
{
|
||||
let page_content = v_flex().id("settings-ui-sub-page-section").size_full();
|
||||
self.render_sub_page_items_in(page_content, items, page_index, window, cx)
|
||||
}
|
||||
|
||||
fn render_sub_page_items_in<'a, Items>(
|
||||
&self,
|
||||
mut page_content: Stateful<Div>,
|
||||
items: Items,
|
||||
page_index: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<SettingsWindow>,
|
||||
) -> impl IntoElement
|
||||
where
|
||||
Items: Iterator<Item = (usize, &'a SettingsPageItem)>,
|
||||
{
|
||||
let items: Vec<_> = items.collect();
|
||||
let items_len = items.len();
|
||||
let mut section_header = None;
|
||||
@@ -2871,18 +2926,25 @@ impl SettingsWindow {
|
||||
)
|
||||
.child(self.render_sub_page_breadcrumbs()),
|
||||
)
|
||||
.child(
|
||||
Button::new("open-in-settings-file", "Edit in settings.json")
|
||||
.tab_index(0_isize)
|
||||
.style(ButtonStyle::OutlinedGhost)
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Edit in settings.json",
|
||||
&OpenCurrentFile,
|
||||
&self.focus_handle,
|
||||
))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.open_current_settings_file(window, cx);
|
||||
})),
|
||||
.when(
|
||||
sub_page_stack()
|
||||
.last()
|
||||
.is_none_or(|sub_page| sub_page.link.in_json),
|
||||
|this| {
|
||||
this.child(
|
||||
Button::new("open-in-settings-file", "Edit in settings.json")
|
||||
.tab_index(0_isize)
|
||||
.style(ButtonStyle::OutlinedGhost)
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Edit in settings.json",
|
||||
&OpenCurrentFile,
|
||||
&self.focus_handle,
|
||||
))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.open_current_settings_file(window, cx);
|
||||
})),
|
||||
)
|
||||
},
|
||||
)
|
||||
.into_any_element();
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod ai;
|
||||
mod avatar;
|
||||
mod banner;
|
||||
mod button;
|
||||
@@ -16,6 +17,7 @@ mod icon;
|
||||
mod image;
|
||||
mod indent_guides;
|
||||
mod indicator;
|
||||
mod inline_code;
|
||||
mod keybinding;
|
||||
mod keybinding_hint;
|
||||
mod label;
|
||||
@@ -43,6 +45,7 @@ mod tree_view_item;
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories;
|
||||
|
||||
pub use ai::*;
|
||||
pub use avatar::*;
|
||||
pub use banner::*;
|
||||
pub use button::*;
|
||||
@@ -61,6 +64,7 @@ pub use icon::*;
|
||||
pub use image::*;
|
||||
pub use indent_guides::*;
|
||||
pub use indicator::*;
|
||||
pub use inline_code::*;
|
||||
pub use keybinding::*;
|
||||
pub use keybinding_hint::*;
|
||||
pub use label::*;
|
||||
|
||||
3
crates/ui/src/components/ai.rs
Normal file
3
crates/ui/src/components/ai.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod configured_api_card;
|
||||
|
||||
pub use configured_api_card::*;
|
||||
@@ -1,10 +1,11 @@
|
||||
use crate::{Tooltip, prelude::*};
|
||||
use gpui::{ClickEvent, IntoElement, ParentElement, SharedString};
|
||||
use ui::{Tooltip, prelude::*};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ConfiguredApiCard {
|
||||
label: SharedString,
|
||||
button_label: Option<SharedString>,
|
||||
button_tab_index: Option<isize>,
|
||||
tooltip_label: Option<SharedString>,
|
||||
disabled: bool,
|
||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||
@@ -15,6 +16,7 @@ impl ConfiguredApiCard {
|
||||
Self {
|
||||
label: label.into(),
|
||||
button_label: None,
|
||||
button_tab_index: None,
|
||||
tooltip_label: None,
|
||||
disabled: false,
|
||||
on_click: None,
|
||||
@@ -43,6 +45,11 @@ impl ConfiguredApiCard {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn button_tab_index(mut self, tab_index: isize) -> Self {
|
||||
self.button_tab_index = Some(tab_index);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ConfiguredApiCard {
|
||||
@@ -51,23 +58,27 @@ impl RenderOnce for ConfiguredApiCard {
|
||||
let button_id = SharedString::new(format!("id-{}", button_label));
|
||||
|
||||
h_flex()
|
||||
.min_w_0()
|
||||
.mt_0p5()
|
||||
.p_1()
|
||||
.justify_between()
|
||||
.rounded_md()
|
||||
.flex_wrap()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().background)
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::Check).color(Color::Success))
|
||||
.child(Label::new(self.label).truncate()),
|
||||
.child(Label::new(self.label)),
|
||||
)
|
||||
.child(
|
||||
Button::new(button_id, button_label)
|
||||
.when_some(self.button_tab_index, |elem, tab_index| {
|
||||
elem.tab_index(tab_index)
|
||||
})
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::Undo)
|
||||
.icon_size(IconSize::Small)
|
||||
@@ -1,12 +1,14 @@
|
||||
mod button;
|
||||
mod button_icon;
|
||||
mod button_like;
|
||||
mod button_link;
|
||||
mod icon_button;
|
||||
mod split_button;
|
||||
mod toggle_button;
|
||||
|
||||
pub use button::*;
|
||||
pub use button_like::*;
|
||||
pub use button_link::*;
|
||||
pub use icon_button::*;
|
||||
pub use split_button::*;
|
||||
pub use toggle_button::*;
|
||||
|
||||
102
crates/ui/src/components/button/button_link.rs
Normal file
102
crates/ui/src/components/button/button_link.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use gpui::{IntoElement, Window, prelude::*};
|
||||
|
||||
use crate::{ButtonLike, prelude::*};
|
||||
|
||||
/// A button that takes an underline to look like a regular web link.
|
||||
/// It also contains an arrow icon to communicate the link takes you out of Zed.
|
||||
///
|
||||
/// # Usage Example
|
||||
///
|
||||
/// ```
|
||||
/// use ui::ButtonLink;
|
||||
///
|
||||
/// let button_link = ButtonLink::new("Click me", "https://example.com");
|
||||
/// ```
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct ButtonLink {
|
||||
label: SharedString,
|
||||
label_size: LabelSize,
|
||||
label_color: Color,
|
||||
link: String,
|
||||
no_icon: bool,
|
||||
}
|
||||
|
||||
impl ButtonLink {
|
||||
pub fn new(label: impl Into<SharedString>, link: impl Into<String>) -> Self {
|
||||
Self {
|
||||
link: link.into(),
|
||||
label: label.into(),
|
||||
label_size: LabelSize::Default,
|
||||
label_color: Color::Default,
|
||||
no_icon: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn no_icon(mut self, no_icon: bool) -> Self {
|
||||
self.no_icon = no_icon;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn label_size(mut self, label_size: LabelSize) -> Self {
|
||||
self.label_size = label_size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn label_color(mut self, label_color: Color) -> Self {
|
||||
self.label_color = label_color;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ButtonLink {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let id = format!("{}-{}", self.label, self.link);
|
||||
|
||||
ButtonLike::new(id)
|
||||
.size(ButtonSize::None)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new(self.label)
|
||||
.size(self.label_size)
|
||||
.color(self.label_color)
|
||||
.underline(),
|
||||
)
|
||||
.when(!self.no_icon, |this| {
|
||||
this.child(
|
||||
Icon::new(IconName::ArrowUpRight)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.on_click(move |_, _, cx| cx.open_url(&self.link))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ButtonLink {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Navigation
|
||||
}
|
||||
|
||||
fn description() -> Option<&'static str> {
|
||||
Some("A button that opens a URL.")
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
Some(
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.child(
|
||||
example_group(vec![single_example(
|
||||
"Simple",
|
||||
ButtonLink::new("zed.dev", "https://zed.dev").into_any_element(),
|
||||
)])
|
||||
.vertical(),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -144,12 +144,18 @@ impl Divider {
|
||||
impl RenderOnce for Divider {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let base = match self.direction {
|
||||
DividerDirection::Horizontal => {
|
||||
div().h_px().w_full().when(self.inset, |this| this.mx_1p5())
|
||||
}
|
||||
DividerDirection::Vertical => {
|
||||
div().w_px().h_full().when(self.inset, |this| this.my_1p5())
|
||||
}
|
||||
DividerDirection::Horizontal => div()
|
||||
.min_w_0()
|
||||
.flex_none()
|
||||
.h_px()
|
||||
.w_full()
|
||||
.when(self.inset, |this| this.mx_1p5()),
|
||||
DividerDirection::Vertical => div()
|
||||
.min_w_0()
|
||||
.flex_none()
|
||||
.w_px()
|
||||
.h_full()
|
||||
.when(self.inset, |this| this.my_1p5()),
|
||||
};
|
||||
|
||||
match self.style {
|
||||
|
||||
64
crates/ui/src/components/inline_code.rs
Normal file
64
crates/ui/src/components/inline_code.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
use crate::prelude::*;
|
||||
use gpui::{AnyElement, IntoElement, ParentElement, Styled};
|
||||
|
||||
/// InlineCode mimics the way inline code is rendered when wrapped in backticks in Markdown.
|
||||
///
|
||||
/// # Usage Example
|
||||
///
|
||||
/// ```
|
||||
/// use ui::InlineCode;
|
||||
///
|
||||
/// let InlineCode = InlineCode::new("<div>hey</div>");
|
||||
/// ```
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct InlineCode {
|
||||
label: SharedString,
|
||||
label_size: LabelSize,
|
||||
}
|
||||
|
||||
impl InlineCode {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
label_size: LabelSize::Default,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the size of the label.
|
||||
pub fn label_size(mut self, size: LabelSize) -> Self {
|
||||
self.label_size = size;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for InlineCode {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
h_flex()
|
||||
.min_w_0()
|
||||
.px_0p5()
|
||||
.overflow_hidden()
|
||||
.bg(cx.theme().colors().text.opacity(0.05))
|
||||
.child(Label::new(self.label).size(self.label_size).buffer_font(cx))
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for InlineCode {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::DataDisplay
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
Some(
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.child(
|
||||
example_group(vec![single_example(
|
||||
"Simple",
|
||||
InlineCode::new("zed.dev").into_any_element(),
|
||||
)])
|
||||
.vertical(),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -227,7 +227,7 @@ impl RenderOnce for LabelLike {
|
||||
.get_or_insert_with(Default::default)
|
||||
.underline = Some(UnderlineStyle {
|
||||
thickness: px(1.),
|
||||
color: None,
|
||||
color: Some(cx.theme().colors().text_muted.opacity(0.4)),
|
||||
wavy: false,
|
||||
});
|
||||
this
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
use crate::{ListItem, prelude::*};
|
||||
use component::{Component, ComponentScope, example_group_with_title, single_example};
|
||||
use crate::{ButtonLink, ListItem, prelude::*};
|
||||
use component::{Component, ComponentScope, example_group, single_example};
|
||||
use gpui::{IntoElement, ParentElement, SharedString};
|
||||
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct ListBulletItem {
|
||||
label: SharedString,
|
||||
label_color: Option<Color>,
|
||||
children: Vec<AnyElement>,
|
||||
}
|
||||
|
||||
impl ListBulletItem {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
label_color: None,
|
||||
children: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label_color(mut self, color: Color) -> Self {
|
||||
self.label_color = Some(color);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for ListBulletItem {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ListBulletItem {
|
||||
@@ -34,7 +49,18 @@ impl RenderOnce for ListBulletItem {
|
||||
.color(Color::Hidden),
|
||||
),
|
||||
)
|
||||
.child(div().w_full().min_w_0().child(Label::new(self.label))),
|
||||
.map(|this| {
|
||||
if !self.children.is_empty() {
|
||||
this.child(h_flex().gap_0p5().flex_wrap().children(self.children))
|
||||
} else {
|
||||
this.child(
|
||||
div().w_full().min_w_0().child(
|
||||
Label::new(self.label)
|
||||
.color(self.label_color.unwrap_or(Color::Default)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -46,37 +72,43 @@ impl Component for ListBulletItem {
|
||||
}
|
||||
|
||||
fn description() -> Option<&'static str> {
|
||||
Some("A list item with a bullet point indicator for unordered lists.")
|
||||
Some("A list item with a dash indicator for unordered lists.")
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
let basic_examples = vec![
|
||||
single_example(
|
||||
"Simple",
|
||||
ListBulletItem::new("First bullet item").into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Multiple Lines",
|
||||
v_flex()
|
||||
.child(ListBulletItem::new("First item"))
|
||||
.child(ListBulletItem::new("Second item"))
|
||||
.child(ListBulletItem::new("Third item"))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Long Text",
|
||||
ListBulletItem::new(
|
||||
"A longer bullet item that demonstrates text wrapping behavior",
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"With Link",
|
||||
ListBulletItem::new("")
|
||||
.child(Label::new("Create a Zed account by"))
|
||||
.child(ButtonLink::new("visiting the website", "https://zed.dev"))
|
||||
.into_any_element(),
|
||||
),
|
||||
];
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.child(example_group_with_title(
|
||||
"Bullet Items",
|
||||
vec![
|
||||
single_example(
|
||||
"Simple",
|
||||
ListBulletItem::new("First bullet item").into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Multiple Lines",
|
||||
v_flex()
|
||||
.child(ListBulletItem::new("First item"))
|
||||
.child(ListBulletItem::new("Second item"))
|
||||
.child(ListBulletItem::new("Third item"))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Long Text",
|
||||
ListBulletItem::new(
|
||||
"A longer bullet item that demonstrates text wrapping behavior",
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
],
|
||||
))
|
||||
.child(example_group(basic_examples).vertical())
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ pub enum NotificationId {
|
||||
|
||||
impl NotificationId {
|
||||
/// Returns a unique [`NotificationId`] for the given type.
|
||||
pub fn unique<T: 'static>() -> Self {
|
||||
pub const fn unique<T: 'static>() -> Self {
|
||||
Self::Unique(TypeId::of::<T>())
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::sync::LazyLock;
|
||||
/// When true, Zed will use in-memory databases instead of persistent storage.
|
||||
pub static ZED_STATELESS: LazyLock<bool> = bool_env_var!("ZED_STATELESS");
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EnvVar {
|
||||
pub name: SharedString,
|
||||
/// Value of the environment variable. Also `None` when set to an empty string.
|
||||
@@ -30,7 +31,7 @@ impl EnvVar {
|
||||
#[macro_export]
|
||||
macro_rules! env_var {
|
||||
($name:expr) => {
|
||||
LazyLock::new(|| $crate::EnvVar::new(($name).into()))
|
||||
::std::sync::LazyLock::new(|| $crate::EnvVar::new(($name).into()))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,6 +40,6 @@ macro_rules! env_var {
|
||||
#[macro_export]
|
||||
macro_rules! bool_env_var {
|
||||
($name:expr) => {
|
||||
LazyLock::new(|| $crate::EnvVar::new(($name).into()).value.is_some())
|
||||
::std::sync::LazyLock::new(|| $crate::EnvVar::new(($name).into()).value.is_some())
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user