assistant: Add a Configuration page (#15490)

- [x] bug: setting a key doesn't update anything
- [x] show high-level text on configuration page to explain what it is
- [x] show "everything okay!" status when credentials are set
- [x] maybe: add "verify" button to check credentials
- [x] open configuration page when opening panel for first time and
nothing is configured
- [x] BUG: need to fix empty assistant panel if provider is `zed.dev`
but not logged in


Co-Authored-By: Thorsten <thorsten@zed.dev>

Release Notes:

- N/A

---------

Co-authored-by: Thorsten <thorsten@zed.dev>
Co-authored-by: Nate Butler <iamnbutler@gmail.com>
Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
This commit is contained in:
Bennet Bo Fenner 2024-08-01 15:54:47 +02:00 committed by GitHub
parent 79213637e2
commit be3a8584ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 934 additions and 476 deletions

1
Cargo.lock generated
View file

@ -423,6 +423,7 @@ dependencies = [
"language", "language",
"language_model", "language_model",
"log", "log",
"markdown",
"menu", "menu",
"multi_buffer", "multi_buffer",
"ollama", "ollama",

View file

@ -47,6 +47,7 @@ indoc.workspace = true
language.workspace = true language.workspace = true
language_model.workspace = true language_model.workspace = true
log.workspace = true log.workspace = true
markdown.workspace = true
menu.workspace = true menu.workspace = true
multi_buffer.workspace = true multi_buffer.workspace = true
ollama = { workspace = true, features = ["schemars"] } ollama = { workspace = true, features = ["schemars"] }

View file

@ -45,8 +45,8 @@ actions!(
QuoteSelection, QuoteSelection,
InsertIntoEditor, InsertIntoEditor,
ToggleFocus, ToggleFocus,
ResetKey,
InsertActivePrompt, InsertActivePrompt,
ShowConfiguration,
DeployHistory, DeployHistory,
DeployPromptLibrary, DeployPromptLibrary,
ConfirmCommand, ConfirmCommand,

View file

@ -1,4 +1,3 @@
use crate::ContextStoreEvent;
use crate::{ use crate::{
assistant_settings::{AssistantDockPosition, AssistantSettings}, assistant_settings::{AssistantDockPosition, AssistantSettings},
humanize_token_count, humanize_token_count,
@ -13,8 +12,9 @@ use crate::{
DebugEditSteps, DeployHistory, DeployPromptLibrary, EditStep, EditStepOperations, DebugEditSteps, DeployHistory, DeployPromptLibrary, EditStep, EditStepOperations,
EditSuggestionGroup, InlineAssist, InlineAssistId, InlineAssistant, InsertIntoEditor, EditSuggestionGroup, InlineAssist, InlineAssistId, InlineAssistant, InsertIntoEditor,
MessageStatus, ModelSelector, PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection, MessageStatus, ModelSelector, PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection,
RemoteContextMetadata, ResetKey, SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector, RemoteContextMetadata, SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector,
}; };
use crate::{ContextStoreEvent, ShowConfiguration};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection}; use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
use client::proto; use client::proto;
@ -31,18 +31,20 @@ 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, Action, Animation, AnimationExt, AnyElement, AnyView, AppContext, div, percentage, point, svg, 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,
UpdateGlobal, View, ViewContext, VisualContext, WeakView, WindowContext, TextStyleRefinement, Transformation, UpdateGlobal, View, ViewContext, VisualContext, WeakView,
WindowContext,
}; };
use indexed_docs::IndexedDocsStore; use indexed_docs::IndexedDocsStore;
use language::{ use language::{
language_settings::SoftWrap, Buffer, Capability, LanguageRegistry, LspAdapterDelegate, Point, language_settings::SoftWrap, Buffer, Capability, LanguageRegistry, LspAdapterDelegate, Point,
ToOffset, ToOffset,
}; };
use language_model::{LanguageModelProviderId, LanguageModelRegistry, Role}; use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Role};
use markdown::{Markdown, MarkdownStyle};
use multi_buffer::MultiBufferRow; use multi_buffer::MultiBufferRow;
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use project::{Project, ProjectLspAdapterDelegate}; use project::{Project, ProjectLspAdapterDelegate};
@ -58,6 +60,7 @@ use std::{
time::Duration, time::Duration,
}; };
use terminal_view::{terminal_panel::TerminalPanel, TerminalView}; use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
use theme::ThemeSettings;
use ui::TintColor; use ui::TintColor;
use ui::{ use ui::{
prelude::*, prelude::*,
@ -91,7 +94,8 @@ pub fn init(cx: &mut AppContext) {
}) })
.register_action(AssistantPanel::inline_assist) .register_action(AssistantPanel::inline_assist)
.register_action(ContextEditor::quote_selection) .register_action(ContextEditor::quote_selection)
.register_action(ContextEditor::insert_selection); .register_action(ContextEditor::insert_selection)
.register_action(AssistantPanel::show_configuration);
}, },
) )
.detach(); .detach();
@ -136,7 +140,6 @@ pub struct AssistantPanel {
languages: Arc<LanguageRegistry>, languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
subscriptions: Vec<Subscription>, subscriptions: Vec<Subscription>,
authentication_prompt: Option<AnyView>,
model_selector_menu_handle: PopoverMenuHandle<ContextMenu>, model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
model_summary_editor: View<Editor>, model_summary_editor: View<Editor>,
authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>, authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
@ -365,6 +368,7 @@ impl AssistantPanel {
.action("New Context", Box::new(NewFile)) .action("New Context", Box::new(NewFile))
.action("History", Box::new(DeployHistory)) .action("History", Box::new(DeployHistory))
.action("Prompt Library", Box::new(DeployPromptLibrary)) .action("Prompt Library", Box::new(DeployPromptLibrary))
.action("Configure", Box::new(ShowConfiguration))
.action(zoom_label, Box::new(ToggleZoom)) .action(zoom_label, Box::new(ToggleZoom))
}); });
cx.subscribe(&menu, |pane, _, _: &DismissEvent, _| { cx.subscribe(&menu, |pane, _, _: &DismissEvent, _| {
@ -399,8 +403,10 @@ impl AssistantPanel {
language_model::Event::ActiveModelChanged => { language_model::Event::ActiveModelChanged => {
this.completion_provider_changed(cx); this.completion_provider_changed(cx);
} }
language_model::Event::ProviderStateChanged language_model::Event::ProviderStateChanged => {
| language_model::Event::AddedProvider(_) this.ensure_authenticated(cx);
}
language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_) => { | language_model::Event::RemovedProvider(_) => {
this.ensure_authenticated(cx); this.ensure_authenticated(cx);
} }
@ -408,7 +414,7 @@ impl AssistantPanel {
), ),
]; ];
Self { let mut this = Self {
pane, pane,
workspace: workspace.weak_handle(), workspace: workspace.weak_handle(),
width: None, width: None,
@ -418,11 +424,21 @@ impl AssistantPanel {
languages: workspace.app_state().languages.clone(), languages: workspace.app_state().languages.clone(),
fs: workspace.app_state().fs.clone(), fs: workspace.app_state().fs.clone(),
subscriptions, subscriptions,
authentication_prompt: None,
model_selector_menu_handle, model_selector_menu_handle,
model_summary_editor, model_summary_editor,
authenticate_provider_task: None, authenticate_provider_task: None,
} };
if LanguageModelRegistry::read_global(cx)
.active_provider()
.is_none()
{
this.show_configuration_for_provider(None, cx);
} else {
this.new_context(cx);
};
this
} }
fn handle_pane_event( fn handle_pane_event(
@ -582,63 +598,39 @@ impl AssistantPanel {
*old_provider_id != new_provider_id *old_provider_id != new_provider_id
}) })
{ {
self.authenticate_provider_task = None;
self.ensure_authenticated(cx); self.ensure_authenticated(cx);
} }
} }
fn authentication_prompt(cx: &mut WindowContext) -> Option<AnyView> {
if let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() {
if !provider.is_authenticated(cx) {
return Some(provider.authentication_prompt(cx));
}
}
None
}
fn ensure_authenticated(&mut self, cx: &mut ViewContext<Self>) { fn ensure_authenticated(&mut self, cx: &mut ViewContext<Self>) {
if self.is_authenticated(cx) { if self.is_authenticated(cx) {
self.set_authentication_prompt(None, cx);
return; return;
} }
let Some(provider_id) = LanguageModelRegistry::read_global(cx) let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
.active_provider()
.map(|p| p.id())
else {
return; return;
}; };
let load_credentials = self.authenticate(cx); let load_credentials = self.authenticate(cx);
if self.authenticate_provider_task.is_none() {
self.authenticate_provider_task = Some(( self.authenticate_provider_task = Some((
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| {
this.show_authentication_prompt(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();
}), }),
)); ));
} }
fn show_authentication_prompt(&mut self, cx: &mut ViewContext<Self>) {
let prompt = Self::authentication_prompt(cx);
self.set_authentication_prompt(prompt, cx);
}
fn set_authentication_prompt(&mut self, prompt: Option<AnyView>, cx: &mut ViewContext<Self>) {
if self.active_context_editor(cx).is_none() {
self.new_context(cx);
}
for context_editor in self.context_editors(cx) {
context_editor.update(cx, |editor, cx| {
editor.set_authentication_prompt(prompt.clone(), cx);
});
}
cx.notify();
} }
pub fn inline_assist( pub fn inline_assist(
@ -900,6 +892,58 @@ impl AssistantPanel {
} }
} }
fn show_configuration(
workspace: &mut Workspace,
_: &ShowConfiguration,
cx: &mut ViewContext<Workspace>,
) {
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
return;
};
if !panel.focus_handle(cx).contains_focused(cx) {
workspace.toggle_panel_focus::<AssistantPanel>(cx);
}
panel.update(cx, |this, cx| {
this.show_configuration_for_active_provider(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>,
) {
let configuration_item_ix = self
.pane
.read(cx)
.items()
.position(|item| item.downcast::<ConfigurationView>().is_some());
if let Some(configuration_item_ix) = configuration_item_ix {
self.pane.update(cx, |pane, cx| {
pane.activate_item(configuration_item_ix, true, true, cx);
});
} else {
let configuration = cx.new_view(|cx| {
let mut view = ConfigurationView::new(self.focus_handle(cx), cx);
if let Some(provider) = provider {
view.set_active_tab(provider, cx);
}
view
});
self.pane.update(cx, |pane, cx| {
pane.add_item(Box::new(configuration), true, true, None, cx);
});
}
}
fn deploy_history(&mut self, _: &DeployHistory, cx: &mut ViewContext<Self>) { fn deploy_history(&mut self, _: &DeployHistory, cx: &mut ViewContext<Self>) {
let history_item_ix = self let history_item_ix = self
.pane .pane
@ -931,30 +975,10 @@ impl AssistantPanel {
open_prompt_library(self.languages.clone(), cx).detach_and_log_err(cx); open_prompt_library(self.languages.clone(), cx).detach_and_log_err(cx);
} }
fn reset_credentials(&mut self, _: &ResetKey, cx: &mut ViewContext<Self>) {
if let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() {
let reset_credentials = provider.reset_credentials(cx);
cx.spawn(|this, mut cx| async move {
reset_credentials.await?;
this.update(&mut cx, |this, cx| {
this.show_authentication_prompt(cx);
})
})
.detach_and_log_err(cx);
}
}
fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) { fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
self.model_selector_menu_handle.toggle(cx); self.model_selector_menu_handle.toggle(cx);
} }
fn context_editors(&self, cx: &AppContext) -> Vec<View<ContextEditor>> {
self.pane
.read(cx)
.items_of_type::<ContextEditor>()
.collect()
}
fn active_context_editor(&self, cx: &AppContext) -> Option<View<ContextEditor>> { fn active_context_editor(&self, cx: &AppContext) -> Option<View<ContextEditor>> {
self.pane self.pane
.read(cx) .read(cx)
@ -962,6 +986,13 @@ 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())
} }
@ -1083,8 +1114,10 @@ impl AssistantPanel {
|provider| provider.authenticate(cx), |provider| provider.authenticate(cx),
) )
} }
}
fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { impl Render for AssistantPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let mut registrar = DivRegistrar::new( let mut registrar = DivRegistrar::new(
|panel, cx| { |panel, cx| {
panel panel
@ -1105,21 +1138,14 @@ impl 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| {
this.show_configuration_for_active_provider(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::reset_credentials))
.on_action(cx.listener(AssistantPanel::toggle_model_selector)) .on_action(cx.listener(AssistantPanel::toggle_model_selector))
.child(registrar.size_full().child(self.pane.clone())) .child(registrar.size_full().child(self.pane.clone()))
} .into_any_element()
}
impl Render for AssistantPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
if let Some(authentication_prompt) = self.authentication_prompt.as_ref() {
authentication_prompt.clone().into_any()
} else {
self.render_signed_in(cx).into_any_element()
}
} }
} }
@ -1242,7 +1268,6 @@ struct ActiveEditStep {
pub struct ContextEditor { pub struct ContextEditor {
context: Model<Context>, context: Model<Context>,
authentication_prompt: Option<AnyView>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
project: Model<Project>, project: Model<Project>,
@ -1300,7 +1325,6 @@ impl ContextEditor {
let sections = context.read(cx).slash_command_output_sections().to_vec(); let sections = context.read(cx).slash_command_output_sections().to_vec();
let mut this = Self { let mut this = Self {
context, context,
authentication_prompt: None,
editor, editor,
lsp_adapter_delegate, lsp_adapter_delegate,
blocks: Default::default(), blocks: Default::default(),
@ -1320,15 +1344,6 @@ impl ContextEditor {
this this
} }
fn set_authentication_prompt(
&mut self,
authentication_prompt: Option<AnyView>,
cx: &mut ViewContext<Self>,
) {
self.authentication_prompt = authentication_prompt;
cx.notify();
}
fn insert_default_prompt(&mut self, cx: &mut ViewContext<Self>) { fn insert_default_prompt(&mut self, cx: &mut ViewContext<Self>) {
let command_name = DefaultSlashCommand.name(); let command_name = DefaultSlashCommand.name();
self.editor.update(cx, |editor, cx| { self.editor.update(cx, |editor, cx| {
@ -1355,10 +1370,6 @@ impl ContextEditor {
} }
fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) { fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
if self.authentication_prompt.is_some() {
return;
}
if !self.apply_edit_step(cx) { if !self.apply_edit_step(cx) {
self.send_to_model(cx); self.send_to_model(cx);
} }
@ -2419,12 +2430,6 @@ impl Render for ContextEditor {
.size_full() .size_full()
.v_flex() .v_flex()
.child( .child(
if let Some(authentication_prompt) = self.authentication_prompt.as_ref() {
div()
.flex_grow()
.bg(cx.theme().colors().editor_background)
.child(authentication_prompt.clone().into_any())
} else {
div() div()
.flex_grow() .flex_grow()
.bg(cx.theme().colors().editor_background) .bg(cx.theme().colors().editor_background)
@ -2437,8 +2442,7 @@ impl Render for ContextEditor {
.p_4() .p_4()
.justify_end() .justify_end()
.child(self.render_send_button(cx)), .child(self.render_send_button(cx)),
) ),
},
) )
} }
} }
@ -2992,6 +2996,253 @@ impl Item for ContextHistory {
} }
} }
pub struct ConfigurationView {
fallback_handle: FocusHandle,
using_assistant_description: View<Markdown>,
active_tab: Option<ActiveTab>,
}
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
}
}
// TODO: We need to remove this once we have proper text and styling
const SHOW_CONFIGURATION_TEXT: bool = false;
impl ConfigurationView {
fn new(fallback_handle: FocusHandle, cx: &mut ViewContext<Self>) -> Self {
let usage_description = cx.new_view(|cx| {
let text = include_str!("./using-the-assistant.md");
let settings = ThemeSettings::get_global(cx);
let mut base_text_style = cx.text_style();
base_text_style.refine(&TextStyleRefinement {
font_family: Some(settings.ui_font.family.clone()),
font_size: Some(TextSize::XSmall.rems(cx).into()),
color: Some(cx.theme().colors().editor_foreground),
background_color: Some(gpui::transparent_black()),
..Default::default()
});
let markdown_style = MarkdownStyle {
base_text_style,
selection_background_color: { cx.theme().players().local().selection },
inline_code: TextStyleRefinement {
background_color: Some(cx.theme().colors().background),
..Default::default()
},
link: TextStyleRefinement {
underline: Some(gpui::UnderlineStyle {
thickness: px(1.),
color: Some(cx.theme().colors().editor_foreground),
wavy: false,
}),
..Default::default()
},
..Default::default()
};
Markdown::new(text.to_string(), markdown_style.clone(), None, cx, None)
});
Self {
fallback_handle,
using_assistant_description: usage_description,
active_tab: None,
}
}
fn set_active_tab(
&mut self,
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.fallback_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 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(
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),
)
}
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 {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let providers = LanguageModelRegistry::read_global(cx).providers();
if self.active_tab.is_none() && !providers.is_empty() {
self.set_active_tab(providers[0].clone(), cx);
}
let tabs = h_flex().mx_neg_1().gap_3().children(
providers
.iter()
.map(|provider| self.render_tab(provider, cx)),
);
v_flex()
.id("assistant-configuration-view")
.w_full()
.min_h_full()
.p(Spacing::XXLarge.rems(cx))
.overflow_y_scroll()
.gap_6()
.child(
v_flex()
.gap_2()
.child(
Headline::new("Get Started with the Assistant").size(HeadlineSize::Medium),
)
.child(
Label::new("Choose a provider to get started with the assistant.")
.color(Color::Muted),
),
)
.child(
v_flex()
.gap_2()
.child(Headline::new("Choosing a Provider").size(HeadlineSize::Small))
.child(tabs)
.children(self.render_active_tab_view(cx)),
)
.when(SHOW_CONFIGURATION_TEXT, |this| {
this.child(self.using_assistant_description.clone())
})
}
}
impl EventEmitter<()> for ConfigurationView {}
impl FocusableView for ConfigurationView {
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
self.active_tab
.as_ref()
.and_then(|tab| tab.focus_handle.clone())
.unwrap_or(self.fallback_handle.clone())
}
}
impl Item for ConfigurationView {
type Event = ();
fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
Some("Configuration".into())
}
}
type ToggleFold = Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>; type ToggleFold = Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>;
fn render_slash_command_output_toggle( fn render_slash_command_output_toggle(

View file

@ -0,0 +1,25 @@
### Using the Assistant
Once you have configured a provider, you can interact with the provider's language models in a context editor.
To create a new context editor, use the menu in the top right of the assistant panel and the `New Context` option.
In the context editor, select a model from one of the configured providers, type a message in the `You` block, and submit with `cmd-enter` (or `ctrl-enter` on Linux).
### Inline assistant
When you're in a normal editor, you can use `ctrl-enter` to open the inline assistant.
The inline assistant allows you to send the current selection (or the current line) to a language model and modify the selection with the language model's response.
### Adding Prompts
You can customize the default prompts that are used in new context editor, by opening the `Prompt Library`.
Open the `Prompt Library` using either the menu in the top right of the assistant panel and choosing the `Prompt Library` option, or by using the `assistant: deploy prompt library` command when the assistant panel is focused.
### Viewing past contexts
You view all previous contexts by opening up the `History` tab in the assistant panel.
Open the `History` using the menu in the top right of the assistant panel and choosing the `History`.

View file

@ -9,7 +9,7 @@ pub mod settings;
use anyhow::Result; use anyhow::Result;
use client::Client; use client::Client;
use futures::{future::BoxFuture, stream::BoxStream}; use futures::{future::BoxFuture, stream::BoxStream};
use gpui::{AnyView, AppContext, AsyncAppContext, SharedString, Task, WindowContext}; use gpui::{AnyView, AppContext, AsyncAppContext, FocusHandle, SharedString, Task, WindowContext};
pub use model::*; pub use model::*;
use project::Fs; use project::Fs;
pub(crate) use rate_limiter::*; pub(crate) use rate_limiter::*;
@ -84,7 +84,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 authentication_prompt(&self, cx: &mut WindowContext) -> AnyView; fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>);
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>>; fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>>;
} }

