diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index d0d333148a..33081b0aee 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -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, authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>, configuration_subscription: Option, + watch_client_status: Option>, + 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) { - let provider = LanguageModelRegistry::read_global(cx).active_provider(); - self.show_configuration_for_provider(provider, cx); - } - - fn show_configuration_for_provider( - &mut self, - provider: Option>, - cx: &mut ViewContext, - ) { + fn show_configuration_tab(&mut self, cx: &mut ViewContext) { 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::() { - 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::() } - fn has_any_context_editors(&self, cx: &AppContext) -> bool { - self.pane - .read(cx) - .items() - .any(|item| item.downcast::().is_some()) - } - pub fn active_context(&self, cx: &AppContext) -> Option> { 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) { 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, - configuration_prompt: AnyView, - focus_handle: Option, - load_credentials_task: Option>, -} - -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, + configuration_views: HashMap, + _registry_subscription: Subscription, } impl ConfigurationView { fn new(cx: &mut ViewContext) -> 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, }; - - let providers = LanguageModelRegistry::read_global(cx).providers(); - if !providers.is_empty() { - this.set_active_tab(providers[0].clone(), cx); - } - + this.build_configuration_views(cx); this } - fn set_active_tab( - &mut self, - provider: Arc, - cx: &mut ViewContext, - ) { - 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); + fn build_configuration_views(&mut self, cx: &mut ViewContext) { + let providers = LanguageModelRegistry::read_global(cx).providers(); + for provider in providers { + self.add_configuration_view(&provider, 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) -> Option
{ - 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(), - )) - }, - )), - ), - ) - }, - ), - ) + fn remove_configuration_view(&mut self, provider_id: &LanguageModelProviderId) { + self.configuration_views.remove(provider_id); } - fn render_tab( - &self, + fn add_configuration_view( + &mut self, provider: &Arc, cx: &mut ViewContext, - ) -> 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); - } - })) + ) { + let configuration_view = provider.configuration_view(cx); + self.configuration_views + .insert(provider.id(), configuration_view); + } + + fn render_provider_view( + &mut self, + provider: &Arc, + cx: &mut ViewContext, + ) -> 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 + .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 + )))) }) - .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 - }, - )), + .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) -> 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::>(); 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)), ) } } diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index 0048d4c50d..0dbe1aeecd 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -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, _cx: &AppContext) {} fn is_authenticated(&self, cx: &AppContext) -> bool; fn authenticate(&self, cx: &mut AppContext) -> Task>; - fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option); + fn configuration_view(&self, cx: &mut WindowContext) -> AnyView; fn reset_credentials(&self, cx: &mut AppContext) -> Task>; } diff --git a/crates/language_model/src/provider/anthropic.rs b/crates/language_model/src/provider/anthropic.rs index 9935bd7a7f..1a2c06e4c7 100644 --- a/crates/language_model/src/provider/anthropic.rs +++ b/crates/language_model/src/provider/anthropic.rs @@ -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) -> Task> { + 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> { - 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) { - 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> { @@ -383,33 +387,46 @@ impl LanguageModel for AnthropicModel { } struct ConfigurationView { - focus_handle: FocusHandle, api_key_editor: View, state: gpui::Model, + load_credentials_task: Option>, } impl ConfigurationView { - fn new(state: gpui::Model, cx: &mut ViewContext) -> 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, cx: &mut ViewContext) -> 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.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) -> 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) -> 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") diff --git a/crates/language_model/src/provider/cloud.rs b/crates/language_model/src/provider/cloud.rs index f0056cb0d8..e5533c5d42 100644 --- a/crates/language_model/src/provider/cloud.rs +++ b/crates/language_model/src/provider/cloud.rs @@ -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) -> Task> { @@ -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> { Task::ready(Ok(())) } - fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option) { - 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> { @@ -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); diff --git a/crates/language_model/src/provider/copilot_chat.rs b/crates/language_model/src/provider/copilot_chat.rs index fc0831a603..5457be3c85 100644 --- a/crates/language_model/src/provider/copilot_chat.rs +++ b/crates/language_model/src/provider/copilot_chat.rs @@ -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) { + 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> { diff --git a/crates/language_model/src/provider/fake.rs b/crates/language_model/src/provider/fake.rs index 511e11788a..dd3ead3511 100644 --- a/crates/language_model/src/provider/fake.rs +++ b/crates/language_model/src/provider/fake.rs @@ -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) { + fn configuration_view(&self, _: &mut WindowContext) -> AnyView { unimplemented!() } diff --git a/crates/language_model/src/provider/google.rs b/crates/language_model/src/provider/google.rs index 3d907389c0..27a1e7cf2f 100644 --- a/crates/language_model/src/provider/google.rs +++ b/crates/language_model/src/provider/google.rs @@ -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) -> Task> { + 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) -> Task> { + 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> { - 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) { - 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> { @@ -292,22 +308,36 @@ impl LanguageModel for GoogleLanguageModel { } struct ConfigurationView { - focus_handle: FocusHandle, api_key_editor: View, state: gpui::Model, + load_credentials_task: Option>, } impl ConfigurationView { fn new(state: gpui::Model, cx: &mut ViewContext) -> 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.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) -> 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) -> 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") diff --git a/crates/language_model/src/provider/ollama.rs b/crates/language_model/src/provider/ollama.rs index 717feb5fad..15b70b1cb6 100644 --- a/crates/language_model/src/provider/ollama.rs +++ b/crates/language_model/src/provider/ollama.rs @@ -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) -> Task> { + 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> { - 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) { + 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> { @@ -296,11 +298,32 @@ impl LanguageModel for OllamaLanguageModel { struct ConfigurationView { state: gpui::Model, + loading_models_task: Option>, } impl ConfigurationView { - pub fn new(state: gpui::Model, _cx: &mut ViewContext) -> Self { - Self { state } + pub fn new(state: gpui::Model, cx: &mut ViewContext) -> 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() + } } } diff --git a/crates/language_model/src/provider/open_ai.rs b/crates/language_model/src/provider/open_ai.rs index b6daa77360..5121ea802a 100644 --- a/crates/language_model/src/provider/open_ai.rs +++ b/crates/language_model/src/provider/open_ai.rs @@ -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) -> Task> { + 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) -> Task> { + 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> { - 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) { - 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> { @@ -302,33 +318,47 @@ pub fn count_open_ai_tokens( } struct ConfigurationView { - focus_handle: FocusHandle, api_key_editor: View, state: gpui::Model, + load_credentials_task: Option>, } impl ConfigurationView { fn new(state: gpui::Model, cx: &mut ViewContext) -> 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.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) -> 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) -> 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") diff --git a/crates/language_model/src/registry.rs b/crates/language_model/src/registry.rs index 067573bc38..f59c614e0d 100644 --- a/crates/language_model/src/registry.rs +++ b/crates/language_model/src/registry.rs @@ -166,11 +166,8 @@ impl LanguageModelRegistry { .collect() } - pub fn provider( - &self, - name: &LanguageModelProviderId, - ) -> Option> { - self.providers.get(name).cloned() + pub fn provider(&self, id: &LanguageModelProviderId) -> Option> { + self.providers.get(id).cloned() } pub fn select_active_model(