summaryrefslogtreecommitdiff
path: root/makima
diff options
context:
space:
mode:
Diffstat (limited to 'makima')
-rw-r--r--makima/frontend/src/components/directives/DirectiveDetail.tsx180
-rw-r--r--makima/frontend/src/lib/api.ts25
-rw-r--r--makima/frontend/src/routes/directives.tsx8
-rw-r--r--makima/src/db/models.rs4
-rw-r--r--makima/src/server/handlers/directives.rs21
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<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>
)}
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}
/>
</main>
);
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<i32>,
}
-/// 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<DirectiveChain>,
+ pub chains: Vec<ChainWithSteps>,
}
/// 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.