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 (
<div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
<Masthead showNav />
<main className="flex-1 flex items-center justify-center">
<p className="text-[#7788aa] font-mono text-sm">Loading...</p>
</main>
</div>
);
}
// Don't render if not authenticated (will redirect)
if (isAuthConfigured && !isAuthenticated) {
return null;
}
return <DirectivesPageContent />;
}
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<DirectiveWithProgress | null>(null);
const [directiveGraph, setDirectiveGraph] = useState<DirectiveGraphResponse | null>(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 (
<div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
<Masthead showNav />
<main className="flex-1 flex flex-col p-4 pt-2 gap-4 overflow-hidden">
{error && (
<div className="p-3 bg-red-400/10 border border-red-400/30 text-red-400 font-mono text-sm">
{error}
</div>
)}
{/* Create directive modal */}
{isCreating && (
<CreateDirectiveModal
onSubmit={handleCreateSubmit}
onCancel={handleCreateCancel}
/>
)}
<div className="flex-1 grid grid-cols-[350px_1fr] gap-4 min-h-0">
{/* Directive list */}
<DirectiveList
directives={directives}
loading={loading}
onSelect={handleSelect}
onCreate={handleCreate}
selectedId={id}
onArchive={handleArchive}
/>
{/* Directive detail or empty state */}
{directiveDetail ? (
<DirectiveDetail
directive={directiveDetail}
graph={directiveGraph}
loading={detailLoading}
onBack={handleBack}
onRefresh={handleRefresh}
onStart={handleStart}
onPause={handlePause}
onResume={handleResume}
onStop={handleStop}
/>
) : (
<div className="panel h-full flex items-center justify-center">
<div className="text-center">
<p className="font-mono text-sm text-[#555] mb-4">
Select a directive or create a new one
</p>
<button
onClick={handleCreate}
className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase"
>
+ New Directive
</button>
</div>
</div>
)}
</div>
</main>
</div>
);
}
// =============================================================================
// 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 (
<div className="panel h-full flex flex-col overflow-hidden">
<div className="flex items-center justify-between p-3 border-b border-[rgba(117,170,252,0.15)]">
<h2 className="font-mono text-sm text-[#75aafc] uppercase">Directives</h2>
<button
onClick={onCreate}
className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase"
>
+ New
</button>
</div>
{/* Filters */}
<div className="flex gap-1 p-2 border-b border-[rgba(117,170,252,0.1)]">
{(["all", "active", "completed", "failed"] as const).map((f) => (
<button
key={f}
onClick={() => setFilter(f)}
className={`px-2 py-1 font-mono text-[10px] uppercase ${
filter === f
? "text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3]"
: "text-[#556677] hover:text-[#9bc3ff]"
}`}
>
{f}
</button>
))}
</div>
{/* List */}
<div className="flex-1 overflow-y-auto">
{loading ? (
<div className="p-4 text-center">
<p className="font-mono text-xs text-[#556677]">Loading...</p>
</div>
) : filteredDirectives.length === 0 ? (
<div className="p-4 text-center">
<p className="font-mono text-xs text-[#556677]">No directives found</p>
</div>
) : (
filteredDirectives.map((d) => (
<DirectiveListItem
key={d.id}
directive={d}
selected={d.id === selectedId}
onClick={() => onSelect(d.id)}
onArchive={() => onArchive(d)}
/>
))
)}
</div>
</div>
);
}
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 (
<div
onClick={onClick}
className={`p-3 cursor-pointer border-b border-[rgba(117,170,252,0.1)] hover:bg-[rgba(117,170,252,0.05)] ${
selected ? "bg-[rgba(117,170,252,0.1)]" : ""
}`}
>
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="font-mono text-sm text-[#dbe7ff] truncate">
{directive.title || directive.goal.slice(0, 50)}
</div>
<div className="flex items-center gap-2 mt-1">
<span className={`font-mono text-[10px] uppercase ${statusColor}`}>
{directive.status}
</span>
<span className="font-mono text-[10px] text-[#556677]">
{directive.completedSteps}/{directive.totalSteps} steps
</span>
</div>
</div>
<div className="flex flex-col items-end gap-1">
{directive.currentConfidence !== null && (
<div className={`w-2 h-2 rounded-full ${confidenceColor}`} title={`Confidence: ${Math.round(directive.currentConfidence * 100)}%`} />
)}
<button
onClick={(e) => {
e.stopPropagation();
onArchive();
}}
className="font-mono text-[10px] text-[#556677] hover:text-red-400"
>
Archive
</button>
</div>
</div>
{/* Progress bar */}
{directive.totalSteps > 0 && (
<div className="mt-2 h-1 bg-[rgba(117,170,252,0.1)] overflow-hidden">
<div
className="h-full bg-[#75aafc] transition-all duration-300"
style={{ width: `${progress}%` }}
/>
</div>
)}
</div>
);
}
// =============================================================================
// 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 (
<div className="panel h-full flex items-center justify-center">
<p className="font-mono text-xs text-[#556677]">Loading...</p>
</div>
);
}
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 (
<div className="panel h-full flex flex-col overflow-hidden">
{/* Header */}
<div className="p-3 border-b border-[rgba(117,170,252,0.15)]">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<button
onClick={onBack}
className="font-mono text-xs text-[#556677] hover:text-[#9bc3ff]"
>
← Back
</button>
<h2 className="font-mono text-sm text-[#dbe7ff]">
{directive.title || directive.goal.slice(0, 50)}
</h2>
<span className={`px-2 py-0.5 font-mono text-[10px] uppercase border ${statusColor}`}>
{directive.status}
</span>
</div>
<div className="flex items-center gap-2">
{directive.status === "draft" && (
<button
onClick={onStart}
className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-green-700 border border-green-600 hover:bg-green-600 uppercase"
>
Start
</button>
)}
{directive.status === "active" && (
<button
onClick={onPause}
className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-yellow-700 border border-yellow-600 hover:bg-yellow-600 uppercase"
>
Pause
</button>
)}
{directive.status === "paused" && (
<button
onClick={onResume}
className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-green-700 border border-green-600 hover:bg-green-600 uppercase"
>
Resume
</button>
)}
{["active", "paused"].includes(directive.status) && (
<button
onClick={onStop}
className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-red-700 border border-red-600 hover:bg-red-600 uppercase"
>
Stop
</button>
)}
<button
onClick={onRefresh}
className="px-3 py-1 font-mono text-[10px] text-[#9bc3ff] hover:text-[#dbe7ff]"
>
Refresh
</button>
</div>
</div>
</div>
{/* Tabs */}
<div className="flex gap-1 p-2 border-b border-[rgba(117,170,252,0.1)]">
{(["overview", "chain", "events", "evaluations", "approvals", "verifiers"] as const).map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`px-3 py-1.5 font-mono text-[10px] uppercase ${
activeTab === tab
? "text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3]"
: "text-[#556677] hover:text-[#9bc3ff]"
}`}
>
{tab}
{tab === "approvals" && directive.pendingApprovals.length > 0 && (
<span className="ml-1 px-1 bg-yellow-500 text-black rounded text-[8px]">
{directive.pendingApprovals.length}
</span>
)}
</button>
))}
</div>
{/* Tab Content */}
<div className="flex-1 overflow-y-auto p-4">
{activeTab === "overview" && (
<OverviewTab directive={directive} />
)}
{activeTab === "chain" && (
<ChainTab directive={directive} graph={graph} />
)}
{activeTab === "events" && (
<EventsTab directive={directive} />
)}
{activeTab === "evaluations" && (
<EvaluationsTab directive={directive} />
)}
{activeTab === "approvals" && (
<ApprovalsTab directive={directive} onRefresh={onRefresh} />
)}
{activeTab === "verifiers" && (
<VerifiersTab directive={directive} />
)}
</div>
</div>
);
}
// =============================================================================
// Tab Components
// =============================================================================
function OverviewTab({ directive }: { directive: DirectiveWithProgress }) {
return (
<div className="space-y-6">
{/* Goal */}
<div>
<h3 className="font-mono text-xs text-[#75aafc] uppercase mb-2">Goal</h3>
<p className="font-mono text-sm text-[#dbe7ff] whitespace-pre-wrap">
{directive.goal}
</p>
</div>
{/* Progress */}
<div>
<h3 className="font-mono text-xs text-[#75aafc] uppercase mb-2">Progress</h3>
<div className="grid grid-cols-3 gap-4">
<div className="p-3 bg-[rgba(117,170,252,0.05)] border border-[rgba(117,170,252,0.15)]">
<div className="font-mono text-2xl text-[#dbe7ff]">
{directive.chain?.completedSteps || 0}
</div>
<div className="font-mono text-[10px] text-[#556677] uppercase">Completed Steps</div>
</div>
<div className="p-3 bg-[rgba(117,170,252,0.05)] border border-[rgba(117,170,252,0.15)]">
<div className="font-mono text-2xl text-[#dbe7ff]">
{directive.chain?.totalSteps || 0}
</div>
<div className="font-mono text-[10px] text-[#556677] uppercase">Total Steps</div>
</div>
<div className="p-3 bg-[rgba(117,170,252,0.05)] border border-[rgba(117,170,252,0.15)]">
<div className="font-mono text-2xl text-[#dbe7ff]">
{directive.chain?.currentConfidence != null
? `${Math.round((directive.chain?.currentConfidence ?? 0) * 100)}%`
: "-"}
</div>
<div className="font-mono text-[10px] text-[#556677] uppercase">Confidence</div>
</div>
</div>
</div>
{/* Configuration */}
<div>
<h3 className="font-mono text-xs text-[#75aafc] uppercase mb-2">Configuration</h3>
<div className="grid grid-cols-2 gap-2 text-sm">
<div className="flex justify-between">
<span className="font-mono text-[#556677]">Autonomy Level</span>
<span className="font-mono text-[#dbe7ff]">{directive.autonomyLevel}</span>
</div>
<div className="flex justify-between">
<span className="font-mono text-[#556677]">Max Rework Cycles</span>
<span className="font-mono text-[#dbe7ff]">{directive.maxReworkCycles}</span>
</div>
<div className="flex justify-between">
<span className="font-mono text-[#556677]">Green Threshold</span>
<span className="font-mono text-[#dbe7ff]">{directive.confidenceThresholdGreen}</span>
</div>
<div className="flex justify-between">
<span className="font-mono text-[#556677]">Yellow Threshold</span>
<span className="font-mono text-[#dbe7ff]">{directive.confidenceThresholdYellow}</span>
</div>
</div>
</div>
{/* Repository */}
{directive.repositoryUrl && (
<div>
<h3 className="font-mono text-xs text-[#75aafc] uppercase mb-2">Repository</h3>
<p className="font-mono text-sm text-[#9bc3ff]">{directive.repositoryUrl}</p>
</div>
)}
</div>
);
}
// Step status colors for both list and DAG views
const stepStatusStyles: Record<StepStatus, { border: string; bg: string; text: string }> = {
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<ConfidenceLevel, string> = {
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 (
<div
className={`rounded-lg border-2 bg-[#0a1628] overflow-hidden ${
isRunning ? "animate-pulse" : ""
}`}
style={{
width: NODE_WIDTH,
height: NODE_HEIGHT,
borderColor: styles.border,
borderStyle: data.status === "pending" ? "dashed" : "solid",
}}
>
<Handle
type="target"
position={Position.Top}
className="!bg-[#75aafc] !w-3 !h-3 !border-2 !border-[#0a1628]"
/>
{/* Status indicator bar */}
<div className="h-1.5" style={{ backgroundColor: styles.bg }} />
{/* Content */}
<div className="p-2">
<div className="flex items-center justify-between mb-1">
<span className="font-mono text-xs text-[#dbe7ff] truncate flex-1">{data.name}</span>
{data.confidenceScore !== null && data.confidenceLevel && (
<div
className="w-2 h-2 rounded-full flex-shrink-0 ml-1"
style={{ backgroundColor: confidenceColors[data.confidenceLevel] }}
title={`Confidence: ${Math.round(data.confidenceScore * 100)}%`}
/>
)}
</div>
<div className="flex items-center justify-between">
<span
className="font-mono text-[10px] uppercase px-1.5 py-0.5 rounded"
style={{
color: styles.text,
backgroundColor: `${styles.bg}20`,
}}
>
{data.status}
</span>
<span className="font-mono text-[10px] text-[#8b949e]">{data.stepType}</span>
</div>
</div>
<Handle
type="source"
position={Position.Bottom}
className="!bg-[#f59e0b] !w-3 !h-3 !border-2 !border-[#0a1628]"
/>
</div>
);
}
// 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 (
<div className="text-center py-8">
<p className="font-mono text-sm text-[#556677]">
No chain generated yet. Start the directive to generate a chain.
</p>
</div>
);
}
return (
<div className="space-y-4">
{/* Chain info header */}
<div className="flex items-center justify-between p-3 bg-[rgba(117,170,252,0.05)] border border-[rgba(117,170,252,0.15)]">
<div>
<div className="font-mono text-sm text-[#dbe7ff]">{directive.chain.name}</div>
<div className="font-mono text-[10px] text-[#556677]">Generation {directive.chain.generation}</div>
</div>
<div className="flex items-center gap-4">
<div className="font-mono text-xs text-[#556677]">
{directive.chain.completedSteps}/{directive.chain.totalSteps} steps
</div>
{/* View toggle */}
<div className="flex border border-[rgba(117,170,252,0.2)]">
<button
onClick={() => setViewMode("dag")}
className={`px-2 py-1 font-mono text-[10px] uppercase ${
viewMode === "dag"
? "bg-[rgba(117,170,252,0.2)] text-[#75aafc]"
: "text-[#556677] hover:text-[#75aafc]"
}`}
>
DAG
</button>
<button
onClick={() => setViewMode("list")}
className={`px-2 py-1 font-mono text-[10px] uppercase ${
viewMode === "list"
? "bg-[rgba(117,170,252,0.2)] text-[#75aafc]"
: "text-[#556677] hover:text-[#75aafc]"
}`}
>
List
</button>
</div>
</div>
</div>
{viewMode === "dag" ? (
/* DAG visualization */
<div className="h-[400px] border border-[rgba(117,170,252,0.1)] rounded bg-[#050d18]">
{directive.steps.length === 0 ? (
<div className="flex items-center justify-center h-full">
<p className="font-mono text-sm text-[#556677]">No steps in chain</p>
</div>
) : (
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
fitView
fitViewOptions={{ padding: 0.2 }}
minZoom={0.5}
maxZoom={1.5}
defaultEdgeOptions={{
type: "smoothstep",
style: { stroke: "#556677", strokeWidth: 2 },
}}
>
<Background variant={BackgroundVariant.Dots} gap={20} size={1} color="#1a2a3a" />
<Controls className="!bg-[#0a1628] !border-[rgba(117,170,252,0.2)]" />
</ReactFlow>
)}
</div>
) : (
/* List view */
<div className="space-y-2">
<h3 className="font-mono text-xs text-[#75aafc] uppercase">Steps</h3>
{directive.steps.length === 0 ? (
<p className="font-mono text-sm text-[#556677]">No steps in chain</p>
) : (
directive.steps.map((step) => {
const styles = stepStatusStyles[step.status] || stepStatusStyles.pending;
return (
<div
key={step.id}
className="p-3 bg-[rgba(117,170,252,0.02)] border border-[rgba(117,170,252,0.1)]"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="font-mono text-sm text-[#dbe7ff]">{step.name}</span>
<span
className="px-1.5 py-0.5 font-mono text-[10px] uppercase border"
style={{ color: styles.text, borderColor: `${styles.border}50` }}
>
{step.status}
</span>
{step.confidenceScore !== null && (
<span className="font-mono text-[10px] text-[#556677]">
({Math.round(step.confidenceScore * 100)}%)
</span>
)}
</div>
<div className="font-mono text-[10px] text-[#556677]">{step.stepType}</div>
</div>
{step.description && (
<p className="font-mono text-xs text-[#556677] mt-1">{step.description}</p>
)}
{step.dependsOn?.length > 0 && (
<div className="font-mono text-[10px] text-[#556677] mt-1">
Depends on: {step.dependsOn.join(", ")}
</div>
)}
</div>
);
})
)}
</div>
)}
</div>
);
}
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 (
<div className="space-y-4">
{/* Connection status */}
<div className="flex items-center justify-between text-[10px] font-mono">
<div className="flex items-center gap-2">
<span className={isConnected ? "text-green-400" : "text-[#556677]"}>
{isConnected ? "● Live" : "○ Connecting..."}
</span>
{sseError && <span className="text-red-400">{sseError}</span>}
</div>
<span className="text-[#556677]">{allEvents.length} events</span>
</div>
{/* Event list */}
{allEvents.length === 0 ? (
<div className="text-center py-8">
<p className="font-mono text-sm text-[#556677]">No events yet</p>
</div>
) : (
<div className="space-y-2">
{allEvents.map((event) => {
const severityColors: Record<string, string> = {
info: "text-[#75aafc]",
warning: "text-yellow-400",
error: "text-red-400",
critical: "text-red-600",
};
const severityColor = severityColors[event.severity] || "text-[#556677]";
return (
<div
key={event.id}
className="p-3 bg-[rgba(117,170,252,0.02)] border border-[rgba(117,170,252,0.1)]"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className={`font-mono text-xs ${severityColor}`}>{event.eventType}</span>
<span className="font-mono text-[10px] text-[#556677]">{event.actorType}</span>
</div>
<span className="font-mono text-[10px] text-[#556677]">
{new Date(event.createdAt).toLocaleString()}
</span>
</div>
{event.eventData != null && (
<pre className="font-mono text-[10px] text-[#556677] mt-1 overflow-x-auto">
{JSON.stringify(event.eventData, null, 2)}
</pre>
)}
</div>
);
})}
</div>
)}
</div>
);
}
function EvaluationsTab({ directive: _directive }: { directive: DirectiveWithProgress }) {
// TODO: Fetch evaluations separately
return (
<div className="text-center py-8">
<p className="font-mono text-sm text-[#556677]">
Evaluations will be shown here after steps are evaluated
</p>
</div>
);
}
function ApprovalsTab({ directive, onRefresh }: { directive: DirectiveWithProgress; onRefresh: () => void }) {
if (directive.pendingApprovals.length === 0) {
return (
<div className="text-center py-8">
<p className="font-mono text-sm text-[#556677]">No pending approvals</p>
</div>
);
}
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 (
<div className="space-y-3">
{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 (
<div
key={approval.id}
className="p-4 bg-[rgba(117,170,252,0.05)] border border-[rgba(117,170,252,0.2)]"
>
<div className="flex items-start justify-between">
<div>
<div className="flex items-center gap-2">
<span className="font-mono text-sm text-[#dbe7ff]">{approval.approvalType}</span>
<span className={`font-mono text-[10px] uppercase ${urgencyColor}`}>
{approval.urgency}
</span>
</div>
<p className="font-mono text-xs text-[#9bc3ff] mt-1">{approval.description}</p>
</div>
<div className="flex gap-2">
<button
onClick={() => handleApprove(approval.id)}
className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-green-700 border border-green-600 hover:bg-green-600 uppercase"
>
Approve
</button>
<button
onClick={() => handleDeny(approval.id)}
className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-red-700 border border-red-600 hover:bg-red-600 uppercase"
>
Deny
</button>
</div>
</div>
</div>
);
})}
</div>
);
}
function VerifiersTab({ directive: _directive }: { directive: DirectiveWithProgress }) {
// TODO: Fetch verifiers separately
return (
<div className="text-center py-8">
<p className="font-mono text-sm text-[#556677]">
Verifiers will be shown here. Use auto-detect to find available verifiers.
</p>
</div>
);
}
// =============================================================================
// 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<AutonomyLevel>("guardrails");
const [suggestions, setSuggestions] = useState<RepositoryHistoryEntry[]>([]);
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 (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<div className="w-full max-w-lg p-6 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] max-h-[90vh] overflow-y-auto">
<h3 className="font-mono text-sm text-[#75aafc] uppercase mb-4">
Create Directive
</h3>
<div className="space-y-4">
{/* Goal */}
<div>
<label className="block font-mono text-xs text-[#8b949e] uppercase mb-1">
Goal *
</label>
<textarea
value={goal}
onChange={(e) => setGoal(e.target.value)}
placeholder="Describe what you want to accomplish..."
rows={3}
className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc] resize-none"
autoFocus
/>
</div>
{/* Repository URL */}
<div>
<label className="block font-mono text-xs text-[#8b949e] uppercase mb-1">
Repository URL (optional)
</label>
<div className="relative">
<input
type="text"
value={repositoryUrl}
onChange={(e) => 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 && (
<div className="absolute top-full left-0 right-0 mt-1 border border-[rgba(117,170,252,0.2)] bg-[#0a1525] max-h-32 overflow-y-auto z-10">
{suggestions.map((s) => (
<button
key={s.id}
type="button"
onClick={() => {
setRepositoryUrl(s.repositoryUrl || "");
setShowSuggestions(false);
}}
className="w-full text-left px-3 py-2 font-mono text-xs hover:bg-[rgba(117,170,252,0.1)] border-b border-[rgba(117,170,252,0.1)] last:border-b-0"
>
<div className="text-[#9bc3ff] truncate">{s.name}</div>
<div className="text-[10px] text-[#556677] truncate">{s.repositoryUrl}</div>
</button>
))}
</div>
)}
</div>
</div>
{/* Autonomy Level */}
<div>
<label className="block font-mono text-xs text-[#8b949e] uppercase mb-2">
Autonomy Level
</label>
<div className="flex gap-2">
{(["full_auto", "guardrails", "manual"] as const).map((level) => (
<button
key={level}
type="button"
onClick={() => setAutonomyLevel(level)}
className={`flex-1 px-3 py-2 font-mono text-xs uppercase ${
autonomyLevel === level
? "text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3]"
: "text-[#556677] border border-[rgba(117,170,252,0.2)] hover:border-[#3f6fb3]"
}`}
>
{level.replace("_", " ")}
</button>
))}
</div>
<p className="font-mono text-[10px] text-[#556677] mt-1">
{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"}
</p>
</div>
<p className="font-mono text-xs text-[#8b949e]">
A directive is a top-level goal that generates a chain of steps. Each step spawns
contracts that are verified before progression.
</p>
{/* Actions */}
<div className="flex gap-2 justify-end pt-2">
<button
onClick={onCancel}
className="px-4 py-2 font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors"
>
Cancel
</button>
<button
onClick={handleSubmit}
disabled={!goal.trim()}
className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Create
</button>
</div>
</div>
</div>
</div>
);
}