From 8f757f561eeb397aaea70d7c10d41445cc5e50b5 Mon Sep 17 00:00:00 2001
From: soryu <soryu@soryu.co>
Date: Sat, 7 Feb 2026 16:58:38 +0000
Subject: Show directive init on frontend

---
 .../src/components/directives/DirectiveDetail.tsx  | 180 ++++++++++++++++++---
 1 file changed, 156 insertions(+), 24 deletions(-)

(limited to 'makima/frontend/src/components/directives/DirectiveDetail.tsx')

diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx
index e519b92..094cdf2 100644
--- a/makima/frontend/src/components/directives/DirectiveDetail.tsx
+++ b/makima/frontend/src/components/directives/DirectiveDetail.tsx
@@ -1,14 +1,19 @@
+import { useEffect, useRef } from "react";
+import { useNavigate } from "react-router";
 import type {
   DirectiveWithChains,
   DirectiveStatus,
-  DirectiveChain,
+  ChainWithSteps,
+  ChainStep,
 } from "../../lib/api";
+import { getDirective } from "../../lib/api";
 
 interface DirectiveDetailProps {
   directive: DirectiveWithChains;
   onBack: () => void;
   onDelete?: (id: string) => void;
   onStart?: (id: string) => void;
+  onRefresh?: (updated: DirectiveWithChains) => void;
 }
 
 const statusColors: Record<DirectiveStatus, string> = {
@@ -21,31 +26,93 @@ const statusColors: Record<DirectiveStatus, string> = {
   failed: "text-red-400",
 };
 
-function ChainCard({ chain }: { chain: DirectiveChain }) {
+const stepStatusColors: Record<string, string> = {
+  pending: "text-[#888]",
+  running: "text-yellow-400",
+  passed: "text-green-400",
+  failed: "text-red-400",
+};
+
+const stepStatusIcons: Record<string, string> = {
+  pending: "\u25CB", // ○
+  running: "\u25D4", // ◔
+  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";
+
   return (
-    <div className="p-3 border border-dashed border-[rgba(117,170,252,0.25)] bg-[rgba(117,170,252,0.03)]">
-      <div className="flex items-center justify-between mb-1">
-        <span className="font-mono text-xs text-[#dbe7ff]">{chain.name}</span>
-        <span className="font-mono text-[10px] text-[#7788aa] uppercase">
-          gen {chain.generation} &middot; {chain.status}
-        </span>
+    <div className="flex items-start gap-2 py-1 px-2 hover:bg-[rgba(117,170,252,0.05)]">
+      <span className={`font-mono text-[11px] ${color} mt-px`}>{icon}</span>
+      <div className="flex-1 min-w-0">
+        <div className="flex items-center gap-2">
+          <span className="font-mono text-[11px] text-[#dbe7ff] truncate">
+            {step.name}
+          </span>
+          <span className={`font-mono text-[9px] uppercase ${color}`}>
+            {step.status}
+          </span>
+        </div>
+        {step.description && (
+          <p className="font-mono text-[10px] text-[#7788aa] truncate">
+            {step.description}
+          </p>
+        )}
       </div>
-      {chain.description && (
-        <p className="font-mono text-[11px] text-[#7788aa] mb-1">
-          {chain.description}
-        </p>
+      {step.contractId && (
+        <button
+          onClick={() => navigate(`/contracts/${step.contractId}`)}
+          className="font-mono text-[9px] text-[#75aafc] hover:text-white transition-colors shrink-0"
+          title="View contract"
+        >
+          contract &rarr;
+        </button>
       )}
-      <div className="flex gap-3 font-mono text-[10px] text-[#7788aa]">
-        <span>
-          {chain.completedSteps}/{chain.totalSteps} steps
-        </span>
-        {chain.failedSteps > 0 && (
-          <span className="text-red-400">{chain.failedSteps} failed</span>
-        )}
-        {chain.currentConfidence != null && (
-          <span>confidence: {(chain.currentConfidence * 100).toFixed(0)}%</span>
+    </div>
+  );
+}
+
+function ChainCard({ chainWithSteps }: { chainWithSteps: ChainWithSteps }) {
+  const chain = chainWithSteps;
+  const steps = chainWithSteps.steps || [];
+
+  return (
+    <div className="border border-dashed border-[rgba(117,170,252,0.25)] bg-[rgba(117,170,252,0.03)]">
+      <div className="p-3">
+        <div className="flex items-center justify-between mb-1">
+          <span className="font-mono text-xs text-[#dbe7ff]">{chain.name}</span>
+          <span className="font-mono text-[10px] text-[#7788aa] uppercase">
+            gen {chain.generation} &middot; {chain.status}
+          </span>
+        </div>
+        {chain.description && (
+          <p className="font-mono text-[11px] text-[#7788aa] mb-1">
+            {chain.description}
+          </p>
         )}
+        <div className="flex gap-3 font-mono text-[10px] text-[#7788aa]">
+          <span>
+            {chain.completedSteps}/{chain.totalSteps} steps
+          </span>
+          {chain.failedSteps > 0 && (
+            <span className="text-red-400">{chain.failedSteps} failed</span>
+          )}
+          {chain.currentConfidence != null && (
+            <span>confidence: {(chain.currentConfidence * 100).toFixed(0)}%</span>
+          )}
+        </div>
       </div>
+      {steps.length > 0 && (
+        <div className="border-t border-dashed border-[rgba(117,170,252,0.15)]">
+          {steps.map((step) => (
+            <StepRow key={step.id} step={step} />
+          ))}
+        </div>
+      )}
     </div>
   );
 }
@@ -81,7 +148,43 @@ export function DirectiveDetail({
   onBack,
   onDelete,
   onStart,
+  onRefresh,
 }: DirectiveDetailProps) {
+  const navigate = useNavigate();
+
+  // Auto-poll when directive is in an active state
+  const isLive =
+    directive.status === "planning" || directive.status === "active";
+  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
+
+  useEffect(() => {
+    if (!isLive) {
+      if (intervalRef.current) {
+        clearInterval(intervalRef.current);
+        intervalRef.current = null;
+      }
+      return;
+    }
+
+    intervalRef.current = setInterval(async () => {
+      try {
+        const updated = await getDirective(directive.id);
+        if (updated && onRefresh) {
+          onRefresh(updated);
+        }
+      } catch {
+        // Ignore poll errors
+      }
+    }, 5000);
+
+    return () => {
+      if (intervalRef.current) {
+        clearInterval(intervalRef.current);
+        intervalRef.current = null;
+      }
+    };
+  }, [isLive, directive.id, onRefresh]);
+
   return (
     <div className="panel h-full flex flex-col">
       {/* Header */}
@@ -100,6 +203,11 @@ export function DirectiveDetail({
           >
             {directive.status}
           </span>
+          {isLive && (
+            <span className="font-mono text-[9px] text-yellow-400/60 animate-pulse">
+              polling
+            </span>
+          )}
           <span className="font-mono text-[10px] text-[#7788aa]">
             v{directive.version}
           </span>
@@ -129,6 +237,28 @@ export function DirectiveDetail({
 
       {/* Content */}
       <div className="flex-1 overflow-y-auto p-4 space-y-4">
+        {/* Orchestrator contract link */}
+        {directive.orchestratorContractId && (
+          <div className="flex items-center gap-2 p-2 border border-dashed border-[rgba(117,170,252,0.2)] bg-[rgba(117,170,252,0.03)]">
+            <span className="font-mono text-[10px] text-[#7788aa] uppercase">
+              Planning Contract
+            </span>
+            <button
+              onClick={() =>
+                navigate(`/contracts/${directive.orchestratorContractId}`)
+              }
+              className="font-mono text-[11px] text-[#75aafc] hover:text-white transition-colors"
+            >
+              {directive.orchestratorContractId.slice(0, 8)}... &rarr;
+            </button>
+            {directive.status === "planning" && (
+              <span className="font-mono text-[9px] text-yellow-400 animate-pulse">
+                planning in progress
+              </span>
+            )}
+          </div>
+        )}
+
         {/* Goal */}
         <div>
           <h4 className="font-mono text-[10px] text-[#75aafc] uppercase tracking-wider mb-1">
@@ -196,12 +326,14 @@ export function DirectiveDetail({
           </h4>
           {directive.chains.length === 0 ? (
             <p className="font-mono text-xs text-[#7788aa]">
-              No chains yet. Chains are created during planning.
+              {directive.status === "planning"
+                ? "Planning in progress... chains will appear when the planner completes."
+                : "No chains yet. Chains are created during planning."}
             </p>
           ) : (
             <div className="space-y-2">
-              {directive.chains.map((chain) => (
-                <ChainCard key={chain.id} chain={chain} />
+              {directive.chains.map((cws) => (
+                <ChainCard key={cws.id} chainWithSteps={cws} />
               ))}
             </div>
           )}
-- 
cgit v1.2.3

