import { useEffect, useRef } from "react"; import type { DirectiveSummary } from "../../lib/api"; interface DirectiveContextMenuProps { x: number; y: number; directive: DirectiveSummary; onClose: () => void; onStart: () => void; onPause: () => void; onArchive: () => void; onDelete: () => void; onGoToPR: () => void; /** * Reset the contract to a fresh empty draft (clears goal + pr_url, status * back to 'draft'). Past revisions stay as history. Optional so the legacy * tabular UI doesn't have to wire it up. */ onNewDraft?: () => void; /** Trigger a fresh PR creation from the current contract state. */ onCreatePR?: () => void; /** Manually advance the DAG (find newly-ready steps). */ onAdvance?: () => void; /** Run the cleanup task to prune merged/stale steps. */ onCleanup?: () => void; /** Pick up linked orders (queue them as new steps). */ onPickUpOrders?: () => void; } export function DirectiveContextMenu({ x, y, directive, onClose, onStart, onPause, onArchive, onDelete, onGoToPR, onNewDraft, onCreatePR, onAdvance, onCleanup, onPickUpOrders, }: DirectiveContextMenuProps) { const menuRef = useRef(null); // Close on click outside useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(e.target as Node)) { onClose(); } }; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { onClose(); } }; document.addEventListener("mousedown", handleClickOutside); document.addEventListener("keydown", handleKeyDown); return () => { document.removeEventListener("mousedown", handleClickOutside); document.removeEventListener("keydown", handleKeyDown); }; }, [onClose]); // Adjust position if menu would overflow viewport useEffect(() => { if (menuRef.current) { const rect = menuRef.current.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; if (rect.right > viewportWidth) { menuRef.current.style.left = `${x - rect.width}px`; } if (rect.bottom > viewportHeight) { menuRef.current.style.top = `${y - rect.height}px`; } } }, [x, y]); const menuItemClass = "w-full px-3 py-1.5 text-left text-xs font-mono text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.1)] flex items-center gap-2"; const dividerClass = "border-t border-[rgba(117,170,252,0.2)] my-1"; const showStart = directive.status === "draft" || directive.status === "paused" || directive.status === "idle"; const showPause = directive.status === "active"; const showArchive = directive.status !== "archived"; const showGoToPR = !!directive.prUrl; // "New draft" appears once the contract is inactive (its iteration has // shipped) — that's the explicit affordance for starting the next cycle // on a clean slate while keeping prior revisions as history. const showNewDraft = !!onNewDraft && directive.status === "inactive"; return (
{/* Header showing directive title */}
{directive.title}
{/* New draft — the canonical action on an inactive (shipped) contract. */} {showNewDraft && ( <>
)} {/* Status actions */} {showStart && ( )} {showPause && ( )} {showArchive && ( )} {/* Orchestration actions — Advance / Pick up orders / Cleanup. */} {(onAdvance || onPickUpOrders || onCleanup) && (
)} {onAdvance && ( )} {onPickUpOrders && ( )} {onCleanup && ( )} {/* PR actions — Create / Update / Go to PR. */} {(onCreatePR || showGoToPR) &&
} {onCreatePR && ( )} {showGoToPR && ( )}
{/* Delete action */}
); }