feat(desktop): default new users to v2 and surface a v2 banner in v1#4115
Conversation
- Removes the `v2-cloud` PostHog feature flag — collapses `useIsV2CloudEnabled` to a boolean read of the local override and drops the now-redundant remote/dev branches at every callsite. - Adds a dismissible "Superset v2 is here" banner anchored at the bottom of both v1 and v2 dashboard sidebars; clicking "Switch to v2" flips the local override (the auth layout already redirects into setup and `useMigrateV1DataToV2` auto-runs). - Defaults `optInV2` to `true` for fresh installs while keeping returning users on their current surface, gated by a synchronous `tabs-storage` localStorage probe (`hasPriorSupersetUsage`).
📝 WalkthroughWalkthroughThis PR refactors V2 cloud feature flag handling by simplifying ChangesV2 Feature Flag Simplification & Banner Introduction
🎯 3 (Moderate) | ⏱️ ~20 minutes
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR retires the
Confidence Score: 4/5Safe to merge; the PostHog flag retirement and fresh-install defaulting are straightforward, but the shared banner dismissed state has a UX edge case worth addressing. The core logic — reading apps/desktop/src/renderer/stores/v2-available-banner/store.ts and apps/desktop/src/renderer/lib/hasPriorSupersetUsage.ts are the two files worth a second look.
|
| Filename | Overview |
|---|---|
| apps/desktop/src/renderer/stores/v2-available-banner/store.ts | Persisted Zustand store for banner dismissed state; dismissed flag is shared across both v1 and v2 banner instances, which causes dismissal in one context to hide the banner in the other. |
| apps/desktop/src/renderer/lib/hasPriorSupersetUsage.ts | New utility that proxies tabs-storage in localStorage as a heuristic for "has the app been used before." Handles SSR-safe typeof localStorage guard correctly. |
| apps/desktop/src/renderer/stores/v2-local-override.ts | Replaces PostHog flag gate with a hasPriorSupersetUsage() check at module load time to default fresh installs to v2; persist hydration correctly overrides this for users with a saved preference. |
| apps/desktop/src/renderer/hooks/useIsV2CloudEnabled.ts | Simplified from a PostHog-flag + local-override composite to a direct boolean read of optInV2; all call sites updated from destructured object to direct boolean. |
| apps/desktop/src/renderer/components/V2AvailableBanner/V2AvailableBanner.tsx | New dismissible banner component rendered in both sidebars; correctly gates the "Switch to v2" action for v1 users only, with framer-motion exit animation. |
| apps/desktop/src/renderer/components/PostHogSurfaceTagger/PostHogSurfaceTagger.tsx | Drops the isRemoteV2Enabled branch and the v2-flag-off surface source; surface_source now correctly emits opted-in / opted-out only. |
| apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx | Adds V2AvailableBanner above the footer in the v1 sidebar; correctly gated behind !isCollapsed. |
| apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx | Adds V2AvailableBanner to the v2 dashboard sidebar; correctly gated behind !isCollapsed. |
| packages/shared/src/constants.ts | Removes the V2_CLOUD feature flag constant now that the PostHog flag is fully retired. |
| apps/desktop/src/renderer/routes/_authenticated/settings/experimental/components/ExperimentalSettings/ExperimentalSettings.tsx | Removes the isRemoteV2Enabled guard that disabled the v2 toggle; switch is now always enabled and the "early access not enabled" message is removed. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[App loads] --> B{hasPriorSupersetUsage?\ntabs-storage in localStorage}
B -- No fresh install --> C[initialOptInV2 = true]
B -- Yes returning user --> D[initialOptInV2 = false]
C --> E{v2-local-override-v2\nin localStorage?}
D --> E
E -- Yes --> F[Persist hydrates\nwith saved optInV2]
E -- No --> G[Keep initialOptInV2]
F --> H{optInV2}
G --> H
H -- true --> I[DashboardSidebar v2\n+ V2AvailableBanner\nannouncement only]
H -- false --> J[WorkspaceSidebar v1\n+ V2AvailableBanner\nwith Switch to v2 action]
J -- User clicks Switch to v2 --> K[setOptInV2 true\nredirects to v2 setup]
I -- User dismisses banner --> L[dismissed=true\npersisted in v2-available-banner-v1]
J -- User dismisses banner --> L
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
apps/desktop/src/renderer/stores/v2-available-banner/store.ts:9-20
**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.
### Issue 2 of 2
apps/desktop/src/renderer/lib/hasPriorSupersetUsage.ts:6-9
**`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.
Reviews (1): Last reviewed commit: "feat(desktop): default new users to v2 a..." | Re-trigger Greptile
| export const useV2AvailableBannerStore = create<V2AvailableBannerState>()( | ||
| devtools( | ||
| persist( | ||
| (set) => ({ | ||
| dismissed: false, | ||
| dismiss: () => set({ dismissed: true }), | ||
| }), | ||
| { name: "v2-available-banner-v1" }, | ||
| ), | ||
| { name: "V2AvailableBannerStore" }, | ||
| ), | ||
| ); |
There was a problem hiding this 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.
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.| export function hasPriorSupersetUsage(): boolean { | ||
| if (typeof localStorage === "undefined") return false; | ||
| return localStorage.getItem("tabs-storage") !== null; | ||
| } |
There was a problem hiding this 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.
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.
🧹 Preview Cleanup CompleteThe following preview resources have been cleaned up:
Thank you for your contribution! 🎉 |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
apps/desktop/src/renderer/stores/v2-local-override.ts (1)
16-27: ⚡ Quick winUpgrade Zustand to ≥ 5.0.10 to fix a known persist race condition
In Zustand v5.0.9 and earlier, concurrent calls to
rehydrateduring persist middleware initialization could cause a race condition where multiple hydration attempts would interfere with each other, leading to inconsistent state. The fix is to upgrade to Zustand v5.0.10 or later — no code changes are needed as the fix is internal to the persist middleware.This PR adds two new
persiststores (useV2LocalOverrideStoreanduseV2AvailableBannerStore). Since the v2/v1 routing decision is derived directly from these stores, inconsistent state during initialization could cause incorrect routing or banner visibility for edge-case startup paths.🤖 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 16 - 27, Upgrade the Zustand dependency to version 5.0.10 or later to avoid the persist middleware race condition affecting useV2LocalOverrideStore and useV2AvailableBannerStore initialization; update package.json (and lockfile) to require "zustand" >= 5.0.10, run install to refresh the lockfile, and ensure CI/builds use the updated lockfile so the persist middleware no longer triggers concurrent rehydrate races at startup.
🤖 Prompt for all review comments with 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.
Inline comments:
In `@apps/desktop/src/renderer/stores/v2-local-override.ts`:
- Around line 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.
---
Nitpick comments:
In `@apps/desktop/src/renderer/stores/v2-local-override.ts`:
- Around line 16-27: Upgrade the Zustand dependency to version 5.0.10 or later
to avoid the persist middleware race condition affecting useV2LocalOverrideStore
and useV2AvailableBannerStore initialization; update package.json (and lockfile)
to require "zustand" >= 5.0.10, run install to refresh the lockfile, and ensure
CI/builds use the updated lockfile so the persist middleware no longer triggers
concurrent rehydrate races at startup.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3b0e8c08-cf7a-47aa-9e8a-c5f647cdbf8d
📒 Files selected for processing (29)
apps/desktop/src/renderer/components/PostHogSurfaceTagger/PostHogSurfaceTagger.tsxapps/desktop/src/renderer/components/V2AvailableBanner/V2AvailableBanner.tsxapps/desktop/src/renderer/components/V2AvailableBanner/index.tsapps/desktop/src/renderer/hooks/useIsV2CloudEnabled.tsapps/desktop/src/renderer/lib/hasPriorSupersetUsage.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/PropertiesSidebar.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsxapps/desktop/src/renderer/routes/_authenticated/hooks/useMigrateV1DataToV2/useMigrateV1DataToV2.tsapps/desktop/src/renderer/routes/_authenticated/layout.tsxapps/desktop/src/renderer/routes/_authenticated/settings/agents/components/AgentsSettings/AgentsSettings.tsxapps/desktop/src/renderer/routes/_authenticated/settings/behavior/page.tsxapps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/GeneralSettings.tsxapps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/SettingsSidebar.tsxapps/desktop/src/renderer/routes/_authenticated/settings/experimental/components/ExperimentalSettings/ExperimentalSettings.tsxapps/desktop/src/renderer/routes/_authenticated/settings/experimental/page.tsxapps/desktop/src/renderer/routes/_authenticated/settings/git/page.tsxapps/desktop/src/renderer/routes/_authenticated/settings/links/page.tsxapps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/TerminalSettings.tsxapps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsxapps/desktop/src/renderer/routes/_authenticated/setup/adopt-worktrees/page.tsxapps/desktop/src/renderer/routes/_authenticated/setup/project/page.tsxapps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsxapps/desktop/src/renderer/stores/v2-available-banner/index.tsapps/desktop/src/renderer/stores/v2-available-banner/store.tsapps/desktop/src/renderer/stores/v2-local-override.tspackages/shared/src/constants.ts
💤 Files with no reviewable changes (1)
- packages/shared/src/constants.ts
| // 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, |
There was a problem hiding this comment.
🧩 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 -100Repository: 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 -20Repository: 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 10Repository: superset-sh/superset
Length of output: 92
🏁 Script executed:
# Find the useMigrateV1DataToV2 hook implementation
rg -n --type ts "useMigrateV1DataToV2" -A 30 | head -150Repository: 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 -20Repository: 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 2Repository: 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.tsRepository: 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.tsxRepository: 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:
hasPriorSupersetUsage()→false→initialOptInV2 = true- Persist finds no
v2-local-override-v2key → keeps the in-memory initial state (optInV2: true) — noset()call fires → subscribe callback is never triggered → nothing is written tov2-local-override-v2 - Dashboard layout calls
useMigrateV1DataToV2()automatically, which migrates v1 data but does NOT callsetOptInV2; user opening the first workspace tab writestabs-storage - Second session:
hasPriorSupersetUsage()→true→initialOptInV2 = false; persist still finds nov2-local-override-v2→optInV2settles onfalse→ 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.
…4115) - Removes the `v2-cloud` PostHog feature flag — collapses `useIsV2CloudEnabled` to a boolean read of the local override and drops the now-redundant remote/dev branches at every callsite. - Adds a dismissible "Superset v2 is here" banner anchored at the bottom of both v1 and v2 dashboard sidebars; clicking "Switch to v2" flips the local override (the auth layout already redirects into setup and `useMigrateV1DataToV2` auto-runs). - Defaults `optInV2` to `true` for fresh installs while keeping returning users on their current surface, gated by a synchronous `tabs-storage` localStorage probe (`hasPriorSupersetUsage`).
The v2 default introduced in #4115 used `localStorage.getItem("tabs-storage")` to detect returning users, but tabs state was migrated off localStorage to app-state.json (lowdb) in #303 (2025-12-10). The key has been null for every install since, so `!hasPriorSupersetUsage()` evaluated to true for all users and force-flipped anyone without an explicit toggle into v2 on first launch. Resolve the default from the canonical signal instead: ≥1 row in `projects` or `workspaces` in localDb means a returning user. `optInV2` starts `null`, `V2DefaultResolver` runs both `*.hasAny` queries once and persists the result. Users who got force-pulled but never toggled have no persisted override yet, so they self-heal back to v1 on next launch.
#4176) * fix(desktop): resolve v2 default from local db, not stale localStorage The v2 default introduced in #4115 used `localStorage.getItem("tabs-storage")` to detect returning users, but tabs state was migrated off localStorage to app-state.json (lowdb) in #303 (2025-12-10). The key has been null for every install since, so `!hasPriorSupersetUsage()` evaluated to true for all users and force-flipped anyone without an explicit toggle into v2 on first launch. Resolve the default from the canonical signal instead: ≥1 row in `projects` or `workspaces` in localDb means a returning user. `optInV2` starts `null`, `V2DefaultResolver` runs both `*.hasAny` queries once and persists the result. Users who got force-pulled but never toggled have no persisted override yet, so they self-heal back to v1 on next launch. * fix(desktop): only fresh installs are forced into onboarding The onboarding gate at _authenticated/layout.tsx only checked isV2CloudEnabled && !requiredComplete, so any existing v1 user who explicitly toggled v2 ON was force-walked through /setup/* — wrong: they already have projects/workspaces and should land on the dashboard. Split the v2 default into two flags on the override store: - optInV2: user-controllable (resolver default OR experimental toggle). - isFreshInstall: sticky one-shot detection from the resolver, never changed by user toggles. Onboarding gate now requires isFreshInstall === true. Resolver runs whenever either flag is null, so users with persisted optInV2 from the broken release also get isFreshInstall backfilled on next launch.
superset-sh#4176) * fix(desktop): resolve v2 default from local db, not stale localStorage The v2 default introduced in superset-sh#4115 used `localStorage.getItem("tabs-storage")` to detect returning users, but tabs state was migrated off localStorage to app-state.json (lowdb) in #303 (2025-12-10). The key has been null for every install since, so `!hasPriorSupersetUsage()` evaluated to true for all users and force-flipped anyone without an explicit toggle into v2 on first launch. Resolve the default from the canonical signal instead: ≥1 row in `projects` or `workspaces` in localDb means a returning user. `optInV2` starts `null`, `V2DefaultResolver` runs both `*.hasAny` queries once and persists the result. Users who got force-pulled but never toggled have no persisted override yet, so they self-heal back to v1 on next launch. * fix(desktop): only fresh installs are forced into onboarding The onboarding gate at _authenticated/layout.tsx only checked isV2CloudEnabled && !requiredComplete, so any existing v1 user who explicitly toggled v2 ON was force-walked through /setup/* — wrong: they already have projects/workspaces and should land on the dashboard. Split the v2 default into two flags on the override store: - optInV2: user-controllable (resolver default OR experimental toggle). - isFreshInstall: sticky one-shot detection from the resolver, never changed by user toggles. Onboarding gate now requires isFreshInstall === true. Resolver runs whenever either flag is null, so users with persisted optInV2 from the broken release also get isFreshInstall backfilled on next launch.
Recorded as integrated via -s ours after batch PRs #455-#464. Taken via individual PRs: - PR 1 (#455): v2 polish 前半 safe set (9 commits) - PR 2 (#456): v2/host-service polish 中盤 (12 commits) - PR 3 (#457): sidebar polish + jwt API (5 commits) - PR 4 (#458): host-service tRPC retry/cache/timeout (3 commits) - PR 5 (#459): v2 diff pane / file pane polish (2 commits) - PR 7 (#462): host-service v2 canonical workspace.create + attachment store (PR1 superset-sh#3893 + PR2 superset-sh#3916) - PR 11 (#463): agents API + onboarding (7 commits + 1 cleanup) - PR 12 (#464): v1→v2 import flow rewrite (11 commits + 2 follow-ups) - PR 13 (#460): host-service shell env probe + typo (2 commits) - PR 16 (#461): marketplace 19 themes (1 commit) Skipped / deferred (recorded as integrated for behind=0): - PR 6: CLI v1 launch (superset-sh#3898 + 30+ CLI/SDK followups) — defer to dedicated migration - PR 9: v2 PR3 (superset-sh#3940) + revert (superset-sh#4017) — net-zero pair - PR 10: pty-daemon (superset-sh#3896, superset-sh#3971, superset-sh#4054) — fork keeps its terminal-host - PR 14: Slack MCP-v2 (superset-sh#4197, superset-sh#4208) — depends on mcp-v2/sdk divergence - PR 15: onboarding remaining (superset-sh#4115, superset-sh#4125, superset-sh#4214, superset-sh#4213, superset-sh#4222, superset-sh#4225) — depends on fork's deleted setup pages Behind: 0 after this merge.
Summary
v2-cloudPostHog feature flag (rolled out broadly viadesktop_version semver_gte 1.6.3; current desktop is 1.8.4).useIsV2CloudEnabledis now a boolean read of the local override; every callsite is updated. Follow-up: a separate task is open to bump the desktop minimum supported version, after which the PostHog flag itself can be deleted.Switch to v2flipsoptInV2; the auth layout redirects into setup anduseMigrateV1DataToV2auto-runs). v2 users see the announcement + dismiss only.optInV2totruefor fresh installs while keeping returning v1 users on v1, gated by a synchronoustabs-storagelocalStorage probe (hasPriorSupersetUsage). Persist hydration still wins for anyone with a saved override.Test plan
localStorage, launch desktop → lands on v2 dashboard (no v1 detour).tabs-storagepopulated and nov2-local-override-v2key, launch → stays on v1 sidebar.WorkspaceSidebarabove the footer; "Switch to v2" flips into v2 setup; dismiss persists across reloads.DashboardSidebar; no action button; dismiss persists.surface_toggledcleanly.surface/surface_sourceproperties update toopted-in/opted-out(no morev2-flag-off).Summary by cubic
Default new desktop installs to v2 and add a dismissible “Superset v2 is here” banner in the sidebar to help v1 users switch. Removes the
v2-cloudflag and simplifies v2 activation to a local override.New Features
v2-available-banner-v1.hasPriorSupersetUsage(tabs-storageprobe); returning v1 users stay on v1.Refactors
useIsV2CloudEnablednow returns a boolean fromv2-local-override; remote flag logic removed across call sites.FEATURE_FLAGS.V2_CLOUDfrompackages/shared/src/constants.ts.surfaceandsurface_sourcenowv1/v2andopted-in/opted-out.surface_toggled.Written for commit 8bb6ecb. Summary will update on new commits.
Summary by CodeRabbit
Release Notes
New Features
Refactor
Chores