From 8bb6ecb6687f189862bd304ea38845cadad40966 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Tue, 5 May 2026 18:59:29 -0700 Subject: [PATCH] feat(desktop): default new users to v2 and surface a v2 banner in v1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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`). --- .../PostHogSurfaceTagger.tsx | 12 ++--- .../V2AvailableBanner/V2AvailableBanner.tsx | 46 +++++++++++++++++++ .../components/V2AvailableBanner/index.ts | 1 + .../src/renderer/hooks/useIsV2CloudEnabled.ts | 29 ++---------- .../src/renderer/lib/hasPriorSupersetUsage.ts | 9 ++++ .../DashboardSidebar/DashboardSidebar.tsx | 2 + .../_dashboard/components/TopBar/TopBar.tsx | 2 +- .../_authenticated/_dashboard/layout.tsx | 2 +- .../PropertiesSidebar/PropertiesSidebar.tsx | 2 +- .../components/TasksTopBar/TasksTopBar.tsx | 2 +- .../useMigrateV1DataToV2.ts | 2 +- .../renderer/routes/_authenticated/layout.tsx | 2 +- .../AgentsSettings/AgentsSettings.tsx | 2 +- .../_authenticated/settings/behavior/page.tsx | 2 +- .../SettingsSidebar/GeneralSettings.tsx | 2 +- .../SettingsSidebar/SettingsSidebar.tsx | 2 +- .../ExperimentalSettings.tsx | 12 ++--- .../settings/experimental/page.tsx | 2 +- .../_authenticated/settings/git/page.tsx | 2 +- .../_authenticated/settings/links/page.tsx | 2 +- .../TerminalSettings/TerminalSettings.tsx | 2 +- .../_authenticated/settings/terminal/page.tsx | 2 +- .../setup/adopt-worktrees/page.tsx | 2 +- .../_authenticated/setup/project/page.tsx | 2 +- .../WorkspaceSidebar/WorkspaceSidebar.tsx | 3 ++ .../stores/v2-available-banner/index.ts | 1 + .../stores/v2-available-banner/store.ts | 20 ++++++++ .../src/renderer/stores/v2-local-override.ts | 10 ++-- packages/shared/src/constants.ts | 2 - 29 files changed, 115 insertions(+), 66 deletions(-) create mode 100644 apps/desktop/src/renderer/components/V2AvailableBanner/V2AvailableBanner.tsx create mode 100644 apps/desktop/src/renderer/components/V2AvailableBanner/index.ts create mode 100644 apps/desktop/src/renderer/lib/hasPriorSupersetUsage.ts create mode 100644 apps/desktop/src/renderer/stores/v2-available-banner/index.ts create mode 100644 apps/desktop/src/renderer/stores/v2-available-banner/store.ts diff --git a/apps/desktop/src/renderer/components/PostHogSurfaceTagger/PostHogSurfaceTagger.tsx b/apps/desktop/src/renderer/components/PostHogSurfaceTagger/PostHogSurfaceTagger.tsx index fefef77ea13..c0997122bcf 100644 --- a/apps/desktop/src/renderer/components/PostHogSurfaceTagger/PostHogSurfaceTagger.tsx +++ b/apps/desktop/src/renderer/components/PostHogSurfaceTagger/PostHogSurfaceTagger.tsx @@ -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 }); @@ -24,7 +18,7 @@ export function PostHogSurfaceTagger() { surface_ever_v2: true, }); } - }, [isV2CloudEnabled, isRemoteV2Enabled, optInV2]); + }, [isV2CloudEnabled]); return null; } diff --git a/apps/desktop/src/renderer/components/V2AvailableBanner/V2AvailableBanner.tsx b/apps/desktop/src/renderer/components/V2AvailableBanner/V2AvailableBanner.tsx new file mode 100644 index 00000000000..a66259e1eca --- /dev/null +++ b/apps/desktop/src/renderer/components/V2AvailableBanner/V2AvailableBanner.tsx @@ -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 ( + + {!dismissed && ( + + + + )} + + ); +} diff --git a/apps/desktop/src/renderer/components/V2AvailableBanner/index.ts b/apps/desktop/src/renderer/components/V2AvailableBanner/index.ts new file mode 100644 index 00000000000..bfdb50e87be --- /dev/null +++ b/apps/desktop/src/renderer/components/V2AvailableBanner/index.ts @@ -0,0 +1 @@ +export { V2AvailableBanner } from "./V2AvailableBanner"; diff --git a/apps/desktop/src/renderer/hooks/useIsV2CloudEnabled.ts b/apps/desktop/src/renderer/hooks/useIsV2CloudEnabled.ts index b9f55c10667..93cb7f0f51d 100644 --- a/apps/desktop/src/renderer/hooks/useIsV2CloudEnabled.ts +++ b/apps/desktop/src/renderer/hooks/useIsV2CloudEnabled.ts @@ -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); } diff --git a/apps/desktop/src/renderer/lib/hasPriorSupersetUsage.ts b/apps/desktop/src/renderer/lib/hasPriorSupersetUsage.ts new file mode 100644 index 00000000000..88ed37a6060 --- /dev/null +++ b/apps/desktop/src/renderer/lib/hasPriorSupersetUsage.ts @@ -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; +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx index eb1c5a9e1fb..a4d63759959 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx @@ -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"; @@ -236,6 +237,7 @@ export function DashboardSidebar({ projectName={activeV2Project.name} /> )} + {!isCollapsed && }
s.isOpen); const isSidebarCollapsed = useWorkspaceSidebarStore((s) => s.isCollapsed()); // Default to Mac layout while loading to avoid overlap with traffic lights diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx index 362b36d5bbe..bbe7d63dbaa 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx @@ -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 diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/PropertiesSidebar.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/PropertiesSidebar.tsx index 26e3dd19827..32ca7789fd5 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/PropertiesSidebar.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/PropertiesSidebar.tsx @@ -14,7 +14,7 @@ interface PropertiesSidebarProps { export function PropertiesSidebar({ task }: PropertiesSidebarProps) { const labels = task.labels ?? []; - const { isV2CloudEnabled } = useIsV2CloudEnabled(); + const isV2CloudEnabled = useIsV2CloudEnabled(); return (
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsx index 77bb21693a1..02a44301a8b 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsx @@ -70,7 +70,7 @@ export function TasksTopBar({ const selectedCount = selectedTasks.length; const searchInputRef = useRef(null); const [isCreateTaskOpen, setIsCreateTaskOpen] = useState(false); - const { isV2CloudEnabled } = useIsV2CloudEnabled(); + const isV2CloudEnabled = useIsV2CloudEnabled(); useHotkey( "FOCUS_TASK_SEARCH", diff --git a/apps/desktop/src/renderer/routes/_authenticated/hooks/useMigrateV1DataToV2/useMigrateV1DataToV2.ts b/apps/desktop/src/renderer/routes/_authenticated/hooks/useMigrateV1DataToV2/useMigrateV1DataToV2.ts index 6e3f1727632..88f6ea81f3c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/hooks/useMigrateV1DataToV2/useMigrateV1DataToV2.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/hooks/useMigrateV1DataToV2/useMigrateV1DataToV2.ts @@ -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, diff --git a/apps/desktop/src/renderer/routes/_authenticated/layout.tsx b/apps/desktop/src/renderer/routes/_authenticated/layout.tsx index 30823cad3ef..b1a0b08d13f 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/layout.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/layout.tsx @@ -64,7 +64,7 @@ function AuthenticatedLayout() { const setOriginRoute = useSettingsStore((s) => s.setOriginRoute); const utils = electronTrpc.useUtils(); const shownWorkspaceInitWarningsRef = useRef(new Set()); - const { isV2CloudEnabled } = useIsV2CloudEnabled(); + const isV2CloudEnabled = useIsV2CloudEnabled(); const requiredComplete = useOnboardingStore(selectRequiredStepsComplete); const firstIncompleteStep = useOnboardingStore(selectFirstIncompleteStep); diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/AgentsSettings/AgentsSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/AgentsSettings/AgentsSettings.tsx index 39234033c70..872aacdc89d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/AgentsSettings/AgentsSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/AgentsSettings/AgentsSettings.tsx @@ -18,7 +18,7 @@ export function AgentsSettings({ visibleItems, initialAgentPresetId, }: AgentsSettingsProps) { - const { isV2CloudEnabled } = useIsV2CloudEnabled(); + const isV2CloudEnabled = useIsV2CloudEnabled(); if (isV2CloudEnabled) { return ; } diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/behavior/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/behavior/page.tsx index 420b0140a02..8ae71d8e541 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/behavior/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/behavior/page.tsx @@ -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( () => diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/GeneralSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/GeneralSettings.tsx index 828bd6c7dd6..b55c975d604 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/GeneralSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/GeneralSettings.tsx @@ -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], diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/SettingsSidebar.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/SettingsSidebar.tsx index 16fdbd386ab..85d0ebb4193 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/SettingsSidebar.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/SettingsSidebar.tsx @@ -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) diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/experimental/components/ExperimentalSettings/ExperimentalSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/experimental/components/ExperimentalSettings/ExperimentalSettings.tsx index d8995df38c1..0aa00725cad 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/experimental/components/ExperimentalSettings/ExperimentalSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/experimental/components/ExperimentalSettings/ExperimentalSettings.tsx @@ -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); @@ -98,13 +98,8 @@ export function ExperimentalSettings({ Try Superset v2

- Use the new workspace experience when early access is available. + Use the new workspace experience.

- {!isRemoteV2Enabled && ( -

- Early access is not enabled for this account. -

- )}
{ track("surface_toggled", { from: isV2CloudEnabled ? "v2" : "v1", - to: enabled && isRemoteV2Enabled ? "v2" : "v1", + to: enabled ? "v2" : "v1", }); setOptInV2(enabled); }} - disabled={!isRemoteV2Enabled} />
)} diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/experimental/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/experimental/page.tsx index 5709fb91576..6cd4d7d9e9c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/experimental/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/experimental/page.tsx @@ -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( () => diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/git/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/git/page.tsx index 727c5067dbe..8e9bbc0e3fd 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/git/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/git/page.tsx @@ -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( () => diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/links/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/links/page.tsx index 40af55e5444..c6b04ff653d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/links/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/links/page.tsx @@ -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( () => diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/TerminalSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/TerminalSettings.tsx index 6909f87e70b..fd3257dde3a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/TerminalSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/TerminalSettings.tsx @@ -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, diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx index d2662dd9de2..bb8da40ddb1 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx @@ -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( () => diff --git a/apps/desktop/src/renderer/routes/_authenticated/setup/adopt-worktrees/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/setup/adopt-worktrees/page.tsx index e4d5c1f07f1..906814ae69d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/setup/adopt-worktrees/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/setup/adopt-worktrees/page.tsx @@ -31,7 +31,7 @@ function OnboardingAdoptWorktreesPage() { ); const utils = electronTrpc.useUtils(); - const { isV2CloudEnabled } = useIsV2CloudEnabled(); + const isV2CloudEnabled = useIsV2CloudEnabled(); const { data: projects, isPending } = electronTrpc.projects.getRecents.useQuery(); diff --git a/apps/desktop/src/renderer/routes/_authenticated/setup/project/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/setup/project/page.tsx index 2d0ff8e0420..4da6c694274 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/setup/project/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/setup/project/page.tsx @@ -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(); diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx index 4198ed8636c..e1415e2e854 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx @@ -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"; @@ -115,6 +116,8 @@ export function WorkspaceSidebar({ projectName={activeProjectName} /> + {!isCollapsed && } + diff --git a/apps/desktop/src/renderer/stores/v2-available-banner/index.ts b/apps/desktop/src/renderer/stores/v2-available-banner/index.ts new file mode 100644 index 00000000000..5757d78a124 --- /dev/null +++ b/apps/desktop/src/renderer/stores/v2-available-banner/index.ts @@ -0,0 +1 @@ +export { useV2AvailableBannerStore } from "./store"; diff --git a/apps/desktop/src/renderer/stores/v2-available-banner/store.ts b/apps/desktop/src/renderer/stores/v2-available-banner/store.ts new file mode 100644 index 00000000000..f4bc9b53b35 --- /dev/null +++ b/apps/desktop/src/renderer/stores/v2-available-banner/store.ts @@ -0,0 +1,20 @@ +import { create } from "zustand"; +import { devtools, persist } from "zustand/middleware"; + +interface V2AvailableBannerState { + dismissed: boolean; + dismiss: () => void; +} + +export const useV2AvailableBannerStore = create()( + devtools( + persist( + (set) => ({ + dismissed: false, + dismiss: () => set({ dismissed: true }), + }), + { name: "v2-available-banner-v1" }, + ), + { name: "V2AvailableBannerStore" }, + ), +); diff --git a/apps/desktop/src/renderer/stores/v2-local-override.ts b/apps/desktop/src/renderer/stores/v2-local-override.ts index 783a41f7655..135153b6210 100644 --- a/apps/desktop/src/renderer/stores/v2-local-override.ts +++ b/apps/desktop/src/renderer/stores/v2-local-override.ts @@ -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()( devtools( persist( (set) => ({ - optInV2: IS_DEV, + optInV2: initialOptInV2, setOptInV2: (optInV2) => set({ optInV2 }), }), { name: "v2-local-override-v2" }, diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index d1c266d0cdd..f8b931169fe 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -71,8 +71,6 @@ export const FEATURE_FLAGS = { CLOUD_ACCESS: "cloud-access", /** When enabled, blocks remote agent execution on the desktop (e.g., for enterprise orgs). */ DISABLE_REMOTE_AGENT: "disable-remote-agent", - /** Gates access to V2 Cloud features (host-service, cloud sprites). */ - V2_CLOUD: "v2-cloud", /** * Gates the Automations feature in the UI (sidebar entry, routes, create * flow). Complementary to the subscriptions.plan paid-tier check —