Skip to content
Merged
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
44 changes: 25 additions & 19 deletions desktop/src/app/App.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { getCurrentWindow } from "@tauri-apps/api/window";
import { useQueryClient } from "@tanstack/react-query";
import { QueryClientProvider } from "@tanstack/react-query";
import { RouterProvider } from "@tanstack/react-router";
import { useCallback, useEffect, useLayoutEffect } from "react";
import {
type ReactNode,
useCallback,
useEffect,
useLayoutEffect,
useState,
} from "react";

import { router } from "@/app/router";
import { UpdaterProvider } from "@/features/settings/hooks/UpdaterProvider";
import { useAppOnboardingState } from "@/features/onboarding/hooks";
import { OnboardingFlow } from "@/features/onboarding/ui/OnboardingFlow";
import { useWorkspaceInit } from "@/features/workspaces/useWorkspaceInit";
import { useWorkspaces } from "@/features/workspaces/useWorkspaces";
import { WelcomeSetup } from "@/features/workspaces/ui/WelcomeSetup";
import { createSproutQueryClient } from "@/shared/api/queryClient";
import { listenForDeepLinks } from "@/shared/deep-link";

function AppLoadingGate() {
Expand All @@ -30,6 +36,14 @@ function AppLoadingGate() {
);
}

function WorkspaceQueryProvider({ children }: { children: ReactNode }) {
const [queryClient] = useState(createSproutQueryClient);

return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}

function AppReady() {
const onboarding = useAppOnboardingState();

Expand All @@ -55,7 +69,6 @@ export function App() {
void getCurrentWindow().show();
}, []);

const queryClient = useQueryClient();
const {
activeWorkspace,
reinitKey,
Expand All @@ -74,19 +87,10 @@ export function App() {
void unlisten.then((fn) => fn());
};
}, [addWorkspace, switchWorkspace, reconnectWorkspace]);
const workspace = useWorkspaceInit(activeWorkspace);

// Composite key: changes when workspace ID changes OR when
// the active workspace's config is updated (relayUrl/token).
const workspaceKey = `${activeWorkspace?.id ?? "none"}-${reinitKey}`;

// Clear stale React Query cache synchronously when workspace changes.
// useLayoutEffect fires before child useEffect hooks, preventing stale
// data from being served to the new workspace's components.
// biome-ignore lint/correctness/useExhaustiveDependencies: workspaceKey drives the re-run intentionally
useLayoutEffect(() => {
queryClient.clear();
}, [workspaceKey, queryClient]);
const workspace = useWorkspaceInit(activeWorkspace, workspaceKey);

