assistant panel: Tab-less configuration view (#15682)
TODOs for follow-up: - [ ] When opening panel: nudge user to sign in if they're not signed-in and have no provider configured (or if they're not signed-in and have Zed AI configured) - [ ] Configuration page is not scrollable - [ ] Design tweaks Current status: https://github.com/user-attachments/assets/d26d65ea-43e8-481b-81a3-b3cba01704a8 Release Notes: - N/A
This commit is contained in:
parent
35a3b00255
commit
390815dd76
10 changed files with 548 additions and 561 deletions
|
@ -31,7 +31,7 @@ use editor::{
|
||||||
use editor::{display_map::CreaseId, FoldPlaceholder};
|
use editor::{display_map::CreaseId, FoldPlaceholder};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, percentage, point, svg, Action, Animation, AnimationExt, AnyElement, AnyView, AppContext,
|
div, percentage, point, Action, Animation, AnimationExt, AnyElement, AnyView, AppContext,
|
||||||
AsyncWindowContext, ClipboardItem, Context as _, DismissEvent, Empty, Entity, EventEmitter,
|
AsyncWindowContext, ClipboardItem, Context as _, DismissEvent, Empty, Entity, EventEmitter,
|
||||||
FocusHandle, FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Pixels,
|
FocusHandle, FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Pixels,
|
||||||
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Transformation,
|
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Transformation,
|
||||||
|
@ -41,12 +41,16 @@ use indexed_docs::IndexedDocsStore;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::SoftWrap, Capability, LanguageRegistry, LspAdapterDelegate, Point, ToOffset,
|
language_settings::SoftWrap, Capability, LanguageRegistry, LspAdapterDelegate, Point, ToOffset,
|
||||||
};
|
};
|
||||||
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Role};
|
use language_model::{
|
||||||
|
provider::cloud::PROVIDER_ID, LanguageModelProvider, LanguageModelProviderId,
|
||||||
|
LanguageModelRegistry, Role,
|
||||||
|
};
|
||||||
use multi_buffer::MultiBufferRow;
|
use multi_buffer::MultiBufferRow;
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use project::{Project, ProjectLspAdapterDelegate};
|
use project::{Project, ProjectLspAdapterDelegate};
|
||||||
use search::{buffer_search::DivRegistrar, BufferSearchBar};
|
use search::{buffer_search::DivRegistrar, BufferSearchBar};
|
||||||
use settings::{update_settings_file, Settings};
|
use settings::{update_settings_file, Settings};
|
||||||
|
use smol::stream::StreamExt;
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
cmp::{self, Ordering},
|
cmp::{self, Ordering},
|
||||||
|
@ -140,6 +144,8 @@ pub struct AssistantPanel {
|
||||||
model_summary_editor: View<Editor>,
|
model_summary_editor: View<Editor>,
|
||||||
authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
|
authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
|
||||||
configuration_subscription: Option<Subscription>,
|
configuration_subscription: Option<Subscription>,
|
||||||
|
watch_client_status: Option<Task<()>>,
|
||||||
|
nudge_sign_in: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -411,6 +417,38 @@ impl AssistantPanel {
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let mut status_rx = workspace.client().clone().status();
|
||||||
|
|
||||||
|
let watch_client_status = cx.spawn(|this, mut cx| async move {
|
||||||
|
let mut old_status = None;
|
||||||
|
while let Some(status) = status_rx.next().await {
|
||||||
|
if old_status.is_none()
|
||||||
|
|| old_status.map_or(false, |old_status| old_status != status)
|
||||||
|
{
|
||||||
|
if status.is_signed_out() {
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
let active_provider =
|
||||||
|
LanguageModelRegistry::read_global(cx).active_provider();
|
||||||
|
|
||||||
|
// If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is
|
||||||
|
// the provider, we want to show a nudge to sign in.
|
||||||
|
if active_provider
|
||||||
|
.map_or(true, |provider| provider.id().0 == PROVIDER_ID)
|
||||||
|
{
|
||||||
|
println!("TODO: Nudge the user to sign in and use Zed AI");
|
||||||
|
this.nudge_sign_in = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
};
|
||||||
|
|
||||||
|
old_status = Some(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.update(&mut cx, |this, _cx| this.watch_client_status = None)
|
||||||
|
.log_err();
|
||||||
|
});
|
||||||
|
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
pane,
|
pane,
|
||||||
workspace: workspace.weak_handle(),
|
workspace: workspace.weak_handle(),
|
||||||
|
@ -425,17 +463,11 @@ impl AssistantPanel {
|
||||||
model_summary_editor,
|
model_summary_editor,
|
||||||
authenticate_provider_task: None,
|
authenticate_provider_task: None,
|
||||||
configuration_subscription: None,
|
configuration_subscription: None,
|
||||||
|
watch_client_status: Some(watch_client_status),
|
||||||
|
// TODO: This is unused!
|
||||||
|
nudge_sign_in: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
if LanguageModelRegistry::read_global(cx)
|
|
||||||
.active_provider()
|
|
||||||
.is_none()
|
|
||||||
{
|
|
||||||
this.show_configuration_for_provider(None, cx);
|
|
||||||
} else {
|
|
||||||
this.new_context(cx);
|
this.new_context(cx);
|
||||||
};
|
|
||||||
|
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -623,12 +655,7 @@ impl AssistantPanel {
|
||||||
provider.id(),
|
provider.id(),
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let _ = load_credentials.await;
|
let _ = load_credentials.await;
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, _cx| {
|
||||||
if !provider.is_authenticated(cx) {
|
|
||||||
this.show_configuration_for_provider(Some(provider), cx)
|
|
||||||
} else if !this.has_any_context_editors(cx) {
|
|
||||||
this.new_context(cx);
|
|
||||||
}
|
|
||||||
this.authenticate_provider_task = None;
|
this.authenticate_provider_task = None;
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
|
@ -908,20 +935,11 @@ impl AssistantPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
panel.update(cx, |this, cx| {
|
panel.update(cx, |this, cx| {
|
||||||
this.show_configuration_for_active_provider(cx);
|
this.show_configuration_tab(cx);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_configuration_for_active_provider(&mut self, cx: &mut ViewContext<Self>) {
|
fn show_configuration_tab(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
let provider = LanguageModelRegistry::read_global(cx).active_provider();
|
|
||||||
self.show_configuration_for_provider(provider, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show_configuration_for_provider(
|
|
||||||
&mut self,
|
|
||||||
provider: Option<Arc<dyn LanguageModelProvider>>,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) {
|
|
||||||
let configuration_item_ix = self
|
let configuration_item_ix = self
|
||||||
.pane
|
.pane
|
||||||
.read(cx)
|
.read(cx)
|
||||||
|
@ -931,24 +949,9 @@ impl AssistantPanel {
|
||||||
if let Some(configuration_item_ix) = configuration_item_ix {
|
if let Some(configuration_item_ix) = configuration_item_ix {
|
||||||
self.pane.update(cx, |pane, cx| {
|
self.pane.update(cx, |pane, cx| {
|
||||||
pane.activate_item(configuration_item_ix, true, true, cx);
|
pane.activate_item(configuration_item_ix, true, true, cx);
|
||||||
if let Some((item, provider)) =
|
|
||||||
pane.item_for_index(configuration_item_ix).zip(provider)
|
|
||||||
{
|
|
||||||
if let Some(view) = item.downcast::<ConfigurationView>() {
|
|
||||||
view.update(cx, |view, cx| {
|
|
||||||
view.set_active_tab(provider, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let configuration = cx.new_view(|cx| {
|
let configuration = cx.new_view(|cx| ConfigurationView::new(cx));
|
||||||
let mut view = ConfigurationView::new(cx);
|
|
||||||
if let Some(provider) = provider {
|
|
||||||
view.set_active_tab(provider, cx);
|
|
||||||
}
|
|
||||||
view
|
|
||||||
});
|
|
||||||
self.configuration_subscription = Some(cx.subscribe(
|
self.configuration_subscription = Some(cx.subscribe(
|
||||||
&configuration,
|
&configuration,
|
||||||
|this, _, event: &ConfigurationViewEvent, cx| match event {
|
|this, _, event: &ConfigurationViewEvent, cx| match event {
|
||||||
|
@ -1018,13 +1021,6 @@ impl AssistantPanel {
|
||||||
.downcast::<ContextEditor>()
|
.downcast::<ContextEditor>()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_any_context_editors(&self, cx: &AppContext) -> bool {
|
|
||||||
self.pane
|
|
||||||
.read(cx)
|
|
||||||
.items()
|
|
||||||
.any(|item| item.downcast::<ContextEditor>().is_some())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn active_context(&self, cx: &AppContext) -> Option<Model<Context>> {
|
pub fn active_context(&self, cx: &AppContext) -> Option<Model<Context>> {
|
||||||
Some(self.active_context_editor(cx)?.read(cx).context.clone())
|
Some(self.active_context_editor(cx)?.read(cx).context.clone())
|
||||||
}
|
}
|
||||||
|
@ -1159,9 +1155,9 @@ impl Render for AssistantPanel {
|
||||||
.on_action(cx.listener(|this, _: &workspace::NewFile, cx| {
|
.on_action(cx.listener(|this, _: &workspace::NewFile, cx| {
|
||||||
this.new_context(cx);
|
this.new_context(cx);
|
||||||
}))
|
}))
|
||||||
.on_action(cx.listener(|this, _: &ShowConfiguration, cx| {
|
.on_action(
|
||||||
this.show_configuration_for_active_provider(cx)
|
cx.listener(|this, _: &ShowConfiguration, cx| this.show_configuration_tab(cx)),
|
||||||
}))
|
)
|
||||||
.on_action(cx.listener(AssistantPanel::deploy_history))
|
.on_action(cx.listener(AssistantPanel::deploy_history))
|
||||||
.on_action(cx.listener(AssistantPanel::deploy_prompt_library))
|
.on_action(cx.listener(AssistantPanel::deploy_prompt_library))
|
||||||
.on_action(cx.listener(AssistantPanel::toggle_model_selector))
|
.on_action(cx.listener(AssistantPanel::toggle_model_selector))
|
||||||
|
@ -1231,14 +1227,7 @@ impl Panel for AssistantPanel {
|
||||||
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||||
if active {
|
if active {
|
||||||
if self.pane.read(cx).items_len() == 0 {
|
if self.pane.read(cx).items_len() == 0 {
|
||||||
if LanguageModelRegistry::read_global(cx)
|
|
||||||
.active_provider()
|
|
||||||
.is_none()
|
|
||||||
{
|
|
||||||
self.show_configuration_for_provider(None, cx);
|
|
||||||
} else {
|
|
||||||
self.new_context(cx);
|
self.new_context(cx);
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.ensure_authenticated(cx);
|
self.ensure_authenticated(cx);
|
||||||
|
@ -3044,124 +3033,73 @@ impl Item for ContextHistory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ActiveTab {
|
|
||||||
provider: Arc<dyn LanguageModelProvider>,
|
|
||||||
configuration_prompt: AnyView,
|
|
||||||
focus_handle: Option<FocusHandle>,
|
|
||||||
load_credentials_task: Option<Task<()>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActiveTab {
|
|
||||||
fn is_loading_credentials(&self) -> bool {
|
|
||||||
if let Some(task) = &self.load_credentials_task {
|
|
||||||
if let Task::Spawned(_) = task {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ConfigurationView {
|
pub struct ConfigurationView {
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
active_tab: Option<ActiveTab>,
|
configuration_views: HashMap<LanguageModelProviderId, AnyView>,
|
||||||
|
_registry_subscription: Subscription,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigurationView {
|
impl ConfigurationView {
|
||||||
fn new(cx: &mut ViewContext<Self>) -> Self {
|
fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||||
let focus_handle = cx.focus_handle();
|
let focus_handle = cx.focus_handle();
|
||||||
|
|
||||||
cx.on_focus(&focus_handle, |this, cx| {
|
let registry_subscription = cx.subscribe(
|
||||||
if let Some(focus_handle) = this
|
&LanguageModelRegistry::global(cx),
|
||||||
.active_tab
|
|this, _, event: &language_model::Event, cx| match event {
|
||||||
.as_ref()
|
language_model::Event::AddedProvider(provider_id) => {
|
||||||
.and_then(|tab| tab.focus_handle.as_ref())
|
let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
|
||||||
{
|
if let Some(provider) = provider {
|
||||||
focus_handle.focus(cx);
|
this.add_configuration_view(&provider, cx);
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.detach();
|
language_model::Event::RemovedProvider(provider_id) => {
|
||||||
|
this.remove_configuration_view(provider_id);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
focus_handle,
|
focus_handle,
|
||||||
active_tab: None,
|
configuration_views: HashMap::default(),
|
||||||
|
_registry_subscription: registry_subscription,
|
||||||
};
|
};
|
||||||
|
this.build_configuration_views(cx);
|
||||||
let providers = LanguageModelRegistry::read_global(cx).providers();
|
|
||||||
if !providers.is_empty() {
|
|
||||||
this.set_active_tab(providers[0].clone(), cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_active_tab(
|
fn build_configuration_views(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
let providers = LanguageModelRegistry::read_global(cx).providers();
|
||||||
|
for provider in providers {
|
||||||
|
self.add_configuration_view(&provider, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
|
||||||
|
self.configuration_views.remove(provider_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_configuration_view(
|
||||||
&mut self,
|
&mut self,
|
||||||
provider: Arc<dyn LanguageModelProvider>,
|
provider: &Arc<dyn LanguageModelProvider>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
let (view, focus_handle) = provider.configuration_view(cx);
|
let configuration_view = provider.configuration_view(cx);
|
||||||
|
self.configuration_views
|
||||||
if let Some(focus_handle) = &focus_handle {
|
.insert(provider.id(), configuration_view);
|
||||||
focus_handle.focus(cx);
|
|
||||||
} else {
|
|
||||||
self.focus_handle.focus(cx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let load_credentials = provider.authenticate(cx);
|
fn render_provider_view(
|
||||||
let load_credentials_task = cx.spawn(|this, mut cx| async move {
|
&mut self,
|
||||||
let _ = load_credentials.await;
|
provider: &Arc<dyn LanguageModelProvider>,
|
||||||
this.update(&mut cx, |this, cx| {
|
cx: &mut ViewContext<Self>,
|
||||||
if let Some(active_tab) = &mut this.active_tab {
|
) -> Div {
|
||||||
active_tab.load_credentials_task = None;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
});
|
|
||||||
|
|
||||||
self.active_tab = Some(ActiveTab {
|
|
||||||
provider,
|
|
||||||
configuration_prompt: view,
|
|
||||||
focus_handle,
|
|
||||||
load_credentials_task: Some(load_credentials_task),
|
|
||||||
});
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_active_tab_view(&mut self, cx: &mut ViewContext<Self>) -> Option<Div> {
|
|
||||||
let Some(active_tab) = &self.active_tab else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
let provider = active_tab.provider.clone();
|
|
||||||
let provider_name = provider.name().0.clone();
|
let provider_name = provider.name().0.clone();
|
||||||
|
let configuration_view = self.configuration_views.get(&provider.id()).cloned();
|
||||||
|
|
||||||
let show_spinner = active_tab.is_loading_credentials();
|
|
||||||
|
|
||||||
let content = if show_spinner {
|
|
||||||
let loading_icon = svg()
|
|
||||||
.size_4()
|
|
||||||
.path(IconName::ArrowCircle.path())
|
|
||||||
.text_color(cx.text_style().color)
|
|
||||||
.with_animation(
|
|
||||||
"icon_circle_arrow",
|
|
||||||
Animation::new(Duration::from_secs(2)).repeat(),
|
|
||||||
|svg, delta| svg.with_transformation(Transformation::rotate(percentage(delta))),
|
|
||||||
);
|
|
||||||
|
|
||||||
h_flex()
|
|
||||||
.gap_2()
|
|
||||||
.child(loading_icon)
|
|
||||||
.child(Label::new("Loading provider configuration...").size(LabelSize::Small))
|
|
||||||
.into_any_element()
|
|
||||||
} else {
|
|
||||||
active_tab.configuration_prompt.clone().into_any_element()
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_4()
|
.gap_4()
|
||||||
|
.child(Headline::new(provider_name.clone()).size(HeadlineSize::Medium))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.p(Spacing::Large.rems(cx))
|
.p(Spacing::Large.rems(cx))
|
||||||
|
@ -3169,11 +3107,17 @@ impl ConfigurationView {
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(cx.theme().colors().border_variant)
|
.border_color(cx.theme().colors().border_variant)
|
||||||
.rounded_md()
|
.rounded_md()
|
||||||
.child(content),
|
.when(configuration_view.is_none(), |this| {
|
||||||
|
this.child(div().child(Label::new(format!(
|
||||||
|
"No configuration view for {}",
|
||||||
|
provider_name
|
||||||
|
))))
|
||||||
|
})
|
||||||
|
.when_some(configuration_view, |this, configuration_view| {
|
||||||
|
this.child(configuration_view)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.when(
|
.when(provider.is_authenticated(cx), move |this| {
|
||||||
!show_spinner && provider.is_authenticated(cx),
|
|
||||||
move |this| {
|
|
||||||
this.child(
|
this.child(
|
||||||
h_flex().justify_end().child(
|
h_flex().justify_end().child(
|
||||||
Button::new(
|
Button::new(
|
||||||
|
@ -3184,71 +3128,27 @@ impl ConfigurationView {
|
||||||
.icon(IconName::Plus)
|
.icon(IconName::Plus)
|
||||||
.style(ButtonStyle::Filled)
|
.style(ButtonStyle::Filled)
|
||||||
.layer(ElevationIndex::ModalSurface)
|
.layer(ElevationIndex::ModalSurface)
|
||||||
.on_click(cx.listener(
|
.on_click(cx.listener({
|
||||||
|
let provider = provider.clone();
|
||||||
move |_, _, cx| {
|
move |_, _, cx| {
|
||||||
cx.emit(ConfigurationViewEvent::NewProviderContextEditor(
|
cx.emit(ConfigurationViewEvent::NewProviderContextEditor(
|
||||||
provider.clone(),
|
provider.clone(),
|
||||||
))
|
))
|
||||||
},
|
}
|
||||||
)),
|
})),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_tab(
|
|
||||||
&self,
|
|
||||||
provider: &Arc<dyn LanguageModelProvider>,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) -> impl IntoElement {
|
|
||||||
let button_id = SharedString::from(format!("tab-{}", provider.id().0));
|
|
||||||
let is_active = self.active_tab.as_ref().map(|t| t.provider.id()) == Some(provider.id());
|
|
||||||
ButtonLike::new(button_id)
|
|
||||||
.size(ButtonSize::Compact)
|
|
||||||
.style(ButtonStyle::Transparent)
|
|
||||||
.selected(is_active)
|
|
||||||
.on_click(cx.listener({
|
|
||||||
let provider = provider.clone();
|
|
||||||
move |this, _, cx| {
|
|
||||||
this.set_active_tab(provider.clone(), cx);
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.my_3()
|
|
||||||
.pb_px()
|
|
||||||
.border_b_1()
|
|
||||||
.border_color(if is_active {
|
|
||||||
cx.theme().colors().text_accent
|
|
||||||
} else {
|
|
||||||
cx.theme().colors().border_transparent
|
|
||||||
})
|
})
|
||||||
.when(!is_active, |this| {
|
|
||||||
this.group_hover("", |this| {
|
|
||||||
this.border_color(cx.theme().colors().border_variant)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.child(Label::new(provider.name().0).size(LabelSize::Small).color(
|
|
||||||
if is_active {
|
|
||||||
Color::Accent
|
|
||||||
} else {
|
|
||||||
Color::Default
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for ConfigurationView {
|
impl Render for ConfigurationView {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
let providers = LanguageModelRegistry::read_global(cx).providers();
|
let providers = LanguageModelRegistry::read_global(cx).providers();
|
||||||
let tabs = h_flex().mx_neg_1().gap_3().children(
|
let provider_views = providers
|
||||||
providers
|
.into_iter()
|
||||||
.iter()
|
.map(|provider| self.render_provider_view(&provider, cx))
|
||||||
.map(|provider| self.render_tab(provider, cx)),
|
.collect::<Vec<_>>();
|
||||||
);
|
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.id("assistant-configuration-view")
|
.id("assistant-configuration-view")
|
||||||
|
@ -3266,20 +3166,13 @@ impl Render for ConfigurationView {
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(Headline::new("Configure providers").size(HeadlineSize::Small))
|
|
||||||
.child(
|
.child(
|
||||||
Label::new(
|
Label::new(
|
||||||
"At least one provider must be configured to use the assistant.",
|
"At least one provider must be configured to use the assistant.",
|
||||||
)
|
)
|
||||||
.color(Color::Muted),
|
.color(Color::Muted),
|
||||||
)
|
)
|
||||||
.child(tabs)
|
.child(v_flex().mt_2().gap_4().children(provider_views)),
|
||||||
.when(self.active_tab.is_some(), |this| {
|
|
||||||
this.children(self.render_active_tab_view(cx))
|
|
||||||
})
|
|
||||||
.when(self.active_tab.is_none(), |this| {
|
|
||||||
this.child(Label::new("No providers configured").color(Color::Warning))
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,7 @@ pub mod settings;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use client::{Client, UserStore};
|
use client::{Client, UserStore};
|
||||||
use futures::{future::BoxFuture, stream::BoxStream};
|
use futures::{future::BoxFuture, stream::BoxStream};
|
||||||
use gpui::{
|
use gpui::{AnyView, AppContext, AsyncAppContext, Model, SharedString, Task, WindowContext};
|
||||||
AnyView, AppContext, AsyncAppContext, FocusHandle, Model, SharedString, Task, WindowContext,
|
|
||||||
};
|
|
||||||
pub use model::*;
|
pub use model::*;
|
||||||
use project::Fs;
|
use project::Fs;
|
||||||
use proto::Plan;
|
use proto::Plan;
|
||||||
|
@ -110,7 +108,7 @@ pub trait LanguageModelProvider: 'static {
|
||||||
fn load_model(&self, _model: Arc<dyn LanguageModel>, _cx: &AppContext) {}
|
fn load_model(&self, _model: Arc<dyn LanguageModel>, _cx: &AppContext) {}
|
||||||
fn is_authenticated(&self, cx: &AppContext) -> bool;
|
fn is_authenticated(&self, cx: &AppContext) -> bool;
|
||||||
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>>;
|
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>>;
|
||||||
fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>);
|
fn configuration_view(&self, cx: &mut WindowContext) -> AnyView;
|
||||||
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>>;
|
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,8 @@ use collections::BTreeMap;
|
||||||
use editor::{Editor, EditorElement, EditorStyle};
|
use editor::{Editor, EditorElement, EditorStyle};
|
||||||
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyView, AppContext, AsyncAppContext, FocusHandle, FocusableView, FontStyle, ModelContext,
|
AnyView, AppContext, AsyncAppContext, FontStyle, ModelContext, Subscription, Task, TextStyle,
|
||||||
Subscription, Task, TextStyle, View, WhiteSpace,
|
View, WhiteSpace,
|
||||||
};
|
};
|
||||||
use http_client::HttpClient;
|
use http_client::HttpClient;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
|
@ -19,6 +19,7 @@ use std::{sync::Arc, time::Duration};
|
||||||
use strum::IntoEnumIterator;
|
use strum::IntoEnumIterator;
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::{prelude::*, Indicator};
|
use ui::{prelude::*, Indicator};
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
const PROVIDER_ID: &str = "anthropic";
|
const PROVIDER_ID: &str = "anthropic";
|
||||||
const PROVIDER_NAME: &str = "Anthropic";
|
const PROVIDER_NAME: &str = "Anthropic";
|
||||||
|
@ -83,6 +84,34 @@ impl State {
|
||||||
fn is_authenticated(&self) -> bool {
|
fn is_authenticated(&self) -> bool {
|
||||||
self.api_key.is_some()
|
self.api_key.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn authenticate(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||||
|
if self.is_authenticated() {
|
||||||
|
Task::ready(Ok(()))
|
||||||
|
} else {
|
||||||
|
let api_url = AllLanguageModelSettings::get_global(cx)
|
||||||
|
.anthropic
|
||||||
|
.api_url
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let api_key = if let Ok(api_key) = std::env::var("ANTHROPIC_API_KEY") {
|
||||||
|
api_key
|
||||||
|
} else {
|
||||||
|
let (_, api_key) = cx
|
||||||
|
.update(|cx| cx.read_credentials(&api_url))?
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("credentials not found"))?;
|
||||||
|
String::from_utf8(api_key)?
|
||||||
|
};
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.api_key = Some(api_key);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AnthropicLanguageModelProvider {
|
impl AnthropicLanguageModelProvider {
|
||||||
|
@ -164,37 +193,12 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
||||||
if self.is_authenticated(cx) {
|
self.state.update(cx, |state, cx| state.authenticate(cx))
|
||||||
Task::ready(Ok(()))
|
|
||||||
} else {
|
|
||||||
let api_url = AllLanguageModelSettings::get_global(cx)
|
|
||||||
.anthropic
|
|
||||||
.api_url
|
|
||||||
.clone();
|
|
||||||
let state = self.state.clone();
|
|
||||||
cx.spawn(|mut cx| async move {
|
|
||||||
let api_key = if let Ok(api_key) = std::env::var("ANTHROPIC_API_KEY") {
|
|
||||||
api_key
|
|
||||||
} else {
|
|
||||||
let (_, api_key) = cx
|
|
||||||
.update(|cx| cx.read_credentials(&api_url))?
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow!("credentials not found"))?;
|
|
||||||
String::from_utf8(api_key)?
|
|
||||||
};
|
|
||||||
|
|
||||||
state.update(&mut cx, |this, cx| {
|
|
||||||
this.api_key = Some(api_key);
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
|
fn configuration_view(&self, cx: &mut WindowContext) -> AnyView {
|
||||||
let view = cx.new_view(|cx| ConfigurationView::new(self.state.clone(), cx));
|
cx.new_view(|cx| ConfigurationView::new(self.state.clone(), cx))
|
||||||
let focus_handle = view.focus_handle(cx);
|
.into()
|
||||||
(view.into(), Some(focus_handle))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
||||||
|
@ -383,33 +387,46 @@ impl LanguageModel for AnthropicModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ConfigurationView {
|
struct ConfigurationView {
|
||||||
focus_handle: FocusHandle,
|
|
||||||
api_key_editor: View<Editor>,
|
api_key_editor: View<Editor>,
|
||||||
state: gpui::Model<State>,
|
state: gpui::Model<State>,
|
||||||
|
load_credentials_task: Option<Task<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigurationView {
|
impl ConfigurationView {
|
||||||
fn new(state: gpui::Model<State>, cx: &mut ViewContext<Self>) -> Self {
|
const PLACEHOLDER_TEXT: &'static str = "sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
|
||||||
let focus_handle = cx.focus_handle();
|
|
||||||
|
|
||||||
cx.on_focus(&focus_handle, |this, cx| {
|
fn new(state: gpui::Model<State>, cx: &mut ViewContext<Self>) -> Self {
|
||||||
if this.should_render_editor(cx) {
|
cx.observe(&state, |_, _, cx| {
|
||||||
this.api_key_editor.read(cx).focus_handle(cx).focus(cx)
|
cx.notify();
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
let load_credentials_task = Some(cx.spawn({
|
||||||
|
let state = state.clone();
|
||||||
|
|this, mut cx| async move {
|
||||||
|
if let Some(task) = state
|
||||||
|
.update(&mut cx, |state, cx| state.authenticate(cx))
|
||||||
|
.log_err()
|
||||||
|
{
|
||||||
|
// We don't log an error, because "not signed in" is also an error.
|
||||||
|
let _ = task.await;
|
||||||
|
}
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.load_credentials_task = None;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
api_key_editor: cx.new_view(|cx| {
|
api_key_editor: cx.new_view(|cx| {
|
||||||
let mut editor = Editor::single_line(cx);
|
let mut editor = Editor::single_line(cx);
|
||||||
editor.set_placeholder_text(
|
editor.set_placeholder_text(Self::PLACEHOLDER_TEXT, cx);
|
||||||
"sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
editor
|
editor
|
||||||
}),
|
}),
|
||||||
focus_handle,
|
|
||||||
state,
|
state,
|
||||||
|
load_credentials_task,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -419,17 +436,30 @@ impl ConfigurationView {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.state
|
let state = self.state.clone();
|
||||||
.update(cx, |state, cx| state.set_api_key(api_key, cx))
|
cx.spawn(|_, mut cx| async move {
|
||||||
|
state
|
||||||
|
.update(&mut cx, |state, cx| state.set_api_key(api_key, cx))?
|
||||||
|
.await
|
||||||
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset_api_key(&mut self, cx: &mut ViewContext<Self>) {
|
fn reset_api_key(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
self.api_key_editor
|
self.api_key_editor
|
||||||
.update(cx, |editor, cx| editor.set_text("", cx));
|
.update(cx, |editor, cx| editor.set_text("", cx));
|
||||||
self.state
|
|
||||||
.update(cx, |state, cx| state.reset_api_key(cx))
|
let state = self.state.clone();
|
||||||
|
cx.spawn(|_, mut cx| async move {
|
||||||
|
state
|
||||||
|
.update(&mut cx, |state, cx| state.reset_api_key(cx))?
|
||||||
|
.await
|
||||||
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_api_key_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render_api_key_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
@ -464,12 +494,6 @@ impl ConfigurationView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FocusableView for ConfigurationView {
|
|
||||||
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
|
||||||
self.focus_handle.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for ConfigurationView {
|
impl Render for ConfigurationView {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
const INSTRUCTIONS: [&str; 4] = [
|
const INSTRUCTIONS: [&str; 4] = [
|
||||||
|
@ -479,10 +503,10 @@ impl Render for ConfigurationView {
|
||||||
"Paste your Anthropic API key below and hit enter to use the assistant:",
|
"Paste your Anthropic API key below and hit enter to use the assistant:",
|
||||||
];
|
];
|
||||||
|
|
||||||
if self.should_render_editor(cx) {
|
if self.load_credentials_task.is_some() {
|
||||||
|
div().child(Label::new("Loading credentials...")).into_any()
|
||||||
|
} else if self.should_render_editor(cx) {
|
||||||
v_flex()
|
v_flex()
|
||||||
.id("anthropic-configuration-view")
|
|
||||||
.track_focus(&self.focus_handle)
|
|
||||||
.size_full()
|
.size_full()
|
||||||
.on_action(cx.listener(Self::save_api_key))
|
.on_action(cx.listener(Self::save_api_key))
|
||||||
.children(
|
.children(
|
||||||
|
@ -507,15 +531,13 @@ impl Render for ConfigurationView {
|
||||||
.into_any()
|
.into_any()
|
||||||
} else {
|
} else {
|
||||||
h_flex()
|
h_flex()
|
||||||
.id("anthropic-configuration-view")
|
|
||||||
.track_focus(&self.focus_handle)
|
|
||||||
.size_full()
|
.size_full()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(Indicator::dot().color(Color::Success))
|
.child(Indicator::dot().color(Color::Success))
|
||||||
.child(Label::new("API Key configured").size(LabelSize::Small)),
|
.child(Label::new("API key configured").size(LabelSize::Small)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("reset-key", "Reset key")
|
Button::new("reset-key", "Reset key")
|
||||||
|
|
|
@ -8,9 +8,7 @@ use anyhow::{anyhow, Context as _, Result};
|
||||||
use client::{Client, UserStore};
|
use client::{Client, UserStore};
|
||||||
use collections::BTreeMap;
|
use collections::BTreeMap;
|
||||||
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
||||||
use gpui::{
|
use gpui::{AnyView, AppContext, AsyncAppContext, Model, ModelContext, Subscription, Task};
|
||||||
AnyView, AppContext, AsyncAppContext, FocusHandle, Model, ModelContext, Subscription, Task,
|
|
||||||
};
|
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{Settings, SettingsStore};
|
use settings::{Settings, SettingsStore};
|
||||||
|
@ -60,8 +58,8 @@ pub struct State {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
fn is_connected(&self) -> bool {
|
fn is_signed_out(&self) -> bool {
|
||||||
self.status.is_connected()
|
self.status.is_signed_out()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn authenticate(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
fn authenticate(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||||
|
@ -191,20 +189,18 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_authenticated(&self, cx: &AppContext) -> bool {
|
fn is_authenticated(&self, cx: &AppContext) -> bool {
|
||||||
self.state.read(cx).status.is_connected()
|
!self.state.read(cx).is_signed_out()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn authenticate(&self, _cx: &mut AppContext) -> Task<Result<()>> {
|
fn authenticate(&self, _cx: &mut AppContext) -> Task<Result<()>> {
|
||||||
Task::ready(Ok(()))
|
Task::ready(Ok(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
|
fn configuration_view(&self, cx: &mut WindowContext) -> AnyView {
|
||||||
let view = cx
|
cx.new_view(|_cx| ConfigurationView {
|
||||||
.new_view(|_cx| ConfigurationView {
|
|
||||||
state: self.state.clone(),
|
state: self.state.clone(),
|
||||||
})
|
})
|
||||||
.into();
|
.into()
|
||||||
(view, None)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset_credentials(&self, _cx: &mut AppContext) -> Task<Result<()>> {
|
fn reset_credentials(&self, _cx: &mut AppContext) -> Task<Result<()>> {
|
||||||
|
@ -439,7 +435,7 @@ impl Render for ConfigurationView {
|
||||||
const ZED_AI_URL: &str = "https://zed.dev/ai";
|
const ZED_AI_URL: &str = "https://zed.dev/ai";
|
||||||
const ACCOUNT_SETTINGS_URL: &str = "https://zed.dev/account";
|
const ACCOUNT_SETTINGS_URL: &str = "https://zed.dev/account";
|
||||||
|
|
||||||
let is_connected = self.state.read(cx).is_connected();
|
let is_connected = self.state.read(cx).is_signed_out();
|
||||||
let plan = self.state.read(cx).user_store.read(cx).current_plan();
|
let plan = self.state.read(cx).user_store.read(cx).current_plan();
|
||||||
|
|
||||||
let is_pro = plan == Some(proto::Plan::ZedPro);
|
let is_pro = plan == Some(proto::Plan::ZedPro);
|
||||||
|
|
|
@ -11,8 +11,8 @@ use futures::future::BoxFuture;
|
||||||
use futures::stream::BoxStream;
|
use futures::stream::BoxStream;
|
||||||
use futures::{FutureExt, StreamExt};
|
use futures::{FutureExt, StreamExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
percentage, svg, Animation, AnimationExt, AnyView, AppContext, AsyncAppContext, FocusHandle,
|
percentage, svg, Animation, AnimationExt, AnyView, AppContext, AsyncAppContext, Model, Render,
|
||||||
Model, Render, Subscription, Task, Transformation,
|
Subscription, Task, Transformation,
|
||||||
};
|
};
|
||||||
use settings::{Settings, SettingsStore};
|
use settings::{Settings, SettingsStore};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
@ -132,10 +132,9 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
|
||||||
Task::ready(result)
|
Task::ready(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
|
fn configuration_view(&self, cx: &mut WindowContext) -> AnyView {
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
let view = cx.new_view(|cx| ConfigurationView::new(state, cx)).into();
|
cx.new_view(|cx| ConfigurationView::new(state, cx)).into()
|
||||||
(view, None)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset_credentials(&self, _cx: &mut AppContext) -> Task<Result<()>> {
|
fn reset_credentials(&self, _cx: &mut AppContext) -> Task<Result<()>> {
|
||||||
|
|
|
@ -6,7 +6,7 @@ use crate::{
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use futures::{channel::mpsc, future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
use futures::{channel::mpsc, future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
||||||
use gpui::{AnyView, AppContext, AsyncAppContext, FocusHandle, Task};
|
use gpui::{AnyView, AppContext, AsyncAppContext, Task};
|
||||||
use http_client::Result;
|
use http_client::Result;
|
||||||
use std::{
|
use std::{
|
||||||
future,
|
future,
|
||||||
|
@ -66,7 +66,7 @@ impl LanguageModelProvider for FakeLanguageModelProvider {
|
||||||
Task::ready(Ok(()))
|
Task::ready(Ok(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn configuration_view(&self, _: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
|
fn configuration_view(&self, _: &mut WindowContext) -> AnyView {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@ use editor::{Editor, EditorElement, EditorStyle};
|
||||||
use futures::{future::BoxFuture, FutureExt, StreamExt};
|
use futures::{future::BoxFuture, FutureExt, StreamExt};
|
||||||
use google_ai::stream_generate_content;
|
use google_ai::stream_generate_content;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyView, AppContext, AsyncAppContext, FocusHandle, FocusableView, FontStyle, ModelContext,
|
AnyView, AppContext, AsyncAppContext, FontStyle, ModelContext, Subscription, Task, TextStyle,
|
||||||
Subscription, Task, TextStyle, View, WhiteSpace,
|
View, WhiteSpace,
|
||||||
};
|
};
|
||||||
use http_client::HttpClient;
|
use http_client::HttpClient;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
|
@ -65,6 +65,48 @@ impl State {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_api_key(&mut self, api_key: String, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||||
|
let settings = &AllLanguageModelSettings::get_global(cx).google;
|
||||||
|
let write_credentials =
|
||||||
|
cx.write_credentials(&settings.api_url, "Bearer", api_key.as_bytes());
|
||||||
|
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
write_credentials.await?;
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.api_key = Some(api_key);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn authenticate(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||||
|
if self.is_authenticated() {
|
||||||
|
Task::ready(Ok(()))
|
||||||
|
} else {
|
||||||
|
let api_url = AllLanguageModelSettings::get_global(cx)
|
||||||
|
.google
|
||||||
|
.api_url
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let api_key = if let Ok(api_key) = std::env::var("GOOGLE_AI_API_KEY") {
|
||||||
|
api_key
|
||||||
|
} else {
|
||||||
|
let (_, api_key) = cx
|
||||||
|
.update(|cx| cx.read_credentials(&api_url))?
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("credentials not found"))?;
|
||||||
|
String::from_utf8(api_key)?
|
||||||
|
};
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.api_key = Some(api_key);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GoogleLanguageModelProvider {
|
impl GoogleLanguageModelProvider {
|
||||||
|
@ -144,38 +186,12 @@ impl LanguageModelProvider for GoogleLanguageModelProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
||||||
if self.is_authenticated(cx) {
|
self.state.update(cx, |state, cx| state.authenticate(cx))
|
||||||
Task::ready(Ok(()))
|
|
||||||
} else {
|
|
||||||
let api_url = AllLanguageModelSettings::get_global(cx)
|
|
||||||
.google
|
|
||||||
.api_url
|
|
||||||
.clone();
|
|
||||||
let state = self.state.clone();
|
|
||||||
cx.spawn(|mut cx| async move {
|
|
||||||
let api_key = if let Ok(api_key) = std::env::var("GOOGLE_AI_API_KEY") {
|
|
||||||
api_key
|
|
||||||
} else {
|
|
||||||
let (_, api_key) = cx
|
|
||||||
.update(|cx| cx.read_credentials(&api_url))?
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow!("credentials not found"))?;
|
|
||||||
String::from_utf8(api_key)?
|
|
||||||
};
|
|
||||||
|
|
||||||
state.update(&mut cx, |this, cx| {
|
|
||||||
this.api_key = Some(api_key);
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
|
fn configuration_view(&self, cx: &mut WindowContext) -> AnyView {
|
||||||
let view = cx.new_view(|cx| ConfigurationView::new(self.state.clone(), cx));
|
cx.new_view(|cx| ConfigurationView::new(self.state.clone(), cx))
|
||||||
|
.into()
|
||||||
let focus_handle = view.focus_handle(cx);
|
|
||||||
(view.into(), Some(focus_handle))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
||||||
|
@ -292,22 +308,36 @@ impl LanguageModel for GoogleLanguageModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ConfigurationView {
|
struct ConfigurationView {
|
||||||
focus_handle: FocusHandle,
|
|
||||||
api_key_editor: View<Editor>,
|
api_key_editor: View<Editor>,
|
||||||
state: gpui::Model<State>,
|
state: gpui::Model<State>,
|
||||||
|
load_credentials_task: Option<Task<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigurationView {
|
impl ConfigurationView {
|
||||||
fn new(state: gpui::Model<State>, cx: &mut ViewContext<Self>) -> Self {
|
fn new(state: gpui::Model<State>, cx: &mut ViewContext<Self>) -> Self {
|
||||||
let focus_handle = cx.focus_handle();
|
cx.observe(&state, |_, _, cx| {
|
||||||
|
cx.notify();
|
||||||
cx.on_focus(&focus_handle, |this, cx| {
|
|
||||||
if this.should_render_editor(cx) {
|
|
||||||
this.api_key_editor.read(cx).focus_handle(cx).focus(cx)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
let load_credentials_task = Some(cx.spawn({
|
||||||
|
let state = state.clone();
|
||||||
|
|this, mut cx| async move {
|
||||||
|
if let Some(task) = state
|
||||||
|
.update(&mut cx, |state, cx| state.authenticate(cx))
|
||||||
|
.log_err()
|
||||||
|
{
|
||||||
|
// We don't log an error, because "not signed in" is also an error.
|
||||||
|
let _ = task.await;
|
||||||
|
}
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.load_credentials_task = None;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
api_key_editor: cx.new_view(|cx| {
|
api_key_editor: cx.new_view(|cx| {
|
||||||
let mut editor = Editor::single_line(cx);
|
let mut editor = Editor::single_line(cx);
|
||||||
|
@ -315,7 +345,7 @@ impl ConfigurationView {
|
||||||
editor
|
editor
|
||||||
}),
|
}),
|
||||||
state,
|
state,
|
||||||
focus_handle,
|
load_credentials_task,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -325,26 +355,30 @@ impl ConfigurationView {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let settings = &AllLanguageModelSettings::get_global(cx).google;
|
|
||||||
let write_credentials =
|
|
||||||
cx.write_credentials(&settings.api_url, "Bearer", api_key.as_bytes());
|
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
cx.spawn(|_, mut cx| async move {
|
cx.spawn(|_, mut cx| async move {
|
||||||
write_credentials.await?;
|
state
|
||||||
state.update(&mut cx, |this, cx| {
|
.update(&mut cx, |state, cx| state.set_api_key(api_key, cx))?
|
||||||
this.api_key = Some(api_key);
|
.await
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset_api_key(&mut self, cx: &mut ViewContext<Self>) {
|
fn reset_api_key(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
self.api_key_editor
|
self.api_key_editor
|
||||||
.update(cx, |editor, cx| editor.set_text("", cx));
|
.update(cx, |editor, cx| editor.set_text("", cx));
|
||||||
self.state
|
|
||||||
.update(cx, |state, cx| state.reset_api_key(cx))
|
let state = self.state.clone();
|
||||||
|
cx.spawn(|_, mut cx| async move {
|
||||||
|
state
|
||||||
|
.update(&mut cx, |state, cx| state.reset_api_key(cx))?
|
||||||
|
.await
|
||||||
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_api_key_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render_api_key_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
@ -379,12 +413,6 @@ impl ConfigurationView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FocusableView for ConfigurationView {
|
|
||||||
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
|
||||||
self.focus_handle.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for ConfigurationView {
|
impl Render for ConfigurationView {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
const INSTRUCTIONS: [&str; 4] = [
|
const INSTRUCTIONS: [&str; 4] = [
|
||||||
|
@ -394,10 +422,10 @@ impl Render for ConfigurationView {
|
||||||
"Paste your Google AI API key below and hit enter to use the assistant:",
|
"Paste your Google AI API key below and hit enter to use the assistant:",
|
||||||
];
|
];
|
||||||
|
|
||||||
if self.should_render_editor(cx) {
|
if self.load_credentials_task.is_some() {
|
||||||
|
div().child(Label::new("Loading credentials...")).into_any()
|
||||||
|
} else if self.should_render_editor(cx) {
|
||||||
v_flex()
|
v_flex()
|
||||||
.id("google-ai-configuration-view")
|
|
||||||
.track_focus(&self.focus_handle)
|
|
||||||
.size_full()
|
.size_full()
|
||||||
.on_action(cx.listener(Self::save_api_key))
|
.on_action(cx.listener(Self::save_api_key))
|
||||||
.children(
|
.children(
|
||||||
|
@ -422,15 +450,13 @@ impl Render for ConfigurationView {
|
||||||
.into_any()
|
.into_any()
|
||||||
} else {
|
} else {
|
||||||
h_flex()
|
h_flex()
|
||||||
.id("google-ai-configuration-view")
|
|
||||||
.track_focus(&self.focus_handle)
|
|
||||||
.size_full()
|
.size_full()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(Indicator::dot().color(Color::Success))
|
.child(Indicator::dot().color(Color::Success))
|
||||||
.child(Label::new("API Key configured").size(LabelSize::Small)),
|
.child(Label::new("API key configured").size(LabelSize::Small)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("reset-key", "Reset key")
|
Button::new("reset-key", "Reset key")
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
||||||
use gpui::{AnyView, AppContext, AsyncAppContext, FocusHandle, ModelContext, Subscription, Task};
|
use gpui::{AnyView, AppContext, AsyncAppContext, ModelContext, Subscription, Task};
|
||||||
use http_client::HttpClient;
|
use http_client::HttpClient;
|
||||||
use ollama::{
|
use ollama::{
|
||||||
get_models, preload_model, stream_chat_completion, ChatMessage, ChatOptions, ChatRequest,
|
get_models, preload_model, stream_chat_completion, ChatMessage, ChatOptions, ChatRequest,
|
||||||
|
@ -8,6 +8,7 @@ use ollama::{
|
||||||
use settings::{Settings, SettingsStore};
|
use settings::{Settings, SettingsStore};
|
||||||
use std::{future, sync::Arc, time::Duration};
|
use std::{future, sync::Arc, time::Duration};
|
||||||
use ui::{prelude::*, ButtonLike, Indicator};
|
use ui::{prelude::*, ButtonLike, Indicator};
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
settings::AllLanguageModelSettings, LanguageModel, LanguageModelId, LanguageModelName,
|
settings::AllLanguageModelSettings, LanguageModel, LanguageModelId, LanguageModelName,
|
||||||
|
@ -70,6 +71,14 @@ impl State {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn authenticate(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||||
|
if self.is_authenticated() {
|
||||||
|
Task::ready(Ok(()))
|
||||||
|
} else {
|
||||||
|
self.fetch_models(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OllamaLanguageModelProvider {
|
impl OllamaLanguageModelProvider {
|
||||||
|
@ -142,19 +151,12 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
||||||
if self.is_authenticated(cx) {
|
self.state.update(cx, |state, cx| state.authenticate(cx))
|
||||||
Task::ready(Ok(()))
|
|
||||||
} else {
|
|
||||||
self.state.update(cx, |state, cx| state.fetch_models(cx))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
|
fn configuration_view(&self, cx: &mut WindowContext) -> AnyView {
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
(
|
cx.new_view(|cx| ConfigurationView::new(state, cx)).into()
|
||||||
cx.new_view(|cx| ConfigurationView::new(state, cx)).into(),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
||||||
|
@ -296,11 +298,32 @@ impl LanguageModel for OllamaLanguageModel {
|
||||||
|
|
||||||
struct ConfigurationView {
|
struct ConfigurationView {
|
||||||
state: gpui::Model<State>,
|
state: gpui::Model<State>,
|
||||||
|
loading_models_task: Option<Task<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigurationView {
|
impl ConfigurationView {
|
||||||
pub fn new(state: gpui::Model<State>, _cx: &mut ViewContext<Self>) -> Self {
|
pub fn new(state: gpui::Model<State>, cx: &mut ViewContext<Self>) -> Self {
|
||||||
Self { state }
|
let loading_models_task = Some(cx.spawn({
|
||||||
|
let state = state.clone();
|
||||||
|
|this, mut cx| async move {
|
||||||
|
if let Some(task) = state
|
||||||
|
.update(&mut cx, |state, cx| state.authenticate(cx))
|
||||||
|
.log_err()
|
||||||
|
{
|
||||||
|
task.await.log_err();
|
||||||
|
}
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.loading_models_task = None;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
state,
|
||||||
|
loading_models_task,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn retry_connection(&self, cx: &mut WindowContext) {
|
fn retry_connection(&self, cx: &mut WindowContext) {
|
||||||
|
@ -321,6 +344,9 @@ impl Render for ConfigurationView {
|
||||||
let mut inline_code_bg = cx.theme().colors().editor_background;
|
let mut inline_code_bg = cx.theme().colors().editor_background;
|
||||||
inline_code_bg.fade_out(0.5);
|
inline_code_bg.fade_out(0.5);
|
||||||
|
|
||||||
|
if self.loading_models_task.is_some() {
|
||||||
|
div().child(Label::new("Loading models...")).into_any()
|
||||||
|
} else {
|
||||||
v_flex()
|
v_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
|
@ -367,7 +393,10 @@ impl Render for ConfigurationView {
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
this.child(
|
this.child(
|
||||||
Button::new("download_ollama_button", "Download Ollama")
|
Button::new(
|
||||||
|
"download_ollama_button",
|
||||||
|
"Download Ollama",
|
||||||
|
)
|
||||||
.style(ButtonStyle::Subtle)
|
.style(ButtonStyle::Subtle)
|
||||||
.icon(IconName::ExternalLink)
|
.icon(IconName::ExternalLink)
|
||||||
.icon_size(IconSize::XSmall)
|
.icon_size(IconSize::XSmall)
|
||||||
|
@ -412,3 +441,4 @@ impl Render for ConfigurationView {
|
||||||
.into_any()
|
.into_any()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,8 +3,8 @@ use collections::BTreeMap;
|
||||||
use editor::{Editor, EditorElement, EditorStyle};
|
use editor::{Editor, EditorElement, EditorStyle};
|
||||||
use futures::{future::BoxFuture, FutureExt, StreamExt};
|
use futures::{future::BoxFuture, FutureExt, StreamExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyView, AppContext, AsyncAppContext, FocusHandle, FocusableView, FontStyle, ModelContext,
|
AnyView, AppContext, AsyncAppContext, FontStyle, ModelContext, Subscription, Task, TextStyle,
|
||||||
Subscription, Task, TextStyle, View, WhiteSpace,
|
View, WhiteSpace,
|
||||||
};
|
};
|
||||||
use http_client::HttpClient;
|
use http_client::HttpClient;
|
||||||
use open_ai::stream_completion;
|
use open_ai::stream_completion;
|
||||||
|
@ -66,6 +66,46 @@ impl State {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_api_key(&mut self, api_key: String, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||||
|
let settings = &AllLanguageModelSettings::get_global(cx).openai;
|
||||||
|
let write_credentials =
|
||||||
|
cx.write_credentials(&settings.api_url, "Bearer", api_key.as_bytes());
|
||||||
|
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
write_credentials.await?;
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.api_key = Some(api_key);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn authenticate(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||||
|
if self.is_authenticated() {
|
||||||
|
Task::ready(Ok(()))
|
||||||
|
} else {
|
||||||
|
let api_url = AllLanguageModelSettings::get_global(cx)
|
||||||
|
.openai
|
||||||
|
.api_url
|
||||||
|
.clone();
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let api_key = if let Ok(api_key) = std::env::var("OPENAI_API_KEY") {
|
||||||
|
api_key
|
||||||
|
} else {
|
||||||
|
let (_, api_key) = cx
|
||||||
|
.update(|cx| cx.read_credentials(&api_url))?
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("credentials not found"))?;
|
||||||
|
String::from_utf8(api_key)?
|
||||||
|
};
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.api_key = Some(api_key);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OpenAiLanguageModelProvider {
|
impl OpenAiLanguageModelProvider {
|
||||||
|
@ -145,36 +185,12 @@ impl LanguageModelProvider for OpenAiLanguageModelProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
||||||
if self.is_authenticated(cx) {
|
self.state.update(cx, |state, cx| state.authenticate(cx))
|
||||||
Task::ready(Ok(()))
|
|
||||||
} else {
|
|
||||||
let api_url = AllLanguageModelSettings::get_global(cx)
|
|
||||||
.openai
|
|
||||||
.api_url
|
|
||||||
.clone();
|
|
||||||
let state = self.state.clone();
|
|
||||||
cx.spawn(|mut cx| async move {
|
|
||||||
let api_key = if let Ok(api_key) = std::env::var("OPENAI_API_KEY") {
|
|
||||||
api_key
|
|
||||||
} else {
|
|
||||||
let (_, api_key) = cx
|
|
||||||
.update(|cx| cx.read_credentials(&api_url))?
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow!("credentials not found"))?;
|
|
||||||
String::from_utf8(api_key)?
|
|
||||||
};
|
|
||||||
state.update(&mut cx, |this, cx| {
|
|
||||||
this.api_key = Some(api_key);
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
|
fn configuration_view(&self, cx: &mut WindowContext) -> AnyView {
|
||||||
let view = cx.new_view(|cx| ConfigurationView::new(self.state.clone(), cx));
|
cx.new_view(|cx| ConfigurationView::new(self.state.clone(), cx))
|
||||||
let focus_handle = view.focus_handle(cx);
|
.into()
|
||||||
(view.into(), Some(focus_handle))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
||||||
|
@ -302,33 +318,47 @@ pub fn count_open_ai_tokens(
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ConfigurationView {
|
struct ConfigurationView {
|
||||||
focus_handle: FocusHandle,
|
|
||||||
api_key_editor: View<Editor>,
|
api_key_editor: View<Editor>,
|
||||||
state: gpui::Model<State>,
|
state: gpui::Model<State>,
|
||||||
|
load_credentials_task: Option<Task<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigurationView {
|
impl ConfigurationView {
|
||||||
fn new(state: gpui::Model<State>, cx: &mut ViewContext<Self>) -> Self {
|
fn new(state: gpui::Model<State>, cx: &mut ViewContext<Self>) -> Self {
|
||||||
let focus_handle = cx.focus_handle();
|
let api_key_editor = cx.new_view(|cx| {
|
||||||
|
let mut editor = Editor::single_line(cx);
|
||||||
|
editor.set_placeholder_text("sk-000000000000000000000000000000000000000000000000", cx);
|
||||||
|
editor
|
||||||
|
});
|
||||||
|
|
||||||
cx.on_focus(&focus_handle, |this, cx| {
|
cx.observe(&state, |_, _, cx| {
|
||||||
if this.should_render_editor(cx) {
|
cx.notify();
|
||||||
this.api_key_editor.read(cx).focus_handle(cx).focus(cx)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
let load_credentials_task = Some(cx.spawn({
|
||||||
|
let state = state.clone();
|
||||||
|
|this, mut cx| async move {
|
||||||
|
if let Some(task) = state
|
||||||
|
.update(&mut cx, |state, cx| state.authenticate(cx))
|
||||||
|
.log_err()
|
||||||
|
{
|
||||||
|
// We don't log an error, because "not signed in" is also an error.
|
||||||
|
let _ = task.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.load_credentials_task = None;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
api_key_editor: cx.new_view(|cx| {
|
api_key_editor,
|
||||||
let mut editor = Editor::single_line(cx);
|
|
||||||
editor.set_placeholder_text(
|
|
||||||
"sk-000000000000000000000000000000000000000000000000",
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
editor
|
|
||||||
}),
|
|
||||||
state,
|
state,
|
||||||
focus_handle,
|
load_credentials_task,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -338,26 +368,30 @@ impl ConfigurationView {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let settings = &AllLanguageModelSettings::get_global(cx).openai;
|
|
||||||
let write_credentials =
|
|
||||||
cx.write_credentials(&settings.api_url, "Bearer", api_key.as_bytes());
|
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
cx.spawn(|_, mut cx| async move {
|
cx.spawn(|_, mut cx| async move {
|
||||||
write_credentials.await?;
|
state
|
||||||
state.update(&mut cx, |this, cx| {
|
.update(&mut cx, |state, cx| state.set_api_key(api_key, cx))?
|
||||||
this.api_key = Some(api_key);
|
.await
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset_api_key(&mut self, cx: &mut ViewContext<Self>) {
|
fn reset_api_key(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
self.api_key_editor
|
self.api_key_editor
|
||||||
.update(cx, |editor, cx| editor.set_text("", cx));
|
.update(cx, |editor, cx| editor.set_text("", cx));
|
||||||
self.state.update(cx, |state, cx| {
|
|
||||||
state.reset_api_key(cx).detach_and_log_err(cx);
|
let state = self.state.clone();
|
||||||
|
cx.spawn(|_, mut cx| async move {
|
||||||
|
state
|
||||||
|
.update(&mut cx, |state, cx| state.reset_api_key(cx))?
|
||||||
|
.await
|
||||||
})
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_api_key_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render_api_key_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
@ -392,12 +426,6 @@ impl ConfigurationView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FocusableView for ConfigurationView {
|
|
||||||
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
|
||||||
self.focus_handle.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for ConfigurationView {
|
impl Render for ConfigurationView {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
const INSTRUCTIONS: [&str; 6] = [
|
const INSTRUCTIONS: [&str; 6] = [
|
||||||
|
@ -409,10 +437,10 @@ impl Render for ConfigurationView {
|
||||||
"Paste your OpenAI API key below and hit enter to use the assistant:",
|
"Paste your OpenAI API key below and hit enter to use the assistant:",
|
||||||
];
|
];
|
||||||
|
|
||||||
if self.should_render_editor(cx) {
|
if self.load_credentials_task.is_some() {
|
||||||
|
div().child(Label::new("Loading credentials...")).into_any()
|
||||||
|
} else if self.should_render_editor(cx) {
|
||||||
v_flex()
|
v_flex()
|
||||||
.id("openai-configuration-view")
|
|
||||||
.track_focus(&self.focus_handle)
|
|
||||||
.size_full()
|
.size_full()
|
||||||
.on_action(cx.listener(Self::save_api_key))
|
.on_action(cx.listener(Self::save_api_key))
|
||||||
.children(
|
.children(
|
||||||
|
@ -437,15 +465,13 @@ impl Render for ConfigurationView {
|
||||||
.into_any()
|
.into_any()
|
||||||
} else {
|
} else {
|
||||||
h_flex()
|
h_flex()
|
||||||
.id("openai-configuration-view")
|
|
||||||
.track_focus(&self.focus_handle)
|
|
||||||
.size_full()
|
.size_full()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(Indicator::dot().color(Color::Success))
|
.child(Indicator::dot().color(Color::Success))
|
||||||
.child(Label::new("API Key configured").size(LabelSize::Small)),
|
.child(Label::new("API key configured").size(LabelSize::Small)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("reset-key", "Reset key")
|
Button::new("reset-key", "Reset key")
|
||||||
|
|
|
@ -166,11 +166,8 @@ impl LanguageModelRegistry {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn provider(
|
pub fn provider(&self, id: &LanguageModelProviderId) -> Option<Arc<dyn LanguageModelProvider>> {
|
||||||
&self,
|
self.providers.get(id).cloned()
|
||||||
name: &LanguageModelProviderId,
|
|
||||||
) -> Option<Arc<dyn LanguageModelProvider>> {
|
|
||||||
self.providers.get(name).cloned()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_active_model(
|
pub fn select_active_model(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue