summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-14 21:29:26 +0000
committerGitHub <noreply@github.com>2026-02-14 21:29:26 +0000
commit9aadbc7958d39d181c0dd0600e2b7c30bb6c391a (patch)
treeef8bed9718c39041191b58a284ee31f5d8d32521 /makima/frontend/src/components
parentc1e55ce4fec79f9909b957f86bd7fa8b76939746 (diff)
downloadsoryu-9aadbc7958d39d181c0dd0600e2b7c30bb6c391a.tar.gz
soryu-9aadbc7958d39d181c0dd0600e2b7c30bb6c391a.zip
Makima system improvements: Orders, directive questions, PR creation fix, bug fixes (#62)
* feat: soryu-co/soryu - makima: Fix directive goal update bug - stale closure issue * WIP: heartbeat checkpoint * WIP: heartbeat checkpoint * feat: soryu-co/soryu - makima: Create Orders database schema and backend API * feat: soryu-co/soryu - makima: Fix task Claude instance not receiving user inputs from input box * WIP: heartbeat checkpoint * feat: soryu-co/soryu - makima: Build Orders frontend page replacing the Board page * WIP: heartbeat checkpoint * WIP: heartbeat checkpoint * feat: soryu-co/soryu - makima: Fix directive PR creation system
Diffstat (limited to 'makima/frontend/src/components')
-rw-r--r--makima/frontend/src/components/NavStrip.tsx2
-rw-r--r--makima/frontend/src/components/directives/DirectiveDetail.tsx139
-rw-r--r--makima/frontend/src/components/mesh/TaskOutput.tsx5
-rw-r--r--makima/frontend/src/components/orders/OrderDetail.tsx530
-rw-r--r--makima/frontend/src/components/orders/OrderList.tsx188
-rw-r--r--makima/frontend/src/components/workflow/PhaseColumn.tsx126
-rw-r--r--makima/frontend/src/components/workflow/WorkflowBoard.tsx98
-rw-r--r--makima/frontend/src/components/workflow/WorkflowContractCard.tsx76
8 files changed, 860 insertions, 304 deletions
diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx
index 9bb7777..5aba6a3 100644
--- a/makima/frontend/src/components/NavStrip.tsx
+++ b/makima/frontend/src/components/NavStrip.tsx
@@ -12,7 +12,7 @@ const NAV_LINKS: NavLink[] = [
{ label: "Listen", href: "/listen" },
{ label: "Directives", href: "/directives", requiresAuth: true },
{ label: "Contracts", href: "/contracts", requiresAuth: true },
- { label: "Board", href: "/workflow", requiresAuth: true },
+ { label: "Orders", href: "/orders", requiresAuth: true },
{ label: "Mesh", href: "/mesh", requiresAuth: true },
{ label: "History", href: "/history", requiresAuth: true },
];
diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx
index b73463d..e278939 100644
--- a/makima/frontend/src/components/directives/DirectiveDetail.tsx
+++ b/makima/frontend/src/components/directives/DirectiveDetail.tsx
@@ -1,8 +1,9 @@
import { useState, useMemo, useEffect, useRef } from "react";
-import type { DirectiveWithSteps, DirectiveStatus } from "../../lib/api";
+import type { DirectiveWithSteps, DirectiveStatus, UpdateDirectiveRequest } from "../../lib/api";
import { DirectiveDAG } from "./DirectiveDAG";
import { DirectiveLogStream } from "./DirectiveLogStream";
import { useMultiTaskSubscription } from "../../hooks/useMultiTaskSubscription";
+import { useSupervisorQuestions } from "../../contexts/SupervisorQuestionsContext";
const STATUS_BADGE: Record<DirectiveStatus, { color: string; label: string }> = {
draft: { color: "text-[#7788aa] border-[#2a3a5a]", label: "DRAFT" },
@@ -21,6 +22,7 @@ interface DirectiveDetailProps {
onFailStep: (stepId: string) => void;
onSkipStep: (stepId: string) => void;
onUpdateGoal: (goal: string) => void;
+ onUpdate: (req: UpdateDirectiveRequest) => void;
onDelete: () => void;
onRefresh: () => void;
onCleanupTasks: () => void;
@@ -35,6 +37,7 @@ export function DirectiveDetail({
onFailStep,
onSkipStep,
onUpdateGoal,
+ onUpdate,
onDelete,
onRefresh,
onCleanupTasks,
@@ -42,6 +45,12 @@ export function DirectiveDetail({
const [editingGoal, setEditingGoal] = useState(false);
const [goalText, setGoalText] = useState(directive.goal);
const [visibleTaskIds, setVisibleTaskIds] = useState<Set<string> | null>(null);
+
+ // Sync goalText and reset editing state when directive changes
+ useEffect(() => {
+ setGoalText(directive.goal);
+ setEditingGoal(false);
+ }, [directive.id, directive.goal]);
const [searchQuery, setSearchQuery] = useState("");
const [isLogCollapsed, setIsLogCollapsed] = useState(true);
const prevHadRunningRef = useRef(false);
@@ -53,6 +62,24 @@ export function DirectiveDetail({
const terminalStatuses = new Set(["completed", "failed", "skipped"]);
const hasTerminalTasks = directive.steps.some((s) => s.taskId && terminalStatuses.has(s.status));
+ // Get pending questions for this directive's tasks
+ const { pendingQuestions, submitAnswer } = useSupervisorQuestions();
+ const directiveTaskIds = useMemo(() => {
+ const ids = new Set<string>();
+ if (directive.orchestratorTaskId) ids.add(directive.orchestratorTaskId);
+ for (const step of directive.steps) {
+ if (step.taskId) ids.add(step.taskId);
+ }
+ return ids;
+ }, [directive.orchestratorTaskId, directive.steps]);
+
+ const directiveQuestions = useMemo(
+ () => pendingQuestions.filter((q) =>
+ q.directiveId === directive.id || directiveTaskIds.has(q.taskId)
+ ),
+ [pendingQuestions, directive.id, directiveTaskIds]
+ );
+
// Build task map from directive steps and orchestrator
// Derive a stable key from the actual task IDs to avoid recreating the map on every poll
const taskMapKey = useMemo(() => {
@@ -149,6 +176,26 @@ export function DirectiveDetail({
</div>
)}
+ {/* Reconcile mode toggle */}
+ <div className="flex items-center gap-2 mb-2">
+ <button
+ type="button"
+ onClick={() => onUpdate({ reconcileMode: !directive.reconcileMode })}
+ className={`text-[10px] font-mono border rounded px-2 py-0.5 transition-colors ${
+ directive.reconcileMode
+ ? "text-amber-400 border-amber-800 bg-amber-900/20"
+ : "text-[#556677] border-[#2a3a5a] hover:text-[#7788aa]"
+ }`}
+ >
+ {directive.reconcileMode ? "Reconcile: ON" : "Reconcile: OFF"}
+ </button>
+ <span className="text-[9px] font-mono text-[#445566]">
+ {directive.reconcileMode
+ ? "Questions pause execution"
+ : "Questions timeout after 30s"}
+ </span>
+ </div>
+
{/* Orchestrator planning indicator */}
{directive.orchestratorTaskId && (
<div className="flex items-center gap-2 mb-2 px-2 py-1.5 bg-[#1a1a30] border border-[rgba(117,170,252,0.2)] rounded">
@@ -199,6 +246,20 @@ export function DirectiveDetail({
</div>
)}
+ {/* Pending Questions */}
+ {directiveQuestions.length > 0 && (
+ <div className="mb-2 space-y-2">
+ {directiveQuestions.map((q) => (
+ <DirectiveQuestionCard
+ key={q.questionId}
+ question={q}
+ taskName={taskMap.get(q.taskId) || "Task"}
+ onAnswer={(response) => submitAnswer(q.questionId, response)}
+ />
+ ))}
+ </div>
+ )}
+
{/* Controls */}
<div className="flex flex-wrap gap-2">
{(directive.status === "draft" || directive.status === "paused") && (
@@ -235,7 +296,7 @@ export function DirectiveDetail({
</span>
<button
type="button"
- onClick={() => setEditingGoal(true)}
+ onClick={() => { setGoalText(directive.goal); setEditingGoal(true); }}
className="text-[10px] font-mono text-[#75aafc] hover:text-white border border-[rgba(117,170,252,0.3)] rounded px-2 py-1"
>
Update Goal
@@ -342,3 +403,77 @@ export function DirectiveDetail({
</div>
);
}
+
+/** Inline question card for directive pending questions */
+function DirectiveQuestionCard({
+ question,
+ taskName,
+ onAnswer,
+}: {
+ question: { questionId: string; question: string; choices: string[]; context: string | null };
+ taskName: string;
+ onAnswer: (response: string) => void;
+}) {
+ const [customResponse, setCustomResponse] = useState("");
+ const [submitting, setSubmitting] = useState(false);
+
+ const handleSubmit = async (response: string) => {
+ setSubmitting(true);
+ await onAnswer(response);
+ setSubmitting(false);
+ };
+
+ return (
+ <div className="px-2 py-2 bg-[#1a1020] border border-purple-900/50 rounded">
+ <div className="flex items-center gap-1.5 mb-1">
+ <span className="inline-block w-2 h-2 rounded-full bg-purple-400 animate-pulse" />
+ <span className="text-[9px] font-mono text-purple-400 uppercase">
+ Question from {taskName}
+ </span>
+ </div>
+ <p className="text-[11px] font-mono text-white mb-1.5">{question.question}</p>
+ {question.context && (
+ <p className="text-[9px] font-mono text-[#556677] mb-1.5">{question.context}</p>
+ )}
+ {question.choices.length > 0 ? (
+ <div className="flex flex-wrap gap-1">
+ {question.choices.map((choice) => (
+ <button
+ key={choice}
+ type="button"
+ disabled={submitting}
+ onClick={() => handleSubmit(choice)}
+ className="text-[10px] font-mono text-purple-300 hover:text-white border border-purple-800 hover:border-purple-600 rounded px-2 py-0.5 disabled:opacity-50"
+ >
+ {choice}
+ </button>
+ ))}
+ </div>
+ ) : (
+ <div className="flex gap-1">
+ <input
+ type="text"
+ value={customResponse}
+ onChange={(e) => setCustomResponse(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" && customResponse.trim()) {
+ handleSubmit(customResponse.trim());
+ }
+ }}
+ placeholder="Type your answer..."
+ className="flex-1 bg-[#0a0618] border border-purple-900/50 rounded px-2 py-0.5 text-[10px] font-mono text-white placeholder:text-[#445566]"
+ disabled={submitting}
+ />
+ <button
+ type="button"
+ disabled={submitting || !customResponse.trim()}
+ onClick={() => handleSubmit(customResponse.trim())}
+ className="text-[10px] font-mono text-purple-300 hover:text-white border border-purple-800 rounded px-2 py-0.5 disabled:opacity-50"
+ >
+ Send
+ </button>
+ </div>
+ )}
+ </div>
+ );
+}
diff --git a/makima/frontend/src/components/mesh/TaskOutput.tsx b/makima/frontend/src/components/mesh/TaskOutput.tsx
index f49c366..2db4250 100644
--- a/makima/frontend/src/components/mesh/TaskOutput.tsx
+++ b/makima/frontend/src/components/mesh/TaskOutput.tsx
@@ -77,7 +77,10 @@ export function TaskOutput({
setInputValue("");
inputRef.current?.focus();
} catch (err) {
- setInputError(err instanceof Error ? err.message : "Failed to send input");
+ const errorMsg = err instanceof Error ? err.message : "Failed to send input";
+ setInputError(errorMsg);
+ // Auto-dismiss error after 5 seconds
+ setTimeout(() => setInputError(null), 5000);
} finally {
setSendingInput(false);
}
diff --git a/makima/frontend/src/components/orders/OrderDetail.tsx b/makima/frontend/src/components/orders/OrderDetail.tsx
new file mode 100644
index 0000000..7f8a95d
--- /dev/null
+++ b/makima/frontend/src/components/orders/OrderDetail.tsx
@@ -0,0 +1,530 @@
+import { useState } from "react";
+import type {
+ Order,
+ OrderStatus,
+ OrderPriority,
+ OrderType,
+ UpdateOrderRequest,
+ DirectiveSummary,
+} from "../../lib/api";
+
+const STATUS_BADGE: Record<OrderStatus, { color: string; label: string }> = {
+ open: { color: "text-[#75aafc] border-[rgba(117,170,252,0.4)]", label: "OPEN" },
+ in_progress: { color: "text-yellow-400 border-yellow-800", label: "IN PROGRESS" },
+ done: { color: "text-emerald-400 border-emerald-800", label: "DONE" },
+ archived: { color: "text-[#556677] border-[#2a3a5a]", label: "ARCHIVED" },
+};
+
+const PRIORITY_OPTIONS: { value: OrderPriority; color: string; label: string }[] = [
+ { value: "critical", color: "text-red-400 border-red-800", label: "Critical" },
+ { value: "high", color: "text-orange-400 border-orange-800", label: "High" },
+ { value: "medium", color: "text-yellow-400 border-yellow-800", label: "Medium" },
+ { value: "low", color: "text-[#75aafc] border-[rgba(117,170,252,0.4)]", label: "Low" },
+ { value: "none", color: "text-[#556677] border-[#2a3a5a]", label: "None" },
+];
+
+const TYPE_OPTIONS: { value: OrderType; color: string; label: string }[] = [
+ { value: "feature", color: "text-[#75aafc]", label: "Feature" },
+ { value: "bug", color: "text-red-400", label: "Bug" },
+ { value: "spike", color: "text-yellow-400", label: "Spike" },
+ { value: "chore", color: "text-[#7788aa]", label: "Chore" },
+ { value: "improvement", color: "text-emerald-400", label: "Improvement" },
+];
+
+const STATUS_OPTIONS: OrderStatus[] = ["open", "in_progress", "done", "archived"];
+
+interface OrderDetailProps {
+ order: Order;
+ directives: DirectiveSummary[];
+ onUpdate: (req: UpdateOrderRequest) => Promise<void>;
+ onDelete: () => void;
+ onLinkDirective: (directiveId: string) => Promise<void>;
+ onLinkContract: (contractId: string) => Promise<void>;
+ onConvertToStep: (directiveId: string) => Promise<void>;
+ onRefresh: () => void;
+}
+
+export function OrderDetail({
+ order,
+ directives,
+ onUpdate,
+ onDelete,
+ onLinkDirective,
+ onLinkContract,
+ onConvertToStep,
+ onRefresh,
+}: OrderDetailProps) {
+ const [editingTitle, setEditingTitle] = useState(false);
+ const [titleText, setTitleText] = useState(order.title);
+ const [editingDesc, setEditingDesc] = useState(false);
+ const [descText, setDescText] = useState(order.description || "");
+ const [editingLabels, setEditingLabels] = useState(false);
+ const [labelsText, setLabelsText] = useState(order.labels.join(", "));
+ const [showLinkDirective, setShowLinkDirective] = useState(false);
+ const [showLinkContract, setShowLinkContract] = useState(false);
+ const [contractIdInput, setContractIdInput] = useState("");
+ const [showConvertToStep, setShowConvertToStep] = useState(false);
+
+ const badge = STATUS_BADGE[order.status] || STATUS_BADGE.open;
+ const currentPriority = PRIORITY_OPTIONS.find((p) => p.value === order.priority) || PRIORITY_OPTIONS[4];
+ const currentType = TYPE_OPTIONS.find((t) => t.value === order.orderType) || TYPE_OPTIONS[0];
+
+ const handleTitleSave = async () => {
+ if (titleText.trim() && titleText !== order.title) {
+ await onUpdate({ title: titleText.trim() });
+ }
+ setEditingTitle(false);
+ };
+
+ const handleDescSave = async () => {
+ const newDesc = descText.trim() || null;
+ if (newDesc !== order.description) {
+ await onUpdate({ description: newDesc });
+ }
+ setEditingDesc(false);
+ };
+
+ const handleLabelsSave = async () => {
+ const newLabels = labelsText
+ .split(",")
+ .map((l) => l.trim())
+ .filter((l) => l.length > 0);
+ await onUpdate({ labels: newLabels });
+ setEditingLabels(false);
+ };
+
+ const handleStatusChange = async (status: OrderStatus) => {
+ await onUpdate({ status });
+ };
+
+ const handlePriorityChange = async (priority: OrderPriority) => {
+ await onUpdate({ priority });
+ };
+
+ const handleTypeChange = async (orderType: OrderType) => {
+ await onUpdate({ orderType });
+ };
+
+ const handleLinkDirective = async (directiveId: string) => {
+ await onLinkDirective(directiveId);
+ setShowLinkDirective(false);
+ };
+
+ const handleLinkContract = async () => {
+ if (!contractIdInput.trim()) return;
+ await onLinkContract(contractIdInput.trim());
+ setContractIdInput("");
+ setShowLinkContract(false);
+ };
+
+ const handleConvertToStep = async (directiveId: string) => {
+ await onConvertToStep(directiveId);
+ setShowConvertToStep(false);
+ };
+
+ return (
+ <div className="flex flex-col h-full overflow-y-auto">
+ {/* Header */}
+ <div className="px-4 py-3 border-b border-dashed border-[rgba(117,170,252,0.2)]">
+ <div className="flex items-center justify-between mb-2">
+ {editingTitle ? (
+ <div className="flex-1 flex items-center gap-2 pr-2">
+ <input
+ value={titleText}
+ onChange={(e) => setTitleText(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") handleTitleSave();
+ if (e.key === "Escape") setEditingTitle(false);
+ }}
+ autoFocus
+ className="flex-1 bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1 text-[14px] font-mono text-white"
+ />
+ <button
+ type="button"
+ onClick={handleTitleSave}
+ className="text-[10px] font-mono text-emerald-400 hover:text-emerald-300"
+ >
+ [save]
+ </button>
+ <button
+ type="button"
+ onClick={() => setEditingTitle(false)}
+ className="text-[10px] font-mono text-[#556677] hover:text-white"
+ >
+ [cancel]
+ </button>
+ </div>
+ ) : (
+ <h2
+ className="text-[14px] font-mono text-white font-medium truncate pr-2 cursor-pointer hover:text-[#9bc3ff]"
+ onClick={() => {
+ setTitleText(order.title);
+ setEditingTitle(true);
+ }}
+ >
+ {order.title}
+ </h2>
+ )}
+ <div className="flex items-center gap-2 shrink-0">
+ <span
+ className={`text-[10px] font-mono ${badge.color} border rounded px-2 py-0.5`}
+ >
+ {badge.label}
+ </span>
+ <button
+ type="button"
+ onClick={onRefresh}
+ className="text-[10px] font-mono text-[#7788aa] hover:text-white"
+ title="Refresh"
+ >
+ [refresh]
+ </button>
+ </div>
+ </div>
+
+ {/* Type + Priority inline */}
+ <div className="flex items-center gap-3 mb-2">
+ <span className={`text-[10px] font-mono ${currentType.color}`}>
+ {currentType.label}
+ </span>
+ <span className="text-[10px] font-mono text-[#2a3a5a]">/</span>
+ <span className={`text-[10px] font-mono ${currentPriority.color} border rounded px-1.5 py-0.5`}>
+ {currentPriority.label}
+ </span>
+ </div>
+
+ {/* Linked entities */}
+ {order.directiveId && (
+ <div className="text-[10px] font-mono text-[#556677] mb-1 truncate">
+ Directive: <a href={`/directives/${order.directiveId}`} className="text-[#75aafc] hover:text-white underline">{order.directiveId.slice(0, 8)}...</a>
+ </div>
+ )}
+ {order.contractId && (
+ <div className="text-[10px] font-mono text-[#556677] mb-1 truncate">
+ Contract: <a href={`/contracts/${order.contractId}`} className="text-[#75aafc] hover:text-white underline">{order.contractId.slice(0, 8)}...</a>
+ </div>
+ )}
+ {order.directiveStepId && (
+ <div className="text-[10px] font-mono text-[#556677] mb-1 truncate">
+ Step: <span className="text-[#7788aa]">{order.directiveStepId.slice(0, 8)}...</span>
+ </div>
+ )}
+
+ {/* Controls */}
+ <div className="flex flex-wrap gap-2 mt-2">
+ <button
+ type="button"
+ onClick={onDelete}
+ className="text-[10px] font-mono text-red-400 hover:text-red-300 border border-red-800 rounded px-2 py-1 ml-auto"
+ >
+ Delete
+ </button>
+ </div>
+ </div>
+
+ {/* Status selector */}
+ <div className="px-4 py-3 border-b border-[rgba(117,170,252,0.1)]">
+ <div className="flex items-center justify-between mb-1.5">
+ <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide">
+ Status
+ </span>
+ </div>
+ <div className="flex gap-1.5 flex-wrap">
+ {STATUS_OPTIONS.map((s) => {
+ const sBadge = STATUS_BADGE[s];
+ return (
+ <button
+ key={s}
+ type="button"
+ onClick={() => handleStatusChange(s)}
+ className={`text-[10px] font-mono border rounded px-2 py-0.5 transition-colors ${
+ s === order.status
+ ? `${sBadge.color} bg-[rgba(117,170,252,0.1)]`
+ : "text-[#556677] border-[#2a3a5a] hover:text-[#7788aa]"
+ }`}
+ >
+ {sBadge.label}
+ </button>
+ );
+ })}
+ </div>
+ </div>
+
+ {/* Priority selector */}
+ <div className="px-4 py-3 border-b border-[rgba(117,170,252,0.1)]">
+ <div className="flex items-center justify-between mb-1.5">
+ <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide">
+ Priority
+ </span>
+ </div>
+ <div className="flex gap-1.5 flex-wrap">
+ {PRIORITY_OPTIONS.map((p) => (
+ <button
+ key={p.value}
+ type="button"
+ onClick={() => handlePriorityChange(p.value)}
+ className={`text-[10px] font-mono border rounded px-2 py-0.5 transition-colors ${
+ p.value === order.priority
+ ? `${p.color} bg-[rgba(117,170,252,0.1)]`
+ : "text-[#556677] border-[#2a3a5a] hover:text-[#7788aa]"
+ }`}
+ >
+ {p.label}
+ </button>
+ ))}
+ </div>
+ </div>
+
+ {/* Type selector */}
+ <div className="px-4 py-3 border-b border-[rgba(117,170,252,0.1)]">
+ <div className="flex items-center justify-between mb-1.5">
+ <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide">
+ Type
+ </span>
+ </div>
+ <div className="flex gap-1.5 flex-wrap">
+ {TYPE_OPTIONS.map((t) => (
+ <button
+ key={t.value}
+ type="button"
+ onClick={() => handleTypeChange(t.value)}
+ className={`text-[10px] font-mono border rounded px-2 py-0.5 transition-colors ${
+ t.value === order.orderType
+ ? `${t.color} border-current bg-[rgba(117,170,252,0.1)]`
+ : "text-[#556677] border-[#2a3a5a] hover:text-[#7788aa]"
+ }`}
+ >
+ {t.label}
+ </button>
+ ))}
+ </div>
+ </div>
+
+ {/* Description */}
+ <div className="px-4 py-3 border-b border-[rgba(117,170,252,0.1)]">
+ <div className="flex items-center justify-between mb-1">
+ <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide">
+ Description
+ </span>
+ {!editingDesc && (
+ <button
+ type="button"
+ onClick={() => {
+ setDescText(order.description || "");
+ setEditingDesc(true);
+ }}
+ className="text-[9px] font-mono text-[#556677] hover:text-[#75aafc]"
+ >
+ [edit]
+ </button>
+ )}
+ </div>
+ {editingDesc ? (
+ <div className="flex flex-col gap-1.5">
+ <textarea
+ value={descText}
+ onChange={(e) => setDescText(e.target.value)}
+ className="w-full bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1.5 text-[11px] font-mono text-white resize-y min-h-[80px]"
+ rows={4}
+ autoFocus
+ />
+ <div className="flex gap-1.5">
+ <button
+ type="button"
+ onClick={handleDescSave}
+ className="text-[10px] font-mono text-emerald-400 hover:text-emerald-300 border border-emerald-800 rounded px-2 py-0.5"
+ >
+ Save
+ </button>
+ <button
+ type="button"
+ onClick={() => setEditingDesc(false)}
+ className="text-[10px] font-mono text-[#7788aa] hover:text-white border border-[#2a3a5a] rounded px-2 py-0.5"
+ >
+ Cancel
+ </button>
+ </div>
+ </div>
+ ) : (
+ <p className="text-[11px] font-mono text-[#c0d0e0] whitespace-pre-wrap">
+ {order.description || <span className="text-[#556677] italic">No description</span>}
+ </p>
+ )}
+ </div>
+
+ {/* Labels */}
+ <div className="px-4 py-3 border-b border-[rgba(117,170,252,0.1)]">
+ <div className="flex items-center justify-between mb-1">
+ <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide">
+ Labels
+ </span>
+ {!editingLabels && (
+ <button
+ type="button"
+ onClick={() => {
+ setLabelsText(order.labels.join(", "));
+ setEditingLabels(true);
+ }}
+ className="text-[9px] font-mono text-[#556677] hover:text-[#75aafc]"
+ >
+ [edit]
+ </button>
+ )}
+ </div>
+ {editingLabels ? (
+ <div className="flex flex-col gap-1.5">
+ <input
+ value={labelsText}
+ onChange={(e) => setLabelsText(e.target.value)}
+ placeholder="label1, label2, ..."
+ className="w-full bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1.5 text-[11px] font-mono text-white"
+ autoFocus
+ />
+ <div className="flex gap-1.5">
+ <button
+ type="button"
+ onClick={handleLabelsSave}
+ className="text-[10px] font-mono text-emerald-400 hover:text-emerald-300 border border-emerald-800 rounded px-2 py-0.5"
+ >
+ Save
+ </button>
+ <button
+ type="button"
+ onClick={() => setEditingLabels(false)}
+ className="text-[10px] font-mono text-[#7788aa] hover:text-white border border-[#2a3a5a] rounded px-2 py-0.5"
+ >
+ Cancel
+ </button>
+ </div>
+ </div>
+ ) : (
+ <div className="flex gap-1 flex-wrap">
+ {order.labels.length > 0 ? (
+ order.labels.map((l) => (
+ <span
+ key={l}
+ className="text-[10px] font-mono text-[#9bc3ff] bg-[rgba(117,170,252,0.1)] border border-[rgba(117,170,252,0.2)] rounded px-1.5 py-0.5"
+ >
+ {l}
+ </span>
+ ))
+ ) : (
+ <span className="text-[10px] font-mono text-[#556677] italic">No labels</span>
+ )}
+ </div>
+ )}
+ </div>
+
+ {/* Actions */}
+ <div className="px-4 py-3 flex-1">
+ <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide block mb-2">
+ Actions
+ </span>
+
+ <div className="flex flex-col gap-2">
+ {/* Link to Directive */}
+ <div>
+ <button
+ type="button"
+ onClick={() => setShowLinkDirective(!showLinkDirective)}
+ className="text-[10px] font-mono text-[#75aafc] hover:text-white border border-[rgba(117,170,252,0.3)] rounded px-2 py-1 w-full text-left"
+ >
+ Link to Directive
+ </button>
+ {showLinkDirective && (
+ <div className="mt-1 border border-[rgba(117,170,252,0.2)] bg-[#0a1525] max-h-32 overflow-y-auto rounded">
+ {directives.length === 0 ? (
+ <div className="px-3 py-2 text-[10px] font-mono text-[#556677]">
+ No directives available
+ </div>
+ ) : (
+ directives.map((d) => (
+ <button
+ key={d.id}
+ type="button"
+ onClick={() => handleLinkDirective(d.id)}
+ className="w-full text-left px-3 py-1.5 text-[10px] font-mono text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.1)] border-b border-[rgba(117,170,252,0.1)] last:border-b-0"
+ >
+ {d.title}
+ </button>
+ ))
+ )}
+ </div>
+ )}
+ </div>
+
+ {/* Link to Contract */}
+ <div>
+ <button
+ type="button"
+ onClick={() => setShowLinkContract(!showLinkContract)}
+ className="text-[10px] font-mono text-[#75aafc] hover:text-white border border-[rgba(117,170,252,0.3)] rounded px-2 py-1 w-full text-left"
+ >
+ Link to Contract
+ </button>
+ {showLinkContract && (
+ <div className="mt-1 flex gap-1.5">
+ <input
+ value={contractIdInput}
+ onChange={(e) => setContractIdInput(e.target.value)}
+ placeholder="Contract ID..."
+ className="flex-1 bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1 text-[10px] font-mono text-white"
+ autoFocus
+ />
+ <button
+ type="button"
+ onClick={handleLinkContract}
+ disabled={!contractIdInput.trim()}
+ className="text-[10px] font-mono text-emerald-400 hover:text-emerald-300 border border-emerald-800 rounded px-2 py-1 disabled:opacity-50"
+ >
+ Link
+ </button>
+ </div>
+ )}
+ </div>
+
+ {/* Convert to Directive Step */}
+ {!order.directiveStepId && (
+ <div>
+ <button
+ type="button"
+ onClick={() => setShowConvertToStep(!showConvertToStep)}
+ className="text-[10px] font-mono text-yellow-400 hover:text-yellow-300 border border-yellow-800 rounded px-2 py-1 w-full text-left"
+ >
+ Convert to Directive Step
+ </button>
+ {showConvertToStep && (
+ <div className="mt-1 border border-[rgba(117,170,252,0.2)] bg-[#0a1525] max-h-32 overflow-y-auto rounded">
+ {directives.length === 0 ? (
+ <div className="px-3 py-2 text-[10px] font-mono text-[#556677]">
+ No directives available
+ </div>
+ ) : (
+ directives.map((d) => (
+ <button
+ key={d.id}
+ type="button"
+ onClick={() => handleConvertToStep(d.id)}
+ className="w-full text-left px-3 py-1.5 text-[10px] font-mono text-yellow-400 hover:bg-[rgba(117,170,252,0.1)] border-b border-[rgba(117,170,252,0.1)] last:border-b-0"
+ >
+ {d.title}
+ </button>
+ ))
+ )}
+ </div>
+ )}
+ </div>
+ )}
+ </div>
+ </div>
+
+ {/* Metadata */}
+ <div className="px-4 py-2 border-t border-[rgba(117,170,252,0.1)]">
+ <div className="flex items-center justify-between text-[9px] font-mono text-[#556677]">
+ <span>Created {new Date(order.createdAt).toLocaleDateString()}</span>
+ <span>Updated {new Date(order.updatedAt).toLocaleDateString()}</span>
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/makima/frontend/src/components/orders/OrderList.tsx b/makima/frontend/src/components/orders/OrderList.tsx
new file mode 100644
index 0000000..76ac7a7
--- /dev/null
+++ b/makima/frontend/src/components/orders/OrderList.tsx
@@ -0,0 +1,188 @@
+import { useState, useMemo } from "react";
+import type { Order, OrderStatus, OrderPriority, OrderType } from "../../lib/api";
+
+const STATUS_BADGE: Record<OrderStatus, { color: string; label: string }> = {
+ open: { color: "text-[#75aafc] border-[rgba(117,170,252,0.4)]", label: "OPEN" },
+ in_progress: { color: "text-yellow-400 border-yellow-800", label: "IN PROGRESS" },
+ done: { color: "text-emerald-400 border-emerald-800", label: "DONE" },
+ archived: { color: "text-[#556677] border-[#2a3a5a]", label: "ARCHIVED" },
+};
+
+const PRIORITY_COLOR: Record<OrderPriority, string> = {
+ critical: "bg-red-400",
+ high: "bg-orange-400",
+ medium: "bg-yellow-400",
+ low: "bg-[#75aafc]",
+ none: "bg-[#556677]",
+};
+
+const TYPE_BADGE: Record<OrderType, { color: string; label: string }> = {
+ feature: { color: "text-[#75aafc] border-[rgba(117,170,252,0.3)]", label: "FEAT" },
+ bug: { color: "text-red-400 border-red-800", label: "BUG" },
+ spike: { color: "text-yellow-400 border-yellow-800", label: "SPIKE" },
+ chore: { color: "text-[#7788aa] border-[#2a3a5a]", label: "CHORE" },
+ improvement: { color: "text-emerald-400 border-emerald-800", label: "IMPROVE" },
+};
+
+interface OrderListProps {
+ orders: Order[];
+ selectedId: string | null;
+ onSelect: (id: string) => void;
+ onCreate: () => void;
+ statusFilter: OrderStatus | undefined;
+ onStatusFilter: (s: OrderStatus | undefined) => void;
+ typeFilter: OrderType | undefined;
+ onTypeFilter: (t: OrderType | undefined) => void;
+}
+
+const STATUS_OPTIONS: (OrderStatus | "all")[] = ["all", "open", "in_progress", "done", "archived"];
+const TYPE_OPTIONS: (OrderType | "all")[] = ["all", "feature", "bug", "spike", "chore", "improvement"];
+
+export function OrderList({
+ orders,
+ selectedId,
+ onSelect,
+ onCreate,
+ statusFilter,
+ onStatusFilter,
+ typeFilter,
+ onTypeFilter,
+}: OrderListProps) {
+ const [search, setSearch] = useState("");
+
+ const filtered = useMemo(() => {
+ if (!search.trim()) return orders;
+ const q = search.toLowerCase();
+ return orders.filter(
+ (o) =>
+ o.title.toLowerCase().includes(q) ||
+ (o.description && o.description.toLowerCase().includes(q)) ||
+ o.labels.some((l) => l.toLowerCase().includes(q)),
+ );
+ }, [orders, search]);
+
+ return (
+ <div className="flex flex-col h-full">
+ {/* Header */}
+ <div className="flex items-center justify-between px-3 py-2 border-b border-dashed border-[rgba(117,170,252,0.2)]">
+ <span className="text-[11px] font-mono text-[#9bc3ff] uppercase tracking-wide">
+ Orders
+ </span>
+ <button
+ type="button"
+ onClick={onCreate}
+ className="text-[11px] font-mono text-[#75aafc] hover:text-white bg-transparent border border-[rgba(117,170,252,0.3)] rounded px-2 py-0.5 hover:border-[rgba(117,170,252,0.6)] transition-colors"
+ >
+ + New
+ </button>
+ </div>
+
+ {/* Search */}
+ <div className="px-3 py-1.5 border-b border-[rgba(117,170,252,0.1)]">
+ <input
+ value={search}
+ onChange={(e) => setSearch(e.target.value)}
+ placeholder="Search orders..."
+ className="w-full bg-transparent border-none outline-none text-[11px] font-mono text-white placeholder:text-[#556677]"
+ />
+ </div>
+
+ {/* Filters */}
+ <div className="px-3 py-1.5 border-b border-[rgba(117,170,252,0.1)] flex flex-col gap-1">
+ <div className="flex items-center gap-1 flex-wrap">
+ <span className="text-[9px] font-mono text-[#556677] uppercase w-[38px] shrink-0">
+ Status
+ </span>
+ {STATUS_OPTIONS.map((s) => (
+ <button
+ key={s}
+ type="button"
+ onClick={() => onStatusFilter(s === "all" ? undefined : s)}
+ className={`text-[9px] font-mono px-1.5 py-0.5 rounded transition-colors ${
+ (s === "all" && !statusFilter) || s === statusFilter
+ ? "text-white bg-[rgba(117,170,252,0.15)] border border-[rgba(117,170,252,0.4)]"
+ : "text-[#556677] hover:text-[#7788aa] border border-transparent"
+ }`}
+ >
+ {s === "all" ? "ALL" : s === "in_progress" ? "WIP" : s.toUpperCase()}
+ </button>
+ ))}
+ </div>
+ <div className="flex items-center gap-1 flex-wrap">
+ <span className="text-[9px] font-mono text-[#556677] uppercase w-[38px] shrink-0">
+ Type
+ </span>
+ {TYPE_OPTIONS.map((t) => (
+ <button
+ key={t}
+ type="button"
+ onClick={() => onTypeFilter(t === "all" ? undefined : t)}
+ className={`text-[9px] font-mono px-1.5 py-0.5 rounded transition-colors ${
+ (t === "all" && !typeFilter) || t === typeFilter
+ ? "text-white bg-[rgba(117,170,252,0.15)] border border-[rgba(117,170,252,0.4)]"
+ : "text-[#556677] hover:text-[#7788aa] border border-transparent"
+ }`}
+ >
+ {t === "all" ? "ALL" : t.toUpperCase()}
+ </button>
+ ))}
+ </div>
+ </div>
+
+ {/* List */}
+ <div className="flex-1 overflow-y-auto">
+ {filtered.length === 0 ? (
+ <div className="px-3 py-6 text-center text-[#556677] font-mono text-[11px]">
+ No orders found
+ </div>
+ ) : (
+ filtered.map((o) => {
+ const statusBadge = STATUS_BADGE[o.status] || STATUS_BADGE.open;
+ const typeBadge = TYPE_BADGE[o.orderType] || TYPE_BADGE.feature;
+ const priorityColor = PRIORITY_COLOR[o.priority] || PRIORITY_COLOR.none;
+
+ return (
+ <button
+ key={o.id}
+ type="button"
+ onClick={() => onSelect(o.id)}
+ className={`w-full text-left px-3 py-2.5 border-b border-[rgba(117,170,252,0.1)] hover:bg-[rgba(117,170,252,0.05)] transition-colors ${
+ selectedId === o.id ? "bg-[rgba(117,170,252,0.1)]" : ""
+ }`}
+ >
+ <div className="flex items-start gap-2 mb-1">
+ {/* Priority dot */}
+ <span
+ className={`w-2 h-2 rounded-full ${priorityColor} shrink-0 mt-[3px]`}
+ title={o.priority}
+ />
+ <span className="text-[12px] font-mono text-white truncate flex-1">
+ {o.title}
+ </span>
+ </div>
+ <div className="flex items-center gap-1.5 pl-4">
+ <span
+ className={`text-[9px] font-mono ${statusBadge.color} border rounded px-1.5 py-0.5`}
+ >
+ {statusBadge.label}
+ </span>
+ <span
+ className={`text-[9px] font-mono ${typeBadge.color} border rounded px-1.5 py-0.5`}
+ >
+ {typeBadge.label}
+ </span>
+ {o.labels.length > 0 && (
+ <span className="text-[9px] font-mono text-[#556677] truncate">
+ {o.labels.slice(0, 2).join(", ")}
+ {o.labels.length > 2 && ` +${o.labels.length - 2}`}
+ </span>
+ )}
+ </div>
+ </button>
+ );
+ })
+ )}
+ </div>
+ </div>
+ );
+}
diff --git a/makima/frontend/src/components/workflow/PhaseColumn.tsx b/makima/frontend/src/components/workflow/PhaseColumn.tsx
deleted file mode 100644
index 277b04c..0000000
--- a/makima/frontend/src/components/workflow/PhaseColumn.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
-import { useState } from "react";
-import type { ContractSummary, ContractPhase } from "../../lib/api";
-import { WorkflowContractCard } from "./WorkflowContractCard";
-
-interface PhaseColumnProps {
- phase: ContractPhase;
- contracts: ContractSummary[];
- onContractClick: (contractId: string) => void;
- onDrop: (contractId: string, phase: ContractPhase) => void;
- onContextMenu?: (e: React.MouseEvent, contract: ContractSummary) => void;
-}
-
-const phaseConfig: Record<
- ContractPhase,
- { label: string; color: string; bgColor: string; borderColor: string }
-> = {
- research: {
- label: "Research",
- color: "text-purple-400",
- bgColor: "bg-purple-400/10",
- borderColor: "border-purple-400/30",
- },
- specify: {
- label: "Specify",
- color: "text-blue-400",
- bgColor: "bg-blue-400/10",
- borderColor: "border-blue-400/30",
- },
- plan: {
- label: "Plan",
- color: "text-cyan-400",
- bgColor: "bg-cyan-400/10",
- borderColor: "border-cyan-400/30",
- },
- execute: {
- label: "Execute",
- color: "text-yellow-400",
- bgColor: "bg-yellow-400/10",
- borderColor: "border-yellow-400/30",
- },
- review: {
- label: "Review",
- color: "text-green-400",
- bgColor: "bg-green-400/10",
- borderColor: "border-green-400/30",
- },
-};
-
-export function PhaseColumn({
- phase,
- contracts,
- onContractClick,
- onDrop,
- onContextMenu,
-}: PhaseColumnProps) {
- const [isDragOver, setIsDragOver] = useState(false);
- const config = phaseConfig[phase];
-
- const handleDragOver = (e: React.DragEvent) => {
- e.preventDefault();
- setIsDragOver(true);
- };
-
- const handleDragLeave = () => {
- setIsDragOver(false);
- };
-
- const handleDrop = (e: React.DragEvent) => {
- e.preventDefault();
- setIsDragOver(false);
- const contractId = e.dataTransfer.getData("contractId");
- if (contractId) {
- onDrop(contractId, phase);
- }
- };
-
- return (
- <div
- className={`
- flex flex-col min-w-[220px] flex-1 border border-[rgba(117,170,252,0.15)]
- ${isDragOver ? "bg-[rgba(117,170,252,0.05)]" : "bg-transparent"}
- transition-colors
- `}
- onDragOver={handleDragOver}
- onDragLeave={handleDragLeave}
- onDrop={handleDrop}
- >
- {/* Column header */}
- <div
- className={`
- p-3 border-b ${config.borderColor} ${config.bgColor}
- flex items-center justify-between
- `}
- >
- <span className={`font-mono text-xs uppercase tracking-wider ${config.color}`}>
- {config.label}
- </span>
- <span className="font-mono text-[10px] text-[#555]">
- ({contracts.length})
- </span>
- </div>
-
- {/* Cards container */}
- <div className="flex-1 overflow-y-auto p-2 space-y-2">
- {contracts.length === 0 ? (
- <div className="p-4 text-center font-mono text-[10px] text-[#555]">
- No contracts
- </div>
- ) : (
- contracts.map((contract) => (
- <WorkflowContractCard
- key={contract.id}
- contract={contract}
- onClick={() => onContractClick(contract.id)}
- onDragStart={(e) => {
- e.dataTransfer.setData("contractId", contract.id);
- e.dataTransfer.effectAllowed = "move";
- }}
- onContextMenu={onContextMenu ? (e) => onContextMenu(e, contract) : undefined}
- />
- ))
- )}
- </div>
- </div>
- );
-}
diff --git a/makima/frontend/src/components/workflow/WorkflowBoard.tsx b/makima/frontend/src/components/workflow/WorkflowBoard.tsx
deleted file mode 100644
index e36ca21..0000000
--- a/makima/frontend/src/components/workflow/WorkflowBoard.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import { useMemo, useState } from "react";
-import type { ContractSummary, ContractPhase } from "../../lib/api";
-import { PhaseColumn } from "./PhaseColumn";
-import { ContractContextMenu } from "../contracts/ContractContextMenu";
-
-interface WorkflowBoardProps {
- contracts: ContractSummary[];
- onContractClick: (contractId: string) => void;
- onPhaseChange: (contractId: string, newPhase: ContractPhase) => void;
- onMarkComplete?: (contract: ContractSummary) => void;
- onMarkActive?: (contract: ContractSummary) => void;
- onArchive?: (contract: ContractSummary) => void;
- onDelete?: (contract: ContractSummary) => void;
- onGoToSupervisor?: (contract: ContractSummary) => void;
-}
-
-const phases: ContractPhase[] = ["research", "specify", "plan", "execute", "review"];
-
-export function WorkflowBoard({
- contracts,
- onContractClick,
- onPhaseChange,
- onMarkComplete,
- onMarkActive,
- onArchive,
- onDelete,
- onGoToSupervisor,
-}: WorkflowBoardProps) {
- const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null);
- const [contextMenuContract, setContextMenuContract] = useState<ContractSummary | null>(null);
-
- const handleContextMenu = (e: React.MouseEvent, contract: ContractSummary) => {
- e.preventDefault();
- e.stopPropagation(); // Prevent interference with drag-and-drop
- setContextMenuPosition({ x: e.clientX, y: e.clientY });
- setContextMenuContract(contract);
- };
-
- const closeContextMenu = () => {
- setContextMenuPosition(null);
- setContextMenuContract(null);
- };
-
- // Group contracts by phase
- const contractsByPhase = useMemo(() => {
- const grouped: Record<ContractPhase, ContractSummary[]> = {
- research: [],
- specify: [],
- plan: [],
- execute: [],
- review: [],
- };
-
- for (const contract of contracts) {
- const phase = contract.phase as ContractPhase;
- if (grouped[phase]) {
- grouped[phase].push(contract);
- } else {
- // Default to research if unknown phase
- grouped.research.push(contract);
- }
- }
-
- return grouped;
- }, [contracts]);
-
- return (
- <>
- <div className="flex gap-2 h-full overflow-x-auto">
- {phases.map((phase) => (
- <PhaseColumn
- key={phase}
- phase={phase}
- contracts={contractsByPhase[phase]}
- onContractClick={onContractClick}
- onDrop={onPhaseChange}
- onContextMenu={handleContextMenu}
- />
- ))}
- </div>
-
- {/* Context Menu */}
- {contextMenuPosition && contextMenuContract && (
- <ContractContextMenu
- x={contextMenuPosition.x}
- y={contextMenuPosition.y}
- contract={contextMenuContract}
- onClose={closeContextMenu}
- onMarkComplete={() => onMarkComplete?.(contextMenuContract)}
- onMarkActive={() => onMarkActive?.(contextMenuContract)}
- onArchive={() => onArchive?.(contextMenuContract)}
- onDelete={() => onDelete?.(contextMenuContract)}
- onGoToSupervisor={() => onGoToSupervisor?.(contextMenuContract)}
- />
- )}
- </>
- );
-}
diff --git a/makima/frontend/src/components/workflow/WorkflowContractCard.tsx b/makima/frontend/src/components/workflow/WorkflowContractCard.tsx
deleted file mode 100644
index 86fcd13..0000000
--- a/makima/frontend/src/components/workflow/WorkflowContractCard.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import { useNavigate } from "react-router";
-import type { ContractSummary, ContractStatus } from "../../lib/api";
-
-interface WorkflowContractCardProps {
- contract: ContractSummary;
- onClick: () => void;
- onDragStart: (e: React.DragEvent) => void;
- onContextMenu?: (e: React.MouseEvent) => void;
-}
-
-const statusConfig: Record<ContractStatus, { label: string; color: string }> = {
- active: { label: "Active", color: "text-green-400" },
- completed: { label: "Done", color: "text-blue-400" },
- archived: { label: "Archived", color: "text-[#555]" },
-};
-
-export function WorkflowContractCard({
- contract,
- onClick,
- onDragStart,
- onContextMenu,
-}: WorkflowContractCardProps) {
- const navigate = useNavigate();
- const status = statusConfig[contract.status] || statusConfig.active;
-
- const handleSupervisorClick = (e: React.MouseEvent) => {
- e.stopPropagation();
- if (contract.supervisorTaskId) {
- navigate(`/mesh/${contract.supervisorTaskId}`);
- }
- };
-
- return (
- <div
- draggable
- onDragStart={onDragStart}
- onClick={onClick}
- onContextMenu={onContextMenu}
- className="p-3 bg-[rgba(9,13,20,0.8)] border border-[rgba(117,170,252,0.2)] hover:border-[rgba(117,170,252,0.4)] cursor-pointer transition-colors select-none"
- >
- {/* Header row with name and supervisor button */}
- <div className="flex items-center justify-between gap-2 mb-1">
- <div className="font-mono text-sm text-[#dbe7ff] truncate flex-1">
- {contract.name}
- </div>
- {contract.supervisorTaskId && (
- <button
- onClick={handleSupervisorClick}
- title="Open Supervisor Task"
- className="flex-shrink-0 px-1.5 py-0.5 font-mono text-[10px] text-[#75aafc] hover:text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] hover:bg-[rgba(117,170,252,0.1)] transition-colors"
- >
- ▶
- </button>
- )}
- </div>
-
- {/* Status and counts row */}
- <div className="flex items-center justify-between">
- <span className={`font-mono text-[10px] uppercase ${status.color}`}>
- {status.label}
- </span>
- <div className="flex items-center gap-2 font-mono text-[10px] text-[#555]">
- <span title="Files">{contract.fileCount} files</span>
- <span title="Tasks">{contract.taskCount} tasks</span>
- </div>
- </div>
-
- {/* Description preview if exists */}
- {contract.description && (
- <div className="mt-1 font-mono text-[10px] text-[#555] truncate">
- {contract.description}
- </div>
- )}
- </div>
- );
-}