From 8f757f561eeb397aaea70d7c10d41445cc5e50b5 Mon Sep 17 00:00:00 2001 From: soryu Date: Sat, 7 Feb 2026 16:58:38 +0000 Subject: Show directive init on frontend --- .../src/components/directives/DirectiveDetail.tsx | 180 ++++++++++++++++++--- makima/frontend/src/lib/api.ts | 25 ++- makima/frontend/src/routes/directives.tsx | 8 + makima/src/db/models.rs | 4 +- makima/src/server/handlers/directives.rs | 21 ++- 5 files changed, 209 insertions(+), 29 deletions(-) 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 = { @@ -21,31 +26,93 @@ const statusColors: Record = { failed: "text-red-400", }; -function ChainCard({ chain }: { chain: DirectiveChain }) { +const stepStatusColors: Record = { + pending: "text-[#888]", + running: "text-yellow-400", + passed: "text-green-400", + failed: "text-red-400", +}; + +const stepStatusIcons: Record = { + 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 ( -
-
- {chain.name} - - gen {chain.generation} · {chain.status} - +
+ {icon} +
+
+ + {step.name} + + + {step.status} + +
+ {step.description && ( +

+ {step.description} +

+ )}
- {chain.description && ( -

- {chain.description} -

+ {step.contractId && ( + )} -
- - {chain.completedSteps}/{chain.totalSteps} steps - - {chain.failedSteps > 0 && ( - {chain.failedSteps} failed - )} - {chain.currentConfidence != null && ( - confidence: {(chain.currentConfidence * 100).toFixed(0)}% +
+ ); +} + +function ChainCard({ chainWithSteps }: { chainWithSteps: ChainWithSteps }) { + const chain = chainWithSteps; + const steps = chainWithSteps.steps || []; + + return ( +
+
+
+ {chain.name} + + gen {chain.generation} · {chain.status} + +
+ {chain.description && ( +

+ {chain.description} +

)} +
+ + {chain.completedSteps}/{chain.totalSteps} steps + + {chain.failedSteps > 0 && ( + {chain.failedSteps} failed + )} + {chain.currentConfidence != null && ( + confidence: {(chain.currentConfidence * 100).toFixed(0)}% + )} +
+ {steps.length > 0 && ( +
+ {steps.map((step) => ( + + ))} +
+ )}
); } @@ -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 | 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 (
{/* Header */} @@ -100,6 +203,11 @@ export function DirectiveDetail({ > {directive.status} + {isLive && ( + + polling + + )} v{directive.version} @@ -129,6 +237,28 @@ export function DirectiveDetail({ {/* Content */}
+ {/* Orchestrator contract link */} + {directive.orchestratorContractId && ( +
+ + Planning Contract + + + {directive.status === "planning" && ( + + planning in progress + + )} +
+ )} + {/* Goal */}

@@ -196,12 +326,14 @@ export function DirectiveDetail({

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

- 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."}

) : (
- {directive.chains.map((chain) => ( - + {directive.chains.map((cws) => ( + ))}
)} diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index 2187c34..ccc7156 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -3075,8 +3075,31 @@ export interface DirectiveChain { updatedAt: string; } +export interface ChainStep { + id: string; + chainId: string; + name: string; + description: string | null; + stepType: string; + contractType: string; + initialPhase: string | null; + taskPlan: string | null; + dependsOn: string[] | null; + status: string; + contractId: string | null; + supervisorTaskId: string | null; + orderIndex: number; + startedAt: string | null; + completedAt: string | null; + createdAt: string; +} + +export interface ChainWithSteps extends DirectiveChain { + steps: ChainStep[]; +} + export interface DirectiveWithChains extends Directive { - chains: DirectiveChain[]; + chains: ChainWithSteps[]; } export interface DirectiveListResponse { diff --git a/makima/frontend/src/routes/directives.tsx b/makima/frontend/src/routes/directives.tsx index 8b82f99..9437389 100644 --- a/makima/frontend/src/routes/directives.tsx +++ b/makima/frontend/src/routes/directives.tsx @@ -129,6 +129,13 @@ function DirectivesContent() { [startDirective, fetchDirective] ); + const handleRefresh = useCallback( + (updated: DirectiveWithChains) => { + setSelectedDirective(updated); + }, + [] + ); + // Detail view if (id) { if (detailLoading) { @@ -152,6 +159,7 @@ function DirectivesContent() { onBack={handleBack} onDelete={handleDelete} onStart={handleStart} + onRefresh={handleRefresh} /> ); diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index ec4ee15..bc90942 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -2849,13 +2849,13 @@ pub struct UpdateDirectiveRequest { pub version: Option, } -/// Directive with its chains for detail view. +/// Directive with its chains and steps for detail view. #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct DirectiveWithChains { #[serde(flatten)] pub directive: Directive, - pub chains: Vec, + pub chains: Vec, } /// Full row from directive_chains table. diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs index 560151b..a877c6b 100644 --- a/makima/src/server/handlers/directives.rs +++ b/makima/src/server/handlers/directives.rs @@ -9,7 +9,7 @@ use axum::{ use uuid::Uuid; use crate::db::models::{ - ChainWithSteps, CreateDirectiveRequest, Directive, DirectiveChain, + ChainStep, ChainWithSteps, CreateDirectiveRequest, Directive, DirectiveChain, DirectiveListResponse, DirectiveWithChains, UpdateDirectiveRequest, }; use crate::db::repository::{self, RepositoryError}; @@ -122,7 +122,24 @@ pub async fn get_directive( } }; - Json(DirectiveWithChains { directive, chains }).into_response() + // Build chains with steps + let mut chains_with_steps = Vec::new(); + for chain in chains { + let steps = match repository::list_steps_for_chain(pool, chain.id).await { + Ok(s) => s, + Err(e) => { + tracing::warn!("Failed to get steps for chain {}: {}", chain.id, e); + Vec::new() + } + }; + chains_with_steps.push(ChainWithSteps { chain, steps }); + } + + Json(DirectiveWithChains { + directive, + chains: chains_with_steps, + }) + .into_response() } /// Create a new directive. -- cgit v1.2.3