Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions apps/desktop/src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { UpdateNotification } from "./components/update-notification";
function AppContent() {
const user = useAuthStore((s) => s.user);
const isLoading = useAuthStore((s) => s.isLoading);
const workspaceHydrated = useWorkspaceStore((s) => s.workspaceHydrated);
// Deep-link login runs loginWithToken → syncToken → listWorkspaces →
// hydrateWorkspace sequentially. loginWithToken sets user+isLoading=false
// as soon as getMe resolves, which would cause DesktopShell to mount
Expand Down Expand Up @@ -72,6 +73,20 @@ function AppContent() {
}

if (!user) return <DesktopLoginPage />;

// Wait for workspace hydration before mounting DesktopShell so that
// OnboardingGate gets a definitive hasWorkspace value on first render.
// Without this, OTP login sets `user` (triggering this branch) before
// listWorkspaces + hydrateWorkspace complete, causing OnboardingGate
// to freeze with hasWorkspace=false even when the user has a workspace.
if (!workspaceHydrated) {
return (
<div className="flex h-screen items-center justify-center">
<MulticaIcon className="size-6 animate-pulse" />
</div>
);
}

return <DesktopShell />;
}

Expand Down
9 changes: 6 additions & 3 deletions packages/core/platform/auth-initializer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@ export function AuthInitializer({
Promise.all([api.getMe(), api.listWorkspaces()])
.then(([user, wsList]) => {
onLogin?.();
useAuthStore.setState({ user, isLoading: false });
// Hydrate workspace BEFORE flipping isLoading — AppContent gates
// on isLoading, so workspace must be ready when it re-renders.
qc.setQueryData(workspaceKeys.list(), wsList);
useWorkspaceStore.getState().hydrateWorkspace(wsList, wsId);
useAuthStore.setState({ user, isLoading: false });
})
.catch((err) => {
logger.error("cookie auth init failed", err);
Expand All @@ -68,10 +70,11 @@ export function AuthInitializer({
Promise.all([api.getMe(), api.listWorkspaces()])
.then(([user, wsList]) => {
onLogin?.();
useAuthStore.setState({ user, isLoading: false });
// Seed React Query cache so components don't need a second fetch
// Hydrate workspace BEFORE flipping isLoading — AppContent gates
// on isLoading, so workspace must be ready when it re-renders.
qc.setQueryData(workspaceKeys.list(), wsList);
useWorkspaceStore.getState().hydrateWorkspace(wsList, wsId);
useAuthStore.setState({ user, isLoading: false });
})
.catch((err) => {
logger.error("auth init failed", err);
Expand Down
9 changes: 6 additions & 3 deletions packages/core/workspace/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ interface WorkspaceStoreOptions {

interface WorkspaceState {
workspace: Workspace | null;
/** True once `hydrateWorkspace` has run (even if no workspace was found). */
workspaceHydrated: boolean;
}

interface WorkspaceActions {
Expand Down Expand Up @@ -39,6 +41,7 @@ export function createWorkspaceStore(api: ApiClient, options?: WorkspaceStoreOpt
// Only the currently selected workspace (UI state).
// The workspace list is server state and lives in React Query.
workspace: null,
workspaceHydrated: false,

hydrateWorkspace: (wsList, preferredWorkspaceId) => {
const nextWorkspace =
Expand All @@ -53,15 +56,15 @@ export function createWorkspaceStore(api: ApiClient, options?: WorkspaceStoreOpt
setCurrentWorkspaceId(null);
rehydrateAllWorkspaceStores();
storage?.removeItem("multica_workspace_id");
set({ workspace: null });
set({ workspace: null, workspaceHydrated: true });
return null;
}

api.setWorkspaceId(nextWorkspace.id);
setCurrentWorkspaceId(nextWorkspace.id);
rehydrateAllWorkspaceStores();
storage?.setItem("multica_workspace_id", nextWorkspace.id);
set({ workspace: nextWorkspace });
set({ workspace: nextWorkspace, workspaceHydrated: true });
logger.debug("hydrate workspace", nextWorkspace.name, nextWorkspace.id);

return nextWorkspace;
Expand All @@ -86,7 +89,7 @@ export function createWorkspaceStore(api: ApiClient, options?: WorkspaceStoreOpt
api.setWorkspaceId(null);
setCurrentWorkspaceId(null);
rehydrateAllWorkspaceStores();
set({ workspace: null });
set({ workspace: null, workspaceHydrated: false });
},
}));
}
Loading