import { useState, useCallback, useMemo, useRef, useEffect } from "react"; import type { ChainWithContracts, ChainGraphResponse, ChainContractDetail, ChainContractDefinition, ChainDefinitionGraphResponse, AddContractDefinitionRequest, } from "../../lib/api"; import { listChainDefinitions, createChainDefinition, updateChainDefinition, deleteChainDefinition, getChainDefinitionGraph, startChain, stopChain, } from "../../lib/api"; const statusColors: Record = { active: "text-green-400", completed: "text-blue-400", archived: "text-[#555]", pending: "text-yellow-400", }; interface ChainEditorProps { chain: ChainWithContracts; graph: ChainGraphResponse | null; loading: boolean; onBack: () => void; onRefresh: () => void; onContractClick: (contractId: string) => void; } // Node dimensions const NODE_WIDTH = 180; const NODE_HEIGHT = 80; const CANVAS_PADDING = 40; export function ChainEditor({ chain, graph, loading, onBack, onRefresh, onContractClick, }: ChainEditorProps) { const canvasRef = useRef(null); const [selectedNode, setSelectedNode] = useState(null); const [hoveredNode, setHoveredNode] = useState(null); const [definitions, setDefinitions] = useState([]); const [definitionGraph, setDefinitionGraph] = useState(null); const [showAddDefinition, setShowAddDefinition] = useState(false); const [isStarting, setIsStarting] = useState(false); const [isStopping, setIsStopping] = useState(false); const [error, setError] = useState(null); // Drag state const [draggedNode, setDraggedNode] = useState(null); const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); const [localPositions, setLocalPositions] = useState>(new Map()); // Edge drawing state const [edgeDrawing, setEdgeDrawing] = useState<{ fromNode: string; toPoint: { x: number; y: number }; } | null>(null); // Context menu state (nodeId is null for canvas context menu) const [contextMenu, setContextMenu] = useState<{ x: number; y: number; nodeId: string | null; canvasPosition?: { gridX: number; gridY: number }; } | null>(null); // Load definitions when chain changes useEffect(() => { async function loadDefinitions() { try { const [defs, defGraph] = await Promise.all([ listChainDefinitions(chain.id), getChainDefinitionGraph(chain.id), ]); setDefinitions(defs); setDefinitionGraph(defGraph); } catch (err) { console.error("Failed to load definitions:", err); } } loadDefinitions(); }, [chain.id]); // Determine which view to show: definitions (pending chain) or contracts (active/completed) const showDefinitions = chain.status === "pending" || chain.status === "archived"; const currentGraph = showDefinitions ? definitionGraph : graph; // Use positions from graph nodes, with local overrides for dragging const nodePositions = useMemo(() => { const positions = new Map(); if (showDefinitions && definitionGraph?.nodes) { for (const node of definitionGraph.nodes) { // Use local position if available (during drag), otherwise use server position const localPos = localPositions.get(node.id); if (localPos) { positions.set(node.id, localPos); } else { positions.set(node.id, { x: CANVAS_PADDING + (node.x || 0) * (NODE_WIDTH + 60), y: CANVAS_PADDING + (node.y || 0) * (NODE_HEIGHT + 40), }); } } } else if (!showDefinitions && graph?.nodes) { for (const node of graph.nodes) { positions.set(node.contractId, { x: CANVAS_PADDING + (node.x || 0) * (NODE_WIDTH + 60), y: CANVAS_PADDING + (node.y || 0) * (NODE_HEIGHT + 40), }); } } return positions; }, [showDefinitions, definitionGraph?.nodes, graph?.nodes, localPositions]); // Canvas dimensions const canvasDimensions = useMemo(() => { if (nodePositions.size === 0) { return { width: 600, height: 400 }; } let maxX = 0; let maxY = 0; for (const pos of nodePositions.values()) { maxX = Math.max(maxX, pos.x + NODE_WIDTH); maxY = Math.max(maxY, pos.y + NODE_HEIGHT); } return { width: Math.max(600, maxX + CANVAS_PADDING), height: Math.max(400, maxY + CANVAS_PADDING), }; }, [nodePositions]); const handleNodeClick = useCallback((nodeId: string) => { setSelectedNode(nodeId); }, []); const handleNodeDoubleClick = useCallback( (nodeId: string) => { // For definitions, we can't open a contract yet if (showDefinitions) return; onContractClick(nodeId); }, [onContractClick, showDefinitions] ); const getStatusColor = (status: string, isCheckpoint = false) => { // Checkpoint contracts use purple/violet colors if (isCheckpoint) { switch (status) { case "active": return { bg: "#a78bfa", border: "#8b5cf6", text: "#5b21b6" }; case "completed": return { bg: "#818cf8", border: "#6366f1", text: "#3730a3" }; case "pending": return { bg: "#c4b5fd", border: "#a78bfa", text: "#6d28d9" }; case "failed": return { bg: "#ef4444", border: "#dc2626", text: "#991b1b" }; default: return { bg: "#a78bfa", border: "#8b5cf6", text: "#5b21b6" }; } } switch (status) { case "active": return { bg: "#4ade80", border: "#22c55e", text: "#166534" }; case "completed": return { bg: "#60a5fa", border: "#3b82f6", text: "#1e40af" }; case "pending": return { bg: "#f59e0b", border: "#d97706", text: "#92400e" }; case "blocked": return { bg: "#ef4444", border: "#dc2626", text: "#991b1b" }; default: return { bg: "#6b7280", border: "#4b5563", text: "#374151" }; } }; // Find selected item (definition or contract) const selectedDefinition = showDefinitions && selectedNode ? definitions.find((d) => d.id === selectedNode) : null; const selectedContract = !showDefinitions && selectedNode ? chain.contracts.find((c) => c.contractId === selectedNode) : null; const handleStartChain = useCallback(async () => { setIsStarting(true); setError(null); try { await startChain(chain.id); onRefresh(); } catch (err) { setError(err instanceof Error ? err.message : "Failed to start chain"); } finally { setIsStarting(false); } }, [chain.id, onRefresh]); const handleStopChain = useCallback(async () => { if (!confirm("Are you sure you want to stop this chain?")) return; setIsStopping(true); setError(null); try { await stopChain(chain.id); onRefresh(); } catch (err) { setError(err instanceof Error ? err.message : "Failed to stop chain"); } finally { setIsStopping(false); } }, [chain.id, onRefresh]); // Position for new definition (set from canvas right-click) const [newDefinitionPosition, setNewDefinitionPosition] = useState<{ x: number; y: number } | null>(null); // Find free space on the grid for new definitions const findFreePosition = useCallback(() => { if (!definitionGraph?.nodes || definitionGraph.nodes.length === 0) { return { x: 0, y: 0 }; } // Get all occupied positions const occupied = new Set(); for (const node of definitionGraph.nodes) { occupied.add(`${node.x},${node.y}`); } // Find first free position by scanning row by row for (let y = 0; y < 10; y++) { for (let x = 0; x < 10; x++) { if (!occupied.has(`${x},${y}`)) { return { x, y }; } } } // Fallback: place at the end of the last row const maxY = Math.max(...definitionGraph.nodes.map((n) => n.y || 0)); return { x: 0, y: maxY + 1 }; }, [definitionGraph?.nodes]); const handleAddDefinition = useCallback(async (req: AddContractDefinitionRequest) => { try { // Use specified position or find free space const position = newDefinitionPosition || findFreePosition(); const reqWithPosition = { ...req, editorX: position.x, editorY: position.y }; await createChainDefinition(chain.id, reqWithPosition); // Reload definitions const [defs, defGraph] = await Promise.all([ listChainDefinitions(chain.id), getChainDefinitionGraph(chain.id), ]); setDefinitions(defs); setDefinitionGraph(defGraph); setShowAddDefinition(false); setNewDefinitionPosition(null); } catch (err) { console.error("Failed to add definition:", err); setError(err instanceof Error ? err.message : "Failed to add definition"); } }, [chain.id, newDefinitionPosition, findFreePosition]); const handleDeleteDefinition = useCallback(async (definitionId: string) => { if (!confirm("Are you sure you want to delete this definition?")) return; try { await deleteChainDefinition(chain.id, definitionId); // Reload definitions const [defs, defGraph] = await Promise.all([ listChainDefinitions(chain.id), getChainDefinitionGraph(chain.id), ]); setDefinitions(defs); setDefinitionGraph(defGraph); setSelectedNode(null); } catch (err) { console.error("Failed to delete definition:", err); setError(err instanceof Error ? err.message : "Failed to delete definition"); } }, [chain.id]); // Refresh definitions helper const refreshDefinitions = useCallback(async () => { const [defs, defGraph] = await Promise.all([ listChainDefinitions(chain.id), getChainDefinitionGraph(chain.id), ]); setDefinitions(defs); setDefinitionGraph(defGraph); setLocalPositions(new Map()); // Clear local positions }, [chain.id]); // Context menu handlers const handleContextMenu = useCallback((e: React.MouseEvent, nodeId: string) => { e.preventDefault(); e.stopPropagation(); setContextMenu({ x: e.clientX, y: e.clientY, nodeId }); }, []); // Canvas context menu (for creating new definitions) const handleCanvasContextMenu = useCallback((e: React.MouseEvent) => { if (!showDefinitions || chain.status !== "pending") return; e.preventDefault(); const canvasRect = canvasRef.current?.getBoundingClientRect(); if (!canvasRect) return; // Calculate grid position from click const clickX = e.clientX - canvasRect.left; const clickY = e.clientY - canvasRect.top; const gridX = Math.round((clickX - CANVAS_PADDING) / (NODE_WIDTH + 60)); const gridY = Math.round((clickY - CANVAS_PADDING) / (NODE_HEIGHT + 40)); setContextMenu({ x: e.clientX, y: e.clientY, nodeId: null, canvasPosition: { gridX: Math.max(0, gridX), gridY: Math.max(0, gridY) }, }); }, [showDefinitions, chain.status]); const closeContextMenu = useCallback(() => { setContextMenu(null); }, []); // Handle creating definition at canvas position const handleCreateAtPosition = useCallback((gridX: number, gridY: number) => { setNewDefinitionPosition({ x: gridX, y: gridY }); setShowAddDefinition(true); closeContextMenu(); }, [closeContextMenu]); // Handle canvas click to close context menu const handleCanvasClick = useCallback(() => { if (contextMenu) { closeContextMenu(); } }, [contextMenu, closeContextMenu]); // Drag handlers const handleDragStart = useCallback((e: React.MouseEvent, nodeId: string) => { if (!showDefinitions || chain.status !== "pending") return; e.preventDefault(); const pos = nodePositions.get(nodeId); if (!pos) return; setDraggedNode(nodeId); setDragOffset({ x: e.clientX - pos.x, y: e.clientY - pos.y, }); }, [showDefinitions, chain.status, nodePositions]); const handleDragMove = useCallback((e: React.MouseEvent) => { if (!draggedNode) return; const canvasRect = canvasRef.current?.getBoundingClientRect(); if (!canvasRect) return; const newX = Math.max(CANVAS_PADDING, e.clientX - dragOffset.x); const newY = Math.max(CANVAS_PADDING, e.clientY - dragOffset.y); setLocalPositions(prev => { const newMap = new Map(prev); newMap.set(draggedNode, { x: newX, y: newY }); return newMap; }); }, [draggedNode, dragOffset]); const handleDragEnd = useCallback(async () => { if (!draggedNode) return; const localPos = localPositions.get(draggedNode); if (localPos) { // Convert pixel position back to grid coordinates const gridX = Math.round((localPos.x - CANVAS_PADDING) / (NODE_WIDTH + 60)); const gridY = Math.round((localPos.y - CANVAS_PADDING) / (NODE_HEIGHT + 40)); try { await updateChainDefinition(chain.id, draggedNode, { editorX: gridX, editorY: gridY, }); await refreshDefinitions(); } catch (err) { console.error("Failed to update position:", err); setError(err instanceof Error ? err.message : "Failed to update position"); } } setDraggedNode(null); }, [draggedNode, localPositions, chain.id, refreshDefinitions]); // Edge drawing handlers const handleEdgeDrawStart = useCallback((e: React.MouseEvent, nodeId: string) => { if (!showDefinitions || chain.status !== "pending") return; e.preventDefault(); e.stopPropagation(); const canvasRect = canvasRef.current?.getBoundingClientRect(); if (!canvasRect) return; const pos = nodePositions.get(nodeId); if (!pos) return; setEdgeDrawing({ fromNode: nodeId, toPoint: { x: pos.x + NODE_WIDTH / 2, y: pos.y + NODE_HEIGHT, }, }); }, [showDefinitions, chain.status, nodePositions]); const handleEdgeDrawMove = useCallback((e: React.MouseEvent) => { if (!edgeDrawing) return; const canvasRect = canvasRef.current?.getBoundingClientRect(); if (!canvasRect) return; setEdgeDrawing(prev => prev ? { ...prev, toPoint: { x: e.clientX - canvasRect.left, y: e.clientY - canvasRect.top, }, } : null); }, [edgeDrawing]); const handleEdgeDrawEnd = useCallback(async (targetNodeId: string | null) => { if (!edgeDrawing) return; if (targetNodeId && targetNodeId !== edgeDrawing.fromNode) { // Find the target definition and its current dependencies const fromDef = definitions.find(d => d.id === edgeDrawing.fromNode); const toDef = definitions.find(d => d.id === targetNodeId); if (fromDef && toDef) { // Add dependency: targetNodeId depends on fromNode const currentDeps = toDef.dependsOnNames || []; if (!currentDeps.includes(fromDef.name)) { try { await updateChainDefinition(chain.id, targetNodeId, { dependsOn: [...currentDeps, fromDef.name], }); await refreshDefinitions(); } catch (err) { console.error("Failed to create dependency:", err); setError(err instanceof Error ? err.message : "Failed to create dependency"); } } } } setEdgeDrawing(null); }, [edgeDrawing, definitions, chain.id, refreshDefinitions]); // Handle removing a dependency via context menu const handleRemoveDependency = useCallback(async (nodeId: string, depName: string) => { const def = definitions.find(d => d.id === nodeId); if (!def) return; const newDeps = (def.dependsOnNames || []).filter(d => d !== depName); try { await updateChainDefinition(chain.id, nodeId, { dependsOn: newDeps, }); await refreshDefinitions(); closeContextMenu(); } catch (err) { console.error("Failed to remove dependency:", err); setError(err instanceof Error ? err.message : "Failed to remove dependency"); } }, [definitions, chain.id, refreshDefinitions, closeContextMenu]); return (
{/* Header */}

