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/components/directives/DirectiveContractsTab.tsx148
-rw-r--r--makima/frontend/src/components/directives/DirectiveDetail.tsx425
-rw-r--r--makima/frontend/src/components/directives/DirectiveList.tsx143
-rw-r--r--makima/frontend/src/components/directives/StepDiagram.tsx313
-rw-r--r--makima/frontend/src/hooks/useDirectives.ts127
-rw-r--r--makima/frontend/src/lib/api.ts213
-rw-r--r--makima/frontend/src/main.tsx17
-rw-r--r--makima/frontend/src/routes/directives.tsx403
9 files changed, 0 insertions, 1790 deletions
diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx
index f7e67db..fb95c7f 100644
--- a/makima/frontend/src/components/NavStrip.tsx
+++ b/makima/frontend/src/components/NavStrip.tsx
@@ -11,7 +11,6 @@ interface NavLink {
const NAV_LINKS: NavLink[] = [
{ label: "Listen", href: "/listen" },
{ label: "Contracts", href: "/contracts", requiresAuth: true },
- { label: "Directives", href: "/directives", requiresAuth: true },
{ label: "Board", href: "/workflow", requiresAuth: true },
{ label: "Mesh", href: "/mesh", requiresAuth: true },
{ label: "History", href: "/history", requiresAuth: true },
diff --git a/makima/frontend/src/components/directives/DirectiveContractsTab.tsx b/makima/frontend/src/components/directives/DirectiveContractsTab.tsx
deleted file mode 100644
index 28da117..0000000
--- a/makima/frontend/src/components/directives/DirectiveContractsTab.tsx
+++ /dev/null
@@ -1,148 +0,0 @@
-import { useNavigate } from "react-router";
-import type {
- DirectiveWithChains,
- StepContractSummary,
- ContractPhase,
-} from "../../lib/api";
-import { PhaseProgressBarCompact } from "../contracts/PhaseProgressBar";
-
-interface DirectiveContractsTabProps {
- directive: DirectiveWithChains;
-}
-
-const statusColors: Record<string, string> = {
- active: "text-green-400",
- completed: "text-blue-400",
- archived: "text-[#555]",
-};
-
-function ContractCard({
- summary,
- label,
-}: {
- summary: StepContractSummary;
- label: string;
-}) {
- const navigate = useNavigate();
-
- const progressPct =
- summary.taskCount > 0
- ? Math.round((summary.tasksDone / summary.taskCount) * 100)
- : 0;
-
- return (
- <div
- className="border border-dashed border-[rgba(117,170,252,0.25)] bg-[rgba(117,170,252,0.03)] p-3 cursor-pointer hover:bg-[rgba(117,170,252,0.06)] transition-colors"
- onClick={() => navigate(`/contracts/${summary.id}`)}
- >
- <div className="flex items-center justify-between mb-1.5">
- <div className="flex items-center gap-2 min-w-0">
- <span className="font-mono text-[11px] text-[#dbe7ff] truncate">
- {summary.name}
- </span>
- <span className="font-mono text-[9px] text-[#7788aa] uppercase shrink-0">
- {summary.contractType}
- </span>
- </div>
- <div className="flex items-center gap-2 shrink-0">
- <span
- className={`font-mono text-[9px] uppercase ${statusColors[summary.status] || "text-[#888]"}`}
- >
- {summary.status}
- </span>
- <span className="font-mono text-[9px] text-[#75aafc]">&rarr;</span>
- </div>
- </div>
-
- <div className="flex items-center gap-2 mb-1.5">
- <span className="font-mono text-[9px] text-[#7788aa] uppercase shrink-0">
- {label}
- </span>
- <PhaseProgressBarCompact
- currentPhase={summary.phase as ContractPhase}
- />
- </div>
-
- {/* Task progress bar */}
- <div className="flex items-center gap-2">
- <div className="flex-1 h-1 bg-[rgba(117,170,252,0.1)] rounded-full overflow-hidden">
- <div
- className="h-full bg-[#3f6fb3] rounded-full transition-all"
- style={{ width: `${progressPct}%` }}
- />
- </div>
- <span className="font-mono text-[9px] text-[#7788aa] shrink-0">
- {summary.tasksDone}/{summary.taskCount} tasks
- </span>
- </div>
- </div>
- );
-}
-
-export function DirectiveContractsTab({
- directive,
-}: DirectiveContractsTabProps) {
- // Collect all contract summaries
- const contracts: { summary: StepContractSummary; label: string }[] = [];
-
- if (directive.orchestratorContractSummary) {
- contracts.push({
- summary: directive.orchestratorContractSummary,
- label: "Planning",
- });
- }
-
- for (const chain of directive.chains) {
- for (const step of chain.steps) {
- if (step.contractSummary) {
- contracts.push({
- summary: step.contractSummary,
- label: step.name,
- });
- }
- // Show monitoring/evaluation contracts
- if (step.monitoringContractId) {
- contracts.push({
- summary: {
- id: step.monitoringContractId,
- name: `${step.name} - Evaluation`,
- contractType: "monitoring",
- status: step.status === "evaluating" ? "active" : "completed",
- phase: "plan",
- taskCount: 1,
- tasksDone: step.status === "evaluating" ? 0 : 1,
- tasksRunning: step.status === "evaluating" ? 1 : 0,
- tasksFailed: 0,
- },
- label: `${step.name} eval`,
- });
- }
- }
- }
-
- if (contracts.length === 0) {
- return (
- <div className="text-center py-8">
- <p className="font-mono text-xs text-[#7788aa]">
- {directive.status === "draft"
- ? "No contracts yet. Start the directive to begin planning."
- : directive.status === "planning"
- ? "Planning in progress... contracts will appear when steps are created."
- : "No contracts associated with this directive."}
- </p>
- </div>
- );
- }
-
- return (
- <div className="space-y-2">
- {contracts.map((c) => (
- <ContractCard
- key={c.summary.id}
- summary={c.summary}
- label={c.label}
- />
- ))}
- </div>
- );
-}
diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx
deleted file mode 100644
index 6bdf5aa..0000000
--- a/makima/frontend/src/components/directives/DirectiveDetail.tsx
+++ /dev/null
@@ -1,425 +0,0 @@
-import { useState, useEffect, useRef } from "react";
-import { useNavigate } from "react-router";
-import type {
- DirectiveWithChains,
- DirectiveStatus,
- ContractPhase,
-} from "../../lib/api";
-import { getDirective } from "../../lib/api";
-import { PhaseProgressBarCompact } from "../contracts/PhaseProgressBar";
-import { StepDiagram } from "./StepDiagram";
-import { DirectiveContractsTab } from "./DirectiveContractsTab";
-
-interface DirectiveDetailProps {
- directive: DirectiveWithChains;
- onBack: () => void;
- onDelete?: (id: string) => void;
- onStart?: (id: string) => void;
- onRefresh?: (updated: DirectiveWithChains) => void;
-}
-
-type Tab = "overview" | "chain" | "contracts";
-
-const statusColors: Record<DirectiveStatus, string> = {
- draft: "text-[#888]",
- planning: "text-yellow-400",
- active: "text-green-400",
- paused: "text-orange-400",
- completed: "text-blue-400",
- archived: "text-[#555]",
- failed: "text-red-400",
-};
-
-function JsonSection({
- label,
- data,
-}: {
- label: string;
- data: unknown[] | unknown;
-}) {
- const items = Array.isArray(data) ? data : [];
- if (items.length === 0) return null;
-
- return (
- <div>
- <h4 className="font-mono text-[10px] text-[#75aafc] uppercase tracking-wider mb-1">
- {label}
- </h4>
- <div className="font-mono text-xs text-[#9bb8d8] bg-[rgba(0,0,0,0.2)] p-2 max-h-32 overflow-y-auto">
- {items.map((item, i) => (
- <div key={i} className="mb-0.5">
- {typeof item === "string" ? item : JSON.stringify(item)}
- </div>
- ))}
- </div>
- </div>
- );
-}
-
-export function DirectiveDetail({
- directive,
- onBack,
- onDelete,
- onStart,
- onRefresh,
-}: DirectiveDetailProps) {
- const navigate = useNavigate();
- const [activeTab, setActiveTab] = useState<Tab>("overview");
-
- // Auto-poll when directive is in an active state
- const isLive =
- directive.status === "planning" || directive.status === "active";
- const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
-
- useEffect(() => {
- if (!isLive) {
- if (intervalRef.current) {
- clearInterval(intervalRef.current);
- intervalRef.current = null;
- }
- return;
- }
-
- intervalRef.current = setInterval(async () => {
- try {
- const updated = await getDirective(directive.id);
- if (updated && onRefresh) {
- onRefresh(updated);
- }
- } catch {
- // Ignore poll errors
- }
- }, 5000);
-
- return () => {
- if (intervalRef.current) {
- clearInterval(intervalRef.current);
- intervalRef.current = null;
- }
- };
- }, [isLive, directive.id, onRefresh]);
-
- // Count total steps and completed steps across all chains
- const totalSteps = directive.chains.reduce(
- (sum, c) => sum + c.totalSteps,
- 0
- );
- const completedSteps = directive.chains.reduce(
- (sum, c) => sum + c.completedSteps,
- 0
- );
-
- // Count contracts
- const contractCount =
- (directive.orchestratorContractSummary ? 1 : 0) +
- directive.chains.reduce(
- (sum, c) =>
- sum + c.steps.filter((s) => s.contractSummary != null).length,
- 0
- );
-
- const tabs: { key: Tab; label: string; count?: number }[] = [
- { key: "overview", label: "Overview" },
- { key: "chain", label: "Chain", count: totalSteps },
- { key: "contracts", label: "Contracts", count: contractCount },
- ];
-
- return (
- <div className="panel h-full flex flex-col">
- {/* Header */}
- <div className="p-4 border-b border-dashed border-[rgba(117,170,252,0.35)]">
- <div className="flex items-center justify-between mb-3">
- <button
- onClick={onBack}
- className="font-mono text-xs text-[#75aafc] hover:text-[#9bc3ff] transition-colors"
- >
- &larr; Back to list
- </button>
- <div className="flex items-center gap-2">
- {onStart && directive.status === "draft" && (
- <button
- onClick={() => onStart(directive.id)}
- className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase"
- >
- Start
- </button>
- )}
- {onDelete && (
- <button
- onClick={() => onDelete(directive.id)}
- className="px-3 py-1.5 font-mono text-xs text-red-400 border border-red-400/30 hover:border-red-400/50 transition-colors uppercase"
- >
- Delete
- </button>
- )}
- </div>
- </div>
- <div className="flex items-center gap-3 mb-2">
- <h2 className="font-mono text-lg text-[#dbe7ff]">
- {directive.title}
- </h2>
- <span
- className={`font-mono text-xs uppercase ${
- statusColors[directive.status as DirectiveStatus] || "text-[#888]"
- }`}
- >
- {directive.status}
- </span>
- {isLive && (
- <span className="font-mono text-[9px] text-yellow-400/60 animate-pulse">
- polling
- </span>
- )}
- <span className="font-mono text-[10px] text-[#7788aa]">
- v{directive.version}
- </span>
- </div>
- </div>
-
- {/* Tabs */}
- <div className="flex border-b border-[rgba(117,170,252,0.2)]">
- {tabs.map((tab) => (
- <button
- key={tab.key}
- onClick={() => setActiveTab(tab.key)}
- className={`
- px-4 py-2 font-mono text-xs uppercase tracking-wider transition-colors
- ${
- activeTab === tab.key
- ? "text-[#dbe7ff] border-b-2 border-[#75aafc]"
- : "text-[#555] hover:text-[#9bc3ff]"
- }
- `}
- >
- {tab.label}
- {tab.count != null && tab.count > 0 && (
- <span className="ml-1 text-[10px] text-[#7788aa]">
- ({tab.count})
- </span>
- )}
- </button>
- ))}
- </div>
-
- {/* Tab content */}
- <div className="flex-1 overflow-y-auto p-4">
- {activeTab === "overview" && (
- <div className="space-y-4">
- {/* Orchestrator contract link */}
- {directive.orchestratorContractId && (
- <div className="flex items-center gap-2 p-2 border border-dashed border-[rgba(117,170,252,0.2)] bg-[rgba(117,170,252,0.03)]">
- <span className="font-mono text-[10px] text-[#7788aa] uppercase">
- Planning Contract
- </span>
- {directive.orchestratorContractSummary && (
- <PhaseProgressBarCompact
- currentPhase={
- directive.orchestratorContractSummary
- .phase as ContractPhase
- }
- />
- )}
- <button
- onClick={() =>
- navigate(
- `/contracts/${directive.orchestratorContractId}`
- )
- }
- className="font-mono text-[11px] text-[#75aafc] hover:text-white transition-colors"
- >
- {directive.orchestratorContractSummary?.name ||
- directive.orchestratorContractId.slice(0, 8) + "..."}{" "}
- &rarr;
- </button>
- {directive.status === "planning" && (
- <span className="font-mono text-[9px] text-yellow-400 animate-pulse">
- planning in progress
- </span>
- )}
- </div>
- )}
-
- {/* Goal */}
- <div>
- <h4 className="font-mono text-[10px] text-[#75aafc] uppercase tracking-wider mb-1">
- Goal
- </h4>
- <p className="font-mono text-xs text-[#9bb8d8] whitespace-pre-wrap">
- {directive.goal}
- </p>
- </div>
-
- {/* Config grid */}
- <div className="grid grid-cols-3 gap-2">
- <div>
- <span className="font-mono text-[10px] text-[#7788aa] uppercase">
- Autonomy
- </span>
- <div className="font-mono text-xs text-[#dbe7ff]">
- {directive.autonomyLevel}
- </div>
- </div>
- <div>
- <span className="font-mono text-[10px] text-[#7788aa] uppercase">
- Chains
- </span>
- <div className="font-mono text-xs text-[#dbe7ff]">
- {directive.chainGenerationCount} generated
- </div>
- </div>
- <div>
- <span className="font-mono text-[10px] text-[#7788aa] uppercase">
- Cost
- </span>
- <div className="font-mono text-xs text-[#dbe7ff]">
- ${directive.totalCostUsd.toFixed(2)}
- </div>
- </div>
- {directive.repositoryUrl && (
- <div className="col-span-3">
- <span className="font-mono text-[10px] text-[#7788aa] uppercase">
- Repository
- </span>
- <div className="font-mono text-xs text-[#dbe7ff] truncate">
- {directive.repositoryUrl}
- </div>
- </div>
- )}
- </div>
-
- {/* Stat cards */}
- <div className="grid grid-cols-3 gap-2">
- <div className="border border-dashed border-[rgba(117,170,252,0.2)] p-2 text-center">
- <div className="font-mono text-lg text-[#dbe7ff]">
- {totalSteps}
- </div>
- <div className="font-mono text-[9px] text-[#7788aa] uppercase">
- Total Steps
- </div>
- </div>
- <div className="border border-dashed border-[rgba(117,170,252,0.2)] p-2 text-center">
- <div className="font-mono text-lg text-green-400">
- {completedSteps}
- </div>
- <div className="font-mono text-[9px] text-[#7788aa] uppercase">
- Completed
- </div>
- </div>
- <div className="border border-dashed border-[rgba(117,170,252,0.2)] p-2 text-center">
- <div className="font-mono text-lg text-[#dbe7ff]">
- ${directive.totalCostUsd.toFixed(2)}
- </div>
- <div className="font-mono text-[9px] text-[#7788aa] uppercase">
- Cost
- </div>
- </div>
- </div>
-
- {/* Structured sections */}
- <JsonSection label="Requirements" data={directive.requirements} />
- <JsonSection
- label="Acceptance Criteria"
- data={directive.acceptanceCriteria}
- />
- <JsonSection label="Constraints" data={directive.constraints} />
- <JsonSection
- label="External Dependencies"
- data={directive.externalDependencies}
- />
-
- {/* Metadata */}
- <div>
- <h4 className="font-mono text-[10px] text-[#75aafc] uppercase tracking-wider mb-1">
- Metadata
- </h4>
- <div className="grid grid-cols-2 gap-1 font-mono text-[10px]">
- <span className="text-[#7788aa]">Created</span>
- <span className="text-[#9bb8d8]">
- {new Date(directive.createdAt).toLocaleString()}
- </span>
- <span className="text-[#7788aa]">Updated</span>
- <span className="text-[#9bb8d8]">
- {new Date(directive.updatedAt).toLocaleString()}
- </span>
- {directive.startedAt && (
- <>
- <span className="text-[#7788aa]">Started</span>
- <span className="text-[#9bb8d8]">
- {new Date(directive.startedAt).toLocaleString()}
- </span>
- </>
- )}
- {directive.completedAt && (
- <>
- <span className="text-[#7788aa]">Completed</span>
- <span className="text-[#9bb8d8]">
- {new Date(directive.completedAt).toLocaleString()}
- </span>
- </>
- )}
- <span className="text-[#7788aa]">Version</span>
- <span className="text-[#9bb8d8]">{directive.version}</span>
- </div>
- </div>
- </div>
- )}
-
- {activeTab === "chain" && (
- <div className="space-y-4">
- {directive.chains.length === 0 ? (
- <div className="flex flex-col items-center justify-center py-12">
- <div className="font-mono text-[40px] text-[#333] mb-3">
- {directive.status === "planning" ? "\u2699" : "\u25CB"}
- </div>
- <p className="font-mono text-xs text-[#7788aa] text-center max-w-[300px]">
- {directive.status === "planning"
- ? "Planning in progress... the chain will appear when the planner submits a plan."
- : directive.status === "draft"
- ? "No chains yet. Start the directive to begin planning."
- : "No chains created for this directive."}
- </p>
- </div>
- ) : (
- <>
- {/* Chain metadata header */}
- {directive.chains.map((chain) => (
- <div key={chain.id} className="space-y-3">
- <div className="flex items-center gap-3 pb-2 border-b border-dashed border-[rgba(117,170,252,0.15)]">
- <span className="font-mono text-xs text-[#dbe7ff]">
- {chain.name}
- </span>
- <span className="font-mono text-[10px] text-[#7788aa] uppercase">
- gen {chain.generation}
- </span>
- <span className={`font-mono text-[10px] uppercase ${
- chain.status === "completed" ? "text-green-400" :
- chain.status === "failed" ? "text-red-400" :
- chain.status === "running" ? "text-yellow-400" :
- "text-[#7788aa]"
- }`}>
- {chain.status}
- </span>
- <span className="font-mono text-[10px] text-[#7788aa] ml-auto">
- {chain.completedSteps}/{chain.totalSteps} steps
- {chain.failedSteps > 0 && (
- <span className="text-red-400 ml-1">
- ({chain.failedSteps} failed)
- </span>
- )}
- </span>
- </div>
- <StepDiagram steps={chain.steps} />
- </div>
- ))}
- </>
- )}
- </div>
- )}
-
- {activeTab === "contracts" && (
- <DirectiveContractsTab directive={directive} />
- )}
- </div>
- </div>
- );
-}
diff --git a/makima/frontend/src/components/directives/DirectiveList.tsx b/makima/frontend/src/components/directives/DirectiveList.tsx
deleted file mode 100644
index 5afa36e..0000000
--- a/makima/frontend/src/components/directives/DirectiveList.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import { useState } from "react";
-import type { DirectiveSummary, DirectiveStatus } from "../../lib/api";
-
-interface DirectiveListProps {
- directives: DirectiveSummary[];
- loading: boolean;
- onSelect: (id: string) => void;
- onCreate: () => void;
- onDelete?: (directive: DirectiveSummary) => void;
- selectedId?: string;
-}
-
-const statusColors: Record<DirectiveStatus, string> = {
- draft: "text-[#888]",
- planning: "text-yellow-400",
- active: "text-green-400",
- paused: "text-orange-400",
- completed: "text-blue-400",
- archived: "text-[#555]",
- failed: "text-red-400",
-};
-
-export function DirectiveList({
- directives,
- loading,
- onSelect,
- onCreate,
- selectedId,
-}: DirectiveListProps) {
- const [filter, setFilter] = useState<DirectiveStatus | "all">("all");
-
- const filteredDirectives =
- filter === "all"
- ? directives
- : directives.filter((d) => d.status === filter);
-
- if (loading) {
- return (
- <div className="panel h-full flex items-center justify-center">
- <div className="font-mono text-[#9bc3ff] text-sm">Loading...</div>
- </div>
- );
- }
-
- return (
- <div className="panel h-full flex flex-col">
- {/* Header */}
- <div className="p-4 border-b border-dashed border-[rgba(117,170,252,0.35)]">
- <div className="flex items-center justify-between mb-3">
- <h2 className="font-mono text-sm text-[#75aafc] uppercase tracking-wider">
- Directives
- </h2>
- <button
- onClick={onCreate}
- className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase"
- >
- + New
- </button>
- </div>
-
- {/* Filter tabs */}
- <div className="flex gap-1 flex-wrap">
- {(["all", "draft", "planning", "active", "paused", "completed", "failed"] as const).map(
- (status) => (
- <button
- key={status}
- onClick={() => setFilter(status)}
- className={`
- px-2 py-1 font-mono text-[10px] uppercase tracking-wider transition-colors
- ${
- filter === status
- ? "bg-[rgba(117,170,252,0.1)] text-[#9bc3ff] border border-[rgba(117,170,252,0.3)]"
- : "text-[#555] hover:text-[#75aafc]"
- }
- `}
- >
- {status}
- </button>
- )
- )}
- </div>
- </div>
-
- {/* List */}
- <div className="flex-1 overflow-y-auto">
- {filteredDirectives.length === 0 ? (
- <div className="p-4 text-center">
- <p className="font-mono text-sm text-[#555]">
- {filter === "all"
- ? "No directives yet"
- : `No ${filter} directives`}
- </p>
- </div>
- ) : (
- <div className="divide-y divide-[rgba(117,170,252,0.15)]">
- {filteredDirectives.map((directive) => (
- <button
- key={directive.id}
- onClick={() => onSelect(directive.id)}
- className={`
- w-full text-left p-4 transition-colors
- ${
- selectedId === directive.id
- ? "bg-[rgba(117,170,252,0.1)]"
- : "hover:bg-[rgba(117,170,252,0.05)]"
- }
- `}
- >
- <div className="flex items-start justify-between gap-2 mb-2">
- <h3 className="font-mono text-sm text-[#dbe7ff] truncate">
- {directive.title}
- </h3>
- <span
- className={`text-[10px] font-mono uppercase shrink-0 ${
- statusColors[directive.status] || "text-[#888]"
- }`}
- >
- {directive.status}
- </span>
- </div>
-
- {directive.goal && (
- <p className="font-mono text-xs text-[#555] mb-2 line-clamp-2">
- {directive.goal}
- </p>
- )}
-
- <div className="flex items-center gap-3 text-[10px] font-mono text-[#555]">
- {directive.chainCount > 0 && (
- <span>{directive.chainCount} chains</span>
- )}
- {directive.stepCount > 0 && (
- <span>{directive.stepCount} steps</span>
- )}
- </div>
- </button>
- ))}
- </div>
- )}
- </div>
- </div>
- );
-}
diff --git a/makima/frontend/src/components/directives/StepDiagram.tsx b/makima/frontend/src/components/directives/StepDiagram.tsx
deleted file mode 100644
index 33892e0..0000000
--- a/makima/frontend/src/components/directives/StepDiagram.tsx
+++ /dev/null
@@ -1,313 +0,0 @@
-import { useNavigate } from "react-router";
-import type { ChainStep, ContractPhase } from "../../lib/api";
-import { PhaseProgressBarCompact } from "../contracts/PhaseProgressBar";
-
-interface StepDiagramProps {
- steps: ChainStep[];
-}
-
-const statusColors: Record<string, { border: string; dot: string; bg: string; glow: string }> = {
- pending: {
- border: "border-[#444]",
- dot: "bg-[#555]",
- bg: "bg-[rgba(40,40,50,0.6)]",
- glow: "",
- },
- running: {
- border: "border-yellow-400/60",
- dot: "bg-yellow-400",
- bg: "bg-[rgba(80,70,20,0.3)]",
- glow: "shadow-[0_0_8px_rgba(250,204,21,0.15)]",
- },
- evaluating: {
- border: "border-blue-400/60",
- dot: "bg-blue-400",
- bg: "bg-[rgba(20,50,80,0.3)]",
- glow: "shadow-[0_0_8px_rgba(96,165,250,0.15)]",
- },
- passed: {
- border: "border-green-400/60",
- dot: "bg-green-400",
- bg: "bg-[rgba(20,60,30,0.3)]",
- glow: "",
- },
- failed: {
- border: "border-red-400/60",
- dot: "bg-red-400",
- bg: "bg-[rgba(60,20,20,0.3)]",
- glow: "",
- },
-};
-
-const statusLabels: Record<string, string> = {
- pending: "Pending",
- ready: "Ready",
- running: "Running",
- evaluating: "Evaluating",
- passed: "Passed",
- failed: "Failed",
- rework: "Rework",
- skipped: "Skipped",
- blocked: "Blocked",
-};
-
-const confidenceColors: Record<string, string> = {
- green: "text-green-400",
- yellow: "text-yellow-400",
- red: "text-red-400",
-};
-
-/**
- * Assign depth to each step via topological sort based on dependsOn UUIDs.
- */
-function assignDepths(steps: ChainStep[]): Map<string, number> {
- const depths = new Map<string, number>();
- const stepMap = new Map(steps.map((s) => [s.id, s]));
-
- function getDepth(id: string): number {
- if (depths.has(id)) return depths.get(id)!;
- const step = stepMap.get(id);
- if (!step || !step.dependsOn || step.dependsOn.length === 0) {
- depths.set(id, 0);
- return 0;
- }
- const maxParent = Math.max(
- ...step.dependsOn.map((depId) => getDepth(depId))
- );
- const d = maxParent + 1;
- depths.set(id, d);
- return d;
- }
-
- for (const step of steps) {
- getDepth(step.id);
- }
-
- return depths;
-}
-
-function StepCard({ step }: { step: ChainStep }) {
- const navigate = useNavigate();
- const colors = statusColors[step.status] || statusColors.pending;
- const summary = step.contractSummary;
- const hasContract = !!step.contractId;
-
- return (
- <div
- className={`
- border ${colors.border} ${colors.bg} ${colors.glow}
- p-3 w-[220px] transition-all duration-200
- ${hasContract ? "cursor-pointer hover:brightness-125" : ""}
- `}
- onClick={() => {
- if (hasContract) navigate(`/contracts/${step.contractId}`);
- }}
- title={hasContract ? "View contract" : undefined}
- >
- {/* Status header */}
- <div className="flex items-center justify-between mb-2">
- <div className="flex items-center gap-2 min-w-0 flex-1">
- <div className={`w-2 h-2 rounded-full ${colors.dot} shrink-0 ${
- step.status === "running" ? "animate-pulse" : ""
- }`} />
- <span className="font-mono text-[11px] text-[#dbe7ff] truncate font-medium">
- {step.name}
- </span>
- </div>
- <span className={`font-mono text-[9px] uppercase tracking-wider shrink-0 ml-2 ${
- step.status === "passed" ? "text-green-400" :
- step.status === "failed" ? "text-red-400" :
- step.status === "running" ? "text-yellow-400" :
- step.status === "evaluating" ? "text-blue-400" :
- "text-[#555]"
- }`}>
- {statusLabels[step.status] || step.status}
- </span>
- </div>
-
- {/* Description */}
- {step.description && (
- <p className="font-mono text-[10px] text-[#7788aa] mb-2 line-clamp-2 leading-relaxed">
- {step.description}
- </p>
- )}
-
- {/* Evaluation info */}
- {(step.confidenceScore != null || step.evaluationCount > 0 || step.reworkCount > 0) && (
- <div className="border-t border-[rgba(117,170,252,0.1)] pt-2 mt-1">
- <div className="flex items-center gap-2 font-mono text-[9px]">
- {step.confidenceScore != null && (
- <span className={confidenceColors[step.confidenceLevel || ""] || "text-[#7788aa]"}>
- {Math.round(step.confidenceScore * 100)}% confidence
- </span>
- )}
- {step.evaluationCount > 0 && (
- <span className="text-[#7788aa]">
- eval #{step.evaluationCount}
- </span>
- )}
- {step.reworkCount > 0 && (
- <span className="text-orange-400">
- rework x{step.reworkCount}
- </span>
- )}
- </div>
- </div>
- )}
-
- {/* Monitoring link (when evaluating) */}
- {step.status === "evaluating" && step.monitoringContractId && (
- <div className="border-t border-[rgba(117,170,252,0.1)] pt-1.5 mt-1">
- <span
- className="font-mono text-[9px] text-blue-400 cursor-pointer hover:text-blue-300"
- onClick={(e) => {
- e.stopPropagation();
- navigate(`/contracts/${step.monitoringContractId}`);
- }}
- >
- evaluation contract &rarr;
- </span>
- </div>
- )}
-
- {/* Contract progress */}
- {summary && (
- <div className="border-t border-[rgba(117,170,252,0.1)] pt-2 mt-1">
- <div className="mb-1.5">
- <PhaseProgressBarCompact
- currentPhase={summary.phase as ContractPhase}
- />
- </div>
- <div className="flex items-center gap-3 font-mono text-[9px]">
- <span className="text-[#7788aa]">
- {summary.tasksDone}/{summary.taskCount} tasks
- </span>
- {summary.tasksRunning > 0 && (
- <span className="text-yellow-400">
- {summary.tasksRunning} running
- </span>
- )}
- {summary.tasksFailed > 0 && (
- <span className="text-red-400">
- {summary.tasksFailed} failed
- </span>
- )}
- </div>
- </div>
- )}
-
- {/* Contract link arrow */}
- {hasContract && !summary && step.status !== "evaluating" && (
- <div className="border-t border-[rgba(117,170,252,0.1)] pt-1.5 mt-1">
- <span className="font-mono text-[9px] text-[#75aafc]">
- view contract &rarr;
- </span>
- </div>
- )}
- </div>
- );
-}
-
-/** Vertical connector between levels */
-function LevelConnector({ count }: { count: number }) {
- return (
- <div className="flex justify-center py-1">
- <div className="flex items-center gap-3">
- {Array.from({ length: count }).map((_, i) => (
- <div key={i} className="flex flex-col items-center">
- <div className="w-px h-4 bg-[rgba(117,170,252,0.25)]" />
- <div className="text-[rgba(117,170,252,0.4)] text-[10px] leading-none">&darr;</div>
- <div className="w-px h-4 bg-[rgba(117,170,252,0.25)]" />
- </div>
- ))}
- </div>
- </div>
- );
-}
-
-export function StepDiagram({ steps }: StepDiagramProps) {
- if (steps.length === 0) {
- return (
- <p className="font-mono text-xs text-[#7788aa]">No steps to display.</p>
- );
- }
-
- const depths = assignDepths(steps);
- const maxDepth = Math.max(...Array.from(depths.values()));
-
- // Group steps by depth level
- const levels: ChainStep[][] = [];
- for (let d = 0; d <= maxDepth; d++) {
- levels.push(
- steps
- .filter((s) => depths.get(s.id) === d)
- .sort((a, b) => a.orderIndex - b.orderIndex)
- );
- }
-
- // Compute overall progress
- const passedCount = steps.filter(s => s.status === "passed").length;
- const failedCount = steps.filter(s => s.status === "failed").length;
- const runningCount = steps.filter(s => s.status === "running").length;
- const evaluatingCount = steps.filter(s => s.status === "evaluating").length;
-
- return (
- <div>
- {/* Progress summary */}
- <div className="flex items-center gap-4 mb-4 font-mono text-[10px]">
- <span className="text-[#7788aa]">
- {levels.length} level{levels.length !== 1 ? "s" : ""} &middot; {steps.length} step{steps.length !== 1 ? "s" : ""}
- </span>
- {passedCount > 0 && (
- <span className="text-green-400">{passedCount} passed</span>
- )}
- {runningCount > 0 && (
- <span className="text-yellow-400">{runningCount} running</span>
- )}
- {evaluatingCount > 0 && (
- <span className="text-blue-400">{evaluatingCount} evaluating</span>
- )}
- {failedCount > 0 && (
- <span className="text-red-400">{failedCount} failed</span>
- )}
- <div className="flex-1 h-1 bg-[rgba(117,170,252,0.1)] rounded-full overflow-hidden">
- <div
- className="h-full bg-green-400/60 rounded-full transition-all duration-500"
- style={{ width: `${(passedCount / steps.length) * 100}%` }}
- />
- </div>
- <span className="text-[#7788aa]">
- {Math.round((passedCount / steps.length) * 100)}%
- </span>
- </div>
-
- {/* Chain flow */}
- <div className="flex flex-col items-center">
- {levels.map((level, li) => (
- <div key={li}>
- {/* Level label */}
- {levels.length > 1 && (
- <div className="flex justify-center mb-2">
- <span className="font-mono text-[9px] text-[#555] uppercase tracking-widest px-2 py-0.5 border border-[rgba(117,170,252,0.1)] bg-[rgba(0,0,0,0.3)]">
- {li === 0 ? "Start" : li === maxDepth ? "Final" : `Level ${li}`}
- </span>
- </div>
- )}
-
- {/* Steps at this level */}
- <div className="flex items-start justify-center gap-3 flex-wrap">
- {level.map((step) => (
- <StepCard key={step.id} step={step} />
- ))}
- </div>
-
- {/* Connector to next level */}
- {li < maxDepth && (
- <LevelConnector count={Math.min(level.length, 3)} />
- )}
- </div>
- ))}
- </div>
- </div>
- );
-}
diff --git a/makima/frontend/src/hooks/useDirectives.ts b/makima/frontend/src/hooks/useDirectives.ts
deleted file mode 100644
index af1c8c6..0000000
--- a/makima/frontend/src/hooks/useDirectives.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-import { useState, useCallback, useEffect } from "react";
-import {
- listDirectives,
- getDirective,
- createDirective,
- updateDirective,
- deleteDirective,
- startDirective as startDirectiveApi,
- type DirectiveSummary,
- type DirectiveWithChains,
- type CreateDirectiveRequest,
- type UpdateDirectiveRequest,
-} from "../lib/api";
-
-export function useDirectives() {
- const [directives, setDirectives] = useState<DirectiveSummary[]>([]);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState<string | null>(null);
-
- const fetchDirectives = useCallback(async () => {
- setLoading(true);
- setError(null);
- try {
- const response = await listDirectives();
- setDirectives(response.directives);
- } catch (e) {
- setError(e instanceof Error ? e.message : "Failed to fetch directives");
- } finally {
- setLoading(false);
- }
- }, []);
-
- const fetchDirective = useCallback(
- async (id: string): Promise<DirectiveWithChains | null> => {
- setError(null);
- try {
- return await getDirective(id);
- } catch (e) {
- setError(e instanceof Error ? e.message : "Failed to fetch directive");
- return null;
- }
- },
- []
- );
-
- const saveDirective = useCallback(
- async (data: CreateDirectiveRequest): Promise<DirectiveSummary | null> => {
- setError(null);
- try {
- const directive = await createDirective(data);
- await fetchDirectives();
- return directive as unknown as DirectiveSummary;
- } catch (e) {
- setError(e instanceof Error ? e.message : "Failed to create directive");
- return null;
- }
- },
- [fetchDirectives]
- );
-
- const editDirective = useCallback(
- async (
- id: string,
- data: UpdateDirectiveRequest
- ): Promise<DirectiveSummary | null> => {
- setError(null);
- try {
- const directive = await updateDirective(id, data);
- await fetchDirectives();
- return directive as unknown as DirectiveSummary;
- } catch (e) {
- setError(e instanceof Error ? e.message : "Failed to update directive");
- return null;
- }
- },
- [fetchDirectives]
- );
-
- const removeDirective = useCallback(
- async (id: string): Promise<boolean> => {
- setError(null);
- try {
- await deleteDirective(id);
- await fetchDirectives();
- return true;
- } catch (e) {
- setError(e instanceof Error ? e.message : "Failed to delete directive");
- return false;
- }
- },
- [fetchDirectives]
- );
-
- const startDirective = useCallback(
- async (id: string): Promise<boolean> => {
- setError(null);
- try {
- await startDirectiveApi(id);
- await fetchDirectives();
- return true;
- } catch (e) {
- setError(
- e instanceof Error ? e.message : "Failed to start directive"
- );
- return false;
- }
- },
- [fetchDirectives]
- );
-
- // Initial fetch
- useEffect(() => {
- fetchDirectives();
- }, [fetchDirectives]);
-
- return {
- directives,
- loading,
- error,
- fetchDirectives,
- fetchDirective,
- saveDirective,
- editDirective,
- removeDirective,
- startDirective,
- };
-}
diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts
index 5080ee1..7732725 100644
--- a/makima/frontend/src/lib/api.ts
+++ b/makima/frontend/src/lib/api.ts
@@ -3003,217 +3003,4 @@ export async function listTaskPatches(taskId: string, contractId: string): Promi
return res.json();
}
-// =============================================================================
-// Directive Types & API
-// =============================================================================
-
-export type DirectiveStatus = "draft" | "planning" | "active" | "paused" | "completed" | "archived" | "failed";
-export type AutonomyLevel = "full_auto" | "guardrails" | "manual";
-
-export interface DirectiveSummary {
- id: string;
- title: string;
- goal: string;
- status: DirectiveStatus;
- autonomyLevel: AutonomyLevel;
- chainCount: number;
- stepCount: number;
- totalCostUsd: number;
- version: number;
- createdAt: string;
- updatedAt: string;
-}
-
-export interface Directive {
- id: string;
- ownerId: string;
- title: string;
- goal: string;
- requirements: unknown[];
- acceptanceCriteria: unknown[];
- constraints: unknown[];
- externalDependencies: unknown[];
- status: DirectiveStatus;
- autonomyLevel: AutonomyLevel;
- confidenceThresholdGreen: number;
- confidenceThresholdYellow: number;
- maxTotalCostUsd: number | null;
- maxWallTimeMinutes: number | null;
- maxReworkCycles: number | null;
- maxChainRegenerations: number | null;
- repositoryUrl: string | null;
- localPath: string | null;
- baseBranch: string | null;
- orchestratorContractId: string | null;
- currentChainId: string | null;
- chainGenerationCount: number;
- totalCostUsd: number;
- startedAt: string | null;
- completedAt: string | null;
- version: number;
- createdAt: string;
- updatedAt: string;
-}
-
-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;
-}
-
-export interface StepContractSummary {
- id: string;
- name: string;
- contractType: string;
- phase: string;
- status: string;
- taskCount: number;
- tasksDone: number;
- tasksRunning: number;
- tasksFailed: number;
-}
-
-export interface ChainStep {
- id: string;
- chainId: string;
- name: string;
- description: string | null;
- stepType: string;
- contractType: string;
- initialPhase: string | null;
- taskPlan: string | null;
- dependsOn: string[] | null;
- status: string;
- contractId: string | null;
- supervisorTaskId: string | null;
- monitoringContractId: string | null;
- monitoringTaskId: string | null;
- confidenceScore: number | null;
- confidenceLevel: string | null;
- evaluationCount: number;
- reworkCount: number;
- lastEvaluationId: string | null;
- orderIndex: number;
- startedAt: string | null;
- completedAt: string | null;
- createdAt: string;
- contractSummary: StepContractSummary | null;
-}
-
-export interface ChainWithSteps extends DirectiveChain {
- steps: ChainStep[];
-}
-
-export interface DirectiveWithChains extends Directive {
- orchestratorContractSummary: StepContractSummary | null;
- chains: ChainWithSteps[];
-}
-
-export interface DirectiveListResponse {
- directives: DirectiveSummary[];
- total: number;
-}
-
-export interface CreateDirectiveRequest {
- title: string;
- goal: string;
- requirements?: unknown[];
- acceptanceCriteria?: unknown[];
- constraints?: unknown[];
- externalDependencies?: unknown[];
- autonomyLevel?: AutonomyLevel;
- repositoryUrl?: string;
- localPath?: string;
- baseBranch?: string;
-}
-
-export interface UpdateDirectiveRequest {
- title?: string;
- goal?: string;
- status?: DirectiveStatus;
- autonomyLevel?: AutonomyLevel;
- version?: number;
-}
-
-export async function listDirectives(): Promise<DirectiveListResponse> {
- const res = await authFetch(`${API_BASE}/api/v1/directives`);
- if (!res.ok) {
- throw new Error(`Failed to list directives: ${res.statusText}`);
- }
- return res.json();
-}
-
-export async function getDirective(id: string): Promise<DirectiveWithChains> {
- const res = await authFetch(`${API_BASE}/api/v1/directives/${id}`);
- if (!res.ok) {
- throw new Error(`Failed to get directive: ${res.statusText}`);
- }
- return res.json();
-}
-
-export async function createDirective(
- data: CreateDirectiveRequest
-): Promise<Directive> {
- const res = await authFetch(`${API_BASE}/api/v1/directives`, {
- method: "POST",
- body: JSON.stringify(data),
- });
- if (!res.ok) {
- throw new Error(`Failed to create directive: ${res.statusText}`);
- }
- return res.json();
-}
-
-export async function updateDirective(
- id: string,
- data: UpdateDirectiveRequest
-): Promise<Directive> {
- const res = await authFetch(`${API_BASE}/api/v1/directives/${id}`, {
- method: "PUT",
- body: JSON.stringify(data),
- });
- if (res.status === 409) {
- const conflict = (await res.json()) as ConflictErrorResponse;
- throw new VersionConflictError(conflict);
- }
- if (!res.ok) {
- throw new Error(`Failed to update directive: ${res.statusText}`);
- }
- return res.json();
-}
-
-export async function deleteDirective(id: string): Promise<void> {
- const res = await authFetch(`${API_BASE}/api/v1/directives/${id}`, {
- method: "DELETE",
- });
- if (!res.ok) {
- throw new Error(`Failed to delete directive: ${res.statusText}`);
- }
-}
-
-export async function startDirective(id: string): Promise<Directive> {
- const res = await authFetch(`${API_BASE}/api/v1/directives/${id}/start`, {
- method: "POST",
- });
- if (!res.ok) {
- const body = await res.json().catch(() => null);
- const msg = body?.message || res.statusText;
- throw new Error(`Failed to start directive: ${msg}`);
- }
- return res.json();
-}
diff --git a/makima/frontend/src/main.tsx b/makima/frontend/src/main.tsx
index f07a143..50fffe4 100644
--- a/makima/frontend/src/main.tsx
+++ b/makima/frontend/src/main.tsx
@@ -18,7 +18,6 @@ import HistoryPage from "./routes/history";
import LoginPage from "./routes/login";
import SettingsPage from "./routes/settings";
import ContractFilePage from "./routes/contract-file";
-import DirectivesPage from "./routes/directives";
import SpeakPage from "./routes/speak";
createRoot(document.getElementById("root")!).render(
@@ -81,22 +80,6 @@ createRoot(document.getElementById("root")!).render(
}
/>
<Route
- path="/directives"
- element={
- <ProtectedRoute>
- <DirectivesPage />
- </ProtectedRoute>
- }
- />
- <Route
- path="/directives/:id"
- element={
- <ProtectedRoute>
- <DirectivesPage />
- </ProtectedRoute>
- }
- />
- <Route
path="/workflow"
element={
<ProtectedRoute>
diff --git a/makima/frontend/src/routes/directives.tsx b/makima/frontend/src/routes/directives.tsx
deleted file mode 100644
index fd3808b..0000000
--- a/makima/frontend/src/routes/directives.tsx
+++ /dev/null
@@ -1,403 +0,0 @@
-import { useState, useCallback, useEffect } from "react";
-import { useParams, useNavigate } from "react-router";
-import { Masthead } from "../components/Masthead";
-import { DirectiveList } from "../components/directives/DirectiveList";
-import { DirectiveDetail } from "../components/directives/DirectiveDetail";
-import { DirectoryInput } from "../components/mesh/DirectoryInput";
-import { useDirectives } from "../hooks/useDirectives";
-import { useAuth } from "../contexts/AuthContext";
-import {
- getDaemonDirectories,
- getRepositorySuggestions,
-} from "../lib/api";
-import type {
- DirectiveWithChains,
- CreateDirectiveRequest,
- RepositorySourceType,
- DaemonDirectory,
- RepositoryHistoryEntry,
-} 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]);
-
- 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>
- );
- }
-
- if (isAuthConfigured && !isAuthenticated) {
- return null;
- }
-
- return <DirectivesContent />;
-}
-
-function DirectivesContent() {
- const { id } = useParams<{ id?: string }>();
- const navigate = useNavigate();
- const {
- directives,
- loading,
- error,
- fetchDirective,
- saveDirective,
- removeDirective,
- startDirective,
- } = useDirectives();
-
- const [selectedDirective, setSelectedDirective] =
- useState<DirectiveWithChains | null>(null);
- const [detailLoading, setDetailLoading] = useState(false);
- const [showCreateForm, setShowCreateForm] = useState(false);
- const [createTitle, setCreateTitle] = useState("");
- const [createGoal, setCreateGoal] = useState("");
-
- // Repository state
- const [repoType, setRepoType] = useState<RepositorySourceType>("remote");
- const [repoUrl, setRepoUrl] = useState("");
- const [repoPath, setRepoPath] = useState("");
- const [suggestedDirectories, setSuggestedDirectories] = useState<DaemonDirectory[]>([]);
- const [repoSuggestions, setRepoSuggestions] = useState<RepositoryHistoryEntry[]>([]);
- const [showRepoSuggestions, setShowRepoSuggestions] = useState(false);
-
- // Fetch repository suggestions when modal opens and repo type changes
- useEffect(() => {
- if (showCreateForm && (repoType === "remote" || repoType === "local")) {
- getRepositorySuggestions(repoType, undefined, 10)
- .then((res) => {
- setRepoSuggestions(res.entries);
- setShowRepoSuggestions(res.entries.length > 0);
- })
- .catch(() => {
- setRepoSuggestions([]);
- setShowRepoSuggestions(false);
- });
- } else {
- setRepoSuggestions([]);
- setShowRepoSuggestions(false);
- }
- }, [showCreateForm, repoType]);
-
- // Fetch daemon directories when "local" repo type is selected
- useEffect(() => {
- if (repoType === "local" && showCreateForm) {
- getDaemonDirectories()
- .then((res) => setSuggestedDirectories(res.directories))
- .catch(() => setSuggestedDirectories([]));
- }
- }, [repoType, showCreateForm]);
-
- // Apply a repository suggestion
- const applyRepoSuggestion = useCallback((suggestion: RepositoryHistoryEntry) => {
- if (suggestion.repositoryUrl) {
- setRepoUrl(suggestion.repositoryUrl);
- }
- if (suggestion.localPath) {
- setRepoPath(suggestion.localPath);
- }
- setShowRepoSuggestions(false);
- }, []);
-
- // Load directive when ID changes
- useEffect(() => {
- if (id) {
- setDetailLoading(true);
- fetchDirective(id).then((d) => {
- setSelectedDirective(d);
- setDetailLoading(false);
- });
- } else {
- setSelectedDirective(null);
- }
- }, [id, fetchDirective]);
-
- const handleSelect = useCallback(
- (directiveId: string) => {
- navigate(`/directives/${directiveId}`);
- },
- [navigate]
- );
-
- const handleBack = useCallback(() => {
- navigate("/directives");
- }, [navigate]);
-
- const resetCreateForm = useCallback(() => {
- setShowCreateForm(false);
- setCreateTitle("");
- setCreateGoal("");
- setRepoType("remote");
- setRepoUrl("");
- setRepoPath("");
- }, []);
-
- const handleCreate = useCallback(async () => {
- if (!createTitle.trim() || !createGoal.trim()) return;
-
- const data: CreateDirectiveRequest = {
- title: createTitle.trim(),
- goal: createGoal.trim(),
- };
- if (repoType === "remote" && repoUrl.trim()) {
- data.repositoryUrl = repoUrl.trim();
- } else if (repoType === "local" && repoPath.trim()) {
- data.localPath = repoPath.trim();
- }
-
- const result = await saveDirective(data);
- if (result) {
- resetCreateForm();
- }
- }, [createTitle, createGoal, repoType, repoUrl, repoPath, saveDirective, resetCreateForm]);
-
- const handleDelete = useCallback(
- async (directiveId: string) => {
- const ok = await removeDirective(directiveId);
- if (ok && id === directiveId) {
- navigate("/directives");
- }
- },
- [removeDirective, id, navigate]
- );
-
- const handleStart = useCallback(
- async (directiveId: string) => {
- const ok = await startDirective(directiveId);
- if (ok) {
- const updated = await fetchDirective(directiveId);
- if (updated) {
- setSelectedDirective(updated);
- }
- }
- },
- [startDirective, fetchDirective]
- );
-
- const handleRefresh = useCallback(
- (updated: DirectiveWithChains) => {
- setSelectedDirective(updated);
- },
- []
- );
-
- 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 */}
- {showCreateForm && (
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
- <div className="w-full max-w-lg max-h-[90vh] overflow-y-auto p-6 bg-[#0a1628] border border-[rgba(117,170,252,0.3)]">
- <h3 className="font-mono text-sm text-[#75aafc] uppercase mb-4">
- New Directive
- </h3>
- <div className="space-y-4">
- {/* Title */}
- <div>
- <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1">
- Title
- </label>
- <input
- type="text"
- placeholder="Directive title"
- value={createTitle}
- onChange={(e) => setCreateTitle(e.target.value)}
- className="w-full px-3 py-2 font-mono text-sm text-[#dbe7ff] bg-[#0d1b2d] border border-[#3f6fb3] focus:border-[#75aafc] outline-none"
- autoFocus
- />
- </div>
-
- {/* Goal */}
- <div>
- <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1">
- Goal
- </label>
- <textarea
- placeholder="What should be accomplished?"
- value={createGoal}
- onChange={(e) => setCreateGoal(e.target.value)}
- rows={3}
- className="w-full px-3 py-2 font-mono text-sm text-[#dbe7ff] bg-[#0d1b2d] border border-[#3f6fb3] focus:border-[#75aafc] outline-none resize-none"
- />
- </div>
-
- {/* Repository Configuration */}
- <div className="border-t border-[rgba(117,170,252,0.2)] pt-4">
- <label className="block font-mono text-xs text-[#75aafc] uppercase mb-3">
- Repository (optional)
- </label>
-
- {/* Repository type selector */}
- <div className="flex gap-2 mb-3">
- <button
- type="button"
- onClick={() => setRepoType("remote")}
- className={`flex-1 px-3 py-2 font-mono text-xs uppercase transition-colors ${
- repoType === "remote"
- ? "bg-[#0f3c78] text-[#dbe7ff] border border-[#75aafc]"
- : "bg-[#0d1b2d] text-[#8b949e] border border-[#3f6fb3] hover:border-[#75aafc]"
- }`}
- >
- Remote
- </button>
- <button
- type="button"
- onClick={() => setRepoType("local")}
- className={`flex-1 px-3 py-2 font-mono text-xs uppercase transition-colors ${
- repoType === "local"
- ? "bg-[#0f3c78] text-[#dbe7ff] border border-[#75aafc]"
- : "bg-[#0d1b2d] text-[#8b949e] border border-[#3f6fb3] hover:border-[#75aafc]"
- }`}
- >
- Local
- </button>
- </div>
-
- {/* Repository suggestions */}
- {showRepoSuggestions && repoSuggestions.length > 0 && (
- <div className="mb-3">
- <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1">
- Recent Repositories
- </label>
- <div className="border border-[rgba(117,170,252,0.2)] bg-[#0a1525] max-h-32 overflow-y-auto">
- {repoSuggestions.map((suggestion) => (
- <button
- key={suggestion.id}
- type="button"
- onClick={() => applyRepoSuggestion(suggestion)}
- 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="flex items-center justify-between">
- <span className="text-[#9bc3ff] truncate">{suggestion.name}</span>
- <span className="text-[10px] text-[#556677] ml-2">
- {suggestion.useCount}&times;
- </span>
- </div>
- <div className="text-[10px] text-[#556677] truncate">
- {repoType === "local" ? suggestion.localPath : suggestion.repositoryUrl}
- </div>
- </button>
- ))}
- </div>
- </div>
- )}
-
- {/* Repository URL (for remote) */}
- {repoType === "remote" && (
- <div>
- <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1">
- Repository URL
- </label>
- <input
- type="text"
- value={repoUrl}
- onChange={(e) => setRepoUrl(e.target.value)}
- placeholder="https://github.com/user/repo.git"
- className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]"
- />
- </div>
- )}
-
- {/* Repository path (for local) */}
- {repoType === "local" && (
- <div>
- <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1">
- Local Path
- </label>
- <DirectoryInput
- value={repoPath}
- onChange={setRepoPath}
- suggestions={suggestedDirectories}
- placeholder="/path/to/repository"
- repoUrl={repoUrl || null}
- />
- </div>
- )}
- </div>
-
- {/* Actions */}
- <div className="flex gap-2 justify-end pt-2">
- <button
- onClick={resetCreateForm}
- className="px-4 py-2 font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors"
- >
- Cancel
- </button>
- <button
- onClick={handleCreate}
- disabled={!createTitle.trim() || !createGoal.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>
- )}
-
- <div className="flex-1 grid grid-cols-[350px_1fr] gap-4 min-h-0">
- {/* Directive list */}
- <DirectiveList
- directives={directives}
- loading={loading}
- onSelect={handleSelect}
- onCreate={() => setShowCreateForm(true)}
- onDelete={(d) => handleDelete(d.id)}
- selectedId={id}
- />
-
- {/* Directive detail or empty state */}
- {detailLoading ? (
- <div className="panel h-full flex items-center justify-center">
- <p className="text-[#7788aa] font-mono text-sm">Loading directive...</p>
- </div>
- ) : selectedDirective ? (
- <DirectiveDetail
- directive={selectedDirective}
- onBack={handleBack}
- onDelete={handleDelete}
- onStart={handleStart}
- onRefresh={handleRefresh}
- />
- ) : (
- <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={() => setShowCreateForm(true)}
- 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>
- );
-}