"use client" import { useEffect, useState, useCallback } from "react" import * as TabsPrimitive from "@radix-ui/react-tabs" import { CheckCircle2, Circle, Play, AlertTriangle, Clock, Download, Activity, GitBranch, ArrowRight, BarChart3, FileText, FileJson, Loader2, Layers, Bot, RotateCcw, ChevronRight, AlertCircle, } from "lucide-react" import { cn } from "@/lib/utils" import { useSFWorkspaceState, buildProjectUrl } from "@/lib/sf-workspace-store" import type { VisualizerData, VisualizerSlice, VisualizerTask, ProjectTotals, } from "@/lib/visualizer-types" import { formatCost, formatTokenCount, formatDuration, } from "@/lib/visualizer-types" import { authFetch } from "@/lib/auth" // ─── Design Tokens ──────────────────────────────────────────────────────────── // Tab definitions — single source of truth const TABS = [ { value: "progress", label: "Progress", Icon: Layers }, { value: "deps", label: "Dependencies", Icon: GitBranch }, { value: "metrics", label: "Metrics", Icon: BarChart3 }, { value: "timeline", label: "Timeline", Icon: Clock }, { value: "agent", label: "Agent", Icon: Bot }, { value: "changes", label: "Changes", Icon: Activity }, { value: "export", label: "Export", Icon: Download }, ] as const type TabValue = (typeof TABS)[number]["value"] // ─── Shared Primitives ──────────────────────────────────────────────────────── function statusIcon(status: "complete" | "active" | "pending" | "done") { switch (status) { case "complete": case "done": return case "active": return case "pending": return } } function taskStatusIcon(task: VisualizerTask) { if (task.done) return statusIcon("done") if (task.active) return statusIcon("active") return statusIcon("pending") } function RiskBadge({ risk }: { risk: string }) { const color = risk === "high" ? "bg-destructive/15 text-destructive border-destructive/25 ring-destructive/10" : risk === "medium" ? "bg-warning/15 text-warning border-warning/25 ring-warning/10" : "bg-success/15 text-success border-success/25 ring-success/10" return ( {risk} ) } function formatRelative(isoDate: string): string { const diff = Date.now() - new Date(isoDate).getTime() if (diff < 60_000) return "just now" const min = Math.floor(diff / 60_000) if (min < 60) return `${min}m ago` const hr = Math.floor(min / 60) if (hr < 24) return `${hr}h ago` return `${Math.floor(hr / 24)}d ago` } function formatTime(ts: number): string { const d = new Date(ts) return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}` } /** Prominent section label with left accent bar */ function SectionLabel({ children }: { children: React.ReactNode }) { return (

