summaryrefslogtreecommitdiff
path: root/makima/frontend/src
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src')
-rw-r--r--makima/frontend/src/components/NavStrip.tsx1
-rw-r--r--makima/frontend/src/hooks/useDirectives.ts298
-rw-r--r--makima/frontend/src/lib/api.ts758
-rw-r--r--makima/frontend/src/main.tsx17
-rw-r--r--makima/frontend/src/routes/directives.tsx1254
5 files changed, 2328 insertions, 0 deletions
diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx
index 4f6cf32..ece07e4 100644
--- a/makima/frontend/src/components/NavStrip.tsx
+++ b/makima/frontend/src/components/NavStrip.tsx
@@ -10,6 +10,7 @@ interface NavLink {
const NAV_LINKS: NavLink[] = [
{ label: "Listen", href: "/listen" },
+ { label: "Directives", href: "/directives", requiresAuth: true },
{ label: "Chains", href: "/chains", requiresAuth: true },
{ label: "Contracts", href: "/contracts", requiresAuth: true },
{ label: "Board", href: "/workflow", requiresAuth: true },
diff --git a/makima/frontend/src/hooks/useDirectives.ts b/makima/frontend/src/hooks/useDirectives.ts
new file mode 100644
index 0000000..6e1654f
--- /dev/null
+++ b/makima/frontend/src/hooks/useDirectives.ts
@@ -0,0 +1,298 @@
+import { useState, useCallback, useEffect, useRef } from "react";
+import {
+ listDirectives,
+ getDirective,
+ createDirective,
+ updateDirective,
+ archiveDirective,
+ startDirective,
+ pauseDirective,
+ resumeDirective,
+ stopDirective,
+ getDirectiveGraph,
+ subscribeToDirectiveEvents,
+ type DirectiveSummary,
+ type DirectiveWithProgress,
+ type DirectiveGraphResponse,
+ type DirectiveStatus,
+ type DirectiveEvent,
+ type CreateDirectiveRequest,
+ type UpdateDirectiveRequest,
+ type StartDirectiveResponse,
+} from "../lib/api";
+
+interface UseDirectivesResult {
+ directives: DirectiveSummary[];
+ loading: boolean;
+ error: string | null;
+ refresh: () => Promise<void>;
+ createNewDirective: (req: CreateDirectiveRequest) => Promise<DirectiveWithProgress | null>;
+ updateExistingDirective: (
+ directiveId: string,
+ req: UpdateDirectiveRequest
+ ) => Promise<DirectiveWithProgress | null>;
+ archiveExistingDirective: (directiveId: string) => Promise<boolean>;
+ getDirectiveById: (directiveId: string) => Promise<DirectiveWithProgress | null>;
+ getGraph: (directiveId: string) => Promise<DirectiveGraphResponse | null>;
+ start: (directiveId: string) => Promise<StartDirectiveResponse | null>;
+ pause: (directiveId: string) => Promise<boolean>;
+ resume: (directiveId: string) => Promise<boolean>;
+ stop: (directiveId: string) => Promise<boolean>;
+}
+
+export function useDirectives(statusFilter?: DirectiveStatus): UseDirectivesResult {
+ const [directives, setDirectives] = useState<DirectiveSummary[]>([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState<string | null>(null);
+
+ const fetchDirectives = useCallback(async () => {
+ setLoading(true);
+ setError(null);
+ try {
+ const response = await listDirectives(statusFilter);
+ setDirectives(response.directives);
+ } catch (err) {
+ console.error("Failed to fetch directives:", err);
+ setError(err instanceof Error ? err.message : "Failed to fetch directives");
+ } finally {
+ setLoading(false);
+ }
+ }, [statusFilter]);
+
+ useEffect(() => {
+ fetchDirectives();
+ }, [fetchDirectives]);
+
+ const createNewDirective = useCallback(
+ async (req: CreateDirectiveRequest): Promise<DirectiveWithProgress | null> => {
+ try {
+ const directive = await createDirective(req);
+ // Refresh the list
+ await fetchDirectives();
+ // Return the full directive with progress
+ return await getDirective(directive.id);
+ } catch (err) {
+ console.error("Failed to create directive:", err);
+ setError(err instanceof Error ? err.message : "Failed to create directive");
+ return null;
+ }
+ },
+ [fetchDirectives]
+ );
+
+ const updateExistingDirective = useCallback(
+ async (
+ directiveId: string,
+ req: UpdateDirectiveRequest
+ ): Promise<DirectiveWithProgress | null> => {
+ try {
+ await updateDirective(directiveId, req);
+ // Refresh the list
+ await fetchDirectives();
+ // Return the updated directive
+ return await getDirective(directiveId);
+ } catch (err) {
+ console.error("Failed to update directive:", err);
+ setError(err instanceof Error ? err.message : "Failed to update directive");
+ return null;
+ }
+ },
+ [fetchDirectives]
+ );
+
+ const archiveExistingDirective = useCallback(
+ async (directiveId: string): Promise<boolean> => {
+ try {
+ await archiveDirective(directiveId);
+ // Refresh the list
+ await fetchDirectives();
+ return true;
+ } catch (err) {
+ console.error("Failed to archive directive:", err);
+ setError(err instanceof Error ? err.message : "Failed to archive directive");
+ return false;
+ }
+ },
+ [fetchDirectives]
+ );
+
+ const getDirectiveById = useCallback(
+ async (directiveId: string): Promise<DirectiveWithProgress | null> => {
+ try {
+ return await getDirective(directiveId);
+ } catch (err) {
+ console.error("Failed to get directive:", err);
+ setError(err instanceof Error ? err.message : "Failed to get directive");
+ return null;
+ }
+ },
+ []
+ );
+
+ const getGraph = useCallback(
+ async (directiveId: string): Promise<DirectiveGraphResponse | null> => {
+ try {
+ return await getDirectiveGraph(directiveId);
+ } catch (err) {
+ console.error("Failed to get directive graph:", err);
+ setError(err instanceof Error ? err.message : "Failed to get directive graph");
+ return null;
+ }
+ },
+ []
+ );
+
+ const start = useCallback(
+ async (directiveId: string): Promise<StartDirectiveResponse | null> => {
+ try {
+ const response = await startDirective(directiveId);
+ await fetchDirectives();
+ return response;
+ } catch (err) {
+ console.error("Failed to start directive:", err);
+ setError(err instanceof Error ? err.message : "Failed to start directive");
+ return null;
+ }
+ },
+ [fetchDirectives]
+ );
+
+ const pause = useCallback(
+ async (directiveId: string): Promise<boolean> => {
+ try {
+ await pauseDirective(directiveId);
+ await fetchDirectives();
+ return true;
+ } catch (err) {
+ console.error("Failed to pause directive:", err);
+ setError(err instanceof Error ? err.message : "Failed to pause directive");
+ return false;
+ }
+ },
+ [fetchDirectives]
+ );
+
+ const resume = useCallback(
+ async (directiveId: string): Promise<boolean> => {
+ try {
+ await resumeDirective(directiveId);
+ await fetchDirectives();
+ return true;
+ } catch (err) {
+ console.error("Failed to resume directive:", err);
+ setError(err instanceof Error ? err.message : "Failed to resume directive");
+ return false;
+ }
+ },
+ [fetchDirectives]
+ );
+
+ const stop = useCallback(
+ async (directiveId: string): Promise<boolean> => {
+ try {
+ await stopDirective(directiveId);
+ await fetchDirectives();
+ return true;
+ } catch (err) {
+ console.error("Failed to stop directive:", err);
+ setError(err instanceof Error ? err.message : "Failed to stop directive");
+ return false;
+ }
+ },
+ [fetchDirectives]
+ );
+
+ return {
+ directives,
+ loading,
+ error,
+ refresh: fetchDirectives,
+ createNewDirective,
+ updateExistingDirective,
+ archiveExistingDirective,
+ getDirectiveById,
+ getGraph,
+ start,
+ pause,
+ resume,
+ stop,
+ };
+}
+
+/** Hook for subscribing to real-time directive events via SSE */
+export function useDirectiveEventSubscription(
+ directiveId: string | null,
+ onEvent?: (event: DirectiveEvent) => void
+): {
+ events: DirectiveEvent[];
+ isConnected: boolean;
+ error: string | null;
+} {
+ const [events, setEvents] = useState<DirectiveEvent[]>([]);
+ const [isConnected, setIsConnected] = useState(false);
+ const [error, setError] = useState<string | null>(null);
+ const cleanupRef = useRef<(() => void) | null>(null);
+
+ useEffect(() => {
+ // Clean up any existing subscription
+ if (cleanupRef.current) {
+ cleanupRef.current();
+ cleanupRef.current = null;
+ }
+
+ if (!directiveId) {
+ setIsConnected(false);
+ setEvents([]);
+ return;
+ }
+
+ // Subscribe to events
+ let mounted = true;
+
+ const setupSubscription = async () => {
+ try {
+ const cleanup = await subscribeToDirectiveEvents(
+ directiveId,
+ (event) => {
+ if (mounted) {
+ setEvents((prev) => [...prev, event]);
+ onEvent?.(event);
+ }
+ },
+ (err) => {
+ if (mounted) {
+ setError(err.message);
+ setIsConnected(false);
+ }
+ }
+ );
+
+ if (mounted) {
+ cleanupRef.current = cleanup;
+ setIsConnected(true);
+ setError(null);
+ } else {
+ // Component unmounted during setup, clean up immediately
+ cleanup();
+ }
+ } catch (err) {
+ if (mounted) {
+ setError(err instanceof Error ? err.message : "Failed to subscribe to events");
+ setIsConnected(false);
+ }
+ }
+ };
+
+ setupSubscription();
+
+ return () => {
+ mounted = false;
+ if (cleanupRef.current) {
+ cleanupRef.current();
+ cleanupRef.current = null;
+ }
+ };
+ }, [directiveId, onEvent]);
+
+ return { events, isConnected, error };
+}
diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts
index 2f4ee62..80a43eb 100644
--- a/makima/frontend/src/lib/api.ts
+++ b/makima/frontend/src/lib/api.ts
@@ -3527,3 +3527,761 @@ export async function setChainRepositoryPrimary(
}
return res.json();
}
+
+// =============================================================================
+// Directive Types and API
+// =============================================================================
+
+/** Directive status */
+export type DirectiveStatus =
+ | "draft"
+ | "planning"
+ | "active"
+ | "paused"
+ | "completed"
+ | "archived"
+ | "failed";
+
+/** Autonomy level */
+export type AutonomyLevel = "full_auto" | "guardrails" | "manual";
+
+/** Confidence level (traffic light) */
+export type ConfidenceLevel = "green" | "yellow" | "red";
+
+/** Step status */
+export type StepStatus =
+ | "pending"
+ | "ready"
+ | "running"
+ | "evaluating"
+ | "passed"
+ | "failed"
+ | "rework"
+ | "skipped"
+ | "blocked";
+
+/** Evaluation type */
+export type EvaluationType = "programmatic" | "llm" | "composite" | "manual";
+
+/** Directive summary for list view */
+export interface DirectiveSummary {
+ id: string;
+ title: string;
+ goal: string;
+ status: DirectiveStatus;
+ autonomyLevel: AutonomyLevel;
+ repositoryUrl: string | null;
+ currentChainId: string | null;
+ currentChainGeneration: number | null;
+ totalSteps: number;
+ completedSteps: number;
+ failedSteps: number;
+ currentConfidence: number | null;
+ totalCostUsd: number;
+ createdAt: string;
+ updatedAt: string;
+}
+
+/** Full directive */
+export interface Directive {
+ id: string;
+ ownerId: string;
+ title: string;
+ goal: string;
+ status: DirectiveStatus;
+ autonomyLevel: AutonomyLevel;
+ repositoryUrl: string | null;
+ localPath: string | null;
+ requirements: unknown | null;
+ acceptanceCriteria: unknown | null;
+ constraints: unknown | null;
+ confidenceThresholdGreen: number;
+ confidenceThresholdYellow: number;
+ maxReworkCycles: number;
+ maxTotalCostUsd: number | null;
+ maxWallTimeMinutes: number | null;
+ maxChainRegenerations: number;
+ totalCostUsd: number;
+ currentChainId: string | null;
+ version: number;
+ createdAt: string;
+ updatedAt: string;
+ startedAt: string | null;
+ completedAt: string | null;
+}
+
+/** Directive chain */
+export interface DirectiveChain {
+ id: string;
+ directiveId: string;
+ generation: number;
+ name: string;
+ description: string | null;
+ rationale: string | null;
+ planningModel: string | null;
+ status: string;
+ totalSteps: number;
+ completedSteps: number;
+ failedSteps: number;
+ currentConfidence: number | null;
+ startedAt: string | null;
+ completedAt: string | null;
+ version: number;
+ createdAt: string;
+ updatedAt: string;
+}
+
+/** Chain step */
+export interface ChainStep {
+ id: string;
+ chainId: string;
+ name: string;
+ description: string | null;
+ stepType: string;
+ contractType: string;
+ initialPhase: string | null;
+ taskPlan: string | null;
+ phases: string[];
+ dependsOn: string[];
+ parallelGroup: string | null;
+ requirementIds: string[];
+ acceptanceCriteriaIds: string[];
+ verifierConfig: unknown;
+ status: StepStatus;
+ contractId: string | null;
+ supervisorTaskId: string | null;
+ confidenceScore: number | null;
+ confidenceLevel: ConfidenceLevel | null;
+ evaluationCount: number;
+ reworkCount: number;
+ lastEvaluationId: string | null;
+ editorX: number | null;
+ editorY: number | null;
+ startedAt: string | null;
+ completedAt: string | null;
+ createdAt: string;
+ updatedAt: string;
+}
+
+/** Directive with progress info */
+export interface DirectiveWithProgress extends Directive {
+ chain: DirectiveChain | null;
+ steps: ChainStep[];
+ recentEvents: DirectiveEvent[];
+ pendingApprovals: DirectiveApproval[];
+}
+
+/** Directive evaluation */
+export interface DirectiveEvaluation {
+ id: string;
+ directiveId: string;
+ chainId: string | null;
+ stepId: string | null;
+ evaluationType: EvaluationType;
+ passed: boolean;
+ overallScore: number;
+ confidenceLevel: ConfidenceLevel;
+ programmaticResults: unknown | null;
+ llmResults: unknown | null;
+ compositeBreakdown: unknown | null;
+ feedback: string | null;
+ reworkInstructions: string | null;
+ verifierIds: string[];
+ evaluatedBy: string | null;
+ createdAt: string;
+}
+
+/** Directive event */
+export interface DirectiveEvent {
+ id: string;
+ directiveId: string;
+ chainId: string | null;
+ stepId: string | null;
+ eventType: string;
+ severity: string;
+ eventData: unknown | null;
+ actorType: string;
+ actorId: string | null;
+ createdAt: string;
+}
+
+/** Directive approval */
+export interface DirectiveApproval {
+ id: string;
+ directiveId: string;
+ chainId: string | null;
+ stepId: string | null;
+ approvalType: string;
+ description: string;
+ context: unknown | null;
+ urgency: string;
+ status: string;
+ requestedAt: string;
+ resolvedAt: string | null;
+ resolvedBy: string | null;
+ response: string | null;
+}
+
+/** Directive verifier */
+export interface DirectiveVerifier {
+ id: string;
+ directiveId: string;
+ name: string;
+ verifierType: string;
+ command: string | null;
+ workingDirectory: string | null;
+ timeoutSeconds: number;
+ environment: unknown;
+ autoDetect: boolean;
+ detectFiles: string[];
+ weight: number;
+ required: boolean;
+ enabled: boolean;
+ lastRunAt: string | null;
+ lastResult: unknown | null;
+ createdAt: string;
+ updatedAt: string;
+}
+
+/** Directive graph node */
+export interface DirectiveGraphNode {
+ id: string;
+ name: string;
+ stepType: string;
+ status: StepStatus;
+ confidenceScore: number | null;
+ confidenceLevel: ConfidenceLevel | null;
+ contractId: string | null;
+ editorX: number | null;
+ editorY: number | null;
+}
+
+/** Directive graph edge */
+export interface DirectiveGraphEdge {
+ source: string;
+ target: string;
+}
+
+/** Directive graph response */
+export interface DirectiveGraphResponse {
+ chainId: string;
+ directiveId: string;
+ nodes: DirectiveGraphNode[];
+ edges: DirectiveGraphEdge[];
+}
+
+/** Create directive request */
+export interface CreateDirectiveRequest {
+ goal: string;
+ repositoryUrl?: string;
+ localPath?: string;
+ autonomyLevel?: AutonomyLevel;
+ confidenceThresholdGreen?: number;
+ confidenceThresholdYellow?: number;
+ maxReworkCycles?: number;
+ maxTotalCostUsd?: number;
+ maxWallTimeMinutes?: number;
+}
+
+/** Update directive request */
+export interface UpdateDirectiveRequest {
+ title?: string;
+ goal?: string;
+ requirements?: unknown;
+ acceptanceCriteria?: unknown;
+ constraints?: unknown;
+ autonomyLevel?: AutonomyLevel;
+ confidenceThresholdGreen?: number;
+ confidenceThresholdYellow?: number;
+ maxReworkCycles?: number;
+ maxTotalCostUsd?: number;
+ maxWallTimeMinutes?: number;
+ version?: number;
+}
+
+/** Add step request */
+export interface AddStepRequest {
+ name: string;
+ description?: string;
+ stepType?: string;
+ contractType?: string;
+ initialPhase?: string;
+ taskPlan?: string;
+ phases?: string[];
+ dependsOn?: string[];
+ parallelGroup?: string;
+ requirementIds?: string[];
+ acceptanceCriteriaIds?: string[];
+ verifierConfig?: unknown;
+ editorX?: number;
+ editorY?: number;
+}
+
+/** Update step request */
+export interface UpdateStepRequest {
+ name?: string;
+ description?: string;
+ initialPhase?: string;
+ taskPlan?: string;
+ phases?: string[];
+ dependsOn?: string[];
+ parallelGroup?: string;
+ requirementIds?: string[];
+ acceptanceCriteriaIds?: string[];
+ verifierConfig?: unknown;
+ editorX?: number;
+ editorY?: number;
+}
+
+/** Create verifier request */
+export interface CreateVerifierRequest {
+ name: string;
+ verifierType: string;
+ command?: string;
+ workingDirectory?: string;
+ timeoutSeconds?: number;
+ weight?: number;
+ required?: boolean;
+ enabled?: boolean;
+}
+
+/** Update verifier request */
+export interface UpdateVerifierRequest {
+ command?: string;
+ weight?: number;
+ required?: boolean;
+ enabled?: boolean;
+}
+
+/** Approval action request */
+export interface ApprovalActionRequest {
+ response?: string;
+}
+
+/** Start directive response */
+export interface StartDirectiveResponse {
+ directiveId: string;
+ chainId: string;
+ chainGeneration: number;
+ steps: ChainStep[];
+ status: string;
+}
+
+/** Directive list response */
+export interface DirectiveListResponse {
+ directives: DirectiveSummary[];
+ total: number;
+}
+
+// =============================================================================
+// Directive API Functions
+// =============================================================================
+
+/** List directives */
+export async function listDirectives(
+ status?: DirectiveStatus,
+ limit = 50,
+ offset = 0
+): Promise<DirectiveListResponse> {
+ const params = new URLSearchParams();
+ if (status) params.set("status", status);
+ params.set("limit", String(limit));
+ params.set("offset", String(offset));
+
+ const res = await authFetch(`${API_BASE}/api/v1/directives?${params}`);
+ if (!res.ok) {
+ throw new Error(`Failed to list directives: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** Get directive by ID */
+export async function getDirective(directiveId: string): Promise<DirectiveWithProgress> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}`);
+ if (!res.ok) {
+ throw new Error(`Failed to get directive: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** Create a new directive */
+export async function createDirective(req: CreateDirectiveRequest): Promise<Directive> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives`, {
+ method: "POST",
+ body: JSON.stringify(req),
+ });
+ if (!res.ok) {
+ const errorText = await res.text();
+ throw new Error(`Failed to create directive: ${errorText || res.statusText}`);
+ }
+ return res.json();
+}
+
+/** Update directive */
+export async function updateDirective(
+ directiveId: string,
+ req: UpdateDirectiveRequest
+): Promise<Directive> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}`, {
+ method: "PUT",
+ body: JSON.stringify(req),
+ });
+ if (!res.ok) {
+ throw new Error(`Failed to update directive: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** Archive directive */
+export async function archiveDirective(directiveId: string): Promise<{ archived: boolean }> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}`, {
+ method: "DELETE",
+ });
+ if (!res.ok) {
+ throw new Error(`Failed to archive directive: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** Start directive */
+export async function startDirective(directiveId: string): Promise<StartDirectiveResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/start`, {
+ method: "POST",
+ });
+ if (!res.ok) {
+ throw new Error(`Failed to start directive: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** Pause directive */
+export async function pauseDirective(directiveId: string): Promise<Directive> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/pause`, {
+ method: "POST",
+ });
+ if (!res.ok) {
+ throw new Error(`Failed to pause directive: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** Resume directive */
+export async function resumeDirective(directiveId: string): Promise<Directive> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/resume`, {
+ method: "POST",
+ });
+ if (!res.ok) {
+ throw new Error(`Failed to resume directive: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** Stop directive */
+export async function stopDirective(directiveId: string): Promise<Directive> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/stop`, {
+ method: "POST",
+ });
+ if (!res.ok) {
+ throw new Error(`Failed to stop directive: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** Get directive chain */
+export async function getDirectiveChain(
+ directiveId: string
+): Promise<{ chain: DirectiveChain | null; steps: ChainStep[] }> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/chain`);
+ if (!res.ok) {
+ throw new Error(`Failed to get directive chain: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** Get directive chain graph */
+export async function getDirectiveGraph(directiveId: string): Promise<DirectiveGraphResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/chain/graph`);
+ if (!res.ok) {
+ throw new Error(`Failed to get directive graph: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** Replan directive chain */
+export async function replanDirectiveChain(directiveId: string): Promise<DirectiveChain> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/chain/replan`, {
+ method: "POST",
+ });
+ if (!res.ok) {
+ throw new Error(`Failed to replan directive chain: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** Add step to directive chain */
+export async function addDirectiveStep(
+ directiveId: string,
+ req: AddStepRequest
+): Promise<ChainStep> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/chain/steps`, {
+ method: "POST",
+ body: JSON.stringify(req),
+ });
+ if (!res.ok) {
+ throw new Error(`Failed to add step: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** Get step details */
+export async function getDirectiveStep(
+ directiveId: string,
+ stepId: string
+): Promise<ChainStep> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/steps/${stepId}`);
+ if (!res.ok) {
+ throw new Error(`Failed to get step: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** Update step */
+export async function updateDirectiveStep(
+ directiveId: string,
+ stepId: string,
+ req: UpdateStepRequest
+): Promise<ChainStep> {
+ const res = await authFetch(
+ `${API_BASE}/api/v1/directives/${directiveId}/chain/steps/${stepId}`,
+ {
+ method: "PUT",
+ body: JSON.stringify(req),
+ }
+ );
+ if (!res.ok) {
+ throw new Error(`Failed to update step: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** Delete step */
+export async function deleteDirectiveStep(
+ directiveId: string,
+ stepId: string
+): Promise<{ deleted: boolean }> {
+ const res = await authFetch(
+ `${API_BASE}/api/v1/directives/${directiveId}/chain/steps/${stepId}`,
+ {
+ method: "DELETE",
+ }
+ );
+ if (!res.ok) {
+ throw new Error(`Failed to delete step: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** Skip step */
+export async function skipDirectiveStep(
+ directiveId: string,
+ stepId: string
+): Promise<ChainStep> {
+ const res = await authFetch(
+ `${API_BASE}/api/v1/directives/${directiveId}/steps/${stepId}/skip`,
+ {
+ method: "POST",
+ }
+ );
+ if (!res.ok) {
+ throw new Error(`Failed to skip step: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** List directive evaluations */
+export async function listDirectiveEvaluations(
+ directiveId: string,
+ limit = 50
+): Promise<DirectiveEvaluation[]> {
+ const res = await authFetch(
+ `${API_BASE}/api/v1/directives/${directiveId}/evaluations?limit=${limit}`
+ );
+ if (!res.ok) {
+ throw new Error(`Failed to list evaluations: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** List directive events */
+export async function listDirectiveEvents(
+ directiveId: string,
+ limit = 50
+): Promise<DirectiveEvent[]> {
+ const res = await authFetch(
+ `${API_BASE}/api/v1/directives/${directiveId}/events?limit=${limit}`
+ );
+ if (!res.ok) {
+ throw new Error(`Failed to list events: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** List directive verifiers */
+export async function listDirectiveVerifiers(
+ directiveId: string
+): Promise<DirectiveVerifier[]> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/verifiers`);
+ if (!res.ok) {
+ throw new Error(`Failed to list verifiers: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** Add verifier */
+export async function addDirectiveVerifier(
+ directiveId: string,
+ req: CreateVerifierRequest
+): Promise<DirectiveVerifier> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/verifiers`, {
+ method: "POST",
+ body: JSON.stringify(req),
+ });
+ if (!res.ok) {
+ throw new Error(`Failed to add verifier: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** Update verifier */
+export async function updateDirectiveVerifier(
+ directiveId: string,
+ verifierId: string,
+ req: UpdateVerifierRequest
+): Promise<DirectiveVerifier> {
+ const res = await authFetch(
+ `${API_BASE}/api/v1/directives/${directiveId}/verifiers/${verifierId}`,
+ {
+ method: "PUT",
+ body: JSON.stringify(req),
+ }
+ );
+ if (!res.ok) {
+ throw new Error(`Failed to update verifier: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** Auto-detect verifiers */
+export async function autoDetectVerifiers(
+ directiveId: string
+): Promise<DirectiveVerifier[]> {
+ const res = await authFetch(
+ `${API_BASE}/api/v1/directives/${directiveId}/verifiers/auto-detect`,
+ {
+ method: "POST",
+ }
+ );
+ if (!res.ok) {
+ throw new Error(`Failed to auto-detect verifiers: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** List pending approvals */
+export async function listDirectiveApprovals(
+ directiveId: string
+): Promise<DirectiveApproval[]> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/approvals`);
+ if (!res.ok) {
+ throw new Error(`Failed to list approvals: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** Approve request */
+export async function approveDirectiveRequest(
+ directiveId: string,
+ approvalId: string,
+ req?: ApprovalActionRequest
+): Promise<DirectiveApproval> {
+ const res = await authFetch(
+ `${API_BASE}/api/v1/directives/${directiveId}/approvals/${approvalId}/approve`,
+ {
+ method: "POST",
+ body: JSON.stringify(req || {}),
+ }
+ );
+ if (!res.ok) {
+ throw new Error(`Failed to approve request: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** Deny request */
+export async function denyDirectiveRequest(
+ directiveId: string,
+ approvalId: string,
+ req?: ApprovalActionRequest
+): Promise<DirectiveApproval> {
+ const res = await authFetch(
+ `${API_BASE}/api/v1/directives/${directiveId}/approvals/${approvalId}/deny`,
+ {
+ method: "POST",
+ body: JSON.stringify(req || {}),
+ }
+ );
+ if (!res.ok) {
+ throw new Error(`Failed to deny request: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** Subscribe to directive events via SSE */
+export async function subscribeToDirectiveEvents(
+ directiveId: string,
+ onEvent: (event: DirectiveEvent) => void,
+ onError?: (error: Error) => void
+): Promise<() => void> {
+ // Get auth token for the request
+ let authToken: string | null = null;
+ if (supabase) {
+ const { data: { session } } = await supabase.auth.getSession();
+ if (session?.access_token) {
+ authToken = session.access_token;
+ }
+ }
+
+ // Build URL with auth token as query param (since EventSource doesn't support headers)
+ const url = new URL(`${API_BASE}/api/v1/directives/${directiveId}/events/stream`);
+ if (authToken) {
+ url.searchParams.set("token", authToken);
+ } else {
+ const apiKey = getStoredApiKey();
+ if (apiKey) {
+ url.searchParams.set("api_key", apiKey);
+ }
+ }
+
+ // Create EventSource connection
+ const eventSource = new EventSource(url.toString());
+
+ eventSource.onmessage = (e) => {
+ try {
+ const event = JSON.parse(e.data) as DirectiveEvent;
+ onEvent(event);
+ } catch (err) {
+ console.error("Failed to parse SSE event:", err);
+ }
+ };
+
+ eventSource.onerror = (_e) => {
+ if (onError) {
+ onError(new Error("SSE connection error"));
+ }
+ };
+
+ // Return cleanup function
+ return () => {
+ eventSource.close();
+ };
+}
diff --git a/makima/frontend/src/main.tsx b/makima/frontend/src/main.tsx
index a7ba1a3..5a1b98e 100644
--- a/makima/frontend/src/main.tsx
+++ b/makima/frontend/src/main.tsx
@@ -13,6 +13,7 @@ import ListenPage from "./routes/listen";
import FilesPage from "./routes/files";
import ContractsPage from "./routes/contracts";
import ChainsPage from "./routes/chains";
+import DirectivesPage from "./routes/directives";
import WorkflowPage from "./routes/workflow";
import MeshPage from "./routes/mesh";
import HistoryPage from "./routes/history";
@@ -89,6 +90,22 @@ createRoot(document.getElementById("root")!).render(
}
/>
<Route
+ path="/directives"
+ element={
+ <ProtectedRoute>
+ <DirectivesPage />
+ </ProtectedRoute>
+ }
+ />
+ <Route
+ path="/directives/:id"
+ element={
+ <ProtectedRoute>
+ <DirectivesPage />
+ </ProtectedRoute>
+ }
+ />
+ <Route
path="/contracts/:id/files/:fileId"
element={
<ProtectedRoute>
diff --git a/makima/frontend/src/routes/directives.tsx b/makima/frontend/src/routes/directives.tsx
new file mode 100644
index 0000000..51fd57a
--- /dev/null
+++ b/makima/frontend/src/routes/directives.tsx
@@ -0,0 +1,1254 @@
+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]"
+ >
+ &larr; 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>
+ );
+}