Skip to repository content733 lines · 27.8 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:57:35.739Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
sign_in.rs
1use anyhow::Context as _;
2use copilot::{
3 Copilot, GlobalCopilotAuth, Status,
4 request::{self, PromptUserDeviceFlow},
5};
6use gpui::{
7 App, ClipboardItem, Context, DismissEvent, Element, Entity, EventEmitter, FocusHandle,
8 Focusable, InteractiveElement, IntoElement, MouseDownEvent, ParentElement, Render, Styled,
9 Subscription, TaskExt, Window, WindowBounds, WindowOptions, div, point,
10};
11use project::project_settings::ProjectSettings;
12use release_channel::ReleaseChannel;
13use settings::Settings as _;
14use ui::{ButtonLike, CommonAnimationExt, ConfiguredApiCard, prelude::*};
15use util::ResultExt as _;
16use workspace::{AppState, Toast, Workspace, notifications::NotificationId};
17
18const COPILOT_SIGN_UP_URL: &str = "https://github.com/features/copilot";
19const ERROR_LABEL: &str =
20 "Copilot had issues starting. You can try reinstalling it and signing in again.";
21
22struct CopilotStatusToast;
23
24pub fn initiate_sign_in(copilot: Entity<Copilot>, window: &mut Window, cx: &mut App) {
25 let is_reinstall = false;
26 initiate_sign_in_impl(copilot, is_reinstall, window, cx)
27}
28
29pub fn initiate_sign_out(copilot: Entity<Copilot>, window: &mut Window, cx: &mut App) {
30 copilot_toast(Some("Signing out of Copilot…"), window, cx);
31
32 let sign_out_task = copilot.update(cx, |copilot, cx| copilot.sign_out(cx));
33 window
34 .spawn(cx, async move |cx| match sign_out_task.await {
35 Ok(()) => {
36 cx.update(|window, cx| copilot_toast(Some("Signed out of Copilot"), window, cx))
37 }
38 Err(err) => cx.update(|window, cx| {
39 if let Some(workspace) = Workspace::for_window(window, cx) {
40 workspace.update(cx, |workspace, cx| {
41 workspace.show_error(format!("Error: {err}"), cx);
42 })
43 } else {
44 log::error!("{:?}", err);
45 }
46 }),
47 })
48 .detach();
49}
50
51pub fn reinstall_and_sign_in(copilot: Entity<Copilot>, window: &mut Window, cx: &mut App) {
52 let _ = copilot.update(cx, |copilot, cx| copilot.reinstall(cx));
53 let is_reinstall = true;
54 initiate_sign_in_impl(copilot, is_reinstall, window, cx);
55}
56
57fn open_copilot_code_verification_window(copilot: &Entity<Copilot>, window: &Window, cx: &mut App) {
58 let current_window_center = window.bounds().center();
59 let width = px(450.);
60 let height = px(350.);
61 let window_bounds = WindowBounds::Windowed(gpui::bounds(
62 current_window_center - point(width / 2.0, height / 2.0),
63 gpui::size(width, height),
64 ));
65 let app_id = ReleaseChannel::global(cx).app_id();
66 cx.open_window(
67 WindowOptions {
68 kind: gpui::WindowKind::Floating,
69 window_bounds: Some(window_bounds),
70 is_resizable: false,
71 is_movable: true,
72 titlebar: Some(gpui::TitlebarOptions {
73 title: Some("Use GitHub Copilot in Omega".into()),
74 appears_transparent: true,
75 ..Default::default()
76 }),
77 app_id: Some(app_id.to_owned()),
78 ..Default::default()
79 },
80 |window, cx| cx.new(|cx| CopilotCodeVerification::new(&copilot, window, cx)),
81 )
82 .context("Failed to open Copilot code verification window")
83 .log_err();
84}
85
86fn copilot_toast(message: Option<&'static str>, window: &Window, cx: &mut App) {
87 const NOTIFICATION_ID: NotificationId = NotificationId::unique::<CopilotStatusToast>();
88
89 let Some(workspace) = Workspace::for_window(window, cx) else {
90 return;
91 };
92
93 cx.defer(move |cx| {
94 workspace.update(cx, |workspace, cx| match message {
95 Some(message) => workspace.show_toast(Toast::new(NOTIFICATION_ID, message), cx),
96 None => workspace.dismiss_toast(&NOTIFICATION_ID, cx),
97 });
98 })
99}
100
101pub fn initiate_sign_in_impl(
102 copilot: Entity<Copilot>,
103 is_reinstall: bool,
104 window: &mut Window,
105 cx: &mut App,
106) {
107 if matches!(copilot.read(cx).status(), Status::Disabled) {
108 copilot.update(cx, |copilot, cx| copilot.start_copilot(false, true, cx));
109 }
110 match copilot.read(cx).status() {
111 Status::Starting { task } => {
112 copilot_toast(
113 Some(if is_reinstall {
114 "Copilot is reinstalling…"
115 } else {
116 "Copilot is starting…"
117 }),
118 window,
119 cx,
120 );
121
122 window
123 .spawn(cx, async move |cx| {
124 task.await;
125 cx.update(|window, cx| match copilot.read(cx).status() {
126 Status::Authorized => {
127 copilot_toast(Some("Copilot has started."), window, cx)
128 }
129 _ => {
130 copilot_toast(None, window, cx);
131 copilot
132 .update(cx, |copilot, cx| copilot.sign_in(cx))
133 .detach_and_log_err(cx);
134 open_copilot_code_verification_window(&copilot, window, cx);
135 }
136 })
137 .log_err();
138 })
139 .detach();
140 }
141 _ => {
142 copilot
143 .update(cx, |copilot, cx| copilot.sign_in(cx))
144 .detach();
145 open_copilot_code_verification_window(&copilot, window, cx);
146 }
147 }
148}
149
150pub struct CopilotCodeVerification {
151 status: Status,
152 connect_clicked: bool,
153 focus_handle: FocusHandle,
154 copilot: Entity<Copilot>,
155 _subscription: Subscription,
156 sign_up_url: Option<String>,
157}
158
159impl Focusable for CopilotCodeVerification {
160 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
161 self.focus_handle.clone()
162 }
163}
164
165impl EventEmitter<DismissEvent> for CopilotCodeVerification {}
166
167impl CopilotCodeVerification {
168 pub fn new(copilot: &Entity<Copilot>, window: &mut Window, cx: &mut Context<Self>) -> Self {
169 window.on_window_should_close(cx, |window, cx| {
170 if let Some(this) = window.root::<CopilotCodeVerification>().flatten() {
171 this.update(cx, |this, cx| {
172 this.before_dismiss(cx);
173 });
174 }
175 true
176 });
177 cx.subscribe_in(
178 &cx.entity(),
179 window,
180 |this, _, _: &DismissEvent, window, cx| {
181 window.remove_window();
182 this.before_dismiss(cx);
183 },
184 )
185 .detach();
186
187 let status = copilot.read(cx).status();
188 Self {
189 status,
190 connect_clicked: false,
191 focus_handle: cx.focus_handle(),
192 copilot: copilot.clone(),
193 sign_up_url: None,
194 _subscription: cx.observe(copilot, |this, copilot, cx| {
195 let status = copilot.read(cx).status();
196 match status {
197 Status::Authorized | Status::Unauthorized | Status::SigningIn { .. } => {
198 this.set_status(status, cx)
199 }
200 _ => cx.emit(DismissEvent),
201 }
202 }),
203 }
204 }
205
206 pub fn set_status(&mut self, status: Status, cx: &mut Context<Self>) {
207 self.status = status;
208 cx.notify();
209 }
210
211 fn render_device_code(data: &PromptUserDeviceFlow, cx: &mut Context<Self>) -> impl IntoElement {
212 let copied = cx
213 .read_from_clipboard()
214 .map(|item| item.text().as_ref() == Some(&data.user_code))
215 .unwrap_or(false);
216
217 ButtonLike::new("copy-button")
218 .full_width()
219 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
220 .size(ButtonSize::Medium)
221 .child(
222 h_flex()
223 .w_full()
224 .p_1()
225 .justify_between()
226 .child(Label::new(data.user_code.clone()))
227 .child(Label::new(if copied { "Copied!" } else { "Copy" })),
228 )
229 .on_click({
230 let user_code = data.user_code.clone();
231 move |_, window, cx| {
232 cx.write_to_clipboard(ClipboardItem::new_string(user_code.clone()));
233 window.refresh();
234 }
235 })
236 }
237
238 fn render_prompting_modal(
239 copilot: Entity<Copilot>,
240 connect_clicked: bool,
241 data: &PromptUserDeviceFlow,
242 cx: &mut Context<Self>,
243 ) -> impl Element {
244 let connect_button_label = if connect_clicked {
245 "Waiting for connection…"
246 } else {
247 "Connect to GitHub"
248 };
249
250 v_flex()
251 .flex_1()
252 .gap_2p5()
253 .items_center()
254 .text_center()
255 .child(Headline::new("Use GitHub Copilot in Omega").size(HeadlineSize::Large))
256 .child(
257 Label::new("Using Copilot requires an active subscription on GitHub.")
258 .color(Color::Muted),
259 )
260 .child(Self::render_device_code(data, cx))
261 .child(
262 Label::new("Paste this code into GitHub after clicking the button below.")
263 .color(Color::Muted),
264 )
265 .child(
266 v_flex()
267 .w_full()
268 .gap_1()
269 .child(
270 Button::new("connect-button", connect_button_label)
271 .full_width()
272 .style(ButtonStyle::Outlined)
273 .size(ButtonSize::Medium)
274 .on_click({
275 let command = data.command.clone();
276 cx.listener(move |this, _, _window, cx| {
277 let command = command.clone();
278 let copilot_clone = copilot.clone();
279 let request_timeout = ProjectSettings::get_global(cx)
280 .global_lsp_settings
281 .get_request_timeout();
282 copilot.update(cx, |copilot, cx| {
283 if let Some(server) = copilot.language_server() {
284 let server = server.clone();
285 cx.spawn(async move |_, cx| {
286 let result = server
287 .request::<lsp::request::ExecuteCommand>(
288 lsp::ExecuteCommandParams {
289 command: command.command.clone(),
290 arguments: command
291 .arguments
292 .clone()
293 .unwrap_or_default(),
294 ..Default::default()
295 },
296 request_timeout,
297 )
298 .await
299 .into_response()
300 .ok()
301 .flatten();
302 if let Some(value) = result {
303 if let Ok(status) = serde_json::from_value::<
304 request::SignInStatus,
305 >(
306 value
307 ) {
308 copilot_clone.update(cx, |copilot, cx| {
309 copilot
310 .update_sign_in_status(status, cx);
311 });
312 }
313 }
314 })
315 .detach();
316 }
317 });
318
319 this.connect_clicked = true;
320 })
321 }),
322 )
323 .child(
324 Button::new("copilot-enable-cancel-button", "Cancel")
325 .full_width()
326 .size(ButtonSize::Medium)
327 .on_click(cx.listener(|_, _, _, cx| {
328 cx.emit(DismissEvent);
329 })),
330 ),
331 )
332 }
333
334 fn render_enabled_modal(cx: &mut Context<Self>) -> impl Element {
335 v_flex()
336 .gap_2()
337 .text_center()
338 .justify_center()
339 .child(Headline::new("Copilot Enabled!").size(HeadlineSize::Large))
340 .child(Label::new("You're all set to use GitHub Copilot.").color(Color::Muted))
341 .child(
342 Button::new("copilot-enabled-done-button", "Done")
343 .full_width()
344 .style(ButtonStyle::Outlined)
345 .size(ButtonSize::Medium)
346 .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
347 )
348 }
349
350 fn render_unauthorized_modal(&self, cx: &mut Context<Self>) -> impl Element {
351 let sign_up_url = self
352 .sign_up_url
353 .as_deref()
354 .unwrap_or(COPILOT_SIGN_UP_URL)
355 .to_owned();
356 let description = "Enable Copilot by connecting your existing license once you have subscribed or renewed your subscription.";
357
358 v_flex()
359 .gap_2()
360 .text_center()
361 .justify_center()
362 .child(
363 Headline::new("You must have an active GitHub Copilot subscription.")
364 .size(HeadlineSize::Large),
365 )
366 .child(Label::new(description).color(Color::Warning))
367 .child(
368 Button::new("copilot-subscribe-button", "Subscribe on GitHub")
369 .full_width()
370 .style(ButtonStyle::Outlined)
371 .size(ButtonSize::Medium)
372 .on_click(move |_, _, cx| cx.open_url(&sign_up_url)),
373 )
374 .child(
375 Button::new("copilot-subscribe-cancel-button", "Cancel")
376 .full_width()
377 .size(ButtonSize::Medium)
378 .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
379 )
380 }
381
382 fn render_error_modal(copilot: Entity<Copilot>, _cx: &mut Context<Self>) -> impl Element {
383 v_flex()
384 .gap_2()
385 .text_center()
386 .justify_center()
387 .child(Headline::new("An Error Happened").size(HeadlineSize::Large))
388 .child(Label::new(ERROR_LABEL).color(Color::Muted))
389 .child(
390 Button::new("copilot-subscribe-button", "Reinstall Copilot and Sign In")
391 .full_width()
392 .style(ButtonStyle::Outlined)
393 .size(ButtonSize::Medium)
394 .start_icon(
395 Icon::new(IconName::Download)
396 .size(IconSize::Small)
397 .color(Color::Muted),
398 )
399 .on_click(move |_, window, cx| {
400 reinstall_and_sign_in(copilot.clone(), window, cx)
401 }),
402 )
403 }
404
405 fn before_dismiss(
406 &mut self,
407 cx: &mut Context<'_, CopilotCodeVerification>,
408 ) -> workspace::DismissDecision {
409 self.copilot.update(cx, |copilot, cx| {
410 if matches!(copilot.status(), Status::SigningIn { .. }) {
411 copilot.sign_out(cx).detach_and_log_err(cx);
412 }
413 });
414 workspace::DismissDecision::Dismiss(true)
415 }
416}
417
418impl Render for CopilotCodeVerification {
419 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
420 let prompt = match &self.status {
421 Status::SigningIn { prompt: None } => Icon::new(IconName::ArrowCircle)
422 .color(Color::Muted)
423 .with_rotate_animation(2)
424 .into_any_element(),
425 Status::SigningIn {
426 prompt: Some(prompt),
427 } => {
428 Self::render_prompting_modal(self.copilot.clone(), self.connect_clicked, prompt, cx)
429 .into_any_element()
430 }
431 Status::Unauthorized => {
432 self.connect_clicked = false;
433 self.render_unauthorized_modal(cx).into_any_element()
434 }
435 Status::Authorized => {
436 self.connect_clicked = false;
437 Self::render_enabled_modal(cx).into_any_element()
438 }
439 Status::Error(..) => {
440 Self::render_error_modal(self.copilot.clone(), cx).into_any_element()
441 }
442 _ => div().into_any_element(),
443 };
444
445 v_flex()
446 .id("copilot_code_verification")
447 .track_focus(&self.focus_handle(cx))
448 .size_full()
449 .px_4()
450 .py_8()
451 .gap_2()
452 .items_center()
453 .justify_center()
454 .elevation_3(cx)
455 .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
456 cx.emit(DismissEvent);
457 }))
458 .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
459 window.focus(&this.focus_handle, cx);
460 }))
461 .child(
462 // OMEGA-DELTA-0022: this was the Zed x Copilot lockup, which
463 // drew a competitor's mark at the top of a modal Omega opens.
464 // The Copilot integration is retained; the co-branding is not.
465 Icon::new(IconName::Copilot)
466 .size(IconSize::Custom(rems(4.)))
467 .color(Color::Custom(cx.theme().colors().icon)),
468 )
469 .child(prompt)
470 }
471}
472
473pub struct ConfigurationView {
474 copilot_status: Option<Status>,
475 is_authenticated: Box<dyn Fn(&mut App) -> bool + 'static>,
476 edit_prediction: bool,
477 /// When `true`, renders a compact control suitable for an inline settings
478 /// row: no explanatory labels (those live in the row's left column) and
479 /// content-sized buttons instead of full-width ones.
480 compact: bool,
481 _subscription: Option<Subscription>,
482}
483
484pub enum ConfigurationMode {
485 Chat,
486 EditPrediction,
487}
488
489impl ConfigurationView {
490 pub fn new(
491 is_authenticated: impl Fn(&mut App) -> bool + 'static,
492 mode: ConfigurationMode,
493 cx: &mut Context<Self>,
494 ) -> Self {
495 let copilot = AppState::try_global(cx)
496 .and_then(|state| GlobalCopilotAuth::try_get_or_init(state, cx));
497
498 Self {
499 copilot_status: copilot.as_ref().map(|copilot| copilot.0.read(cx).status()),
500 is_authenticated: Box::new(is_authenticated),
501 edit_prediction: matches!(mode, ConfigurationMode::EditPrediction),
502 compact: false,
503 _subscription: copilot.as_ref().map(|copilot| {
504 cx.observe(&copilot.0, |this, model, cx| {
505 this.copilot_status = Some(model.read(cx).status());
506 cx.notify();
507 })
508 }),
509 }
510 }
511
512 /// Renders the view compactly for an inline settings row (no labels,
513 /// content-sized buttons). The explanatory copy is expected to be shown
514 /// elsewhere (e.g. the row's left column).
515 pub fn compact(mut self) -> Self {
516 self.compact = true;
517 self
518 }
519}
520
521impl ConfigurationView {
522 fn is_starting(&self) -> bool {
523 matches!(&self.copilot_status, Some(Status::Starting { .. }))
524 }
525
526 fn is_signing_in(&self) -> bool {
527 matches!(
528 &self.copilot_status,
529 Some(Status::SigningIn { .. })
530 | Some(Status::SignedOut {
531 awaiting_signing_in: true
532 })
533 )
534 }
535
536 fn is_error(&self) -> bool {
537 matches!(&self.copilot_status, Some(Status::Error(_)))
538 }
539
540 fn has_no_status(&self) -> bool {
541 self.copilot_status.is_none()
542 }
543
544 fn loading_message(&self) -> Option<SharedString> {
545 if self.is_starting() {
546 Some("Starting Copilot…".into())
547 } else if self.is_signing_in() {
548 Some("Signing into Copilot…".into())
549 } else {
550 None
551 }
552 }
553
554 fn render_loading_button(
555 &self,
556 label: impl Into<SharedString>,
557 edit_prediction: bool,
558 ) -> impl IntoElement {
559 Button::new("loading_button", label)
560 .map(|this| {
561 if edit_prediction || self.compact {
562 this.size(ButtonSize::Medium)
563 } else {
564 this.full_width()
565 }
566 })
567 .disabled(true)
568 .loading(true)
569 .style(ButtonStyle::Outlined)
570 }
571
572 fn render_sign_in_button(&self, edit_prediction: bool) -> impl IntoElement {
573 let label = if edit_prediction {
574 "Sign in to GitHub"
575 } else {
576 "Sign In"
577 };
578
579 Button::new("sign_in", label)
580 .map(|this| {
581 if edit_prediction || self.compact {
582 this.size(ButtonSize::Medium)
583 } else {
584 this.full_width()
585 }
586 })
587 .style(ButtonStyle::Outlined)
588 .when(edit_prediction, |this| this.tab_index(0isize))
589 .on_click(|_, window, cx| {
590 let app_state = AppState::global(cx);
591 if let Some(copilot) = GlobalCopilotAuth::try_get_or_init(app_state, cx) {
592 initiate_sign_in(copilot.0, window, cx)
593 }
594 })
595 }
596
597 fn render_reinstall_button(&self, edit_prediction: bool) -> impl IntoElement {
598 let label = if edit_prediction {
599 "Reinstall and Sign in"
600 } else {
601 "Reinstall Copilot and Sign in"
602 };
603
604 Button::new("reinstall_and_sign_in", label)
605 .map(|this| {
606 if edit_prediction || self.compact {
607 this.size(ButtonSize::Medium)
608 } else {
609 this.full_width()
610 }
611 })
612 .style(ButtonStyle::Outlined)
613 .start_icon(
614 Icon::new(IconName::Download)
615 .size(IconSize::Small)
616 .color(Color::Muted),
617 )
618 .on_click(|_, window, cx| {
619 let app_state = AppState::global(cx);
620 if let Some(copilot) = GlobalCopilotAuth::try_get_or_init(app_state, cx) {
621 reinstall_and_sign_in(copilot.0, window, cx);
622 }
623 })
624 }
625
626 fn render_for_edit_prediction(&self) -> impl IntoElement {
627 let container = |description: SharedString, action: AnyElement| {
628 h_flex()
629 .pt_2p5()
630 .w_full()
631 .justify_between()
632 .child(
633 v_flex()
634 .w_full()
635 .max_w_1_2()
636 .child(Label::new("Authenticate To Use"))
637 .child(
638 Label::new(description)
639 .color(Color::Muted)
640 .size(LabelSize::Small),
641 ),
642 )
643 .child(action)
644 };
645
646 let start_label = "To use Copilot for edit predictions, you need to be logged in to GitHub. Note that your GitHub account must have an active Copilot subscription.".into();
647 let no_status_label = "Copilot requires an active GitHub Copilot subscription. Please ensure Copilot is configured and try again, or use a different edit predictions provider.".into();
648
649 if let Some(msg) = self.loading_message() {
650 container(
651 start_label,
652 self.render_loading_button(msg, true).into_any_element(),
653 )
654 .into_any_element()
655 } else if self.is_error() {
656 container(
657 ERROR_LABEL.into(),
658 self.render_reinstall_button(true).into_any_element(),
659 )
660 .into_any_element()
661 } else if self.has_no_status() {
662 container(
663 no_status_label,
664 self.render_sign_in_button(true).into_any_element(),
665 )
666 .into_any_element()
667 } else {
668 container(
669 start_label,
670 self.render_sign_in_button(true).into_any_element(),
671 )
672 .into_any_element()
673 }
674 }
675
676 fn render_for_chat(&self) -> impl IntoElement {
677 let start_label = "To use Omega Agent with GitHub Copilot, you need to be logged in to GitHub. Note that your GitHub account must have an active Copilot Chat subscription.";
678 let no_status_label = "Copilot Chat requires an active GitHub Copilot subscription. Please ensure Copilot is configured and try again, or use a different LLM provider.";
679
680 let (label, button) = if let Some(msg) = self.loading_message() {
681 (
682 start_label,
683 self.render_loading_button(msg, false).into_any_element(),
684 )
685 } else if self.is_error() {
686 (
687 ERROR_LABEL,
688 self.render_reinstall_button(false).into_any_element(),
689 )
690 } else if self.has_no_status() {
691 (
692 no_status_label,
693 self.render_sign_in_button(false).into_any_element(),
694 )
695 } else {
696 (
697 start_label,
698 self.render_sign_in_button(false).into_any_element(),
699 )
700 };
701
702 v_flex()
703 .gap_2()
704 .when(!self.compact, |this| this.child(Label::new(label)))
705 .child(button)
706 }
707}
708
709impl Render for ConfigurationView {
710 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
711 let is_authenticated = &self.is_authenticated;
712
713 if is_authenticated(cx) {
714 let sign_out = |_: &gpui::ClickEvent, window: &mut Window, cx: &mut App| {
715 if let Some(auth) = GlobalCopilotAuth::try_global(cx) {
716 initiate_sign_out(auth.0.clone(), window, cx);
717 }
718 };
719
720 return ConfiguredApiCard::new("copilot-authorized", "Authorized")
721 .button_label("Sign Out")
722 .on_click(sign_out)
723 .into_any_element();
724 }
725
726 if self.edit_prediction {
727 self.render_for_edit_prediction().into_any_element()
728 } else {
729 self.render_for_chat().into_any_element()
730 }
731 }
732}
733