From 76cd936f8fca9c73fbd5d45c8709284756f672a0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:27:45 +0000 Subject: [PATCH 01/13] Initial plan From e52a557be7a4c54f1d0da1d953dc21e7d03cefc5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:31:00 +0000 Subject: [PATCH 02/13] feat: add OAuth (GitHub/Google) sign-in and show-password toggle to login dialog Agent-Logs-Url: https://github.com/highperformancecoder/minsky/sessions/93883e56-cb4c-463a-838b-565280e1743a Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com> --- .../src/environments/environment.dev.ts | 1 + .../src/environments/environment.prod.ts | 1 + .../src/environments/environment.ts | 1 + .../src/environments/environment.web.ts | 1 + .../src/lib/services/clerk/clerk.service.ts | 9 +++++ .../src/lib/login/login.component.html | 18 ++++++++- .../src/lib/login/login.component.scss | 40 +++++++++++++++++++ .../src/lib/login/login.component.ts | 32 +++++++++++++++ 8 files changed, 102 insertions(+), 1 deletion(-) diff --git a/gui-js/apps/minsky-web/src/environments/environment.dev.ts b/gui-js/apps/minsky-web/src/environments/environment.dev.ts index 6546a7376..d5367b8ba 100644 --- a/gui-js/apps/minsky-web/src/environments/environment.dev.ts +++ b/gui-js/apps/minsky-web/src/environments/environment.dev.ts @@ -7,4 +7,5 @@ export const AppConfig = { production: false, environment: 'DEV', clerkPublishableKey: 'pk_test_cG9zaXRpdmUtcGhvZW5peC04NS5jbGVyay5hY2NvdW50cy5kZXYk', + clerkOAuthRedirectUrl: 'https://clerk.minsky.app/v1/oauth_callback', }; diff --git a/gui-js/apps/minsky-web/src/environments/environment.prod.ts b/gui-js/apps/minsky-web/src/environments/environment.prod.ts index ab40633a3..5124714d6 100644 --- a/gui-js/apps/minsky-web/src/environments/environment.prod.ts +++ b/gui-js/apps/minsky-web/src/environments/environment.prod.ts @@ -2,4 +2,5 @@ export const AppConfig = { production: true, environment: 'PROD', clerkPublishableKey: 'pk_test_cG9zaXRpdmUtcGhvZW5peC04NS5jbGVyay5hY2NvdW50cy5kZXYk', + clerkOAuthRedirectUrl: 'https://clerk.minsky.app/v1/oauth_callback', }; diff --git a/gui-js/apps/minsky-web/src/environments/environment.ts b/gui-js/apps/minsky-web/src/environments/environment.ts index 17dad3283..3dcdac461 100644 --- a/gui-js/apps/minsky-web/src/environments/environment.ts +++ b/gui-js/apps/minsky-web/src/environments/environment.ts @@ -2,4 +2,5 @@ export const AppConfig = { production: false, environment: 'LOCAL', clerkPublishableKey: 'pk_test_cG9zaXRpdmUtcGhvZW5peC04NS5jbGVyay5hY2NvdW50cy5kZXYk', + clerkOAuthRedirectUrl: 'https://clerk.minsky.app/v1/oauth_callback', }; diff --git a/gui-js/apps/minsky-web/src/environments/environment.web.ts b/gui-js/apps/minsky-web/src/environments/environment.web.ts index 6546a7376..d5367b8ba 100644 --- a/gui-js/apps/minsky-web/src/environments/environment.web.ts +++ b/gui-js/apps/minsky-web/src/environments/environment.web.ts @@ -7,4 +7,5 @@ export const AppConfig = { production: false, environment: 'DEV', clerkPublishableKey: 'pk_test_cG9zaXRpdmUtcGhvZW5peC04NS5jbGVyay5hY2NvdW50cy5kZXYk', + clerkOAuthRedirectUrl: 'https://clerk.minsky.app/v1/oauth_callback', }; diff --git a/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts b/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts index f526b1488..25004dc9f 100644 --- a/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts +++ b/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts @@ -46,6 +46,15 @@ export class ClerkService { } } + async signInWithOAuth(provider: 'oauth_github' | 'oauth_google'): Promise { + if (!this.clerk) throw new Error('Clerk is not initialized.'); + await this.clerk.client.signIn.authenticateWithRedirect({ + strategy: provider, + redirectUrl: AppConfig.clerkOAuthRedirectUrl, + redirectUrlComplete: AppConfig.clerkOAuthRedirectUrl, + }); + } + async signOut(): Promise { if (!this.clerk) throw new Error('Clerk is not initialized.'); await this.clerk.signOut(); diff --git a/gui-js/libs/ui-components/src/lib/login/login.component.html b/gui-js/libs/ui-components/src/lib/login/login.component.html index 04d705eba..ecbc5ed93 100644 --- a/gui-js/libs/ui-components/src/lib/login/login.component.html +++ b/gui-js/libs/ui-components/src/lib/login/login.component.html @@ -20,10 +20,12 @@

Sign In

