logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree with a 3-file patch applied (no upstream history). Patch (crates/gpui_linux/src/linux/wayland/): - serial.rs: add SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; add latest_serial_of() - window.rs: activate() uses keyboard-enter serial (Mutter focus gate) Mutter honors window activation only when the token carries the keyboard- focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial. See docs/tray-window-focus-wayland.md in logiguard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
35
crates/copilot_ui/Cargo.toml
Normal file
35
crates/copilot_ui/Cargo.toml
Normal file
@@ -0,0 +1,35 @@
|
||||
[package]
|
||||
name = "copilot_ui"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/copilot_ui.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
default = []
|
||||
test-support = [
|
||||
"copilot/test-support",
|
||||
"gpui/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
copilot.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
menu.workspace = true
|
||||
project.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
1
crates/copilot_ui/LICENSE-GPL
Symbolic link
1
crates/copilot_ui/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
35
crates/copilot_ui/src/copilot_ui.rs
Normal file
35
crates/copilot_ui/src/copilot_ui.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
mod sign_in;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use copilot::GlobalCopilotAuth;
|
||||
use gpui::AppContext;
|
||||
use language::language_settings::AllLanguageSettings;
|
||||
use project::DisableAiSettings;
|
||||
use settings::SettingsStore;
|
||||
pub use sign_in::{
|
||||
ConfigurationMode, ConfigurationView, CopilotCodeVerification, initiate_sign_in,
|
||||
initiate_sign_out, reinstall_and_sign_in,
|
||||
};
|
||||
use ui::App;
|
||||
use workspace::AppState;
|
||||
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut App) {
|
||||
let disable_ai = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<DisableAiSettings>(None).disable_ai
|
||||
});
|
||||
let provider = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllLanguageSettings>(None)
|
||||
.edit_predictions
|
||||
.provider
|
||||
});
|
||||
if !disable_ai && provider == settings::EditPredictionProvider::Copilot {
|
||||
GlobalCopilotAuth::set_global(
|
||||
app_state.languages.next_language_server_id(),
|
||||
app_state.fs.clone(),
|
||||
app_state.node_runtime.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
719
crates/copilot_ui/src/sign_in.rs
Normal file
719
crates/copilot_ui/src/sign_in.rs
Normal file
@@ -0,0 +1,719 @@
|
||||
use anyhow::Context as _;
|
||||
use copilot::{
|
||||
Copilot, GlobalCopilotAuth, Status,
|
||||
request::{self, PromptUserDeviceFlow},
|
||||
};
|
||||
use gpui::{
|
||||
App, ClipboardItem, Context, DismissEvent, Element, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, InteractiveElement, IntoElement, MouseDownEvent, ParentElement, Render, Styled,
|
||||
Subscription, TaskExt, Window, WindowBounds, WindowOptions, div, point,
|
||||
};
|
||||
use project::project_settings::ProjectSettings;
|
||||
use settings::Settings as _;
|
||||
use ui::{ButtonLike, CommonAnimationExt, ConfiguredApiCard, Vector, VectorName, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{AppState, Toast, Workspace, notifications::NotificationId};
|
||||
|
||||
const COPILOT_SIGN_UP_URL: &str = "https://github.com/features/copilot";
|
||||
const ERROR_LABEL: &str =
|
||||
"Copilot had issues starting. You can try reinstalling it and signing in again.";
|
||||
|
||||
struct CopilotStatusToast;
|
||||
|
||||
pub fn initiate_sign_in(copilot: Entity<Copilot>, window: &mut Window, cx: &mut App) {
|
||||
let is_reinstall = false;
|
||||
initiate_sign_in_impl(copilot, is_reinstall, window, cx)
|
||||
}
|
||||
|
||||
pub fn initiate_sign_out(copilot: Entity<Copilot>, window: &mut Window, cx: &mut App) {
|
||||
copilot_toast(Some("Signing out of Copilot…"), window, cx);
|
||||
|
||||
let sign_out_task = copilot.update(cx, |copilot, cx| copilot.sign_out(cx));
|
||||
window
|
||||
.spawn(cx, async move |cx| match sign_out_task.await {
|
||||
Ok(()) => {
|
||||
cx.update(|window, cx| copilot_toast(Some("Signed out of Copilot"), window, cx))
|
||||
}
|
||||
Err(err) => cx.update(|window, cx| {
|
||||
if let Some(workspace) = Workspace::for_window(window, cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.show_error(&err, cx);
|
||||
})
|
||||
} else {
|
||||
log::error!("{:?}", err);
|
||||
}
|
||||
}),
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn reinstall_and_sign_in(copilot: Entity<Copilot>, window: &mut Window, cx: &mut App) {
|
||||
let _ = copilot.update(cx, |copilot, cx| copilot.reinstall(cx));
|
||||
let is_reinstall = true;
|
||||
initiate_sign_in_impl(copilot, is_reinstall, window, cx);
|
||||
}
|
||||
|
||||
fn open_copilot_code_verification_window(copilot: &Entity<Copilot>, window: &Window, cx: &mut App) {
|
||||
let current_window_center = window.bounds().center();
|
||||
let height = px(450.);
|
||||
let width = px(350.);
|
||||
let window_bounds = WindowBounds::Windowed(gpui::bounds(
|
||||
current_window_center - point(height / 2.0, width / 2.0),
|
||||
gpui::size(height, width),
|
||||
));
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
kind: gpui::WindowKind::PopUp,
|
||||
window_bounds: Some(window_bounds),
|
||||
is_resizable: false,
|
||||
is_movable: true,
|
||||
titlebar: Some(gpui::TitlebarOptions {
|
||||
appears_transparent: true,
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| cx.new(|cx| CopilotCodeVerification::new(&copilot, window, cx)),
|
||||
)
|
||||
.context("Failed to open Copilot code verification window")
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn copilot_toast(message: Option<&'static str>, window: &Window, cx: &mut App) {
|
||||
const NOTIFICATION_ID: NotificationId = NotificationId::unique::<CopilotStatusToast>();
|
||||
|
||||
let Some(workspace) = Workspace::for_window(window, cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
cx.defer(move |cx| {
|
||||
workspace.update(cx, |workspace, cx| match message {
|
||||
Some(message) => workspace.show_toast(Toast::new(NOTIFICATION_ID, message), cx),
|
||||
None => workspace.dismiss_toast(&NOTIFICATION_ID, cx),
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn initiate_sign_in_impl(
|
||||
copilot: Entity<Copilot>,
|
||||
is_reinstall: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
if matches!(copilot.read(cx).status(), Status::Disabled) {
|
||||
copilot.update(cx, |copilot, cx| copilot.start_copilot(false, true, cx));
|
||||
}
|
||||
match copilot.read(cx).status() {
|
||||
Status::Starting { task } => {
|
||||
copilot_toast(
|
||||
Some(if is_reinstall {
|
||||
"Copilot is reinstalling…"
|
||||
} else {
|
||||
"Copilot is starting…"
|
||||
}),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
task.await;
|
||||
cx.update(|window, cx| match copilot.read(cx).status() {
|
||||
Status::Authorized => {
|
||||
copilot_toast(Some("Copilot has started."), window, cx)
|
||||
}
|
||||
_ => {
|
||||
copilot_toast(None, window, cx);
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.detach_and_log_err(cx);
|
||||
open_copilot_code_verification_window(&copilot, window, cx);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
_ => {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.detach();
|
||||
open_copilot_code_verification_window(&copilot, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CopilotCodeVerification {
|
||||
status: Status,
|
||||
connect_clicked: bool,
|
||||
focus_handle: FocusHandle,
|
||||
copilot: Entity<Copilot>,
|
||||
_subscription: Subscription,
|
||||
sign_up_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Focusable for CopilotCodeVerification {
|
||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for CopilotCodeVerification {}
|
||||
|
||||
impl CopilotCodeVerification {
|
||||
pub fn new(copilot: &Entity<Copilot>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
window.on_window_should_close(cx, |window, cx| {
|
||||
if let Some(this) = window.root::<CopilotCodeVerification>().flatten() {
|
||||
this.update(cx, |this, cx| {
|
||||
this.before_dismiss(cx);
|
||||
});
|
||||
}
|
||||
true
|
||||
});
|
||||
cx.subscribe_in(
|
||||
&cx.entity(),
|
||||
window,
|
||||
|this, _, _: &DismissEvent, window, cx| {
|
||||
window.remove_window();
|
||||
this.before_dismiss(cx);
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
let status = copilot.read(cx).status();
|
||||
Self {
|
||||
status,
|
||||
connect_clicked: false,
|
||||
focus_handle: cx.focus_handle(),
|
||||
copilot: copilot.clone(),
|
||||
sign_up_url: None,
|
||||
_subscription: cx.observe(copilot, |this, copilot, cx| {
|
||||
let status = copilot.read(cx).status();
|
||||
match status {
|
||||
Status::Authorized | Status::Unauthorized | Status::SigningIn { .. } => {
|
||||
this.set_status(status, cx)
|
||||
}
|
||||
_ => cx.emit(DismissEvent),
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_status(&mut self, status: Status, cx: &mut Context<Self>) {
|
||||
self.status = status;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_device_code(data: &PromptUserDeviceFlow, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let copied = cx
|
||||
.read_from_clipboard()
|
||||
.map(|item| item.text().as_ref() == Some(&data.user_code))
|
||||
.unwrap_or(false);
|
||||
|
||||
ButtonLike::new("copy-button")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.size(ButtonSize::Medium)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.p_1()
|
||||
.justify_between()
|
||||
.child(Label::new(data.user_code.clone()))
|
||||
.child(Label::new(if copied { "Copied!" } else { "Copy" })),
|
||||
)
|
||||
.on_click({
|
||||
let user_code = data.user_code.clone();
|
||||
move |_, window, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(user_code.clone()));
|
||||
window.refresh();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn render_prompting_modal(
|
||||
copilot: Entity<Copilot>,
|
||||
connect_clicked: bool,
|
||||
data: &PromptUserDeviceFlow,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl Element {
|
||||
let connect_button_label = if connect_clicked {
|
||||
"Waiting for connection…"
|
||||
} else {
|
||||
"Connect to GitHub"
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.gap_2p5()
|
||||
.items_center()
|
||||
.text_center()
|
||||
.child(Headline::new("Use GitHub Copilot in Zed").size(HeadlineSize::Large))
|
||||
.child(
|
||||
Label::new("Using Copilot requires an active subscription on GitHub.")
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Self::render_device_code(data, cx))
|
||||
.child(
|
||||
Label::new("Paste this code into GitHub after clicking the button below.")
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("connect-button", connect_button_label)
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.size(ButtonSize::Medium)
|
||||
.on_click({
|
||||
let command = data.command.clone();
|
||||
cx.listener(move |this, _, _window, cx| {
|
||||
let command = command.clone();
|
||||
let copilot_clone = copilot.clone();
|
||||
let request_timeout = ProjectSettings::get_global(cx)
|
||||
.global_lsp_settings
|
||||
.get_request_timeout();
|
||||
copilot.update(cx, |copilot, cx| {
|
||||
if let Some(server) = copilot.language_server() {
|
||||
let server = server.clone();
|
||||
cx.spawn(async move |_, cx| {
|
||||
let result = server
|
||||
.request::<lsp::request::ExecuteCommand>(
|
||||
lsp::ExecuteCommandParams {
|
||||
command: command.command.clone(),
|
||||
arguments: command
|
||||
.arguments
|
||||
.clone()
|
||||
.unwrap_or_default(),
|
||||
..Default::default()
|
||||
},
|
||||
request_timeout,
|
||||
)
|
||||
.await
|
||||
.into_response()
|
||||
.ok()
|
||||
.flatten();
|
||||
if let Some(value) = result {
|
||||
if let Ok(status) = serde_json::from_value::<
|
||||
request::SignInStatus,
|
||||
>(
|
||||
value
|
||||
) {
|
||||
copilot_clone.update(cx, |copilot, cx| {
|
||||
copilot
|
||||
.update_sign_in_status(status, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
});
|
||||
|
||||
this.connect_clicked = true;
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("copilot-enable-cancel-button", "Cancel")
|
||||
.full_width()
|
||||
.size(ButtonSize::Medium)
|
||||
.on_click(cx.listener(|_, _, _, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_enabled_modal(cx: &mut Context<Self>) -> impl Element {
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.text_center()
|
||||
.justify_center()
|
||||
.child(Headline::new("Copilot Enabled!").size(HeadlineSize::Large))
|
||||
.child(Label::new("You're all set to use GitHub Copilot.").color(Color::Muted))
|
||||
.child(
|
||||
Button::new("copilot-enabled-done-button", "Done")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.size(ButtonSize::Medium)
|
||||
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_unauthorized_modal(&self, cx: &mut Context<Self>) -> impl Element {
|
||||
let sign_up_url = self
|
||||
.sign_up_url
|
||||
.as_deref()
|
||||
.unwrap_or(COPILOT_SIGN_UP_URL)
|
||||
.to_owned();
|
||||
let description = "Enable Copilot by connecting your existing license once you have subscribed or renewed your subscription.";
|
||||
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.text_center()
|
||||
.justify_center()
|
||||
.child(
|
||||
Headline::new("You must have an active GitHub Copilot subscription.")
|
||||
.size(HeadlineSize::Large),
|
||||
)
|
||||
.child(Label::new(description).color(Color::Warning))
|
||||
.child(
|
||||
Button::new("copilot-subscribe-button", "Subscribe on GitHub")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.size(ButtonSize::Medium)
|
||||
.on_click(move |_, _, cx| cx.open_url(&sign_up_url)),
|
||||
)
|
||||
.child(
|
||||
Button::new("copilot-subscribe-cancel-button", "Cancel")
|
||||
.full_width()
|
||||
.size(ButtonSize::Medium)
|
||||
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_error_modal(copilot: Entity<Copilot>, _cx: &mut Context<Self>) -> impl Element {
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.text_center()
|
||||
.justify_center()
|
||||
.child(Headline::new("An Error Happened").size(HeadlineSize::Large))
|
||||
.child(Label::new(ERROR_LABEL).color(Color::Muted))
|
||||
.child(
|
||||
Button::new("copilot-subscribe-button", "Reinstall Copilot and Sign In")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.size(ButtonSize::Medium)
|
||||
.start_icon(
|
||||
Icon::new(IconName::Download)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.on_click(move |_, window, cx| {
|
||||
reinstall_and_sign_in(copilot.clone(), window, cx)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn before_dismiss(
|
||||
&mut self,
|
||||
cx: &mut Context<'_, CopilotCodeVerification>,
|
||||
) -> workspace::DismissDecision {
|
||||
self.copilot.update(cx, |copilot, cx| {
|
||||
if matches!(copilot.status(), Status::SigningIn { .. }) {
|
||||
copilot.sign_out(cx).detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
workspace::DismissDecision::Dismiss(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for CopilotCodeVerification {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let prompt = match &self.status {
|
||||
Status::SigningIn { prompt: None } => Icon::new(IconName::ArrowCircle)
|
||||
.color(Color::Muted)
|
||||
.with_rotate_animation(2)
|
||||
.into_any_element(),
|
||||
Status::SigningIn {
|
||||
prompt: Some(prompt),
|
||||
} => {
|
||||
Self::render_prompting_modal(self.copilot.clone(), self.connect_clicked, prompt, cx)
|
||||
.into_any_element()
|
||||
}
|
||||
Status::Unauthorized => {
|
||||
self.connect_clicked = false;
|
||||
self.render_unauthorized_modal(cx).into_any_element()
|
||||
}
|
||||
Status::Authorized => {
|
||||
self.connect_clicked = false;
|
||||
Self::render_enabled_modal(cx).into_any_element()
|
||||
}
|
||||
Status::Error(..) => {
|
||||
Self::render_error_modal(self.copilot.clone(), cx).into_any_element()
|
||||
}
|
||||
_ => div().into_any_element(),
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.id("copilot_code_verification")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.size_full()
|
||||
.px_4()
|
||||
.py_8()
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.elevation_3(cx)
|
||||
.on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
|
||||
window.focus(&this.focus_handle, cx);
|
||||
}))
|
||||
.child(
|
||||
Vector::new(VectorName::ZedXCopilot, rems(8.), rems(4.))
|
||||
.color(Color::Custom(cx.theme().colors().icon)),
|
||||
)
|
||||
.child(prompt)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ConfigurationView {
|
||||
copilot_status: Option<Status>,
|
||||
is_authenticated: Box<dyn Fn(&mut App) -> bool + 'static>,
|
||||
edit_prediction: bool,
|
||||
_subscription: Option<Subscription>,
|
||||
}
|
||||
|
||||
pub enum ConfigurationMode {
|
||||
Chat,
|
||||
EditPrediction,
|
||||
}
|
||||
|
||||
impl ConfigurationView {
|
||||
pub fn new(
|
||||
is_authenticated: impl Fn(&mut App) -> bool + 'static,
|
||||
mode: ConfigurationMode,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let copilot = AppState::try_global(cx)
|
||||
.and_then(|state| GlobalCopilotAuth::try_get_or_init(state, cx));
|
||||
|
||||
Self {
|
||||
copilot_status: copilot.as_ref().map(|copilot| copilot.0.read(cx).status()),
|
||||
is_authenticated: Box::new(is_authenticated),
|
||||
edit_prediction: matches!(mode, ConfigurationMode::EditPrediction),
|
||||
_subscription: copilot.as_ref().map(|copilot| {
|
||||
cx.observe(&copilot.0, |this, model, cx| {
|
||||
this.copilot_status = Some(model.read(cx).status());
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigurationView {
|
||||
fn is_starting(&self) -> bool {
|
||||
matches!(&self.copilot_status, Some(Status::Starting { .. }))
|
||||
}
|
||||
|
||||
fn is_signing_in(&self) -> bool {
|
||||
matches!(
|
||||
&self.copilot_status,
|
||||
Some(Status::SigningIn { .. })
|
||||
| Some(Status::SignedOut {
|
||||
awaiting_signing_in: true
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
fn is_error(&self) -> bool {
|
||||
matches!(&self.copilot_status, Some(Status::Error(_)))
|
||||
}
|
||||
|
||||
fn has_no_status(&self) -> bool {
|
||||
self.copilot_status.is_none()
|
||||
}
|
||||
|
||||
fn loading_message(&self) -> Option<SharedString> {
|
||||
if self.is_starting() {
|
||||
Some("Starting Copilot…".into())
|
||||
} else if self.is_signing_in() {
|
||||
Some("Signing into Copilot…".into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn render_loading_button(
|
||||
&self,
|
||||
label: impl Into<SharedString>,
|
||||
edit_prediction: bool,
|
||||
) -> impl IntoElement {
|
||||
ButtonLike::new("loading_button")
|
||||
.disabled(true)
|
||||
.style(ButtonStyle::Outlined)
|
||||
.when(edit_prediction, |this| this.size(ButtonSize::Medium))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.justify_center()
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.with_rotate_animation(4),
|
||||
)
|
||||
.child(Label::new(label)),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_sign_in_button(&self, edit_prediction: bool) -> impl IntoElement {
|
||||
let label = if edit_prediction {
|
||||
"Sign in to GitHub"
|
||||
} else {
|
||||
"Sign in to use GitHub Copilot"
|
||||
};
|
||||
|
||||
Button::new("sign_in", label)
|
||||
.map(|this| {
|
||||
if edit_prediction {
|
||||
this.size(ButtonSize::Medium)
|
||||
} else {
|
||||
this.full_width()
|
||||
}
|
||||
})
|
||||
.style(ButtonStyle::Outlined)
|
||||
.start_icon(
|
||||
Icon::new(IconName::Github)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.when(edit_prediction, |this| this.tab_index(0isize))
|
||||
.on_click(|_, window, cx| {
|
||||
let app_state = AppState::global(cx);
|
||||
if let Some(copilot) = GlobalCopilotAuth::try_get_or_init(app_state, cx) {
|
||||
initiate_sign_in(copilot.0, window, cx)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn render_reinstall_button(&self, edit_prediction: bool) -> impl IntoElement {
|
||||
let label = if edit_prediction {
|
||||
"Reinstall and Sign in"
|
||||
} else {
|
||||
"Reinstall Copilot and Sign in"
|
||||
};
|
||||
|
||||
Button::new("reinstall_and_sign_in", label)
|
||||
.map(|this| {
|
||||
if edit_prediction {
|
||||
this.size(ButtonSize::Medium)
|
||||
} else {
|
||||
this.full_width()
|
||||
}
|
||||
})
|
||||
.style(ButtonStyle::Outlined)
|
||||
.start_icon(
|
||||
Icon::new(IconName::Download)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.on_click(|_, window, cx| {
|
||||
let app_state = AppState::global(cx);
|
||||
if let Some(copilot) = GlobalCopilotAuth::try_get_or_init(app_state, cx) {
|
||||
reinstall_and_sign_in(copilot.0, window, cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn render_for_edit_prediction(&self) -> impl IntoElement {
|
||||
let container = |description: SharedString, action: AnyElement| {
|
||||
h_flex()
|
||||
.pt_2p5()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.max_w_1_2()
|
||||
.child(Label::new("Authenticate To Use"))
|
||||
.child(
|
||||
Label::new(description)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
)
|
||||
.child(action)
|
||||
};
|
||||
|
||||
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();
|
||||
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();
|
||||
|
||||
if let Some(msg) = self.loading_message() {
|
||||
container(
|
||||
start_label,
|
||||
self.render_loading_button(msg, true).into_any_element(),
|
||||
)
|
||||
.into_any_element()
|
||||
} else if self.is_error() {
|
||||
container(
|
||||
ERROR_LABEL.into(),
|
||||
self.render_reinstall_button(true).into_any_element(),
|
||||
)
|
||||
.into_any_element()
|
||||
} else if self.has_no_status() {
|
||||
container(
|
||||
no_status_label,
|
||||
self.render_sign_in_button(true).into_any_element(),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
container(
|
||||
start_label,
|
||||
self.render_sign_in_button(true).into_any_element(),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_for_chat(&self) -> impl IntoElement {
|
||||
let start_label = "To use Zed's agent with GitHub Copilot, you need to be logged in to GitHub. Note that your GitHub account must have an active Copilot Chat subscription.";
|
||||
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.";
|
||||
|
||||
if let Some(msg) = self.loading_message() {
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(Label::new(start_label))
|
||||
.child(self.render_loading_button(msg, false))
|
||||
.into_any_element()
|
||||
} else if self.is_error() {
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(Label::new(ERROR_LABEL))
|
||||
.child(self.render_reinstall_button(false))
|
||||
.into_any_element()
|
||||
} else if self.has_no_status() {
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(Label::new(no_status_label))
|
||||
.child(self.render_sign_in_button(false))
|
||||
.into_any_element()
|
||||
} else {
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(Label::new(start_label))
|
||||
.child(self.render_sign_in_button(false))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ConfigurationView {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let is_authenticated = &self.is_authenticated;
|
||||
|
||||
if is_authenticated(cx) {
|
||||
return ConfiguredApiCard::new("Authorized")
|
||||
.button_label("Sign Out")
|
||||
.on_click(|_, window, cx| {
|
||||
if let Some(auth) = GlobalCopilotAuth::try_global(cx) {
|
||||
initiate_sign_out(auth.0.clone(), window, cx);
|
||||
}
|
||||
})
|
||||
.into_any_element();
|
||||
}
|
||||
|
||||
if self.edit_prediction {
|
||||
self.render_for_edit_prediction().into_any_element()
|
||||
} else {
|
||||
self.render_for_chat().into_any_element()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user