"use client"; import { ArrowRight, ChevronRight, CornerLeftUp, Folder, FolderOpen, FolderRoot, Loader2, SkipForward, } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; import { useCallback, useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; import { authFetch } from "@/lib/auth"; import { cn } from "@/lib/utils"; interface StepDevRootProps { onNext: () => void; onBack: () => void; } const SUGGESTED_PATHS = ["~/Projects", "~/Developer", "~/Code", "~/dev"]; // ─── Inline folder browser ────────────────────────────────────────── interface BrowseEntry { name: string; path: string; } function InlineFolderBrowser({ onSelect, onCancel, }: { onSelect: (path: string) => void; onCancel: () => void; }) { const [currentPath, setCurrentPath] = useState(""); const [parentPath, setParentPath] = useState(null); const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const browse = useCallback(async (targetPath?: string) => { setLoading(true); setError(null); try { const param = targetPath ? `?path=${encodeURIComponent(targetPath)}` : ""; const res = await authFetch(`/api/browse-directories${param}`); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error((body as { error?: string }).error ?? `${res.status}`); } const data = (await res.json()) as { current: string; parent: string | null; entries: BrowseEntry[]; }; setCurrentPath(data.current); setParentPath(data.parent); setEntries(data.entries); } catch (err) { setError(err instanceof Error ? err.message : "Failed to browse"); } finally { setLoading(false); } }, []); useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect -- async fetch, setState runs after await void browse(); }, [browse]); return (
{/* Current path */}

{currentPath}

{/* Directory listing */}
{loading && (
)} {error && (
{error}
)} {!loading && !error && ( <> {parentPath && ( )} {entries.map((entry) => ( ))} {entries.length === 0 && !parentPath && (
No subdirectories
)} )}
{/* Cancel */}
); } // ─── Main step ────────────────────────────────────────────────────── export function StepDevRoot({ onNext, onBack }: StepDevRootProps) { const [path, setPath] = useState(""); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [browsing, setBrowsing] = useState(false); const handleSuggestionClick = useCallback((suggestion: string) => { setPath(suggestion); setError(null); }, []); const handleContinue = useCallback(async () => { const trimmed = path.trim(); if (!trimmed) { setError("Enter a path or skip this step"); return; } setSaving(true); setError(null); try { const res = await authFetch("/api/preferences", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ devRoot: trimmed }), }); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error( (body as { error?: string }).error ?? `Request failed (${res.status})`, ); } onNext(); } catch (err) { setError( err instanceof Error ? err.message : "Failed to save preference", ); } finally { setSaving(false); } }, [path, onNext]); return (
{/* Icon */}
Dev root The folder that contains your projects. SF discovers and manages workspaces inside it. {/* Input + browse */} {browsing ? ( { setPath(selected); setBrowsing(false); setError(null); }} onCancel={() => setBrowsing(false)} /> ) : (
{ setPath(e.target.value); if (error) setError(null); }} placeholder="/Users/you/Projects" className={cn( "h-11 flex-1 font-mono text-sm", error && "border-destructive/50 focus-visible:ring-destructive/30", )} data-testid="onboarding-devroot-input" autoFocus onKeyDown={(e) => { if (e.key === "Enter" && path.trim()) { void handleContinue(); } }} />
{error && (

{error}

)} {/* Suggestions */}
{SUGGESTED_PATHS.map((suggestion) => ( ))}
)}
{/* Navigation */}
); }