{chain.name}

{chain.description && (

{chain.description}

)}
{chain.status} {/* Chain control buttons */} {chain.status === "pending" && definitions.length > 0 && ( )} {chain.status === "active" && ( )}
{error && (
{error}
)}
{/* Main content */}
{/* DAG Canvas */}
{loading ? (

Loading graph...

) : !currentGraph || currentGraph.nodes.length === 0 ? (

{showDefinitions ? "No contract definitions yet" : "No contracts in this chain yet"}

{showDefinitions ? "Add contract definitions to build your chain" : "Start the chain to create contracts from definitions"}

{showDefinitions && ( )}
) : (
{ if (draggedNode) handleDragMove(e); if (edgeDrawing) handleEdgeDrawMove(e); }} onMouseUp={() => { if (draggedNode) handleDragEnd(); if (edgeDrawing) handleEdgeDrawEnd(null); }} onMouseLeave={() => { if (draggedNode) handleDragEnd(); if (edgeDrawing) setEdgeDrawing(null); }} > {/* SVG layer for edges */} {currentGraph.edges.map((edge, index) => { const fromPos = nodePositions.get(edge.from); const toPos = nodePositions.get(edge.to); if (!fromPos || !toPos) return null; // Calculate edge path (from bottom of source to top of target) const startX = fromPos.x + NODE_WIDTH / 2; const startY = fromPos.y + NODE_HEIGHT; const endX = toPos.x + NODE_WIDTH / 2; const endY = toPos.y; // Bezier control points for smooth curves const midY = (startY + endY) / 2; const isHighlighted = hoveredNode === edge.from || hoveredNode === edge.to; return ( ); })} {/* Edge drawing preview line */} {edgeDrawing && (() => { const fromPos = nodePositions.get(edgeDrawing.fromNode); if (!fromPos) return null; const startX = fromPos.x + NODE_WIDTH / 2; const startY = fromPos.y + NODE_HEIGHT; const endX = edgeDrawing.toPoint.x; const endY = edgeDrawing.toPoint.y; const midY = (startY + endY) / 2; return ( ); })()} {/* Node layer */} {showDefinitions && definitionGraph ? definitionGraph.nodes.map((node) => { const pos = nodePositions.get(node.id); if (!pos) return null; const isCheckpoint = node.contractType === "checkpoint"; const status = node.isInstantiated ? node.contractStatus || "pending" : "pending"; const colors = getStatusColor(status, isCheckpoint); const isSelected = selectedNode === node.id; const isHovered = hoveredNode === node.id; const isDragging = draggedNode === node.id; const canEdit = chain.status === "pending"; return (
handleNodeClick(node.id)} onContextMenu={(e) => canEdit && handleContextMenu(e, node.id)} onMouseEnter={() => !draggedNode && setHoveredNode(node.id)} onMouseLeave={() => !draggedNode && setHoveredNode(null)} onMouseUp={() => edgeDrawing && handleEdgeDrawEnd(node.id)} className={`absolute ${ isDragging ? "z-50 shadow-lg" : "transition-all duration-150" } ${ isSelected ? isCheckpoint ? "ring-2 ring-[#a78bfa] ring-offset-2 ring-offset-[#050d18]" : "ring-2 ring-[#75aafc] ring-offset-2 ring-offset-[#050d18]" : "" }`} style={{ left: pos.x, top: pos.y, width: NODE_WIDTH, height: NODE_HEIGHT, transform: isHovered && !isDragging ? "scale(1.02)" : "scale(1)", cursor: isDragging ? "grabbing" : canEdit ? "grab" : "pointer", }} > {/* Drag handle (entire node) */}
canEdit && handleDragStart(e, node.id)} > {/* Status indicator bar */}
{/* Content */}
{node.name} {isCheckpoint ? ( ) : ( )}
{node.isInstantiated ? status : isCheckpoint ? "checkpoint" : "definition"} {node.contractType}
{/* Edge drawing handle (connector dot at bottom) */} {canEdit && (isHovered || isSelected) && !isDragging && (
handleEdgeDrawStart(e, node.id)} title="Drag to create dependency" /> )}
); }) : graph?.nodes.map((node) => { const pos = nodePositions.get(node.contractId); if (!pos) return null; const colors = getStatusColor(node.status); const isSelected = selectedNode === node.contractId; const isHovered = hoveredNode === node.contractId; return (
handleNodeClick(node.contractId)} onDoubleClick={() => handleNodeDoubleClick(node.contractId)} onMouseEnter={() => setHoveredNode(node.contractId)} onMouseLeave={() => setHoveredNode(null)} className={`absolute cursor-pointer transition-all duration-150 ${ isSelected ? "ring-2 ring-[#75aafc] ring-offset-2 ring-offset-[#050d18]" : "" }`} style={{ left: pos.x, top: pos.y, width: NODE_WIDTH, height: NODE_HEIGHT, transform: isHovered ? "scale(1.02)" : "scale(1)", }} >
{/* Status indicator bar */}
{/* Content */}
{node.name}
{node.status} {node.phase && ( {node.phase} )}
); })}
)}
{/* Detail panel */} {selectedDefinition && ( setSelectedNode(null)} onDelete={handleDeleteDefinition} /> )} {selectedContract && ( setSelectedNode(null)} onSelectContract={setSelectedNode} onOpenContract={onContractClick} /> )}
{/* Footer with stats */}
{showDefinitions ? ( <> {definitions.length} definitions {chain.status === "pending" && ( <> | Drag nodes to reposition | Drag from to link | Right-click for options )} {chain.status === "pending" && ( )} ) : ( <> {chain.contracts.length} contracts {chain.contracts.filter((c) => c.contractStatus === "completed").length} completed {chain.contracts.filter((c) => c.contractStatus === "active").length} active Double-click node to open contract )}
{/* Add Definition Modal */} {showAddDefinition && ( d.name)} onSubmit={handleAddDefinition} onCancel={() => setShowAddDefinition(false)} /> )} {/* Context Menu */} {contextMenu && ( d.id === contextMenu.nodeId) : undefined} allDefinitions={definitions} onClose={closeContextMenu} onDelete={handleDeleteDefinition} onRemoveDependency={handleRemoveDependency} onCreateAtPosition={handleCreateAtPosition} /> )}
); } // Context Menu Component interface ContextMenuProps { x: number; y: number; nodeId: string | null; canvasPosition?: { gridX: number; gridY: number }; definition?: ChainContractDefinition; allDefinitions: ChainContractDefinition[]; onClose: () => void; onDelete: (id: string) => void; onRemoveDependency: (nodeId: string, depName: string) => void; onCreateAtPosition: (gridX: number, gridY: number) => void; } function ContextMenu({ x, y, nodeId, canvasPosition, definition, allDefinitions: _allDefinitions, onClose, onDelete, onRemoveDependency, onCreateAtPosition, }: ContextMenuProps) { const menuRef = useRef(null); // Close on click outside useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(e.target as Node)) { onClose(); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [onClose]); // Close on escape useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [onClose]); // Canvas context menu (no node selected) if (!nodeId && canvasPosition) { return (
); } // Node context menu requires both nodeId and definition if (!nodeId || !definition) return null; const dependencies = definition.dependsOnNames || []; return (
{/* Header */}
{definition.name}
{/* Actions */}
{/* Dependencies section */} {dependencies.length > 0 && ( <>
Dependencies
{dependencies.map((depName) => (
{depName}
))}
)} {/* Hint */}
Drag from connector to create dependency
); } // Trash Icon function TrashIcon({ className }: { className?: string }) { return ( ); } // Plus Icon function PlusIcon({ className }: { className?: string }) { return ( ); } interface DefinitionDetailPanelProps { definition: ChainContractDefinition; onClose: () => void; onDelete: (id: string) => void; } function DefinitionDetailPanel({ definition, onClose, onDelete, }: DefinitionDetailPanelProps) { return (