{children}

) } /** Large empty state with icon */ function EmptyState({ message, icon: Icon = AlertCircle }: { message: string; icon?: React.ComponentType<{ className?: string }> }) { return (

{message}

) } /** Metric card — key number with label */ function StatCard({ label, value, sub, accent, }: { label: string value: string sub?: string accent?: "sky" | "emerald" | "amber" | "default" }) { const accentClasses = { sky: "from-info/8 border-info/20", emerald: "from-success/8 border-success/20", amber: "from-warning/8 border-warning/20", default: "from-transparent border-border", }[accent ?? "default"] return (

{label}

{value}

{sub && (

{sub}

)}
) } /** Horizontal progress bar with label */ function ProgressBar({ value, max, color = "sky", animated = false, }: { value: number max: number color?: "sky" | "emerald" | "amber" animated?: boolean }) { const pct = max > 0 ? Math.max(1, (value / max) * 100) : 0 const barColor = { sky: "bg-info", emerald: "bg-success", amber: "bg-warning" }[color] return (
) } // ─── Progress Tab ───────────────────────────────────────────────────────────── function ProgressTab({ data }: { data: VisualizerData }) { if (data.milestones.length === 0) { return } const allSlices = data.milestones.flatMap((m) => m.slices) const riskCounts = { low: 0, medium: 0, high: 0 } for (const sl of allSlices) { if (sl.risk === "high") riskCounts.high++ else if (sl.risk === "medium") riskCounts.medium++ else riskCounts.low++ } return (
{/* Risk Heatmap */} {allSlices.length > 0 && (
Risk Heatmap
{data.milestones .filter((m) => m.slices.length > 0) .map((ms) => (
{ms.id}
{ms.slices.map((sl) => (
))}
))}
Low ({riskCounts.low}) Medium ({riskCounts.medium}) High ({riskCounts.high})
)} {/* Milestone tree */}
{data.milestones.map((ms) => (
{/* Milestone header */}
{statusIcon(ms.status)} {ms.id} {ms.title}
{ms.status}
{ms.status === "pending" && ms.dependsOn.length > 0 && (
Depends on {ms.dependsOn.join(", ")}
)} {/* Slices */} {ms.slices.length > 0 && (
{ms.slices.map((sl) => { const doneTasks = sl.tasks.filter((t) => t.done).length const slStatus = sl.done ? "done" : sl.active ? "active" : "pending" return (
{statusIcon(slStatus)} {sl.id} {sl.title}
{sl.depends.length > 0 && ( deps: {sl.depends.join(", ")} )} {sl.tasks.length > 0 && ( {doneTasks}/{sl.tasks.length} )}
{/* Tasks — only shown for active or partially-done slices */} {(sl.active || sl.tasks.some((t) => t.active)) && sl.tasks.length > 0 && (
{sl.tasks.map((task) => (
{taskStatusIcon(task)} {task.id} {task.title} {task.active && ( running )}
))}
)}
) })}
)}
))}
) } // ─── Deps Tab ───────────────────────────────────────────────────────────────── function DepsTab({ data }: { data: VisualizerData }) { const cp = data.criticalPath const activeMs = data.milestones.find((m) => m.status === "active") const milestoneDeps = data.milestones.filter((m) => m.dependsOn.length > 0) return (
{/* Milestone Dependencies */}
Milestone Dependencies
{milestoneDeps.length === 0 ? (

No milestone dependencies configured.

) : (
{milestoneDeps.flatMap((ms) => ms.dependsOn.map((dep) => (
{dep} {ms.id} {ms.title}
)), )}
)}
{/* Slice Dependencies */}
Slice Dependencies — Active Milestone
{!activeMs ? (

No active milestone.

) : ( (() => { const slDeps = activeMs.slices.filter((s) => s.depends.length > 0) if (slDeps.length === 0) return

No slice dependencies in {activeMs.id}.

return (
{slDeps.flatMap((sl) => sl.depends.map((dep) => (
{dep} {sl.id} {sl.title}
)), )}
) })() )}
{/* Critical Path */}
Critical Path
{cp.milestonePath.length === 0 ? (

No critical path data.

) : (
{/* Milestone chain */}

Milestone Chain

{cp.milestonePath.map((id, i) => ( {id} {i < cp.milestonePath.length - 1 && ( )} ))}
{/* Milestone slack */} {Object.keys(cp.milestoneSlack).length > 0 && (

Milestone Slack

{data.milestones .filter((m) => !cp.milestonePath.includes(m.id)) .map((m) => (
{m.id} {m.title} slack: {cp.milestoneSlack[m.id] ?? 0}
))}
)} {/* Slice critical path */} {cp.slicePath.length > 0 && (

Slice Critical Path

{cp.slicePath.map((id, i) => ( {id} {i < cp.slicePath.length - 1 && ( )} ))}
{/* Bottleneck warnings */} {activeMs && (
{cp.slicePath .map((sid) => activeMs.slices.find((s) => s.id === sid)) .filter( (sl): sl is VisualizerSlice => sl != null && !sl.done && !sl.active, ) .map((sl) => (
{sl.id} is on the critical path but not yet started
))}
)}
)} {/* Slice slack */} {Object.keys(cp.sliceSlack).length > 0 && (

Slice Slack

{Object.entries(cp.sliceSlack).map(([id, slack]) => ( {id}: {slack} ))}
)}
)}
) } // ─── Metrics Tab ────────────────────────────────────────────────────────────── function MetricsTab({ data }: { data: VisualizerData }) { if (!data.totals) { return } const totals = data.totals return (
{/* Summary stats */}
{/* By Phase */} {data.byPhase.length > 0 && (
Cost by Phase
{data.byPhase.map((phase) => { const pct = totals.cost > 0 ? (phase.cost / totals.cost) * 100 : 0 return (
{phase.phase}
{formatCost(phase.cost)} {pct.toFixed(1)}% {formatTokenCount(phase.tokens.total)} tok {phase.units} units
) })}
)} {/* By Model */} {data.byModel.length > 0 && (
Cost by Model
{data.byModel.map((model) => { const pct = totals.cost > 0 ? (model.cost / totals.cost) * 100 : 0 return (
{model.model}
{formatCost(model.cost)} {pct.toFixed(1)}% {formatTokenCount(model.tokens.total)} tok {model.units} units
) })}
)} {/* By Slice */} {data.bySlice.length > 0 && (
Cost by Slice
{data.bySlice.map((sl) => ( ))}
Slice Units Cost Duration Tokens
{sl.sliceId} {sl.units} {formatCost(sl.cost)} {formatDuration(sl.duration)} {formatTokenCount(sl.tokens.total)}
)} {/* Projections */} {data.bySlice.length >= 2 && }
) } function ProjectionsSection({ data, totals, }: { data: VisualizerData totals: ProjectTotals }) { const sliceLevelEntries = data.bySlice.filter((s) => s.sliceId.includes("/")) if (sliceLevelEntries.length < 2) return null const totalSliceCost = sliceLevelEntries.reduce((sum, s) => sum + s.cost, 0) const avgCostPerSlice = totalSliceCost / sliceLevelEntries.length const projectedRemaining = avgCostPerSlice * data.remainingSliceCount const projectedTotal = totals.cost + projectedRemaining const burnRate = totals.duration > 0 ? totals.cost / (totals.duration / 3_600_000) : 0 return (
Projections
{burnRate > 0 && ( )}
{projectedTotal > 2 * totals.cost && data.remainingSliceCount > 0 && (
Projected total {formatCost(projectedTotal)} exceeds 2× current spend
)}
) } // ─── Timeline Tab ───────────────────────────────────────────────────────────── function TimelineTab({ data }: { data: VisualizerData }) { const sorted = [...data.units].sort((a, b) => a.startedAt - b.startedAt) const recent = sorted.slice(-30) const hasRunningUnit = recent.some((u) => !u.finishedAt || u.finishedAt === 0) const [runningNow, setRunningNow] = useState(() => Date.now()) useEffect(() => { if (!hasRunningUnit) return const interval = window.setInterval(() => { setRunningNow(Date.now()) }, 1000) return () => window.clearInterval(interval) }, [hasRunningUnit]) const referenceNow = hasRunningUnit ? runningNow : 0 const durationForUnit = useCallback( (unit: VisualizerData["units"][number]) => (unit.finishedAt || referenceNow) - unit.startedAt, [referenceNow], ) if (data.units.length === 0) { return } const maxDuration = Math.max(...recent.map(durationForUnit), 1) return (
{/* Header */}
Execution Timeline

