1use std::pin::Pin;
2use std::str::FromStr as _;
3use std::sync::Arc;
4
5use anyhow::{Result, anyhow};
6use collections::HashMap;
7use copilot::copilot_chat::{
8 ChatMessage, ChatMessageContent, ChatMessagePart, CopilotChat, ImageUrl,
9 Model as CopilotChatModel, ModelVendor, Request as CopilotChatRequest, ResponseEvent, Tool,
10 ToolCall,
11};
12use copilot::{Copilot, Status};
13use editor::{Editor, EditorElement, EditorStyle};
14use fs::Fs;
15use futures::future::BoxFuture;
16use futures::stream::BoxStream;
17use futures::{FutureExt, Stream, StreamExt};
18use gpui::{
19 Action, Animation, AnimationExt, AnyView, App, AsyncApp, Entity, FontStyle, Render,
20 Subscription, Task, TextStyle, Transformation, WhiteSpace, percentage, svg,
21};
22use language_model::{
23 AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
24 LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
25 LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
26 LanguageModelRequestMessage, LanguageModelToolChoice, LanguageModelToolResultContent,
27 LanguageModelToolSchemaFormat, LanguageModelToolUse, MessageContent, RateLimiter, Role,
28 StopReason,
29};
30use settings::{Settings, SettingsStore, update_settings_file};
31use std::time::Duration;
32use theme::ThemeSettings;
33use ui::prelude::*;
34use util::debug_panic;
35
36use crate::{AllLanguageModelSettings, CopilotChatSettingsContent};
37
38use super::anthropic::count_anthropic_tokens;
39use super::google::count_google_tokens;
40use super::open_ai::count_open_ai_tokens;
41pub(crate) use copilot::copilot_chat::CopilotChatSettings;
42
43const PROVIDER_ID: &str = "copilot_chat";
44const PROVIDER_NAME: &str = "GitHub Copilot Chat";
45
46pub struct CopilotChatLanguageModelProvider {
47 state: Entity<State>,
48}
49
50pub struct State {
51 _copilot_chat_subscription: Option<Subscription>,
52 _settings_subscription: Subscription,
53}
54
55impl State {
56 fn is_authenticated(&self, cx: &App) -> bool {
57 CopilotChat::global(cx)
58 .map(|m| m.read(cx).is_authenticated())
59 .unwrap_or(false)
60 }
61}
62
63impl CopilotChatLanguageModelProvider {
64 pub fn new(cx: &mut App) -> Self {
65 let state = cx.new(|cx| {
66 let copilot_chat_subscription = CopilotChat::global(cx)
67 .map(|copilot_chat| cx.observe(&copilot_chat, |_, _, cx| cx.notify()));
68 State {
69 _copilot_chat_subscription: copilot_chat_subscription,
70 _settings_subscription: cx.observe_global::<SettingsStore>(|_, cx| {
71 cx.notify();
72 }),
73 }
74 });
75
76 Self { state }
77 }
78
79 fn create_language_model(&self, model: CopilotChatModel) -> Arc<dyn LanguageModel> {
80 Arc::new(CopilotChatLanguageModel {
81 model,
82 request_limiter: RateLimiter::new(4),
83 })
84 }
85}
86
87impl LanguageModelProviderState for CopilotChatLanguageModelProvider {
88 type ObservableEntity = State;
89
90 fn observable_entity(&self) -> Option<gpui::Entity<Self::ObservableEntity>> {
91 Some(self.state.clone())
92 }
93}
94
95impl LanguageModelProvider for CopilotChatLanguageModelProvider {
96 fn id(&self) -> LanguageModelProviderId {
97 LanguageModelProviderId(PROVIDER_ID.into())
98 }
99
100 fn name(&self) -> LanguageModelProviderName {
101 LanguageModelProviderName(PROVIDER_NAME.into())
102 }
103
104 fn icon(&self) -> IconName {
105 IconName::Copilot
106 }
107
108 fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
109 let models = CopilotChat::global(cx).and_then(|m| m.read(cx).models())?;
110 models
111 .first()
112 .map(|model| self.create_language_model(model.clone()))
113 }
114
115 fn default_fast_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
116 // The default model should be Copilot Chat's 'base model', which is likely a relatively fast
117 // model (e.g. 4o) and a sensible choice when considering premium requests
118 self.default_model(cx)
119 }
120
121 fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
122 let Some(models) = CopilotChat::global(cx).and_then(|m| m.read(cx).models()) else {
123 return Vec::new();
124 };
125 models
126 .iter()
127 .map(|model| self.create_language_model(model.clone()))
128 .collect()
129 }
130
131 fn is_authenticated(&self, cx: &App) -> bool {
132 self.state.read(cx).is_authenticated(cx)
133 }
134
135 fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {
136 if self.is_authenticated(cx) {
137 return Task::ready(Ok(()));
138 };
139
140 let Some(copilot) = Copilot::global(cx) else {
141 return Task::ready( Err(anyhow!(
142 "Copilot must be enabled for Copilot Chat to work. Please enable Copilot and try again."
143 ).into()));
144 };
145
146 let err = match copilot.read(cx).status() {
147 Status::Authorized => return Task::ready(Ok(())),
148 Status::Disabled => anyhow!(
149 "Copilot must be enabled for Copilot Chat to work. Please enable Copilot and try again."
150 ),
151 Status::Error(err) => anyhow!(format!(
152 "Received the following error while signing into Copilot: {err}"
153 )),
154 Status::Starting { task: _ } => anyhow!(
155 "Copilot is still starting, please wait for Copilot to start then try again"
156 ),
157 Status::Unauthorized => anyhow!(
158 "Unable to authorize with Copilot. Please make sure that you have an active Copilot and Copilot Chat subscription."
159 ),
160 Status::SignedOut { .. } => {
161 anyhow!("You have signed out of Copilot. Please sign in to Copilot and try again.")
162 }
163 Status::SigningIn { prompt: _ } => anyhow!("Still signing into Copilot..."),
164 };
165
166 Task::ready(Err(err.into()))
167 }
168
169 fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView {
170 let state = self.state.clone();
171 cx.new(|cx| ConfigurationView::new(state, window, cx))
172 .into()
173 }
174
175 fn reset_credentials(&self, _cx: &mut App) -> Task<Result<()>> {
176 Task::ready(Err(anyhow!(
177 "Signing out of GitHub Copilot Chat is currently not supported."
178 )))
179 }
180}
181
182pub struct CopilotChatLanguageModel {
183 model: CopilotChatModel,
184 request_limiter: RateLimiter,
185}
186
187impl LanguageModel for CopilotChatLanguageModel {
188 fn id(&self) -> LanguageModelId {
189 LanguageModelId::from(self.model.id().to_string())
190 }
191
192 fn name(&self) -> LanguageModelName {
193 LanguageModelName::from(self.model.display_name().to_string())
194 }
195
196 fn provider_id(&self) -> LanguageModelProviderId {
197 LanguageModelProviderId(PROVIDER_ID.into())
198 }
199
200 fn provider_name(&self) -> LanguageModelProviderName {
201 LanguageModelProviderName(PROVIDER_NAME.into())
202 }
203
204 fn supports_tools(&self) -> bool {
205 self.model.supports_tools()
206 }
207
208 fn supports_images(&self) -> bool {
209 self.model.supports_vision()
210 }
211
212 fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
213 match self.model.vendor() {
214 ModelVendor::OpenAI | ModelVendor::Anthropic => {
215 LanguageModelToolSchemaFormat::JsonSchema
216 }
217 ModelVendor::Google => LanguageModelToolSchemaFormat::JsonSchemaSubset,
218 }
219 }
220
221 fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
222 match choice {
223 LanguageModelToolChoice::Auto
224 | LanguageModelToolChoice::Any
225 | LanguageModelToolChoice::None => self.supports_tools(),
226 }
227 }
228
229 fn telemetry_id(&self) -> String {
230 format!("copilot_chat/{}", self.model.id())
231 }
232
233 fn max_token_count(&self) -> usize {
234 self.model.max_token_count()
235 }
236
237 fn count_tokens(
238 &self,
239 request: LanguageModelRequest,
240 cx: &App,
241 ) -> BoxFuture<'static, Result<usize>> {
242 match self.model.vendor() {
243 ModelVendor::Anthropic => count_anthropic_tokens(request, cx),
244 ModelVendor::Google => count_google_tokens(request, cx),
245 ModelVendor::OpenAI => {
246 let model = open_ai::Model::from_id(self.model.id()).unwrap_or_default();
247 count_open_ai_tokens(request, model, cx)
248 }
249 }
250 }
251
252 fn stream_completion(
253 &self,
254 request: LanguageModelRequest,
255 cx: &AsyncApp,
256 ) -> BoxFuture<
257 'static,
258 Result<
259 BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
260 >,
261 > {
262 if let Some(message) = request.messages.last() {
263 if message.contents_empty() {
264 const EMPTY_PROMPT_MSG: &str =
265 "Empty prompts aren't allowed. Please provide a non-empty prompt.";
266 return futures::future::ready(Err(anyhow::anyhow!(EMPTY_PROMPT_MSG))).boxed();
267 }
268
269 // Copilot Chat has a restriction that the final message must be from the user.
270 // While their API does return an error message for this, we can catch it earlier
271 // and provide a more helpful error message.
272 if !matches!(message.role, Role::User) {
273 const USER_ROLE_MSG: &str = "The final message must be from the user. To provide a system prompt, you must provide the system prompt followed by a user prompt.";
274 return futures::future::ready(Err(anyhow::anyhow!(USER_ROLE_MSG))).boxed();
275 }
276 }
277
278 let copilot_request = match into_copilot_chat(&self.model, request) {
279 Ok(request) => request,
280 Err(err) => return futures::future::ready(Err(err)).boxed(),
281 };
282 let is_streaming = copilot_request.stream;
283
284 let request_limiter = self.request_limiter.clone();
285 let future = cx.spawn(async move |cx| {
286 let request = CopilotChat::stream_completion(copilot_request, cx.clone());
287 request_limiter
288 .stream(async move {
289 let response = request.await?;
290 Ok(map_to_language_model_completion_events(
291 response,
292 is_streaming,
293 ))
294 })
295 .await
296 });
297 async move { Ok(future.await?.boxed()) }.boxed()
298 }
299}
300
301pub fn map_to_language_model_completion_events(
302 events: Pin<Box<dyn Send + Stream<Item = Result<ResponseEvent>>>>,
303 is_streaming: bool,
304) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
305 #[derive(Default)]
306 struct RawToolCall {
307 id: String,
308 name: String,
309 arguments: String,
310 }
311
312 struct State {
313 events: Pin<Box<dyn Send + Stream<Item = Result<ResponseEvent>>>>,
314 tool_calls_by_index: HashMap<usize, RawToolCall>,
315 }
316
317 futures::stream::unfold(
318 State {
319 events,
320 tool_calls_by_index: HashMap::default(),
321 },
322 move |mut state| async move {
323 if let Some(event) = state.events.next().await {
324 match event {
325 Ok(event) => {
326 let Some(choice) = event.choices.first() else {
327 return Some((
328 vec![Err(anyhow!("Response contained no choices").into())],
329 state,
330 ));
331 };
332
333 let delta = if is_streaming {
334 choice.delta.as_ref()
335 } else {
336 choice.message.as_ref()
337 };
338
339 let Some(delta) = delta else {
340 return Some((
341 vec![Err(anyhow!("Response contained no delta").into())],
342 state,
343 ));
344 };
345
346 let mut events = Vec::new();
347 if let Some(content) = delta.content.clone() {
348 events.push(Ok(LanguageModelCompletionEvent::Text(content)));
349 }
350
351 for tool_call in &delta.tool_calls {
352 let entry = state
353 .tool_calls_by_index
354 .entry(tool_call.index)
355 .or_default();
356
357 if let Some(tool_id) = tool_call.id.clone() {
358 entry.id = tool_id;
359 }
360
361 if let Some(function) = tool_call.function.as_ref() {
362 if let Some(name) = function.name.clone() {
363 entry.name = name;
364 }
365
366 if let Some(arguments) = function.arguments.clone() {
367 entry.arguments.push_str(&arguments);
368 }
369 }
370 }
371
372 match choice.finish_reason.as_deref() {
373 Some("stop") => {
374 events.push(Ok(LanguageModelCompletionEvent::Stop(
375 StopReason::EndTurn,
376 )));
377 }
378 Some("tool_calls") => {
379 events.extend(state.tool_calls_by_index.drain().map(
380 |(_, tool_call)| {
381 // The model can output an empty string
382 // to indicate the absence of arguments.
383 // When that happens, create an empty
384 // object instead.
385 let arguments = if tool_call.arguments.is_empty() {
386 Ok(serde_json::Value::Object(Default::default()))
387 } else {
388 serde_json::Value::from_str(&tool_call.arguments)
389 };
390 match arguments {
391 Ok(input) => Ok(LanguageModelCompletionEvent::ToolUse(
392 LanguageModelToolUse {
393 id: tool_call.id.clone().into(),
394 name: tool_call.name.as_str().into(),
395 is_input_complete: true,
396 input,
397 raw_input: tool_call.arguments.clone(),
398 },
399 )),
400 Err(error) => {
401 Err(LanguageModelCompletionError::BadInputJson {
402 id: tool_call.id.into(),
403 tool_name: tool_call.name.as_str().into(),
404 raw_input: tool_call.arguments.into(),
405 json_parse_error: error.to_string(),
406 })
407 }
408 }
409 },
410 ));
411
412 events.push(Ok(LanguageModelCompletionEvent::Stop(
413 StopReason::ToolUse,
414 )));
415 }
416 Some(stop_reason) => {
417 log::error!("Unexpected Copilot Chat stop_reason: {stop_reason:?}");
418 events.push(Ok(LanguageModelCompletionEvent::Stop(
419 StopReason::EndTurn,
420 )));
421 }
422 None => {}
423 }
424
425 return Some((events, state));
426 }
427 Err(err) => return Some((vec![Err(anyhow!(err).into())], state)),
428 }
429 }
430
431 None
432 },
433 )
434 .flat_map(futures::stream::iter)
435}
436
437fn into_copilot_chat(
438 model: &copilot::copilot_chat::Model,
439 request: LanguageModelRequest,
440) -> Result<CopilotChatRequest> {
441 let mut request_messages: Vec<LanguageModelRequestMessage> = Vec::new();
442 for message in request.messages {
443 if let Some(last_message) = request_messages.last_mut() {
444 if last_message.role == message.role {
445 last_message.content.extend(message.content);
446 } else {
447 request_messages.push(message);
448 }
449 } else {
450 request_messages.push(message);
451 }
452 }
453
454 let mut tool_called = false;
455 let mut messages: Vec<ChatMessage> = Vec::new();
456 for message in request_messages {
457 match message.role {
458 Role::User => {
459 for content in &message.content {
460 if let MessageContent::ToolResult(tool_result) = content {
461 let content = match &tool_result.content {
462 LanguageModelToolResultContent::Text(text) => text.to_string().into(),
463 LanguageModelToolResultContent::Image(image) => {
464 if model.supports_vision() {
465 ChatMessageContent::Multipart(vec![ChatMessagePart::Image {
466 image_url: ImageUrl {
467 url: image.to_base64_url(),
468 },
469 }])
470 } else {
471 debug_panic!(
472 "This should be caught at {} level",
473 tool_result.tool_name
474 );
475 "[Tool responded with an image, but this model does not support vision]".to_string().into()
476 }
477 }
478 };
479
480 messages.push(ChatMessage::Tool {
481 tool_call_id: tool_result.tool_use_id.to_string(),
482 content,
483 });
484 }
485 }
486
487 let mut content_parts = Vec::new();
488 for content in &message.content {
489 match content {
490 MessageContent::Text(text) | MessageContent::Thinking { text, .. }
491 if !text.is_empty() =>
492 {
493 if let Some(ChatMessagePart::Text { text: text_content }) =
494 content_parts.last_mut()
495 {
496 text_content.push_str(text);
497 } else {
498 content_parts.push(ChatMessagePart::Text {
499 text: text.to_string(),
500 });
501 }
502 }
503 MessageContent::Image(image) if model.supports_vision() => {
504 content_parts.push(ChatMessagePart::Image {
505 image_url: ImageUrl {
506 url: image.to_base64_url(),
507 },
508 });
509 }
510 _ => {}
511 }
512 }
513
514 if !content_parts.is_empty() {
515 messages.push(ChatMessage::User {
516 content: content_parts.into(),
517 });
518 }
519 }
520 Role::Assistant => {
521 let mut tool_calls = Vec::new();
522 for content in &message.content {
523 if let MessageContent::ToolUse(tool_use) = content {
524 tool_called = true;
525 tool_calls.push(ToolCall {
526 id: tool_use.id.to_string(),
527 content: copilot::copilot_chat::ToolCallContent::Function {
528 function: copilot::copilot_chat::FunctionContent {
529 name: tool_use.name.to_string(),
530 arguments: serde_json::to_string(&tool_use.input)?,
531 },
532 },
533 });
534 }
535 }
536
537 let text_content = {
538 let mut buffer = String::new();
539 for string in message.content.iter().filter_map(|content| match content {
540 MessageContent::Text(text) | MessageContent::Thinking { text, .. } => {
541 Some(text.as_str())
542 }
543 MessageContent::ToolUse(_)
544 | MessageContent::RedactedThinking(_)
545 | MessageContent::ToolResult(_)
546 | MessageContent::Image(_) => None,
547 }) {
548 buffer.push_str(string);
549 }
550
551 buffer
552 };
553
554 messages.push(ChatMessage::Assistant {
555 content: if text_content.is_empty() {
556 ChatMessageContent::empty()
557 } else {
558 text_content.into()
559 },
560 tool_calls,
561 });
562 }
563 Role::System => messages.push(ChatMessage::System {
564 content: message.string_contents(),
565 }),
566 }
567 }
568
569 let mut tools = request
570 .tools
571 .iter()
572 .map(|tool| Tool::Function {
573 function: copilot::copilot_chat::Function {
574 name: tool.name.clone(),
575 description: tool.description.clone(),
576 parameters: tool.input_schema.clone(),
577 },
578 })
579 .collect::<Vec<_>>();
580
581 // The API will return a Bad Request (with no error message) when tools
582 // were used previously in the conversation but no tools are provided as
583 // part of this request. Inserting a dummy tool seems to circumvent this
584 // error.
585 if tool_called && tools.is_empty() {
586 tools.push(Tool::Function {
587 function: copilot::copilot_chat::Function {
588 name: "noop".to_string(),
589 description: "No operation".to_string(),
590 parameters: serde_json::json!({
591 "type": "object"
592 }),
593 },
594 });
595 }
596
597 Ok(CopilotChatRequest {
598 intent: true,
599 n: 1,
600 stream: model.uses_streaming(),
601 temperature: 0.1,
602 model: model.id().to_string(),
603 messages,
604 tools,
605 tool_choice: request.tool_choice.map(|choice| match choice {
606 LanguageModelToolChoice::Auto => copilot::copilot_chat::ToolChoice::Auto,
607 LanguageModelToolChoice::Any => copilot::copilot_chat::ToolChoice::Any,
608 LanguageModelToolChoice::None => copilot::copilot_chat::ToolChoice::None,
609 }),
610 })
611}
612
613struct ConfigurationView {
614 copilot_status: Option<copilot::Status>,
615 api_url_editor: Entity<Editor>,
616 models_url_editor: Entity<Editor>,
617 auth_url_editor: Entity<Editor>,
618 state: Entity<State>,
619 _subscription: Option<Subscription>,
620}
621
622impl ConfigurationView {
623 pub fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
624 let copilot = Copilot::global(cx);
625 let settings = AllLanguageModelSettings::get_global(cx)
626 .copilot_chat
627 .clone();
628 let api_url_editor = cx.new(|cx| Editor::single_line(window, cx));
629 api_url_editor.update(cx, |this, cx| {
630 this.set_text(settings.api_url.clone(), window, cx);
631 this.set_placeholder_text("GitHub Copilot API URL", cx);
632 });
633 let models_url_editor = cx.new(|cx| Editor::single_line(window, cx));
634 models_url_editor.update(cx, |this, cx| {
635 this.set_text(settings.models_url.clone(), window, cx);
636 this.set_placeholder_text("GitHub Copilot Models URL", cx);
637 });
638 let auth_url_editor = cx.new(|cx| Editor::single_line(window, cx));
639 auth_url_editor.update(cx, |this, cx| {
640 this.set_text(settings.auth_url.clone(), window, cx);
641 this.set_placeholder_text("GitHub Copilot Auth URL", cx);
642 });
643 Self {
644 api_url_editor,
645 models_url_editor,
646 auth_url_editor,
647 copilot_status: copilot.as_ref().map(|copilot| copilot.read(cx).status()),
648 state,
649 _subscription: copilot.as_ref().map(|copilot| {
650 cx.observe(copilot, |this, model, cx| {
651 this.copilot_status = Some(model.read(cx).status());
652 cx.notify();
653 })
654 }),
655 }
656 }
657 fn make_input_styles(&self, cx: &App) -> Div {
658 let bg_color = cx.theme().colors().editor_background;
659 let border_color = cx.theme().colors().border;
660
661 h_flex()
662 .w_full()
663 .px_2()
664 .py_1()
665 .bg(bg_color)
666 .border_1()
667 .border_color(border_color)
668 .rounded_sm()
669 }
670
671 fn make_text_style(&self, cx: &Context<Self>) -> TextStyle {
672 let settings = ThemeSettings::get_global(cx);
673 TextStyle {
674 color: cx.theme().colors().text,
675 font_family: settings.ui_font.family.clone(),
676 font_features: settings.ui_font.features.clone(),
677 font_fallbacks: settings.ui_font.fallbacks.clone(),
678 font_size: rems(0.875).into(),
679 font_weight: settings.ui_font.weight,
680 font_style: FontStyle::Normal,
681 line_height: relative(1.3),
682 background_color: None,
683 underline: None,
684 strikethrough: None,
685 white_space: WhiteSpace::Normal,
686 text_overflow: None,
687 text_align: Default::default(),
688 line_clamp: None,
689 }
690 }
691
692 fn render_api_url_editor(&self, cx: &mut Context<Self>) -> impl IntoElement {
693 let text_style = self.make_text_style(cx);
694
695 EditorElement::new(
696 &self.api_url_editor,
697 EditorStyle {
698 background: cx.theme().colors().editor_background,
699 local_player: cx.theme().players().local(),
700 text: text_style,
701 ..Default::default()
702 },
703 )
704 }
705
706 fn render_auth_url_editor(&self, cx: &mut Context<Self>) -> impl IntoElement {
707 let text_style = self.make_text_style(cx);
708
709 EditorElement::new(
710 &self.auth_url_editor,
711 EditorStyle {
712 background: cx.theme().colors().editor_background,
713 local_player: cx.theme().players().local(),
714 text: text_style,
715 ..Default::default()
716 },
717 )
718 }
719 fn render_models_editor(&self, cx: &mut Context<Self>) -> impl IntoElement {
720 let text_style = self.make_text_style(cx);
721
722 EditorElement::new(
723 &self.models_url_editor,
724 EditorStyle {
725 background: cx.theme().colors().editor_background,
726 local_player: cx.theme().players().local(),
727 text: text_style,
728 ..Default::default()
729 },
730 )
731 }
732
733 fn update_copilot_settings(&self, cx: &mut Context<'_, Self>) {
734 let settings = CopilotChatSettings {
735 api_url: self.api_url_editor.read(cx).text(cx).into(),
736 models_url: self.models_url_editor.read(cx).text(cx).into(),
737 auth_url: self.auth_url_editor.read(cx).text(cx).into(),
738 };
739 update_settings_file::<AllLanguageModelSettings>(<dyn Fs>::global(cx), cx, {
740 let settings = settings.clone();
741 move |content, _| {
742 content.copilot_chat = Some(CopilotChatSettingsContent {
743 api_url: Some(settings.api_url.as_ref().into()),
744 models_url: Some(settings.models_url.as_ref().into()),
745 auth_url: Some(settings.auth_url.as_ref().into()),
746 });
747 }
748 });
749 if let Some(chat) = CopilotChat::global(cx) {
750 chat.update(cx, |this, cx| {
751 this.set_settings(settings, cx);
752 });
753 }
754 }
755}
756
757impl Render for ConfigurationView {
758 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
759 if self.state.read(cx).is_authenticated(cx) {
760 h_flex()
761 .mt_1()
762 .p_1()
763 .justify_between()
764 .rounded_md()
765 .border_1()
766 .border_color(cx.theme().colors().border)
767 .bg(cx.theme().colors().background)
768 .child(
769 h_flex()
770 .gap_1()
771 .child(Icon::new(IconName::Check).color(Color::Success))
772 .child(Label::new("Authorized")),
773 )
774 .child(
775 Button::new("sign_out", "Sign Out")
776 .label_size(LabelSize::Small)
777 .on_click(|_, window, cx| {
778 window.dispatch_action(copilot::SignOut.boxed_clone(), cx);
779 }),
780 )
781 } else {
782 let loading_icon = Icon::new(IconName::ArrowCircle).with_animation(
783 "arrow-circle",
784 Animation::new(Duration::from_secs(4)).repeat(),
785 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
786 );
787
788 const ERROR_LABEL: &str = "Copilot Chat requires an active GitHub Copilot subscription. Please ensure Copilot is configured and try again, or use a different Assistant provider.";
789
790 match &self.copilot_status {
791 Some(status) => match status {
792 Status::Starting { task: _ } => h_flex()
793 .gap_2()
794 .child(loading_icon)
795 .child(Label::new("Starting Copilot…")),
796 Status::SigningIn { prompt: _ }
797 | Status::SignedOut {
798 awaiting_signing_in: true,
799 } => h_flex()
800 .gap_2()
801 .child(loading_icon)
802 .child(Label::new("Signing into Copilot…")),
803 Status::Error(_) => {
804 const LABEL: &str = "Copilot had issues starting. Please try restarting it. If the issue persists, try reinstalling Copilot.";
805 v_flex()
806 .gap_6()
807 .child(Label::new(LABEL))
808 .child(svg().size_8().path(IconName::CopilotError.path()))
809 }
810 _ => {
811 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.";
812 v_flex()
813 .gap_2()
814 .child(Label::new(LABEL))
815 .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
816 this.update_copilot_settings(cx);
817 copilot::initiate_sign_in(window, cx);
818 }))
819 .child(
820 v_flex()
821 .gap_0p5()
822 .child(Label::new("API URL").size(LabelSize::Small))
823 .child(
824 self.make_input_styles(cx)
825 .child(self.render_api_url_editor(cx)),
826 ),
827 )
828 .child(
829 v_flex()
830 .gap_0p5()
831 .child(Label::new("Auth URL").size(LabelSize::Small))
832 .child(
833 self.make_input_styles(cx)
834 .child(self.render_auth_url_editor(cx)),
835 ),
836 )
837 .child(
838 v_flex()
839 .gap_0p5()
840 .child(Label::new("Models list URL").size(LabelSize::Small))
841 .child(
842 self.make_input_styles(cx)
843 .child(self.render_models_editor(cx)),
844 ),
845 )
846 .child(
847 Button::new("sign_in", "Sign in to use GitHub Copilot")
848 .icon_color(Color::Muted)
849 .icon(IconName::Github)
850 .icon_position(IconPosition::Start)
851 .icon_size(IconSize::Medium)
852 .full_width()
853 .on_click(cx.listener(|this, _, window, cx| {
854 this.update_copilot_settings(cx);
855 copilot::initiate_sign_in(window, cx)
856 })),
857 )
858 }
859 },
860 None => v_flex().gap_6().child(Label::new(ERROR_LABEL)),
861 }
862 }
863 }
864}