Password - + Password is required. + Show password + + +
+ or +
+ + + + diff --git a/gui-js/libs/ui-components/src/lib/login/login.component.scss b/gui-js/libs/ui-components/src/lib/login/login.component.scss index 41fd46be5..5bd57a02e 100644 --- a/gui-js/libs/ui-components/src/lib/login/login.component.scss +++ b/gui-js/libs/ui-components/src/lib/login/login.component.scss @@ -29,3 +29,43 @@ mat-spinner { display: inline-block; margin-right: 8px; } + +.show-password-checkbox { + margin-bottom: 12px; + align-self: flex-start; +} + +.oauth-divider { + display: flex; + align-items: center; + width: 100%; + margin: 12px 0; + + &::before, + &::after { + content: ''; + flex: 1; + border-bottom: 1px solid #ccc; + } + + span { + padding: 0 8px; + color: #666; + font-size: 0.85em; + } +} + +.oauth-button { + margin-bottom: 8px; + justify-content: center; +} + +.github-button { + border-color: #333; + color: #333; +} + +.google-button { + border-color: #4285f4; + color: #4285f4; +} diff --git a/gui-js/libs/ui-components/src/lib/login/login.component.ts b/gui-js/libs/ui-components/src/lib/login/login.component.ts index b1b2afb30..1b773a8ca 100644 --- a/gui-js/libs/ui-components/src/lib/login/login.component.ts +++ b/gui-js/libs/ui-components/src/lib/login/login.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormControl, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatInputModule } from '@angular/material/input'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @@ -19,6 +20,7 @@ import { take } from 'rxjs'; CommonModule, ReactiveFormsModule, MatButtonModule, + MatCheckboxModule, MatInputModule, MatFormFieldModule, MatProgressSpinnerModule, @@ -33,6 +35,8 @@ export class LoginComponent implements OnInit { isLoading = false; errorMessage = ''; isAuthenticated = false; + showPassword = false; + isOAuthLoading: 'github' | 'google' | null = null; constructor( private clerkService: ClerkService, @@ -69,6 +73,10 @@ export class LoginComponent implements OnInit { return this.loginForm.get('password'); } + togglePasswordVisibility() { + this.showPassword = !this.showPassword; + } + async onSubmit() { if (this.loginForm.invalid) return; @@ -89,6 +97,30 @@ export class LoginComponent implements OnInit { } } + async onSignInWithGitHub() { + this.isOAuthLoading = 'github'; + this.errorMessage = ''; + try { + await this.clerkService.signInWithOAuth('oauth_github'); + } catch (err: any) { + this.errorMessage = err?.errors?.[0]?.message ?? err?.message ?? 'GitHub sign-in failed.'; + } finally { + this.isOAuthLoading = null; + } + } + + async onSignInWithGoogle() { + this.isOAuthLoading = 'google'; + this.errorMessage = ''; + try { + await this.clerkService.signInWithOAuth('oauth_google'); + } catch (err: any) { + this.errorMessage = err?.errors?.[0]?.message ?? err?.message ?? 'Google sign-in failed.'; + } finally { + this.isOAuthLoading = null; + } + } + async onSignOut() { this.isLoading = true; this.errorMessage = ''; From 7fcfe8623a7fcca1f9a8c76093e744b343fa5189 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 23:13:36 +0000 Subject: [PATCH 03/13] feat: replace custom login form with Clerk's mounted sign-in UI Agent-Logs-Url: https://github.com/highperformancecoder/minsky/sessions/d709eaa8-072b-4e05-ad66-96a5dbfd2e48 Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com> --- .../src/environments/environment.dev.ts | 1 - .../src/environments/environment.prod.ts | 1 - .../src/environments/environment.ts | 1 - .../src/environments/environment.web.ts | 1 - .../src/lib/services/clerk/clerk.service.ts | 19 ++-- .../src/lib/login/login.component.html | 56 ++-------- .../src/lib/login/login.component.scss | 55 +--------- .../src/lib/login/login.component.ts | 103 +++++++----------- 8 files changed, 60 insertions(+), 177 deletions(-) diff --git a/gui-js/apps/minsky-web/src/environments/environment.dev.ts b/gui-js/apps/minsky-web/src/environments/environment.dev.ts index d5367b8ba..6546a7376 100644 --- a/gui-js/apps/minsky-web/src/environments/environment.dev.ts +++ b/gui-js/apps/minsky-web/src/environments/environment.dev.ts @@ -7,5 +7,4 @@ export const AppConfig = { production: false, environment: 'DEV', clerkPublishableKey: 'pk_test_cG9zaXRpdmUtcGhvZW5peC04NS5jbGVyay5hY2NvdW50cy5kZXYk', - clerkOAuthRedirectUrl: 'https://clerk.minsky.app/v1/oauth_callback', }; diff --git a/gui-js/apps/minsky-web/src/environments/environment.prod.ts b/gui-js/apps/minsky-web/src/environments/environment.prod.ts index 5124714d6..ab40633a3 100644 --- a/gui-js/apps/minsky-web/src/environments/environment.prod.ts +++ b/gui-js/apps/minsky-web/src/environments/environment.prod.ts @@ -2,5 +2,4 @@ export const AppConfig = { production: true, environment: 'PROD', clerkPublishableKey: 'pk_test_cG9zaXRpdmUtcGhvZW5peC04NS5jbGVyay5hY2NvdW50cy5kZXYk', - clerkOAuthRedirectUrl: 'https://clerk.minsky.app/v1/oauth_callback', }; diff --git a/gui-js/apps/minsky-web/src/environments/environment.ts b/gui-js/apps/minsky-web/src/environments/environment.ts index 3dcdac461..17dad3283 100644 --- a/gui-js/apps/minsky-web/src/environments/environment.ts +++ b/gui-js/apps/minsky-web/src/environments/environment.ts @@ -2,5 +2,4 @@ export const AppConfig = { production: false, environment: 'LOCAL', clerkPublishableKey: 'pk_test_cG9zaXRpdmUtcGhvZW5peC04NS5jbGVyay5hY2NvdW50cy5kZXYk', - clerkOAuthRedirectUrl: 'https://clerk.minsky.app/v1/oauth_callback', }; diff --git a/gui-js/apps/minsky-web/src/environments/environment.web.ts b/gui-js/apps/minsky-web/src/environments/environment.web.ts index d5367b8ba..6546a7376 100644 --- a/gui-js/apps/minsky-web/src/environments/environment.web.ts +++ b/gui-js/apps/minsky-web/src/environments/environment.web.ts @@ -7,5 +7,4 @@ export const AppConfig = { production: false, environment: 'DEV', clerkPublishableKey: 'pk_test_cG9zaXRpdmUtcGhvZW5peC04NS5jbGVyay5hY2NvdW50cy5kZXYk', - clerkOAuthRedirectUrl: 'https://clerk.minsky.app/v1/oauth_callback', }; diff --git a/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts b/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts index 25004dc9f..5a4bd3f37 100644 --- a/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts +++ b/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts @@ -31,6 +31,16 @@ export class ClerkService { return await this.clerk.session.getToken(); } + mountSignIn(element: HTMLElement): void { + if (!this.clerk) throw new Error('Clerk is not initialized.'); + this.clerk.mountSignIn(element); + } + + addListener(callback: (resources: { session: { id: string } | null }) => void): () => void { + if (!this.clerk) throw new Error('Clerk is not initialized.'); + return this.clerk.addListener(callback); + } + async signInWithEmailPassword(email: string | null | undefined, password: string | null | undefined): Promise { if (!this.clerk) throw new Error('Clerk is not initialized.'); if (!email || !password) throw new Error('Email and password are required.'); @@ -46,15 +56,6 @@ export class ClerkService { } } - async signInWithOAuth(provider: 'oauth_github' | 'oauth_google'): Promise { - if (!this.clerk) throw new Error('Clerk is not initialized.'); - await this.clerk.client.signIn.authenticateWithRedirect({ - strategy: provider, - redirectUrl: AppConfig.clerkOAuthRedirectUrl, - redirectUrlComplete: AppConfig.clerkOAuthRedirectUrl, - }); - } - async signOut(): Promise { if (!this.clerk) throw new Error('Clerk is not initialized.'); await this.clerk.signOut(); diff --git a/gui-js/libs/ui-components/src/lib/login/login.component.html b/gui-js/libs/ui-components/src/lib/login/login.component.html index ecbc5ed93..07f9748cf 100644 --- a/gui-js/libs/ui-components/src/lib/login/login.component.html +++ b/gui-js/libs/ui-components/src/lib/login/login.component.html @@ -1,51 +1,17 @@ diff --git a/gui-js/libs/ui-components/src/lib/login/login.component.scss b/gui-js/libs/ui-components/src/lib/login/login.component.scss index 53fddff08..3c021cd99 100644 --- a/gui-js/libs/ui-components/src/lib/login/login.component.scss +++ b/gui-js/libs/ui-components/src/lib/login/login.component.scss @@ -18,27 +18,3 @@ .signed-in-message { margin-bottom: 16px; } - -.sign-in-form { - display: flex; - flex-direction: column; - width: 100%; - max-width: 320px; - gap: 8px; - - mat-form-field { - width: 100%; - } - - button[type='submit'] { - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - width: 100%; - } -} - -.button-spinner { - display: inline-flex; -} diff --git a/gui-js/libs/ui-components/src/lib/login/login.component.ts b/gui-js/libs/ui-components/src/lib/login/login.component.ts index 46b6badf9..cec3baa17 100644 --- a/gui-js/libs/ui-components/src/lib/login/login.component.ts +++ b/gui-js/libs/ui-components/src/lib/login/login.component.ts @@ -1,9 +1,6 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, OnDestroy, ElementRef, ViewChild } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatInputModule } from '@angular/material/input'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ClerkService } from '@minsky/core'; import { ElectronService } from '@minsky/core'; @@ -17,20 +14,35 @@ import { take } from 'rxjs'; standalone: true, imports: [ CommonModule, - FormsModule, MatButtonModule, - MatFormFieldModule, - MatInputModule, MatProgressSpinnerModule, ], }) -export class LoginComponent implements OnInit { +export class LoginComponent implements OnInit, OnDestroy { + // Use a setter so mountClerkUI() fires as soon as *ngIf renders the div, + // regardless of whether initializeSession has already finished or not. + // The call is deferred via Promise.resolve().then() to avoid NG0100 + // (ExpressionChangedAfterItHasBeenCheckedError) because the setter may fire + // during Angular's change-detection pass. + @ViewChild('clerkSignIn') + set clerkSignInEl(el: ElementRef) { + this._clerkSignInEl = el; + if (el && this.pendingMount) { + this.pendingMount = false; + Promise.resolve().then(() => this.mountClerkUI()); + } + } + get clerkSignInEl(): ElementRef | undefined { + return this._clerkSignInEl; + } + private _clerkSignInEl: ElementRef | undefined; + isLoading = true; - isSigningIn = false; errorMessage = ''; isAuthenticated = false; - email = ''; - password = ''; + + private unsubscribeClerk: (() => void) | null = null; + private pendingMount = false; constructor( private clerkService: ClerkService, @@ -44,7 +56,13 @@ export class LoginComponent implements OnInit { }); } + ngOnDestroy() { + this.unsubscribeClerk?.(); + } + private async initializeSession(authToken: string | undefined) { + // Keep initialize() errors separate: if Clerk itself fails to load we + // cannot show the sign-in UI and must bail out early. try { await this.clerkService.initialize(); } catch (err) { @@ -57,27 +75,43 @@ export class LoginComponent implements OnInit { if (authToken) { await this.clerkService.setSession(authToken); } + this.isAuthenticated = await this.clerkService.isSignedIn(); + + if (!this.isAuthenticated) { + this.scheduleOrMountUI(); + } } catch (err) { this.errorMessage = 'Session expired. Please sign in again.'; this.isAuthenticated = false; + this.scheduleOrMountUI(); } finally { this.isLoading = false; } } - async onSignIn() { - this.isSigningIn = true; - this.errorMessage = ''; + private scheduleOrMountUI() { + if (this._clerkSignInEl) { + this.mountClerkUI(); + } else { + this.pendingMount = true; + } + } + + private mountClerkUI() { + if (!this._clerkSignInEl) return; try { - await this.clerkService.signInWithEmailPassword(this.email, this.password); - this.isAuthenticated = true; - this.electronService.closeWindow(); + this.clerkService.mountSignIn(this._clerkSignInEl.nativeElement); } catch (err: any) { - this.errorMessage = err?.message ?? 'Sign in failed.'; - } finally { - this.isSigningIn = false; + this.errorMessage = err?.message ?? 'Failed to load sign-in UI.'; + return; } + this.unsubscribeClerk = this.clerkService.addListener(async ({ session }) => { + if (session) { + await this.clerkService.sendTokenToElectron(); + this.electronService.closeWindow(); + } + }); } async onSignOut() { From 9907411302db79128251416599d2dd0105070943 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 01:53:15 +0000 Subject: [PATCH 10/13] fix: use headless Clerk API with custom email/password form for Electron auth Agent-Logs-Url: https://github.com/highperformancecoder/minsky/sessions/ef399f0f-200a-4aeb-81c0-f9660582d2c5 Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com> --- .../src/lib/services/clerk/clerk.service.ts | 87 +++++-------------- .../src/lib/login/login.component.html | 15 +++- .../src/lib/login/login.component.ts | 74 +++++----------- 3 files changed, 56 insertions(+), 120 deletions(-) diff --git a/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts b/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts index feb5a488d..5cfe94ba6 100644 --- a/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts +++ b/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts @@ -1,16 +1,9 @@ import { Injectable } from '@angular/core'; import { ElectronService } from '../electron/electron.service'; import { events } from '@minsky/shared'; -import type { Clerk } from '@clerk/clerk-js'; +import { Clerk } from '@clerk/clerk-js'; import { AppConfig } from '@minsky/environment'; -declare global { - interface Window { - Clerk?: Clerk; - __clerk_publishable_key?: string; - } -} - @Injectable({ providedIn: 'root', }) @@ -23,59 +16,18 @@ export class ClerkService { async initialize(): Promise { if (this.initialized) return; - // Load Clerk via its CDN browser bundle rather than the npm-imported module. - // The npm dist files (clerk.js / clerk.mjs) do not bundle the ClerkUI - // implementation, so clerk.mountSignIn() would always throw - // "Clerk was not loaded with Ui components" when called on an instance - // created with `new Clerk(key)` from the npm package. - // The browser bundle served from Clerk's CDN lazily loads the UI chunks - // (React-based pre-built components) from the same CDN origin, enabling - // mountSignIn() to work correctly. - await this.loadClerkBrowserBundle(); - if (!window.Clerk) { - throw new Error('Clerk failed to initialize: window.Clerk is not set after loading the browser bundle'); - } - this.clerk = window.Clerk; - await this.clerk.load(); + // Use the headless Clerk JS package (no React/ClerkUI dependency). + // Pre-built UI components (mountSignIn etc.) require @clerk/ui which loads + // React chunks lazily from Clerk's CDN. Electron blocks those requests, so + // mountSignIn() always throws "Clerk was not loaded with Ui components". + // Authentication is performed directly via clerk.client.signIn.create(). + // standardBrowser: false uses the lightweight non-cookie path appropriate + // for Electron's renderer process. + this.clerk = new Clerk(AppConfig.clerkPublishableKey); + await this.clerk.load({ standardBrowser: false }); this.initialized = true; } - /** - * Dynamically injects Clerk's CDN browser bundle script into the document. - * The CDN URL is derived from the publishable key's embedded frontendApi. - * Returns a Promise that resolves once the script has loaded and - * window.Clerk has been set by the bundle. - */ - private loadClerkBrowserBundle(): Promise { - return new Promise((resolve, reject) => { - if (window.Clerk) { - resolve(); - return; - } - - const pk = AppConfig.clerkPublishableKey; - // Publishable key format: pk__ - const encoded = pk.split('_')[2] ?? ''; - const padded = encoded + '='.repeat((4 - (encoded.length % 4)) % 4); - let frontendApi: string; - try { - frontendApi = atob(padded).replace(/\$$/, ''); - } catch { - reject(new Error('Invalid Clerk publishable key')); - return; - } - - const script = document.createElement('script'); - script.src = `https://${frontendApi}/npm/@clerk/clerk-js@6.6.0/dist/clerk.browser.js`; - script.setAttribute('data-clerk-publishable-key', pk); - script.async = true; - script.onload = () => resolve(); - script.onerror = () => - reject(new Error('Failed to load Clerk authentication service')); - document.head.appendChild(script); - }); - } - async isSignedIn(): Promise { if (!this.clerk) return false; return !!this.clerk.user; @@ -86,14 +38,19 @@ export class ClerkService { return await this.clerk.session.getToken(); } - mountSignIn(element: HTMLDivElement): void { + async signInWithEmailPassword(email: string | null | undefined, password: string | null | undefined): Promise { if (!this.clerk) throw new Error('Clerk is not initialized.'); - this.clerk.mountSignIn(element); - } - - addListener(callback: (resources: { session: { id: string } | null }) => void): () => void { - if (!this.clerk) throw new Error('Clerk is not initialized.'); - return this.clerk.addListener(callback); + if (!email || !password) throw new Error('Email and password are required.'); + const result = await this.clerk.client.signIn.create({ + identifier: email, + password, + }); + if (result.status === 'complete') { + await this.clerk.setActive({ session: result.createdSessionId }); + await this.sendTokenToElectron(); + } else { + throw new Error('Sign-in was not completed. Additional steps may be required.'); + } } async signOut(): Promise { diff --git a/gui-js/libs/ui-components/src/lib/login/login.component.html b/gui-js/libs/ui-components/src/lib/login/login.component.html index 07f9748cf..50e5da495 100644 --- a/gui-js/libs/ui-components/src/lib/login/login.component.html +++ b/gui-js/libs/ui-components/src/lib/login/login.component.html @@ -11,7 +11,20 @@ -
+
diff --git a/gui-js/libs/ui-components/src/lib/login/login.component.ts b/gui-js/libs/ui-components/src/lib/login/login.component.ts index cec3baa17..46b6badf9 100644 --- a/gui-js/libs/ui-components/src/lib/login/login.component.ts +++ b/gui-js/libs/ui-components/src/lib/login/login.component.ts @@ -1,6 +1,9 @@ -import { Component, OnInit, OnDestroy, ElementRef, ViewChild } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ClerkService } from '@minsky/core'; import { ElectronService } from '@minsky/core'; @@ -14,35 +17,20 @@ import { take } from 'rxjs'; standalone: true, imports: [ CommonModule, + FormsModule, MatButtonModule, + MatFormFieldModule, + MatInputModule, MatProgressSpinnerModule, ], }) -export class LoginComponent implements OnInit, OnDestroy { - // Use a setter so mountClerkUI() fires as soon as *ngIf renders the div, - // regardless of whether initializeSession has already finished or not. - // The call is deferred via Promise.resolve().then() to avoid NG0100 - // (ExpressionChangedAfterItHasBeenCheckedError) because the setter may fire - // during Angular's change-detection pass. - @ViewChild('clerkSignIn') - set clerkSignInEl(el: ElementRef) { - this._clerkSignInEl = el; - if (el && this.pendingMount) { - this.pendingMount = false; - Promise.resolve().then(() => this.mountClerkUI()); - } - } - get clerkSignInEl(): ElementRef | undefined { - return this._clerkSignInEl; - } - private _clerkSignInEl: ElementRef | undefined; - +export class LoginComponent implements OnInit { isLoading = true; + isSigningIn = false; errorMessage = ''; isAuthenticated = false; - - private unsubscribeClerk: (() => void) | null = null; - private pendingMount = false; + email = ''; + password = ''; constructor( private clerkService: ClerkService, @@ -56,13 +44,7 @@ export class LoginComponent implements OnInit, OnDestroy { }); } - ngOnDestroy() { - this.unsubscribeClerk?.(); - } - private async initializeSession(authToken: string | undefined) { - // Keep initialize() errors separate: if Clerk itself fails to load we - // cannot show the sign-in UI and must bail out early. try { await this.clerkService.initialize(); } catch (err) { @@ -75,43 +57,27 @@ export class LoginComponent implements OnInit, OnDestroy { if (authToken) { await this.clerkService.setSession(authToken); } - this.isAuthenticated = await this.clerkService.isSignedIn(); - - if (!this.isAuthenticated) { - this.scheduleOrMountUI(); - } } catch (err) { this.errorMessage = 'Session expired. Please sign in again.'; this.isAuthenticated = false; - this.scheduleOrMountUI(); } finally { this.isLoading = false; } } - private scheduleOrMountUI() { - if (this._clerkSignInEl) { - this.mountClerkUI(); - } else { - this.pendingMount = true; - } - } - - private mountClerkUI() { - if (!this._clerkSignInEl) return; + async onSignIn() { + this.isSigningIn = true; + this.errorMessage = ''; try { - this.clerkService.mountSignIn(this._clerkSignInEl.nativeElement); + await this.clerkService.signInWithEmailPassword(this.email, this.password); + this.isAuthenticated = true; + this.electronService.closeWindow(); } catch (err: any) { - this.errorMessage = err?.message ?? 'Failed to load sign-in UI.'; - return; + this.errorMessage = err?.message ?? 'Sign in failed.'; + } finally { + this.isSigningIn = false; } - this.unsubscribeClerk = this.clerkService.addListener(async ({ session }) => { - if (session) { - await this.clerkService.sendTokenToElectron(); - this.electronService.closeWindow(); - } - }); } async onSignOut() { From 4596f7f4c969cb7b16aa47f7e42aaa3fcc2211ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 02:08:13 +0000 Subject: [PATCH 11/13] fix: use Clerk hosted sign-in page in BrowserWindow (fixes 'not loaded with Ui components') Agent-Logs-Url: https://github.com/highperformancecoder/minsky/sessions/7da5d79d-7c5f-4968-8943-58398993784d Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com> --- .../src/app/managers/WindowManager.ts | 90 ++++++++++++++++--- .../src/lib/services/clerk/clerk.service.ts | 31 ++----- .../shared/src/lib/constants/constants.ts | 4 + .../src/lib/login/login.component.html | 18 +--- .../src/lib/login/login.component.scss | 2 +- .../src/lib/login/login.component.ts | 23 ----- 6 files changed, 91 insertions(+), 77 deletions(-) diff --git a/gui-js/apps/minsky-electron/src/app/managers/WindowManager.ts b/gui-js/apps/minsky-electron/src/app/managers/WindowManager.ts index d880cf920..38c7c4549 100644 --- a/gui-js/apps/minsky-electron/src/app/managers/WindowManager.ts +++ b/gui-js/apps/minsky-electron/src/app/managers/WindowManager.ts @@ -2,6 +2,7 @@ import { ActiveWindow, AppLayoutPayload, CreateWindowPayload, + CLERK_PUBLISHABLE_KEY, events, Functions, minsky, @@ -12,7 +13,7 @@ import { Utility, } from '@minsky/shared'; import { StoreManager } from './StoreManager'; -import { BrowserWindow, dialog, Menu, OpenDialogOptions, SaveDialogOptions, screen } from 'electron'; +import { BrowserWindow, dialog, Menu, OpenDialogOptions, SaveDialogOptions, safeStorage, screen } from 'electron'; import log from 'electron-log'; import os from 'os'; import { join, dirname } from 'path'; @@ -384,21 +385,82 @@ export class WindowManager { } } - static async openLoginWindow() { - const existingToken = StoreManager.store.get('authToken') || ''; - const loginWindow = WindowManager.createPopupWindowWithRouting({ - width: 420, - height: 500, - title: 'Login', - modal: false, - url: `#/headless/login?authToken=${encodeURIComponent(existingToken)}`, - }); - - return new Promise((resolve)=>{ - // Resolve with null if the user closes the window before authenticating + static async openLoginWindow(): Promise { + // Derive the Clerk frontendApi hostname from the publishable key. + // Key format: pk__ + const encoded = CLERK_PUBLISHABLE_KEY.split('_')[2] ?? ''; + const padded = encoded + '='.repeat((4 - (encoded.length % 4)) % 4); + let frontendApi: string; + try { + frontendApi = Buffer.from(padded, 'base64').toString('utf8').replace(/\$$/, ''); + } catch { + log.error('WindowManager.openLoginWindow: invalid Clerk publishable key'); + return Promise.resolve(null); + } + + // Open Clerk's hosted sign-in page in a dedicated BrowserWindow. + // Because this window loads from HTTPS (not file://), Clerk's CDN + // resources and React UI components load normally — the full standard + // Clerk sign-in UI is displayed, including every configured OAuth provider. + // This mirrors the approach used by @clerk/electron without requiring that + // package. + // + // After successful sign-in, Clerk redirects to redirect_url ('minsky://signed-in'). + // We intercept that navigation with will-navigate, execute JS in the still-live + // sign-in page to obtain a JWT from window.Clerk.session.getToken(), stash it, + // and close the window. + const redirectUrl = 'minsky://signed-in'; + const signInUrl = `https://${frontendApi}/sign-in?redirect_url=${encodeURIComponent(redirectUrl)}`; + + return new Promise((resolve) => { + const loginWindow = new BrowserWindow({ + width: 480, + height: 640, + title: 'Sign In', + parent: WindowManager.getMainWindow(), + modal: false, + webPreferences: { + contextIsolation: true, + nodeIntegration: false, + }, + icon: __dirname + '/assets/favicon.png', + }); + + loginWindow.setMenu(null); + loginWindow.once('ready-to-show', () => loginWindow.show()); + + loginWindow.webContents.on('will-navigate', async (event, url) => { + if (url.startsWith('minsky://')) { + // The sign-in page is about to redirect to our custom scheme, meaning + // sign-in completed successfully. Prevent the navigation (the minsky:// + // scheme is not registered as a real protocol), then extract the JWT + // from window.Clerk.session in the still-live sign-in page context. + event.preventDefault(); + try { + const token: string | null = await loginWindow.webContents.executeJavaScript( + '(async () => { try { return await window.Clerk?.session?.getToken() ?? null; } catch(e) { return null; } })()' + ); + if (token) { + // Inline token stash (mirrors CommandsManager.stashClerkToken). + if (safeStorage.isEncryptionAvailable()) { + const encrypted = safeStorage.encryptString(token); + StoreManager.store.set('authToken', encrypted.toString('latin1')); + } else { + StoreManager.store.set('authToken', token); + } + } + } catch (err) { + log.error('WindowManager.openLoginWindow: failed to retrieve Clerk token', err); + } + loginWindow.close(); + } + }); + loginWindow.once('closed', () => { - resolve(StoreManager.store.get('authToken')); + resolve(StoreManager.store.get('authToken') as string | null ?? null); }); + + loginWindow.loadURL(signInUrl); }); } diff --git a/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts b/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts index 5cfe94ba6..e3edae1a8 100644 --- a/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts +++ b/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts @@ -16,13 +16,13 @@ export class ClerkService { async initialize(): Promise { if (this.initialized) return; - // Use the headless Clerk JS package (no React/ClerkUI dependency). - // Pre-built UI components (mountSignIn etc.) require @clerk/ui which loads - // React chunks lazily from Clerk's CDN. Electron blocks those requests, so - // mountSignIn() always throws "Clerk was not loaded with Ui components". - // Authentication is performed directly via clerk.client.signIn.create(). - // standardBrowser: false uses the lightweight non-cookie path appropriate - // for Electron's renderer process. + // The npm dist build of @clerk/clerk-js is headless: it deliberately omits + // the React-based pre-built UI components (mountSignIn etc.) to keep the + // bundle small. In Electron the login window is Clerk's own hosted sign-in + // page opened by the main process in a dedicated BrowserWindow, so this + // renderer-side Clerk instance is only used for session queries (isSignedIn, + // getToken, setSession, signOut). standardBrowser:false selects the + // lightweight non-cookie path appropriate for Electron's renderer process. this.clerk = new Clerk(AppConfig.clerkPublishableKey); await this.clerk.load({ standardBrowser: false }); this.initialized = true; @@ -38,21 +38,6 @@ export class ClerkService { return await this.clerk.session.getToken(); } - async signInWithEmailPassword(email: string | null | undefined, password: string | null | undefined): Promise { - if (!this.clerk) throw new Error('Clerk is not initialized.'); - if (!email || !password) throw new Error('Email and password are required.'); - const result = await this.clerk.client.signIn.create({ - identifier: email, - password, - }); - if (result.status === 'complete') { - await this.clerk.setActive({ session: result.createdSessionId }); - await this.sendTokenToElectron(); - } else { - throw new Error('Sign-in was not completed. Additional steps may be required.'); - } - } - async signOut(): Promise { if (!this.clerk) throw new Error('Clerk is not initialized.'); await this.clerk.signOut(); @@ -77,7 +62,7 @@ export class ClerkService { await this.clerk.setActive({ session: this.clerk.client.sessions[0].id }); } if (!this.clerk.session) { - if (this.electronService.isElectron) + if (this.electronService.isElectron) await this.electronService.invoke(events.SET_AUTH_TOKEN, null); throw new Error('Session expired or invalid'); } diff --git a/gui-js/libs/shared/src/lib/constants/constants.ts b/gui-js/libs/shared/src/lib/constants/constants.ts index b72a14ae4..38253e65a 100644 --- a/gui-js/libs/shared/src/lib/constants/constants.ts +++ b/gui-js/libs/shared/src/lib/constants/constants.ts @@ -3,6 +3,10 @@ export const rendererAppURL = `http://localhost:${rendererAppPort}`; export const rendererAppName = 'minsky-web'; export const electronAppName = 'minsky-electron'; export const backgroundColor = '#c1c1c1'; + +// Clerk publishable key — used in both the Angular renderer and the Electron main process. +// The frontendApi hostname is base64-encoded in the third segment of the key. +export const CLERK_PUBLISHABLE_KEY = 'pk_test_cG9zaXRpdmUtcGhvZW5peC04NS5jbGVyay5hY2NvdW50cy5kZXYk'; export const updateServerUrl = 'https://deployment-server-url.com'; // TODO: insert your update server url here export const defaultBackgroundColor = '#ffffff'; diff --git a/gui-js/libs/ui-components/src/lib/login/login.component.html b/gui-js/libs/ui-components/src/lib/login/login.component.html index 50e5da495..d3c8bf3fd 100644 --- a/gui-js/libs/ui-components/src/lib/login/login.component.html +++ b/gui-js/libs/ui-components/src/lib/login/login.component.html @@ -4,27 +4,13 @@ -
+

You are signed in.

- + -
diff --git a/gui-js/libs/ui-components/src/lib/login/login.component.scss b/gui-js/libs/ui-components/src/lib/login/login.component.scss index 3c021cd99..c1427f112 100644 --- a/gui-js/libs/ui-components/src/lib/login/login.component.scss +++ b/gui-js/libs/ui-components/src/lib/login/login.component.scss @@ -17,4 +17,4 @@ .signed-in-message { margin-bottom: 16px; -} +} \ No newline at end of file diff --git a/gui-js/libs/ui-components/src/lib/login/login.component.ts b/gui-js/libs/ui-components/src/lib/login/login.component.ts index 46b6badf9..be8b96c41 100644 --- a/gui-js/libs/ui-components/src/lib/login/login.component.ts +++ b/gui-js/libs/ui-components/src/lib/login/login.component.ts @@ -1,9 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatInputModule } from '@angular/material/input'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ClerkService } from '@minsky/core'; import { ElectronService } from '@minsky/core'; @@ -17,20 +14,14 @@ import { take } from 'rxjs'; standalone: true, imports: [ CommonModule, - FormsModule, MatButtonModule, - MatFormFieldModule, - MatInputModule, MatProgressSpinnerModule, ], }) export class LoginComponent implements OnInit { isLoading = true; - isSigningIn = false; errorMessage = ''; isAuthenticated = false; - email = ''; - password = ''; constructor( private clerkService: ClerkService, @@ -66,20 +57,6 @@ export class LoginComponent implements OnInit { } } - async onSignIn() { - this.isSigningIn = true; - this.errorMessage = ''; - try { - await this.clerkService.signInWithEmailPassword(this.email, this.password); - this.isAuthenticated = true; - this.electronService.closeWindow(); - } catch (err: any) { - this.errorMessage = err?.message ?? 'Sign in failed.'; - } finally { - this.isSigningIn = false; - } - } - async onSignOut() { this.isLoading = true; this.errorMessage = ''; From d89b32ae09213d6093bfd29443086ac0f32bcf4a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 02:17:00 +0000 Subject: [PATCH 12/13] fix: use accounts.clerk.com with __publishable_key param (no hostname derivation) Agent-Logs-Url: https://github.com/highperformancecoder/minsky/sessions/7a238887-8850-4b05-95ab-e2adf8d60497 Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com> --- .../src/app/managers/WindowManager.ts | 34 ++++++------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/gui-js/apps/minsky-electron/src/app/managers/WindowManager.ts b/gui-js/apps/minsky-electron/src/app/managers/WindowManager.ts index 38c7c4549..adfeded7e 100644 --- a/gui-js/apps/minsky-electron/src/app/managers/WindowManager.ts +++ b/gui-js/apps/minsky-electron/src/app/managers/WindowManager.ts @@ -386,31 +386,19 @@ export class WindowManager { } static async openLoginWindow(): Promise { - // Derive the Clerk frontendApi hostname from the publishable key. - // Key format: pk__ - const encoded = CLERK_PUBLISHABLE_KEY.split('_')[2] ?? ''; - const padded = encoded + '='.repeat((4 - (encoded.length % 4)) % 4); - let frontendApi: string; - try { - frontendApi = Buffer.from(padded, 'base64').toString('utf8').replace(/\$$/, ''); - } catch { - log.error('WindowManager.openLoginWindow: invalid Clerk publishable key'); - return Promise.resolve(null); - } - - // Open Clerk's hosted sign-in page in a dedicated BrowserWindow. - // Because this window loads from HTTPS (not file://), Clerk's CDN - // resources and React UI components load normally — the full standard - // Clerk sign-in UI is displayed, including every configured OAuth provider. - // This mirrors the approach used by @clerk/electron without requiring that - // package. + // Open Clerk's Accounts Portal sign-in page in a dedicated BrowserWindow. + // Passing __publishable_key tells Clerk which app to authenticate against — + // no hostname derivation required. Because this window loads from HTTPS + // (not file://), Clerk's CDN resources and React UI components load + // normally — the full standard Clerk sign-in UI is displayed, including + // every configured OAuth provider. // - // After successful sign-in, Clerk redirects to redirect_url ('minsky://signed-in'). - // We intercept that navigation with will-navigate, execute JS in the still-live - // sign-in page to obtain a JWT from window.Clerk.session.getToken(), stash it, - // and close the window. + // After successful sign-in, Clerk redirects to redirect_url + // ('minsky://signed-in'). We intercept that navigation with will-navigate, + // execute JS in the still-live sign-in page to obtain a JWT from + // window.Clerk.session.getToken(), stash it, and close the window. const redirectUrl = 'minsky://signed-in'; - const signInUrl = `https://${frontendApi}/sign-in?redirect_url=${encodeURIComponent(redirectUrl)}`; + const signInUrl = `https://accounts.clerk.com/sign-in?__publishable_key=${encodeURIComponent(CLERK_PUBLISHABLE_KEY)}&redirect_url=${encodeURIComponent(redirectUrl)}`; return new Promise((resolve) => { const loginWindow = new BrowserWindow({ From b5c3c412cbd075dec142ea6b8725d2772a31639d Mon Sep 17 00:00:00 2001 From: Russell Standish Date: Tue, 14 Apr 2026 08:54:35 +1000 Subject: [PATCH 13/13] Stash working dir before travel --- .../app/managers/ApplicationMenuManager.ts | 8 ++-- .../src/app/managers/CommandsManager.ts | 9 ++++ .../src/app/managers/WindowManager.ts | 47 +++++++++++++++++-- 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts b/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts index 176009361..5fd2c5594 100644 --- a/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts +++ b/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts @@ -132,10 +132,12 @@ export class ApplicationMenuManager { click() {CommandsManager.upgradeUsingClerk();}, }, { - label: 'Manage Clerk Session', - click() {WindowManager.openLoginWindow();}, + label: 'Logout Clerk Session', + click() { + CommandsManager.stashClerkToken(null); + WindowManager.clerkLogout(); + }, }, - { label: 'New System', accelerator: 'CmdOrCtrl + Shift + N', diff --git a/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts b/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts index 786d9950d..1e74dd25a 100644 --- a/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts +++ b/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts @@ -1501,6 +1501,7 @@ export class CommandsManager { } static async upgradeUsingClerk(installCase: InstallCase=InstallCase.theLot) { + console.log('authToken:',StoreManager.store.get('authToken')); while (!StoreManager.store.get('authToken')) if (!await WindowManager.openLoginWindow()) return; @@ -1540,4 +1541,12 @@ export class CommandsManager { } + static clerkLogout() { + console.log(WindowManager.clerkApi()); + net.fetch(`https://${WindowManager.clerkApi()}/sign-out`); + CommandsManager.stashClerkToken(null); + } + + + } diff --git a/gui-js/apps/minsky-electron/src/app/managers/WindowManager.ts b/gui-js/apps/minsky-electron/src/app/managers/WindowManager.ts index adfeded7e..387f6306e 100644 --- a/gui-js/apps/minsky-electron/src/app/managers/WindowManager.ts +++ b/gui-js/apps/minsky-electron/src/app/managers/WindowManager.ts @@ -385,6 +385,32 @@ export class WindowManager { } } + // Derive the Clerk frontendApi hostname from the publishable key. + // Key format: pk__ + static clerkApi(): string { +// const encoded = CLERK_PUBLISHABLE_KEY.split('_')[2] ?? ''; +// const padded = encoded + '='.repeat((4 - (encoded.length % 4)) % 4); +// return Buffer.from(padded, 'base64').toString('utf8').replace(/\$$/, ''); + return 'positive-phoenix-85.accounts.dev'; + } + + static clerkLogout() { + const loginWindow = new BrowserWindow({ + width: 480, + height: 640, + title: 'Sign In', + parent: WindowManager.getMainWindow(), + modal: false, + webPreferences: { + contextIsolation: true, + nodeIntegration: false, + }, + icon: __dirname + '/assets/favicon.png', + }); + console.log(`https://${WindowManager.clerkApi()}/sign-out`); + loginWindow.loadURL(`https://${WindowManager.clerkApi()}/sign-out`); + } + static async openLoginWindow(): Promise { // Open Clerk's Accounts Portal sign-in page in a dedicated BrowserWindow. // Passing __publishable_key tells Clerk which app to authenticate against — @@ -397,9 +423,23 @@ export class WindowManager { // ('minsky://signed-in'). We intercept that navigation with will-navigate, // execute JS in the still-live sign-in page to obtain a JWT from // window.Clerk.session.getToken(), stash it, and close the window. - const redirectUrl = 'minsky://signed-in'; - const signInUrl = `https://accounts.clerk.com/sign-in?__publishable_key=${encodeURIComponent(CLERK_PUBLISHABLE_KEY)}&redirect_url=${encodeURIComponent(redirectUrl)}`; + // This is a dummy URL. It needs to be something like this form for Clerk to redirect back to here + const redirectUrl = '/headless/sign-in'; + let frontendApi: string; + try { + frontendApi = WindowManager.clerkApi(); + } catch { + log.error('WindowManager.openLoginWindow: invalid Clerk publishable key'); + return Promise.resolve(null); + } + + //https://positive-phoenix-85.accounts.dev + + const signInUrl = `https://${frontendApi}/sign-in?&redirect_url=${encodeURIComponent(redirectUrl)}`; + //const signInUrl = `https://${frontendApi}/sign-in`; + + console.log(signInUrl); return new Promise((resolve) => { const loginWindow = new BrowserWindow({ width: 480, @@ -418,7 +458,8 @@ export class WindowManager { loginWindow.once('ready-to-show', () => loginWindow.show()); loginWindow.webContents.on('will-navigate', async (event, url) => { - if (url.startsWith('minsky://')) { + console.log('will-navigate',url); + /*if (url.startsWith('https://explore'))*/ { // The sign-in page is about to redirect to our custom scheme, meaning // sign-in completed successfully. Prevent the navigation (the minsky:// // scheme is not registered as a real protocol), then extract the JWT