Skip to content

feat(desktop): default new users to v2 and surface a v2 banner in v1#4115

Merged
saddlepaddle merged 1 commit into
mainfrom
v2-migration-banner-defau
May 6, 2026
Merged

feat(desktop): default new users to v2 and surface a v2 banner in v1#4115
saddlepaddle merged 1 commit into
mainfrom
v2-migration-banner-defau

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented May 6, 2026

Summary

  • Drops the v2-cloud PostHog feature flag (rolled out broadly via desktop_version semver_gte 1.6.3; current desktop is 1.8.4). useIsV2CloudEnabled is 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.
  • Adds a dismissible "Superset v2 is here" banner anchored at the bottom of both the v1 and v2 dashboard sidebars. Action only renders for v1 users (Switch to v2 flips optInV2; the auth layout redirects into setup and useMigrateV1DataToV2 auto-runs). v2 users see the announcement + dismiss only.
  • Defaults optInV2 to true for fresh installs while keeping returning v1 users on v1, gated by a synchronous tabs-storage localStorage probe (hasPriorSupersetUsage). Persist hydration still wins for anyone with a saved override.

Test plan

  • Fresh-install simulation: clear localStorage, launch desktop → lands on v2 dashboard (no v1 detour).
  • Returning v1 user: with tabs-storage populated and no v2-local-override-v2 key, launch → stays on v1 sidebar.
  • V1 banner: visible at the bottom of WorkspaceSidebar above the footer; "Switch to v2" flips into v2 setup; dismiss persists across reloads.
  • V2 banner: visible at the bottom of DashboardSidebar; no action button; dismiss persists.
  • Toggle Experimental → "Try Superset v2" — switch is no longer disabled and tracks surface_toggled cleanly.
  • PostHog surface / surface_source properties update to opted-in / opted-out (no more v2-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-cloud flag and simplifies v2 activation to a local override.

  • New Features

    • Banner at the bottom of v1 and v2 sidebars; v1 shows “Switch to v2”, v2 is announcement + dismiss.
    • Dismissal persists via v2-available-banner-v1.
    • Fresh installs default to v2 using hasPriorSupersetUsage (tabs-storage probe); returning v1 users stay on v1.
  • Refactors

    • useIsV2CloudEnabled now returns a boolean from v2-local-override; remote flag logic removed across call sites.
    • Deleted FEATURE_FLAGS.V2_CLOUD from packages/shared/src/constants.ts.
    • PostHog tagging simplified: surface and surface_source now v1/v2 and opted-in/opted-out.
    • Experimental settings toggle is always enabled and tracks surface_toggled.

Written for commit 8bb6ecb. Summary will update on new commits.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added an animated banner prompting users to switch to Superset v2, with dismissal capability and analytics tracking for user interactions.
  • Refactor

    • Simplified v2 cloud enablement logic to improve the migration experience for returning users.
  • Chores

    • Removed unused feature flag configuration.

- 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`).
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

📝 Walkthrough

Walkthrough

This PR refactors V2 cloud feature flag handling by simplifying useIsV2CloudEnabled to return a boolean directly from local storage instead of an object with remote flag state. It introduces a new dismissible V2AvailableBanner component with associated stores, updates 20+ consumers to the new hook signature, removes the V2_CLOUD feature flag constant, and integrates the banner into dashboard and workspace sidebars.

Changes

V2 Feature Flag Simplification & Banner Introduction

Layer / File(s) Summary
Data Shape & Store Initialization
packages/shared/src/constants.ts, apps/desktop/src/renderer/stores/v2-local-override.ts, apps/desktop/src/renderer/lib/hasPriorSupersetUsage.ts
Removes V2_CLOUD feature flag constant. Introduces hasPriorSupersetUsage() utility to detect prior app usage from localStorage. Updates v2-local-override store to initialize opt-in based on prior usage instead of environment mode.
Hook & Banner Stores
apps/desktop/src/renderer/hooks/useIsV2CloudEnabled.ts, apps/desktop/src/renderer/stores/v2-available-banner/store.ts, apps/desktop/src/renderer/stores/v2-available-banner/index.ts
Simplifies useIsV2CloudEnabled() to return a plain boolean from local override store, removing remote flag logic. Creates new useV2AvailableBannerStore with dismissed state and dismiss() action, persisted and wrapped with devtools.
Banner Component
apps/desktop/src/renderer/components/V2AvailableBanner/V2AvailableBanner.tsx, apps/desktop/src/renderer/components/V2AvailableBanner/index.ts
Introduces animated V2AvailableBanner component with switch/dismiss handlers, analytics tracking (toggle & dismissal events), conditional V2-specific action label, and framer-motion entrance/exit animations. Exports via index barrel.
Core Logic Simplifications
apps/desktop/src/renderer/components/PostHogSurfaceTagger/PostHogSurfaceTagger.tsx, apps/desktop/src/renderer/routes/_authenticated/settings/experimental/components/ExperimentalSettings/ExperimentalSettings.tsx
Simplifies PostHogSurfaceTagger to use isV2CloudEnabled boolean directly; changes surface_source to "opted-in" or "opted-out". Removes remote flag gating in ExperimentalSettings; deletes "Early access not enabled" message and disabled state logic.
Hook Consumer Updates
apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx, apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsx, apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/PropertiesSidebar.tsx, apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsx, apps/desktop/src/renderer/routes/_authenticated/layout.tsx, apps/desktop/src/renderer/routes/_authenticated/hooks/useMigrateV1DataToV2/useMigrateV1DataToV2.ts, apps/desktop/src/renderer/routes/_authenticated/settings/* (8 files), apps/desktop/src/renderer/routes/_authenticated/setup/* (2 files)
Updates ~20 files to consume useIsV2CloudEnabled() as a direct boolean instead of destructuring an object. All changes follow the pattern: const isV2CloudEnabled = useIsV2CloudEnabled().
Banner Integration
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx, apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx
Renders <V2AvailableBanner /> conditionally when sidebar is not collapsed, placed after setup/script card sections.

🎯 3 (Moderate) | ⏱️ ~20 minutes

A banner springs forth with glee,
"Try v2, if you please!" it says to me.
The hook now speaks in boolean rhyme,
Simplifying flags for a cloudier time.
No remote gatekeeping stands in the way,
Just local wishes to v2 convey. 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main changes: defaulting new users to v2 and adding a banner in v1.
Description check ✅ Passed The description covers all required template sections with comprehensive detail: summary of changes, related work, type of change context, and testing checklist provided.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch v2-migration-banner-defau

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 6, 2026

Greptile Summary

This PR retires the v2-cloud PostHog feature flag and replaces it with a synchronous localStorage heuristic (hasPriorSupersetUsage) to default fresh installs to v2 while keeping returning v1 users on v1. It also introduces a dismissible V2AvailableBanner rendered in both the v1 (WorkspaceSidebar) and v2 (DashboardSidebar) sidebars.

  • PostHog flag removal: useIsV2CloudEnabled is simplified from a composite of remote flag + local opt-in to a direct boolean read of optInV2; all 15+ call sites are updated from the old destructured { isV2CloudEnabled } shape to the new boolean return.
  • Fresh-install defaulting: initialOptInV2 = !hasPriorSupersetUsage() is evaluated at module load time (before persist hydration), so fresh installs get optInV2: true by default; persist hydration overrides this for any user with a saved preference in v2-local-override-v2.
  • V2 banner: A new V2AvailableBanner appears above the footer in both sidebars; v1 users get a "Switch to v2" action that calls setOptInV2(true), while v2 users see an announcement-only card; dismissal is persisted in a new v2-available-banner-v1 Zustand store.

Confidence Score: 4/5

Safe 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 optInV2 directly and initialising it from a localStorage probe — is sound. The two concerns are: a shared dismissed flag across v1 and v2 banner instances means dismissing in one context silently suppresses the banner in the other (including the 'Switch to v2' action for v1 users), and the hasPriorSupersetUsage fallback returns false when localStorage is unavailable, which could cause unexpected v2 opt-in in degraded environments. Neither is a showstopper, but the banner dismissal cross-context issue could meaningfully reduce discovery of the 'Switch to v2' nudge for users who visit v2 first then go back to v1.

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.

Important Files Changed

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
Loading
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

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

Comment on lines +6 to +9
export function hasPriorSupersetUsage(): boolean {
if (typeof localStorage === "undefined") return false;
return localStorage.getItem("tabs-storage") !== null;
}
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 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.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch

Thank you for your contribution! 🎉

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/desktop/src/renderer/stores/v2-local-override.ts (1)

16-27: ⚡ Quick win

Upgrade Zustand to ≥ 5.0.10 to fix a known persist race condition

In Zustand v5.0.9 and earlier, concurrent calls to rehydrate during 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 persist stores (useV2LocalOverrideStore and useV2AvailableBannerStore). 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

📥 Commits

Reviewing files that changed from the base of the PR and between 41d3941 and 8bb6ecb.

📒 Files selected for processing (29)
  • apps/desktop/src/renderer/components/PostHogSurfaceTagger/PostHogSurfaceTagger.tsx
  • apps/desktop/src/renderer/components/V2AvailableBanner/V2AvailableBanner.tsx
  • apps/desktop/src/renderer/components/V2AvailableBanner/index.ts
  • apps/desktop/src/renderer/hooks/useIsV2CloudEnabled.ts
  • apps/desktop/src/renderer/lib/hasPriorSupersetUsage.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/PropertiesSidebar.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsx
  • apps/desktop/src/renderer/routes/_authenticated/hooks/useMigrateV1DataToV2/useMigrateV1DataToV2.ts
  • apps/desktop/src/renderer/routes/_authenticated/layout.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/AgentsSettings/AgentsSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/behavior/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/GeneralSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/SettingsSidebar.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/experimental/components/ExperimentalSettings/ExperimentalSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/experimental/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/git/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/links/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/TerminalSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/setup/adopt-worktrees/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/setup/project/page.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx
  • apps/desktop/src/renderer/stores/v2-available-banner/index.ts
  • apps/desktop/src/renderer/stores/v2-available-banner/store.ts
  • apps/desktop/src/renderer/stores/v2-local-override.ts
  • packages/shared/src/constants.ts
💤 Files with no reviewable changes (1)
  • packages/shared/src/constants.ts

Comment on lines +11 to +20
// 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,
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.

@saddlepaddle saddlepaddle merged commit af7a1a2 into main May 6, 2026
17 checks passed
saddlepaddle added a commit that referenced this pull request May 6, 2026
…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`).
@Kitenite Kitenite deleted the v2-migration-banner-defau branch May 6, 2026 04:50
saddlepaddle added a commit that referenced this pull request May 7, 2026
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.
saddlepaddle added a commit that referenced this pull request May 7, 2026
#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.
MocA-Love pushed a commit to MocA-Love/superset that referenced this pull request May 8, 2026
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.
MocA-Love pushed a commit to MocA-Love/superset that referenced this pull request May 8, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant