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
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import { useEffect } from "react";
import { useIsV2CloudEnabled } from "renderer/hooks/useIsV2CloudEnabled";
import { posthog } from "renderer/lib/posthog";
import { useV2LocalOverrideStore } from "renderer/stores/v2-local-override";

export function PostHogSurfaceTagger() {
const { isV2CloudEnabled, isRemoteV2Enabled } = useIsV2CloudEnabled();
const optInV2 = useV2LocalOverrideStore((s) => s.optInV2);
const isV2CloudEnabled = useIsV2CloudEnabled();

useEffect(() => {
const surface = isV2CloudEnabled ? "v2" : "v1";
const surface_source = !isRemoteV2Enabled
? "v2-flag-off"
: optInV2
? "opted-in"
: "opted-out";
const surface_source = isV2CloudEnabled ? "opted-in" : "opted-out";

posthog.register({ surface, surface_source });

Expand All @@ -24,7 +18,7 @@ export function PostHogSurfaceTagger() {
surface_ever_v2: true,
});
}
}, [isV2CloudEnabled, isRemoteV2Enabled, optInV2]);
}, [isV2CloudEnabled]);

return null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { SidebarCard } from "@superset/ui/sidebar-card";
import { AnimatePresence, motion } from "framer-motion";
import { useIsV2CloudEnabled } from "renderer/hooks/useIsV2CloudEnabled";
import { track } from "renderer/lib/analytics";
import { useV2AvailableBannerStore } from "renderer/stores/v2-available-banner";
import { useV2LocalOverrideStore } from "renderer/stores/v2-local-override";

export function V2AvailableBanner() {
const isV2CloudEnabled = useIsV2CloudEnabled();
const dismissed = useV2AvailableBannerStore((s) => s.dismissed);
const dismiss = useV2AvailableBannerStore((s) => s.dismiss);
const setOptInV2 = useV2LocalOverrideStore((s) => s.setOptInV2);

function handleSwitch() {
track("surface_toggled", { from: "v1", to: "v2", source: "v1_banner" });
setOptInV2(true);
}

function handleDismiss() {
track("v2_banner_dismissed");
dismiss();
}

return (
<AnimatePresence>
{!dismissed && (
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 8 }}
transition={{ duration: 0.2 }}
className="px-3 pb-2"
>
<SidebarCard
badge="New"
title="Superset v2 is here"
description="The new cloud workspace experience is now available."
actionLabel={isV2CloudEnabled ? undefined : "Switch to v2"}
onAction={isV2CloudEnabled ? undefined : handleSwitch}
onDismiss={handleDismiss}
/>
</motion.div>
)}
</AnimatePresence>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { V2AvailableBanner } from "./V2AvailableBanner";
29 changes: 3 additions & 26 deletions apps/desktop/src/renderer/hooks/useIsV2CloudEnabled.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,6 @@
import { FEATURE_FLAGS } from "@superset/shared/constants";
import { useFeatureFlagEnabled } from "posthog-js/react";
import { useV2LocalOverrideStore } from "renderer/stores/v2-local-override";

const IS_DEV = process.env.NODE_ENV === "development";

