"use client" import { useEffect, useState, useCallback } from "react" import { Activity, Clock, DollarSign, Zap, CheckCircle2, Circle, Play, GitBranch, TrendingDown, } from "lucide-react" import { cn } from "@/lib/utils" import { useSFWorkspaceState, useSFWorkspaceActions, buildPromptCommand, buildProjectUrl, formatDuration, formatCost, formatTokens, getCurrentScopeLabel, getCurrentBranch, getCurrentSlice, getLiveAutoDashboard, getLiveWorkspaceIndex, type WorkspaceTerminalLine, type TerminalLineType, } from "@/lib/sf-workspace-store" import { getTaskStatus, type ItemStatus } from "@/lib/workspace-status" import { deriveWorkflowAction } from "@/lib/workflow-actions" import { executeWorkflowActionInPowerMode } from "@/lib/workflow-action-execution" import { Skeleton } from "@/components/ui/skeleton" import { CurrentSliceCardSkeleton, ActivityCardSkeleton, } from "@/components/sf/loading-skeletons" import { ScopeBadge } from "@/components/sf/scope-badge" import { ProjectWelcome } from "@/components/sf/project-welcome" import { authFetch } from "@/lib/auth" import { type ProjectTotals } from "@/lib/visualizer-types" /** Interpolate progress bar color from red (0%) through yellow (50%) to green (100%) using oklch. */ function getProgressColor(percent: number): string { const p = Math.max(0, Math.min(100, percent)) // Hue: 25 (red) → 85 (yellow) at 50% → 145 (green) at 100% const hue = 25 + (p / 100) * 120 return `oklch(0.65 0.16 ${hue.toFixed(1)})` } interface MetricCardProps { label: string value: string | null subtext?: string | null icon: React.ReactNode } function MetricCard({ label, value, subtext, icon }: MetricCardProps) { return (

{label}

{value === null ? ( <> ) : ( <>

{value}

{subtext &&

{subtext}

} )}
{icon}
) } function taskStatusIcon(status: ItemStatus) { switch (status) { case "done": return case "in-progress": return case "pending": return } } function activityDotColor(type: TerminalLineType): string { switch (type) { case "success": return "bg-success" case "error": return "bg-destructive" default: return "bg-foreground/50" } } interface DashboardProps { onSwitchView?: (view: string) => void onExpandTerminal?: () => void } export function Dashboard({ onSwitchView, onExpandTerminal }: DashboardProps = {}) { const state = useSFWorkspaceState() const { sendCommand } = useSFWorkspaceActions() const boot = state.boot const workspace = getLiveWorkspaceIndex(state) const auto = getLiveAutoDashboard(state) const bridge = boot?.bridge ?? null const freshness = state.live.freshness const projectCwd = boot?.project.cwd // ── Project-level totals from visualizer API ── // Provides fallback metrics when auto-mode is not active (#2709). // Same polling pattern as status-bar.tsx. const [projectTotals, setProjectTotals] = useState(null) const fetchProjectTotals = useCallback(async () => { try { const resp = await authFetch(buildProjectUrl("/api/visualizer", projectCwd)) if (!resp.ok) return const json = await resp.json() if (json.totals) setProjectTotals(json.totals) } catch { // Silently ignore — dashboard metrics are non-critical } }, [projectCwd]) useEffect(() => { const timeout = window.setTimeout(() => { void fetchProjectTotals() }, 0) const interval = window.setInterval(() => { void fetchProjectTotals() }, 30_000) return () => { window.clearTimeout(timeout) window.clearInterval(interval) } }, [fetchProjectTotals]) const elapsed = projectTotals?.duration ?? auto?.elapsed ?? 0 const totalCost = projectTotals?.cost ?? auto?.totalCost ?? 0 const totalTokens = projectTotals?.tokens.total ?? auto?.totalTokens ?? 0 const rtkSavings = auto?.rtkSavings ?? null const rtkEnabled = auto?.rtkEnabled === true const currentSlice = getCurrentSlice(workspace) const doneTasks = currentSlice?.tasks.filter((t) => t.done).length ?? 0 const totalTasks = currentSlice?.tasks.length ?? 0 const progressPercent = totalTasks > 0 ? Math.round((doneTasks / totalTasks) * 100) : 0 const scopeLabel = getCurrentScopeLabel(workspace) const branch = getCurrentBranch(workspace) const isAutoActive = auto?.active ?? false const currentUnitLabel = auto?.currentUnit?.id ?? scopeLabel const currentUnitFreshness = freshness.auto.stale ? "stale" : freshness.auto.status const workflowAction = deriveWorkflowAction({ phase: workspace?.active.phase ?? "pre-planning", autoActive: auto?.active ?? false, autoPaused: auto?.paused ?? false, onboardingLocked: boot?.onboarding.locked ?? false, commandInFlight: state.commandInFlight, bootStatus: state.bootStatus, hasMilestones: (workspace?.milestones.length ?? 0) > 0, projectDetectionKind: boot?.projectDetection?.kind ?? null, }) const handleWorkflowAction = (command: string) => { executeWorkflowActionInPowerMode({ dispatch: () => sendCommand(buildPromptCommand(command, bridge)), }) } const handlePrimaryAction = () => { if (!workflowAction.primary) return handleWorkflowAction(workflowAction.primary.command) } const recentLines: WorkspaceTerminalLine[] = (state.terminalLines ?? []).slice(-6) const isConnecting = state.bootStatus === "idle" || state.bootStatus === "loading" const rtkValue = isConnecting ? null : formatTokens(rtkSavings?.savedTokens ?? 0) const rtkSubtext = isConnecting ? null : rtkSavings && rtkSavings.commands > 0 ? `${Math.round(rtkSavings.savingsPct)}% saved • ${rtkSavings.commands} cmd${rtkSavings.commands === 1 ? "" : "s"}` : "Waiting for shell usage" // ─── Project Welcome Gate ─────────────────────────────────────────── // Show welcome screen for projects that aren't initialized with SF yet const detection = boot?.projectDetection const showWelcome = !isConnecting && detection && detection.kind !== "active-sf" && detection.kind !== "empty-sf" if (showWelcome) { return (
handleWorkflowAction(cmd)} onSwitchView={(view) => onSwitchView?.(view)} disabled={!!state.commandInFlight || boot?.onboarding.locked} />
) } return (

Dashboard

{!isConnecting && scopeLabel && ( <> / )} {isConnecting && }
{isConnecting ? ( <> ) : null} {!isConnecting && (
{isAutoActive ? "Auto Mode Active" : "Auto Mode Inactive"}
)} {!isConnecting && branch && (
{branch}
)}

Current Unit

{isConnecting ? ( <> ) : ( <>

Auto freshness: {currentUnitFreshness}

)}
} /> } /> } /> {rtkEnabled && ( } /> )}
{/* Current Slice */} {isConnecting ? ( ) : (
{/* Header */}

Current Slice

{currentSlice ? (

{currentSlice.id} — {currentSlice.title}

) : (

No active slice

)}
{currentSlice && totalTasks > 0 && (
{progressPercent} %
)}
{currentSlice && totalTasks > 0 && (

{doneTasks} of {totalTasks} tasks complete

)}
{/* Task list */}
{currentSlice && currentSlice.tasks.length > 0 ? (
{currentSlice.tasks.map((task) => { const status = getTaskStatus( workspace!.active.milestoneId!, currentSlice.id, task, workspace!.active, ) return (
{taskStatusIcon(status)} {task.id} · {task.title} {status === "in-progress" && ( active )}
) })}
) : (

No active slice or no tasks defined yet.

)}
)}
{isConnecting ? (
) : (

Recent Activity

{recentLines.length > 0 ? (
{recentLines.map((line) => (
{line.timestamp} {line.content}
))}
) : (
No activity yet.
)}
)}
) }