View file

@ -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, FontStyle, Subscription, Task, TextStyle, View, AnyView, AppContext, AsyncAppContext, FocusHandle, FocusableView, FontStyle, ModelContext,
WhiteSpace, Subscription, Task, TextStyle, View, WhiteSpace,
}; };
use http_client::HttpClient; use http_client::HttpClient;
use schemars::JsonSchema; use schemars::JsonSchema;
@ -18,8 +18,7 @@ use settings::{Settings, SettingsStore};
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::prelude::*; 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";
@ -49,6 +48,43 @@ pub struct State {
_subscription: Subscription, _subscription: Subscription,
} }
impl State {
fn reset_api_key(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
let delete_credentials =
cx.delete_credentials(&AllLanguageModelSettings::get_global(cx).anthropic.api_url);
cx.spawn(|this, mut cx| async move {
delete_credentials.await.ok();
this.update(&mut cx, |this, cx| {
this.api_key = None;
cx.notify();
})
})
}
fn set_api_key(&mut self, api_key: String, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
let write_credentials = cx.write_credentials(
AllLanguageModelSettings::get_global(cx)
.anthropic
.api_url
.as_str(),
"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 is_authenticated(&self) -> bool {
self.api_key.is_some()
}
}
impl AnthropicLanguageModelProvider { impl AnthropicLanguageModelProvider {
pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Self { pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Self {
let state = cx.new_model(|cx| State { let state = cx.new_model(|cx| State {
@ -120,7 +156,7 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider {
} }
fn is_authenticated(&self, cx: &AppContext) -> bool { fn is_authenticated(&self, cx: &AppContext) -> bool {
self.state.read(cx).api_key.is_some() self.state.read(cx).is_authenticated()
} }
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> { fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> {
@ -151,22 +187,14 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider {
} }
} }
fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView { fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
cx.new_view(|cx| AuthenticationPrompt::new(self.state.clone(), cx)) let view = 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<()>> {
let state = self.state.clone(); self.state.update(cx, |state, cx| state.reset_api_key(cx))
let delete_credentials =
cx.delete_credentials(&AllLanguageModelSettings::get_global(cx).anthropic.api_url);
cx.spawn(|mut cx| async move {
delete_credentials.await.log_err();
state.update(&mut cx, |this, cx| {
this.api_key = None;
cx.notify();
})
})
} }
} }
@ -350,18 +378,24 @@ impl LanguageModel for AnthropicModel {
} }
} }
struct AuthenticationPrompt { struct ConfigurationView {
api_key: View<Editor>, api_key_editor: View<Editor>,
state: gpui::Model<State>, state: gpui::Model<State>,
} }
impl AuthenticationPrompt { impl FocusableView for ConfigurationView {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.api_key_editor.read(cx).focus_handle(cx)
}
}
impl ConfigurationView {
fn new(state: gpui::Model<State>, cx: &mut WindowContext) -> Self { fn new(state: gpui::Model<State>, cx: &mut WindowContext) -> Self {
Self { Self {
api_key: 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(
"sk-000000000000000000000000000000000000000000000000", "sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
cx, cx,
); );
editor editor
@ -371,28 +405,21 @@ impl AuthenticationPrompt {
} }
fn save_api_key(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) { fn save_api_key(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
let api_key = self.api_key.read(cx).text(cx); let api_key = self.api_key_editor.read(cx).text(cx);
if api_key.is_empty() { if api_key.is_empty() {
return; return;
} }
let write_credentials = cx.write_credentials( self.state
AllLanguageModelSettings::get_global(cx) .update(cx, |state, cx| state.set_api_key(api_key, cx))
.anthropic .detach_and_log_err(cx);
.api_url }
.as_str(),
"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| { fn reset_api_key(&mut self, cx: &mut ViewContext<Self>) {
this.api_key = Some(api_key); self.api_key_editor
cx.notify(); .update(cx, |editor, cx| editor.set_text("", cx));
}) self.state
}) .update(cx, |state, cx| state.reset_api_key(cx))
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
@ -413,7 +440,7 @@ impl AuthenticationPrompt {
white_space: WhiteSpace::Normal, white_space: WhiteSpace::Normal,
}; };
EditorElement::new( EditorElement::new(
&self.api_key, &self.api_key_editor,
EditorStyle { EditorStyle {
background: cx.theme().colors().editor_background, background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(), local_player: cx.theme().players().local(),
@ -424,7 +451,7 @@ impl AuthenticationPrompt {
} }
} }
impl Render for AuthenticationPrompt { 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] = [
"To use the assistant panel or inline assistant, you need to add your Anthropic API key.", "To use the assistant panel or inline assistant, you need to add your Anthropic API key.",
@ -433,8 +460,26 @@ impl Render for AuthenticationPrompt {
"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.state.read(cx).is_authenticated() {
h_flex()
.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(
Button::new("reset-key", "Reset key")
.icon(Some(IconName::Trash))
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.on_click(cx.listener(|this, _, cx| this.reset_api_key(cx))),
)
.into_any()
} else {
v_flex() v_flex()
.p_4()
.size_full() .size_full()
.on_action(cx.listener(Self::save_api_key)) .on_action(cx.listener(Self::save_api_key))
.children( .children(
@ -456,15 +501,7 @@ impl Render for AuthenticationPrompt {
) )
.size(LabelSize::Small), .size(LabelSize::Small),
) )
.child(
h_flex()
.gap_2()
.child(Label::new("Click on").size(LabelSize::Small))
.child(Icon::new(IconName::ZedAssistant).size(IconSize::XSmall))
.child(
Label::new("in the status bar to close this panel.").size(LabelSize::Small),
),
)
.into_any() .into_any()
} }
}
} }

