import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { ArrowDown01Icon, Cancel01Icon, FileEditIcon, FilePlusIcon, FolderAddIcon, Tick02Icon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { useState } from "react"; import { usePlanStore, type QueuedEdit } from "../store/planStore"; function basename(p: string): string { const i = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\")); return i >= 0 ? p.slice(i + 1) : p; } function diffStats( original: string, proposed: string, ): { added: number; removed: number } { const a = original.split("\n"); const b = proposed.split("\n"); const setA = new Set(a); const setB = new Set(b); let added = 0; let removed = 0; for (const line of b) if (!setA.has(line)) added++; for (const line of a) if (!setB.has(line)) removed++; return { added, removed }; } export function PlanDiffReview() { const queue = usePlanStore((s) => s.queue); const removeOne = usePlanStore((s) => s.removeOne); const clear = usePlanStore((s) => s.clear); const applyAll = usePlanStore((s) => s.applyAll); const [busy, setBusy] = useState(false); if (queue.length === 0) return null; const onApply = async () => { setBusy(true); try { const results = await applyAll(); const failed = results.filter((r) => !r.ok); if (failed.length) { console.error("plan apply failures:", failed); } } finally { setBusy(false); } }; return (
Plan review {queue.length} pending change{queue.length === 1 ? "" : "s"}
); } function PlanRow({ item, onReject, }: { item: QueuedEdit; onReject: () => void; }) { const [open, setOpen] = useState(false); const isDir = item.kind === "create_directory"; const isNew = item.isNewFile && !isDir; const stats = isDir ? null : diffStats(item.originalContent, item.proposedContent); const Icon = isDir ? FolderAddIcon : isNew ? FilePlusIcon : FileEditIcon; return (
  • {basename(item.path)} {isNew ? ( new ) : null}
    {item.path}
    {stats ? (
    +{stats.added} −{stats.removed} {item.kind === "multi_edit" ? "multi-edit" : item.kind}
    ) : (
    {item.description ?? "create directory"}
    )}
    {open && !isDir ? (
    ) : null}
  • ); } function UnifiedDiffPreview({ original, proposed, }: { original: string; proposed: string; }) { // Coarse line-level diff (LCS-lite via set membership). For real diffs // we'd reach for a library; this is good enough for at-a-glance review. const a = original.split("\n"); const b = proposed.split("\n"); const setA = new Set(a); const setB = new Set(b); const lines: Array<{ kind: "add" | "del" | "ctx"; text: string }> = []; // First pass: removed (in a, not in b). for (const l of a) if (!setB.has(l)) lines.push({ kind: "del", text: l }); // Then: added (in b, not in a). for (const l of b) if (!setA.has(l)) lines.push({ kind: "add", text: l }); if (lines.length === 0) { return (
    no line-level changes
    ); } const MAX = 80; const shown = lines.slice(0, MAX); const rest = lines.length - shown.length; return (
    {shown.map((l, i) => (
    {l.kind === "add" ? "+" : "-"} {l.text || " "}
    ))} {rest > 0 ? (
    … {rest} more changes
    ) : null}
    ); }