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:
parent
79213637e2
commit
be3a8584ff
13 changed files with 934 additions and 476 deletions
|
@ -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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue