import { useState, useCallback, useEffect, useMemo } from "react";
import { useParams, useNavigate } from "react-router";
import {
ReactFlow,
Edge,
Controls,
Background,
Handle,
Position,
BackgroundVariant,
MarkerType,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";
import { Masthead } from "../components/Masthead";
import { useDirectives, useDirectiveEventSubscription } from "../hooks/useDirectives";
import { useAuth } from "../contexts/AuthContext";
import type {
DirectiveSummary,
DirectiveWithProgress,
DirectiveGraphResponse,
DirectiveGraphNode,
CreateDirectiveRequest,
RepositoryHistoryEntry,
AutonomyLevel,
StepStatus,
ConfidenceLevel,
} from "../lib/api";
import { getRepositorySuggestions } from "../lib/api";
export default function DirectivesPage() {
const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth();
const navigate = useNavigate();
// Redirect to login if not authenticated (when auth is configured)
useEffect(() => {
if (!authLoading && isAuthConfigured && !isAuthenticated) {
navigate("/login");
}
}, [authLoading, isAuthConfigured, isAuthenticated, navigate]);
// Show loading while checking auth
if (authLoading) {
return (
);
}
// Don't render if not authenticated (will redirect)
if (isAuthConfigured && !isAuthenticated) {
return null;
}
return ;
}
function DirectivesPageContent() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const {
directives,
loading,
error,
createNewDirective,
archiveExistingDirective,
getDirectiveById,
getGraph,
start,
pause,
resume,
stop,
} = useDirectives();
const [directiveDetail, setDirectiveDetail] = useState(null);
const [directiveGraph, setDirectiveGraph] = useState(null);
const [detailLoading, setDetailLoading] = useState(false);
const [isCreating, setIsCreating] = useState(false);
// Load directive detail when ID changes
useEffect(() => {
if (id) {
setDetailLoading(true);
Promise.all([getDirectiveById(id), getGraph(id).catch(() => null)]).then(([directive, graph]) => {
setDirectiveDetail(directive);
setDirectiveGraph(graph);
setDetailLoading(false);
});
} else {
setDirectiveDetail(null);
setDirectiveGraph(null);
}
}, [id, getDirectiveById, getGraph]);
const handleSelect = useCallback(
(directiveId: string) => {
navigate(`/directives/${directiveId}`);
},
[navigate]
);
const handleBack = useCallback(() => {
navigate("/directives");
}, [navigate]);
const handleCreate = useCallback(() => {
setIsCreating(true);
}, []);
const handleCreateSubmit = useCallback(
async (goal: string, repositoryUrl: string | undefined, autonomyLevel: AutonomyLevel) => {
const data: CreateDirectiveRequest = {
goal: goal.trim(),
repositoryUrl: repositoryUrl?.trim() || undefined,
autonomyLevel,
};
try {
const result = await createNewDirective(data);
if (result) {
setIsCreating(false);
navigate(`/directives/${result.id}`);
}
} catch (err) {
console.error("Failed to create directive:", err);
}
},
[createNewDirective, navigate]
);
const handleCreateCancel = useCallback(() => {
setIsCreating(false);
}, []);
const handleArchive = useCallback(
async (directive: DirectiveSummary) => {
if (confirm(`Are you sure you want to archive this directive?`)) {
const success = await archiveExistingDirective(directive.id);
if (success && directive.id === id) {
navigate("/directives");
}
}
},
[archiveExistingDirective, id, navigate]
);
const handleRefresh = useCallback(async () => {
if (id) {
const [directive, graph] = await Promise.all([
getDirectiveById(id),
getGraph(id).catch(() => null),
]);
setDirectiveDetail(directive);
setDirectiveGraph(graph);
}
}, [id, getDirectiveById, getGraph]);
const handleStart = useCallback(async () => {
if (id) {
await start(id);
handleRefresh();
}
}, [id, start, handleRefresh]);
const handlePause = useCallback(async () => {
if (id) {
await pause(id);
handleRefresh();
}
}, [id, pause, handleRefresh]);
const handleResume = useCallback(async () => {
if (id) {
await resume(id);
handleRefresh();
}
}, [id, resume, handleRefresh]);
const handleStop = useCallback(async () => {
if (id) {
await stop(id);
handleRefresh();
}
}, [id, stop, handleRefresh]);
return (
{error && (
{error}
)}
{/* Create directive modal */}
{isCreating && (
)}
{/* Directive list */}
{/* Directive detail or empty state */}
{directiveDetail ? (
) : (
Select a directive or create a new one
)}
);
}
// =============================================================================
// Directive List Component
// =============================================================================
interface DirectiveListProps {
directives: DirectiveSummary[];
loading: boolean;
onSelect: (id: string) => void;
onCreate: () => void;
selectedId?: string;
onArchive: (directive: DirectiveSummary) => void;
}
function DirectiveList({
directives,
loading,
onSelect,
onCreate,
selectedId,
onArchive,
}: DirectiveListProps) {
const [filter, setFilter] = useState<"all" | "active" | "completed" | "failed">("all");
const filteredDirectives = directives.filter((d) => {
if (filter === "all") return true;
if (filter === "active") return ["draft", "planning", "active", "paused"].includes(d.status);
if (filter === "completed") return d.status === "completed";
if (filter === "failed") return d.status === "failed";
return true;
});
return (
Directives
{/* Filters */}
{(["all", "active", "completed", "failed"] as const).map((f) => (
))}
{/* List */}
{loading ? (
) : filteredDirectives.length === 0 ? (
) : (
filteredDirectives.map((d) => (
onSelect(d.id)}
onArchive={() => onArchive(d)}
/>
))
)}
);
}
interface DirectiveListItemProps {
directive: DirectiveSummary;
selected: boolean;
onClick: () => void;
onArchive: () => void;
}
function DirectiveListItem({ directive, selected, onClick, onArchive }: DirectiveListItemProps) {
const progress = directive.totalSteps > 0
? Math.round((directive.completedSteps / directive.totalSteps) * 100)
: 0;
const statusColor = {
draft: "text-[#556677]",
planning: "text-yellow-400",
active: "text-green-400",
paused: "text-yellow-400",
completed: "text-[#75aafc]",
archived: "text-[#556677]",
failed: "text-red-400",
}[directive.status] || "text-[#556677]";
const confidenceColor = {
green: "bg-green-500",
yellow: "bg-yellow-500",
red: "bg-red-500",
}[directive.currentConfidence !== null && directive.currentConfidence >= 0.8
? "green"
: directive.currentConfidence !== null && directive.currentConfidence >= 0.5
? "yellow"
: "red"] || "bg-[#556677]";
return (
{directive.title || directive.goal.slice(0, 50)}
{directive.status}
{directive.completedSteps}/{directive.totalSteps} steps
{directive.currentConfidence !== null && (
)}
{/* Progress bar */}
{directive.totalSteps > 0 && (
)}
);
}
// =============================================================================
// Directive Detail Component
// =============================================================================
interface DirectiveDetailProps {
directive: DirectiveWithProgress;
graph: DirectiveGraphResponse | null;
loading: boolean;
onBack: () => void;
onRefresh: () => void;
onStart: () => void;
onPause: () => void;
onResume: () => void;
onStop: () => void;
}
function DirectiveDetail({
directive,
graph,
loading,
onBack,
onRefresh,
onStart,
onPause,
onResume,
onStop,
}: DirectiveDetailProps) {
const [activeTab, setActiveTab] = useState<"overview" | "chain" | "events" | "evaluations" | "approvals" | "verifiers">("overview");
if (loading) {
return (
);
}
const statusColor = {
draft: "text-[#556677] bg-[#556677]/10 border-[#556677]/30",
planning: "text-yellow-400 bg-yellow-400/10 border-yellow-400/30",
active: "text-green-400 bg-green-400/10 border-green-400/30",
paused: "text-yellow-400 bg-yellow-400/10 border-yellow-400/30",
completed: "text-[#75aafc] bg-[#75aafc]/10 border-[#75aafc]/30",
archived: "text-[#556677] bg-[#556677]/10 border-[#556677]/30",
failed: "text-red-400 bg-red-400/10 border-red-400/30",
}[directive.status] || "text-[#556677] bg-[#556677]/10 border-[#556677]/30";
return (
{/* Header */}
{directive.title || directive.goal.slice(0, 50)}
{directive.status}
{directive.status === "draft" && (
)}
{directive.status === "active" && (
)}
{directive.status === "paused" && (
)}
{["active", "paused"].includes(directive.status) && (
)}
{/* Tabs */}
{(["overview", "chain", "events", "evaluations", "approvals", "verifiers"] as const).map((tab) => (
))}
{/* Tab Content */}
{activeTab === "overview" && (
)}
{activeTab === "chain" && (
)}
{activeTab === "events" && (
)}
{activeTab === "evaluations" && (
)}
{activeTab === "approvals" && (
)}
{activeTab === "verifiers" && (
)}
);
}
// =============================================================================
// Tab Components
// =============================================================================
function OverviewTab({ directive }: { directive: DirectiveWithProgress }) {
return (
{/* Goal */}
{/* Progress */}
Progress
{directive.chain?.completedSteps || 0}
Completed Steps
{directive.chain?.totalSteps || 0}
Total Steps
{directive.chain?.currentConfidence != null
? `${Math.round((directive.chain?.currentConfidence ?? 0) * 100)}%`
: "-"}
Confidence
{/* Configuration */}
Configuration
Autonomy Level
{directive.autonomyLevel}
Max Rework Cycles
{directive.maxReworkCycles}
Green Threshold
{directive.confidenceThresholdGreen}
Yellow Threshold
{directive.confidenceThresholdYellow}
{/* Repository */}
{directive.repositoryUrl && (
Repository
{directive.repositoryUrl}
)}
);
}
// Step status colors for both list and DAG views
const stepStatusStyles: Record = {
pending: { border: "#556677", bg: "#556677", text: "#556677" },
ready: { border: "#3b82f6", bg: "#3b82f6", text: "#3b82f6" },
running: { border: "#22c55e", bg: "#22c55e", text: "#22c55e" },
evaluating: { border: "#eab308", bg: "#eab308", text: "#eab308" },
passed: { border: "#75aafc", bg: "#75aafc", text: "#75aafc" },
failed: { border: "#ef4444", bg: "#ef4444", text: "#ef4444" },
rework: { border: "#f97316", bg: "#f97316", text: "#f97316" },
skipped: { border: "#556677", bg: "#556677", text: "#556677" },
blocked: { border: "#ef4444", bg: "#ef4444", text: "#ef4444" },
};
// Confidence level colors
const confidenceColors: Record = {
green: "#22c55e",
yellow: "#eab308",
red: "#ef4444",
};
// Node dimensions
const NODE_WIDTH = 180;
const NODE_HEIGHT = 70;
// Custom node component for steps
function StepNodeComponent({ data }: { data: DirectiveGraphNode & { selected?: boolean } }) {
const styles = stepStatusStyles[data.status] || stepStatusStyles.pending;
const isRunning = data.status === "running" || data.status === "evaluating";
return (
{/* Status indicator bar */}
{/* Content */}
{data.name}
{data.confidenceScore !== null && data.confidenceLevel && (
)}
{data.status}
{data.stepType}
);
}
// Node types for React Flow
const nodeTypes = {
step: StepNodeComponent,
};
function ChainTab({ directive, graph }: { directive: DirectiveWithProgress; graph: DirectiveGraphResponse | null }) {
const [viewMode, setViewMode] = useState<"dag" | "list">("dag");
// Convert graph to React Flow nodes and edges
const { nodes, edges } = useMemo(() => {
if (!graph || !graph.nodes.length) {
// Fallback: generate positions from directive.steps
const stepNodes = directive.steps.map((step, index) => ({
id: step.id,
type: "step" as const,
position: {
x: (index % 3) * 220 + 50,
y: Math.floor(index / 3) * 120 + 50,
},
data: {
id: step.id,
name: step.name,
stepType: step.stepType,
status: step.status,
confidenceScore: step.confidenceScore,
confidenceLevel: step.confidenceLevel,
contractId: step.contractId,
editorX: null,
editorY: null,
},
}));
// Build edges from dependencies
const stepEdges: Edge[] = [];
directive.steps.forEach((step) => {
step.dependsOn.forEach((depName) => {
const depStep = directive.steps.find((s) => s.name === depName);
if (depStep) {
stepEdges.push({
id: `${depStep.id}-${step.id}`,
source: depStep.id,
target: step.id,
type: "smoothstep",
markerEnd: { type: MarkerType.ArrowClosed, color: "#556677" },
style: { stroke: "#556677", strokeWidth: 2 },
});
}
});
});
return { nodes: stepNodes, edges: stepEdges };
}
// Use graph data
const graphNodes = graph.nodes.map((node) => ({
id: node.id,
type: "step" as const,
position: {
x: node.editorX ?? 50,
y: node.editorY ?? 50,
},
data: { ...node },
}));
const graphEdges: Edge[] = graph.edges.map((edge) => ({
id: `${edge.source}-${edge.target}`,
source: edge.source,
target: edge.target,
type: "smoothstep",
markerEnd: { type: MarkerType.ArrowClosed, color: "#556677" },
style: { stroke: "#556677", strokeWidth: 2 },
}));
return { nodes: graphNodes, edges: graphEdges };
}, [graph, directive.steps]);
if (!directive.chain) {
return (
No chain generated yet. Start the directive to generate a chain.
);
}
return (
{/* Chain info header */}
{directive.chain.name}
Generation {directive.chain.generation}
{directive.chain.completedSteps}/{directive.chain.totalSteps} steps
{/* View toggle */}
{viewMode === "dag" ? (
/* DAG visualization */
{directive.steps.length === 0 ? (
) : (
)}
) : (
/* List view */
Steps
{directive.steps.length === 0 ? (
No steps in chain
) : (
directive.steps.map((step) => {
const styles = stepStatusStyles[step.status] || stepStatusStyles.pending;
return (
{step.name}
{step.status}
{step.confidenceScore !== null && (
({Math.round(step.confidenceScore * 100)}%)
)}
{step.stepType}
{step.description && (
{step.description}
)}
{step.dependsOn.length > 0 && (
Depends on: {step.dependsOn.join(", ")}
)}
);
})
)}
)}
);
}
function EventsTab({ directive }: { directive: DirectiveWithProgress }) {
// Subscribe to real-time events via SSE
const { events: streamEvents, isConnected, error: sseError } = useDirectiveEventSubscription(directive.id);
// Combine initial events with streamed events (avoiding duplicates)
const allEvents = useMemo(() => {
const eventMap = new Map();
// Add initial events first
directive.recentEvents.forEach((e) => eventMap.set(e.id, e));
// Add streamed events (will override any duplicates)
streamEvents.forEach((e) => eventMap.set(e.id, e));
// Sort by created_at descending (most recent first)
return Array.from(eventMap.values()).sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
}, [directive.recentEvents, streamEvents]);
return (
{/* Connection status */}
{isConnected ? "● Live" : "○ Connecting..."}
{sseError && {sseError}}
{allEvents.length} events
{/* Event list */}
{allEvents.length === 0 ? (
) : (
{allEvents.map((event) => {
const severityColors: Record
= {
info: "text-[#75aafc]",
warning: "text-yellow-400",
error: "text-red-400",
critical: "text-red-600",
};
const severityColor = severityColors[event.severity] || "text-[#556677]";
return (
{event.eventType}
{event.actorType}
{new Date(event.createdAt).toLocaleString()}
{event.eventData != null && (
{JSON.stringify(event.eventData, null, 2)}
)}
);
})}
)}
);
}
function EvaluationsTab({ directive: _directive }: { directive: DirectiveWithProgress }) {
// TODO: Fetch evaluations separately
return (
Evaluations will be shown here after steps are evaluated
);
}
function ApprovalsTab({ directive, onRefresh }: { directive: DirectiveWithProgress; onRefresh: () => void }) {
if (directive.pendingApprovals.length === 0) {
return (
);
}
const handleApprove = async (approvalId: string) => {
try {
const { approveDirectiveRequest } = await import("../lib/api");
await approveDirectiveRequest(directive.id, approvalId);
onRefresh();
} catch (err) {
console.error("Failed to approve:", err);
}
};
const handleDeny = async (approvalId: string) => {
try {
const { denyDirectiveRequest } = await import("../lib/api");
await denyDirectiveRequest(directive.id, approvalId);
onRefresh();
} catch (err) {
console.error("Failed to deny:", err);
}
};
return (
{directive.pendingApprovals.map((approval) => {
const urgencyColor = {
low: "text-[#556677]",
normal: "text-[#75aafc]",
high: "text-yellow-400",
critical: "text-red-400",
}[approval.urgency] || "text-[#556677]";
return (
{approval.approvalType}
{approval.urgency}
{approval.description}
);
})}
);
}
function VerifiersTab({ directive: _directive }: { directive: DirectiveWithProgress }) {
// TODO: Fetch verifiers separately
return (
Verifiers will be shown here. Use auto-detect to find available verifiers.
);
}
// =============================================================================
// Create Directive Modal
// =============================================================================
interface CreateDirectiveModalProps {
onSubmit: (goal: string, repositoryUrl: string | undefined, autonomyLevel: AutonomyLevel) => void;
onCancel: () => void;
}
function CreateDirectiveModal({ onSubmit, onCancel }: CreateDirectiveModalProps) {
const [goal, setGoal] = useState("");
const [repositoryUrl, setRepositoryUrl] = useState("");
const [autonomyLevel, setAutonomyLevel] = useState("guardrails");
const [suggestions, setSuggestions] = useState([]);
const [showSuggestions, setShowSuggestions] = useState(false);
// Load suggestions
useEffect(() => {
getRepositorySuggestions("remote", undefined, 5)
.then((res) => {
setSuggestions(res.entries);
})
.catch(() => {
setSuggestions([]);
});
}, []);
const handleSubmit = () => {
if (goal.trim()) {
onSubmit(goal.trim(), repositoryUrl.trim() || undefined, autonomyLevel);
}
};
return (
Create Directive
{/* Goal */}
{/* Repository URL */}
setRepositoryUrl(e.target.value)}
onFocus={() => suggestions.length > 0 && setShowSuggestions(true)}
onBlur={() => setTimeout(() => setShowSuggestions(false), 200)}
placeholder="https://github.com/owner/repo"
className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]"
/>
{showSuggestions && suggestions.length > 0 && (
{suggestions.map((s) => (
))}
)}
{/* Autonomy Level */}
{(["full_auto", "guardrails", "manual"] as const).map((level) => (
))}
{autonomyLevel === "full_auto" && "Automatic progression without approval gates"}
{autonomyLevel === "guardrails" && "Request approval for yellow/red confidence scores"}
{autonomyLevel === "manual" && "Request approval for all step completions"}
A directive is a top-level goal that generates a chain of steps. Each step spawns
contracts that are verified before progression.
{/* Actions */}
);
}