View file

@ -8,7 +8,7 @@ use anyhow::{anyhow, Context as _, Result};
use client::Client; use client::Client;
use collections::BTreeMap; use collections::BTreeMap;
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt}; use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
use gpui::{AnyView, AppContext, AsyncAppContext, ModelContext, Subscription, Task}; use gpui::{AnyView, AppContext, AsyncAppContext, FocusHandle, 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};
@ -21,7 +21,7 @@ use crate::LanguageModelProvider;
use super::anthropic::count_anthropic_tokens; use super::anthropic::count_anthropic_tokens;
pub const PROVIDER_ID: &str = "zed.dev"; pub const PROVIDER_ID: &str = "zed.dev";
pub const PROVIDER_NAME: &str = "zed.dev"; pub const PROVIDER_NAME: &str = "Zed AI";
#[derive(Default, Clone, Debug, PartialEq)] #[derive(Default, Clone, Debug, PartialEq)]
pub struct ZedDotDevSettings { pub struct ZedDotDevSettings {
@ -57,6 +57,10 @@ pub struct State {
} }
impl State { impl State {
fn is_connected(&self) -> bool {
self.status.is_connected()
}
fn authenticate(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> { fn authenticate(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
let client = self.client.clone(); let client = self.client.clone();
cx.spawn(move |this, mut cx| async move { cx.spawn(move |this, mut cx| async move {
@ -179,15 +183,17 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
self.state.read(cx).status.is_connected() self.state.read(cx).status.is_connected()
} }
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> { fn authenticate(&self, _cx: &mut AppContext) -> Task<Result<()>> {
self.state.update(cx, |state, cx| state.authenticate(cx)) Task::ready(Ok(()))
} }
fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView { fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
cx.new_view(|_cx| AuthenticationPrompt { let view = cx
.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<()>> {
@ -376,15 +382,69 @@ impl LanguageModel for CloudLanguageModel {
} }
} }
struct AuthenticationPrompt { struct ConfigurationView {
state: gpui::Model<State>, state: gpui::Model<State>,
} }
impl Render for AuthenticationPrompt { impl ConfigurationView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn authenticate(&mut self, cx: &mut ViewContext<Self>) {
const LABEL: &str = "Generate and analyze code with language models. You can dialog with the assistant in this panel or transform code inline."; self.state.update(cx, |state, cx| {
state.authenticate(cx).detach_and_log_err(cx);
});
cx.notify();
}
}
v_flex().gap_6().p_4().child(Label::new(LABEL)).child( impl Render for ConfigurationView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
const ZED_AI_URL: &str = "https://zed.dev/ai";
const ACCOUNT_SETTINGS_URL: &str = "https://zed.dev/settings";
let is_connected = self.state.read(cx).is_connected();
let is_pro = false;
if is_connected {
v_flex()
.gap_3()
.max_w_4_5()
.child(Label::new(
if is_pro {
"You have full access to Zed's hosted models from Anthropic, OpenAI, Google through Zed Pro."
} else {
"You have basic access to models from Anthropic, OpenAI, Google and more through the Zed AI Free plan."
}))
.child(
if is_pro {
h_flex().child(
Button::new("manage_settings", "Manage Subscription")
.style(ButtonStyle::Filled)
.on_click(cx.listener(|_, _, cx| {
cx.open_url(ACCOUNT_SETTINGS_URL)
})))
} else {
h_flex()
.gap_2()
.child(
Button::new("learn_more", "Learn more")
.style(ButtonStyle::Subtle)
.on_click(cx.listener(|_, _, cx| {
cx.open_url(ZED_AI_URL)
})))
.child(
Button::new("upgrade", "Upgrade")
.style(ButtonStyle::Subtle)
.color(Color::Accent)
.on_click(cx.listener(|_, _, cx| {
cx.open_url(ACCOUNT_SETTINGS_URL)
})))
},
)
} else {
v_flex()
.gap_6()
.child(Label::new("Use the zed.dev to access language models."))
.child(
v_flex() v_flex()
.gap_2() .gap_2()
.child( .child(
@ -394,12 +454,7 @@ impl Render for AuthenticationPrompt {
.icon_position(IconPosition::Start) .icon_position(IconPosition::Start)
.style(ButtonStyle::Filled) .style(ButtonStyle::Filled)
.full_width() .full_width()
.on_click(cx.listener(move |this, _, cx| { .on_click(cx.listener(move |this, _, cx| this.authenticate(cx))),
this.state.update(cx, |provider, cx| {
provider.authenticate(cx).detach_and_log_err(cx);
cx.notify();
});
})),
) )
.child( .child(
div().flex().w_full().items_center().child( div().flex().w_full().items_center().child(
@ -410,4 +465,5 @@ impl Render for AuthenticationPrompt {
), ),
) )
} }
}
} }

View file

@ -11,16 +11,16 @@ 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, Model, Render, percentage, svg, Animation, AnimationExt, AnyView, AppContext, AsyncAppContext, FocusHandle,
Subscription, Task, Transformation, Model, Render, Subscription, Task, Transformation,
}; };
use settings::{Settings, SettingsStore}; use settings::{Settings, SettingsStore};
use std::time::Duration; use std::time::Duration;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use ui::{ use ui::{
div, v_flex, Button, ButtonCommon, Clickable, Color, Context, FixedWidth, IconName, div, h_flex, v_flex, Button, ButtonCommon, Clickable, Color, Context, FixedWidth, IconName,
IconPosition, IconSize, IntoElement, Label, LabelCommon, ParentElement, Styled, ViewContext, IconPosition, IconSize, Indicator, IntoElement, Label, LabelCommon, ParentElement, Styled,
VisualContext, WindowContext, ViewContext, VisualContext, WindowContext,
}; };
use crate::settings::AllLanguageModelSettings; use crate::settings::AllLanguageModelSettings;
@ -49,6 +49,14 @@ pub struct State {
_settings_subscription: Subscription, _settings_subscription: Subscription,
} }
impl State {
fn is_authenticated(&self, cx: &AppContext) -> bool {
CopilotChat::global(cx)
.map(|m| m.read(cx).is_authenticated())
.unwrap_or(false)
}
}
impl CopilotChatLanguageModelProvider { impl CopilotChatLanguageModelProvider {
pub fn new(cx: &mut AppContext) -> Self { pub fn new(cx: &mut AppContext) -> Self {
let state = cx.new_model(|cx| { let state = cx.new_model(|cx| {
@ -95,9 +103,7 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
} }
fn is_authenticated(&self, cx: &AppContext) -> bool { fn is_authenticated(&self, cx: &AppContext) -> bool {
CopilotChat::global(cx) self.state.read(cx).is_authenticated(cx)
.map(|m| m.read(cx).is_authenticated())
.unwrap_or(false)
} }
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> { fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> {
@ -122,29 +128,16 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
Task::ready(result) Task::ready(result)
} }
fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView { fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
cx.new_view(|cx| AuthenticationPrompt::new(cx)).into() let state = self.state.clone();
let view = 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<()>> {
let Some(copilot) = Copilot::global(cx) else { Task::ready(Err(anyhow!(
return Task::ready(Err(anyhow::anyhow!( "Signing out of GitHub Copilot Chat is currently not supported."
"Copilot is not available. Please ensure Copilot is enabled and running and try again." )))
)));
};
let state = self.state.clone();
cx.spawn(|mut cx| async move {
cx.update_model(&copilot, |model, cx| model.sign_out(cx))?
.await?;
cx.update_model(&state, |_, cx| {
cx.notify();
})?;
Ok(())
})
} }
} }
@ -281,17 +274,19 @@ impl CopilotChatLanguageModel {
} }
} }
struct AuthenticationPrompt { struct ConfigurationView {
copilot_status: Option<copilot::Status>, copilot_status: Option<copilot::Status>,
state: Model<State>,
_subscription: Option<Subscription>, _subscription: Option<Subscription>,
} }
impl AuthenticationPrompt { impl ConfigurationView {
pub fn new(cx: &mut ViewContext<Self>) -> Self { pub fn new(state: Model<State>, cx: &mut ViewContext<Self>) -> Self {
let copilot = Copilot::global(cx); let copilot = Copilot::global(cx);
Self { Self {
copilot_status: copilot.as_ref().map(|copilot| copilot.read(cx).status()), copilot_status: copilot.as_ref().map(|copilot| copilot.read(cx).status()),
state,
_subscription: copilot.as_ref().map(|copilot| { _subscription: copilot.as_ref().map(|copilot| {
cx.observe(copilot, |this, model, cx| { cx.observe(copilot, |this, model, cx| {
this.copilot_status = Some(model.read(cx).status()); this.copilot_status = Some(model.read(cx).status());
@ -302,8 +297,15 @@ impl AuthenticationPrompt {
} }
} }
impl Render for AuthenticationPrompt { impl Render for ConfigurationView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
if self.state.read(cx).is_authenticated(cx) {
const LABEL: &str = "Authorized.";
h_flex()
.gap_2()
.child(Indicator::dot().color(Color::Success))
.child(Label::new(LABEL))
} else {
let loading_icon = svg() let loading_icon = svg()
.size_8() .size_8()
.path(IconName::ArrowCircle.path()) .path(IconName::ArrowCircle.path())
@ -315,43 +317,39 @@ impl Render for AuthenticationPrompt {
); );
const ERROR_LABEL: &str = "Copilot Chat requires the Copilot plugin to be available and running. Please ensure Copilot is running and try again, or use a different Assistant provider."; const ERROR_LABEL: &str = "Copilot Chat requires the Copilot plugin to be available and running. Please ensure Copilot is running and try again, or use a different Assistant provider.";
match &self.copilot_status { match &self.copilot_status {
Some(status) => match status { Some(status) => match status {
Status::Disabled => { Status::Disabled => v_flex().gap_6().p_4().child(Label::new(ERROR_LABEL)),
return v_flex().gap_6().p_4().child(Label::new(ERROR_LABEL));
}
Status::Starting { task: _ } => { Status::Starting { task: _ } => {
const LABEL: &str = "Starting Copilot..."; const LABEL: &str = "Starting Copilot...";
return v_flex() v_flex()
.gap_6() .gap_6()
.p_4()
.justify_center() .justify_center()
.items_center() .items_center()
.child(Label::new(LABEL)) .child(Label::new(LABEL))
.child(loading_icon); .child(loading_icon)
} }
Status::SigningIn { prompt: _ } => { Status::SigningIn { prompt: _ } => {
const LABEL: &str = "Signing in to Copilot..."; const LABEL: &str = "Signing in to Copilot...";
return v_flex() v_flex()
.gap_6() .gap_6()
.p_4()
.justify_center() .justify_center()
.items_center() .items_center()
.child(Label::new(LABEL)) .child(Label::new(LABEL))
.child(loading_icon); .child(loading_icon)
} }
Status::Error(_) => { Status::Error(_) => {
const LABEL: &str = "Copilot had issues starting. Please try restarting it. If the issue persists, try reinstalling Copilot."; const LABEL: &str = "Copilot had issues starting. Please try restarting it. If the issue persists, try reinstalling Copilot.";
return v_flex() v_flex()
.gap_6() .gap_6()
.p_4()
.child(Label::new(LABEL)) .child(Label::new(LABEL))
.child(svg().size_8().path(IconName::CopilotError.path())); .child(svg().size_8().path(IconName::CopilotError.path()))
} }
_ => { _ => {
const LABEL: &str = const LABEL: &str =
"To use the assistant panel or inline assistant, you must login to GitHub Copilot. Your GitHub account must have an active Copilot Chat subscription."; "To use the assistant panel or inline assistant, you must login to GitHub Copilot. Your GitHub account must have an active Copilot Chat subscription.";
v_flex().gap_6().p_4().child(Label::new(LABEL)).child( v_flex().gap_6().child(Label::new(LABEL)).child(
v_flex() v_flex()
.gap_2() .gap_2()
.child( .child(
@ -376,7 +374,8 @@ impl Render for AuthenticationPrompt {
) )
} }
}, },
None => v_flex().gap_6().p_4().child(Label::new(ERROR_LABEL)), None => v_flex().gap_6().child(Label::new(ERROR_LABEL)),
}
} }
} }
} }

View file

@ -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, Task}; use gpui::{AnyView, AppContext, AsyncAppContext, FocusHandle, 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 authentication_prompt(&self, _: &mut WindowContext) -> AnyView { fn configuration_view(&self, _: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
unimplemented!() unimplemented!()
} }

View file

@ -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, FontStyle, Subscription, Task, TextStyle, View, AnyView, AppContext, AsyncAppContext, FocusHandle, FocusableView, FontStyle, ModelContext,
WhiteSpace, Subscription, Task, TextStyle, View, WhiteSpace,
}; };
use http_client::HttpClient; use http_client::HttpClient;
use schemars::JsonSchema; use schemars::JsonSchema;
@ -14,7 +14,7 @@ use settings::{Settings, SettingsStore};
use std::{future, sync::Arc, time::Duration}; use std::{future, sync::Arc, time::Duration};
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::prelude::*; use ui::{prelude::*, Indicator};
use util::ResultExt; use util::ResultExt;
use crate::{ use crate::{
@ -49,6 +49,24 @@ pub struct State {
_subscription: Subscription, _subscription: Subscription,
} }
impl State {
fn is_authenticated(&self) -> bool {
self.api_key.is_some()
}
fn reset_api_key(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
let delete_credentials =
cx.delete_credentials(&AllLanguageModelSettings::get_global(cx).google.api_url);
cx.spawn(|this, mut cx| async move {
delete_credentials.await.ok();
this.update(&mut cx, |this, cx| {
this.api_key = None;
cx.notify();
})
})
}
}
impl GoogleLanguageModelProvider { impl GoogleLanguageModelProvider {
pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Self { pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Self {
let state = cx.new_model(|cx| State { let state = cx.new_model(|cx| State {
@ -118,7 +136,7 @@ impl LanguageModelProvider for GoogleLanguageModelProvider {
} }
fn is_authenticated(&self, cx: &AppContext) -> bool { fn is_authenticated(&self, cx: &AppContext) -> bool {
self.state.read(cx).api_key.is_some() self.state.read(cx).is_authenticated()
} }
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> { fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> {
@ -149,9 +167,11 @@ impl LanguageModelProvider for GoogleLanguageModelProvider {
} }
} }
fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView { fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
cx.new_view(|cx| AuthenticationPrompt::new(self.state.clone(), cx)) let view = 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<()>> {
@ -267,15 +287,15 @@ impl LanguageModel for GoogleLanguageModel {
} }
} }
struct AuthenticationPrompt { struct ConfigurationView {
api_key: View<Editor>, api_key_editor: View<Editor>,
state: gpui::Model<State>, state: gpui::Model<State>,
} }
impl AuthenticationPrompt { impl ConfigurationView {
fn new(state: gpui::Model<State>, cx: &mut WindowContext) -> Self { fn new(state: gpui::Model<State>, cx: &mut WindowContext) -> Self {
Self { Self {
api_key: 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("AIzaSy...", cx); editor.set_placeholder_text("AIzaSy...", cx);
editor editor
@ -285,7 +305,7 @@ impl AuthenticationPrompt {
} }
fn save_api_key(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) { fn save_api_key(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
let api_key = self.api_key.read(cx).text(cx); let api_key = self.api_key_editor.read(cx).text(cx);
if api_key.is_empty() { if api_key.is_empty() {
return; return;
} }
@ -304,6 +324,14 @@ impl AuthenticationPrompt {
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
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);
}
fn render_api_key_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render_api_key_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx); let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle { let text_style = TextStyle {
@ -321,7 +349,7 @@ impl AuthenticationPrompt {
white_space: WhiteSpace::Normal, white_space: WhiteSpace::Normal,
}; };
EditorElement::new( EditorElement::new(
&self.api_key, &self.api_key_editor,
EditorStyle { EditorStyle {
background: cx.theme().colors().editor_background, background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(), local_player: cx.theme().players().local(),
@ -332,7 +360,13 @@ impl AuthenticationPrompt {
} }
} }
impl Render for AuthenticationPrompt { impl FocusableView for ConfigurationView {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.api_key_editor.read(cx).focus_handle(cx)
}
}
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] = [
"To use the Google AI assistant, you need to add your Google AI API key.", "To use the Google AI assistant, you need to add your Google AI API key.",
@ -341,8 +375,26 @@ impl Render for AuthenticationPrompt {
"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.state.read(cx).is_authenticated() {
h_flex()
.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(
Button::new("reset-key", "Reset key")
.icon(Some(IconName::Trash))
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.on_click(cx.listener(|this, _, cx| this.reset_api_key(cx))),
)
.into_any()
} else {
v_flex() v_flex()
.p_4()
.size_full() .size_full()
.on_action(cx.listener(Self::save_api_key)) .on_action(cx.listener(Self::save_api_key))
.children( .children(
@ -364,15 +416,7 @@ impl Render for AuthenticationPrompt {
) )
.size(LabelSize::Small), .size(LabelSize::Small),
) )
.child(
h_flex()
.gap_2()
.child(Label::new("Click on").size(LabelSize::Small))
.child(Icon::new(IconName::ZedAssistant).size(IconSize::XSmall))
.child(
Label::new("in the status bar to close this panel.").size(LabelSize::Small),
),
)
.into_any() .into_any()
} }
}
} }

View file

@ -1,13 +1,13 @@
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, ModelContext, Subscription, Task}; use gpui::{AnyView, AppContext, AsyncAppContext, FocusHandle, 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,
}; };
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, ElevationIndex}; use ui::{prelude::*, ButtonLike, ElevationIndex, Indicator};
use crate::{ use crate::{
settings::AllLanguageModelSettings, LanguageModel, LanguageModelId, LanguageModelName, settings::AllLanguageModelSettings, LanguageModel, LanguageModelId, LanguageModelName,
@ -39,6 +39,10 @@ pub struct State {
} }
impl State { impl State {
fn is_authenticated(&self) -> bool {
!self.available_models.is_empty()
}
fn fetch_models(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> { fn fetch_models(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
let settings = &AllLanguageModelSettings::get_global(cx).ollama; let settings = &AllLanguageModelSettings::get_global(cx).ollama;
let http_client = self.http_client.clone(); let http_client = self.http_client.clone();
@ -129,7 +133,7 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
} }
fn is_authenticated(&self, cx: &AppContext) -> bool { fn is_authenticated(&self, cx: &AppContext) -> bool {
!self.state.read(cx).available_models.is_empty() self.state.read(cx).is_authenticated()
} }
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> { fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> {
@ -140,14 +144,12 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
} }
} }
fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView { fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
let state = self.state.clone(); let state = self.state.clone();
let fetch_models = Box::new(move |cx: &mut WindowContext| { (
state.update(cx, |this, cx| this.fetch_models(cx)) cx.new_view(|cx| ConfigurationView::new(state, cx)).into(),
}); None,
)
cx.new_view(|cx| DownloadOllamaMessage::new(fetch_models, cx))
.into()
} }
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>> { fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>> {
@ -287,16 +289,19 @@ impl LanguageModel for OllamaLanguageModel {
} }
} }
struct DownloadOllamaMessage { struct ConfigurationView {
retry_connection: Box<dyn Fn(&mut WindowContext) -> Task<Result<()>>>, state: gpui::Model<State>,
} }
impl DownloadOllamaMessage { impl ConfigurationView {
pub fn new( pub fn new(state: gpui::Model<State>, _cx: &mut ViewContext<Self>) -> Self {
retry_connection: Box<dyn Fn(&mut WindowContext) -> Task<Result<()>>>, Self { state }
_cx: &mut ViewContext<Self>, }
) -> Self {
Self { retry_connection } fn retry_connection(&self, cx: &mut WindowContext) {
self.state
.update(cx, |state, cx| state.fetch_models(cx))
.detach_and_log_err(cx);
} }
fn render_download_button(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement { fn render_download_button(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
@ -314,15 +319,7 @@ impl DownloadOllamaMessage {
.size(ButtonSize::Large) .size(ButtonSize::Large)
.layer(ElevationIndex::ModalSurface) .layer(ElevationIndex::ModalSurface)
.child(Label::new("Retry")) .child(Label::new("Retry"))
.on_click(cx.listener(move |this, _, cx| { .on_click(cx.listener(move |this, _, cx| this.retry_connection(cx)))
let connected = (this.retry_connection)(cx);
cx.spawn(|_this, _cx| async move {
connected.await?;
anyhow::Ok(())
})
.detach_and_log_err(cx)
}))
} }
fn render_next_steps(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement { fn render_next_steps(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
@ -347,10 +344,22 @@ impl DownloadOllamaMessage {
} }
} }
impl Render for DownloadOllamaMessage { 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 is_authenticated = self.state.read(cx).is_authenticated();
if is_authenticated {
v_flex()
.size_full()
.child(
h_flex()
.gap_2()
.child(Indicator::dot().color(Color::Success))
.child(Label::new("Ollama configured").size(LabelSize::Small)),
)
.into_any()
} else {
v_flex() v_flex()
.p_4()
.size_full() .size_full()
.gap_2() .gap_2()
.child(Label::new("To use Ollama models via the assistant, Ollama must be running on your machine with at least one model downloaded.").size(LabelSize::Large)) .child(Label::new("To use Ollama models via the assistant, Ollama must be running on your machine with at least one model downloaded.").size(LabelSize::Large))
@ -370,4 +379,5 @@ impl Render for DownloadOllamaMessage {
.child(self.render_next_steps(cx)) .child(self.render_next_steps(cx))
.into_any() .into_any()
} }
}
} }

View file

@ -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, FontStyle, Subscription, Task, TextStyle, View, AnyView, AppContext, AsyncAppContext, FocusHandle, FocusableView, FontStyle, ModelContext,
WhiteSpace, Subscription, Task, TextStyle, View, WhiteSpace,
}; };
use http_client::HttpClient; use http_client::HttpClient;
use open_ai::stream_completion; use open_ai::stream_completion;
@ -14,7 +14,7 @@ use settings::{Settings, SettingsStore};
use std::{future, sync::Arc, time::Duration}; use std::{future, sync::Arc, time::Duration};
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::prelude::*; use ui::{prelude::*, Indicator};
use util::ResultExt; use util::ResultExt;
use crate::{ use crate::{
@ -50,6 +50,24 @@ pub struct State {
_subscription: Subscription, _subscription: Subscription,
} }
impl State {
fn is_authenticated(&self) -> bool {
self.api_key.is_some()
}
fn reset_api_key(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
let settings = &AllLanguageModelSettings::get_global(cx).openai;
let delete_credentials = cx.delete_credentials(&settings.api_url);
cx.spawn(|this, mut cx| async move {
delete_credentials.await.log_err();
this.update(&mut cx, |this, cx| {
this.api_key = None;
cx.notify();
})
})
}
}
impl OpenAiLanguageModelProvider { impl OpenAiLanguageModelProvider {
pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Self { pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Self {
let state = cx.new_model(|cx| State { let state = cx.new_model(|cx| State {
@ -119,7 +137,7 @@ impl LanguageModelProvider for OpenAiLanguageModelProvider {
} }
fn is_authenticated(&self, cx: &AppContext) -> bool { fn is_authenticated(&self, cx: &AppContext) -> bool {
self.state.read(cx).api_key.is_some() self.state.read(cx).is_authenticated()
} }
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> { fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> {
@ -149,22 +167,14 @@ impl LanguageModelProvider for OpenAiLanguageModelProvider {
} }
} }
fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView { fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
cx.new_view(|cx| AuthenticationPrompt::new(self.state.clone(), cx)) let view = 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<()>> {
let settings = &AllLanguageModelSettings::get_global(cx).openai; self.state.update(cx, |state, cx| state.reset_api_key(cx))
let delete_credentials = cx.delete_credentials(&settings.api_url);
let state = self.state.clone();
cx.spawn(|mut cx| async move {
delete_credentials.await.log_err();
state.update(&mut cx, |this, cx| {
this.api_key = None;
cx.notify();
})
})
} }
} }
@ -287,15 +297,15 @@ pub fn count_open_ai_tokens(
.boxed() .boxed()
} }
struct AuthenticationPrompt { struct ConfigurationView {
api_key: View<Editor>, api_key_editor: View<Editor>,
state: gpui::Model<State>, state: gpui::Model<State>,
} }
impl AuthenticationPrompt { impl ConfigurationView {
fn new(state: gpui::Model<State>, cx: &mut WindowContext) -> Self { fn new(state: gpui::Model<State>, cx: &mut WindowContext) -> Self {
Self { Self {
api_key: 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(
"sk-000000000000000000000000000000000000000000000000", "sk-000000000000000000000000000000000000000000000000",
@ -308,7 +318,7 @@ impl AuthenticationPrompt {
} }
fn save_api_key(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) { fn save_api_key(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
let api_key = self.api_key.read(cx).text(cx); let api_key = self.api_key_editor.read(cx).text(cx);
if api_key.is_empty() { if api_key.is_empty() {
return; return;
} }
@ -327,6 +337,14 @@ impl AuthenticationPrompt {
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
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);
})
}
fn render_api_key_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render_api_key_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx); let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle { let text_style = TextStyle {
@ -344,7 +362,7 @@ impl AuthenticationPrompt {
white_space: WhiteSpace::Normal, white_space: WhiteSpace::Normal,
}; };
EditorElement::new( EditorElement::new(
&self.api_key, &self.api_key_editor,
EditorStyle { EditorStyle {
background: cx.theme().colors().editor_background, background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(), local_player: cx.theme().players().local(),
@ -355,7 +373,13 @@ impl AuthenticationPrompt {
} }
} }
impl Render for AuthenticationPrompt { impl FocusableView for ConfigurationView {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.api_key_editor.read(cx).focus_handle(cx)
}
}
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] = [
"To use the assistant panel or inline assistant, you need to add your OpenAI API key.", "To use the assistant panel or inline assistant, you need to add your OpenAI API key.",
@ -366,8 +390,26 @@ impl Render for AuthenticationPrompt {
"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.state.read(cx).is_authenticated() {
h_flex()
.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(
Button::new("reset-key", "Reset key")
.icon(Some(IconName::Trash))
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.on_click(cx.listener(|this, _, cx| this.reset_api_key(cx))),
)
.into_any()
} else {
v_flex() v_flex()
.p_4()
.size_full() .size_full()
.on_action(cx.listener(Self::save_api_key)) .on_action(cx.listener(Self::save_api_key))
.children( .children(
@ -389,15 +431,7 @@ impl Render for AuthenticationPrompt {
) )
.size(LabelSize::Small), .size(LabelSize::Small),
) )
.child(
h_flex()
.gap_2()
.child(Label::new("Click on").size(LabelSize::Small))
.child(Icon::new(IconName::ZedAssistant).size(IconSize::XSmall))
.child(
Label::new("in the status bar to close this panel.").size(LabelSize::Small),
),
)
.into_any() .into_any()
} }
}
} }