copilot_chat.rs

  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}