import { useState, useMemo, useEffect, useRef } from "react"; import type { DirectiveWithSteps, DirectiveStatus } from "../../lib/api"; import { DirectiveDAG } from "./DirectiveDAG"; import { DirectiveLogStream } from "./DirectiveLogStream"; import { useMultiTaskSubscription } from "../../hooks/useMultiTaskSubscription"; 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; 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 | 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)); // 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 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}`}
)} {/* 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
)} {/* Controls */}
{(directive.status === "draft" || directive.status === "paused") && ( )} {directive.status === "active" && ( <> )} {directive.status === "idle" && (
All steps done. Update goal to add new work.
)} {hasTerminalTasks && ( )}
{/* Goal */}
Goal {!editingGoal && ( )}
{editingGoal ? (