diff options
| author | soryu <soryu@soryu.co> | 2026-02-04 01:07:14 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-02-04 01:07:14 +0000 |
| commit | a734bf1a472b19d63341769d26a66628575df7f4 (patch) | |
| tree | ec78f57e5721d157c620df0c99de5b5efe485231 /makima/frontend | |
| parent | c732dd048128808cd9f67f6e1176a5b565df5678 (diff) | |
| download | soryu-a734bf1a472b19d63341769d26a66628575df7f4.tar.gz soryu-a734bf1a472b19d63341769d26a66628575df7f4.zip | |
Add chain checkpoint contracts
Diffstat (limited to 'makima/frontend')
| -rw-r--r-- | makima/frontend/src/components/chains/ChainEditor.tsx | 207 | ||||
| -rw-r--r-- | makima/frontend/src/lib/api.ts | 44 | ||||
| -rw-r--r-- | makima/frontend/tsconfig.tsbuildinfo | 2 |
3 files changed, 216 insertions, 37 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> + ); +} diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index e5cf1d8..a08cba7 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -3255,6 +3255,20 @@ export interface ChainDeliverableDefinition { priority?: string; } +/** Validation configuration for checkpoint contracts */ +export interface CheckpointValidation { + /** Check that all required deliverables from upstream contracts exist */ + checkDeliverables?: boolean; + /** Run tests in the repository */ + runTests?: boolean; + /** Custom validation instructions for Claude */ + checkContent?: string; + /** Action on failure: "block", "retry", "warn" */ + onFailure?: "block" | "retry" | "warn"; + /** Max retry attempts for upstream contracts */ + maxRetries?: number; +} + /** Contract definition stored in chain (before actual contract is created) */ export interface ChainContractDefinition { id: string; @@ -3266,6 +3280,8 @@ export interface ChainContractDefinition { dependsOnNames: string[]; tasks: ChainTaskDefinition[] | null; deliverables: ChainDeliverableDefinition[] | null; + /** Validation config for checkpoint contracts */ + validation: CheckpointValidation | null; editorX: number | null; editorY: number | null; orderIndex: number; @@ -3281,6 +3297,8 @@ export interface AddContractDefinitionRequest { dependsOn?: string[]; tasks?: ChainTaskDefinition[]; deliverables?: ChainDeliverableDefinition[]; + /** Validation config (for checkpoint contracts) */ + validation?: CheckpointValidation; editorX?: number; editorY?: number; orderIndex?: number; @@ -3295,6 +3313,8 @@ export interface UpdateContractDefinitionRequest { dependsOn?: string[]; tasks?: ChainTaskDefinition[]; deliverables?: ChainDeliverableDefinition[]; + /** Validation config (for checkpoint contracts) */ + validation?: CheckpointValidation; editorX?: number; editorY?: number; orderIndex?: number; @@ -3402,10 +3422,30 @@ export async function getChainDefinitionGraph( return res.json(); } -/** Start a chain (creates root contracts and spawns supervisor) */ -export async function startChain(chainId: string): Promise<StartChainResponse> { +/** Options for starting a chain */ +export interface StartChainOptions { + /** Whether to create a supervisor task that monitors chain progress */ + withSupervisor?: boolean; + /** Repository URL for the supervisor task to work with */ + repositoryUrl?: string; +} + +/** Start a chain (creates root contracts and optionally spawns supervisor) */ +export async function startChain( + chainId: string, + options?: StartChainOptions +): Promise<StartChainResponse> { + const body = options + ? JSON.stringify({ + withSupervisor: options.withSupervisor ?? false, + repositoryUrl: options.repositoryUrl, + }) + : undefined; + const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}/start`, { method: "POST", + headers: body ? { "Content-Type": "application/json" } : undefined, + body, }); if (!res.ok) { const error = await res.json().catch(() => ({ message: res.statusText })); diff --git a/makima/frontend/tsconfig.tsbuildinfo b/makima/frontend/tsconfig.tsbuildinfo index 26dc69a..425babe 100644 --- a/makima/frontend/tsconfig.tsbuildinfo +++ b/makima/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/phaseconfirmationnotification.tsx","./src/components/protectedroute.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/chains/chaineditor.tsx","./src/components/chains/chainlist.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/commandmodepanel.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractcontextmenu.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phaseconfirmationmodal.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/history/checkpointcard.tsx","./src/components/history/checkpointlist.tsx","./src/components/history/conversationmessage.tsx","./src/components/history/conversationview.tsx","./src/components/history/historyfilters.tsx","./src/components/history/resumecontrols.tsx","./src/components/history/timelineeventcard.tsx","./src/components/history/timelinelist.tsx","./src/components/history/index.ts","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/branchtaskmodal.tsx","./src/components/mesh/contractcompletequestion.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/gitactionspanel.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/patcheslistpanel.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/mesh/worktreefilespanel.tsx","./src/components/workflow/phasecolumn.tsx","./src/components/workflow/workflowboard.tsx","./src/components/workflow/workflowcontractcard.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usechains.ts","./src/hooks/usecontracts.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usespeakwebsocket.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/chains.tsx","./src/routes/contract-file.tsx","./src/routes/contracts.tsx","./src/routes/files.tsx","./src/routes/history.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/settings.tsx","./src/routes/speak.tsx","./src/routes/workflow.tsx","./src/types/messages.ts"],"version":"5.9.3"}
\ No newline at end of file +{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/phaseconfirmationnotification.tsx","./src/components/protectedroute.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/chains/chaineditor.tsx","./src/components/chains/chainlist.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/commandmodepanel.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractcontextmenu.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phaseconfirmationmodal.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/history/checkpointcard.tsx","./src/components/history/checkpointlist.tsx","./src/components/history/conversationmessage.tsx","./src/components/history/conversationview.tsx","./src/components/history/historyfilters.tsx","./src/components/history/resumecontrols.tsx","./src/components/history/timelineeventcard.tsx","./src/components/history/timelinelist.tsx","./src/components/history/index.ts","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/discusscontractmodal.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/branchtaskmodal.tsx","./src/components/mesh/contractcompletequestion.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/gitactionspanel.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/patcheslistpanel.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/mesh/worktreefilespanel.tsx","./src/components/workflow/phasecolumn.tsx","./src/components/workflow/workflowboard.tsx","./src/components/workflow/workflowcontractcard.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usechains.ts","./src/hooks/usecontracts.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usespeakwebsocket.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/chains.tsx","./src/routes/contract-file.tsx","./src/routes/contracts.tsx","./src/routes/files.tsx","./src/routes/history.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/settings.tsx","./src/routes/speak.tsx","./src/routes/workflow.tsx","./src/types/messages.ts"],"version":"5.9.3"}
\ No newline at end of file |
