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
9 changes: 9 additions & 0 deletions apps/desktop/src/lib/trpc/routers/projects/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,15 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => {
return resolveDefaultEditor(input.projectId);
}),

hasAny: publicProcedure.query(() => {
const row = localDb
.select({ id: projects.id })
.from(projects)
.limit(1)
.all();
return row.length > 0;
}),

getRecents: publicProcedure.query((): Project[] => {
return localDb
.select()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ export const createQueryProcedures = () => {
.sort((a, b) => a.tabOrder - b.tabOrder);
}),

hasAny: publicProcedure.query(() => {
const row = localDb
.select({ id: workspaces.id })
.from(workspaces)
.limit(1)
.all();
return row.length > 0;
}),
Comment thread
coderabbitai[bot] marked this conversation as resolved.

getAllGrouped: publicProcedure.query(() => {
type WorkspaceItem = {
id: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useEffect } from "react";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { useV2LocalOverrideStore } from "renderer/stores/v2-local-override";

export function V2DefaultResolver() {
const optInV2 = useV2LocalOverrideStore((s) => s.optInV2);
const isFreshInstall = useV2LocalOverrideStore((s) => s.isFreshInstall);
const setOptInV2 = useV2LocalOverrideStore((s) => s.setOptInV2);
const setIsFreshInstall = useV2LocalOverrideStore((s) => s.setIsFreshInstall);
const utils = electronTrpc.useUtils();

useEffect(() => {
if (optInV2 !== null && isFreshInstall !== null) return;
let cancelled = false;
void Promise.all([
utils.workspaces.hasAny.fetch(),
utils.projects.hasAny.fetch(),
]).then(([hasWorkspace, hasProject]) => {
if (cancelled) return;
const isFresh = !hasWorkspace && !hasProject;
const current = useV2LocalOverrideStore.getState();
if (current.optInV2 === null) setOptInV2(isFresh);
if (current.isFreshInstall === null) setIsFreshInstall(isFresh);
});
Comment on lines +15 to +24
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle resolver fetch failures to avoid unhandled rejections.

If either hasAny.fetch() rejects, the promise chain is unhandled and the resolver silently never sets a value for this run.

Suggested fix
 		void Promise.all([
 			utils.workspaces.hasAny.fetch(),
 			utils.projects.hasAny.fetch(),
-		]).then(([hasWorkspace, hasProject]) => {
-			if (cancelled) return;
-			if (useV2LocalOverrideStore.getState().optInV2 !== null) return;
-			setOptInV2(!hasWorkspace && !hasProject);
-		});
+		])
+			.then(([hasWorkspace, hasProject]) => {
+				if (cancelled) return;
+				if (useV2LocalOverrideStore.getState().optInV2 !== null) return;
+				setOptInV2(!hasWorkspace && !hasProject);
+			})
+			.catch(() => {
+				// Keep unresolved state on failure; retry on next mount.
+			});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void Promise.all([
utils.workspaces.hasAny.fetch(),
utils.projects.hasAny.fetch(),
]).then(([hasWorkspace, hasProject]) => {
if (cancelled) return;
if (useV2LocalOverrideStore.getState().optInV2 !== null) return;
setOptInV2(!hasWorkspace && !hasProject);
});
void Promise.all([
utils.workspaces.hasAny.fetch(),
utils.projects.hasAny.fetch(),
])
.then(([hasWorkspace, hasProject]) => {
if (cancelled) return;
if (useV2LocalOverrideStore.getState().optInV2 !== null) return;
setOptInV2(!hasWorkspace && !hasProject);
})
.catch(() => {
// Keep unresolved state on failure; retry on next mount.
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/components/V2DefaultResolver/V2DefaultResolver.tsx`
around lines 13 - 20, The Promise.all([...].then(...)) call can reject and cause
an unhandled rejection; wrap the fetches with error handling by appending a
.catch handler (or convert to async/await with try/catch) around
Promise.all(utils.workspaces.hasAny.fetch(), utils.projects.hasAny.fetch()) so
any rejection is caught, log or report the error, and still call setOptInV2 with
a safe default (e.g., false) unless cancelled; ensure you check cancelled and
useV2LocalOverrideStore.getState().optInV2 in both the success .then and the
.catch paths so the resolver always sets a value or exits cleanly.

Comment on lines +15 to +24
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Unhandled rejection leaves optInV2 permanently null

The Promise.all is prefixed with void, which silences the unhandled-rejection warning but means a tRPC error (e.g., IPC hiccup on startup) causes both hasAny calls to throw, the .then callback never runs, and optInV2 stays null forever for that session. Because useIsV2CloudEnabled treats null as false, a fresh-install user would see v1 indefinitely and never get the intended v2 default — even after the IPC channel recovers — until they manually restart or toggle.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/components/V2DefaultResolver/V2DefaultResolver.tsx
Line: 13-20

Comment:
**Unhandled rejection leaves `optInV2` permanently `null`**

The `Promise.all` is prefixed with `void`, which silences the unhandled-rejection warning but means a tRPC error (e.g., IPC hiccup on startup) causes both `hasAny` calls to throw, the `.then` callback never runs, and `optInV2` stays `null` forever for that session. Because `useIsV2CloudEnabled` treats `null` as `false`, a fresh-install user would see v1 indefinitely and never get the intended v2 default — even after the IPC channel recovers — until they manually restart or toggle.

How can I resolve this? If you propose a fix, please make it concise.

return () => {
cancelled = true;
};
}, [optInV2, isFreshInstall, setOptInV2, setIsFreshInstall, utils]);

return null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { V2DefaultResolver } from "./V2DefaultResolver";
6 changes: 6 additions & 0 deletions apps/desktop/src/renderer/hooks/useIsFreshInstall.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { useV2LocalOverrideStore } from "renderer/stores/v2-local-override";

/** Returns whether this install was empty when first detected, or null if still resolving. */
export function useIsFreshInstall(): boolean | null {
return useV2LocalOverrideStore((s) => s.isFreshInstall);
}
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/hooks/useIsV2CloudEnabled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import { useV2LocalOverrideStore } from "renderer/stores/v2-local-override";

/** Returns whether v2 is currently active for this user. */
export function useIsV2CloudEnabled(): boolean {
return useV2LocalOverrideStore((s) => s.optInV2);
return useV2LocalOverrideStore((s) => s.optInV2 === true);
}
9 changes: 0 additions & 9 deletions apps/desktop/src/renderer/lib/hasPriorSupersetUsage.ts

This file was deleted.

2 changes: 2 additions & 0 deletions apps/desktop/src/renderer/routes/-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { PostHogSurfaceTagger } from "renderer/components/PostHogSurfaceTagger";
import { PostHogUserIdentifier } from "renderer/components/PostHogUserIdentifier";
import { TelemetrySync } from "renderer/components/TelemetrySync";
import { ThemedToaster } from "renderer/components/ThemedToaster";
import { V2DefaultResolver } from "renderer/components/V2DefaultResolver";
import { AuthProvider } from "renderer/providers/AuthProvider";
import { ElectronTRPCProvider } from "renderer/providers/ElectronTRPCProvider";
import { PostHogProvider } from "renderer/providers/PostHogProvider";
Expand All @@ -12,6 +13,7 @@ export function RootLayout({ children }: { children: ReactNode }) {
return (
<PostHogProvider>
<ElectronTRPCProvider>
<V2DefaultResolver />
<PostHogUserIdentifier />
<PostHogSurfaceTagger />
<TelemetrySync />
Expand Down
9 changes: 8 additions & 1 deletion apps/desktop/src/renderer/routes/_authenticated/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { NewWorkspaceModal } from "renderer/components/NewWorkspaceModal";
import { Paywall } from "renderer/components/Paywall";
import { useUpdateListener } from "renderer/components/UpdateToast";
import { env } from "renderer/env.renderer";
import { useIsFreshInstall } from "renderer/hooks/useIsFreshInstall";
import { useIsV2CloudEnabled } from "renderer/hooks/useIsV2CloudEnabled";
import { useOnlineStatus } from "renderer/hooks/useOnlineStatus";
import { authClient, getAuthToken } from "renderer/lib/auth-client";
Expand Down Expand Up @@ -65,6 +66,7 @@ function AuthenticatedLayout() {
const utils = electronTrpc.useUtils();
const shownWorkspaceInitWarningsRef = useRef(new Set<string>());
const isV2CloudEnabled = useIsV2CloudEnabled();
const isFreshInstall = useIsFreshInstall();
const requiredComplete = useOnboardingStore(selectRequiredStepsComplete);
const firstIncompleteStep = useOnboardingStore(selectFirstIncompleteStep);

Expand Down Expand Up @@ -205,7 +207,12 @@ function AuthenticatedLayout() {
}

const isOnSetupRoute = location.pathname.startsWith("/setup");
if (isV2CloudEnabled && !requiredComplete && !isOnSetupRoute) {
if (
isV2CloudEnabled &&
isFreshInstall === true &&
!requiredComplete &&
!isOnSetupRoute
) {
return <Navigate to={STEP_ROUTES[firstIncompleteStep]} replace />;
}

Expand Down
15 changes: 6 additions & 9 deletions apps/desktop/src/renderer/stores/v2-local-override.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
import { hasPriorSupersetUsage } from "renderer/lib/hasPriorSupersetUsage";
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";

interface V2LocalOverrideState {
/** When true, the user has opted into v2. v2 is gated behind both the remote flag and this opt-in. */
optInV2: boolean;
optInV2: boolean | null;
isFreshInstall: boolean | null;
setOptInV2: (optInV2: boolean) => void;
setIsFreshInstall: (isFreshInstall: boolean) => void;
}

// Fresh installs default to v2; returning v1 users default to v1 and discover
// v2 via the in-sidebar banner. Persist hydration overrides this for anyone
// with a saved override.
const initialOptInV2 = !hasPriorSupersetUsage();

export const useV2LocalOverrideStore = create<V2LocalOverrideState>()(
devtools(
persist(
(set) => ({
optInV2: initialOptInV2,
optInV2: null,
isFreshInstall: null,
setOptInV2: (optInV2) => set({ optInV2 }),
setIsFreshInstall: (isFreshInstall) => set({ isFreshInstall }),
}),
{ name: "v2-local-override-v2" },
),
Expand Down
Loading