diff options
Diffstat (limited to 'makima/frontend/src/components/chains/ChainEditor.tsx')
| -rw-r--r-- | makima/frontend/src/components/chains/ChainEditor.tsx | 207 |
1 files changed, 173 insertions, 34 deletions
diff --git a/makima/frontend/src/components/chains/ChainEditor.tsx b/makima/frontend/src/components/chains/ChainEditor.tsx index 9028c3e..49e585c 100644 --- a/makima/frontend/src/components/chains/ChainEditor.tsx +++ b/makima/frontend/src/components/chains/ChainEditor.tsx @@ -54,6 +54,7 @@ export function ChainEditor({ const [isStarting, setIsStarting] = useState(false); const [isStopping, setIsStopping] = useState(false); const [error, setError] = useState<string | null>(null); + const [withSupervisor, setWithSupervisor] = useState(false); // Load definitions when chain changes useEffect(() => { @@ -131,7 +132,22 @@ export function ChainEditor({ [onContractClick, showDefinitions] ); - const getStatusColor = (status: string) => { + 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" }; @@ -158,14 +174,14 @@ export function ChainEditor({ setIsStarting(true); setError(null); try { - await startChain(chain.id); + await startChain(chain.id, { withSupervisor }); onRefresh(); } catch (err) { setError(err instanceof Error ? err.message : "Failed to start chain"); } finally { setIsStarting(false); } - }, [chain.id, onRefresh]); + }, [chain.id, onRefresh, withSupervisor]); const handleStopChain = useCallback(async () => { if (!confirm("Are you sure you want to stop this chain?")) return; @@ -245,13 +261,26 @@ export function ChainEditor({ </span> {/* Chain control buttons */} {chain.status === "pending" && definitions.length > 0 && ( - <button - onClick={handleStartChain} - disabled={isStarting} - className="px-3 py-1 font-mono text-xs text-[#dbe7ff] bg-green-600 hover:bg-green-700 border border-green-500 transition-colors disabled:opacity-50" - > - {isStarting ? "Starting..." : "Start Chain"} - </button> + <> + <label className="flex items-center gap-1 font-mono text-[10px] text-[#9bc3ff] cursor-pointer"> + <input + type="checkbox" + checked={withSupervisor} + onChange={(e) => setWithSupervisor(e.target.checked)} + className="w-3 h-3 accent-[#75aafc]" + /> + <span title="Create a Claude supervisor task to monitor chain progress"> + Supervisor + </span> + </label> + <button + onClick={handleStartChain} + disabled={isStarting} + className="px-3 py-1 font-mono text-xs text-[#dbe7ff] bg-green-600 hover:bg-green-700 border border-green-500 transition-colors disabled:opacity-50" + > + {isStarting ? "Starting..." : "Start Chain"} + </button> + </> )} {chain.status === "active" && ( <button @@ -381,10 +410,11 @@ export function ChainEditor({ 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); + const colors = getStatusColor(status, isCheckpoint); const isSelected = selectedNode === node.id; const isHovered = hoveredNode === node.id; @@ -396,7 +426,9 @@ export function ChainEditor({ onMouseLeave={() => setHoveredNode(null)} className={`absolute cursor-pointer transition-all duration-150 ${ isSelected - ? "ring-2 ring-[#75aafc] ring-offset-2 ring-offset-[#050d18]" + ? isCheckpoint + ? "ring-2 ring-[#a78bfa] ring-offset-2 ring-offset-[#050d18]" + : "ring-2 ring-[#75aafc] ring-offset-2 ring-offset-[#050d18]" : "" }`} style={{ @@ -408,9 +440,15 @@ export function ChainEditor({ }} > <div - className="w-full h-full rounded-lg border-2 bg-[#0a1628] overflow-hidden" + className={`w-full h-full rounded-lg border-2 overflow-hidden ${ + isCheckpoint ? "bg-[#0f0a1e]" : "bg-[#0a1628]" + }`} style={{ - borderColor: isSelected ? "#75aafc" : colors.border, + borderColor: isSelected + ? isCheckpoint + ? "#a78bfa" + : "#75aafc" + : colors.border, borderStyle: node.isInstantiated ? "solid" : "dashed", }} > @@ -425,7 +463,11 @@ export function ChainEditor({ <span className="font-mono text-xs text-[#dbe7ff] truncate flex-1"> {node.name} </span> - <ChainIcon className="w-4 h-4 text-[#75aafc] opacity-50 flex-shrink-0 ml-1" /> + {isCheckpoint ? ( + <CheckIcon className="w-4 h-4 text-[#a78bfa] opacity-70 flex-shrink-0 ml-1" /> + ) : ( + <ChainIcon className="w-4 h-4 text-[#75aafc] opacity-50 flex-shrink-0 ml-1" /> + )} </div> <div className="flex items-center justify-between"> <span @@ -435,7 +477,7 @@ export function ChainEditor({ backgroundColor: `${colors.bg}20`, }} > - {node.isInstantiated ? status : "definition"} + {node.isInstantiated ? status : isCheckpoint ? "checkpoint" : "definition"} </span> <span className="font-mono text-[10px] text-[#8b949e]"> {node.contractType} @@ -833,16 +875,33 @@ function AddDefinitionModal({ const [contractType, setContractType] = useState("simple"); const [initialPhase, setInitialPhase] = useState("plan"); const [dependsOn, setDependsOn] = useState<string[]>([]); + // 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; - onSubmit({ + const req: AddContractDefinitionRequest = { name: name.trim(), description: description.trim() || undefined, contractType, - initialPhase, + 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) => { @@ -901,24 +960,87 @@ function AddDefinitionModal({ <option value="simple">Simple</option> <option value="specification">Specification</option> <option value="execute">Execute</option> + <option value="checkpoint">Checkpoint (Validation)</option> </select> + {isCheckpoint && ( + <p className="mt-1 font-mono text-[10px] text-[#a78bfa]"> + Checkpoint contracts validate outputs before allowing downstream contracts to proceed. + </p> + )} </div> - {/* Initial Phase */} - <div> - <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1"> - Initial Phase - </label> - <select - value={initialPhase} - onChange={(e) => setInitialPhase(e.target.value)} - className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]" - > - <option value="plan">Plan</option> - <option value="execute">Execute</option> - <option value="review">Review</option> - </select> - </div> + {/* Initial Phase - hidden for checkpoints */} + {!isCheckpoint && ( + <div> + <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1"> + Initial Phase + </label> + <select + value={initialPhase} + onChange={(e) => setInitialPhase(e.target.value)} + className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]" + > + <option value="plan">Plan</option> + <option value="execute">Execute</option> + <option value="review">Review</option> + </select> + </div> + )} + + {/* Checkpoint Validation Options */} + {isCheckpoint && ( + <div className="p-3 bg-[#0f0a1e] border border-[#6366f1]/30 space-y-3"> + <h4 className="font-mono text-xs text-[#a78bfa] uppercase">Validation Options</h4> + + <label className="flex items-center gap-2 cursor-pointer"> + <input + type="checkbox" + checked={checkDeliverables} + onChange={(e) => setCheckDeliverables(e.target.checked)} + className="accent-[#a78bfa]" + /> + <span className="font-mono text-xs text-[#dbe7ff]">Check required deliverables exist</span> + </label> + + <label className="flex items-center gap-2 cursor-pointer"> + <input + type="checkbox" + checked={runTests} + onChange={(e) => setRunTests(e.target.checked)} + className="accent-[#a78bfa]" + /> + <span className="font-mono text-xs text-[#dbe7ff]">Run test suite</span> + </label> + + <div> + <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1"> + Custom Validation Instructions (optional) + </label> + <textarea + value={checkContent} + onChange={(e) => setCheckContent(e.target.value)} + placeholder="Additional criteria for Claude to check..." + rows={3} + className="w-full px-2 py-1.5 bg-[#0d1b2d] border border-[#6366f1]/50 text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#a78bfa] resize-none" + /> + </div> + + <div> + <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1"> + On Failure + </label> + <select + value={onFailure} + onChange={(e) => setOnFailure(e.target.value as "block" | "retry" | "warn")} + className="w-full px-2 py-1.5 bg-[#0d1b2d] border border-[#6366f1]/50 text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#a78bfa]" + > + <option value="block">Block - Stop chain until fixed</option> + <option value="retry">Retry - Retry upstream contracts</option> + <option value="warn">Warn - Log warning but continue</option> + </select> + </div> + </div> + )} {/* Dependencies */} {existingNames.length > 0 && ( @@ -980,3 +1102,20 @@ function ChainIcon({ className }: { className?: string }) { </svg> ); } + +function CheckIcon({ className }: { className?: string }) { + return ( + <svg + className={className} + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + strokeWidth="2" + strokeLinecap="round" + strokeLinejoin="round" + > + <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" /> + <polyline points="22 4 12 14.01 9 11.01" /> + </svg> + ); +} |
