1use crate::{request::PromptUserDeviceFlow, Copilot, Status};
2use gpui::{
3 div, AppContext, ClipboardItem, DismissEvent, Element, EventEmitter, FocusHandle,
4 FocusableView, InteractiveElement, IntoElement, Model, MouseDownEvent, ParentElement, Render,
5 Styled, Subscription, ViewContext,
6};
7use ui::{prelude::*, Button, Label, Vector, VectorName};
8use util::ResultExt as _;
9use workspace::notifications::NotificationId;
10use workspace::{ModalView, Toast, Workspace};
11
12const COPILOT_SIGN_UP_URL: &str = "https://github.com/features/copilot";
13
14struct CopilotStartingToast;
15
16pub fn initiate_sign_in(cx: &mut WindowContext) {
17 let Some(copilot) = Copilot::global(cx) else {
18 return;
19 };
20 let status = copilot.read(cx).status();
21 let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
22 return;
23 };
24 match status {
25 Status::Starting { task } => {
26 let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
27 return;
28 };
29
30 let Ok(workspace) = workspace.update(cx, |workspace, cx| {
31 workspace.show_toast(
32 Toast::new(
33 NotificationId::unique::<CopilotStartingToast>(),
34 "Copilot is starting...",
35 ),
36 cx,
37 );
38 workspace.weak_handle()
39 }) else {
40 return;
41 };
42
43 cx.spawn(|mut cx| async move {
44 task.await;
45 if let Some(copilot) = cx.update(|cx| Copilot::global(cx)).ok().flatten() {
46 workspace
47 .update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
48 Status::Authorized => workspace.show_toast(
49 Toast::new(
50 NotificationId::unique::<CopilotStartingToast>(),
51 "Copilot has started!",
52 ),
53 cx,
54 ),
55 _ => {
56 workspace.dismiss_toast(
57 &NotificationId::unique::<CopilotStartingToast>(),
58 cx,
59 );
60 copilot
61 .update(cx, |copilot, cx| copilot.sign_in(cx))
62 .detach_and_log_err(cx);
63 }
64 })
65 .log_err();
66 }
67 })
68 .detach();
69 }
70 _ => {
71 copilot.update(cx, |this, cx| this.sign_in(cx)).detach();
72 workspace
73 .update(cx, |this, cx| {
74 this.toggle_modal(cx, |cx| CopilotCodeVerification::new(&copilot, cx));
75 })
76 .ok();
77 }
78 }
79}
80
81pub struct CopilotCodeVerification {
82 status: Status,
83 connect_clicked: bool,
84 focus_handle: FocusHandle,
85 _subscription: Subscription,
86}
87
88impl FocusableView for CopilotCodeVerification {
89 fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
90 self.focus_handle.clone()
91 }
92}
93
94impl EventEmitter<DismissEvent> for CopilotCodeVerification {}
95impl ModalView for CopilotCodeVerification {}
96
97impl CopilotCodeVerification {
98 pub fn new(copilot: &Model<Copilot>, cx: &mut ViewContext<Self>) -> Self {
99 let status = copilot.read(cx).status();
100 Self {
101 status,
102 connect_clicked: false,
103 focus_handle: cx.focus_handle(),
104 _subscription: cx.observe(copilot, |this, copilot, cx| {
105 let status = copilot.read(cx).status();
106 match status {
107 Status::Authorized | Status::Unauthorized | Status::SigningIn { .. } => {
108 this.set_status(status, cx)
109 }
110 _ => cx.emit(DismissEvent),
111 }
112 }),
113 }
114 }
115
116 pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
117 self.status = status;
118 cx.notify();
119 }
120
121 fn render_device_code(
122 data: &PromptUserDeviceFlow,
123 cx: &mut ViewContext<Self>,
124 ) -> impl IntoElement {
125 let copied = cx
126 .read_from_clipboard()
127 .map(|item| item.text().as_ref() == Some(&data.user_code))
128 .unwrap_or(false);
129 h_flex()
130 .w_full()
131 .p_1()
132 .border_1()
133 .border_muted(cx)
134 .rounded_md()
135 .cursor_pointer()
136 .justify_between()
137 .on_mouse_down(gpui::MouseButton::Left, {
138 let user_code = data.user_code.clone();
139 move |_, cx| {
140 cx.write_to_clipboard(ClipboardItem::new_string(user_code.clone()));
141 cx.refresh();
142 }
143 })
144 .child(div().flex_1().child(Label::new(data.user_code.clone())))
145 .child(div().flex_none().px_1().child(Label::new(if copied {
146 "Copied!"
147 } else {
148 "Copy"
149 })))
150 }
151
152 fn render_prompting_modal(
153 connect_clicked: bool,
154 data: &PromptUserDeviceFlow,
155 cx: &mut ViewContext<Self>,
156 ) -> impl Element {
157 let connect_button_label = if connect_clicked {
158 "Waiting for connection..."
159 } else {
160 "Connect to GitHub"
161 };
162 v_flex()
163 .flex_1()
164 .gap_2()
165 .items_center()
166 .child(Headline::new("Use GitHub Copilot in Zed.").size(HeadlineSize::Large))
167 .child(
168 Label::new("Using Copilot requires an active subscription on GitHub.")
169 .color(Color::Muted),
170 )
171 .child(Self::render_device_code(data, cx))
172 .child(
173 Label::new("Paste this code into GitHub after clicking the button below.")
174 .size(ui::LabelSize::Small),
175 )
176 .child(
177 Button::new("connect-button", connect_button_label)
178 .on_click({
179 let verification_uri = data.verification_uri.clone();
180 cx.listener(move |this, _, cx| {
181 cx.open_url(&verification_uri);
182 this.connect_clicked = true;
183 })
184 })
185 .full_width()
186 .style(ButtonStyle::Filled),
187 )
188 .child(
189 Button::new("copilot-enable-cancel-button", "Cancel")
190 .full_width()
191 .on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))),
192 )
193 }
194 fn render_enabled_modal(cx: &mut ViewContext<Self>) -> impl Element {
195 v_flex()
196 .gap_2()
197 .child(Headline::new("Copilot Enabled!").size(HeadlineSize::Large))
198 .child(Label::new(
199 "You can update your settings or sign out from the Copilot menu in the status bar.",
200 ))
201 .child(
202 Button::new("copilot-enabled-done-button", "Done")
203 .full_width()
204 .on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))),
205 )
206 }
207
208 fn render_unauthorized_modal(cx: &mut ViewContext<Self>) -> impl Element {
209 v_flex()
210 .child(Headline::new("You must have an active GitHub Copilot subscription.").size(HeadlineSize::Large))
211
212 .child(Label::new(
213 "You can enable Copilot by connecting your existing license once you have subscribed or renewed your subscription.",
214 ).color(Color::Warning))
215 .child(
216 Button::new("copilot-subscribe-button", "Subscribe on GitHub")
217 .full_width()
218 .on_click(|_, cx| cx.open_url(COPILOT_SIGN_UP_URL)),
219 )
220 .child(
221 Button::new("copilot-subscribe-cancel-button", "Cancel")
222 .full_width()
223 .on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))),
224 )
225 }
226
227 fn render_disabled_modal() -> impl Element {
228 v_flex()
229 .child(Headline::new("Copilot is disabled").size(HeadlineSize::Large))
230 .child(Label::new("You can enable Copilot in your settings."))
231 }
232}
233
234impl Render for CopilotCodeVerification {
235 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
236 let prompt = match &self.status {
237 Status::SigningIn {
238 prompt: Some(prompt),
239 } => Self::render_prompting_modal(self.connect_clicked, prompt, cx).into_any_element(),
240 Status::Unauthorized => {
241 self.connect_clicked = false;
242 Self::render_unauthorized_modal(cx).into_any_element()
243 }
244 Status::Authorized => {
245 self.connect_clicked = false;
246 Self::render_enabled_modal(cx).into_any_element()
247 }
248 Status::Disabled => {
249 self.connect_clicked = false;
250 Self::render_disabled_modal().into_any_element()
251 }
252 _ => div().into_any_element(),
253 };
254
255 v_flex()
256 .id("copilot code verification")
257 .track_focus(&self.focus_handle(cx))
258 .elevation_3(cx)
259 .w_96()
260 .items_center()
261 .p_4()
262 .gap_2()
263 .on_action(cx.listener(|_, _: &menu::Cancel, cx| {
264 cx.emit(DismissEvent);
265 }))
266 .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, cx| {
267 cx.focus(&this.focus_handle);
268 }))
269 .child(
270 Vector::new(VectorName::ZedXCopilot, rems(8.), rems(4.))
271 .color(Color::Custom(cx.theme().colors().icon)),
272 )
273 .child(prompt)
274 }
275}