@@ -8,6 +8,7 @@ use chrono::DateTime;
use collections::HashSet;
use fs::Fs;
use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
+use gpui::WeakEntity;
use gpui::{App, AsyncApp, Global, prelude::*};
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use itertools::Itertools;
@@ -15,9 +16,12 @@ use paths::home_dir;
use serde::{Deserialize, Serialize};
use settings::watch_config_dir;
-pub const COPILOT_CHAT_COMPLETION_URL: &str = "https://api.githubcopilot.com/chat/completions";
-pub const COPILOT_CHAT_AUTH_URL: &str = "https://api.github.com/copilot_internal/v2/token";
-pub const COPILOT_CHAT_MODELS_URL: &str = "https://api.githubcopilot.com/models";
+#[derive(Default, Clone, Debug, PartialEq)]
+pub struct CopilotChatSettings {
+ pub api_url: Arc<str>,
+ pub auth_url: Arc<str>,
+ pub models_url: Arc<str>,
+}
// Copilot's base model; defined by Microsoft in premium requests table
// This will be moved to the front of the Copilot model list, and will be used for
@@ -340,6 +344,7 @@ impl Global for GlobalCopilotChat {}
pub struct CopilotChat {
oauth_token: Option<String>,
api_token: Option<ApiToken>,
+ settings: CopilotChatSettings,
models: Option<Vec<Model>>,
client: Arc<dyn HttpClient>,
}
@@ -373,53 +378,30 @@ impl CopilotChat {
.map(|model| model.0.clone())
}
- pub fn new(fs: Arc<dyn Fs>, client: Arc<dyn HttpClient>, cx: &App) -> Self {
+ fn new(fs: Arc<dyn Fs>, client: Arc<dyn HttpClient>, cx: &mut Context<Self>) -> Self {
let config_paths: HashSet<PathBuf> = copilot_chat_config_paths().into_iter().collect();
let dir_path = copilot_chat_config_dir();
+ let settings = CopilotChatSettings::default();
+ cx.spawn(async move |this, cx| {
+ let mut parent_watch_rx = watch_config_dir(
+ cx.background_executor(),
+ fs.clone(),
+ dir_path.clone(),
+ config_paths,
+ );
+ while let Some(contents) = parent_watch_rx.next().await {
+ let oauth_token = extract_oauth_token(contents);
+
+ this.update(cx, |this, cx| {
+ this.oauth_token = oauth_token.clone();
+ cx.notify();
+ })?;
- cx.spawn({
- let client = client.clone();
- async move |cx| {
- let mut parent_watch_rx = watch_config_dir(
- cx.background_executor(),
- fs.clone(),
- dir_path.clone(),
- config_paths,
- );
- while let Some(contents) = parent_watch_rx.next().await {
- let oauth_token = extract_oauth_token(contents);
- cx.update(|cx| {
- if let Some(this) = Self::global(cx).as_ref() {
- this.update(cx, |this, cx| {
- this.oauth_token = oauth_token.clone();
- cx.notify();
- });
- }
- })?;
-
- if let Some(ref oauth_token) = oauth_token {
- let api_token = request_api_token(oauth_token, client.clone()).await?;
- cx.update(|cx| {
- if let Some(this) = Self::global(cx).as_ref() {
- this.update(cx, |this, cx| {
- this.api_token = Some(api_token.clone());
- cx.notify();
- });
- }
- })?;
- let models = get_models(api_token.api_key, client.clone()).await?;
- cx.update(|cx| {
- if let Some(this) = Self::global(cx).as_ref() {
- this.update(cx, |this, cx| {
- this.models = Some(models);
- cx.notify();
- });
- }
- })?;
- }
+ if oauth_token.is_some() {
+ Self::update_models(&this, cx).await?;
}
- anyhow::Ok(())
}
+ anyhow::Ok(())
})
.detach_and_log_err(cx);
@@ -427,10 +409,42 @@ impl CopilotChat {
oauth_token: None,
api_token: None,
models: None,
+ settings,
client,
}
}
+ async fn update_models(this: &WeakEntity<Self>, cx: &mut AsyncApp) -> Result<()> {
+ let (oauth_token, client, auth_url) = this.read_with(cx, |this, _| {
+ (
+ this.oauth_token.clone(),
+ this.client.clone(),
+ this.settings.auth_url.clone(),
+ )
+ })?;
+ let api_token = request_api_token(
+ &oauth_token.ok_or_else(|| {
+ anyhow!("OAuth token is missing while updating Copilot Chat models")
+ })?,
+ auth_url,
+ client.clone(),
+ )
+ .await?;
+
+ let models_url = this.update(cx, |this, cx| {
+ this.api_token = Some(api_token.clone());
+ cx.notify();
+ this.settings.models_url.clone()
+ })?;
+ let models = get_models(models_url, api_token.api_key, client.clone()).await?;
+
+ this.update(cx, |this, cx| {
+ this.models = Some(models);
+ cx.notify();
+ })?;
+ anyhow::Ok(())
+ }
+
pub fn is_authenticated(&self) -> bool {
self.oauth_token.is_some()
}
@@ -449,20 +463,23 @@ impl CopilotChat {
.flatten()
.context("Copilot chat is not enabled")?;
- let (oauth_token, api_token, client) = this.read_with(&cx, |this, _| {
- (
- this.oauth_token.clone(),
- this.api_token.clone(),
- this.client.clone(),
- )
- })?;
+ let (oauth_token, api_token, client, api_url, auth_url) =
+ this.read_with(&cx, |this, _| {
+ (
+ this.oauth_token.clone(),
+ this.api_token.clone(),
+ this.client.clone(),
+ this.settings.api_url.clone(),
+ this.settings.auth_url.clone(),
+ )
+ })?;
let oauth_token = oauth_token.context("No OAuth token available")?;
let token = match api_token {
Some(api_token) if api_token.remaining_seconds() > 5 * 60 => api_token.clone(),
_ => {
- let token = request_api_token(&oauth_token, client.clone()).await?;
+ let token = request_api_token(&oauth_token, auth_url, client.clone()).await?;
this.update(&mut cx, |this, cx| {
this.api_token = Some(token.clone());
cx.notify();
@@ -471,12 +488,28 @@ impl CopilotChat {
}
};
- stream_completion(client.clone(), token.api_key, request).await
+ stream_completion(client.clone(), token.api_key, api_url, request).await
+ }
+
+ pub fn set_settings(&mut self, settings: CopilotChatSettings, cx: &mut Context<Self>) {
+ let same_settings = self.settings == settings;
+ self.settings = settings;
+ if !same_settings {
+ cx.spawn(async move |this, cx| {
+ Self::update_models(&this, cx).await?;
+ Ok::<_, anyhow::Error>(())
+ })
+ .detach();
+ }
}
}
-async fn get_models(api_token: String, client: Arc<dyn HttpClient>) -> Result<Vec<Model>> {
- let all_models = request_models(api_token, client).await?;
+async fn get_models(
+ models_url: Arc<str>,
+ api_token: String,
+ client: Arc<dyn HttpClient>,
+) -> Result<Vec<Model>> {
+ let all_models = request_models(models_url, api_token, client).await?;
let mut models: Vec<Model> = all_models
.into_iter()
@@ -504,10 +537,14 @@ async fn get_models(api_token: String, client: Arc<dyn HttpClient>) -> Result<Ve
Ok(models)
}
-async fn request_models(api_token: String, client: Arc<dyn HttpClient>) -> Result<Vec<Model>> {
+async fn request_models(
+ models_url: Arc<str>,
+ api_token: String,
+ client: Arc<dyn HttpClient>,
+) -> Result<Vec<Model>> {
let request_builder = HttpRequest::builder()
.method(Method::GET)
- .uri(COPILOT_CHAT_MODELS_URL)
+ .uri(models_url.as_ref())
.header("Authorization", format!("Bearer {}", api_token))
.header("Content-Type", "application/json")
.header("Copilot-Integration-Id", "vscode-chat");
@@ -531,10 +568,14 @@ async fn request_models(api_token: String, client: Arc<dyn HttpClient>) -> Resul
Ok(models)
}
-async fn request_api_token(oauth_token: &str, client: Arc<dyn HttpClient>) -> Result<ApiToken> {
+async fn request_api_token(
+ oauth_token: &str,
+ auth_url: Arc<str>,
+ client: Arc<dyn HttpClient>,
+) -> Result<ApiToken> {
let request_builder = HttpRequest::builder()
.method(Method::GET)
- .uri(COPILOT_CHAT_AUTH_URL)
+ .uri(auth_url.as_ref())
.header("Authorization", format!("token {}", oauth_token))
.header("Accept", "application/json");
@@ -579,6 +620,7 @@ fn extract_oauth_token(contents: String) -> Option<String> {
async fn stream_completion(
client: Arc<dyn HttpClient>,
api_key: String,
+ completion_url: Arc<str>,
request: Request,
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
let is_vision_request = request.messages.last().map_or(false, |message| match message {
@@ -592,7 +634,7 @@ async fn stream_completion(
let request_builder = HttpRequest::builder()
.method(Method::POST)
- .uri(COPILOT_CHAT_COMPLETION_URL)
+ .uri(completion_url.as_ref())
.header(
"Editor-Version",
format!(
@@ -10,12 +10,14 @@ use copilot::copilot_chat::{
ToolCall,
};
use copilot::{Copilot, Status};
+use editor::{Editor, EditorElement, EditorStyle};
+use fs::Fs;
use futures::future::BoxFuture;
use futures::stream::BoxStream;
use futures::{FutureExt, Stream, StreamExt};
use gpui::{
- Action, Animation, AnimationExt, AnyView, App, AsyncApp, Entity, Render, Subscription, Task,
- Transformation, percentage, svg,
+ Action, Animation, AnimationExt, AnyView, App, AsyncApp, Entity, FontStyle, Render,
+ Subscription, Task, TextStyle, Transformation, WhiteSpace, percentage, svg,
};
use language_model::{
AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
@@ -25,21 +27,22 @@ use language_model::{
LanguageModelToolSchemaFormat, LanguageModelToolUse, MessageContent, RateLimiter, Role,
StopReason,
};
-use settings::SettingsStore;
+use settings::{Settings, SettingsStore, update_settings_file};
use std::time::Duration;
+use theme::ThemeSettings;
use ui::prelude::*;
use util::debug_panic;
+use crate::{AllLanguageModelSettings, CopilotChatSettingsContent};
+
use super::anthropic::count_anthropic_tokens;
use super::google::count_google_tokens;
use super::open_ai::count_open_ai_tokens;
+pub(crate) use copilot::copilot_chat::CopilotChatSettings;
const PROVIDER_ID: &str = "copilot_chat";
const PROVIDER_NAME: &str = "GitHub Copilot Chat";
-#[derive(Default, Clone, Debug, PartialEq)]
-pub struct CopilotChatSettings {}
-
pub struct CopilotChatLanguageModelProvider {
state: Entity<State>,
}
@@ -163,9 +166,10 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
Task::ready(Err(err.into()))
}
- fn configuration_view(&self, _: &mut Window, cx: &mut App) -> AnyView {
+ fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView {
let state = self.state.clone();
- cx.new(|cx| ConfigurationView::new(state, cx)).into()
+ cx.new(|cx| ConfigurationView::new(state, window, cx))
+ .into()
}
fn reset_credentials(&self, _cx: &mut App) -> Task<Result<()>> {
@@ -608,15 +612,38 @@ fn into_copilot_chat(
struct ConfigurationView {
copilot_status: Option<copilot::Status>,
+ api_url_editor: Entity<Editor>,
+ models_url_editor: Entity<Editor>,
+ auth_url_editor: Entity<Editor>,
state: Entity<State>,
_subscription: Option<Subscription>,
}
impl ConfigurationView {
- pub fn new(state: Entity<State>, cx: &mut Context<Self>) -> Self {
+ pub fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let copilot = Copilot::global(cx);
-
+ let settings = AllLanguageModelSettings::get_global(cx)
+ .copilot_chat
+ .clone();
+ let api_url_editor = cx.new(|cx| Editor::single_line(window, cx));
+ api_url_editor.update(cx, |this, cx| {
+ this.set_text(settings.api_url.clone(), window, cx);
+ this.set_placeholder_text("GitHub Copilot API URL", cx);
+ });
+ let models_url_editor = cx.new(|cx| Editor::single_line(window, cx));
+ models_url_editor.update(cx, |this, cx| {
+ this.set_text(settings.models_url.clone(), window, cx);
+ this.set_placeholder_text("GitHub Copilot Models URL", cx);
+ });
+ let auth_url_editor = cx.new(|cx| Editor::single_line(window, cx));
+ auth_url_editor.update(cx, |this, cx| {
+ this.set_text(settings.auth_url.clone(), window, cx);
+ this.set_placeholder_text("GitHub Copilot Auth URL", cx);
+ });
Self {
+ api_url_editor,
+ models_url_editor,
+ auth_url_editor,
copilot_status: copilot.as_ref().map(|copilot| copilot.read(cx).status()),
state,
_subscription: copilot.as_ref().map(|copilot| {
@@ -627,6 +654,104 @@ impl ConfigurationView {
}),
}
}
+ fn make_input_styles(&self, cx: &App) -> Div {
+ let bg_color = cx.theme().colors().editor_background;
+ let border_color = cx.theme().colors().border;
+
+ h_flex()
+ .w_full()
+ .px_2()
+ .py_1()
+ .bg(bg_color)
+ .border_1()
+ .border_color(border_color)
+ .rounded_sm()
+ }
+
+ fn make_text_style(&self, cx: &Context<Self>) -> TextStyle {
+ let settings = ThemeSettings::get_global(cx);
+ TextStyle {
+ color: cx.theme().colors().text,
+ font_family: settings.ui_font.family.clone(),
+ font_features: settings.ui_font.features.clone(),
+ font_fallbacks: settings.ui_font.fallbacks.clone(),
+ font_size: rems(0.875).into(),
+ font_weight: settings.ui_font.weight,
+ font_style: FontStyle::Normal,
+ line_height: relative(1.3),
+ background_color: None,
+ underline: None,
+ strikethrough: None,
+ white_space: WhiteSpace::Normal,
+ text_overflow: None,
+ text_align: Default::default(),
+ line_clamp: None,
+ }
+ }
+
+ fn render_api_url_editor(&self, cx: &mut Context<Self>) -> impl IntoElement {
+ let text_style = self.make_text_style(cx);
+
+ EditorElement::new(
+ &self.api_url_editor,
+ EditorStyle {
+ background: cx.theme().colors().editor_background,
+ local_player: cx.theme().players().local(),
+ text: text_style,
+ ..Default::default()
+ },
+ )
+ }
+
+ fn render_auth_url_editor(&self, cx: &mut Context<Self>) -> impl IntoElement {
+ let text_style = self.make_text_style(cx);
+
+ EditorElement::new(
+ &self.auth_url_editor,
+ EditorStyle {
+ background: cx.theme().colors().editor_background,
+ local_player: cx.theme().players().local(),
+ text: text_style,
+ ..Default::default()
+ },
+ )
+ }
+ fn render_models_editor(&self, cx: &mut Context<Self>) -> impl IntoElement {
+ let text_style = self.make_text_style(cx);
+
+ EditorElement::new(
+ &self.models_url_editor,
+ EditorStyle {
+ background: cx.theme().colors().editor_background,
+ local_player: cx.theme().players().local(),
+ text: text_style,
+ ..Default::default()
+ },
+ )
+ }
+
+ fn update_copilot_settings(&self, cx: &mut Context<'_, Self>) {
+ let settings = CopilotChatSettings {
+ api_url: self.api_url_editor.read(cx).text(cx).into(),
+ models_url: self.models_url_editor.read(cx).text(cx).into(),
+ auth_url: self.auth_url_editor.read(cx).text(cx).into(),
+ };
+ update_settings_file::<AllLanguageModelSettings>(<dyn Fs>::global(cx), cx, {
+ let settings = settings.clone();
+ move |content, _| {
+ content.copilot_chat = Some(CopilotChatSettingsContent {
+ api_url: Some(settings.api_url.as_ref().into()),
+ models_url: Some(settings.models_url.as_ref().into()),
+ auth_url: Some(settings.auth_url.as_ref().into()),
+ });
+ }
+ });
+ if let Some(chat) = CopilotChat::global(cx) {
+ chat.update(cx, |this, cx| {
+ this.set_settings(settings, cx);
+ });
+ }
+ }
}
impl Render for ConfigurationView {
@@ -684,15 +809,52 @@ impl Render for ConfigurationView {
}
_ => {
const LABEL: &str = "To use Zed's assistant with GitHub Copilot, you need to be logged in to GitHub. Note that your GitHub account must have an active Copilot Chat subscription.";
- v_flex().gap_2().child(Label::new(LABEL)).child(
- Button::new("sign_in", "Sign in to use GitHub Copilot")
- .icon_color(Color::Muted)
- .icon(IconName::Github)
- .icon_position(IconPosition::Start)
- .icon_size(IconSize::Medium)
- .full_width()
- .on_click(|_, window, cx| copilot::initiate_sign_in(window, cx)),
- )
+ v_flex()
+ .gap_2()
+ .child(Label::new(LABEL))
+ .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
+ this.update_copilot_settings(cx);
+ copilot::initiate_sign_in(window, cx);
+ }))
+ .child(
+ v_flex()
+ .gap_0p5()
+ .child(Label::new("API URL").size(LabelSize::Small))
+ .child(
+ self.make_input_styles(cx)
+ .child(self.render_api_url_editor(cx)),
+ ),
+ )
+ .child(
+ v_flex()
+ .gap_0p5()
+ .child(Label::new("Auth URL").size(LabelSize::Small))
+ .child(
+ self.make_input_styles(cx)
+ .child(self.render_auth_url_editor(cx)),
+ ),
+ )
+ .child(
+ v_flex()
+ .gap_0p5()
+ .child(Label::new("Models list URL").size(LabelSize::Small))
+ .child(
+ self.make_input_styles(cx)
+ .child(self.render_models_editor(cx)),
+ ),
+ )
+ .child(
+ Button::new("sign_in", "Sign in to use GitHub Copilot")
+ .icon_color(Color::Muted)
+ .icon(IconName::Github)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::Medium)
+ .full_width()
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.update_copilot_settings(cx);
+ copilot::initiate_sign_in(window, cx)
+ })),
+ )
}
},
None => v_flex().gap_6().child(Label::new(ERROR_LABEL)),