From 8c23b3ab6f7fabca01b0468911bae073aa5ced32 Mon Sep 17 00:00:00 2001 From: soryu Date: Mon, 9 Feb 2026 00:11:51 +0000 Subject: Add new directive mechanism v3 --- makima/frontend/src/components/NavStrip.tsx | 1 + .../src/components/directives/DirectiveDAG.tsx | 87 ++++++++ .../src/components/directives/DirectiveDetail.tsx | 216 ++++++++++++++++++++ .../src/components/directives/DirectiveList.tsx | 87 ++++++++ .../src/components/directives/StepNode.tsx | 82 ++++++++ makima/frontend/src/hooks/useDirectives.ts | 150 ++++++++++++++ makima/frontend/src/lib/api.ts | 222 +++++++++++++++++++++ makima/frontend/src/main.tsx | 17 ++ makima/frontend/src/routes/directives.tsx | 168 ++++++++++++++++ 9 files changed, 1030 insertions(+) create mode 100644 makima/frontend/src/components/directives/DirectiveDAG.tsx create mode 100644 makima/frontend/src/components/directives/DirectiveDetail.tsx create mode 100644 makima/frontend/src/components/directives/DirectiveList.tsx create mode 100644 makima/frontend/src/components/directives/StepNode.tsx create mode 100644 makima/frontend/src/hooks/useDirectives.ts create mode 100644 makima/frontend/src/routes/directives.tsx (limited to 'makima/frontend/src') diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx index fb95c7f..46fef7a 100644 --- a/makima/frontend/src/components/NavStrip.tsx +++ b/makima/frontend/src/components/NavStrip.tsx @@ -13,6 +13,7 @@ const NAV_LINKS: NavLink[] = [ { label: "Contracts", href: "/contracts", requiresAuth: true }, { label: "Board", href: "/workflow", requiresAuth: true }, { label: "Mesh", href: "/mesh", requiresAuth: true }, + { label: "Directives", href: "/directives", requiresAuth: true }, { label: "History", href: "/history", requiresAuth: true }, ]; diff --git a/makima/frontend/src/components/directives/DirectiveDAG.tsx b/makima/frontend/src/components/directives/DirectiveDAG.tsx new file mode 100644 index 0000000..f288a0d --- /dev/null +++ b/makima/frontend/src/components/directives/DirectiveDAG.tsx @@ -0,0 +1,87 @@ +import { useMemo } from "react"; +import type { DirectiveStep } from "../../lib/api"; +import { StepNode } from "./StepNode"; + +interface DirectiveDAGProps { + steps: DirectiveStep[]; + onComplete?: (stepId: string) => void; + onFail?: (stepId: string) => void; + onSkip?: (stepId: string) => void; +} + +interface Layer { + steps: DirectiveStep[]; +} + +function topoSort(steps: DirectiveStep[]): Layer[] { + if (steps.length === 0) return []; + + const stepMap = new Map(steps.map((s) => [s.id, s])); + const assigned = new Set(); + const layers: Layer[] = []; + + // Iteratively find steps whose dependencies are all assigned + let remaining = [...steps]; + while (remaining.length > 0) { + const layer: DirectiveStep[] = []; + for (const step of remaining) { + const depsResolved = step.dependsOn.every( + (depId) => assigned.has(depId) || !stepMap.has(depId) + ); + if (depsResolved) { + layer.push(step); + } + } + + if (layer.length === 0) { + // Cycle detected or orphaned — push all remaining + layers.push({ steps: remaining }); + break; + } + + for (const s of layer) { + assigned.add(s.id); + } + layers.push({ steps: layer.sort((a, b) => a.orderIndex - b.orderIndex) }); + remaining = remaining.filter((s) => !assigned.has(s.id)); + } + + return layers; +} + +export function DirectiveDAG({ steps, onComplete, onFail, onSkip }: DirectiveDAGProps) { + const layers = useMemo(() => topoSort(steps), [steps]); + + if (steps.length === 0) { + return ( +
+ No steps yet. Add steps to build the DAG. +
+ ); + } + + return ( +
+ {layers.map((layer, layerIdx) => ( +
+ {layerIdx > 0 && ( +
+
+
+ )} +
+ {layer.steps.map((step) => ( + onComplete(step.id) : undefined} + onFail={onFail ? () => onFail(step.id) : undefined} + onSkip={onSkip ? () => onSkip(step.id) : undefined} + /> + ))} +
+
+ ))} +
+ ); +} diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx new file mode 100644 index 0000000..abd2c55 --- /dev/null +++ b/makima/frontend/src/components/directives/DirectiveDetail.tsx @@ -0,0 +1,216 @@ +import { useState } from "react"; +import type { DirectiveWithSteps, DirectiveStatus } from "../../lib/api"; +import { DirectiveDAG } from "./DirectiveDAG"; + +const STATUS_BADGE: Record = { + draft: { color: "text-[#7788aa] border-[#2a3a5a]", label: "DRAFT" }, + active: { color: "text-green-400 border-green-800", label: "ACTIVE" }, + idle: { color: "text-yellow-400 border-yellow-800", label: "IDLE" }, + paused: { color: "text-orange-400 border-orange-800", label: "PAUSED" }, + archived: { color: "text-[#556677] border-[#2a3a5a]", label: "ARCHIVED" }, +}; + +interface DirectiveDetailProps { + directive: DirectiveWithSteps; + onStart: () => void; + onPause: () => void; + onAdvance: () => void; + onCompleteStep: (stepId: string) => void; + onFailStep: (stepId: string) => void; + onSkipStep: (stepId: string) => void; + onUpdateGoal: (goal: string) => void; + onDelete: () => void; + onRefresh: () => void; +} + +export function DirectiveDetail({ + directive, + onStart, + onPause, + onAdvance, + onCompleteStep, + onFailStep, + onSkipStep, + onUpdateGoal, + onDelete, + onRefresh, +}: DirectiveDetailProps) { + const [editingGoal, setEditingGoal] = useState(false); + const [goalText, setGoalText] = useState(directive.goal); + const badge = STATUS_BADGE[directive.status] || STATUS_BADGE.draft; + + const completedSteps = directive.steps.filter((s) => s.status === "completed").length; + const totalSteps = directive.steps.length; + const progress = totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0; + + const handleGoalSave = () => { + if (goalText.trim() && goalText !== directive.goal) { + onUpdateGoal(goalText.trim()); + } + setEditingGoal(false); + }; + + return ( +
+ {/* Header */} +
+
+

+ {directive.title} +

+
+ + {badge.label} + + +
+
+ + {/* Progress bar */} + {totalSteps > 0 && ( +
+
+
+
+ + {completedSteps}/{totalSteps} steps + +
+ )} + + {/* Repo info */} + {(directive.repositoryUrl || directive.localPath) && ( +
+ {directive.repositoryUrl || directive.localPath} + {directive.baseBranch && ` @ ${directive.baseBranch}`} +
+ )} + + {/* Controls */} +
+ {(directive.status === "draft" || directive.status === "paused") && ( + + )} + {directive.status === "active" && ( + <> + + + + )} + {directive.status === "idle" && ( +
+ + All steps done. Update goal to add new work. + + +
+ )} + +
+
+ + {/* Goal */} +
+
+ + Goal + + {!editingGoal && ( + + )} +
+ {editingGoal ? ( +
+