From 11119959968b43a4bd896f3bf2ade7aa69c0b481 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Wed, 6 May 2026 02:00:53 -0700 Subject: [PATCH 1/5] fix(desktop): drop onboarding auto-skip, switch SetupButton to design system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The auto-advance logic in onboarding steps was rerouting users away from pages they explicitly navigated to (e.g. clicking "Configure Codex" dropped them on /setup/gh-cli because Claude was already connected). The gate intended to suppress this — `manualWalkthrough` — was never set to true anywhere, so the auto-skip fired unconditionally. Removes: - `shouldAutoAdvance` effects in providers, gh-cli, and project steps - The no-projects / no-worktrees auto-finish in adopt-worktrees - The dead `manualWalkthrough` flag and its action Per-step "Skip for now" buttons are unchanged. Also rewrites SetupButton as a thin wrapper around @superset/ui/button, mapping primary/secondary/link to the design system's variants and the xs size, so onboarding CTAs share theming, focus ring, and hover behavior with the rest of the app. --- .../setup/adopt-worktrees/page.tsx | 34 ++----------------- .../components/SetupButton/SetupButton.tsx | 29 ++++++++-------- .../_authenticated/setup/gh-cli/page.tsx | 18 +--------- .../_authenticated/setup/project/page.tsx | 16 +-------- .../_authenticated/setup/providers/page.tsx | 23 ++----------- .../stores/onboarding/onboardingStore.ts | 10 ------ bun.lock | 2 +- 7 files changed, 23 insertions(+), 109 deletions(-) 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 0952ee7ad75..0dca81bd757 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 @@ -33,10 +33,6 @@ function OnboardingAdoptWorktreesPage() { const goTo = useOnboardingStore((s) => s.goTo); const markComplete = useOnboardingStore((s) => s.markComplete); const markSkipped = useOnboardingStore((s) => s.markSkipped); - const manualWalkthrough = useOnboardingStore((s) => s.manualWalkthrough); - const setManualWalkthrough = useOnboardingStore( - (s) => s.setManualWalkthrough, - ); const utils = electronTrpc.useUtils(); const isV2CloudEnabled = useIsV2CloudEnabled(); @@ -98,9 +94,8 @@ function OnboardingAdoptWorktreesPage() { duration_ms: startedAt ? Date.now() - startedAt : null, }); markComplete("adopt-worktrees"); - setManualWalkthrough(false); void navigateAfterFlow(true); - }, [markComplete, setManualWalkthrough, navigateAfterFlow]); + }, [markComplete, navigateAfterFlow]); const skipFlow = useCallback(() => { const startedAt = useOnboardingStore.getState().startedAt; @@ -109,9 +104,8 @@ function OnboardingAdoptWorktreesPage() { duration_ms: startedAt ? Date.now() - startedAt : null, }); markSkipped("adopt-worktrees"); - setManualWalkthrough(false); void navigateAfterFlow(true); - }, [markSkipped, setManualWalkthrough, navigateAfterFlow]); + }, [markSkipped, navigateAfterFlow]); if (isPending) { return ( @@ -121,31 +115,15 @@ function OnboardingAdoptWorktreesPage() { ); } - if (!projects || projects.length === 0) { - return ; - } - return ( ({ id: p.id, name: p.name }))} + projects={(projects ?? []).map((p) => ({ id: p.id, name: p.name }))} onSkip={skipFlow} onFinish={finishFlow} - manualWalkthrough={manualWalkthrough} /> ); } -function AutoAdvance({ onAdvance }: { onAdvance: () => void }) { - useEffect(() => { - onAdvance(); - }, [onAdvance]); - return ( -
- -
- ); -} - interface ProjectResult { worktrees: ExternalWorktree[]; loaded: boolean; @@ -155,14 +133,12 @@ interface AdoptWorktreesContentProps { projects: { id: string; name: string }[]; onSkip: () => void; onFinish: () => void; - manualWalkthrough: boolean; } function AdoptWorktreesContent({ projects, onSkip, onFinish, - manualWalkthrough, }: AdoptWorktreesContentProps) { const importExternalWorktrees = useImportExternalWorktrees(); const [results, setResults] = useState>({}); @@ -179,10 +155,6 @@ function AdoptWorktreesContent({ ); const totalSelected = useMemo(() => countSelected(selected), [selected]); - useEffect(() => { - if (allLoaded && total === 0 && !manualWalkthrough) onFinish(); - }, [allLoaded, total, manualWalkthrough, onFinish]); - const handleResult = useCallback( (projectId: string, worktrees: ExternalWorktree[]) => { setResults((prev) => ({ diff --git a/apps/desktop/src/renderer/routes/_authenticated/setup/components/SetupButton/SetupButton.tsx b/apps/desktop/src/renderer/routes/_authenticated/setup/components/SetupButton/SetupButton.tsx index e34cd5eafba..21f526f5a06 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/setup/components/SetupButton/SetupButton.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/setup/components/SetupButton/SetupButton.tsx @@ -1,35 +1,36 @@ +import { Button } from "@superset/ui/button"; import { cn } from "@superset/ui/utils"; -import type { ButtonHTMLAttributes, ReactNode } from "react"; +import type { ComponentProps } from "react"; type Variant = "primary" | "secondary" | "link"; -interface SetupButtonProps extends ButtonHTMLAttributes { +interface SetupButtonProps + extends Omit, "variant" | "size"> { variant?: Variant; - children: ReactNode; } -const VARIANT_STYLES: Record = { - primary: - "relative h-[28px] w-full rounded-[4px] border border-[rgba(255,136,70,0.8)] bg-[rgba(255,91,0,0.8)] px-2 text-[12px] font-medium text-[#eae8e6] shadow-[inset_0_1px_6.9px_0_rgba(255,255,255,0.14)] transition-colors hover:bg-[rgba(255,91,0,0.95)] disabled:opacity-60", - secondary: - "relative h-[28px] w-full rounded-[4px] border border-[#2a2827] bg-[#201e1c] px-2 text-[12px] font-medium text-[#eae8e6] shadow-[inset_0_1px_6.9px_0_rgba(255,255,255,0.1)] transition-colors hover:bg-[#2a2827] disabled:opacity-60", - link: "text-[12px] font-medium text-[#a8a5a3] underline-offset-4 transition-colors hover:text-[#eae8e6] hover:underline", -}; +const VARIANT_MAP = { + primary: "default", + secondary: "secondary", + link: "link", +} as const satisfies Record["variant"]>; export function SetupButton({ variant = "primary", className, - children, type = "button", + children, ...rest }: SetupButtonProps) { return ( - + ); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/setup/gh-cli/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/setup/gh-cli/page.tsx index 0adbe361492..2995f0ee477 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/setup/gh-cli/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/setup/gh-cli/page.tsx @@ -17,9 +17,6 @@ function OnboardingGhCliPage() { const goTo = useOnboardingStore((s) => s.goTo); const markComplete = useOnboardingStore((s) => s.markComplete); const markSkipped = useOnboardingStore((s) => s.markSkipped); - const completed = useOnboardingStore((s) => s.completed["gh-cli"]); - const skipped = useOnboardingStore((s) => s.skipped["gh-cli"]); - const manualWalkthrough = useOnboardingStore((s) => s.manualWalkthrough); const { data: ghStatus, @@ -32,19 +29,6 @@ function OnboardingGhCliPage() { goTo("gh-cli"); }, [goTo]); - const shouldAutoAdvance = - !completed && - !skipped && - !manualWalkthrough && - ghStatus?.installed === true; - - useEffect(() => { - if (shouldAutoAdvance) { - markComplete("gh-cli"); - navigate({ to: STEP_ROUTES.permissions, replace: true }); - } - }, [shouldAutoAdvance, markComplete, navigate]); - const handleSkip = () => { markSkipped("gh-cli"); navigate({ to: STEP_ROUTES.permissions }); @@ -54,7 +38,7 @@ function OnboardingGhCliPage() { navigate({ to: STEP_ROUTES.permissions }); }; - if (isPending || shouldAutoAdvance) { + if (isPending) { return (
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 9392e36d0f8..615bdc076f0 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/setup/project/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/setup/project/page.tsx @@ -31,11 +31,6 @@ function OnboardingProjectPage() { const goTo = useOnboardingStore((s) => s.goTo); const markComplete = useOnboardingStore((s) => s.markComplete); const markSkipped = useOnboardingStore((s) => s.markSkipped); - const completed = useOnboardingStore((s) => s.completed.project); - const manualWalkthrough = useOnboardingStore((s) => s.manualWalkthrough); - const setManualWalkthrough = useOnboardingStore( - (s) => s.setManualWalkthrough, - ); const { data: projects, isPending } = electronTrpc.projects.getRecents.useQuery(); @@ -65,14 +60,6 @@ function OnboardingProjectPage() { const projectCount = projects?.length ?? 0; const hasProjects = projectCount > 0; - const shouldAutoAdvance = !completed && !manualWalkthrough && hasProjects; - - useEffect(() => { - if (shouldAutoAdvance) { - markComplete("project"); - navigate({ to: STEP_ROUTES["adopt-worktrees"], replace: true }); - } - }, [shouldAutoAdvance, markComplete, navigate]); // After creating a new project, route to the project page in v2 — that's // where the user creates their first proper worktree workspace (which sets @@ -114,7 +101,6 @@ function OnboardingProjectPage() { const project = created[0]; if (project) { markComplete("project"); - setManualWalkthrough(false); await openProjectInWorkspace(project.id); } }; @@ -129,7 +115,7 @@ function OnboardingProjectPage() { navigate({ to: STEP_ROUTES["adopt-worktrees"] }); }; - if (isPending || shouldAutoAdvance) { + if (isPending) { return (
diff --git a/apps/desktop/src/renderer/routes/_authenticated/setup/providers/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/setup/providers/page.tsx index 827f619647f..a71614e9790 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/setup/providers/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/setup/providers/page.tsx @@ -1,7 +1,7 @@ import { chatServiceTrpc } from "@superset/chat/client"; import { Spinner } from "@superset/ui/spinner"; import { createFileRoute, useNavigate } from "@tanstack/react-router"; -import { type ReactNode, useEffect, useRef, useState } from "react"; +import { type ReactNode, useEffect, useState } from "react"; import { LuKeyRound, LuSettings } from "react-icons/lu"; import { STEP_ROUTES, useOnboardingStore } from "renderer/stores/onboarding"; import { SetupButton } from "../components/SetupButton"; @@ -21,8 +21,6 @@ function OnboardingProvidersPage() { const goTo = useOnboardingStore((s) => s.goTo); const markComplete = useOnboardingStore((s) => s.markComplete); const markSkipped = useOnboardingStore((s) => s.markSkipped); - const completed = useOnboardingStore((s) => s.completed.providers); - const manualWalkthrough = useOnboardingStore((s) => s.manualWalkthrough); const { data: anthropicAuthStatus, isPending: isAnthropicPending } = chatServiceTrpc.auth.getAnthropicStatus.useQuery(); @@ -36,13 +34,6 @@ function OnboardingProvidersPage() { const isStatusPending = isAnthropicPending || isOpenAIPending; const atLeastOneConnected = claudeConnected || codexConnected; - const wasConfiguredOnMount = useRef(null); - useEffect(() => { - if (!isStatusPending && wasConfiguredOnMount.current === null) { - wasConfiguredOnMount.current = atLeastOneConnected; - } - }, [isStatusPending, atLeastOneConnected]); - const [claudeMethod, setClaudeMethod] = useState("oauth"); const [codexMethod, setCodexMethod] = useState("oauth"); const [reconfiguringClaude, setReconfiguringClaude] = useState(false); @@ -52,17 +43,7 @@ function OnboardingProvidersPage() { goTo("providers"); }, [goTo]); - const shouldAutoAdvance = - !completed && !manualWalkthrough && wasConfiguredOnMount.current === true; - - useEffect(() => { - if (shouldAutoAdvance) { - markComplete("providers"); - navigate({ to: STEP_ROUTES["gh-cli"], replace: true }); - } - }, [shouldAutoAdvance, markComplete, navigate]); - - if (isStatusPending || shouldAutoAdvance) { + if (isStatusPending) { return (
diff --git a/apps/desktop/src/renderer/stores/onboarding/onboardingStore.ts b/apps/desktop/src/renderer/stores/onboarding/onboardingStore.ts index 2fbffdfa528..b862acedd3c 100644 --- a/apps/desktop/src/renderer/stores/onboarding/onboardingStore.ts +++ b/apps/desktop/src/renderer/stores/onboarding/onboardingStore.ts @@ -44,12 +44,6 @@ interface OnboardingState { skipped: Record; startedAt: number | null; completedAt: number | null; - /** - * When true, the user explicitly restarted onboarding from Settings. - * Steps must NOT auto-advance based on already-satisfied prerequisites — - * the user wants to walk through. Cleared when the flow finishes. - */ - manualWalkthrough: boolean; } interface OnboardingActions { @@ -59,7 +53,6 @@ interface OnboardingActions { next: () => OnboardingStep | null; back: () => OnboardingStep | null; reset: () => void; - setManualWalkthrough: (value: boolean) => void; } type OnboardingStore = OnboardingState & OnboardingActions; @@ -70,7 +63,6 @@ const initialState: OnboardingState = { skipped: { ...STEP_FLAGS_INITIAL }, startedAt: null, completedAt: null, - manualWalkthrough: false, }; function getNextStep(step: OnboardingStep): OnboardingStep | null { @@ -150,10 +142,8 @@ export const useOnboardingStore = create()( skipped: { ...STEP_FLAGS_INITIAL }, startedAt: null, completedAt: null, - manualWalkthrough: false, }); }, - setManualWalkthrough: (value) => set({ manualWalkthrough: value }), }), { name: "superset-onboarding-v1" }, ), diff --git a/bun.lock b/bun.lock index 63d814f1cd9..3b6a4eb82ff 100644 --- a/bun.lock +++ b/bun.lock @@ -111,7 +111,7 @@ }, "apps/desktop": { "name": "@superset/desktop", - "version": "1.8.4", + "version": "1.8.5", "dependencies": { "@ai-sdk/anthropic": "^3.0.43", "@ai-sdk/openai": "3.0.36", From def431e3d017773650ca2ebc5c33d367709cccc6 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Wed, 6 May 2026 02:08:11 -0700 Subject: [PATCH 2/5] fix(desktop): create v2 projects from onboarding when v2 is enabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The onboarding "Select new repo" button always called the v1 projects.openNew IPC mutation, which writes a v1 project row in the local Electron SQLite. When v2 is enabled, navigating to /project/$projectId then loads a v2 project page that has no matching cloud row, leaving the user with a stranded v1 project. Routes the v2 case through useFolderFirstImport — the same hook the v2 dashboard's "Import folder" uses — so it goes through client.project.create on the host service and ends up as a real v2 project + main workspace. Falls back to the existing v1 path otherwise. The /new-project clone flow is still v1-only and is reached from many places outside onboarding; left as a follow-up. --- .../_authenticated/setup/project/page.tsx | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) 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 615bdc076f0..04f60a578e6 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/setup/project/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/setup/project/page.tsx @@ -17,6 +17,7 @@ import { LuFolder, LuX } from "react-icons/lu"; import { useIsV2CloudEnabled } from "renderer/hooks/useIsV2CloudEnabled"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { useOpenProject } from "renderer/react-query/projects/useOpenProject"; +import { useFolderFirstImport } from "renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/hooks/useFolderFirstImport"; import { STEP_ROUTES, useOnboardingStore } from "renderer/stores/onboarding"; import { SetupButton } from "../components/SetupButton"; import { StepHeader, StepShell, SupersetPill } from "../components/StepShell"; @@ -37,6 +38,9 @@ function OnboardingProjectPage() { const { openNew, isPending: isOpenPending } = useOpenProject(); const utils = electronTrpc.useUtils(); const isV2CloudEnabled = useIsV2CloudEnabled(); + const folderImport = useFolderFirstImport({ + onError: (message) => toast.error(message), + }); const closeProject = electronTrpc.projects.close.useMutation({ onSuccess: async () => { await utils.projects.getRecents.invalidate(); @@ -61,20 +65,7 @@ function OnboardingProjectPage() { const projectCount = projects?.length ?? 0; const hasProjects = projectCount > 0; - // After creating a new project, route to the project page in v2 — that's - // where the user creates their first proper worktree workspace (which sets - // up the v2 pane layout). The default "branch" workspace auto-created by - // openNew → ensureMainWorkspace has no pane layout, so navigating there - // renders an empty workspace surface. In v1, navigate to the existing - // branch workspace as before. - const openProjectInWorkspace = async (projectId: string) => { - if (isV2CloudEnabled) { - navigate({ - to: "/project/$projectId", - params: { projectId }, - }); - return; - } + const openV1ProjectInWorkspace = async (projectId: string) => { try { const grouped = await utils.workspaces.getAllGrouped.fetch(); const wsForProject = grouped @@ -97,11 +88,23 @@ function OnboardingProjectPage() { }; const handleSelectNewRepo = async () => { + if (isV2CloudEnabled) { + const result = await folderImport.start(); + if (result) { + markComplete("project"); + navigate({ + to: "/project/$projectId", + params: { projectId: result.projectId }, + }); + } + return; + } + const created = await openNew(); const project = created[0]; if (project) { markComplete("project"); - await openProjectInWorkspace(project.id); + await openV1ProjectInWorkspace(project.id); } }; From f23aa745227d1e7e86f2d326bc32e8410479bc5d Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Wed, 6 May 2026 02:13:56 -0700 Subject: [PATCH 3/5] refactor(desktop): make onboarding project step v2-only v1 is deprecated; the onboarding flow shouldn't read from or write to v1 anymore. Drops the isV2CloudEnabled gate and all v1 paths from the project setup step: - Project list now reads collections.v2Projects directly via useLiveQuery, scoped to the active org. Replaces the v1 electronTrpc.projects.getRecents query. - "Select new repo" always goes through useFolderFirstImport (client.project.create on the host service). No more v1 fallback. - "Clone from GitHub" is removed because /new-project is still v1. Folder-first import is the only supported entry until a v2 clone surface lands. - Per-row delete is removed. v2 client.project.remove deletes from the org and is too heavy for an onboarding affordance; users can manage projects from settings/projects. The next adopt-worktrees step is still v1-only (no v2 equivalent for external worktree adoption exists). Left as a follow-up. --- .../_authenticated/setup/project/page.tsx | 181 +++++------------- 1 file changed, 46 insertions(+), 135 deletions(-) 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 04f60a578e6..91fca84e5f2 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/setup/project/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/setup/project/page.tsx @@ -1,24 +1,16 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@superset/ui/alert-dialog"; import { toast } from "@superset/ui/sonner"; import { Spinner } from "@superset/ui/spinner"; +import { eq } from "@tanstack/db"; +import { useLiveQuery } from "@tanstack/react-db"; import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { useEffect } from "react"; -import { LuFolder, LuX } from "react-icons/lu"; -import { useIsV2CloudEnabled } from "renderer/hooks/useIsV2CloudEnabled"; -import { electronTrpc } from "renderer/lib/electron-trpc"; -import { useOpenProject } from "renderer/react-query/projects/useOpenProject"; +import { LuFolder } from "react-icons/lu"; +import { env } from "renderer/env.renderer"; +import { authClient } from "renderer/lib/auth-client"; import { useFolderFirstImport } from "renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/hooks/useFolderFirstImport"; +import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import { STEP_ROUTES, useOnboardingStore } from "renderer/stores/onboarding"; +import { MOCK_ORG_ID } from "shared/constants"; import { SetupButton } from "../components/SetupButton"; import { StepHeader, StepShell, SupersetPill } from "../components/StepShell"; import { SupersetIcon } from "../providers/components/SupersetIcon"; @@ -33,78 +25,46 @@ function OnboardingProjectPage() { const markComplete = useOnboardingStore((s) => s.markComplete); const markSkipped = useOnboardingStore((s) => s.markSkipped); - const { data: projects, isPending } = - electronTrpc.projects.getRecents.useQuery(); - const { openNew, isPending: isOpenPending } = useOpenProject(); - const utils = electronTrpc.useUtils(); - const isV2CloudEnabled = useIsV2CloudEnabled(); + const collections = useCollections(); + const { data: session } = authClient.useSession(); + const activeOrganizationId = env.SKIP_ENV_VALIDATION + ? MOCK_ORG_ID + : (session?.session?.activeOrganizationId ?? null); + + const { data: projects = [], isLoading } = useLiveQuery( + (q) => + q + .from({ projects: collections.v2Projects }) + .where(({ projects }) => + eq(projects.organizationId, activeOrganizationId ?? ""), + ) + .select(({ projects }) => ({ + id: projects.id, + name: projects.name, + repoCloneUrl: projects.repoCloneUrl, + })), + [collections, activeOrganizationId], + ); + const folderImport = useFolderFirstImport({ onError: (message) => toast.error(message), }); - const closeProject = electronTrpc.projects.close.useMutation({ - onSuccess: async () => { - await utils.projects.getRecents.invalidate(); - }, - }); - - const handleRemoveProject = async (id: string, name: string) => { - try { - await closeProject.mutateAsync({ id }); - toast.success(`Removed ${name}`); - } catch (err) { - toast.error( - err instanceof Error ? err.message : "Failed to remove project", - ); - } - }; useEffect(() => { goTo("project"); }, [goTo]); - const projectCount = projects?.length ?? 0; + const projectCount = projects.length; const hasProjects = projectCount > 0; - const openV1ProjectInWorkspace = async (projectId: string) => { - try { - const grouped = await utils.workspaces.getAllGrouped.fetch(); - const wsForProject = grouped - .flatMap((g) => g.workspaces) - .find((w) => w.projectId === projectId); - if (wsForProject) { - navigate({ - to: "/workspace/$workspaceId", - params: { workspaceId: wsForProject.id }, - }); - return; - } - } catch { - // fall through - } - navigate({ - to: "/project/$projectId", - params: { projectId }, - }); - }; - const handleSelectNewRepo = async () => { - if (isV2CloudEnabled) { - const result = await folderImport.start(); - if (result) { - markComplete("project"); - navigate({ - to: "/project/$projectId", - params: { projectId: result.projectId }, - }); - } - return; - } - - const created = await openNew(); - const project = created[0]; - if (project) { + const result = await folderImport.start(); + if (result) { markComplete("project"); - await openV1ProjectInWorkspace(project.id); + navigate({ + to: "/project/$projectId", + params: { projectId: result.projectId }, + }); } }; @@ -118,7 +78,7 @@ function OnboardingProjectPage() { navigate({ to: STEP_ROUTES["adopt-worktrees"] }); }; - if (isPending) { + if (isLoading) { return (
@@ -134,7 +94,7 @@ function OnboardingProjectPage() { ); - if (hasProjects && projects) { + if (hasProjects) { return ( (
@@ -157,43 +117,12 @@ function OnboardingProjectPage() {

{project.name}

-

- {project.mainRepoPath} -

+ {project.repoCloneUrl && ( +

+ {project.repoCloneUrl} +

+ )}
- - - - - - - - Remove {project.name}? - - - This removes the project and its tracked workspaces from - Superset. The folder on disk and your git history are - untouched — you can re-add it any time. - - - - Cancel - - handleRemoveProject(project.id, project.name) - } - > - Remove - - - -
))}
@@ -203,18 +132,8 @@ function OnboardingProjectPage() { Continue with current - - {isOpenPending ? "Opening…" : "Select new repo"} - - navigate({ to: "/new-project" })} - > - Clone from GitHub + + Select new repo Skip for now @@ -233,15 +152,7 @@ function OnboardingProjectPage() { />
- - {isOpenPending ? "Opening…" : "Select new repo"} - - navigate({ to: "/new-project" })} - > - Clone from GitHub - + Select new repo Skip for now From d5d0c5d057f8fd183cf538529adaa5a8f88e1ed7 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Wed, 6 May 2026 02:19:30 -0700 Subject: [PATCH 4/5] fix(desktop): keep onboarding flow after import in project step handleSelectNewRepo navigated to /project/$projectId immediately after the folder-first import resolved, kicking the user out of the onboarding flow. The v2 project page also rendered before Electric had synced the new v2_projects row into collections.v2Projects, so it looked like nothing was added. Stay on /setup/project after import. The live query against collections.v2Projects updates once Electric streams the new row in, the project shows up in the on-page list, and the user clicks "Continue with current" to advance. --- .../src/renderer/routes/_authenticated/setup/project/page.tsx | 4 ---- 1 file changed, 4 deletions(-) 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 91fca84e5f2..4d87de3c329 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/setup/project/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/setup/project/page.tsx @@ -61,10 +61,6 @@ function OnboardingProjectPage() { const result = await folderImport.start(); if (result) { markComplete("project"); - navigate({ - to: "/project/$projectId", - params: { projectId: result.projectId }, - }); } }; From 85125db5f501cf580e7ab1ab6bf5efac001f641e Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Wed, 6 May 2026 12:21:28 -0700 Subject: [PATCH 5/5] feat(desktop): make onboarding v2-compatible end to end MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - adopt-worktrees: drop v1 fallbacks (useIsV2CloudEnabled / projects navigation / lastViewedWorkspaceId), drop the v1 importExternalWorktrees path. Land users on /v2-workspaces and let the dashboard route handle empty/last-workspace logic instead of duplicating it here. - adopt-worktrees: per-project ensureV2Project + finalizeProjectSetup, then submit each worktree as a v2 workspace via useWorkspaceCreates and pin via ensureWorkspaceInSidebar. "Importing X of Y…" progress, stay on page if every selected import fails, "Connecting…" gating until the host service is up. - setup/project: gate "Select new repo" / "Continue with current" buttons on activeHostUrl with a "Connecting…" label so v2 actions can't fire before the host returns a port. - providers: focus one provider at a time during configure/reconfigure, smart-default to the unconnected provider when one is connected, "Set up X instead" switcher when neither is connected, hide Continue while any step is active. - V2AvailableBanner: only render for v1 users; action routes to /settings/experimental ("Try new version of Superset") instead of flipping setOptInV2 directly. - New useEnsureV2Project hook: findByPath → setup, falling through to project.create only on TRPC NOT_FOUND so transient errors don't mint duplicate cloud projects. --- .../V2AvailableBanner/V2AvailableBanner.tsx | 18 +- .../hooks/useEnsureV2Project/index.ts | 4 + .../useEnsureV2Project/useEnsureV2Project.ts | 66 +++++ .../setup/adopt-worktrees/page.tsx | 196 ++++++++------- .../_authenticated/setup/project/page.tsx | 15 +- .../_authenticated/setup/providers/page.tsx | 233 +++++++++++------- 6 files changed, 345 insertions(+), 187 deletions(-) create mode 100644 apps/desktop/src/renderer/hooks/useEnsureV2Project/index.ts create mode 100644 apps/desktop/src/renderer/hooks/useEnsureV2Project/useEnsureV2Project.ts diff --git a/apps/desktop/src/renderer/components/V2AvailableBanner/V2AvailableBanner.tsx b/apps/desktop/src/renderer/components/V2AvailableBanner/V2AvailableBanner.tsx index a66259e1eca..d6c166402a4 100644 --- a/apps/desktop/src/renderer/components/V2AvailableBanner/V2AvailableBanner.tsx +++ b/apps/desktop/src/renderer/components/V2AvailableBanner/V2AvailableBanner.tsx @@ -1,19 +1,19 @@ import { SidebarCard } from "@superset/ui/sidebar-card"; +import { useNavigate } from "@tanstack/react-router"; 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); + const navigate = useNavigate(); - function handleSwitch() { - track("surface_toggled", { from: "v1", to: "v2", source: "v1_banner" }); - setOptInV2(true); + function handleManage() { + track("v2_banner_manage_clicked"); + navigate({ to: "/settings/experimental" }); } function handleDismiss() { @@ -21,6 +21,8 @@ export function V2AvailableBanner() { dismiss(); } + if (isV2CloudEnabled) return null; + return ( {!dismissed && ( @@ -34,9 +36,9 @@ export function V2AvailableBanner() { diff --git a/apps/desktop/src/renderer/hooks/useEnsureV2Project/index.ts b/apps/desktop/src/renderer/hooks/useEnsureV2Project/index.ts new file mode 100644 index 00000000000..858e388d8e7 --- /dev/null +++ b/apps/desktop/src/renderer/hooks/useEnsureV2Project/index.ts @@ -0,0 +1,4 @@ +export { + type EnsureV2ProjectResult, + useEnsureV2Project, +} from "./useEnsureV2Project"; diff --git a/apps/desktop/src/renderer/hooks/useEnsureV2Project/useEnsureV2Project.ts b/apps/desktop/src/renderer/hooks/useEnsureV2Project/useEnsureV2Project.ts new file mode 100644 index 00000000000..3857a09c03d --- /dev/null +++ b/apps/desktop/src/renderer/hooks/useEnsureV2Project/useEnsureV2Project.ts @@ -0,0 +1,66 @@ +import { useCallback } from "react"; +import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; +import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; + +export interface EnsureV2ProjectResult { + hostUrl: string; + projectId: string; + repoPath: string; + mainWorkspaceId: string | null; +} + +export function useEnsureV2Project(): (args: { + repoPath: string; + name: string; +}) => Promise { + const { activeHostUrl } = useLocalHostService(); + + return useCallback( + async ({ repoPath, name }) => { + if (!activeHostUrl) { + throw new Error("No active host service"); + } + const hostService = getHostServiceClientByUrl(activeHostUrl); + + const found = await hostService.project.findByPath.query({ repoPath }); + const candidate = found.candidates[0]; + if (candidate) { + try { + const setupResult = await hostService.project.setup.mutate({ + projectId: candidate.id, + mode: { kind: "import", repoPath }, + }); + return { + hostUrl: activeHostUrl, + projectId: candidate.id, + repoPath: setupResult.repoPath, + mainWorkspaceId: setupResult.mainWorkspaceId, + }; + } catch (err) { + // findByPath returns local sqlite rows even when no cloud v2 project + // exists for that id; setup → v2Project.get → NOT_FOUND. Only that + // case is safe to fall through to create — every other error + // (network, auth, 5xx) must propagate so retries don't silently mint + // duplicate cloud projects. + const code = (err as { data?: { code?: string } } | null | undefined) + ?.data?.code; + if (code !== "NOT_FOUND") { + throw err; + } + } + } + + const created = await hostService.project.create.mutate({ + name, + mode: { kind: "importLocal", repoPath }, + }); + return { + hostUrl: activeHostUrl, + projectId: created.projectId, + repoPath: created.repoPath, + mainWorkspaceId: created.mainWorkspaceId, + }; + }, + [activeHostUrl], + ); +} 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 0dca81bd757..af0f66f5fbd 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 @@ -4,11 +4,14 @@ import { Spinner } from "@superset/ui/spinner"; import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { GoGitBranch } from "react-icons/go"; -import { useIsV2CloudEnabled } from "renderer/hooks/useIsV2CloudEnabled"; +import { useEnsureV2Project } from "renderer/hooks/useEnsureV2Project"; import { track } from "renderer/lib/analytics"; import { electronTrpc } from "renderer/lib/electron-trpc"; -import { useImportExternalWorktrees } from "renderer/react-query/workspaces/useImportExternalWorktrees"; +import { useFinalizeProjectSetup } from "renderer/react-query/projects/useFinalizeProjectSetup"; +import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; +import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; import { STEP_ROUTES, useOnboardingStore } from "renderer/stores/onboarding"; +import { useWorkspaceCreates } from "renderer/stores/workspace-creates"; import { SetupButton } from "../components/SetupButton"; import { StepHeader, StepShell } from "../components/StepShell"; import { @@ -34,8 +37,6 @@ function OnboardingAdoptWorktreesPage() { const markComplete = useOnboardingStore((s) => s.markComplete); const markSkipped = useOnboardingStore((s) => s.markSkipped); - const utils = electronTrpc.useUtils(); - const isV2CloudEnabled = useIsV2CloudEnabled(); const { data: projects, isPending } = electronTrpc.projects.getRecents.useQuery(); @@ -43,48 +44,11 @@ function OnboardingAdoptWorktreesPage() { goTo("adopt-worktrees"); }, [goTo]); - const navigateAfterFlow = useCallback( - async (replace: boolean) => { - try { - const grouped = await utils.workspaces.getAllGrouped.fetch(); - const allWorkspaces = grouped.flatMap((g) => g.workspaces); - const lastViewedId = localStorage.getItem("lastViewedWorkspaceId"); - const candidates = isV2CloudEnabled - ? allWorkspaces.filter((w) => w.type === "worktree") - : allWorkspaces; - const target = - candidates.find((w) => w.id === lastViewedId) ?? candidates[0]; - if (target) { - if (isV2CloudEnabled) { - navigate({ - to: "/v2-workspace/$workspaceId", - params: { workspaceId: target.id }, - replace, - }); - } else { - navigate({ - to: "/workspace/$workspaceId", - params: { workspaceId: target.id }, - replace, - }); - } - return; - } - } catch { - // fall through to project / welcome routing - } - const project = projects?.[0]; - if (project) { - navigate({ - to: "/project/$projectId", - params: { projectId: project.id }, - replace, - }); - return; - } - navigate({ to: "/welcome", replace }); + const goToDashboard = useCallback( + (replace: boolean) => { + navigate({ to: "/v2-workspaces", replace }); }, - [utils, isV2CloudEnabled, navigate, projects], + [navigate], ); const finishFlow = useCallback(() => { @@ -94,8 +58,8 @@ function OnboardingAdoptWorktreesPage() { duration_ms: startedAt ? Date.now() - startedAt : null, }); markComplete("adopt-worktrees"); - void navigateAfterFlow(true); - }, [markComplete, navigateAfterFlow]); + goToDashboard(true); + }, [markComplete, goToDashboard]); const skipFlow = useCallback(() => { const startedAt = useOnboardingStore.getState().startedAt; @@ -104,8 +68,8 @@ function OnboardingAdoptWorktreesPage() { duration_ms: startedAt ? Date.now() - startedAt : null, }); markSkipped("adopt-worktrees"); - void navigateAfterFlow(true); - }, [markSkipped, navigateAfterFlow]); + goToDashboard(true); + }, [markSkipped, goToDashboard]); if (isPending) { return ( @@ -117,7 +81,11 @@ function OnboardingAdoptWorktreesPage() { return ( ({ id: p.id, name: p.name }))} + projects={(projects ?? []).map((p) => ({ + id: p.id, + name: p.name, + mainRepoPath: p.mainRepoPath, + }))} onSkip={skipFlow} onFinish={finishFlow} /> @@ -130,7 +98,7 @@ interface ProjectResult { } interface AdoptWorktreesContentProps { - projects: { id: string; name: string }[]; + projects: { id: string; name: string; mainRepoPath: string }[]; onSkip: () => void; onFinish: () => void; } @@ -140,9 +108,19 @@ function AdoptWorktreesContent({ onSkip, onFinish, }: AdoptWorktreesContentProps) { - const importExternalWorktrees = useImportExternalWorktrees(); + const { submit } = useWorkspaceCreates(); + const { machineId, activeHostUrl } = useLocalHostService(); + const ensureV2Project = useEnsureV2Project(); + const finalizeProjectSetup = useFinalizeProjectSetup(); + const { ensureWorkspaceInSidebar } = useDashboardSidebarState(); const [results, setResults] = useState>({}); const [selected, setSelected] = useState({}); + const [isImporting, setIsImporting] = useState(false); + const [progress, setProgress] = useState<{ + current: number; + total: number; + } | null>(null); + const hostNotReady = !activeHostUrl; const allLoaded = projects.every((p) => results[p.id]?.loaded); const total = useMemo( @@ -192,23 +170,73 @@ function AdoptWorktreesContent({ ); const handleImportSelected = async () => { + if (!machineId) { + toast.error("No active host"); + return; + } + const totalToImport = totalSelected; + setIsImporting(true); + setProgress({ current: 0, total: totalToImport }); let totalImported = 0; - for (const project of projects) { - const paths = Array.from(selected[project.id] ?? []); - if (paths.length === 0) continue; - try { - const result = await importExternalWorktrees.mutateAsync({ - projectId: project.id, - paths, - }); - totalImported += result.imported; - } catch (err) { - toast.error( - err instanceof Error - ? err.message - : `Failed to import worktrees for ${project.name}`, - ); + try { + for (const project of projects) { + const paths = Array.from(selected[project.id] ?? []); + if (paths.length === 0) continue; + const projectResult = results[project.id]; + if (!projectResult) continue; + + let v2ProjectId: string; + try { + const ensureResult = await ensureV2Project({ + repoPath: project.mainRepoPath, + name: project.name, + }); + v2ProjectId = ensureResult.projectId; + finalizeProjectSetup(ensureResult.hostUrl, { + projectId: ensureResult.projectId, + repoPath: ensureResult.repoPath, + mainWorkspaceId: ensureResult.mainWorkspaceId, + }); + } catch (err) { + toast.error( + err instanceof Error + ? `Could not link "${project.name}" to v2: ${err.message}` + : `Could not link "${project.name}" to v2`, + ); + continue; + } + + for (const path of paths) { + const wt = projectResult.worktrees.find((w) => w.path === path); + if (!wt) continue; + const result = await submit({ + hostId: machineId, + snapshot: { + id: crypto.randomUUID(), + projectId: v2ProjectId, + name: wt.branch, + branch: wt.branch, + }, + }); + if (result.ok) { + totalImported++; + ensureWorkspaceInSidebar(result.workspaceId, v2ProjectId); + } else { + toast.error(`Failed to import ${wt.branch}: ${result.error}`); + } + setProgress({ current: totalImported, total: totalToImport }); + } } + } finally { + setIsImporting(false); + setProgress(null); + } + + const hadSelections = projects.some((p) => (selected[p.id]?.size ?? 0) > 0); + if (hadSelections && totalImported === 0) { + // All selected imports failed. Errors were already toasted; keep the + // user here to retry rather than yanking them into the dashboard. + return; } if (totalImported > 0) { toast.success( @@ -225,11 +253,13 @@ function AdoptWorktreesContent({ @@ -264,22 +294,18 @@ function AdoptWorktreesContent({ - {importExternalWorktrees.isPending - ? "Importing…" - : totalSelected === 0 - ? "Select worktrees" - : `Import ${totalSelected} selected`} + {isImporting && progress + ? `Importing ${progress.current} of ${progress.total}…` + : hostNotReady + ? "Connecting…" + : totalSelected === 0 + ? "Select worktrees" + : `Import ${totalSelected} selected`} - + Skip for now 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 4d87de3c329..ae531495760 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/setup/project/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/setup/project/page.tsx @@ -9,6 +9,7 @@ import { env } from "renderer/env.renderer"; import { authClient } from "renderer/lib/auth-client"; import { useFolderFirstImport } from "renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/hooks/useFolderFirstImport"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; +import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; import { STEP_ROUTES, useOnboardingStore } from "renderer/stores/onboarding"; import { MOCK_ORG_ID } from "shared/constants"; import { SetupButton } from "../components/SetupButton"; @@ -49,6 +50,8 @@ function OnboardingProjectPage() { const folderImport = useFolderFirstImport({ onError: (message) => toast.error(message), }); + const { activeHostUrl } = useLocalHostService(); + const hostReady = activeHostUrl !== null; useEffect(() => { goTo("project"); @@ -128,8 +131,12 @@ function OnboardingProjectPage() { Continue with current - - Select new repo + + {hostReady ? "Select new repo" : "Connecting…"} Skip for now @@ -148,7 +155,9 @@ function OnboardingProjectPage() { />
- Select new repo + + {hostReady ? "Select new repo" : "Connecting…"} + Skip for now diff --git a/apps/desktop/src/renderer/routes/_authenticated/setup/providers/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/setup/providers/page.tsx index a71614e9790..8e44bbf71bf 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/setup/providers/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/setup/providers/page.tsx @@ -38,6 +38,14 @@ function OnboardingProvidersPage() { const [codexMethod, setCodexMethod] = useState("oauth"); const [reconfiguringClaude, setReconfiguringClaude] = useState(false); const [reconfiguringCodex, setReconfiguringCodex] = useState(false); + const [activeProvider, setActiveProvider] = useState<"claude" | "codex">( + "claude", + ); + + useEffect(() => { + if (claudeConnected && !codexConnected) setActiveProvider("codex"); + else if (!claudeConnected && codexConnected) setActiveProvider("claude"); + }, [claudeConnected, codexConnected]); useEffect(() => { goTo("providers"); @@ -74,115 +82,158 @@ function OnboardingProvidersPage() { ? "Add another provider or continue to the next step." : "Connect Claude Code, Codex, or both to get started."; + const inAnyStep = + !claudeConnected || + !codexConnected || + reconfiguringClaude || + reconfiguringCodex; + const effectiveProvider: "claude" | "codex" = reconfiguringClaude + ? "claude" + : reconfiguringCodex + ? "codex" + : activeProvider; + const showBothCompact = !inAnyStep; + const showClaude = showBothCompact || effectiveProvider === "claude"; + const showCodex = showBothCompact || effectiveProvider === "codex"; + // Only offer the switcher when neither provider is connected. Once one is + // connected, the auto-switch effect points activeProvider at the unconnected + // one, so "Set up instead" would just drop the user into a + // Reconfigure panel for an already-working provider. + const showSwitcher = + !claudeConnected && + !codexConnected && + !reconfiguringClaude && + !reconfiguringCodex; + const otherLabel = effectiveProvider === "claude" ? "Codex" : "Claude Code"; + return ( - - handleConnect("/setup/providers/claude-code", claudeMethod) - } - onReconfigure={() => setReconfiguringClaude(true)} - onCancelReconfigure={() => setReconfiguringClaude(false)} - connectedPanel={ - - } - title="Claude Code is connected" - /> - } - options={ - <> - + handleConnect("/setup/providers/claude-code", claudeMethod) + } + onReconfigure={() => setReconfiguringClaude(true)} + onCancelReconfigure={() => setReconfiguringClaude(false)} + connectedPanel={ + + } - title="Claude Pro/Max" - description="Use your Claude subscription for unlimited access." - recommended - selected={claudeMethod === "oauth"} - onSelect={() => setClaudeMethod("oauth")} - /> - } />} - title="Anthropic API Key" - description="Pay-as-you-go with your own API key." - selected={claudeMethod === "api-key"} - onSelect={() => setClaudeMethod("api-key")} - /> - } />} - title="Custom Model" - description="Use a custom base URL and model." - selected={claudeMethod === "custom"} - onSelect={() => setClaudeMethod("custom")} + title="Claude Code is connected" /> - - } - /> - - handleConnect("/setup/providers/codex", codexMethod)} - onReconfigure={() => setReconfiguringCodex(true)} - onCancelReconfigure={() => setReconfiguringCodex(false)} - connectedPanel={ - + + } + title="Claude Pro/Max" + description="Use your Claude subscription for unlimited access." + recommended + selected={claudeMethod === "oauth"} + onSelect={() => setClaudeMethod("oauth")} /> - } - title="Codex is connected" - /> - } - options={ - <> - } />} + title="Anthropic API Key" + description="Pay-as-you-go with your own API key." + selected={claudeMethod === "api-key"} + onSelect={() => setClaudeMethod("api-key")} + /> + } />} + title="Custom Model" + description="Use a custom base URL and model." + selected={claudeMethod === "custom"} + onSelect={() => setClaudeMethod("custom")} + /> + + } + /> + )} + + {showCodex && ( + handleConnect("/setup/providers/codex", codexMethod)} + onReconfigure={() => setReconfiguringCodex(true)} + onCancelReconfigure={() => setReconfiguringCodex(false)} + connectedPanel={ + } - title="ChatGPT Plus/Pro" - description="Use your ChatGPT subscription via Codex." - recommended - selected={codexMethod === "oauth"} - onSelect={() => setCodexMethod("oauth")} + title="Codex is connected" /> - } />} - title="OpenAI API Key" - description="Pay-as-you-go with your own API key." - selected={codexMethod === "api-key"} - onSelect={() => setCodexMethod("api-key")} - /> - } />} - title="Custom Model" - description="Use a custom base URL and model." - selected={codexMethod === "custom"} - onSelect={() => setCodexMethod("custom")} - /> - - } - /> + } + options={ + <> + + } + title="ChatGPT Plus/Pro" + description="Use your ChatGPT subscription via Codex." + recommended + selected={codexMethod === "oauth"} + onSelect={() => setCodexMethod("oauth")} + /> + } />} + title="OpenAI API Key" + description="Pay-as-you-go with your own API key." + selected={codexMethod === "api-key"} + onSelect={() => setCodexMethod("api-key")} + /> + } />} + title="Custom Model" + description="Use a custom base URL and model." + selected={codexMethod === "custom"} + onSelect={() => setCodexMethod("custom")} + /> + + } + /> + )}
- {atLeastOneConnected && ( + {atLeastOneConnected && !inAnyStep && ( Continue )} + {showSwitcher && ( + + setActiveProvider( + effectiveProvider === "claude" ? "codex" : "claude", + ) + } + > + Set up {otherLabel} instead + + )} Skip for now