From 97e21c8296ec5f91912d56980ebf3b18a1ca3507 Mon Sep 17 00:00:00 2001 From: soryu Date: Sat, 7 Feb 2026 18:27:54 +0000 Subject: Add directive monitor contracts --- .../directives/DirectiveContractsTab.tsx | 131 +++++++ .../src/components/directives/DirectiveDetail.tsx | 393 +++++++++++++++------ .../src/components/directives/StepDiagram.tsx | 152 ++++++++ makima/frontend/src/lib/api.ts | 14 + 4 files changed, 585 insertions(+), 105 deletions(-) create mode 100644 makima/frontend/src/components/directives/DirectiveContractsTab.tsx create mode 100644 makima/frontend/src/components/directives/StepDiagram.tsx (limited to 'makima/frontend/src') diff --git a/makima/frontend/src/components/directives/DirectiveContractsTab.tsx b/makima/frontend/src/components/directives/DirectiveContractsTab.tsx new file mode 100644 index 0000000..59ebfc8 --- /dev/null +++ b/makima/frontend/src/components/directives/DirectiveContractsTab.tsx @@ -0,0 +1,131 @@ +import { useNavigate } from "react-router"; +import type { + DirectiveWithChains, + StepContractSummary, + ContractPhase, +} from "../../lib/api"; +import { PhaseProgressBarCompact } from "../contracts/PhaseProgressBar"; + +interface DirectiveContractsTabProps { + directive: DirectiveWithChains; +} + +const statusColors: Record = { + active: "text-green-400", + completed: "text-blue-400", + archived: "text-[#555]", +}; + +function ContractCard({ + summary, + label, +}: { + summary: StepContractSummary; + label: string; +}) { + const navigate = useNavigate(); + + const progressPct = + summary.taskCount > 0 + ? Math.round((summary.tasksDone / summary.taskCount) * 100) + : 0; + + return ( +
navigate(`/contracts/${summary.id}`)} + > +
+
+ + {summary.name} + + + {summary.contractType} + +
+
+ + {summary.status} + + +
+
+ +
+ + {label} + + +
+ + {/* Task progress bar */} +
+
+
+
+ + {summary.tasksDone}/{summary.taskCount} tasks + +
+
+ ); +} + +export function DirectiveContractsTab({ + directive, +}: DirectiveContractsTabProps) { + // Collect all contract summaries + const contracts: { summary: StepContractSummary; label: string }[] = []; + + if (directive.orchestratorContractSummary) { + contracts.push({ + summary: directive.orchestratorContractSummary, + label: "Planning", + }); + } + + for (const chain of directive.chains) { + for (const step of chain.steps) { + if (step.contractSummary) { + contracts.push({ + summary: step.contractSummary, + label: step.name, + }); + } + } + } + + if (contracts.length === 0) { + return ( +
+

+ {directive.status === "draft" + ? "No contracts yet. Start the directive to begin planning." + : directive.status === "planning" + ? "Planning in progress... contracts will appear when steps are created." + : "No contracts associated with this directive."} +

+
+ ); + } + + return ( +
+ {contracts.map((c) => ( + + ))} +
+ ); +} diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx index 094cdf2..95dc7cc 100644 --- a/makima/frontend/src/components/directives/DirectiveDetail.tsx +++ b/makima/frontend/src/components/directives/DirectiveDetail.tsx @@ -1,12 +1,16 @@ -import { useEffect, useRef } from "react"; +import { useState, useEffect, useRef } from "react"; import { useNavigate } from "react-router"; import type { DirectiveWithChains, DirectiveStatus, ChainWithSteps, ChainStep, + ContractPhase, } from "../../lib/api"; import { getDirective } from "../../lib/api"; +import { PhaseProgressBarCompact } from "../contracts/PhaseProgressBar"; +import { StepDiagram } from "./StepDiagram"; +import { DirectiveContractsTab } from "./DirectiveContractsTab"; interface DirectiveDetailProps { directive: DirectiveWithChains; @@ -16,6 +20,8 @@ interface DirectiveDetailProps { onRefresh?: (updated: DirectiveWithChains) => void; } +type Tab = "overview" | "chain" | "contracts"; + const statusColors: Record = { draft: "text-[#888]", planning: "text-yellow-400", @@ -36,17 +42,18 @@ const stepStatusColors: Record = { const stepStatusIcons: Record = { pending: "\u25CB", // ○ running: "\u25D4", // ◔ - passed: "\u25CF", // ● - failed: "\u2715", // ✕ + passed: "\u25CF", // ● + failed: "\u2715", // ✕ }; function StepRow({ step }: { step: ChainStep }) { const navigate = useNavigate(); const color = stepStatusColors[step.status] || "text-[#888]"; const icon = stepStatusIcons[step.status] || "\u25CB"; + const summary = step.contractSummary; return ( -
+
{icon}
@@ -57,21 +64,41 @@ function StepRow({ step }: { step: ChainStep }) { {step.status}
+ {summary && ( +
+ + + {summary.tasksDone}/{summary.taskCount} tasks + + {step.contractId && ( + + )} +
+ )} + {!summary && step.contractId && ( + + )} {step.description && ( -

+

{step.description}

)}
- {step.contractId && ( - - )}
); } @@ -84,7 +111,9 @@ function ChainCard({ chainWithSteps }: { chainWithSteps: ChainWithSteps }) {
- {chain.name} + + {chain.name} + gen {chain.generation} · {chain.status} @@ -102,7 +131,9 @@ function ChainCard({ chainWithSteps }: { chainWithSteps: ChainWithSteps }) { {chain.failedSteps} failed )} {chain.currentConfidence != null && ( - confidence: {(chain.currentConfidence * 100).toFixed(0)}% + + confidence: {(chain.currentConfidence * 100).toFixed(0)}% + )}
@@ -151,6 +182,7 @@ export function DirectiveDetail({ onRefresh, }: DirectiveDetailProps) { const navigate = useNavigate(); + const [activeTab, setActiveTab] = useState("overview"); // Auto-poll when directive is in an active state const isLive = @@ -185,6 +217,31 @@ export function DirectiveDetail({ }; }, [isLive, directive.id, onRefresh]); + // Count total steps and completed steps across all chains + const totalSteps = directive.chains.reduce( + (sum, c) => sum + c.totalSteps, + 0 + ); + const completedSteps = directive.chains.reduce( + (sum, c) => sum + c.completedSteps, + 0 + ); + + // Count contracts + const contractCount = + (directive.orchestratorContractSummary ? 1 : 0) + + directive.chains.reduce( + (sum, c) => + sum + c.steps.filter((s) => s.contractSummary != null).length, + 0 + ); + + const tabs: { key: Tab; label: string; count?: number }[] = [ + { key: "overview", label: "Overview" }, + { key: "chain", label: "Chain", count: totalSteps }, + { key: "contracts", label: "Contracts", count: contractCount }, + ]; + return (
{/* Header */} @@ -235,109 +292,235 @@ export function DirectiveDetail({
- {/* Content */} -
- {/* Orchestrator contract link */} - {directive.orchestratorContractId && ( -
- - Planning Contract - - - {directive.status === "planning" && ( - - planning in progress + `} + > + {tab.label} + {tab.count != null && tab.count > 0 && ( + + ({tab.count}) )} -
- )} + + ))} +
- {/* Goal */} -
-

- Goal -

-

- {directive.goal} -

-
+ {/* Tab content */} +
+ {activeTab === "overview" && ( +
+ {/* Orchestrator contract link */} + {directive.orchestratorContractId && ( +
+ + Planning Contract + + {directive.orchestratorContractSummary && ( + + )} + + {directive.status === "planning" && ( + + planning in progress + + )} +
+ )} - {/* Config */} -
-
- - Autonomy - -
- {directive.autonomyLevel} + {/* Goal */} +
+

+ Goal +

+

+ {directive.goal} +

-
-
- - Chains - -
- {directive.chainGenerationCount} generated + + {/* Config grid */} +
+
+ + Autonomy + +
+ {directive.autonomyLevel} +
+
+
+ + Chains + +
+ {directive.chainGenerationCount} generated +
+
+
+ + Cost + +
+ ${directive.totalCostUsd.toFixed(2)} +
+
+ {directive.repositoryUrl && ( +
+ + Repository + +
+ {directive.repositoryUrl} +
+
+ )}
-
-
- - Cost - -
- ${directive.totalCostUsd.toFixed(2)} + + {/* Stat cards */} +
+
+
+ {totalSteps} +
+
+ Total Steps +
+
+
+
+ {completedSteps} +
+
+ Completed +
+
+
+
+ ${directive.totalCostUsd.toFixed(2)} +
+
+ Cost +
+
-
- {directive.repositoryUrl && ( + + {/* Structured sections */} + + + + + + {/* Metadata */}
- - Repository - -
- {directive.repositoryUrl} +

+ Metadata +

+
+ Created + + {new Date(directive.createdAt).toLocaleString()} + + Updated + + {new Date(directive.updatedAt).toLocaleString()} + + {directive.startedAt && ( + <> + Started + + {new Date(directive.startedAt).toLocaleString()} + + + )} + {directive.completedAt && ( + <> + Completed + + {new Date(directive.completedAt).toLocaleString()} + + + )} + Version + {directive.version}
- )} -
+
+ )} - {/* Structured sections */} - - - - + {activeTab === "chain" && ( +
+ {/* Step diagram */} + {directive.chains.length > 0 && ( +
+

+ Step Dependencies +

+ c.steps)} + /> +
+ )} - {/* Chains */} -
-

- Chains ({directive.chains.length}) -

- {directive.chains.length === 0 ? ( -

- {directive.status === "planning" - ? "Planning in progress... chains will appear when the planner completes." - : "No chains yet. Chains are created during planning."} -

- ) : ( -
- {directive.chains.map((cws) => ( - - ))} + {/* Chain cards */} +
+

+ Chains ({directive.chains.length}) +

+ {directive.chains.length === 0 ? ( +

+ {directive.status === "planning" + ? "Planning in progress... chains will appear when the planner completes." + : directive.status === "draft" + ? "No chains yet. Start the directive to begin planning." + : "No chains created for this directive."} +

+ ) : ( +
+ {directive.chains.map((cws) => ( + + ))} +
+ )}
- )} -
+
+ )} + + {activeTab === "contracts" && ( + + )}
); diff --git a/makima/frontend/src/components/directives/StepDiagram.tsx b/makima/frontend/src/components/directives/StepDiagram.tsx new file mode 100644 index 0000000..5c65ae1 --- /dev/null +++ b/makima/frontend/src/components/directives/StepDiagram.tsx @@ -0,0 +1,152 @@ +import { useNavigate } from "react-router"; +import type { ChainStep, ContractPhase } from "../../lib/api"; +import { PhaseProgressBarCompact } from "../contracts/PhaseProgressBar"; + +interface StepDiagramProps { + steps: ChainStep[]; +} + +const statusBorderColors: Record = { + pending: "border-[#555]", + running: "border-yellow-400", + passed: "border-green-400", + failed: "border-red-400", +}; + +const statusDotColors: Record = { + pending: "bg-[#555]", + running: "bg-yellow-400", + passed: "bg-green-400", + failed: "bg-red-400", +}; + +/** + * Assign depth to each step via topological sort. + * Steps with no dependsOn = depth 0. Steps depending only on depth-0 = depth 1. Etc. + */ +function assignDepths(steps: ChainStep[]): Map { + const depths = new Map(); + const stepMap = new Map(steps.map((s) => [s.id, s])); + + function getDepth(id: string): number { + if (depths.has(id)) return depths.get(id)!; + const step = stepMap.get(id); + if (!step || !step.dependsOn || step.dependsOn.length === 0) { + depths.set(id, 0); + return 0; + } + const maxParent = Math.max( + ...step.dependsOn.map((depId) => getDepth(depId)) + ); + const d = maxParent + 1; + depths.set(id, d); + return d; + } + + for (const step of steps) { + getDepth(step.id); + } + + return depths; +} + +export function StepDiagram({ steps }: StepDiagramProps) { + const navigate = useNavigate(); + + if (steps.length === 0) { + return ( +

No steps to display.

+ ); + } + + const depths = assignDepths(steps); + const maxDepth = Math.max(...Array.from(depths.values())); + + // Group steps by depth + const levels: ChainStep[][] = []; + for (let d = 0; d <= maxDepth; d++) { + levels.push( + steps + .filter((s) => depths.get(s.id) === d) + .sort((a, b) => a.orderIndex - b.orderIndex) + ); + } + + // Build position map for connectors + const stepPositions = new Map(); + levels.forEach((level, li) => { + level.forEach((step, si) => { + stepPositions.set(step.id, { level: li, index: si }); + }); + }); + + return ( +
+ {levels.map((level, li) => ( +
+ {li > 0 && ( +
+
+
+ )} + {level.map((step) => { + const borderColor = + statusBorderColors[step.status] || "border-[#555]"; + const dotColor = statusDotColors[step.status] || "bg-[#555]"; + const summary = step.contractSummary; + const hasContract = !!step.contractId; + + return ( +
{ + if (hasContract) navigate(`/contracts/${step.contractId}`); + }} + title={hasContract ? "View contract" : undefined} + > +
+
+ + {step.name} + + {hasContract && ( + + → + + )} +
+ {summary && ( + <> +
+ +
+
+ {summary.tasksDone}/{summary.taskCount} tasks + {summary.tasksRunning > 0 && ( + + {summary.tasksRunning} running + + )} + {summary.tasksFailed > 0 && ( + + {summary.tasksFailed} failed + + )} +
+ + )} +
+ ); + })} +
+ ))} +
+ ); +} diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index ccc7156..9782a07 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -3075,6 +3075,18 @@ export interface DirectiveChain { updatedAt: string; } +export interface StepContractSummary { + id: string; + name: string; + contractType: string; + phase: string; + status: string; + taskCount: number; + tasksDone: number; + tasksRunning: number; + tasksFailed: number; +} + export interface ChainStep { id: string; chainId: string; @@ -3092,6 +3104,7 @@ export interface ChainStep { startedAt: string | null; completedAt: string | null; createdAt: string; + contractSummary: StepContractSummary | null; } export interface ChainWithSteps extends DirectiveChain { @@ -3099,6 +3112,7 @@ export interface ChainWithSteps extends DirectiveChain { } export interface DirectiveWithChains extends Directive { + orchestratorContractSummary: StepContractSummary | null; chains: ChainWithSteps[]; } -- cgit v1.2.3