summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-07 18:27:54 +0000
committersoryu <soryu@soryu.co>2026-02-07 18:27:54 +0000
commit97e21c8296ec5f91912d56980ebf3b18a1ca3507 (patch)
tree3650e2eb62ab5b387006563ce64139aa7688da5f /makima/frontend/src/components
parent8f757f561eeb397aaea70d7c10d41445cc5e50b5 (diff)
downloadsoryu-97e21c8296ec5f91912d56980ebf3b18a1ca3507.tar.gz
soryu-97e21c8296ec5f91912d56980ebf3b18a1ca3507.zip
Add directive monitor contracts
Diffstat (limited to 'makima/frontend/src/components')
-rw-r--r--makima/frontend/src/components/directives/DirectiveContractsTab.tsx131
-rw-r--r--makima/frontend/src/components/directives/DirectiveDetail.tsx393
-rw-r--r--makima/frontend/src/components/directives/StepDiagram.tsx152
3 files changed, 571 insertions, 105 deletions
diff --git a/makima/frontend/src/components/directives/DirectiveContractsTab.tsx b/makima/frontend/src/components/directives/DirectiveContractsTab.tsx
new file mode 100644
index 0000000..59ebfc8
--- /dev/null
+++ b/makima/frontend/src/components/directives/DirectiveContractsTab.tsx
@@ -0,0 +1,131 @@
+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,
+ });
+ }
+ }
+ }
+
+ 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
index 094cdf2..95dc7cc 100644
--- a/makima/frontend/src/components/directives/DirectiveDetail.tsx
+++ b/makima/frontend/src/components/directives/DirectiveDetail.tsx
@@ -1,12 +1,16 @@
-import { useEffect, useRef } from "react";
+import { useState, useEffect, useRef } from "react";
import { useNavigate } from "react-router";
import type {
DirectiveWithChains,
DirectiveStatus,
ChainWithSteps,
ChainStep,
+ 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;
@@ -16,6 +20,8 @@ interface DirectiveDetailProps {
onRefresh?: (updated: DirectiveWithChains) => void;
}
+type Tab = "overview" | "chain" | "contracts";
+
const statusColors: Record<DirectiveStatus, string> = {
draft: "text-[#888]",
planning: "text-yellow-400",
@@ -36,17 +42,18 @@ const stepStatusColors: Record<string, string> = {
const stepStatusIcons: Record<string, string> = {
pending: "\u25CB", // ○
running: "\u25D4", // ◔
- passed: "\u25CF", // ●
- failed: "\u2715", // ✕
+ passed: "\u25CF", // ●
+ failed: "\u2715", // ✕
};
function StepRow({ step }: { step: ChainStep }) {
const navigate = useNavigate();
const color = stepStatusColors[step.status] || "text-[#888]";
const icon = stepStatusIcons[step.status] || "\u25CB";
+ const summary = step.contractSummary;
return (
- <div className="flex items-start gap-2 py-1 px-2 hover:bg-[rgba(117,170,252,0.05)]">
+ <div className="flex items-start gap-2 py-1.5 px-2 hover:bg-[rgba(117,170,252,0.05)]">
<span className={`font-mono text-[11px] ${color} mt-px`}>{icon}</span>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
@@ -57,21 +64,41 @@ function StepRow({ step }: { step: ChainStep }) {
{step.status}
</span>
</div>
+ {summary && (
+ <div className="flex items-center gap-2 mt-0.5">
+ <PhaseProgressBarCompact
+ currentPhase={summary.phase as ContractPhase}
+ />
+ <span className="font-mono text-[9px] text-[#7788aa]">
+ {summary.tasksDone}/{summary.taskCount} tasks
+ </span>
+ {step.contractId && (
+ <button
+ onClick={(e) => {
+ e.stopPropagation();
+ navigate(`/contracts/${step.contractId}`);
+ }}
+ className="font-mono text-[9px] text-[#75aafc] hover:text-white transition-colors"
+ >
+ contract &rarr;
+ </button>
+ )}
+ </div>
+ )}
+ {!summary && step.contractId && (
+ <button
+ onClick={() => navigate(`/contracts/${step.contractId}`)}
+ className="font-mono text-[9px] text-[#75aafc] hover:text-white transition-colors mt-0.5"
+ >
+ contract &rarr;
+ </button>
+ )}
{step.description && (
- <p className="font-mono text-[10px] text-[#7788aa] truncate">
+ <p className="font-mono text-[10px] text-[#7788aa] truncate mt-0.5">
{step.description}
</p>
)}
</div>
- {step.contractId && (
- <button
- onClick={() => navigate(`/contracts/${step.contractId}`)}
- className="font-mono text-[9px] text-[#75aafc] hover:text-white transition-colors shrink-0"
- title="View contract"
- >
- contract &rarr;
- </button>
- )}
</div>
);
}
@@ -84,7 +111,9 @@ function ChainCard({ chainWithSteps }: { chainWithSteps: ChainWithSteps }) {
<div className="border border-dashed border-[rgba(117,170,252,0.25)] bg-[rgba(117,170,252,0.03)]">
<div className="p-3">
<div className="flex items-center justify-between mb-1">
- <span className="font-mono text-xs text-[#dbe7ff]">{chain.name}</span>
+ <span className="font-mono text-xs text-[#dbe7ff]">
+ {chain.name}
+ </span>
<span className="font-mono text-[10px] text-[#7788aa] uppercase">
gen {chain.generation} &middot; {chain.status}
</span>
@@ -102,7 +131,9 @@ function ChainCard({ chainWithSteps }: { chainWithSteps: ChainWithSteps }) {
<span className="text-red-400">{chain.failedSteps} failed</span>
)}
{chain.currentConfidence != null && (
- <span>confidence: {(chain.currentConfidence * 100).toFixed(0)}%</span>
+ <span>
+ confidence: {(chain.currentConfidence * 100).toFixed(0)}%
+ </span>
)}
</div>
</div>
@@ -151,6 +182,7 @@ export function DirectiveDetail({
onRefresh,
}: DirectiveDetailProps) {
const navigate = useNavigate();
+ const [activeTab, setActiveTab] = useState<Tab>("overview");
// Auto-poll when directive is in an active state
const isLive =
@@ -185,6 +217,31 @@ export function DirectiveDetail({
};
}, [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 */}
@@ -235,109 +292,235 @@ export function DirectiveDetail({
</h2>
</div>
- {/* Content */}
- <div className="flex-1 overflow-y-auto p-4 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>
- <button
- onClick={() =>
- navigate(`/contracts/${directive.orchestratorContractId}`)
+ {/* 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]"
}
- className="font-mono text-[11px] text-[#75aafc] hover:text-white transition-colors"
- >
- {directive.orchestratorContractId.slice(0, 8)}... &rarr;
- </button>
- {directive.status === "planning" && (
- <span className="font-mono text-[9px] text-yellow-400 animate-pulse">
- planning in progress
+ `}
+ >
+ {tab.label}
+ {tab.count != null && tab.count > 0 && (
+ <span className="ml-1 text-[10px] text-[#7788aa]">
+ ({tab.count})
</span>
)}
- </div>
- )}
+ </button>
+ ))}
+ </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>
+ {/* 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>
+ )}
- {/* Config */}
- <div className="grid grid-cols-2 gap-2">
- <div>
- <span className="font-mono text-[10px] text-[#7788aa] uppercase">
- Autonomy
- </span>
- <div className="font-mono text-xs text-[#dbe7ff]">
- {directive.autonomyLevel}
+ {/* 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>
- </div>
- <div>
- <span className="font-mono text-[10px] text-[#7788aa] uppercase">
- Chains
- </span>
- <div className="font-mono text-xs text-[#dbe7ff]">
- {directive.chainGenerationCount} generated
+
+ {/* 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>
- </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)}
+
+ {/* 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>
- </div>
- {directive.repositoryUrl && (
+
+ {/* 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>
- <span className="font-mono text-[10px] text-[#7788aa] uppercase">
- Repository
- </span>
- <div className="font-mono text-xs text-[#dbe7ff] truncate">
- {directive.repositoryUrl}
+ <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>
+ </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}
- />
+ {activeTab === "chain" && (
+ <div className="space-y-4">
+ {/* Step diagram */}
+ {directive.chains.length > 0 && (
+ <div>
+ <h4 className="font-mono text-[10px] text-[#75aafc] uppercase tracking-wider mb-2">
+ Step Dependencies
+ </h4>
+ <StepDiagram
+ steps={directive.chains.flatMap((c) => c.steps)}
+ />
+ </div>
+ )}
- {/* Chains */}
- <div>
- <h4 className="font-mono text-[10px] text-[#75aafc] uppercase tracking-wider mb-2">
- Chains ({directive.chains.length})
- </h4>
- {directive.chains.length === 0 ? (
- <p className="font-mono text-xs text-[#7788aa]">
- {directive.status === "planning"
- ? "Planning in progress... chains will appear when the planner completes."
- : "No chains yet. Chains are created during planning."}
- </p>
- ) : (
- <div className="space-y-2">
- {directive.chains.map((cws) => (
- <ChainCard key={cws.id} chainWithSteps={cws} />
- ))}
+ {/* Chain cards */}
+ <div>
+ <h4 className="font-mono text-[10px] text-[#75aafc] uppercase tracking-wider mb-2">
+ Chains ({directive.chains.length})
+ </h4>
+ {directive.chains.length === 0 ? (
+ <p className="font-mono text-xs text-[#7788aa]">
+ {directive.status === "planning"
+ ? "Planning in progress... chains will appear when the planner completes."
+ : directive.status === "draft"
+ ? "No chains yet. Start the directive to begin planning."
+ : "No chains created for this directive."}
+ </p>
+ ) : (
+ <div className="space-y-2">
+ {directive.chains.map((cws) => (
+ <ChainCard key={cws.id} chainWithSteps={cws} />
+ ))}
+ </div>
+ )}
</div>
- )}
- </div>
+ </div>
+ )}
+
+ {activeTab === "contracts" && (
+ <DirectiveContractsTab directive={directive} />
+ )}
</div>
</div>
);
diff --git a/makima/frontend/src/components/directives/StepDiagram.tsx b/makima/frontend/src/components/directives/StepDiagram.tsx
new file mode 100644
index 0000000..5c65ae1
--- /dev/null
+++ b/makima/frontend/src/components/directives/StepDiagram.tsx
@@ -0,0 +1,152 @@
+import { useNavigate } from "react-router";
+import type { ChainStep, ContractPhase } from "../../lib/api";
+import { PhaseProgressBarCompact } from "../contracts/PhaseProgressBar";
+
+interface StepDiagramProps {
+ steps: ChainStep[];
+}
+
+const statusBorderColors: Record<string, string> = {
+ pending: "border-[#555]",
+ running: "border-yellow-400",
+ passed: "border-green-400",
+ failed: "border-red-400",
+};
+
+const statusDotColors: Record<string, string> = {
+ pending: "bg-[#555]",
+ running: "bg-yellow-400",
+ passed: "bg-green-400",
+ failed: "bg-red-400",
+};
+
+/**
+ * Assign depth to each step via topological sort.
+ * Steps with no dependsOn = depth 0. Steps depending only on depth-0 = depth 1. Etc.
+ */
+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;
+}
+
+export function StepDiagram({ steps }: StepDiagramProps) {
+ const navigate = useNavigate();
+
+ 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
+ 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)
+ );
+ }
+
+ // Build position map for connectors
+ const stepPositions = new Map<string, { level: number; index: number }>();
+ levels.forEach((level, li) => {
+ level.forEach((step, si) => {
+ stepPositions.set(step.id, { level: li, index: si });
+ });
+ });
+
+ return (
+ <div className="space-y-3">
+ {levels.map((level, li) => (
+ <div key={li} className="flex items-start gap-2 flex-wrap">
+ {li > 0 && (
+ <div className="w-full flex justify-center mb-1">
+ <div className="w-px h-3 bg-[rgba(117,170,252,0.3)]" />
+ </div>
+ )}
+ {level.map((step) => {
+ const borderColor =
+ statusBorderColors[step.status] || "border-[#555]";
+ const dotColor = statusDotColors[step.status] || "bg-[#555]";
+ const summary = step.contractSummary;
+ const hasContract = !!step.contractId;
+
+ return (
+ <div
+ key={step.id}
+ className={`
+ border ${borderColor} bg-[rgba(0,0,0,0.2)] p-2 min-w-[180px] max-w-[220px]
+ ${hasContract ? "cursor-pointer hover:bg-[rgba(117,170,252,0.05)]" : ""}
+ transition-colors
+ `}
+ onClick={() => {
+ if (hasContract) navigate(`/contracts/${step.contractId}`);
+ }}
+ title={hasContract ? "View contract" : undefined}
+ >
+ <div className="flex items-center gap-1.5 mb-1">
+ <div className={`w-1.5 h-1.5 rounded-full ${dotColor}`} />
+ <span className="font-mono text-[11px] text-[#dbe7ff] truncate flex-1">
+ {step.name}
+ </span>
+ {hasContract && (
+ <span className="font-mono text-[9px] text-[#75aafc] shrink-0">
+ &rarr;
+ </span>
+ )}
+ </div>
+ {summary && (
+ <>
+ <div className="mb-1">
+ <PhaseProgressBarCompact
+ currentPhase={summary.phase as ContractPhase}
+ />
+ </div>
+ <div className="font-mono text-[9px] text-[#7788aa]">
+ {summary.tasksDone}/{summary.taskCount} tasks
+ {summary.tasksRunning > 0 && (
+ <span className="text-yellow-400 ml-1">
+ {summary.tasksRunning} running
+ </span>
+ )}
+ {summary.tasksFailed > 0 && (
+ <span className="text-red-400 ml-1">
+ {summary.tasksFailed} failed
+ </span>
+ )}
+ </div>
+ </>
+ )}
+ </div>
+ );
+ })}
+ </div>
+ ))}
+ </div>
+ );
+}