Add UI for configuring the API Url directly (#32248)
Closes #22901 Release Notes: - Copilot Chat endpoint URLs can now be configured via `settings.json` or Configuration View.
This commit is contained in:
parent
019a14bcde
commit
73cd6ef92c
3 changed files with 306 additions and 80 deletions
|
@ -8,6 +8,7 @@ use chrono::DateTime;
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
|
use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
|
||||||
|
use gpui::WeakEntity;
|
||||||
use gpui::{App, AsyncApp, Global, prelude::*};
|
use gpui::{App, AsyncApp, Global, prelude::*};
|
||||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
@ -15,9 +16,12 @@ use paths::home_dir;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::watch_config_dir;
|
use settings::watch_config_dir;
|
||||||
|
|
||||||
pub const COPILOT_CHAT_COMPLETION_URL: &str = "https://api.githubcopilot.com/chat/completions";
|
#[derive(Default, Clone, Debug, PartialEq)]
|
||||||
pub const COPILOT_CHAT_AUTH_URL: &str = "https://api.github.com/copilot_internal/v2/token";
|
pub struct CopilotChatSettings {
|
||||||
pub const COPILOT_CHAT_MODELS_URL: &str = "https://api.githubcopilot.com/models";
|
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
|
// 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
|
// 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 {
|
pub struct CopilotChat {
|
||||||
oauth_token: Option<String>,
|
oauth_token: Option<String>,
|
||||||
api_token: Option<ApiToken>,
|
api_token: Option<ApiToken>,
|
||||||
|
settings: CopilotChatSettings,
|
||||||
models: Option<Vec<Model>>,
|
models: Option<Vec<Model>>,
|
||||||
client: Arc<dyn HttpClient>,
|
client: Arc<dyn HttpClient>,
|
||||||
}
|
}
|
||||||
|
@ -373,53 +378,30 @@ impl CopilotChat {
|
||||||
.map(|model| model.0.clone())
|
.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 config_paths: HashSet<PathBuf> = copilot_chat_config_paths().into_iter().collect();
|
||||||
let dir_path = copilot_chat_config_dir();
|
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);
|
||||||
|
|
||||||
cx.spawn({
|
this.update(cx, |this, cx| {
|
||||||
let client = client.clone();
|
this.oauth_token = oauth_token.clone();
|
||||||
async move |cx| {
|
cx.notify();
|
||||||
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 {
|
if oauth_token.is_some() {
|
||||||
let api_token = request_api_token(oauth_token, client.clone()).await?;
|
Self::update_models(&this, cx).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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
anyhow::Ok(())
|
|
||||||
}
|
}
|
||||||
|
anyhow::Ok(())
|
||||||
})
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
|
|
||||||
|
@ -427,10 +409,42 @@ impl CopilotChat {
|
||||||
oauth_token: None,
|
oauth_token: None,
|
||||||
api_token: None,
|
api_token: None,
|
||||||
models: None,
|
models: None,
|
||||||
|
settings,
|
||||||
client,
|
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 {
|
pub fn is_authenticated(&self) -> bool {
|
||||||
self.oauth_token.is_some()
|
self.oauth_token.is_some()
|
||||||
}
|
}
|
||||||
|
@ -449,20 +463,23 @@ impl CopilotChat {
|
||||||
.flatten()
|
.flatten()
|
||||||
.context("Copilot chat is not enabled")?;
|
.context("Copilot chat is not enabled")?;
|
||||||
|
|
||||||
let (oauth_token, api_token, client) = this.read_with(&cx, |this, _| {
|
let (oauth_token, api_token, client, api_url, auth_url) =
|
||||||
(
|
this.read_with(&cx, |this, _| {
|
||||||
this.oauth_token.clone(),
|
(
|
||||||
this.api_token.clone(),
|
this.oauth_token.clone(),
|
||||||
this.client.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 oauth_token = oauth_token.context("No OAuth token available")?;
|
||||||
|
|
||||||
let token = match api_token {
|
let token = match api_token {
|
||||||
Some(api_token) if api_token.remaining_seconds() > 5 * 60 => api_token.clone(),
|
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.update(&mut cx, |this, cx| {
|
||||||
this.api_token = Some(token.clone());
|
this.api_token = Some(token.clone());
|
||||||
cx.notify();
|
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>> {
|
async fn get_models(
|
||||||
let all_models = request_models(api_token, client).await?;
|
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
|
let mut models: Vec<Model> = all_models
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -504,10 +537,14 @@ async fn get_models(api_token: String, client: Arc<dyn HttpClient>) -> Result<Ve
|
||||||
Ok(models)
|
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()
|
let request_builder = HttpRequest::builder()
|
||||||
.method(Method::GET)
|
.method(Method::GET)
|
||||||
.uri(COPILOT_CHAT_MODELS_URL)
|
.uri(models_url.as_ref())
|
||||||
.header("Authorization", format!("Bearer {}", api_token))
|
.header("Authorization", format!("Bearer {}", api_token))
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.header("Copilot-Integration-Id", "vscode-chat");
|
.header("Copilot-Integration-Id", "vscode-chat");
|
||||||
|
@ -531,10 +568,14 @@ async fn request_models(api_token: String, client: Arc<dyn HttpClient>) -> Resul
|
||||||
Ok(models)
|
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()
|
let request_builder = HttpRequest::builder()
|
||||||
.method(Method::GET)
|
.method(Method::GET)
|
||||||
.uri(COPILOT_CHAT_AUTH_URL)
|
.uri(auth_url.as_ref())
|
||||||
.header("Authorization", format!("token {}", oauth_token))
|
.header("Authorization", format!("token {}", oauth_token))
|
||||||
.header("Accept", "application/json");
|
.header("Accept", "application/json");
|
||||||
|
|
||||||
|
@ -579,6 +620,7 @@ fn extract_oauth_token(contents: String) -> Option<String> {
|
||||||
async fn stream_completion(
|
async fn stream_completion(
|
||||||
client: Arc<dyn HttpClient>,
|
client: Arc<dyn HttpClient>,
|
||||||
api_key: String,
|
api_key: String,
|
||||||
|
completion_url: Arc<str>,
|
||||||
request: Request,
|
request: Request,
|
||||||
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
|
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
|
||||||
let is_vision_request = request.messages.last().map_or(false, |message| match message {
|
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()
|
let request_builder = HttpRequest::builder()
|
||||||
.method(Method::POST)
|
.method(Method::POST)
|
||||||
.uri(COPILOT_CHAT_COMPLETION_URL)
|
.uri(completion_url.as_ref())
|
||||||
.header(
|
.header(
|
||||||
"Editor-Version",
|
"Editor-Version",
|
||||||
format!(
|
format!(
|
||||||
|
|
|
@ -10,12 +10,14 @@ use copilot::copilot_chat::{
|
||||||
ToolCall,
|
ToolCall,
|
||||||
};
|
};
|
||||||
use copilot::{Copilot, Status};
|
use copilot::{Copilot, Status};
|
||||||
|
use editor::{Editor, EditorElement, EditorStyle};
|
||||||
|
use fs::Fs;
|
||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
use futures::stream::BoxStream;
|
use futures::stream::BoxStream;
|
||||||
use futures::{FutureExt, Stream, StreamExt};
|
use futures::{FutureExt, Stream, StreamExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, Animation, AnimationExt, AnyView, App, AsyncApp, Entity, Render, Subscription, Task,
|
Action, Animation, AnimationExt, AnyView, App, AsyncApp, Entity, FontStyle, Render,
|
||||||
Transformation, percentage, svg,
|
Subscription, Task, TextStyle, Transformation, WhiteSpace, percentage, svg,
|
||||||
};
|
};
|
||||||
use language_model::{
|
use language_model::{
|
||||||
AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||||
|
@ -25,21 +27,22 @@ use language_model::{
|
||||||
LanguageModelToolSchemaFormat, LanguageModelToolUse, MessageContent, RateLimiter, Role,
|
LanguageModelToolSchemaFormat, LanguageModelToolUse, MessageContent, RateLimiter, Role,
|
||||||
StopReason,
|
StopReason,
|
||||||
};
|
};
|
||||||
use settings::SettingsStore;
|
use settings::{Settings, SettingsStore, update_settings_file};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use theme::ThemeSettings;
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
use util::debug_panic;
|
use util::debug_panic;
|
||||||
|
|
||||||
|
use crate::{AllLanguageModelSettings, CopilotChatSettingsContent};
|
||||||
|
|
||||||
use super::anthropic::count_anthropic_tokens;
|
use super::anthropic::count_anthropic_tokens;
|
||||||
use super::google::count_google_tokens;
|
use super::google::count_google_tokens;
|
||||||
use super::open_ai::count_open_ai_tokens;
|
use super::open_ai::count_open_ai_tokens;
|
||||||
|
pub(crate) use copilot::copilot_chat::CopilotChatSettings;
|
||||||
|
|
||||||
const PROVIDER_ID: &str = "copilot_chat";
|
const PROVIDER_ID: &str = "copilot_chat";
|
||||||
const PROVIDER_NAME: &str = "GitHub Copilot Chat";
|
const PROVIDER_NAME: &str = "GitHub Copilot Chat";
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug, PartialEq)]
|
|
||||||
pub struct CopilotChatSettings {}
|
|
||||||
|
|
||||||
pub struct CopilotChatLanguageModelProvider {
|
pub struct CopilotChatLanguageModelProvider {
|
||||||
state: Entity<State>,
|
state: Entity<State>,
|
||||||
}
|
}
|
||||||
|
@ -163,9 +166,10 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
|
||||||
Task::ready(Err(err.into()))
|
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();
|
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<()>> {
|
fn reset_credentials(&self, _cx: &mut App) -> Task<Result<()>> {
|
||||||
|
@ -608,15 +612,38 @@ fn into_copilot_chat(
|
||||||
|
|
||||||
struct ConfigurationView {
|
struct ConfigurationView {
|
||||||
copilot_status: Option<copilot::Status>,
|
copilot_status: Option<copilot::Status>,
|
||||||
|
api_url_editor: Entity<Editor>,
|
||||||
|
models_url_editor: Entity<Editor>,
|
||||||
|
auth_url_editor: Entity<Editor>,
|
||||||
state: Entity<State>,
|
state: Entity<State>,
|
||||||
_subscription: Option<Subscription>,
|
_subscription: Option<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigurationView {
|
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 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 {
|
Self {
|
||||||
|
api_url_editor,
|
||||||
|
models_url_editor,
|
||||||
|
auth_url_editor,
|
||||||
copilot_status: copilot.as_ref().map(|copilot| copilot.read(cx).status()),
|
copilot_status: copilot.as_ref().map(|copilot| copilot.read(cx).status()),
|
||||||
state,
|
state,
|
||||||
_subscription: copilot.as_ref().map(|copilot| {
|
_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 {
|
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.";
|
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(
|
v_flex()
|
||||||
Button::new("sign_in", "Sign in to use GitHub Copilot")
|
.gap_2()
|
||||||
.icon_color(Color::Muted)
|
.child(Label::new(LABEL))
|
||||||
.icon(IconName::Github)
|
.on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
|
||||||
.icon_position(IconPosition::Start)
|
this.update_copilot_settings(cx);
|
||||||
.icon_size(IconSize::Medium)
|
copilot::initiate_sign_in(window, cx);
|
||||||
.full_width()
|
}))
|
||||||
.on_click(|_, window, 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)),
|
None => v_flex().gap_6().child(Label::new(ERROR_LABEL)),
|
||||||
|
|
|
@ -272,7 +272,11 @@ pub struct ZedDotDevSettingsContent {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
|
#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
|
||||||
pub struct CopilotChatSettingsContent {}
|
pub struct CopilotChatSettingsContent {
|
||||||
|
pub api_url: Option<String>,
|
||||||
|
pub auth_url: Option<String>,
|
||||||
|
pub models_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
|
#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
|
||||||
pub struct OpenRouterSettingsContent {
|
pub struct OpenRouterSettingsContent {
|
||||||
|
@ -431,6 +435,24 @@ impl settings::Settings for AllLanguageModelSettings {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|s| s.available_models.clone()),
|
.and_then(|s| s.available_models.clone()),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Copilot Chat
|
||||||
|
let copilot_chat = value.copilot_chat.clone().unwrap_or_default();
|
||||||
|
|
||||||
|
settings.copilot_chat.api_url = copilot_chat.api_url.map_or_else(
|
||||||
|
|| Arc::from("https://api.githubcopilot.com/chat/completions"),
|
||||||
|
Arc::from,
|
||||||
|
);
|
||||||
|
|
||||||
|
settings.copilot_chat.auth_url = copilot_chat.auth_url.map_or_else(
|
||||||
|
|| Arc::from("https://api.github.com/copilot_internal/v2/token"),
|
||||||
|
Arc::from,
|
||||||
|
);
|
||||||
|
|
||||||
|
settings.copilot_chat.models_url = copilot_chat.models_url.map_or_else(
|
||||||
|
|| Arc::from("https://api.githubcopilot.com/models"),
|
||||||
|
Arc::from,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(settings)
|
Ok(settings)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue