Detailed changes
@@ -31,7 +31,7 @@ use editor::{
use editor::{display_map::CreaseId, FoldPlaceholder};
use fs::Fs;
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,
FocusHandle, FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Pixels,
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Transformation,
@@ -41,12 +41,16 @@ use indexed_docs::IndexedDocsStore;
use language::{
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 picker::{Picker, PickerDelegate};
use project::{Project, ProjectLspAdapterDelegate};
use search::{buffer_search::DivRegistrar, BufferSearchBar};
use settings::{update_settings_file, Settings};
+use smol::stream::StreamExt;
use std::{
borrow::Cow,
cmp::{self, Ordering},
@@ -140,6 +144,8 @@ pub struct AssistantPanel {
model_summary_editor: View<Editor>,
authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
configuration_subscription: Option<Subscription>,
+ watch_client_status: Option<Task<()>>,
+ nudge_sign_in: bool,
}
#[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 {
pane,
workspace: workspace.weak_handle(),
@@ -425,17 +463,11 @@ impl AssistantPanel {
model_summary_editor,
authenticate_provider_task: 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
}
@@ -623,12 +655,7 @@ impl AssistantPanel {
provider.id(),
cx.spawn(|this, mut cx| async move {
let _ = load_credentials.await;
- 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.update(&mut cx, |this, _cx| {
this.authenticate_provider_task = None;
})
.log_err();
@@ -908,20 +935,11 @@ impl AssistantPanel {
}
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>) {
- 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>,
- ) {
+ fn show_configuration_tab(&mut self, cx: &mut ViewContext<Self>) {
let configuration_item_ix = self
.pane
.read(cx)
@@ -931,24 +949,9 @@ impl AssistantPanel {
if let Some(configuration_item_ix) = configuration_item_ix {
self.pane.update(cx, |pane, 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 {
- let configuration = cx.new_view(|cx| {
- let mut view = ConfigurationView::new(cx);
- if let Some(provider) = provider {
- view.set_active_tab(provider, cx);
- }
- view
- });
+ let configuration = cx.new_view(|cx| ConfigurationView::new(cx));
self.configuration_subscription = Some(cx.subscribe(
&configuration,
|this, _, event: &ConfigurationViewEvent, cx| match event {
@@ -1018,13 +1021,6 @@ impl AssistantPanel {
.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>> {
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| {
this.new_context(cx);
}))
- .on_action(cx.listener(|this, _: &ShowConfiguration, cx| {
- this.show_configuration_for_active_provider(cx)
- }))
+ .on_action(
+ cx.listener(|this, _: &ShowConfiguration, cx| this.show_configuration_tab(cx)),
+ )
.on_action(cx.listener(AssistantPanel::deploy_history))
.on_action(cx.listener(AssistantPanel::deploy_prompt_library))
.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>) {
if active {
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);
@@ -3044,211 +3033,122 @@ 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 {
focus_handle: FocusHandle,
- active_tab: Option<ActiveTab>,
+ configuration_views: HashMap<LanguageModelProviderId, AnyView>,
+ _registry_subscription: Subscription,
}
impl ConfigurationView {
fn new(cx: &mut ViewContext<Self>) -> Self {
let focus_handle = cx.focus_handle();
- cx.on_focus(&focus_handle, |this, cx| {
- if let Some(focus_handle) = this
- .active_tab
- .as_ref()
- .and_then(|tab| tab.focus_handle.as_ref())
- {
- focus_handle.focus(cx);
- }
- })
- .detach();
+ let registry_subscription = cx.subscribe(
+ &LanguageModelRegistry::global(cx),
+ |this, _, event: &language_model::Event, cx| match event {
+ language_model::Event::AddedProvider(provider_id) => {
+ let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
+ if let Some(provider) = provider {
+ this.add_configuration_view(&provider, cx);
+ }
+ }
+ language_model::Event::RemovedProvider(provider_id) => {
+ this.remove_configuration_view(provider_id);
+ }
+ _ => {}
+ },
+ );
let mut this = Self {
focus_handle,
- active_tab: None,
+ configuration_views: HashMap::default(),
+ _registry_subscription: registry_subscription,
};
+ this.build_configuration_views(cx);
+ this
+ }
+ fn build_configuration_views(&mut self, cx: &mut ViewContext<Self>) {
let providers = LanguageModelRegistry::read_global(cx).providers();
- if !providers.is_empty() {
- this.set_active_tab(providers[0].clone(), cx);
+ for provider in providers {
+ self.add_configuration_view(&provider, cx);
}
+ }
- this
+ fn remove_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
+ self.configuration_views.remove(provider_id);
}
- fn set_active_tab(
+ fn add_configuration_view(
&mut self,
- provider: Arc<dyn LanguageModelProvider>,
+ provider: &Arc<dyn LanguageModelProvider>,
cx: &mut ViewContext<Self>,
) {
- let (view, focus_handle) = provider.configuration_view(cx);
-
- if let Some(focus_handle) = &focus_handle {
- focus_handle.focus(cx);
- } else {
- self.focus_handle.focus(cx);
- }
-
- let load_credentials = provider.authenticate(cx);
- let load_credentials_task = cx.spawn(|this, mut cx| async move {
- let _ = load_credentials.await;
- this.update(&mut cx, |this, cx| {
- if let Some(active_tab) = &mut this.active_tab {
- 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 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()
- .gap_4()
- .child(
- div()
- .p(Spacing::Large.rems(cx))
- .bg(cx.theme().colors().title_bar_background)
- .border_1()
- .border_color(cx.theme().colors().border_variant)
- .rounded_md()
- .child(content),
- )
- .when(
- !show_spinner && provider.is_authenticated(cx),
- move |this| {
- this.child(
- h_flex().justify_end().child(
- Button::new(
- "new-context",
- format!("Open new context using {}", provider_name),
- )
- .icon_position(IconPosition::Start)
- .icon(IconName::Plus)
- .style(ButtonStyle::Filled)
- .layer(ElevationIndex::ModalSurface)
- .on_click(cx.listener(
- move |_, _, cx| {
- cx.emit(ConfigurationViewEvent::NewProviderContextEditor(
- provider.clone(),
- ))
- },
- )),
- ),
- )
- },
- ),
- )
+ let configuration_view = provider.configuration_view(cx);
+ self.configuration_views
+ .insert(provider.id(), configuration_view);
}
- fn render_tab(
- &self,
+ fn render_provider_view(
+ &mut 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);
- }
- }))
+ ) -> Div {
+ let provider_name = provider.name().0.clone();
+ let configuration_view = self.configuration_views.get(&provider.id()).cloned();
+
+ v_flex()
+ .gap_4()
+ .child(Headline::new(provider_name.clone()).size(HeadlineSize::Medium))
.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)
- })
+ .p(Spacing::Large.rems(cx))
+ .bg(cx.theme().colors().title_bar_background)
+ .border_1()
+ .border_color(cx.theme().colors().border_variant)
+ .rounded_md()
+ .when(configuration_view.is_none(), |this| {
+ this.child(div().child(Label::new(format!(
+ "No configuration view for {}",
+ provider_name
+ ))))
})
- .child(Label::new(provider.name().0).size(LabelSize::Small).color(
- if is_active {
- Color::Accent
- } else {
- Color::Default
- },
- )),
+ .when_some(configuration_view, |this, configuration_view| {
+ this.child(configuration_view)
+ }),
)
+ .when(provider.is_authenticated(cx), move |this| {
+ this.child(
+ h_flex().justify_end().child(
+ Button::new(
+ "new-context",
+ format!("Open new context using {}", provider_name),
+ )
+ .icon_position(IconPosition::Start)
+ .icon(IconName::Plus)
+ .style(ButtonStyle::Filled)
+ .layer(ElevationIndex::ModalSurface)
+ .on_click(cx.listener({
+ let provider = provider.clone();
+ move |_, _, cx| {
+ cx.emit(ConfigurationViewEvent::NewProviderContextEditor(
+ provider.clone(),
+ ))
+ }
+ })),
+ ),
+ )
+ })
}
}
impl Render for ConfigurationView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let providers = LanguageModelRegistry::read_global(cx).providers();
- let tabs = h_flex().mx_neg_1().gap_3().children(
- providers
- .iter()
- .map(|provider| self.render_tab(provider, cx)),
- );
+ let provider_views = providers
+ .into_iter()
+ .map(|provider| self.render_provider_view(&provider, cx))
+ .collect::<Vec<_>>();
v_flex()
.id("assistant-configuration-view")
@@ -3266,20 +3166,13 @@ impl Render for ConfigurationView {
.child(
v_flex()
.gap_2()
- .child(Headline::new("Configure providers").size(HeadlineSize::Small))
.child(
Label::new(
"At least one provider must be configured to use the assistant.",
)
.color(Color::Muted),
)
- .child(tabs)
- .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))
- }),
+ .child(v_flex().mt_2().gap_4().children(provider_views)),
)
}
}
@@ -9,9 +9,7 @@ pub mod settings;
use anyhow::Result;
use client::{Client, UserStore};
use futures::{future::BoxFuture, stream::BoxStream};
-use gpui::{
- AnyView, AppContext, AsyncAppContext, FocusHandle, Model, SharedString, Task, WindowContext,
-};
+use gpui::{AnyView, AppContext, AsyncAppContext, Model, SharedString, Task, WindowContext};
pub use model::*;
use project::Fs;
use proto::Plan;
@@ -110,7 +108,7 @@ pub trait LanguageModelProvider: 'static {
fn load_model(&self, _model: Arc<dyn LanguageModel>, _cx: &AppContext) {}
fn is_authenticated(&self, cx: &AppContext) -> bool;
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<()>>;
}
@@ -8,8 +8,8 @@ use collections::BTreeMap;
use editor::{Editor, EditorElement, EditorStyle};
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
use gpui::{
- AnyView, AppContext, AsyncAppContext, FocusHandle, FocusableView, FontStyle, ModelContext,
- Subscription, Task, TextStyle, View, WhiteSpace,
+ AnyView, AppContext, AsyncAppContext, FontStyle, ModelContext, Subscription, Task, TextStyle,
+ View, WhiteSpace,
};
use http_client::HttpClient;
use schemars::JsonSchema;
@@ -19,6 +19,7 @@ use std::{sync::Arc, time::Duration};
use strum::IntoEnumIterator;
use theme::ThemeSettings;
use ui::{prelude::*, Indicator};
+use util::ResultExt;
const PROVIDER_ID: &str = "anthropic";
const PROVIDER_NAME: &str = "Anthropic";
@@ -83,6 +84,34 @@ impl State {
fn is_authenticated(&self) -> bool {
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 {
@@ -164,37 +193,12 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider {
}
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> {
- if self.is_authenticated(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();
- })
- })
- }
+ self.state.update(cx, |state, cx| state.authenticate(cx))
}
- fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
- let view = cx.new_view(|cx| ConfigurationView::new(self.state.clone(), cx));
- let focus_handle = view.focus_handle(cx);
- (view.into(), Some(focus_handle))
+ fn configuration_view(&self, cx: &mut WindowContext) -> AnyView {
+ cx.new_view(|cx| ConfigurationView::new(self.state.clone(), cx))
+ .into()
}
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>> {
@@ -383,33 +387,46 @@ impl LanguageModel for AnthropicModel {
}
struct ConfigurationView {
- focus_handle: FocusHandle,
api_key_editor: View<Editor>,
state: gpui::Model<State>,
+ load_credentials_task: Option<Task<()>>,
}
impl ConfigurationView {
- fn new(state: gpui::Model<State>, cx: &mut ViewContext<Self>) -> Self {
- let focus_handle = cx.focus_handle();
+ const PLACEHOLDER_TEXT: &'static str = "sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
- cx.on_focus(&focus_handle, |this, cx| {
- if this.should_render_editor(cx) {
- this.api_key_editor.read(cx).focus_handle(cx).focus(cx)
- }
+ fn new(state: gpui::Model<State>, cx: &mut ViewContext<Self>) -> Self {
+ cx.observe(&state, |_, _, cx| {
+ cx.notify();
})
.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 {
api_key_editor: cx.new_view(|cx| {
let mut editor = Editor::single_line(cx);
- editor.set_placeholder_text(
- "sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
- cx,
- );
+ editor.set_placeholder_text(Self::PLACEHOLDER_TEXT, cx);
editor
}),
- focus_handle,
state,
+ load_credentials_task,
}
}
@@ -419,17 +436,30 @@ impl ConfigurationView {
return;
}
- self.state
- .update(cx, |state, cx| state.set_api_key(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.set_api_key(api_key, cx))?
+ .await
+ })
+ .detach_and_log_err(cx);
+
+ cx.notify();
}
fn reset_api_key(&mut self, cx: &mut ViewContext<Self>) {
self.api_key_editor
.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 {
@@ -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 {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
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:",
];
- 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()
- .id("anthropic-configuration-view")
- .track_focus(&self.focus_handle)
.size_full()
.on_action(cx.listener(Self::save_api_key))
.children(
@@ -507,15 +531,13 @@ impl Render for ConfigurationView {
.into_any()
} else {
h_flex()
- .id("anthropic-configuration-view")
- .track_focus(&self.focus_handle)
.size_full()
.justify_between()
.child(
h_flex()
.gap_2()
.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(
Button::new("reset-key", "Reset key")
@@ -8,9 +8,7 @@ use anyhow::{anyhow, Context as _, Result};
use client::{Client, UserStore};
use collections::BTreeMap;
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
-use gpui::{
- AnyView, AppContext, AsyncAppContext, FocusHandle, Model, ModelContext, Subscription, Task,
-};
+use gpui::{AnyView, AppContext, AsyncAppContext, Model, ModelContext, Subscription, Task};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
@@ -60,8 +58,8 @@ pub struct State {
}
impl State {
- fn is_connected(&self) -> bool {
- self.status.is_connected()
+ fn is_signed_out(&self) -> bool {
+ self.status.is_signed_out()
}
fn authenticate(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
@@ -191,20 +189,18 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
}
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<()>> {
Task::ready(Ok(()))
}
- fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
- let view = cx
- .new_view(|_cx| ConfigurationView {
- state: self.state.clone(),
- })
- .into();
- (view, None)
+ fn configuration_view(&self, cx: &mut WindowContext) -> AnyView {
+ cx.new_view(|_cx| ConfigurationView {
+ state: self.state.clone(),
+ })
+ .into()
}
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 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 is_pro = plan == Some(proto::Plan::ZedPro);
@@ -11,8 +11,8 @@ use futures::future::BoxFuture;
use futures::stream::BoxStream;
use futures::{FutureExt, StreamExt};
use gpui::{
- percentage, svg, Animation, AnimationExt, AnyView, AppContext, AsyncAppContext, FocusHandle,
- Model, Render, Subscription, Task, Transformation,
+ percentage, svg, Animation, AnimationExt, AnyView, AppContext, AsyncAppContext, Model, Render,
+ Subscription, Task, Transformation,
};
use settings::{Settings, SettingsStore};
use std::time::Duration;
@@ -132,10 +132,9 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
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 view = cx.new_view(|cx| ConfigurationView::new(state, cx)).into();
- (view, None)
+ cx.new_view(|cx| ConfigurationView::new(state, cx)).into()
}
fn reset_credentials(&self, _cx: &mut AppContext) -> Task<Result<()>> {
@@ -6,7 +6,7 @@ use crate::{
use anyhow::anyhow;
use collections::HashMap;
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 std::{
future,
@@ -66,7 +66,7 @@ impl LanguageModelProvider for FakeLanguageModelProvider {
Task::ready(Ok(()))
}
- fn configuration_view(&self, _: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
+ fn configuration_view(&self, _: &mut WindowContext) -> AnyView {
unimplemented!()
}
@@ -4,8 +4,8 @@ use editor::{Editor, EditorElement, EditorStyle};
use futures::{future::BoxFuture, FutureExt, StreamExt};
use google_ai::stream_generate_content;
use gpui::{
- AnyView, AppContext, AsyncAppContext, FocusHandle, FocusableView, FontStyle, ModelContext,
- Subscription, Task, TextStyle, View, WhiteSpace,
+ AnyView, AppContext, AsyncAppContext, FontStyle, ModelContext, Subscription, Task, TextStyle,
+ View, WhiteSpace,
};
use http_client::HttpClient;
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 {
@@ -144,38 +186,12 @@ impl LanguageModelProvider for GoogleLanguageModelProvider {
}
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> {
- if self.is_authenticated(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();
- })
- })
- }
+ self.state.update(cx, |state, cx| state.authenticate(cx))
}
- fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
- let view = cx.new_view(|cx| ConfigurationView::new(self.state.clone(), cx));
-
- let focus_handle = view.focus_handle(cx);
- (view.into(), Some(focus_handle))
+ fn configuration_view(&self, cx: &mut WindowContext) -> AnyView {
+ cx.new_view(|cx| ConfigurationView::new(self.state.clone(), cx))
+ .into()
}
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>> {
@@ -292,22 +308,36 @@ impl LanguageModel for GoogleLanguageModel {
}
struct ConfigurationView {
- focus_handle: FocusHandle,
api_key_editor: View<Editor>,
state: gpui::Model<State>,
+ load_credentials_task: Option<Task<()>>,
}
impl ConfigurationView {
fn new(state: gpui::Model<State>, cx: &mut ViewContext<Self>) -> Self {
- let focus_handle = cx.focus_handle();
-
- cx.on_focus(&focus_handle, |this, cx| {
- if this.should_render_editor(cx) {
- this.api_key_editor.read(cx).focus_handle(cx).focus(cx)
- }
+ cx.observe(&state, |_, _, cx| {
+ cx.notify();
})
.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 {
api_key_editor: cx.new_view(|cx| {
let mut editor = Editor::single_line(cx);
@@ -315,7 +345,7 @@ impl ConfigurationView {
editor
}),
state,
- focus_handle,
+ load_credentials_task,
}
}
@@ -325,26 +355,30 @@ impl ConfigurationView {
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();
cx.spawn(|_, mut cx| async move {
- write_credentials.await?;
- state.update(&mut cx, |this, cx| {
- this.api_key = Some(api_key);
- cx.notify();
- })
+ state
+ .update(&mut cx, |state, cx| state.set_api_key(api_key, cx))?
+ .await
})
.detach_and_log_err(cx);
+
+ cx.notify();
}
fn reset_api_key(&mut self, cx: &mut ViewContext<Self>) {
self.api_key_editor
.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 {
@@ -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 {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
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:",
];
- 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()
- .id("google-ai-configuration-view")
- .track_focus(&self.focus_handle)
.size_full()
.on_action(cx.listener(Self::save_api_key))
.children(
@@ -422,15 +450,13 @@ impl Render for ConfigurationView {
.into_any()
} else {
h_flex()
- .id("google-ai-configuration-view")
- .track_focus(&self.focus_handle)
.size_full()
.justify_between()
.child(
h_flex()
.gap_2()
.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(
Button::new("reset-key", "Reset key")
@@ -1,6 +1,6 @@
use anyhow::{anyhow, Result};
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 ollama::{
get_models, preload_model, stream_chat_completion, ChatMessage, ChatOptions, ChatRequest,
@@ -8,6 +8,7 @@ use ollama::{
use settings::{Settings, SettingsStore};
use std::{future, sync::Arc, time::Duration};
use ui::{prelude::*, ButtonLike, Indicator};
+use util::ResultExt;
use crate::{
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 {
@@ -142,19 +151,12 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
}
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> {
- if self.is_authenticated(cx) {
- Task::ready(Ok(()))
- } else {
- self.state.update(cx, |state, cx| state.fetch_models(cx))
- }
+ self.state.update(cx, |state, cx| state.authenticate(cx))
}
- fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
+ fn configuration_view(&self, cx: &mut WindowContext) -> AnyView {
let state = self.state.clone();
- (
- cx.new_view(|cx| ConfigurationView::new(state, cx)).into(),
- None,
- )
+ cx.new_view(|cx| ConfigurationView::new(state, cx)).into()
}
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>> {
@@ -296,11 +298,32 @@ impl LanguageModel for OllamaLanguageModel {
struct ConfigurationView {
state: gpui::Model<State>,
+ loading_models_task: Option<Task<()>>,
}
impl ConfigurationView {
- pub fn new(state: gpui::Model<State>, _cx: &mut ViewContext<Self>) -> Self {
- Self { state }
+ pub fn new(state: gpui::Model<State>, cx: &mut ViewContext<Self>) -> Self {
+ 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) {
@@ -321,94 +344,101 @@ impl Render for ConfigurationView {
let mut inline_code_bg = cx.theme().colors().editor_background;
inline_code_bg.fade_out(0.5);
- v_flex()
- .size_full()
- .gap_3()
- .child(
- v_flex()
- .size_full()
- .gap_2()
- .p_1()
- .child(Label::new(ollama_intro))
- .child(Label::new(ollama_reqs))
- .child(
- h_flex()
- .gap_0p5()
- .child(Label::new("Once installed, try "))
- .child(
- div()
- .bg(inline_code_bg)
- .px_1p5()
- .rounded_md()
- .child(Label::new("ollama run llama3.1")),
- ),
- ),
- )
- .child(
- h_flex()
- .w_full()
- .pt_2()
- .justify_between()
- .gap_2()
- .child(
- h_flex()
- .w_full()
- .gap_2()
- .map(|this| {
- if is_authenticated {
- this.child(
- Button::new("ollama-site", "Ollama")
- .style(ButtonStyle::Subtle)
- .icon(IconName::ExternalLink)
- .icon_size(IconSize::XSmall)
- .icon_color(Color::Muted)
- .on_click(move |_, cx| cx.open_url(OLLAMA_SITE))
- .into_any_element(),
- )
- } else {
- this.child(
- Button::new("download_ollama_button", "Download Ollama")
+ if self.loading_models_task.is_some() {
+ div().child(Label::new("Loading models...")).into_any()
+ } else {
+ v_flex()
+ .size_full()
+ .gap_3()
+ .child(
+ v_flex()
+ .size_full()
+ .gap_2()
+ .p_1()
+ .child(Label::new(ollama_intro))
+ .child(Label::new(ollama_reqs))
+ .child(
+ h_flex()
+ .gap_0p5()
+ .child(Label::new("Once installed, try "))
+ .child(
+ div()
+ .bg(inline_code_bg)
+ .px_1p5()
+ .rounded_md()
+ .child(Label::new("ollama run llama3.1")),
+ ),
+ ),
+ )
+ .child(
+ h_flex()
+ .w_full()
+ .pt_2()
+ .justify_between()
+ .gap_2()
+ .child(
+ h_flex()
+ .w_full()
+ .gap_2()
+ .map(|this| {
+ if is_authenticated {
+ this.child(
+ Button::new("ollama-site", "Ollama")
+ .style(ButtonStyle::Subtle)
+ .icon(IconName::ExternalLink)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Muted)
+ .on_click(move |_, cx| cx.open_url(OLLAMA_SITE))
+ .into_any_element(),
+ )
+ } else {
+ this.child(
+ Button::new(
+ "download_ollama_button",
+ "Download Ollama",
+ )
.style(ButtonStyle::Subtle)
.icon(IconName::ExternalLink)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.on_click(move |_, cx| cx.open_url(OLLAMA_DOWNLOAD_URL))
.into_any_element(),
- )
- }
- })
- .child(
- Button::new("view-models", "All Models")
- .style(ButtonStyle::Subtle)
- .icon(IconName::ExternalLink)
- .icon_size(IconSize::XSmall)
- .icon_color(Color::Muted)
- .on_click(move |_, cx| cx.open_url(OLLAMA_LIBRARY_URL)),
- ),
- )
- .child(if is_authenticated {
- // This is only a button to ensure the spacing is correct
- // it should stay disabled
- ButtonLike::new("connected")
- .disabled(true)
- // Since this won't ever be clickable, we can use the arrow cursor
- .cursor_style(gpui::CursorStyle::Arrow)
- .child(
- h_flex()
- .gap_2()
- .child(Indicator::dot().color(Color::Success))
- .child(Label::new("Connected"))
- .into_any_element(),
- )
- .into_any_element()
- } else {
- Button::new("retry_ollama_models", "Connect")
- .icon_position(IconPosition::Start)
- .icon(IconName::ArrowCircle)
- .on_click(cx.listener(move |this, _, cx| this.retry_connection(cx)))
- .into_any_element()
- }),
- )
- .into_any()
+ )
+ }
+ })
+ .child(
+ Button::new("view-models", "All Models")
+ .style(ButtonStyle::Subtle)
+ .icon(IconName::ExternalLink)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Muted)
+ .on_click(move |_, cx| cx.open_url(OLLAMA_LIBRARY_URL)),
+ ),
+ )
+ .child(if is_authenticated {
+ // This is only a button to ensure the spacing is correct
+ // it should stay disabled
+ ButtonLike::new("connected")
+ .disabled(true)
+ // Since this won't ever be clickable, we can use the arrow cursor
+ .cursor_style(gpui::CursorStyle::Arrow)
+ .child(
+ h_flex()
+ .gap_2()
+ .child(Indicator::dot().color(Color::Success))
+ .child(Label::new("Connected"))
+ .into_any_element(),
+ )
+ .into_any_element()
+ } else {
+ Button::new("retry_ollama_models", "Connect")
+ .icon_position(IconPosition::Start)
+ .icon(IconName::ArrowCircle)
+ .on_click(cx.listener(move |this, _, cx| this.retry_connection(cx)))
+ .into_any_element()
+ }),
+ )
+ .into_any()
+ }
}
}
@@ -3,8 +3,8 @@ use collections::BTreeMap;
use editor::{Editor, EditorElement, EditorStyle};
use futures::{future::BoxFuture, FutureExt, StreamExt};
use gpui::{
- AnyView, AppContext, AsyncAppContext, FocusHandle, FocusableView, FontStyle, ModelContext,
- Subscription, Task, TextStyle, View, WhiteSpace,
+ AnyView, AppContext, AsyncAppContext, FontStyle, ModelContext, Subscription, Task, TextStyle,
+ View, WhiteSpace,
};
use http_client::HttpClient;
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 {
@@ -145,36 +185,12 @@ impl LanguageModelProvider for OpenAiLanguageModelProvider {
}
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> {
- if self.is_authenticated(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();
- })
- })
- }
+ self.state.update(cx, |state, cx| state.authenticate(cx))
}
- fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
- let view = cx.new_view(|cx| ConfigurationView::new(self.state.clone(), cx));
- let focus_handle = view.focus_handle(cx);
- (view.into(), Some(focus_handle))
+ fn configuration_view(&self, cx: &mut WindowContext) -> AnyView {
+ cx.new_view(|cx| ConfigurationView::new(self.state.clone(), cx))
+ .into()
}
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>> {
@@ -302,33 +318,47 @@ pub fn count_open_ai_tokens(
}
struct ConfigurationView {
- focus_handle: FocusHandle,
api_key_editor: View<Editor>,
state: gpui::Model<State>,
+ load_credentials_task: Option<Task<()>>,
}
impl ConfigurationView {
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| {
- if this.should_render_editor(cx) {
- this.api_key_editor.read(cx).focus_handle(cx).focus(cx)
- }
+ cx.observe(&state, |_, _, cx| {
+ cx.notify();
})
.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 {
- api_key_editor: cx.new_view(|cx| {
- let mut editor = Editor::single_line(cx);
- editor.set_placeholder_text(
- "sk-000000000000000000000000000000000000000000000000",
- cx,
- );
- editor
- }),
+ api_key_editor,
state,
- focus_handle,
+ load_credentials_task,
}
}
@@ -338,26 +368,30 @@ impl ConfigurationView {
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();
cx.spawn(|_, mut cx| async move {
- write_credentials.await?;
- state.update(&mut cx, |this, cx| {
- this.api_key = Some(api_key);
- cx.notify();
- })
+ state
+ .update(&mut cx, |state, cx| state.set_api_key(api_key, cx))?
+ .await
})
.detach_and_log_err(cx);
+
+ cx.notify();
}
fn reset_api_key(&mut self, cx: &mut ViewContext<Self>) {
self.api_key_editor
.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 {
@@ -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 {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
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:",
];
- 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()
- .id("openai-configuration-view")
- .track_focus(&self.focus_handle)
.size_full()
.on_action(cx.listener(Self::save_api_key))
.children(
@@ -437,15 +465,13 @@ impl Render for ConfigurationView {
.into_any()
} else {
h_flex()
- .id("openai-configuration-view")
- .track_focus(&self.focus_handle)
.size_full()
.justify_between()
.child(
h_flex()
.gap_2()
.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(
Button::new("reset-key", "Reset key")
@@ -166,11 +166,8 @@ impl LanguageModelRegistry {
.collect()
}
- pub fn provider(
- &self,
- name: &LanguageModelProviderId,
- ) -> Option<Arc<dyn LanguageModelProvider>> {
- self.providers.get(name).cloned()
+ pub fn provider(&self, id: &LanguageModelProviderId) -> Option<Arc<dyn LanguageModelProvider>> {
+ self.providers.get(id).cloned()
}
pub fn select_active_model(