Showing {recent.length} of {data.units.length} units — most recent first

{/* Column headers */}
Time Type ID Duration Time Cost
{[...recent].reverse().map((unit, i) => { const duration = durationForUnit(unit) const pct = (duration / maxDuration) * 100 const isRunning = !unit.finishedAt || unit.finishedAt === 0 return (
{formatTime(unit.startedAt)} {isRunning ? ( ) : ( )} {unit.type} {unit.id}
{formatDuration(duration)} {formatCost(unit.cost)}
) })}
) } // ─── Agent Tab ──────────────────────────────────────────────────────────────── function AgentTab({ data }: { data: VisualizerData }) { const activity = data.agentActivity if (!activity) { return } const completed = activity.completedUnits const total = Math.max(completed, activity.totalSlices) const pct = total > 0 ? Math.min(100, Math.round((completed / total) * 100)) : 0 return (
{/* Status card */}
{activity.active && (
)}

{activity.active ? "Active" : "Idle"}

{activity.active ? "Agent is running" : "Waiting for next task"}

{activity.active && (

{formatDuration(activity.elapsed)}

elapsed

)}
{activity.currentUnit && (

Currently executing

{activity.currentUnit.type} — {activity.currentUnit.id}

)}
{/* Completion progress */} {total > 0 && (
Completion Progress {completed} / {total} slices
{pct}% complete {total - completed} remaining
)} {/* Stats grid */}
0 ? `${activity.completionRate.toFixed(1)}/hr` : "—"} accent="sky" />
{/* Recent units */} {data.units.filter((u) => u.finishedAt > 0).length > 0 && (
Recent Completed Units
{data.units .filter((u) => u.finishedAt > 0) .slice(-5) .reverse() .map((u, i) => (
{formatTime(u.startedAt)} {u.type} {u.id} {formatDuration(u.finishedAt - u.startedAt)} {formatCost(u.cost)}
))}
)}
) } // ─── Changes Tab ────────────────────────────────────────────────────────────── function ChangesTab({ data }: { data: VisualizerData }) { const entries = data.changelog.entries if (entries.length === 0) { return } const sorted = [...entries].reverse() return (
{sorted.map((entry, i) => (
{/* Header */}
{entry.milestoneId}/{entry.sliceId} {entry.title}
{entry.completedAt && ( {formatRelative(entry.completedAt)} )}
{/* One-liner */} {entry.oneLiner && (

“{entry.oneLiner}”

)} {/* Files modified */} {entry.filesModified.length > 0 && (

Files Modified

{entry.filesModified.map((f, fi) => (
{f.path} {f.description && ( — {f.description} )}
))}
)}
))}
) } // ─── Export Tab ─────────────────────────────────────────────────────────────── function ExportTab({ data }: { data: VisualizerData }) { const downloadBlob = useCallback( (content: string, filename: string, mimeType: string) => { const blob = new Blob([content], { type: mimeType }) const url = URL.createObjectURL(blob) const a = document.createElement("a") a.href = url a.download = filename document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) }, [], ) const generateMarkdown = useCallback(() => { const lines: string[] = [] lines.push("# SF Workflow Report") lines.push("") lines.push(`Generated: ${new Date().toISOString()}`) lines.push(`Phase: ${data.phase}`) lines.push("") lines.push("## Milestones") lines.push("") for (const ms of data.milestones) { const icon = ms.status === "complete" ? "✓" : ms.status === "active" ? "▸" : "○" lines.push(`### ${icon} ${ms.id}: ${ms.title} (${ms.status})`) if (ms.dependsOn.length > 0) lines.push(`Depends on: ${ms.dependsOn.join(", ")}`) lines.push("") for (const sl of ms.slices) { const slIcon = sl.done ? "✓" : sl.active ? "▸" : "○" lines.push(`- ${slIcon} **${sl.id}**: ${sl.title} [risk: ${sl.risk}]`) for (const t of sl.tasks) { const tIcon = t.done ? "✓" : t.active ? "▸" : "○" lines.push(` - ${tIcon} ${t.id}: ${t.title}`) } } lines.push("") } if (data.totals) { lines.push("## Metrics Summary") lines.push("") lines.push(`| Metric | Value |`) lines.push(`|--------|-------|`) lines.push(`| Units | ${data.totals.units} |`) lines.push(`| Total Cost | ${formatCost(data.totals.cost)} |`) lines.push(`| Duration | ${formatDuration(data.totals.duration)} |`) lines.push(`| Tokens | ${formatTokenCount(data.totals.tokens.total)} |`) lines.push("") } if (data.criticalPath.milestonePath.length > 0) { lines.push("## Critical Path") lines.push("") lines.push(`Milestone: ${data.criticalPath.milestonePath.join(" → ")}`) if (data.criticalPath.slicePath.length > 0) { lines.push(`Slice: ${data.criticalPath.slicePath.join(" → ")}`) } lines.push("") } if (data.changelog.entries.length > 0) { lines.push("## Changelog") lines.push("") for (const entry of data.changelog.entries) { lines.push(`### ${entry.milestoneId}/${entry.sliceId}: ${entry.title}`) if (entry.oneLiner) lines.push(`> ${entry.oneLiner}`) if (entry.filesModified.length > 0) { lines.push("Files:") for (const f of entry.filesModified) lines.push(`- \`${f.path}\` — ${f.description}`) } if (entry.completedAt) lines.push(`Completed: ${entry.completedAt}`) lines.push("") } } return lines.join("\n") }, [data]) const handleMarkdown = () => downloadBlob(generateMarkdown(), "sf-report.md", "text/markdown") const handleJSON = () => downloadBlob(JSON.stringify(data, null, 2), "sf-report.json", "application/json") return (
Export Project Data

