Cargo.lock 🔗
@@ -423,6 +423,7 @@ dependencies = [
"language",
"language_model",
"log",
+ "markdown",
"menu",
"multi_buffer",
"ollama",
Bennet Bo Fenner , Thorsten , Nate Butler , and Thorsten Ball created
- [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>
Cargo.lock | 1
crates/assistant/Cargo.toml | 1
crates/assistant/src/assistant.rs | 2
crates/assistant/src/assistant_panel.rs | 493 ++++++++++++---
crates/assistant/src/using-the-assistant.md | 25
crates/language_model/src/language_model.rs | 4
crates/language_model/src/provider/anthropic.rs | 187 +++--
crates/language_model/src/provider/cloud.rs | 122 ++-
crates/language_model/src/provider/copilot_chat.rs | 207 +++---
crates/language_model/src/provider/fake.rs | 4
crates/language_model/src/provider/google.rs | 136 ++-
crates/language_model/src/provider/ollama.rs | 70 +
crates/language_model/src/provider/open_ai.rs | 146 ++-
13 files changed, 928 insertions(+), 470 deletions(-)
@@ -423,6 +423,7 @@ dependencies = [
"language",
"language_model",
"log",
+ "markdown",
"menu",
"multi_buffer",
"ollama",
@@ -47,6 +47,7 @@ indoc.workspace = true
language.workspace = true
language_model.workspace = true
log.workspace = true
+markdown.workspace = true
menu.workspace = true
multi_buffer.workspace = true
ollama = { workspace = true, features = ["schemars"] }
@@ -45,8 +45,8 @@ actions!(
QuoteSelection,
InsertIntoEditor,
ToggleFocus,
- ResetKey,
InsertActivePrompt,
+ ShowConfiguration,
DeployHistory,
DeployPromptLibrary,
ConfirmCommand,
@@ -1,4 +1,3 @@
-use crate::ContextStoreEvent;
use crate::{
assistant_settings::{AssistantDockPosition, AssistantSettings},
humanize_token_count,
@@ -13,8 +12,9 @@ use crate::{
DebugEditSteps, DeployHistory, DeployPromptLibrary, EditStep, EditStepOperations,
EditSuggestionGroup, InlineAssist, InlineAssistId, InlineAssistant, InsertIntoEditor,
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 assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
use client::proto;
@@ -31,18 +31,20 @@ use editor::{
use editor::{display_map::CreaseId, FoldPlaceholder};
use fs::Fs;
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,
FocusHandle, FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Pixels,
- Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Transformation,
- UpdateGlobal, View, ViewContext, VisualContext, WeakView, WindowContext,
+ Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task,
+ TextStyleRefinement, Transformation, UpdateGlobal, View, ViewContext, VisualContext, WeakView,
+ WindowContext,
};
use indexed_docs::IndexedDocsStore;
use language::{
language_settings::SoftWrap, Buffer, Capability, LanguageRegistry, LspAdapterDelegate, Point,
ToOffset,
};
-use language_model::{LanguageModelProviderId, LanguageModelRegistry, Role};
+use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Role};
+use markdown::{Markdown, MarkdownStyle};
use multi_buffer::MultiBufferRow;
use picker::{Picker, PickerDelegate};
use project::{Project, ProjectLspAdapterDelegate};
@@ -58,6 +60,7 @@ use std::{
time::Duration,
};
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
+use theme::ThemeSettings;
use ui::TintColor;
use ui::{
prelude::*,
@@ -91,7 +94,8 @@ pub fn init(cx: &mut AppContext) {
})
.register_action(AssistantPanel::inline_assist)
.register_action(ContextEditor::quote_selection)
- .register_action(ContextEditor::insert_selection);
+ .register_action(ContextEditor::insert_selection)
+ .register_action(AssistantPanel::show_configuration);
},
)
.detach();
@@ -136,7 +140,6 @@ pub struct AssistantPanel {
languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
subscriptions: Vec<Subscription>,
- authentication_prompt: Option<AnyView>,
model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
model_summary_editor: View<Editor>,
authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
@@ -365,6 +368,7 @@ impl AssistantPanel {
.action("New Context", Box::new(NewFile))
.action("History", Box::new(DeployHistory))
.action("Prompt Library", Box::new(DeployPromptLibrary))
+ .action("Configure", Box::new(ShowConfiguration))
.action(zoom_label, Box::new(ToggleZoom))
});
cx.subscribe(&menu, |pane, _, _: &DismissEvent, _| {
@@ -399,8 +403,10 @@ impl AssistantPanel {
language_model::Event::ActiveModelChanged => {
this.completion_provider_changed(cx);
}
- language_model::Event::ProviderStateChanged
- | language_model::Event::AddedProvider(_)
+ language_model::Event::ProviderStateChanged => {
+ this.ensure_authenticated(cx);
+ }
+ language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_) => {
this.ensure_authenticated(cx);
}
@@ -408,7 +414,7 @@ impl AssistantPanel {
),
];
- Self {
+ let mut this = Self {
pane,
workspace: workspace.weak_handle(),
width: None,
@@ -418,11 +424,21 @@ impl AssistantPanel {
languages: workspace.app_state().languages.clone(),
fs: workspace.app_state().fs.clone(),
subscriptions,
- authentication_prompt: None,
model_selector_menu_handle,
model_summary_editor,
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(
@@ -582,63 +598,39 @@ impl AssistantPanel {
*old_provider_id != new_provider_id
})
{
+ self.authenticate_provider_task = None;
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>) {
if self.is_authenticated(cx) {
- self.set_authentication_prompt(None, cx);
return;
}
- let Some(provider_id) = LanguageModelRegistry::read_global(cx)
- .active_provider()
- .map(|p| p.id())
- else {
+ let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
return;
};
let load_credentials = self.authenticate(cx);
- self.authenticate_provider_task = Some((
- provider_id,
- cx.spawn(|this, mut cx| async move {
- let _ = load_credentials.await;
- this.update(&mut cx, |this, cx| {
- this.show_authentication_prompt(cx);
- this.authenticate_provider_task = None;
- })
- .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);
- });
+ if self.authenticate_provider_task.is_none() {
+ self.authenticate_provider_task = Some((
+ 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.authenticate_provider_task = None;
+ })
+ .log_err();
+ }),
+ ));
}
- cx.notify();
}
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>) {
let history_item_ix = self
.pane
@@ -931,35 +975,22 @@ impl AssistantPanel {
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>) {
self.model_selector_menu_handle.toggle(cx);
}
- fn context_editors(&self, cx: &AppContext) -> Vec<View<ContextEditor>> {
+ fn active_context_editor(&self, cx: &AppContext) -> Option<View<ContextEditor>> {
self.pane
.read(cx)
- .items_of_type::<ContextEditor>()
- .collect()
+ .active_item()?
+ .downcast::<ContextEditor>()
}
- fn active_context_editor(&self, cx: &AppContext) -> Option<View<ContextEditor>> {
+ fn has_any_context_editors(&self, cx: &AppContext) -> bool {
self.pane
.read(cx)
- .active_item()?
- .downcast::<ContextEditor>()
+ .items()
+ .any(|item| item.downcast::<ContextEditor>().is_some())
}
pub fn active_context(&self, cx: &AppContext) -> Option<Model<Context>> {
@@ -1083,8 +1114,10 @@ impl AssistantPanel {
|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(
|panel, cx| {
panel
@@ -1105,21 +1138,14 @@ impl 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(AssistantPanel::deploy_history))
.on_action(cx.listener(AssistantPanel::deploy_prompt_library))
- .on_action(cx.listener(AssistantPanel::reset_credentials))
.on_action(cx.listener(AssistantPanel::toggle_model_selector))
.child(registrar.size_full().child(self.pane.clone()))
- }
-}
-
-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()
- }
+ .into_any_element()
}
}
@@ -1242,7 +1268,6 @@ struct ActiveEditStep {
pub struct ContextEditor {
context: Model<Context>,
- authentication_prompt: Option<AnyView>,
fs: Arc<dyn Fs>,
workspace: WeakView<Workspace>,
project: Model<Project>,
@@ -1300,7 +1325,6 @@ impl ContextEditor {
let sections = context.read(cx).slash_command_output_sections().to_vec();
let mut this = Self {
context,
- authentication_prompt: None,
editor,
lsp_adapter_delegate,
blocks: Default::default(),
@@ -1320,15 +1344,6 @@ impl ContextEditor {
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>) {
let command_name = DefaultSlashCommand.name();
self.editor.update(cx, |editor, cx| {
@@ -1355,10 +1370,6 @@ impl ContextEditor {
}
fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
- if self.authentication_prompt.is_some() {
- return;
- }
-
if !self.apply_edit_step(cx) {
self.send_to_model(cx);
}
@@ -2419,26 +2430,19 @@ impl Render for ContextEditor {
.size_full()
.v_flex()
.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()
- .flex_grow()
- .bg(cx.theme().colors().editor_background)
- .child(self.editor.clone())
- .child(
- h_flex()
- .w_full()
- .absolute()
- .bottom_0()
- .p_4()
- .justify_end()
- .child(self.render_send_button(cx)),
- )
- },
+ div()
+ .flex_grow()
+ .bg(cx.theme().colors().editor_background)
+ .child(self.editor.clone())
+ .child(
+ h_flex()
+ .w_full()
+ .absolute()
+ .bottom_0()
+ .p_4()
+ .justify_end()
+ .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>;
fn render_slash_command_output_toggle(
@@ -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`.
@@ -9,7 +9,7 @@ pub mod settings;
use anyhow::Result;
use client::Client;
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::*;
use project::Fs;
pub(crate) use rate_limiter::*;
@@ -84,7 +84,7 @@ pub trait LanguageModelProvider: 'static {
fn load_model(&self, _model: Arc<dyn LanguageModel>, _cx: &AppContext) {}
fn is_authenticated(&self, cx: &AppContext) -> bool;
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>>;
- fn 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<()>>;
}
@@ -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, FontStyle, Subscription, Task, TextStyle, View,
- WhiteSpace,
+ AnyView, AppContext, AsyncAppContext, FocusHandle, FocusableView, FontStyle, ModelContext,
+ Subscription, Task, TextStyle, View, WhiteSpace,
};
use http_client::HttpClient;
use schemars::JsonSchema;
@@ -18,8 +18,7 @@ use settings::{Settings, SettingsStore};
use std::{sync::Arc, time::Duration};
use strum::IntoEnumIterator;
use theme::ThemeSettings;
-use ui::prelude::*;
-use util::ResultExt;
+use ui::{prelude::*, Indicator};
const PROVIDER_ID: &str = "anthropic";
const PROVIDER_NAME: &str = "Anthropic";
@@ -49,6 +48,43 @@ pub struct State {
_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 {
pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Self {
let state = cx.new_model(|cx| State {
@@ -120,7 +156,7 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider {
}
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<()>> {
@@ -151,22 +187,14 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider {
}
}
- fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
- cx.new_view(|cx| AuthenticationPrompt::new(self.state.clone(), cx))
- .into()
+ fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
+ let view = cx.new_view(|cx| ConfigurationView::new(self.state.clone(), cx));
+ let focus_handle = view.focus_handle(cx);
+ (view.into(), Some(focus_handle))
}
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>> {
- let state = self.state.clone();
- 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();
- })
- })
+ self.state.update(cx, |state, cx| state.reset_api_key(cx))
}
}
@@ -350,18 +378,24 @@ impl LanguageModel for AnthropicModel {
}
}
-struct AuthenticationPrompt {
- api_key: View<Editor>,
+struct ConfigurationView {
+ api_key_editor: View<Editor>,
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 {
Self {
- api_key: cx.new_view(|cx| {
+ api_key_editor: cx.new_view(|cx| {
let mut editor = Editor::single_line(cx);
editor.set_placeholder_text(
- "sk-000000000000000000000000000000000000000000000000",
+ "sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
cx,
);
editor
@@ -371,29 +405,22 @@ impl AuthenticationPrompt {
}
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() {
return;
}
- let write_credentials = cx.write_credentials(
- AllLanguageModelSettings::get_global(cx)
- .anthropic
- .api_url
- .as_str(),
- "Bearer",
- api_key.as_bytes(),
- );
- let state = self.state.clone();
- cx.spawn(|_, mut cx| async move {
- write_credentials.await?;
+ self.state
+ .update(cx, |state, cx| state.set_api_key(api_key, cx))
+ .detach_and_log_err(cx);
+ }
- state.update(&mut cx, |this, cx| {
- this.api_key = Some(api_key);
- cx.notify();
- })
- })
- .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 {
@@ -413,7 +440,7 @@ impl AuthenticationPrompt {
white_space: WhiteSpace::Normal,
};
EditorElement::new(
- &self.api_key,
+ &self.api_key_editor,
EditorStyle {
background: cx.theme().colors().editor_background,
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 {
const INSTRUCTIONS: [&str; 4] = [
"To use the assistant panel or inline assistant, you need to add your Anthropic API key.",
@@ -433,38 +460,48 @@ impl Render for AuthenticationPrompt {
"Paste your Anthropic API key below and hit enter to use the assistant:",
];
- v_flex()
- .p_4()
- .size_full()
- .on_action(cx.listener(Self::save_api_key))
- .children(
- INSTRUCTIONS.map(|instruction| Label::new(instruction).size(LabelSize::Small)),
- )
- .child(
- h_flex()
- .w_full()
- .my_2()
- .px_2()
- .py_1()
- .bg(cx.theme().colors().editor_background)
- .rounded_md()
- .child(self.render_api_key_editor(cx)),
- )
- .child(
- Label::new(
- "You can also assign the ANTHROPIC_API_KEY environment variable and restart Zed.",
+ 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)),
)
- .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()
+ .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()
+ .size_full()
+ .on_action(cx.listener(Self::save_api_key))
+ .children(
+ INSTRUCTIONS.map(|instruction| Label::new(instruction).size(LabelSize::Small)),
+ )
+ .child(
+ h_flex()
+ .w_full()
+ .my_2()
+ .px_2()
+ .py_1()
+ .bg(cx.theme().colors().editor_background)
+ .rounded_md()
+ .child(self.render_api_key_editor(cx)),
+ )
+ .child(
+ Label::new(
+ "You can also assign the ANTHROPIC_API_KEY environment variable and restart Zed.",
+ )
+ .size(LabelSize::Small),
+ )
+ .into_any()
+ }
}
}
@@ -8,7 +8,7 @@ use anyhow::{anyhow, Context as _, Result};
use client::Client;
use collections::BTreeMap;
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 serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
@@ -21,7 +21,7 @@ use crate::LanguageModelProvider;
use super::anthropic::count_anthropic_tokens;
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)]
pub struct ZedDotDevSettings {
@@ -57,6 +57,10 @@ pub struct State {
}
impl State {
+ fn is_connected(&self) -> bool {
+ self.status.is_connected()
+ }
+
fn authenticate(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
let client = self.client.clone();
cx.spawn(move |this, mut cx| async move {
@@ -179,15 +183,17 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
self.state.read(cx).status.is_connected()
}
- fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> {
- self.state.update(cx, |state, cx| state.authenticate(cx))
+ fn authenticate(&self, _cx: &mut AppContext) -> Task<Result<()>> {
+ Task::ready(Ok(()))
}
- fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
- cx.new_view(|_cx| AuthenticationPrompt {
- state: self.state.clone(),
- })
- .into()
+ fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
+ let view = cx
+ .new_view(|_cx| ConfigurationView {
+ state: self.state.clone(),
+ })
+ .into();
+ (view, None)
}
fn reset_credentials(&self, _cx: &mut AppContext) -> Task<Result<()>> {
@@ -376,38 +382,88 @@ impl LanguageModel for CloudLanguageModel {
}
}
-struct AuthenticationPrompt {
+struct ConfigurationView {
state: gpui::Model<State>,
}
-impl Render for AuthenticationPrompt {
+impl ConfigurationView {
+ fn authenticate(&mut self, cx: &mut ViewContext<Self>) {
+ self.state.update(cx, |state, cx| {
+ state.authenticate(cx).detach_and_log_err(cx);
+ });
+ cx.notify();
+ }
+}
+
+impl Render for ConfigurationView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- const LABEL: &str = "Generate and analyze code with language models. You can dialog with the assistant in this panel or transform code inline.";
+ 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;
- v_flex().gap_6().p_4().child(Label::new(LABEL)).child(
+ if is_connected {
v_flex()
- .gap_2()
+ .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(
- Button::new("sign_in", "Sign in")
- .icon_color(Color::Muted)
- .icon(IconName::Github)
- .icon_position(IconPosition::Start)
- .style(ButtonStyle::Filled)
- .full_width()
- .on_click(cx.listener(move |this, _, cx| {
- this.state.update(cx, |provider, cx| {
- provider.authenticate(cx).detach_and_log_err(cx);
- cx.notify();
- });
- })),
+ 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(
- div().flex().w_full().items_center().child(
- Label::new("Sign in to enable collaboration.")
- .color(Color::Muted)
- .size(LabelSize::Small),
- ),
- ),
- )
+ v_flex()
+ .gap_2()
+ .child(
+ Button::new("sign_in", "Sign in")
+ .icon_color(Color::Muted)
+ .icon(IconName::Github)
+ .icon_position(IconPosition::Start)
+ .style(ButtonStyle::Filled)
+ .full_width()
+ .on_click(cx.listener(move |this, _, cx| this.authenticate(cx))),
+ )
+ .child(
+ div().flex().w_full().items_center().child(
+ Label::new("Sign in to enable collaboration.")
+ .color(Color::Muted)
+ .size(LabelSize::Small),
+ ),
+ ),
+ )
+ }
}
}
@@ -11,16 +11,16 @@ use futures::future::BoxFuture;
use futures::stream::BoxStream;
use futures::{FutureExt, StreamExt};
use gpui::{
- percentage, svg, Animation, AnimationExt, AnyView, AppContext, AsyncAppContext, Model, Render,
- Subscription, Task, Transformation,
+ percentage, svg, Animation, AnimationExt, AnyView, AppContext, AsyncAppContext, FocusHandle,
+ Model, Render, Subscription, Task, Transformation,
};
use settings::{Settings, SettingsStore};
use std::time::Duration;
use strum::IntoEnumIterator;
use ui::{
- div, v_flex, Button, ButtonCommon, Clickable, Color, Context, FixedWidth, IconName,
- IconPosition, IconSize, IntoElement, Label, LabelCommon, ParentElement, Styled, ViewContext,
- VisualContext, WindowContext,
+ div, h_flex, v_flex, Button, ButtonCommon, Clickable, Color, Context, FixedWidth, IconName,
+ IconPosition, IconSize, Indicator, IntoElement, Label, LabelCommon, ParentElement, Styled,
+ ViewContext, VisualContext, WindowContext,
};
use crate::settings::AllLanguageModelSettings;
@@ -49,6 +49,14 @@ pub struct State {
_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 {
pub fn new(cx: &mut AppContext) -> Self {
let state = cx.new_model(|cx| {
@@ -95,9 +103,7 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
}
fn is_authenticated(&self, cx: &AppContext) -> bool {
- CopilotChat::global(cx)
- .map(|m| m.read(cx).is_authenticated())
- .unwrap_or(false)
+ self.state.read(cx).is_authenticated(cx)
}
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> {
@@ -122,29 +128,16 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
Task::ready(result)
}
- fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
- cx.new_view(|cx| AuthenticationPrompt::new(cx)).into()
- }
-
- fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>> {
- let Some(copilot) = Copilot::global(cx) else {
- return Task::ready(Err(anyhow::anyhow!(
- "Copilot is not available. Please ensure Copilot is enabled and running and try again."
- )));
- };
-
+ fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
let state = self.state.clone();
+ let view = cx.new_view(|cx| ConfigurationView::new(state, cx)).into();
+ (view, None)
+ }
- 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(())
- })
+ fn reset_credentials(&self, _cx: &mut AppContext) -> Task<Result<()>> {
+ Task::ready(Err(anyhow!(
+ "Signing out of GitHub Copilot Chat is currently not supported."
+ )))
}
}
@@ -281,17 +274,19 @@ impl CopilotChatLanguageModel {
}
}
-struct AuthenticationPrompt {
+struct ConfigurationView {
copilot_status: Option<copilot::Status>,
+ state: Model<State>,
_subscription: Option<Subscription>,
}
-impl AuthenticationPrompt {
- pub fn new(cx: &mut ViewContext<Self>) -> Self {
+impl ConfigurationView {
+ pub fn new(state: Model<State>, cx: &mut ViewContext<Self>) -> Self {
let copilot = Copilot::global(cx);
Self {
copilot_status: copilot.as_ref().map(|copilot| copilot.read(cx).status()),
+ state,
_subscription: copilot.as_ref().map(|copilot| {
cx.observe(copilot, |this, model, cx| {
this.copilot_status = Some(model.read(cx).status());
@@ -302,81 +297,85 @@ impl AuthenticationPrompt {
}
}
-impl Render for AuthenticationPrompt {
+impl Render for ConfigurationView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- let loading_icon = svg()
- .size_8()
- .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))),
- );
-
- 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 {
- Some(status) => match status {
- Status::Disabled => {
- return v_flex().gap_6().p_4().child(Label::new(ERROR_LABEL));
- }
- Status::Starting { task: _ } => {
- const LABEL: &str = "Starting Copilot...";
- return v_flex()
- .gap_6()
- .p_4()
- .justify_center()
- .items_center()
- .child(Label::new(LABEL))
- .child(loading_icon);
- }
- Status::SigningIn { prompt: _ } => {
- const LABEL: &str = "Signing in to Copilot...";
- return v_flex()
- .gap_6()
- .p_4()
- .justify_center()
- .items_center()
- .child(Label::new(LABEL))
- .child(loading_icon);
- }
- Status::Error(_) => {
- const LABEL: &str = "Copilot had issues starting. Please try restarting it. If the issue persists, try reinstalling Copilot.";
- return v_flex()
- .gap_6()
- .p_4()
- .child(Label::new(LABEL))
- .child(svg().size_8().path(IconName::CopilotError.path()));
- }
- _ => {
- 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.";
- v_flex().gap_6().p_4().child(Label::new(LABEL)).child(
+ 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()
+ .size_8()
+ .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))),
+ );
+
+ 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 {
+ Some(status) => match status {
+ Status::Disabled => v_flex().gap_6().p_4().child(Label::new(ERROR_LABEL)),
+ Status::Starting { task: _ } => {
+ const LABEL: &str = "Starting Copilot...";
+ v_flex()
+ .gap_6()
+ .justify_center()
+ .items_center()
+ .child(Label::new(LABEL))
+ .child(loading_icon)
+ }
+ Status::SigningIn { prompt: _ } => {
+ const LABEL: &str = "Signing in to Copilot...";
v_flex()
- .gap_2()
- .child(
- Button::new("sign_in", "Sign In")
- .icon_color(Color::Muted)
- .icon(IconName::Github)
- .icon_position(IconPosition::Start)
- .icon_size(IconSize::Medium)
- .style(ui::ButtonStyle::Filled)
- .full_width()
- .on_click(|_, cx| {
- inline_completion_button::initiate_sign_in(cx)
- }),
- )
- .child(
- div().flex().w_full().items_center().child(
- Label::new("Sign in to start using Github Copilot Chat.")
- .color(Color::Muted)
- .size(ui::LabelSize::Small),
+ .gap_6()
+ .justify_center()
+ .items_center()
+ .child(Label::new(LABEL))
+ .child(loading_icon)
+ }
+ Status::Error(_) => {
+ const LABEL: &str = "Copilot had issues starting. Please try restarting it. If the issue persists, try reinstalling Copilot.";
+ v_flex()
+ .gap_6()
+ .child(Label::new(LABEL))
+ .child(svg().size_8().path(IconName::CopilotError.path()))
+ }
+ _ => {
+ 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.";
+ v_flex().gap_6().child(Label::new(LABEL)).child(
+ v_flex()
+ .gap_2()
+ .child(
+ Button::new("sign_in", "Sign In")
+ .icon_color(Color::Muted)
+ .icon(IconName::Github)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::Medium)
+ .style(ui::ButtonStyle::Filled)
+ .full_width()
+ .on_click(|_, cx| {
+ inline_completion_button::initiate_sign_in(cx)
+ }),
+ )
+ .child(
+ div().flex().w_full().items_center().child(
+ Label::new("Sign in to start using Github Copilot Chat.")
+ .color(Color::Muted)
+ .size(ui::LabelSize::Small),
+ ),
),
- ),
- )
- }
- },
- None => v_flex().gap_6().p_4().child(Label::new(ERROR_LABEL)),
+ )
+ }
+ },
+ None => v_flex().gap_6().child(Label::new(ERROR_LABEL)),
+ }
}
}
}
@@ -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, Task};
+use gpui::{AnyView, AppContext, AsyncAppContext, FocusHandle, Task};
use http_client::Result;
use std::{
future,
@@ -66,7 +66,7 @@ impl LanguageModelProvider for FakeLanguageModelProvider {
Task::ready(Ok(()))
}
- fn authentication_prompt(&self, _: &mut WindowContext) -> AnyView {
+ fn configuration_view(&self, _: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
unimplemented!()
}
@@ -4,8 +4,8 @@ use editor::{Editor, EditorElement, EditorStyle};
use futures::{future::BoxFuture, FutureExt, StreamExt};
use google_ai::stream_generate_content;
use gpui::{
- AnyView, AppContext, AsyncAppContext, FontStyle, Subscription, Task, TextStyle, View,
- WhiteSpace,
+ AnyView, AppContext, AsyncAppContext, FocusHandle, FocusableView, FontStyle, ModelContext,
+ Subscription, Task, TextStyle, View, WhiteSpace,
};
use http_client::HttpClient;
use schemars::JsonSchema;
@@ -14,7 +14,7 @@ use settings::{Settings, SettingsStore};
use std::{future, sync::Arc, time::Duration};
use strum::IntoEnumIterator;
use theme::ThemeSettings;
-use ui::prelude::*;
+use ui::{prelude::*, Indicator};
use util::ResultExt;
use crate::{
@@ -49,6 +49,24 @@ pub struct State {
_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 {
pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Self {
let state = cx.new_model(|cx| State {
@@ -118,7 +136,7 @@ impl LanguageModelProvider for GoogleLanguageModelProvider {
}
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<()>> {
@@ -149,9 +167,11 @@ impl LanguageModelProvider for GoogleLanguageModelProvider {
}
}
- fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
- cx.new_view(|cx| AuthenticationPrompt::new(self.state.clone(), cx))
- .into()
+ fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
+ let view = cx.new_view(|cx| ConfigurationView::new(self.state.clone(), cx));
+
+ let focus_handle = view.focus_handle(cx);
+ (view.into(), Some(focus_handle))
}
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>> {
@@ -267,15 +287,15 @@ impl LanguageModel for GoogleLanguageModel {
}
}
-struct AuthenticationPrompt {
- api_key: View<Editor>,
+struct ConfigurationView {
+ api_key_editor: View<Editor>,
state: gpui::Model<State>,
}
-impl AuthenticationPrompt {
+impl ConfigurationView {
fn new(state: gpui::Model<State>, cx: &mut WindowContext) -> Self {
Self {
- api_key: cx.new_view(|cx| {
+ api_key_editor: cx.new_view(|cx| {
let mut editor = Editor::single_line(cx);
editor.set_placeholder_text("AIzaSy...", cx);
editor
@@ -285,7 +305,7 @@ impl AuthenticationPrompt {
}
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() {
return;
}
@@ -304,6 +324,14 @@ impl AuthenticationPrompt {
.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 {
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
@@ -321,7 +349,7 @@ impl AuthenticationPrompt {
white_space: WhiteSpace::Normal,
};
EditorElement::new(
- &self.api_key,
+ &self.api_key_editor,
EditorStyle {
background: cx.theme().colors().editor_background,
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 {
const INSTRUCTIONS: [&str; 4] = [
"To use the Google AI assistant, you need to add your Google AI API key.",
@@ -341,38 +375,48 @@ impl Render for AuthenticationPrompt {
"Paste your Google AI API key below and hit enter to use the assistant:",
];
- v_flex()
- .p_4()
- .size_full()
- .on_action(cx.listener(Self::save_api_key))
- .children(
- INSTRUCTIONS.map(|instruction| Label::new(instruction).size(LabelSize::Small)),
- )
- .child(
- h_flex()
- .w_full()
- .my_2()
- .px_2()
- .py_1()
- .bg(cx.theme().colors().editor_background)
- .rounded_md()
- .child(self.render_api_key_editor(cx)),
- )
- .child(
- Label::new(
- "You can also assign the GOOGLE_AI_API_KEY environment variable and restart Zed.",
+ 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)),
)
- .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()
+ .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()
+ .size_full()
+ .on_action(cx.listener(Self::save_api_key))
+ .children(
+ INSTRUCTIONS.map(|instruction| Label::new(instruction).size(LabelSize::Small)),
+ )
+ .child(
+ h_flex()
+ .w_full()
+ .my_2()
+ .px_2()
+ .py_1()
+ .bg(cx.theme().colors().editor_background)
+ .rounded_md()
+ .child(self.render_api_key_editor(cx)),
+ )
+ .child(
+ Label::new(
+ "You can also assign the GOOGLE_AI_API_KEY environment variable and restart Zed.",
+ )
+ .size(LabelSize::Small),
+ )
+ .into_any()
+ }
}
}
@@ -1,13 +1,13 @@
use anyhow::{anyhow, Result};
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 ollama::{
get_models, preload_model, stream_chat_completion, ChatMessage, ChatOptions, ChatRequest,
};
use settings::{Settings, SettingsStore};
use std::{future, sync::Arc, time::Duration};
-use ui::{prelude::*, ButtonLike, ElevationIndex};
+use ui::{prelude::*, ButtonLike, ElevationIndex, Indicator};
use crate::{
settings::AllLanguageModelSettings, LanguageModel, LanguageModelId, LanguageModelName,
@@ -39,6 +39,10 @@ pub struct State {
}
impl State {
+ fn is_authenticated(&self) -> bool {
+ !self.available_models.is_empty()
+ }
+
fn fetch_models(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
let settings = &AllLanguageModelSettings::get_global(cx).ollama;
let http_client = self.http_client.clone();
@@ -129,7 +133,7 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
}
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<()>> {
@@ -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 fetch_models = Box::new(move |cx: &mut WindowContext| {
- state.update(cx, |this, cx| this.fetch_models(cx))
- });
-
- cx.new_view(|cx| DownloadOllamaMessage::new(fetch_models, cx))
- .into()
+ (
+ cx.new_view(|cx| ConfigurationView::new(state, cx)).into(),
+ None,
+ )
}
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>> {
@@ -287,16 +289,19 @@ impl LanguageModel for OllamaLanguageModel {
}
}
-struct DownloadOllamaMessage {
- retry_connection: Box<dyn Fn(&mut WindowContext) -> Task<Result<()>>>,
+struct ConfigurationView {
+ state: gpui::Model<State>,
}
-impl DownloadOllamaMessage {
- pub fn new(
- retry_connection: Box<dyn Fn(&mut WindowContext) -> Task<Result<()>>>,
- _cx: &mut ViewContext<Self>,
- ) -> Self {
- Self { retry_connection }
+impl ConfigurationView {
+ pub fn new(state: gpui::Model<State>, _cx: &mut ViewContext<Self>) -> Self {
+ Self { state }
+ }
+
+ 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 {
@@ -314,15 +319,7 @@ impl DownloadOllamaMessage {
.size(ButtonSize::Large)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Retry"))
- .on_click(cx.listener(move |this, _, cx| {
- let connected = (this.retry_connection)(cx);
-
- cx.spawn(|_this, _cx| async move {
- connected.await?;
- anyhow::Ok(())
- })
- .detach_and_log_err(cx)
- }))
+ .on_click(cx.listener(move |this, _, cx| this.retry_connection(cx)))
}
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 {
- v_flex()
- .p_4()
+ 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()
.size_full()
.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))
@@ -369,5 +378,6 @@ impl Render for DownloadOllamaMessage {
)
.child(self.render_next_steps(cx))
.into_any()
+ }
}
}
@@ -3,8 +3,8 @@ use collections::BTreeMap;
use editor::{Editor, EditorElement, EditorStyle};
use futures::{future::BoxFuture, FutureExt, StreamExt};
use gpui::{
- AnyView, AppContext, AsyncAppContext, FontStyle, Subscription, Task, TextStyle, View,
- WhiteSpace,
+ AnyView, AppContext, AsyncAppContext, FocusHandle, FocusableView, FontStyle, ModelContext,
+ Subscription, Task, TextStyle, View, WhiteSpace,
};
use http_client::HttpClient;
use open_ai::stream_completion;
@@ -14,7 +14,7 @@ use settings::{Settings, SettingsStore};
use std::{future, sync::Arc, time::Duration};
use strum::IntoEnumIterator;
use theme::ThemeSettings;
-use ui::prelude::*;
+use ui::{prelude::*, Indicator};
use util::ResultExt;
use crate::{
@@ -50,6 +50,24 @@ pub struct State {
_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 {
pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Self {
let state = cx.new_model(|cx| State {
@@ -119,7 +137,7 @@ impl LanguageModelProvider for OpenAiLanguageModelProvider {
}
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<()>> {
@@ -149,22 +167,14 @@ impl LanguageModelProvider for OpenAiLanguageModelProvider {
}
}
- fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
- cx.new_view(|cx| AuthenticationPrompt::new(self.state.clone(), cx))
- .into()
+ fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
+ let view = cx.new_view(|cx| ConfigurationView::new(self.state.clone(), cx));
+ let focus_handle = view.focus_handle(cx);
+ (view.into(), Some(focus_handle))
}
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>> {
- let settings = &AllLanguageModelSettings::get_global(cx).openai;
- 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();
- })
- })
+ self.state.update(cx, |state, cx| state.reset_api_key(cx))
}
}
@@ -287,15 +297,15 @@ pub fn count_open_ai_tokens(
.boxed()
}
-struct AuthenticationPrompt {
- api_key: View<Editor>,
+struct ConfigurationView {
+ api_key_editor: View<Editor>,
state: gpui::Model<State>,
}
-impl AuthenticationPrompt {
+impl ConfigurationView {
fn new(state: gpui::Model<State>, cx: &mut WindowContext) -> Self {
Self {
- api_key: cx.new_view(|cx| {
+ api_key_editor: cx.new_view(|cx| {
let mut editor = Editor::single_line(cx);
editor.set_placeholder_text(
"sk-000000000000000000000000000000000000000000000000",
@@ -308,7 +318,7 @@ impl AuthenticationPrompt {
}
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() {
return;
}
@@ -327,6 +337,14 @@ impl AuthenticationPrompt {
.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 {
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
@@ -344,7 +362,7 @@ impl AuthenticationPrompt {
white_space: WhiteSpace::Normal,
};
EditorElement::new(
- &self.api_key,
+ &self.api_key_editor,
EditorStyle {
background: cx.theme().colors().editor_background,
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 {
const INSTRUCTIONS: [&str; 6] = [
"To use the assistant panel or inline assistant, you need to add your OpenAI API key.",
@@ -366,38 +390,48 @@ impl Render for AuthenticationPrompt {
"Paste your OpenAI API key below and hit enter to use the assistant:",
];
- v_flex()
- .p_4()
- .size_full()
- .on_action(cx.listener(Self::save_api_key))
- .children(
- INSTRUCTIONS.map(|instruction| Label::new(instruction).size(LabelSize::Small)),
- )
- .child(
- h_flex()
- .w_full()
- .my_2()
- .px_2()
- .py_1()
- .bg(cx.theme().colors().editor_background)
- .rounded_md()
- .child(self.render_api_key_editor(cx)),
- )
- .child(
- Label::new(
- "You can also assign the OPENAI_API_KEY environment variable and restart Zed.",
+ 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)),
)
- .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()
+ .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()
+ .size_full()
+ .on_action(cx.listener(Self::save_api_key))
+ .children(
+ INSTRUCTIONS.map(|instruction| Label::new(instruction).size(LabelSize::Small)),
+ )
+ .child(
+ h_flex()
+ .w_full()
+ .my_2()
+ .px_2()
+ .py_1()
+ .bg(cx.theme().colors().editor_background)
+ .rounded_md()
+ .child(self.render_api_key_editor(cx)),
+ )
+ .child(
+ Label::new(
+ "You can also assign the OPENAI_API_KEY environment variable and restart Zed.",
+ )
+ .size(LabelSize::Small),
+ )
+ .into_any()
+ }
}
}