"use client" import { LogoFull } from "@ui/assets/Logo" import { Button } from "@ui/components/button" import { cn } from "@lib/utils" import { ArrowRight, Check, CheckCircle2, ChevronLeft, FolderOpen, Info, Loader2, X, } from "lucide-react" import { useRouter } from "next/navigation" import { useCallback, useEffect, useState, type ReactNode } from "react" import { getSession, type AuthSession } from "@/lib/auth" import { completeDesktopOnboarding, getDesktopOnboardingStatus, skipDesktopOnboarding, type DesktopOnboardingStep, updateDesktopOnboardingStatus, } from "@/lib/onboarding" import { chooseSmfsMountPath, getDefaultSmfsContainerTag, getSmfsState, mountSmfs, type SmfsStatus, } from "@/lib/smfs" import { sortDesktopToolCards, toDesktopToolCard, type DesktopToolCard, } from "@/lib/tool-catalog" import { connectDesktopTool, detectDesktopTools, type DesktopToolId, type DesktopToolPreview, previewConnectDesktopTool, } from "@/lib/tools" const STEPS: DesktopOnboardingStep[] = [ "welcome", "tools", "filesystem", "done", ] const STEP_LABELS: Record = { welcome: "Welcome", tools: "Tools", filesystem: "Files", done: "Done", } const sourceCardStyle = { boxShadow: "0 2.842px 14.211px 0 rgba(0, 0, 0, 0.25), 0.711px 0.711px 0.711px 0 rgba(255, 255, 255, 0.10) inset", } const sourceIconStyle = { boxShadow: "0px 1px 2px 0px rgba(0,43,87,0.1), inset 0px 0px 0px 1px rgba(43,49,67,0.08), inset 0px 1px 1px 0px rgba(0,0,0,0.08), inset 0px 2px 4px 0px rgba(0,0,0,0.02)", } const novaCardClassName = "group relative flex min-h-[190px] flex-col overflow-hidden rounded-[18px] bg-[#1B1F24] p-5 transition-colors focus-within:ring-2 focus-within:ring-[#4BA0FA]/45" const novaIconBoxClassName = "flex size-12 shrink-0 items-center justify-center rounded-[12px] border border-[rgba(82,89,102,0.2)] bg-[#14161A]" export default function DesktopOnboardingPage() { const router = useRouter() const [session, setSession] = useState(null) const [authChecked, setAuthChecked] = useState(false) const [step, setStep] = useState("welcome") const [connectedTools, setConnectedTools] = useState([]) const [filesystemMounted, setFilesystemMounted] = useState(false) useEffect(() => { let cancelled = false getSession() .then((nextSession) => { if (cancelled) return const status = getDesktopOnboardingStatus(nextSession) setSession(nextSession) setStep(status.lastStep === "done" ? "welcome" : status.lastStep) setConnectedTools(status.connectedTools) setAuthChecked(true) }) .catch(() => { if (cancelled) return router.replace("/login") }) return () => { cancelled = true } }, [router]) const goToStep = useCallback( (nextStep: DesktopOnboardingStep) => { setStep(nextStep) updateDesktopOnboardingStatus({ lastStep: nextStep }, session) }, [session], ) const complete = useCallback( (nextConnectedTools = connectedTools) => { completeDesktopOnboarding(nextConnectedTools, session) router.replace("/") }, [connectedTools, router, session], ) const skip = useCallback(() => { skipDesktopOnboarding(step, session) router.replace("/") }, [router, session, step]) if (!authChecked || !session) { return (
Loading onboarding...
) } return (
{step === "welcome" ? : null} {step === "tools" ? ( ) : null} {step === "filesystem" ? ( goToStep("done")} onMountedChange={setFilesystemMounted} /> ) : null} {step === "done" ? ( ) : null}
0} filesystemMounted={filesystemMounted} onBack={() => { if (step === "tools") goToStep("welcome") if (step === "filesystem") goToStep("tools") if (step === "done") goToStep("filesystem") }} onPrimary={() => { if (step === "welcome") goToStep("tools") if (step === "tools") goToStep("filesystem") if (step === "filesystem") goToStep("done") if (step === "done") complete() }} onSkip={skip} />
) } function WelcomeStep() { return (

Set up Supermemory on this Mac.

Connect local AI tools, mount your memory folder, and make Nova available everywhere you work.

) } function ToolsStep({ session, onConnectedToolsChange, }: { session: AuthSession onConnectedToolsChange: (tools: DesktopToolId[]) => void }) { const [tools, setTools] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [busyTool, setBusyTool] = useState(null) const [preview, setPreview] = useState(null) const detectedCount = tools.filter((tool) => tool.detected).length const connectedCount = tools.filter((tool) => tool.connected).length const refreshTools = useCallback(async () => { setError(null) setLoading(true) try { const statuses = await detectDesktopTools() const cards = sortDesktopToolCards(statuses.map(toDesktopToolCard)) setTools(cards) const nextConnected = cards .filter((tool) => tool.connected) .map((tool) => tool.id) onConnectedToolsChange(nextConnected) updateDesktopOnboardingStatus({ connectedTools: nextConnected }, session) } catch (err) { setError(formatUnknownError(err, "Could not scan local tools")) } finally { setLoading(false) } }, [onConnectedToolsChange, session]) useEffect(() => { refreshTools() }, [refreshTools]) async function previewTool(toolId: DesktopToolId) { setError(null) setBusyTool(toolId) try { setPreview(await previewConnectDesktopTool(toolId)) } catch (err) { setError(formatUnknownError(err, "Could not preview setup")) } finally { setBusyTool(null) } } async function applyPreview() { if (!preview) return setError(null) setBusyTool(preview.tool.id) try { await connectDesktopTool(preview.tool.id) setPreview(null) await refreshTools() } catch (err) { setError(formatUnknownError(err, "Could not connect tool")) } finally { setBusyTool(null) } } return (
{tools.map((tool) => ( previewTool(tool.id)} /> ))}
{error ?

{error}

: null} {preview ? ( setPreview(null)} onApply={applyPreview} /> ) : null}
) } function FilesystemStep({ onContinue, onMountedChange, }: { onContinue: () => void onMountedChange: (mounted: boolean) => void }) { const [tag, setTag] = useState("sm_fs_desktop") const [status, setStatus] = useState(null) const [selectedMountPath, setSelectedMountPath] = useState( null, ) const [loading, setLoading] = useState(true) const [busy, setBusy] = useState(false) const [error, setError] = useState(null) const refresh = useCallback(async () => { setError(null) setLoading(true) try { const [defaultTag, statuses] = await Promise.all([ getDefaultSmfsContainerTag(), getSmfsState(), ]) setTag(defaultTag) const nextStatus = statuses[0] ?? null setStatus(nextStatus) if ( nextStatus?.mountPath && (nextStatus.mountPathConfigured || nextStatus.state === "mounted" || nextStatus.state === "external") ) { setSelectedMountPath(nextStatus.mountPath) } } catch (err) { setError(formatUnknownError(err, "Could not load filesystem status")) } finally { setLoading(false) } }, []) useEffect(() => { refresh() }, [refresh]) async function mount() { setError(null) setBusy(true) try { if (!selectedMountPath) { throw new Error("Choose a folder before mounting SMFS") } const nextStatus = await mountSmfs(tag, selectedMountPath) setStatus(nextStatus) setSelectedMountPath(nextStatus.mountPath) } catch (err) { setError(formatUnknownError(err, "Could not mount memory folder")) } finally { setBusy(false) } } async function chooseFolder() { setError(null) setBusy(true) try { const path = await chooseSmfsMountPath() if (path) { setSelectedMountPath(path) } } catch (err) { setError(formatUnknownError(err, "Could not choose mount folder")) } finally { setBusy(false) } } const mounted = status?.state === "mounted" || status?.state === "external" const mountPath = selectedMountPath ?? (status?.mountPathConfigured ? status.mountPath : null) const canMount = Boolean(mountPath) && !loading && !busy useEffect(() => { onMountedChange(mounted) }, [mounted, onMountedChange]) return (

Filesystem space

Local Supermemory folder for editors and command-line tools

{mountPath ? compactPath(mountPath) : "No folder selected"}

{error ?

{error}

: null}

Mount folder

Recommended

Choose or create the local folder SMFS should mount into

) } function FilesystemStatus({ mounted, loading, }: { mounted: boolean loading: boolean }) { if (loading) { return (
Checking
) } if (mounted) { return (
Mounted
) } return (
Unmounted
) } function DoneStep({ connectedCount }: { connectedCount: number }) { return (

Desktop setup is ready.

{connectedCount > 0 ? `${connectedCount} tool${connectedCount === 1 ? "" : "s"} connected. You can add more from Settings anytime.` : "No tools connected yet. You can come back from Settings whenever you are ready."}

) } function ToolCard({ tool, busy, onPreview, }: { tool: DesktopToolCard busy: boolean onPreview: () => void }) { const canPreview = !busy && !tool.connected return (

{tool.name}

{tool.id !== "codex" ? Pro : null}

{tool.tagline}

{tool.connected ? (
Connected
) : ( )}
    {tool.perks.map((perk) => (
  • {perk}
  • ))}

{tool.configPath}

) } function CardInfoButton({ href, name }: { href?: string; name: string }) { return ( { event.stopPropagation() }} className="absolute top-4 right-4 z-10 flex size-8 shrink-0 items-center justify-center rounded-full bg-[#0D121A] text-[#A1A1AA] opacity-0 shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)] transition-all hover:text-[#FAFAFA] focus-visible:opacity-100 focus-visible:outline-none group-hover:opacity-100" > ) } function NovaInlineChip({ children }: { children: ReactNode }) { return ( {children} ) } function ToolCardStatus({ tool }: { tool: DesktopToolCard }) { if (tool.connected) { return (
Active
) } return (
{tool.detected ? "Detected" : "Not detected"}
) } function DiffPreview({ preview, busy, onCancel, onApply, }: { preview: DesktopToolPreview busy: boolean onCancel: () => void onApply: () => void }) { return (

Review config change

{preview.configPath}

Backup: {preview.backupPath ?? "created only if the file exists"}

				{preview.diff}
			
) } function StepHeader({ eyebrow, title, description, meta, action, }: { eyebrow?: string title: string description?: string meta?: string action?: ReactNode }) { return (
{eyebrow ? (

{eyebrow}

) : null}

{title}

{description ? (

{description}

) : null} {meta ?

{meta}

: null}
{action}
) } function OnboardingFooter({ step, toolsConnected, filesystemMounted, onBack, onPrimary, onSkip, }: { step: DesktopOnboardingStep toolsConnected: boolean filesystemMounted: boolean onBack: () => void onPrimary: () => void onSkip: () => void }) { const isWelcome = step === "welcome" const primaryLabel = step === "welcome" ? "Start setup" : step === "tools" ? toolsConnected ? "Continue" : "Skip tools" : step === "filesystem" ? filesystemMounted ? "Continue" : "Skip for now" : "Open Supermemory" return (
) } function StepIndicator({ step }: { step: DesktopOnboardingStep }) { const currentIdx = STEPS.indexOf(step) return (
{STEPS.map((item, index) => { const isDone = index < currentIdx const isCurrent = index === currentIdx const isLast = index === STEPS.length - 1 return (
{isCurrent ? ( ) : null} {STEP_LABELS[item]}
{!isLast ? ( ) : null}
) })}
) } function NovaBackground() { return ( <>
) } function compactPath(path: string) { if (path.startsWith("/Users/")) { const parts = path.split("/") if (parts.length > 3) { return `~/${parts.slice(3).join("/")}` } } return path } function formatUnknownError(error: unknown, fallback: string) { return error instanceof Error ? error.message : typeof error === "string" ? error : fallback }