import { useState, useMemo, useEffect, useRef } from "react"; import type { DirectiveWithSteps, DirectiveStatus, UpdateDirectiveRequest, DirectiveOrderGroup, CreateDOGRequest, UpdateDOGRequest } from "../../lib/api"; import { DirectiveDAG } from "./DirectiveDAG"; import type { SpecializedStep } from "./DirectiveDAG"; import { DirectiveLogStream } from "./DirectiveLogStream"; import { TaskSlideOutPanel } from "./TaskSlideOutPanel"; import { DOGList } from "./DOGList"; 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; onCleanup: () => void; onPickUpOrders: () => Promise<{ message: string; orderCount: number; taskId: string | null } | null>; onCreatePR: () => Promise; dogs: DirectiveOrderGroup[]; dogsLoading: boolean; onCreateDog: (req: CreateDOGRequest) => Promise; onUpdateDog: (dogId: string, req: UpdateDOGRequest) => Promise; onDeleteDog: (dogId: string) => Promise; onPickUpDogOrders: (dogId: string) => Promise; } export function DirectiveDetail({ directive, onStart, onPause, onAdvance, onCompleteStep, onFailStep, onSkipStep, onUpdateGoal, onUpdate, onDelete, onRefresh, onCleanup, onPickUpOrders, onCreatePR, dogs, dogsLoading, onCreateDog, onUpdateDog, onDeleteDog, onPickUpDogOrders, }: DirectiveDetailProps) { const [activeTab, setActiveTab] = useState<"steps" | "dogs">("steps"); 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); const [slideOutTaskId, setSlideOutTaskId] = useState(null); const handleViewTask = (taskId: string) => { setSlideOutTaskId(taskId); }; // 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; // 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 // Build specialized steps for DAG visualization const specializedSteps = useMemo(() => { const steps: SpecializedStep[] = []; if (directive.orchestratorTaskId) { steps.push({ id: `orchestrator-${directive.orchestratorTaskId}`, name: taskMap.get(directive.orchestratorTaskId) || "Planning", type: "orchestrator", taskId: directive.orchestratorTaskId, status: "running", }); } if (directive.completionTaskId) { steps.push({ id: `completion-${directive.completionTaskId}`, name: directive.prUrl ? "Updating PR" : "Creating PR", type: "completion", taskId: directive.completionTaskId, status: "running", }); } return steps; }, [directive.orchestratorTaskId, directive.completionTaskId, directive.prUrl, taskMap]); // 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); }; // Find the task name for the slide-out panel const slideOutTaskName = slideOutTaskId ? (directive.steps.find((s) => s.taskId === slideOutTaskId)?.name ?? taskMap.get(slideOutTaskId) ?? undefined) : undefined; 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 */}
{(["auto", "semi-auto", "manual"] as const).map((mode) => { const isActive = directive.reconcileMode === mode; const modeStyles: Record = { auto: isActive ? "text-[#9bc3ff] bg-[#1a2540]" : "text-[#445566] hover:text-[#7788aa]", "semi-auto": isActive ? "text-amber-400 bg-amber-900/20" : "text-[#445566] hover:text-[#7788aa]", manual: isActive ? "text-orange-400 bg-orange-900/20" : "text-[#445566] hover:text-[#7788aa]", }; const labels: Record = { auto: "Auto", "semi-auto": "Semi", manual: "Manual" }; return ( ); })}
{directive.reconcileMode === "auto" && "Questions timeout after 30s"} {directive.reconcileMode === "semi-auto" && "Questions pause execution"} {directive.reconcileMode === "manual" && "Tasks ask clarifying questions"}
{/* PR link */} {directive.prUrl && ( )} {/* 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.
)} {completedSteps > 0 && !directive.completionTaskId && ( )}
{pickUpResult && (
{pickUpResult}
)}
{/* Goal */}
Goal {!editingGoal && ( )}
{editingGoal ? (