/**
* Returns effective v2 state: remote PostHog flag AND local opt-in.
* Also returns the raw remote flag so the toggle can be shown conditionally.
*/
export function useIsV2CloudEnabled() {
const remoteV2Enabled =
useFeatureFlagEnabled(FEATURE_FLAGS.V2_CLOUD) ?? false;
const optInV2 = useV2LocalOverrideStore((s) => s.optInV2);

if (IS_DEV) {
return {
isV2CloudEnabled: optInV2,
isRemoteV2Enabled: true,
};
}

return {
/** The effective value — use this wherever you previously checked the flag directly. */
isV2CloudEnabled: remoteV2Enabled && optInV2,
/** Whether the remote PostHog flag is on (for showing the toggle). */
isRemoteV2Enabled: remoteV2Enabled,
};
/** Returns whether v2 is currently active for this user. */
export function useIsV2CloudEnabled(): boolean {
return useV2LocalOverrideStore((s) => s.optInV2);
}
9 changes: 9 additions & 0 deletions apps/desktop/src/renderer/lib/hasPriorSupersetUsage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* True when this install has been used before. Backed by `tabs-storage`,
* which is written the first time any workspace tab opens. Use this to
* distinguish a fresh install from a returning user.
*/
export function hasPriorSupersetUsage(): boolean {
if (typeof localStorage === "undefined") return false;
return localStorage.getItem("tabs-storage") !== null;
}
Comment on lines +6 to +9
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.

P2 hasPriorSupersetUsage returns false when localStorage is unavailable, silently defaulting users to v2

When localStorage is undefined (e.g., in a test environment or a stripped Electron context), the function returns false, causing initialOptInV2 to be true. In a context where localStorage truly isn't available, the persist middleware will also fail to save, so the opt-in will be ephemeral — the user lands on v2 but their state isn't persisted, meaning the next load starts over at v2 again. Consider returning true (treat as "has prior usage" / stay conservative) when storage is inaccessible, to avoid silently opting users into v2 in degraded environments.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/lib/hasPriorSupersetUsage.ts
Line: 6-9

Comment:
**`hasPriorSupersetUsage` returns `false` when `localStorage` is unavailable, silently defaulting users to v2**

When `localStorage` is `undefined` (e.g., in a test environment or a stripped Electron context), the function returns `false`, causing `initialOptInV2` to be `true`. In a context where `localStorage` truly isn't available, the persist middleware will also fail to save, so the opt-in will be ephemeral — the user lands on v2 but their state isn't persisted, meaning the next load starts over at v2 again. Consider returning `true` (treat as "has prior usage" / stay conservative) when storage is inaccessible, to avoid silently opting users into v2 in degraded environments.

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

Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { useMatchRoute, useNavigate } from "@tanstack/react-router";
import { memo, useCallback, useEffect, useMemo, useState } from "react";
import { createPortal } from "react-dom";
import { HiOutlineCog6Tooth } from "react-icons/hi2";
import { V2AvailableBanner } from "renderer/components/V2AvailableBanner";
import { useHotkeyDisplay } from "renderer/hotkeys";
import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState";
import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider";
Expand Down Expand Up @@ -236,6 +237,7 @@ export function DashboardSidebar({
projectName={activeV2Project.name}
/>
)}
{!isCollapsed && <V2AvailableBanner />}
<div
className={cn(
"border-t border-border",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function TopBar() {
{ enabled: !!workspaceId && !isV2WorkspaceRoute },
);
const isOnline = useOnlineStatus();
const { isV2CloudEnabled } = useIsV2CloudEnabled();
const isV2CloudEnabled = useIsV2CloudEnabled();
const isSidebarOpen = useWorkspaceSidebarStore((s) => s.isOpen);
const isSidebarCollapsed = useWorkspaceSidebarStore((s) => s.isCollapsed());
// Default to Mac layout while loading to avoid overlap with traffic lights
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const Route = createFileRoute("/_authenticated/_dashboard")({
function DashboardLayout() {
const navigate = useNavigate();
const openNewWorkspaceModal = useOpenNewWorkspaceModal();
const { isV2CloudEnabled } = useIsV2CloudEnabled();
const isV2CloudEnabled = useIsV2CloudEnabled();
useDevSeedV2Sidebar();
useMigrateV1DataToV2();
// Get current workspace from route to pre-select project in new workspace modal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface PropertiesSidebarProps {

export function PropertiesSidebar({ task }: PropertiesSidebarProps) {
const labels = task.labels ?? [];
const { isV2CloudEnabled } = useIsV2CloudEnabled();
const isV2CloudEnabled = useIsV2CloudEnabled();

return (
<div className="w-64 border-l border-border shrink-0">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export function TasksTopBar({
const selectedCount = selectedTasks.length;
const searchInputRef = useRef<HTMLInputElement>(null);
const [isCreateTaskOpen, setIsCreateTaskOpen] = useState(false);
const { isV2CloudEnabled } = useIsV2CloudEnabled();
const isV2CloudEnabled = useIsV2CloudEnabled();

useHotkey(
"FOCUS_TASK_SEARCH",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export function useMigrateV1DataToV2({
} = {}) {
const { data: session } = authClient.useSession();
const { activeHostUrl } = useLocalHostService();
const { isV2CloudEnabled } = useIsV2CloudEnabled();
const isV2CloudEnabled = useIsV2CloudEnabled();
const collections = useCollections();
const isRunning = useSyncExternalStore(
subscribeMigrationRunning,
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/routes/_authenticated/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ function AuthenticatedLayout() {
const setOriginRoute = useSettingsStore((s) => s.setOriginRoute);
const utils = electronTrpc.useUtils();
const shownWorkspaceInitWarningsRef = useRef(new Set<string>());
const { isV2CloudEnabled } = useIsV2CloudEnabled();
const isV2CloudEnabled = useIsV2CloudEnabled();
const requiredComplete = useOnboardingStore(selectRequiredStepsComplete);
const firstIncompleteStep = useOnboardingStore(selectFirstIncompleteStep);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function AgentsSettings({
visibleItems,
initialAgentPresetId,
}: AgentsSettingsProps) {
const { isV2CloudEnabled } = useIsV2CloudEnabled();
const isV2CloudEnabled = useIsV2CloudEnabled();
if (isV2CloudEnabled) {
return <V2AgentsSettings initialAgentPresetId={initialAgentPresetId} />;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const Route = createFileRoute("/_authenticated/settings/behavior/")({

function BehaviorSettingsPage() {
const searchQuery = useSettingsSearchQuery();
const { isV2CloudEnabled } = useIsV2CloudEnabled();
const isV2CloudEnabled = useIsV2CloudEnabled();

const visibleItems = useMemo(
() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ export function GeneralSettings({ matchCounts }: GeneralSettingsProps) {
const matchRoute = useMatchRoute();
const { data: platform } = electronTrpc.window.getPlatform.useQuery();
const isMac = platform === "darwin";
const { isV2CloudEnabled } = useIsV2CloudEnabled();
const isV2CloudEnabled = useIsV2CloudEnabled();
const allowedSections = useMemo(
() => getAllowedSectionsForVariant(isV2CloudEnabled),
[isV2CloudEnabled],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function SettingsSidebar() {
const searchQuery = useSettingsSearchQuery();
const setSearchQuery = useSetSettingsSearchQuery();
const originRoute = useSettingsOriginRoute();
const { isV2CloudEnabled } = useIsV2CloudEnabled();
const isV2CloudEnabled = useIsV2CloudEnabled();
const normalizedSearchQuery = searchQuery.trim();
const matchCounts = normalizedSearchQuery
? getVisibleMatchCountBySection(normalizedSearchQuery, isV2CloudEnabled)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export function ExperimentalSettings({
SETTING_ITEM_ID.EXPERIMENTAL_RESTART_ONBOARDING,
visibleItems,
);
const { isV2CloudEnabled, isRemoteV2Enabled } = useIsV2CloudEnabled();
const isV2CloudEnabled = useIsV2CloudEnabled();
const { rerun, isRunning } = useMigrateV1DataToV2({ autoRun: false });
const setOptInV2 = useV2LocalOverrideStore((state) => state.setOptInV2);
const resetOnboarding = useOnboardingStore((state) => state.reset);
Expand Down Expand Up @@ -98,25 +98,19 @@ export function ExperimentalSettings({
Try Superset v2
</Label>
<p className="text-xs text-muted-foreground">
Use the new workspace experience when early access is available.
Use the new workspace experience.
</p>
{!isRemoteV2Enabled && (
<p className="text-xs text-muted-foreground">
Early access is not enabled for this account.
</p>
)}
</div>
<Switch
id="superset-v2"
checked={isV2CloudEnabled}
onCheckedChange={(enabled) => {
track("surface_toggled", {
from: isV2CloudEnabled ? "v2" : "v1",
to: enabled && isRemoteV2Enabled ? "v2" : "v1",
to: enabled ? "v2" : "v1",
});
setOptInV2(enabled);
}}
disabled={!isRemoteV2Enabled}
/>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const Route = createFileRoute("/_authenticated/settings/experimental/")({

function ExperimentalSettingsPage() {
const searchQuery = useSettingsSearchQuery();
const { isV2CloudEnabled } = useIsV2CloudEnabled();
const isV2CloudEnabled = useIsV2CloudEnabled();

const visibleItems = useMemo(
() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const Route = createFileRoute("/_authenticated/settings/git/")({

function GitSettingsPage() {
const searchQuery = useSettingsSearchQuery();
const { isV2CloudEnabled } = useIsV2CloudEnabled();
const isV2CloudEnabled = useIsV2CloudEnabled();

const visibleItems = useMemo(
() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const Route = createFileRoute("/_authenticated/settings/links/")({

function LinksSettingsPage() {
const searchQuery = useSettingsSearchQuery();
const { isV2CloudEnabled } = useIsV2CloudEnabled();
const isV2CloudEnabled = useIsV2CloudEnabled();

const visibleItems = useMemo(
() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function TerminalSettings({
pendingCreateProjectId,
onPendingCreateProjectIdChange,
}: TerminalSettingsProps) {
const { isV2CloudEnabled } = useIsV2CloudEnabled();
const isV2CloudEnabled = useIsV2CloudEnabled();
const showPresets = isItemVisible(
SETTING_ITEM_ID.TERMINAL_PRESETS,
visibleItems,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ function TerminalSettingsPage() {
const navigate = Route.useNavigate();
const { editPresetId, createProjectId } = Route.useSearch();
const searchQuery = useSettingsSearchQuery();
const { isV2CloudEnabled } = useIsV2CloudEnabled();
const isV2CloudEnabled = useIsV2CloudEnabled();

const visibleItems = useMemo(
() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function OnboardingAdoptWorktreesPage() {
);

const utils = electronTrpc.useUtils();
const { isV2CloudEnabled } = useIsV2CloudEnabled();
const isV2CloudEnabled = useIsV2CloudEnabled();
const { data: projects, isPending } =
electronTrpc.projects.getRecents.useQuery();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function OnboardingProjectPage() {
electronTrpc.projects.getRecents.useQuery();
const { openNew, isPending: isOpenPending } = useOpenProject();
const utils = electronTrpc.useUtils();
const { isV2CloudEnabled } = useIsV2CloudEnabled();
const isV2CloudEnabled = useIsV2CloudEnabled();
const closeProject = electronTrpc.projects.close.useMutation({
onSuccess: async () => {
await utils.projects.getRecents.invalidate();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo } from "react";
import { V2AvailableBanner } from "renderer/components/V2AvailableBanner";
import { useWorkspaceShortcuts } from "renderer/hooks/useWorkspaceShortcuts";
import { useWorkspaceSelectionStore } from "renderer/stores/workspace-selection";
import { MultiDragPreview } from "./MultiDragPreview";
Expand Down Expand Up @@ -115,6 +116,8 @@ export function WorkspaceSidebar({
projectName={activeProjectName}
/>

{!isCollapsed && <V2AvailableBanner />}

<WorkspaceSidebarFooter isCollapsed={isCollapsed} />
<MultiDragPreview />
</SidebarDropZone>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useV2AvailableBannerStore } from "./store";
20 changes: 20 additions & 0 deletions apps/desktop/src/renderer/stores/v2-available-banner/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";

interface V2AvailableBannerState {
dismissed: boolean;
dismiss: () => void;
}

export const useV2AvailableBannerStore = create<V2AvailableBannerState>()(
devtools(
persist(
(set) => ({
dismissed: false,
dismiss: () => set({ dismissed: true }),
}),
{ name: "v2-available-banner-v1" },
),
{ name: "V2AvailableBannerStore" },
),
);
Comment on lines +9 to +20
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.

P2 Shared dismissed flag hides banner across v1↔v2 transitions

A single dismissed boolean is shared by both the v1 (WorkspaceSidebar) and v2 (DashboardSidebar) banner instances because they both read from the same v2-available-banner-v1 store. Dismissing the announcement in one context silently suppresses it in the other. The most impactful path: a fresh-install user starts in v2, sees the announcement banner, dismisses it, then navigates to Settings → Experimental and toggles off v2 — the v1 "Switch to v2" banner will never appear because dismissed is already true. The user would have to go back to Settings to re-enable v2, with no in-sidebar nudge visible.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/stores/v2-available-banner/store.ts
Line: 9-20

Comment:
**Shared dismissed flag hides banner across v1↔v2 transitions**

A single `dismissed` boolean is shared by both the v1 (`WorkspaceSidebar`) and v2 (`DashboardSidebar`) banner instances because they both read from the same `v2-available-banner-v1` store. Dismissing the announcement in one context silently suppresses it in the other. The most impactful path: a fresh-install user starts in v2, sees the announcement banner, dismisses it, then navigates to Settings → Experimental and toggles off v2 — the v1 "Switch to v2" banner will never appear because `dismissed` is already `true`. The user would have to go back to Settings to re-enable v2, with no in-sidebar nudge visible.

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

10 changes: 7 additions & 3 deletions apps/desktop/src/renderer/stores/v2-local-override.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { hasPriorSupersetUsage } from "renderer/lib/hasPriorSupersetUsage";
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";

const IS_DEV = process.env.NODE_ENV === "development";

interface V2LocalOverrideState {
/** When true, the user has opted into v2. v2 is gated behind both the remote flag and this opt-in. */
optInV2: boolean;
setOptInV2: (optInV2: 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: IS_DEV,
optInV2: initialOptInV2,
Comment on lines +11 to +20
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find all callsites of setOptInV2 to see if fresh-install v2 flow explicitly persists true.
rg -n --type=ts --type=tsx -C3 'setOptInV2' 2>/dev/null || rg -n -C3 'setOptInV2'

Repository: superset-sh/superset

Length of output: 5762


🏁 Script executed:

# Find the useMigrateV1DataToV2 hook to see if it calls setOptInV2
rg -n --type=ts --type=tsx "useMigrateV1DataToV2" -A 30 | head -100

Repository: superset-sh/superset

Length of output: 92


🏁 Script executed:

# Search for auth layout files that might initialize v2 opt-in
fd -type f -name "*auth*" -o -name "*Auth*" | grep -E "\.(ts|tsx)$" | head -20

Repository: superset-sh/superset

Length of output: 235


🏁 Script executed:

# Look for any place that calls setOptInV2 automatically (not in response to user action)
rg -n --type=ts --type=tsx "setOptInV2\(" -B 10 | grep -E "(useEffect|onMount|initialize)" -A 10

Repository: superset-sh/superset

Length of output: 92


🏁 Script executed:

# Find the useMigrateV1DataToV2 hook implementation
rg -n --type ts "useMigrateV1DataToV2" -A 30 | head -150

Repository: superset-sh/superset

Length of output: 21601


🏁 Script executed:

# Search for auth layout files
fd -type f \( -name "*auth*" -o -name "*Auth*" \) | grep -E "\.(ts|tsx)$" | head -20

Repository: superset-sh/superset

Length of output: 235


🏁 Script executed:

# Look for setOptInV2 calls with surrounding context to find auto-initialization
rg -n "setOptInV2\(" -B 5 -A 2

Repository: superset-sh/superset

Length of output: 3040


🏁 Script executed:

# Get the full useMigrateV1DataToV2 hook implementation
cat apps/desktop/src/renderer/routes/_authenticated/hooks/useMigrateV1DataToV2/useMigrateV1DataToV2.ts

Repository: superset-sh/superset

Length of output: 7532


🏁 Script executed:

# Check the auth layout to see how useMigrateV1DataToV2 is used
cat apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx

Repository: superset-sh/superset

Length of output: 5406


Fresh-install optInV2: true is never written to localStorage — second-session users silently land on v1

Zustand's persist middleware with localStorage hydrates synchronously, so the store is already hydrated at its creation. However, Zustand's persist layer writes to storage only when a state change occurs (via its internal api.subscribe listener). On a fresh install:

  1. hasPriorSupersetUsage()falseinitialOptInV2 = true
  2. Persist finds no v2-local-override-v2 key → keeps the in-memory initial state (optInV2: true) — no set() call fires → subscribe callback is never triggered → nothing is written to v2-local-override-v2
  3. Dashboard layout calls useMigrateV1DataToV2() automatically, which migrates v1 data but does NOT call setOptInV2; user opening the first workspace tab writes tabs-storage
  4. Second session: hasPriorSupersetUsage()trueinitialOptInV2 = false; persist still finds no v2-local-override-v2optInV2 settles on false → user silently lands on v1

The only places setOptInV2(true) is called are user-triggered: the v1 banner click (V2AvailableBanner) and the experimental settings toggle (ExperimentalSettings). There is no automatic call during fresh-install onboarding.

The simplest fix is to force-persist the initial value immediately after store creation when no stored value is found:

🛡️ Proposed fix — force-persist initial value on first run
 export const useV2LocalOverrideStore = create<V2LocalOverrideState>()(
 	devtools(
 		persist(
 			(set) => ({
 				optInV2: initialOptInV2,
 				setOptInV2: (optInV2) => set({ optInV2 }),
 			}),
-			{ name: "v2-local-override-v2" },
+			{
+				name: "v2-local-override-v2",
+				onRehydrateStorage: () => (state) => {
+					// If no persisted value was found (fresh install), force a write
+					// so that initialOptInV2 survives across sessions independently
+					// of whether tabs-storage exists.
+					if (state && state.optInV2 === initialOptInV2) {
+						state.setOptInV2(initialOptInV2);
+					}
+				},
+			},
 		),
 		{ name: "V2LocalOverrideStore" },
 	),
 );

Alternatively, calling setOptInV2(!hasPriorSupersetUsage()) at the end of the v2 onboarding flow (auth layout redirect or useMigrateV1DataToV2) achieves the same result without touching the store definition.

🤖 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/stores/v2-local-override.ts` around lines 11 - 20,
Persisted initial opt-in (initialOptInV2) is never written on fresh installs
because Zustand only writes on state changes; fix by forcing a write when the
store is created and no persisted key exists: after creating
useV2LocalOverrideStore (the create(...) persisted store that exposes optInV2
and setOptInV2), check localStorage for the persist key (e.g.,
"v2-local-override-v2") and if it's missing call setOptInV2(initialOptInV2) (or
setOptInV2(!hasPriorSupersetUsage())) to trigger the persist write;
alternatively, call setOptInV2(!hasPriorSupersetUsage()) at the end of the v2
onboarding/migration flow (e.g., in useMigrateV1DataToV2 or auth layout
redirect) so the initial true value is saved.

setOptInV2: (optInV2) => set({ optInV2 }),
}),
{ name: "v2-local-override-v2" },
Expand Down
Loading
Loading