"use client"; import { CheckIcon, GripVerticalIcon, PencilIcon, PlayIcon, PlusIcon, RotateCcwIcon, SquareIcon, XIcon } from "lucide-react"; import { AnimatePresence, motion, Reorder, useDragControls, useReducedMotion } from "motion/react"; import { type KeyboardEvent, useEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; export interface PlanStep { id: string; label: string; tool?: string; duration?: number; failOnce?: boolean; error?: string; } export interface PlanReviewProps { steps: PlanStep[]; title?: string; agent?: string; onRun?: (steps: PlanStep[]) => void; className?: string; } type Phase = "review" | "running" | "failed" | "done"; type StepStatus = "pending" | "running" | "done" | "failed"; let nextId = 0; const press = "transition-[scale,box-shadow,background-color] duration-150 ease-out active:scale-[0.97]"; function secs(ms: number) { return `${(ms / 1000).toFixed(1)}s`; } export function PlanReview({ steps: initial, title = "Plan", agent = "Agent", onRun, className }: PlanReviewProps) { const reduce = useReducedMotion(); const [steps, setSteps] = useState(initial); const [phase, setPhase] = useState("review"); const [cursor, setCursor] = useState(0); const [status, setStatus] = useState>({}); const [times, setTimes] = useState>({}); const [editing, setEditing] = useState(null); const failedBefore = useRef(new Set()); useEffect(() => { if (phase !== "running") return; const step = steps[cursor]; if (!step) return; const duration = step.duration ?? 1800; const started = performance.now(); setStatus((s) => ({ ...s, [step.id]: "running" })); const timer = setInterval(() => { const ms = performance.now() - started; const willFail = step.failOnce && !failedBefore.current.has(step.id); const end = willFail ? duration * 0.6 : duration; if (ms < end) { setTimes((t) => ({ ...t, [step.id]: ms })); return; } clearInterval(timer); setTimes((t) => ({ ...t, [step.id]: end })); if (willFail) { failedBefore.current.add(step.id); setStatus((s) => ({ ...s, [step.id]: "failed" })); setPhase("failed"); return; } setStatus((s) => ({ ...s, [step.id]: "done" })); if (cursor + 1 >= steps.length) setPhase("done"); else setCursor(cursor + 1); }, 50); return () => clearInterval(timer); }, [phase, cursor, steps]); const run = () => { setEditing(null); setStatus({}); setTimes({}); failedBefore.current.clear(); setCursor(0); setPhase("running"); onRun?.(steps); }; const retryFrom = (index: number) => { const rest = steps.slice(index).map((s) => s.id); setStatus((s) => Object.fromEntries(Object.entries(s).filter(([id]) => !rest.includes(id)))); setTimes((t) => Object.fromEntries(Object.entries(t).filter(([id]) => !rest.includes(id)))); setCursor(index); setPhase("running"); }; const backToReview = () => { setPhase("review"); setStatus({}); setTimes({}); failedBefore.current.clear(); }; const move = (index: number, by: number) => { const to = index + by; if (to < 0 || to >= steps.length) return; setSteps((s) => { const next = [...s]; const [item] = next.splice(index, 1); next.splice(to, 0, item); return next; }); }; const add = () => { const id = `new-${++nextId}`; setSteps((s) => [...s, { id, label: "", duration: 1800 }]); setEditing(id); }; const rename = (id: string, label: string) => { setEditing(null); setSteps((s) => { const trimmed = label.trim(); if (!trimmed) return s.filter((x) => x.id !== id || x.label); return s.map((x) => (x.id === id ? { ...x, label: trimmed } : x)); }); }; const doneCount = steps.filter((s) => status[s.id] === "done").length; const total = Object.values(times).reduce((a, b) => a + b, 0); const estimate = steps.reduce((a, s) => a + (s.duration ?? 1800), 0); const failedIndex = steps.findIndex((s) => status[s.id] === "failed"); const editable = phase === "review"; const spring = reduce ? { duration: 0 } : { type: "spring" as const, stiffness: 420, damping: 34 }; return (

{title}

{phase === "review" && `${steps.length} steps · ~${Math.round(estimate / 1000)}s`} {phase === "running" && `Step ${cursor + 1} of ${steps.length} · ${secs(total)}`} {phase === "failed" && `Stopped at step ${failedIndex + 1}`} {phase === "done" && `Done in ${secs(total)}`}

{phase === "review" && `${agent} runs these top to bottom. Edit them before you start.`} {phase === "running" && `${agent} is working through the plan.`} {phase === "failed" && `${agent} stopped. Retry from the failed step, or edit the plan.`} {phase === "done" && `${agent} finished every step.`}

{steps.length === 0 && (
  • No steps yet Add a step to tell {agent} what to do first.
  • )} {steps.map((step, index) => ( setEditing(step.id)} onMove={(by) => move(index, by)} onRemove={() => setSteps((s) => s.filter((x) => x.id !== step.id))} onRename={(label) => rename(step.id, label)} onRetry={() => retryFrom(index)} reduce={!!reduce} status={status[step.id] ?? "pending"} step={step} /> ))}
    {editable ? ( ) : ( )} {(phase === "review" || phase === "done") && ( )}
    ); } function PlanRow({ step, index, isLast, status, ms, editable, editing, onEdit, onRename, onRemove, onMove, onRetry, reduce, }: { step: PlanStep; index: number; isLast: boolean; status: StepStatus; ms?: number; editable: boolean; editing: boolean; onEdit: () => void; onRename: (label: string) => void; onRemove: () => void; onMove: (by: number) => void; onRetry: () => void; reduce: boolean; }) { const controls = useDragControls(); const [draft, setDraft] = useState(step.label); const inputRef = useRef(null); useEffect(() => { if (editing) { setDraft(step.label); requestAnimationFrame(() => inputRef.current?.select()); } }, [editing, step.label]); const onHandleKey = (e: KeyboardEvent) => { if (e.key === "ArrowUp" || e.key === "ArrowDown") { e.preventDefault(); onMove(e.key === "ArrowUp" ? -1 : 1); } }; return (
    {editable ? ( ) : ( )} {!isLast && ( )}
    {editing ? ( onRename(draft)} onChange={(e) => setDraft(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") onRename(draft); if (e.key === "Escape") onRename(step.label); }} placeholder="Describe the step" ref={inputRef} value={draft} /> ) : editable ? ( ) : (

    {step.label}

    )} {step.tool && !editing && ( {step.tool} )} {status === "failed" && (

    {step.error ?? "Step failed"}

    )}
    {editable ? ( ) : ( ms !== undefined && ( {secs(ms)} ) )}
    ); } function StepDot({ index, status, reduce }: { index: number; status: StepStatus; reduce: boolean }) { return ( {status === "pending" && index + 1} {status === "running" && ( )} {status === "done" && } {status === "failed" && } ); }