"use client" import { useCallback, useEffect, useState } from "react" import { motion, AnimatePresence } from "motion/react" import { ArrowRight, ChevronRight, CornerLeftUp, Folder, FolderOpen, FolderRoot, Loader2, SkipForward, } from "lucide-react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { ScrollArea } from "@/components/ui/scroll-area" import { cn } from "@/lib/utils" import { authFetch } from "@/lib/auth" 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(() => { 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. GSD 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 */}
) }