diff options
| author | soryu <soryu@soryu.co> | 2026-02-05 23:42:48 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-02-05 23:42:48 +0000 |
| commit | 88a4f15ce1310f8ee8693835be14aa5280233f17 (patch) | |
| tree | 5c1a0417e02071d2198d13478ffa85533b19f891 /makima/frontend | |
| parent | f1a50b80f3969d150bd1c31edde0aff05369157e (diff) | |
| download | soryu-88a4f15ce1310f8ee8693835be14aa5280233f17.tar.gz soryu-88a4f15ce1310f8ee8693835be14aa5280233f17.zip | |
Add directive-first chain system redesign
Redesigns the chain system with a directive-first architecture where
Directive is the top-level entity (the "why/what") and Chains are
generated execution plans (the "how") that can be dynamically modified.
Backend:
- Add database migration for directive system tables
- Add Directive, DirectiveChain, ChainStep, DirectiveEvent models
- Add DirectiveVerifier and DirectiveApproval models
- Add orchestration module with engine, planner, and verifier
- Add comprehensive API handlers for directives
- Add daemon CLI commands for directive management
- Add directive skill documentation
- Integrate contract completion with directive engine
- Add SSE endpoint for real-time directive events
Frontend:
- Add directives route with split-view layout
- Add 6-tab detail view (Overview, Chain, Events, Evaluations, Approvals, Verifiers)
- Add React Flow DAG visualization for chain steps
- Add SSE subscription hook for real-time event updates
- Add useDirectives and useDirectiveEventSubscription hooks
- Add directive types and API functions
Fixes:
- Fix test failures in ws/protocol, task_output, completion_gate, patch
- Fix word boundary matching in looks_like_task()
- Fix parse_last() to find actual last completion gate
- Fix create_export_patch when merge-base equals HEAD
- Clean up clippy warnings in new code
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'makima/frontend')
| -rw-r--r-- | makima/frontend/src/components/NavStrip.tsx | 1 | ||||
| -rw-r--r-- | makima/frontend/src/hooks/useDirectives.ts | 298 | ||||
| -rw-r--r-- | makima/frontend/src/lib/api.ts | 758 | ||||
| -rw-r--r-- | makima/frontend/src/main.tsx | 17 | ||||
| -rw-r--r-- | makima/frontend/src/routes/directives.tsx | 1254 | ||||
| -rw-r--r-- | makima/frontend/tsconfig.tsbuildinfo | 2 |
6 files changed, 2329 insertions, 1 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]" + > + ← 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> + ); +} diff --git a/makima/frontend/tsconfig.tsbuildinfo b/makima/frontend/tsconfig.tsbuildinfo index 425babe..966ee38 100644 --- a/makima/frontend/tsconfig.tsbuildinfo +++ b/makima/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/phaseconfirmationnotification.tsx","./src/components/protectedroute.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/chains/chaineditor.tsx","./src/components/chains/chainlist.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/commandmodepanel.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractcontextmenu.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phaseconfirmationmodal.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/history/checkpointcard.tsx","./src/components/history/checkpointlist.tsx","./src/components/history/conversationmessage.tsx","./src/components/history/conversationview.tsx","./src/components/history/historyfilters.tsx","./src/components/history/resumecontrols.tsx","./src/components/history/timelineeventcard.tsx","./src/components/history/timelinelist.tsx","./src/components/history/index.ts","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/discusscontractmodal.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/branchtaskmodal.tsx","./src/components/mesh/contractcompletequestion.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/gitactionspanel.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/patcheslistpanel.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/mesh/worktreefilespanel.tsx","./src/components/workflow/phasecolumn.tsx","./src/components/workflow/workflowboard.tsx","./src/components/workflow/workflowcontractcard.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usechains.ts","./src/hooks/usecontracts.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usespeakwebsocket.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/chains.tsx","./src/routes/contract-file.tsx","./src/routes/contracts.tsx","./src/routes/files.tsx","./src/routes/history.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/settings.tsx","./src/routes/speak.tsx","./src/routes/workflow.tsx","./src/types/messages.ts"],"version":"5.9.3"}
\ No newline at end of file +{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/phaseconfirmationnotification.tsx","./src/components/protectedroute.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/chains/chaineditor.tsx","./src/components/chains/chainlist.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/commandmodepanel.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractcontextmenu.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phaseconfirmationmodal.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/history/checkpointcard.tsx","./src/components/history/checkpointlist.tsx","./src/components/history/conversationmessage.tsx","./src/components/history/conversationview.tsx","./src/components/history/historyfilters.tsx","./src/components/history/resumecontrols.tsx","./src/components/history/timelineeventcard.tsx","./src/components/history/timelinelist.tsx","./src/components/history/index.ts","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/discusscontractmodal.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/branchtaskmodal.tsx","./src/components/mesh/contractcompletequestion.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/gitactionspanel.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/patcheslistpanel.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/mesh/worktreefilespanel.tsx","./src/components/workflow/phasecolumn.tsx","./src/components/workflow/workflowboard.tsx","./src/components/workflow/workflowcontractcard.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usechains.ts","./src/hooks/usecontracts.ts","./src/hooks/usedirectives.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usespeakwebsocket.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/chains.tsx","./src/routes/contract-file.tsx","./src/routes/contracts.tsx","./src/routes/directives.tsx","./src/routes/files.tsx","./src/routes/history.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/settings.tsx","./src/routes/speak.tsx","./src/routes/workflow.tsx","./src/types/messages.ts"],"version":"5.9.3"}
\ No newline at end of file |
