diff --git a/src/main.tsx b/src/main.tsx index 04f8fa2e3..e62071098 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -134,10 +134,6 @@ function App() { const [authUsername, setAuthUsername] = useState(stored?.username ?? ""); const [verifyRetryCount, setVerifyRetryCount] = useState(0); const timerRef = useRef | null>(null); - // Track whether fading-in came from a fresh login (vs. session verification on page load). - // When session-verified, Auth must not mount during the transition — it would trigger - // silent OIDC redirect and cause an infinite refresh loop. - const fadingInFromLoginRef = useRef(false); // Dedupes concurrent handleLogout() calls within the same tick -- see // handleLogout for why phase state alone isn't sufficient for this. const loggingOutRef = useRef(false); @@ -175,7 +171,6 @@ function App() { // Non-fatal: WebSocket connections will fall back to cookie auth } } - fadingInFromLoginRef.current = false; setPhase("loading-app"); await Promise.all([ startPluginRuntime() @@ -241,7 +236,6 @@ function App() { loggingOutRef.current = false; clearDesktopManualLogout(); setAuthUsername(u); - fadingInFromLoginRef.current = true; setPhase("loading-app"); void (async () => { await Promise.all([ @@ -299,10 +293,7 @@ function App() { const showApp = phase === "idle-app" || phase === "fading-in" || phase === "fading-out"; - const showAuth = - phase === "idle-auth" || - (phase === "fading-in" && fadingInFromLoginRef.current) || - phase === "fading-out"; + const showAuth = phase === "idle-auth" || phase === "fading-out"; const appOpacity = phase === "idle-app" ? 1 : 0; const authOpacity = phase === "idle-auth" ? 1 : 0; diff --git a/src/ui/plugin-host/loader.ts b/src/ui/plugin-host/loader.ts index ad9fba2a5..d4fdc2fdc 100644 --- a/src/ui/plugin-host/loader.ts +++ b/src/ui/plugin-host/loader.ts @@ -442,13 +442,20 @@ export async function startPreLoginPlugins(): Promise { } catch { return; } - if (list.length === 0) return; + if (list.length === 0 || started) return; setPluginLocaleResolver((namespace, _language, file) => { const summary = getPluginRecord(namespace)?.summary; if (!summary) return Promise.resolve(null); return currentDeps().loadLocale(summary, file); }); - await syncPlugins(list); + await enqueue(async () => { + // Once signed in the full sync owns the list, and reconciling this + // subset would unload everything else. Auth remounts during the + // post-login fade, so this runs after sign-in every time. + if (started) return; + await reconcile(list); + markPluginsSettled(); + }); } /** diff --git a/src/ui/tests/plugin-host/loader.test.tsx b/src/ui/tests/plugin-host/loader.test.tsx index 1b193aebf..afbfa0797 100644 --- a/src/ui/tests/plugin-host/loader.test.tsx +++ b/src/ui/tests/plugin-host/loader.test.tsx @@ -7,6 +7,8 @@ import { isPluginFrontendActive, orderForActivation, resetPluginLoader, + startPluginRuntime, + startPreLoginPlugins, syncPlugins, } from "@/plugin-host/loader"; import { @@ -208,6 +210,42 @@ describe("plugin loader", () => { expect(railIds()).not.toContain("headless-panel"); }); + it("keeps every plugin when the pre-login list lands after sign-in", async () => { + modules.login = railPlugin("login"); + modules.alpha = railPlugin("alpha"); + plugins = [summary("login"), summary("alpha")]; + + let releasePreLogin: (value: Response) => void = () => {}; + vi.spyOn(globalThis, "fetch").mockImplementation( + () => + new Promise((resolve) => { + releasePreLogin = resolve; + }), + ); + + const preLogin = startPreLoginPlugins(); + await startPluginRuntime(); + releasePreLogin(new Response(JSON.stringify([summary("login")]))); + await preLogin; + + expect(isPluginFrontendActive("login")).toBe(true); + expect(isPluginFrontendActive("alpha")).toBe(true); + }); + + it("ignores a pre-login load started after sign-in", async () => { + modules.login = railPlugin("login"); + modules.alpha = railPlugin("alpha"); + plugins = [summary("login"), summary("alpha")]; + vi.spyOn(globalThis, "fetch").mockImplementation( + async () => new Response(JSON.stringify([summary("login")])), + ); + + await startPluginRuntime(); + await startPreLoginPlugins(); + + expect(isPluginFrontendActive("alpha")).toBe(true); + }); + it("settles even when the plugin list cannot be fetched", async () => { vi.spyOn(console, "error").mockImplementation(() => {}); configurePluginLoader({