import { useState, useMemo, useEffect, useRef } from "react";
import type { DirectiveWithSteps, DirectiveStatus, MemoryCategory } from "../../lib/api";
import { DirectiveDAG } from "./DirectiveDAG";
import { DirectiveLogStream } from "./DirectiveLogStream";
import { useDirectiveMemories } from "../../hooks/useDirectiveMemories";
import { useMultiTaskSubscription } from "../../hooks/useMultiTaskSubscription";
const STATUS_BADGE: Record<DirectiveStatus, { color: string; label: string }> = {
draft: { color: "text-[#7788aa] border-[#2a3a5a]", label: "DRAFT" },
active: { color: "text-green-400 border-green-800", label: "ACTIVE" },
idle: { color: "text-yellow-400 border-yellow-800", label: "IDLE" },
paused: { color: "text-orange-400 border-orange-800", label: "PAUSED" },
archived: { color: "text-[#556677] border-[#2a3a5a]", label: "ARCHIVED" },
};
const CATEGORY_COLORS: Record<MemoryCategory, { text: string; border: string; bg: string; label: string }> = {
decision: { text: "text-amber-400", border: "border-amber-800", bg: "bg-amber-900/20", label: "Decision" },
context: { text: "text-cyan-400", border: "border-cyan-800", bg: "bg-cyan-900/20", label: "Context" },
preference: { text: "text-violet-400", border: "border-violet-800", bg: "bg-violet-900/20", label: "Preference" },
learning: { text: "text-emerald-400", border: "border-emerald-800", bg: "bg-emerald-900/20", label: "Learning" },
other: { text: "text-[#7788aa]", border: "border-[#2a3a5a]", bg: "bg-[#1a2540]", label: "Other" },
};
const ALL_CATEGORIES: MemoryCategory[] = ["decision", "context", "preference", "learning", "other"];
interface DirectiveDetailProps {
directive: DirectiveWithSteps;
onStart: () => void;
onPause: () => void;
onAdvance: () => void;
onCompleteStep: (stepId: string) => void;
onFailStep: (stepId: string) => void;
onSkipStep: (stepId: string) => void;
onUpdateGoal: (goal: string) => void;
onDelete: () => void;
onRefresh: () => void;
onCleanupTasks: () => void;
}
export function DirectiveDetail({
directive,
onStart,
onPause,
onAdvance,
onCompleteStep,
onFailStep,
onSkipStep,
onUpdateGoal,
onDelete,
onRefresh,
onCleanupTasks,
}: DirectiveDetailProps) {
const [editingGoal, setEditingGoal] = useState(false);
const [goalText, setGoalText] = useState(directive.goal);
const [visibleTaskIds, setVisibleTaskIds] = useState<Set<string> | null>(null);
const [searchQuery, setSearchQuery] = useState("");
const [isLogCollapsed, setIsLogCollapsed] = useState(true);
const prevHadRunningRef = useRef(false);
const badge = STATUS_BADGE[directive.status] || STATUS_BADGE.draft;
const completedSteps = directive.steps.filter((s) => s.status === "completed").length;
const totalSteps = directive.steps.length;
const progress = totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0;
const terminalStatuses = new Set(["completed", "failed", "skipped"]);
const hasTerminalTasks = directive.steps.some((s) => s.taskId && terminalStatuses.has(s.status));
// Memory panel state
const [memoryOpen, setMemoryOpen] = useState(false);
const [addingMemory, setAddingMemory] = useState(false);
const [newCategory, setNewCategory] = useState<MemoryCategory>("context");
const [newContent, setNewContent] = useState("");
const [newSource, setNewSource] = useState("");
const [confirmClear, setConfirmClear] = useState(false);
const {
grouped,
config: memoryConfig,
loading: memoryLoading,
error: memoryError,
toggleEnabled,
add: addMemory,
remove: removeMemory,
clearAll: clearMemories,
refresh: refreshMemories,
} = useDirectiveMemories(directive.id);
const memoryEnabled = memoryConfig?.enabled ?? false;
const totalMemories = Object.values(grouped).reduce((sum, arr) => sum + arr.length, 0);
// Build task map from directive steps and orchestrator
const taskMap = useMemo(() => {
const map = new Map<string, string>();
if (directive.orchestratorTaskId) {
map.set(directive.orchestratorTaskId, "Orchestrator");
}
for (const step of directive.steps) {
if (step.taskId) {
map.set(step.taskId, step.name);
}
}
return map;
}, [directive.orchestratorTaskId, directive.steps]);
// Subscribe to all task outputs
const { connected, entries, clearEntries } = useMultiTaskSubscription({
taskMap,
enabled: taskMap.size > 0,
});
// Auto-expand log panel when tasks start running
const hasRunningTasks = directive.steps.some((s) => s.status === "running") ||
!!directive.orchestratorTaskId;
useEffect(() => {
if (hasRunningTasks && !prevHadRunningRef.current) {
setIsLogCollapsed(false);
}
prevHadRunningRef.current = hasRunningTasks;
}, [hasRunningTasks]);
const handleGoalSave = () => {
if (goalText.trim() && goalText !== directive.goal) {
onUpdateGoal(goalText.trim());
}
setEditingGoal(false);
};
const handleAddMemory = async () => {
if (!newContent.trim()) return;
await addMemory({
category: newCategory,
content: newContent.trim(),
source: newSource.trim() || undefined,
});
setNewContent("");
setNewSource("");
setAddingMemory(false);
};
const handleClearAll = async () => {
await clearMemories();
setConfirmClear(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">
<h2 className="text-[14px] font-mono text-white font-medium truncate pr-2">
{directive.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>
{/* Progress bar */}
{totalSteps > 0 && (
<div className="flex items-center gap-2 mb-2">
<div className="flex-1 h-1.5 bg-[#1a2540] rounded overflow-hidden">
<div
className="h-full bg-emerald-600 rounded transition-all"
style={{ width: `${progress}%` }}
/>
</div>
<span className="text-[10px] font-mono text-[#7788aa] shrink-0">
{completedSteps}/{totalSteps} steps
</span>
</div>
)}
{/* Repo info */}
{(directive.repositoryUrl || directive.localPath) && (
<div className="text-[10px] font-mono text-[#556677] mb-2 truncate">
{directive.repositoryUrl || directive.localPath}
{directive.baseBranch && ` @ ${directive.baseBranch}`}
</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">
<span className="inline-block w-2 h-2 rounded-full bg-[#75aafc] animate-pulse" />
<span className="text-[10px] font-mono text-[#75aafc]">
Planning in progress...
</span>
<a
href={`/mesh/${directive.orchestratorTaskId}`}
className="text-[9px] font-mono text-[#556677] hover:text-[#75aafc] underline ml-auto"
>
View task
</a>
</div>
)}
{/* PR link */}
{directive.prUrl && (
<div className="flex items-center gap-2 mb-2 px-2 py-1.5 bg-[#0a1a10] border border-emerald-900 rounded">
<span className="inline-block w-2 h-2 rounded-full bg-emerald-400" />
<span className="text-[10px] font-mono text-emerald-400">
PR created
</span>
<a
href={directive.prUrl}
target="_blank"
rel="noopener noreferrer"
className="text-[10px] font-mono text-emerald-400 hover:text-emerald-300 underline ml-auto truncate max-w-[200px]"
>
{directive.prUrl}
</a>
</div>
)}
{/* Completion task indicator */}
{directive.completionTaskId && (
<div className="flex items-center gap-2 mb-2 px-2 py-1.5 bg-[#1a1a10] border border-yellow-900 rounded">
<span className="inline-block w-2 h-2 rounded-full bg-yellow-400 animate-pulse" />
<span className="text-[10px] font-mono text-yellow-400">
{directive.prUrl ? "Updating PR..." : "Creating PR..."}
</span>
<a
href={`/mesh/${directive.completionTaskId}`}
className="text-[9px] font-mono text-[#556677] hover:text-yellow-400 underline ml-auto"
>
View task
</a>
</div>
)}
{/* Controls */}
<div className="flex flex-wrap gap-2">
{(directive.status === "draft" || directive.status === "paused") && (
<button
type="button"
onClick={onStart}
className="text-[10px] font-mono text-green-400 hover:text-green-300 border border-green-800 rounded px-2 py-1"
>
Start
</button>
)}
{directive.status === "active" && (
<>
<button
type="button"
onClick={onPause}
className="text-[10px] font-mono text-orange-400 hover:text-orange-300 border border-orange-800 rounded px-2 py-1"
>
Pause
</button>
<button
type="button"
onClick={onAdvance}
className="text-[10px] font-mono text-[#75aafc] hover:text-white border border-[rgba(117,170,252,0.3)] rounded px-2 py-1"
>
Advance
</button>
</>
)}
{directive.status === "idle" && (
<div className="flex items-center gap-2">
<span className="text-[10px] font-mono text-yellow-400">
All steps done. Update goal to add new work.
</span>
<button
type="button"
onClick={() => 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
</button>
</div>
)}
{hasTerminalTasks && (
<button
type="button"
onClick={onCleanupTasks}
className="text-[10px] font-mono text-[#7788aa] hover:text-white border border-[#2a3a5a] rounded px-2 py-1 ml-auto"
>
Clean up tasks
</button>
)}
<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 ${hasTerminalTasks ? "" : "ml-auto"}`}
>
Delete
</button>
</div>
</div>
{/* Goal */}
<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">
Goal
</span>
{!editingGoal && (
<button
type="button"
onClick={() => { setGoalText(directive.goal); setEditingGoal(true); }}
className="text-[9px] font-mono text-[#556677] hover:text-[#75aafc]"
>
[edit]
</button>
)}
</div>
{editingGoal ? (
<div className="flex flex-col gap-1.5">
<textarea
value={goalText}
onChange={(e) => setGoalText(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-[60px]"
rows={3}
/>
<div className="flex gap-1.5">
<button
type="button"
onClick={handleGoalSave}
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={() => setEditingGoal(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">
{directive.goal}
</p>
)}
</div>
{/* Memory Panel */}
<div className="px-4 py-3 border-b border-[rgba(117,170,252,0.1)]">
{/* Memory header — always visible */}
<div className="flex items-center justify-between">
<button
type="button"
onClick={() => setMemoryOpen((v) => !v)}
className="flex items-center gap-1.5 group"
>
<span className="text-[10px] font-mono text-[#556677] group-hover:text-[#9bc3ff] transition-colors">
{memoryOpen ? "\u25BC" : "\u25B6"}
</span>
<span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide">
Memory
</span>
{totalMemories > 0 && (
<span className="text-[9px] font-mono text-[#556677] ml-1">
({totalMemories})
</span>
)}
</button>
<div className="flex items-center gap-2">
{/* Enable/disable toggle */}
<button
type="button"
onClick={() => toggleEnabled(!memoryEnabled)}
className={`text-[9px] font-mono border rounded px-1.5 py-0.5 transition-colors ${
memoryEnabled
? "text-emerald-400 border-emerald-800 hover:text-emerald-300"
: "text-[#556677] border-[#2a3a5a] hover:text-[#7788aa]"
}`}
title={memoryEnabled ? "Disable memory" : "Enable memory"}
>
{memoryEnabled ? "ON" : "OFF"}
</button>
</div>
</div>
{/* Collapsible content */}
{memoryOpen && (
<div className="mt-2">
{memoryError && (
<div className="text-[10px] font-mono text-red-400 mb-2 px-2 py-1 bg-red-900/10 border border-red-800/30 rounded">
{memoryError}
</div>
)}
{memoryLoading ? (
<div className="text-[10px] font-mono text-[#556677] py-2">Loading...</div>
) : totalMemories === 0 ? (
<div className="text-[10px] font-mono text-[#556677] py-2">
No memory entries yet.
{!memoryEnabled && " Enable memory to start capturing entries."}
</div>
) : (
/* Grouped entries */
<div className="flex flex-col gap-2">
{ALL_CATEGORIES.map((cat) => {
const entries = grouped[cat];
if (entries.length === 0) return null;
const style = CATEGORY_COLORS[cat];
return (
<div key={cat}>
<div className="flex items-center gap-1.5 mb-1">
<span className={`text-[9px] font-mono ${style.text} uppercase tracking-wider`}>
{style.label}
</span>
<span className="text-[9px] font-mono text-[#556677]">
({entries.length})
</span>
</div>
<div className="flex flex-col gap-1">
{entries.map((entry) => (
<div
key={entry.id}
className={`flex items-start gap-2 px-2 py-1.5 rounded border ${style.border} ${style.bg}`}
>
<div className="flex-1 min-w-0">
<p className="text-[10px] font-mono text-[#c0d0e0] whitespace-pre-wrap break-words">
{entry.content}
</p>
{entry.source && (
<span className="text-[9px] font-mono text-[#556677] mt-0.5 block">
src: {entry.source}
</span>
)}
</div>
<button
type="button"
onClick={() => removeMemory(entry.id)}
className="text-[9px] font-mono text-[#556677] hover:text-red-400 shrink-0 mt-0.5"
title="Delete entry"
>
x
</button>
</div>
))}
</div>
</div>
);
})}
</div>
)}
{/* Action bar: Add + Clear */}
<div className="flex items-center gap-2 mt-2 pt-2 border-t border-[rgba(117,170,252,0.1)]">
<button
type="button"
onClick={() => setAddingMemory((v) => !v)}
className="text-[10px] font-mono text-[#75aafc] hover:text-white border border-[rgba(117,170,252,0.2)] rounded px-2 py-0.5"
>
{addingMemory ? "Cancel" : "+ Add"}
</button>
{totalMemories > 0 && (
<>
{confirmClear ? (
<div className="flex items-center gap-1.5 ml-auto">
<span className="text-[9px] font-mono text-red-400">Clear all?</span>
<button
type="button"
onClick={handleClearAll}
className="text-[9px] font-mono text-red-400 hover:text-red-300 border border-red-800 rounded px-1.5 py-0.5"
>
Yes
</button>
<button
type="button"
onClick={() => setConfirmClear(false)}
className="text-[9px] font-mono text-[#7788aa] hover:text-white border border-[#2a3a5a] rounded px-1.5 py-0.5"
>
No
</button>
</div>
) : (
<button
type="button"
onClick={() => setConfirmClear(true)}
className="text-[10px] font-mono text-[#556677] hover:text-red-400 ml-auto"
>
Clear all
</button>
)}
</>
)}
<button
type="button"
onClick={refreshMemories}
className="text-[9px] font-mono text-[#556677] hover:text-[#75aafc]"
title="Refresh memories"
>
[refresh]
</button>
</div>
{/* Add form */}
{addingMemory && (
<div className="mt-2 p-2 bg-[#0a1628] border border-[rgba(117,170,252,0.15)] rounded flex flex-col gap-2">
<div className="flex items-center gap-2">
<label className="text-[9px] font-mono text-[#7788aa] shrink-0">Category</label>
<select
value={newCategory}
onChange={(e) => setNewCategory(e.target.value as MemoryCategory)}
className="bg-[#1a2540] border border-[rgba(117,170,252,0.2)] rounded px-1.5 py-0.5 text-[10px] font-mono text-white flex-1"
>
{ALL_CATEGORIES.map((c) => (
<option key={c} value={c}>
{CATEGORY_COLORS[c].label}
</option>
))}
</select>
</div>
<textarea
value={newContent}
onChange={(e) => setNewContent(e.target.value)}
placeholder="Memory content..."
className="w-full bg-[#1a2540] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1.5 text-[10px] font-mono text-white resize-y min-h-[40px] placeholder:text-[#556677]"
rows={2}
/>
<input
type="text"
value={newSource}
onChange={(e) => setNewSource(e.target.value)}
placeholder="Source (optional)"
className="w-full bg-[#1a2540] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1 text-[10px] font-mono text-white placeholder:text-[#556677]"
/>
<div className="flex gap-1.5">
<button
type="button"
onClick={handleAddMemory}
disabled={!newContent.trim()}
className="text-[10px] font-mono text-emerald-400 hover:text-emerald-300 border border-emerald-800 rounded px-2 py-0.5 disabled:opacity-40 disabled:cursor-not-allowed"
>
Save
</button>
<button
type="button"
onClick={() => { setAddingMemory(false); setNewContent(""); setNewSource(""); }}
className="text-[10px] font-mono text-[#7788aa] hover:text-white border border-[#2a3a5a] rounded px-2 py-0.5"
>
Cancel
</button>
</div>
</div>
)}
</div>
)}
</div>
{/* DAG */}
<div className="px-4 py-3 flex-1">
<span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide block mb-2">
Steps ({totalSteps})
</span>
<DirectiveDAG
steps={directive.steps}
onComplete={onCompleteStep}
onFail={onFailStep}
onSkip={onSkipStep}
/>
</div>
{/* Log Stream */}
{taskMap.size > 0 && (
<div className="px-4 py-3 border-t border-[rgba(117,170,252,0.1)]">
<DirectiveLogStream
entries={entries}
taskMap={taskMap}
connected={connected}
visibleTaskIds={visibleTaskIds}
searchQuery={searchQuery}
isCollapsed={isLogCollapsed}
onToggleCollapse={() => setIsLogCollapsed((prev) => !prev)}
onSetVisibleTaskIds={setVisibleTaskIds}
onSetSearchQuery={setSearchQuery}
onClear={clearEntries}
/>
</div>
)}
</div>
);
}