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

View file

@ -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 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)
}
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."
)));
};
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(())
})
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))),
);
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 => {
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(
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_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::SigningIn { prompt: _ } => {
const LABEL: &str = "Signing in to Copilot...";
v_flex()
.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)),
}
}
}
}