const handleSetupComplete = useCallback(() => {
// Force a full reload so useWorkspaces re-initializes from localStorage.
Expand All @@ -104,15 +108,17 @@ export function App() {
);
}

// Wait for workspace config to be applied to the backend before
// rendering anything that connects to the relay.
if (!workspace.isReady) {
// Wait for this exact workspace config to be applied to the backend before
// rendering anything that connects to the relay. The appliedKey check avoids
// a one-render race where React sees the new active workspace while the Tauri
// backend is still configured for the previous one.
if (!workspace.isReady || workspace.appliedKey !== workspaceKey) {
return <AppLoadingGate />;
}

return (
<UpdaterProvider>
<WorkspaceQueryProvider key={workspaceKey}>
<AppReady key={workspaceKey} />
</UpdaterProvider>
</WorkspaceQueryProvider>
);
}
32 changes: 25 additions & 7 deletions desktop/src/features/workspaces/useWorkspaceInit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ function resetWorkspaceState(): void {
}

type WorkspaceInitResult =
| { isReady: true; needsSetup: false }
| { isReady: true; needsSetup: false; appliedKey: string }
| {
isReady: false;
needsSetup: true;
defaultRelayUrl: string;
}
| { isReady: false; needsSetup: false };
| { isReady: false; needsSetup: false; appliedKey: string | null };

/**
* Applies the active workspace config to the Tauri backend and resets
Expand All @@ -42,10 +42,12 @@ type WorkspaceInitResult =
*/
export function useWorkspaceInit(
activeWorkspace: Workspace | null,
workspaceKey: string,
): WorkspaceInitResult {
const [result, setResult] = useState<WorkspaceInitResult>({
isReady: false,
needsSetup: false,
appliedKey: null,
});

// Track whether this is the initial mount or a workspace switch.
Expand Down Expand Up @@ -80,16 +82,23 @@ export function useWorkspaceInit(
return;
}

// Mark this workspace config as pending while it is applied to the
// backend. App.tsx also checks appliedKey against the active workspaceKey,
// which prevents rendering workspace-scoped UI for a new workspace until
// that exact config has finished applying.
setResult({
isReady: false,
needsSetup: false,
appliedKey: workspaceKey,
});

// On workspace switch (not initial mount), reset module singletons
// so the new tree starts with a clean slate.
if (hasInitializedRef.current) {
resetWorkspaceState();
}
hasInitializedRef.current = true;

// Show loading gate while we apply the new workspace config
setResult({ isReady: false, needsSetup: false });

// Apply workspace config to the Tauri backend.
//
// Note: we deliberately do NOT pass an nsec here. The persisted
Expand All @@ -110,7 +119,11 @@ export function useWorkspaceInit(
}

if (!cancelled) {
setResult({ isReady: true, needsSetup: false });
setResult({
isReady: true,
needsSetup: false,
appliedKey: workspaceKey,
});
}
}

Expand All @@ -119,7 +132,12 @@ export function useWorkspaceInit(
return () => {
cancelled = true;
};
}, [activeWorkspace?.id, activeWorkspace?.relayUrl, activeWorkspace?.token]);
}, [
activeWorkspace?.id,
activeWorkspace?.relayUrl,
activeWorkspace?.token,
workspaceKey,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude reinit-only key from workspace reset trigger

Adding workspaceKey to this effect’s dependencies makes every reconnectWorkspace() bump run resetWorkspaceState(), even when the active workspace is unchanged. In the deep-link flow, reconnectWorkspace() is always called after connect handling, so reconnecting to an already-active relay now clears module caches such as drafts (clearAllDrafts) and search hits for the same workspace, which is user-visible data loss rather than cross-workspace isolation.

Useful? React with 👍 / 👎.

]);

return result;
}
34 changes: 10 additions & 24 deletions desktop/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { App } from "@/app/App";
import "@/shared/styles/globals.css";
import { UpdaterProvider } from "@/features/settings/hooks/UpdaterProvider";
import { WorkspacesProvider } from "@/features/workspaces/useWorkspaces";
import { ThemeProvider } from "@/shared/theme/ThemeProvider";
import { Toaster } from "@/shared/ui/sonner";
Expand All @@ -12,33 +12,19 @@ type E2eWindow = Window & {
__SPROUT_E2E__?: unknown;
};

const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1,
refetchOnWindowFocus: false,
networkMode: "always",
gcTime: 5 * 60 * 1_000,
},
mutations: {
networkMode: "always",
},
},
});

function renderApp() {
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<WorkspacesProvider>
<ThemeProvider defaultTheme="houston">
<TooltipProvider delayDuration={300}>
<WorkspacesProvider>
<ThemeProvider defaultTheme="houston">
<TooltipProvider delayDuration={300}>
<UpdaterProvider>
<App />
<Toaster />
</TooltipProvider>
</ThemeProvider>
</WorkspacesProvider>
</QueryClientProvider>
</UpdaterProvider>
<Toaster />
</TooltipProvider>
</ThemeProvider>
</WorkspacesProvider>
</React.StrictMode>,
);
}
Expand Down
17 changes: 17 additions & 0 deletions desktop/src/shared/api/queryClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { QueryClient } from "@tanstack/react-query";

export function createSproutQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
retry: 1,
refetchOnWindowFocus: false,
networkMode: "always",
gcTime: 5 * 60 * 1_000,
},
mutations: {
networkMode: "always",
},
},
});
}
Loading