diff --git a/studio/frontend/src/app/provider.tsx b/studio/frontend/src/app/provider.tsx
index 92783ee2be..cacf318deb 100644
--- a/studio/frontend/src/app/provider.tsx
+++ b/studio/frontend/src/app/provider.tsx
@@ -9,12 +9,13 @@ import {
shouldUseCustomWindowTitlebar,
} from "@/components/tauri/window-titlebar";
import { Toaster } from "@/components/ui/sonner";
+import { getTauriAuthFailure, tauriAutoAuth } from "@/features/auth";
import { useTauriBackend } from "@/hooks/use-tauri-backend";
import { useTauriUpdate } from "@/hooks/use-tauri-update";
import { isTauri } from "@/lib/api-base";
import { useRouterState } from "@tanstack/react-router";
import { ThemeProvider } from "next-themes";
-import { useEffect, useRef, type ReactNode } from "react";
+import { useEffect, useRef, useState, type ReactNode } from "react";
interface AppProviderProps {
children: ReactNode;
@@ -141,6 +142,8 @@ function TauriWrapper({ children }: { children: ReactNode }) {
const hasResized = useRef(false);
const abortRef = useRef(false);
+ const [desktopAuthReady, setDesktopAuthReady] = useState(!isTauri);
+ const [desktopAuthRetry, setDesktopAuthRetry] = useState(0);
// Show the window once the frontend mounts (for pre-running states)
useEffect(() => {
@@ -163,20 +166,56 @@ function TauriWrapper({ children }: { children: ReactNode }) {
return () => { abortRef.current = true; };
}, [status]);
+ useEffect(() => {
+ if (!isTauri) {
+ setDesktopAuthReady(true);
+ return;
+ }
+ if (status !== "running") {
+ setDesktopAuthReady(false);
+ setDesktopAuthRetry(0);
+ return;
+ }
+
+ let disposed = false;
+ setDesktopAuthReady(false);
+ tauriAutoAuth({ force: true }).then((authenticated) => {
+ if (disposed) return;
+ if (authenticated) {
+ setDesktopAuthReady(true);
+ return;
+ }
+ if (!getTauriAuthFailure()) {
+ window.setTimeout(() => {
+ if (!disposed) setDesktopAuthRetry((value) => value + 1);
+ }, 500);
+ }
+ });
+
+ return () => { disposed = true; };
+ }, [status, desktopAuthRetry]);
+
if (!isTauri) return <>{children}>;
- const content = status === "running" ? (
+ const showApp = status === "running" && desktopAuthReady;
+ const startupStatus = status === "running" ? "starting" : status;
+ const startupProgressDetail =
+ status === "running" && !desktopAuthReady
+ ? "Signing in to desktop session..."
+ : progressDetail;
+
+ const content = showApp ? (
<>
{children}
>
) : (
diff --git a/studio/frontend/src/features/auth/tauri-auto-auth.ts b/studio/frontend/src/features/auth/tauri-auto-auth.ts
index d67730d2d6..a0199f9ac3 100644
--- a/studio/frontend/src/features/auth/tauri-auto-auth.ts
+++ b/studio/frontend/src/features/auth/tauri-auto-auth.ts
@@ -15,9 +15,13 @@ type DesktopAuthResponse = {
refresh_token: string;
};
+type TauriAutoAuthOptions = {
+ force?: boolean;
+};
+
// Concurrency guard: multiple route guards can call tauriAutoAuth simultaneously.
// Without this, the first-launch password-change could race with itself.
-let pending: Promise | null = null;
+let pending: { promise: Promise; force: boolean } | null = null;
let lastTauriAuthFailure: string | null = null;
const TAURI_AUTH_FAILURE_FALLBACK =
@@ -49,15 +53,15 @@ function isBackendNotReady(error: unknown): boolean {
return authFailureMessage(error).includes(BACKEND_NOT_READY_MESSAGE);
}
-async function doTauriAutoAuth(): Promise {
+async function doTauriAutoAuth(options: TauriAutoAuthOptions): Promise {
// Desktop must handle password-change state internally in Rust.
- if (hasAuthToken() && !mustChangePassword()) {
+ if (!options.force && hasAuthToken() && !mustChangePassword()) {
clearTauriAuthFailure();
return true;
}
// Try refreshing existing session
- if (hasRefreshToken()) {
+ if (!options.force && hasRefreshToken()) {
const refreshed = await refreshSession();
if (refreshed && hasAuthToken() && !mustChangePassword()) {
clearTauriAuthFailure();
@@ -86,10 +90,17 @@ async function doTauriAutoAuth(): Promise {
* Returns true if authentication succeeded.
* Concurrent calls are coalesced into a single in-flight attempt.
*/
-export function tauriAutoAuth(): Promise {
+export function tauriAutoAuth(
+ options: TauriAutoAuthOptions = {},
+): Promise {
if (!isTauri) return Promise.resolve(false);
- if (!pending) {
- pending = doTauriAutoAuth().finally(() => { pending = null; });
+ const force = options.force === true;
+ if (!pending || (force && !pending.force)) {
+ let promise: Promise;
+ promise = doTauriAutoAuth({ force }).finally(() => {
+ if (pending?.promise === promise) pending = null;
+ });
+ pending = { promise, force };
}
- return pending;
+ return pending.promise;
}