Download the current visualizer data as a structured report. Markdown includes milestones, metrics, critical path, and changelog in a readable format. JSON contains the full raw data payload.

) } // ─── Custom Tab Bar ──────────────────────────────────────────────────────────── function VisualizerTabs({ defaultValue, children, }: { defaultValue: TabValue children: React.ReactNode }) { return ( {children} ) } function VisualizerTabList() { return ( {TABS.map(({ value, label, Icon }) => ( {/* Active bottom border indicator */} {/* Hover background */} {/* Icon */} {/* Label */} {label} ))} ) } // ─── Main Component ─────────────────────────────────────────────────────────── export function VisualizerView() { const workspace = useSFWorkspaceState() const projectCwd = workspace.boot?.project.cwd const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const fetchData = useCallback(async () => { try { const resp = await authFetch(buildProjectUrl("/api/visualizer", projectCwd)) if (!resp.ok) { const body = await resp.json().catch(() => ({ error: "Unknown error" })) throw new Error(body.error || `HTTP ${resp.status}`) } const json: VisualizerData = await resp.json() setData(json) setError(null) } catch (err) { setError(err instanceof Error ? err.message : "Failed to fetch visualizer data") } finally { setLoading(false) } }, [projectCwd]) useEffect(() => { fetchData() const interval = setInterval(fetchData, 10_000) return () => clearInterval(interval) }, [fetchData]) // Loading if (loading && !data) { return (

Loading visualizer data…

) } // Error (no cached data) if (error && !data) { return (

Failed to load visualizer

{error}

) } if (!data) return null return (
{/* Header */}

Workflow Visualizer

Phase:{" "} {data.phase} {data.remainingSliceCount > 0 && ( <> · {data.remainingSliceCount} slice{data.remainingSliceCount !== 1 ? "s" : ""} remaining )} {error && ( <> · Stale — {error} )}
{/* Tabs */}
) }