Definition Details

{/* Name */}

{definition.name}

{/* Description */} {definition.description && (

{definition.description}

)} {/* Contract Type */}
{definition.contractType}
{/* Initial Phase */}
{definition.initialPhase || "plan"}
{/* Dependencies */} {definition.dependsOnNames && definition.dependsOnNames.length > 0 && (
{definition.dependsOnNames.map((depName) => ( {depName} ))}
)} {/* Tasks */} {definition.tasks && definition.tasks.length > 0 && (
{definition.tasks.map((task, i) => (
{task.name}
))}
)} {/* Actions */}
); } interface ContractDetailPanelProps { contract: ChainContractDetail; allContracts: ChainContractDetail[]; onClose: () => void; onSelectContract: (contractId: string) => void; onOpenContract: (contractId: string) => void; } function ContractDetailPanel({ contract, allContracts, onClose, onSelectContract, onOpenContract, }: ContractDetailPanelProps) { return (

Contract Details

{/* Name */}

{contract.contractName}

{/* Status */}
{contract.contractStatus}
{/* Phase */}
{contract.contractPhase}
{/* Dependencies */} {contract.dependsOn && contract.dependsOn.length > 0 && (
{contract.dependsOn.map((depId) => { const dep = allContracts.find((c) => c.contractId === depId); return ( ); })}
)} {/* Order Index */}

{contract.orderIndex}

{/* Created */}

{new Date(contract.createdAt).toLocaleString()}

{/* Actions */}
); } interface AddDefinitionModalProps { existingNames: string[]; onSubmit: (req: AddContractDefinitionRequest) => void; onCancel: () => void; } function AddDefinitionModal({ existingNames, onSubmit, onCancel, }: AddDefinitionModalProps) { const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [contractType, setContractType] = useState("simple"); const [initialPhase, setInitialPhase] = useState("plan"); const [dependsOn, setDependsOn] = useState([]); // Checkpoint validation options const [checkDeliverables, setCheckDeliverables] = useState(true); const [runTests, setRunTests] = useState(false); const [checkContent, setCheckContent] = useState(""); const [onFailure, setOnFailure] = useState<"block" | "retry" | "warn">("block"); const isCheckpoint = contractType === "checkpoint"; const handleSubmit = () => { if (!name.trim()) return; const req: AddContractDefinitionRequest = { name: name.trim(), description: description.trim() || undefined, contractType, initialPhase: isCheckpoint ? "execute" : initialPhase, // Checkpoints always start in execute dependsOn: dependsOn.length > 0 ? dependsOn : undefined, }; // Add validation config for checkpoint contracts if (isCheckpoint) { req.validation = { checkDeliverables, runTests, checkContent: checkContent.trim() || undefined, onFailure, }; } onSubmit(req); }; const toggleDependency = (depName: string) => { setDependsOn((prev) => prev.includes(depName) ? prev.filter((d) => d !== depName) : [...prev, depName] ); }; return (

Add Contract Definition

{/* Name */}
setName(e.target.value)} placeholder="e.g., Research Phase" className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]" autoFocus />
{/* Description */}