import { useState, useMemo, useEffect, useRef } from "react"; 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 = { 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" }, }; 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; onUpdate: (req: UpdateDirectiveRequest) => void; onDelete: () => void; onRefresh: () => void; onCleanupTasks: () => void; onPickUpOrders: () => Promise<{ message: string; orderCount: number; taskId: string | null } | null>; onCreatePR: () => Promise; } export function DirectiveDetail({ directive, onStart, onPause, onAdvance, onCompleteStep, onFailStep, onSkipStep, onUpdateGoal, onUpdate, onDelete, onRefresh, onCleanupTasks, onPickUpOrders, onCreatePR, }: DirectiveDetailProps) { const [editingGoal, setEditingGoal] = useState(false); const [goalText, setGoalText] = useState(directive.goal); const [visibleTaskIds, setVisibleTaskIds] = useState | null>(null); const [pickingUpOrders, setPickingUpOrders] = useState(false); const [pickUpResult, setPickUpResult] = useState(null); const [creatingPR, setCreatingPR] = useState(false); // 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); 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)); // Get pending questions for this directive's tasks const { pendingQuestions, submitAnswer } = useSupervisorQuestions(); const directiveTaskIds = useMemo(() => { const ids = new Set(); 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(() => { const parts: string[] = []; if (directive.orchestratorTaskId) parts.push(`o:${directive.orchestratorTaskId}`); for (const step of directive.steps) { if (step.taskId) parts.push(`${step.id}:${step.taskId}`); } return parts.join(","); }, [directive.orchestratorTaskId, directive.steps]); const taskMap = useMemo(() => { const map = new Map(); 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; }, [taskMapKey]); // eslint-disable-line react-hooks/exhaustive-deps // 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 handlePickUpOrders = async () => { setPickingUpOrders(true); setPickUpResult(null); try { const result = await onPickUpOrders(); if (result) { setPickUpResult(result.message); setTimeout(() => setPickUpResult(null), 5000); } } catch (e) { setPickUpResult(e instanceof Error ? e.message : "Failed to pick up orders"); setTimeout(() => setPickUpResult(null), 5000); } finally { setPickingUpOrders(false); } }; const handleGoalSave = () => { if (goalText.trim() && goalText !== directive.goal) { onUpdateGoal(goalText.trim()); } setEditingGoal(false); }; return (
{/* Header */}

{directive.title}

{badge.label}
{/* Progress bar */} {totalSteps > 0 && (
{completedSteps}/{totalSteps} steps
)} {/* Repo info */} {(directive.repositoryUrl || directive.localPath) && (
{directive.repositoryUrl || directive.localPath} {directive.baseBranch && ` @ ${directive.baseBranch}`}
)} {/* Reconcile mode toggle */}
{directive.reconcileMode ? "Questions pause execution" : "Questions timeout after 30s"}
{/* Orchestrator planning indicator */} {directive.orchestratorTaskId && (
Planning in progress... View task
)} {/* PR link */} {directive.prUrl && ( )} {/* Completion task indicator */} {directive.completionTaskId && (
{directive.prUrl ? "Updating PR..." : "Creating PR..."} View task
)} {/* Pending Questions */} {directiveQuestions.length > 0 && (
{directiveQuestions.map((q) => ( submitAnswer(q.questionId, response)} /> ))}
)} {/* Controls */}
{(directive.status === "draft" || directive.status === "paused") && ( )} {directive.status === "active" && ( <> )} {directive.status === "idle" && (
All steps done. Update goal to add new work.
)} {hasTerminalTasks && ( )} {completedSteps > 0 && !directive.completionTaskId && ( )}
{pickUpResult && (
{pickUpResult}
)}
{/* Goal */}
Goal {!editingGoal && ( )}
{editingGoal ? (