import { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { X, Plus, Pencil, Trash2, ChevronRight, ChevronDown, ChevronLeft, Target, Layers, Package, Box, Check, Loader2, Link, Unlink, Play, Square, Sparkles, Zap, Activity, FileText, RefreshCw, AlertCircle, } from "lucide-react"; import type { ToastType } from "../hooks/useToast"; import { subscribeSse } from "../sse-bus"; import { MissionInterviewModal } from "./MissionInterviewModal"; import { MilestoneSliceInterviewModal } from "./MilestoneSliceInterviewModal"; import type { Mission, MissionWithHierarchy, MissionWithSummary, Milestone, Slice, MissionFeature, MissionStatus, MilestoneStatus, SliceStatus, FeatureStatus, MilestoneWithSlices, SliceWithFeatures, MissionHealth, MissionEvent, MissionEventType, FeatureLoopState, MissionAssertionStatus, MissionContractAssertion, ContractAssertionCreateInput, ContractAssertionUpdateInput, MilestoneValidationRollup, MilestoneValidationTelemetry, MissionFeatureLoopSnapshot, MissionValidatorRun, } from "./mission-types"; import { fetchMissions, createMission, fetchMission, updateMission, deleteMission, createMilestone, updateMilestone, deleteMilestone, createSlice, updateSlice, deleteSlice, activateSlice, createFeature, updateFeature, deleteFeature, linkFeatureToTask, unlinkFeatureFromTask, triageFeature, triageAllSliceFeatures, previewEnrichedDescription, resumeMission, stopMission, startMission, updateMissionAutopilot, fetchMissionHealth, fetchMissionsHealth, fetchMissionEvents, fetchAssertions, createAssertion, updateAssertion, deleteAssertion, reorderAssertions, linkFeatureToAssertion, unlinkFeatureFromAssertion, fetchAssertionsForFeature, fetchFeaturesForAssertion, fetchMilestoneValidation, fetchMilestoneValidationTelemetry, triggerValidation, fetchValidationLoopState, fetchValidationRuns, fetchValidationRun, fetchAssertion, fetchAiSessions, fetchAiSession, type AiSessionSummary, } from "../api"; import type { AutopilotStatus as AutopilotStatusType, AutopilotState } from "./mission-types"; interface MissionManagerProps { isOpen: boolean; isInline?: boolean; onClose: () => void; addToast: (message: string, type?: ToastType) => void; projectId?: string; onSelectTask?: (taskId: string) => void; availableTasks?: Array<{ id: string; title?: string }>; resumeSessionId?: string; /** Pre-select and load this mission when the modal opens */ targetMissionId?: string; /** Resume session ID for milestone/slice interview sessions */ milestoneSliceResumeSessionId?: string; /** Called when milestone/slice resume session fetch fails */ onMilestoneSliceResumeFetchError?: () => void; } // Status badge colors — use CSS custom-property-compatible tokens const missionStatusColors: Record = { planning: { bg: "var(--mission-planning-bg)", text: "var(--mission-planning-text)" }, active: { bg: "var(--mission-active-bg)", text: "var(--mission-active-text)" }, blocked: { bg: "var(--mission-blocked-bg)", text: "var(--mission-blocked-text)" }, complete: { bg: "var(--mission-complete-bg)", text: "var(--mission-complete-text)" }, archived: { bg: "var(--mission-archived-bg)", text: "var(--mission-archived-text)" }, }; const milestoneStatusColors: Record = { planning: { bg: "var(--mission-planning-bg)", text: "var(--mission-planning-text)" }, active: { bg: "var(--mission-active-bg)", text: "var(--mission-active-text)" }, blocked: { bg: "var(--mission-blocked-bg)", text: "var(--mission-blocked-text)" }, complete: { bg: "var(--mission-complete-bg)", text: "var(--mission-complete-text)" }, }; const sliceStatusColors: Record = { pending: { bg: "var(--slice-pending-bg)", text: "var(--slice-pending-text)" }, active: { bg: "var(--slice-active-bg)", text: "var(--slice-active-text)" }, complete: { bg: "var(--slice-complete-bg)", text: "var(--slice-complete-text)" }, }; const featureStatusColors: Record = { defined: { bg: "var(--feature-defined-bg)", text: "var(--feature-defined-text)" }, triaged: { bg: "var(--feature-triaged-bg)", text: "var(--feature-triaged-text)" }, "in-progress": { bg: "var(--feature-in-progress-bg)", text: "var(--feature-in-progress-text)" }, done: { bg: "var(--feature-done-bg)", text: "var(--feature-done-text)" }, }; const autopilotStateColors: Record = { inactive: { bg: "var(--autopilot-inactive-bg)", text: "var(--autopilot-inactive-text)" }, watching: { bg: "var(--autopilot-watching-bg)", text: "var(--autopilot-watching-text)" }, activating: { bg: "var(--autopilot-activating-bg)", text: "var(--autopilot-activating-text)" }, completing: { bg: "var(--autopilot-completing-bg)", text: "var(--autopilot-completing-text)" }, }; /** Loop state colors for feature execution loop */ const loopStateColors: Record = { idle: { bg: "var(--loop-idle-bg)", text: "var(--loop-idle-text)", indicator: "var(--loop-idle-indicator)" }, implementing: { bg: "var(--loop-implementing-bg)", text: "var(--loop-implementing-text)", indicator: "var(--loop-implementing-indicator)" }, validating: { bg: "var(--loop-validating-bg)", text: "var(--loop-validating-text)", indicator: "var(--loop-validating-indicator)" }, needs_fix: { bg: "var(--loop-needs-fix-bg)", text: "var(--loop-needs-fix-text)", indicator: "var(--loop-needs-fix-indicator)" }, passed: { bg: "var(--loop-passed-bg)", text: "var(--loop-passed-text)", indicator: "var(--loop-passed-indicator)" }, blocked: { bg: "var(--loop-blocked-bg)", text: "var(--loop-blocked-text)", indicator: "var(--loop-blocked-indicator)" }, }; /** Assertion status colors */ const assertionStatusColors: Record = { pending: { bg: "var(--assertion-pending-bg)", text: "var(--assertion-pending-text)" }, passed: { bg: "var(--assertion-passed-bg)", text: "var(--assertion-passed-text)" }, failed: { bg: "var(--assertion-failed-bg)", text: "var(--assertion-failed-text)" }, blocked: { bg: "var(--assertion-blocked-bg)", text: "var(--assertion-blocked-text)" }, }; const validationStateColors: Record = { not_started: { bg: "var(--assertion-pending-bg)", text: "var(--assertion-pending-text)" }, needs_coverage: { bg: "var(--loop-needs-fix-bg)", text: "var(--loop-needs-fix-text)" }, ready: { bg: "var(--loop-validating-bg)", text: "var(--loop-validating-text)" }, passed: { bg: "var(--loop-passed-bg)", text: "var(--loop-passed-text)" }, failed: { bg: "var(--loop-blocked-bg)", text: "var(--loop-blocked-text)" }, blocked: { bg: "var(--loop-blocked-bg)", text: "var(--loop-blocked-text)" }, }; const featureRetryBudgetMax = 3; /** Get the plan state for a milestone (derived from interviewState) */ function getMilestonePlanState(interviewState?: string): "not_started" | "planned" | "needs_update" { if (interviewState === "completed") return "planned"; if (interviewState === "needs_update") return "needs_update"; return "not_started"; } /** Render a plan state indicator badge */ function PlanStateIndicator({ state }: { state: "not_started" | "planned" | "needs_update" }) { const stateClass = state === "planned" ? "mission-plan-state-indicator--planned" : state === "needs_update" ? "mission-plan-state-indicator--needs-update" : "mission-plan-state-indicator--not-started"; const title = state === "planned" ? "Planned" : state === "needs_update" ? "Needs update" : "Not planned"; return ( ); } /** Convert validation state snake_case to human-readable label */ function formatValidationState(state?: string): string { if (!state) return "Not started"; // Replace underscores with spaces and title-case the result return state.replace(/_/g, " ").replace(/^\w/, (c) => c.toUpperCase()); } // Form types interface MissionFormData { title: string; description: string; status: MissionStatus; autopilotEnabled: boolean; } interface MilestoneFormData { title: string; description: string; status: MilestoneStatus; dependencies: string[]; } interface SliceFormData { title: string; description: string; status: SliceStatus; } interface FeatureFormData { title: string; description: string; acceptanceCriteria: string; status: FeatureStatus; } const EMPTY_MISSION_FORM: MissionFormData = { title: "", description: "", status: "planning", autopilotEnabled: false, }; const EMPTY_MILESTONE_FORM: MilestoneFormData = { title: "", description: "", status: "planning", dependencies: [], }; const EMPTY_SLICE_FORM: SliceFormData = { title: "", description: "", status: "pending", }; const EMPTY_FEATURE_FORM: FeatureFormData = { title: "", description: "", acceptanceCriteria: "", status: "defined", }; type MissionHealthState = "healthy" | "warning" | "error"; const HOUR_MS = 60 * 60 * 1000; function getRelativeTime(timestamp?: string): string { if (!timestamp) return "—"; const ts = new Date(timestamp).getTime(); if (Number.isNaN(ts)) return "—"; const diffMs = Date.now() - ts; if (diffMs < 0) return "just now"; const diffMinutes = Math.floor(diffMs / (60 * 1000)); if (diffMinutes < 1) return "just now"; if (diffMinutes < 60) return `${diffMinutes}m ago`; const diffHours = Math.floor(diffMinutes / 60); if (diffHours < 24) return `${diffHours}h ago`; const diffDays = Math.floor(diffHours / 24); return `${diffDays}d ago`; } function getMissionHealthState(health?: MissionHealth): MissionHealthState { if (!health) return "healthy"; const hasRecentError = typeof health.lastErrorAt === "string" && Date.now() - new Date(health.lastErrorAt).getTime() <= HOUR_MS; const failureRateThresholdExceeded = health.totalTasks > 0 && health.tasksFailed > health.totalTasks * 0.3; if (hasRecentError || failureRateThresholdExceeded) { return "error"; } if (health.tasksFailed > 0) { return "warning"; } if (health.tasksFailed === 0 && health.tasksInFlight <= health.totalTasks) { return "healthy"; } return "warning"; } function isMissionHealth(value: unknown): value is MissionHealth { if (!value || typeof value !== "object") return false; const candidate = value as Partial; return ( typeof candidate.missionId === "string" && typeof candidate.tasksCompleted === "number" && typeof candidate.tasksFailed === "number" && typeof candidate.tasksInFlight === "number" && typeof candidate.totalTasks === "number" && typeof candidate.estimatedCompletionPercent === "number" ); } function isMissionEvent(value: unknown): value is MissionEvent { if (!value || typeof value !== "object") return false; const candidate = value as Partial; return ( typeof candidate.id === "string" && typeof candidate.missionId === "string" && typeof candidate.eventType === "string" && typeof candidate.description === "string" && typeof candidate.timestamp === "string" ); } function isMilestoneValidationTelemetry(value: unknown): value is MilestoneValidationTelemetry { if (!value || typeof value !== "object") { return false; } const candidate = value as Partial; return ( typeof candidate.rollup?.milestoneId === "string" && typeof candidate.rollup?.state === "string" && Array.isArray(candidate.validationTelemetry?.validationRounds) && typeof candidate.validationTelemetry?.totalRuns === "number" && candidate.validationContract !== undefined && Array.isArray(candidate.fixFeatures) ); } const TASK_EVENT_TYPES: MissionEventType[] = ["feature_triaged", "feature_completed"]; const SLICE_EVENT_TYPES: MissionEventType[] = ["slice_activated", "slice_completed", "milestone_completed"]; const STATE_CHANGE_EVENT_TYPES: MissionEventType[] = [ "mission_started", "mission_paused", "mission_resumed", "mission_completed", ]; const AUTOPILOT_EVENT_TYPES: MissionEventType[] = [ "autopilot_enabled", "autopilot_disabled", "autopilot_state_changed", "autopilot_retry", "autopilot_stale", ]; function matchesEventFilter( eventType: MissionEventType, filter: "all" | "errors" | "state_changes" | "tasks" | "slices" | "autopilot", ): boolean { switch (filter) { case "errors": return eventType === "error" || eventType === "warning"; case "state_changes": return STATE_CHANGE_EVENT_TYPES.includes(eventType); case "tasks": return TASK_EVENT_TYPES.includes(eventType); case "slices": return SLICE_EVENT_TYPES.includes(eventType); case "autopilot": return AUTOPILOT_EVENT_TYPES.includes(eventType); default: return true; } } function getEventTypeClassName(eventType: MissionEventType): string { if (eventType === "error" || eventType === "warning") { return "mission-event__type--error"; } if (STATE_CHANGE_EVENT_TYPES.includes(eventType)) { return "mission-event__type--state"; } if (TASK_EVENT_TYPES.includes(eventType)) { return "mission-event__type--task"; } if (SLICE_EVENT_TYPES.includes(eventType)) { return "mission-event__type--slice"; } if (AUTOPILOT_EVENT_TYPES.includes(eventType)) { return "mission-event__type--autopilot"; } return "mission-event__type--default"; } function getEventTypeLabel(eventType: MissionEventType): string { return eventType.replace(/_/g, " "); } function getActivityQueryEventType( _filter: "all" | "errors" | "state_changes" | "tasks" | "slices" | "autopilot", ): MissionEventType | undefined { // Keep query unfiltered to support grouped UI filters (e.g. errors + warnings). return undefined; } function getAutopilotActivitySummary(state: AutopilotState, lastActivityAt?: string): string | null { if (!lastActivityAt) { return null; } if (state === "watching") { return `Watching since ${getRelativeTime(lastActivityAt)}`; } return `Last activation ${getRelativeTime(lastActivityAt)}`; } export function MissionManager({ isOpen, isInline = false, onClose, addToast, projectId, onSelectTask, availableTasks = [], resumeSessionId, targetMissionId, milestoneSliceResumeSessionId, onMilestoneSliceResumeFetchError }: MissionManagerProps) { const isActive = isInline || isOpen; const [missions, setMissions] = useState([]); const [selectedMission, setSelectedMission] = useState(null); const [loading, setLoading] = useState(true); const [detailLoading, setDetailLoading] = useState(false); // Form states const [isCreatingMission, setIsCreatingMission] = useState(false); const [editingMissionId, setEditingMissionId] = useState(null); const [missionForm, setMissionForm] = useState(EMPTY_MISSION_FORM); const [saving, setSaving] = useState(false); const [expandedMilestones, setExpandedMilestones] = useState>(new Set()); const [expandedSlices, setExpandedSlices] = useState>(new Set()); // Editing states for nested items const [editingMilestoneId, setEditingMilestoneId] = useState(null); const [milestoneForm, setMilestoneForm] = useState(EMPTY_MILESTONE_FORM); const [isCreatingMilestone, setIsCreatingMilestone] = useState(false); const [editingSliceId, setEditingSliceId] = useState(null); const [sliceForm, setSliceForm] = useState(EMPTY_SLICE_FORM); const [isCreatingSlice, setIsCreatingSlice] = useState(false); const [selectedMilestoneIdForNewSlice, setSelectedMilestoneIdForNewSlice] = useState(null); const [editingFeatureId, setEditingFeatureId] = useState(null); const [featureForm, setFeatureForm] = useState(EMPTY_FEATURE_FORM); const [isCreatingFeature, setIsCreatingFeature] = useState(false); const [selectedSliceIdForNewFeature, setSelectedSliceIdForNewFeature] = useState(null); // Link task modal state const [linkTaskFeatureId, setLinkTaskFeatureId] = useState(null); const [selectedTaskId, setSelectedTaskId] = useState(""); // AI Interview modal const [showInterviewModal, setShowInterviewModal] = useState(false); // Pending mission interview sessions (for resume prompt after page reload) const [pendingInterviewSessions, setPendingInterviewSessions] = useState([]); const [localResumeSessionId, setLocalResumeSessionId] = useState(undefined); const effectiveResumeSessionId = localResumeSessionId ?? resumeSessionId; // Milestone/Slice interview modal const [interviewTarget, setInterviewTarget] = useState<{ type: "milestone" | "slice"; id: string; title: string; resumeSessionId?: string; } | null>(null); // Triage preview state const [triagePreview, setTriagePreview] = useState<{ featureId: string; enrichedDescription: string; } | null>(null); const [triagePreviewLoading, setTriagePreviewLoading] = useState(null); // Auto-open interview modal when resuming a session useEffect(() => { if (isActive && effectiveResumeSessionId) { setShowInterviewModal(true); } }, [isActive, effectiveResumeSessionId]); // Detect pending mission interview sessions for resume prompt useEffect(() => { if (!isActive || effectiveResumeSessionId) return; let cancelled = false; fetchAiSessions(projectId).then((sessions) => { if (cancelled) return; const pending = sessions.filter( (s) => s.type === "mission_interview" && (s.status === "awaiting_input" || s.status === "error"), ); setPendingInterviewSessions(pending); }).catch((err) => { console.warn("[MissionManager] Failed to fetch pending interview sessions:", err); }); return () => { cancelled = true; }; }, [isActive, projectId, effectiveResumeSessionId]); // Auto-open milestone/slice interview modal when resuming from background session useEffect(() => { if (!isActive || !milestoneSliceResumeSessionId) return; let cancelled = false; fetchAiSession(milestoneSliceResumeSessionId).then((session) => { if (cancelled || !session) return; // Parse the inputPayload to get target info try { const payload = JSON.parse(session.inputPayload || "{}"); if (payload.targetId && payload.targetType) { setInterviewTarget({ type: payload.targetType as "milestone" | "slice", id: payload.targetId, title: payload.targetTitle || session.title, resumeSessionId: milestoneSliceResumeSessionId, }); } } catch { // If parsing fails, try to use session title as fallback setInterviewTarget({ type: "milestone", id: "", title: session.title, resumeSessionId: milestoneSliceResumeSessionId, }); } }).catch((err) => { if (cancelled) return; console.warn("[MissionManager] Failed to fetch session for milestone/slice resume:", err); onMilestoneSliceResumeFetchError?.(); }); return () => { cancelled = true; }; }, [isActive, milestoneSliceResumeSessionId, onMilestoneSliceResumeFetchError]); // Delete confirmation const [deleteConfirmId, setDeleteConfirmId] = useState<{ type: string; id: string } | null>(null); // Assertion panel state const [assertionsByMilestone, setAssertionsByMilestone] = useState>(new Map()); const [assertionsLoading, setAssertionsLoading] = useState(false); const [editingAssertionId, setEditingAssertionId] = useState(null); const [assertionForm, setAssertionForm] = useState<{ title: string; assertion: string; status: MissionAssertionStatus }>({ title: "", assertion: "", status: "pending", }); const [isCreatingAssertion, setIsCreatingAssertion] = useState(false); const [expandedAssertionId, setExpandedAssertionId] = useState(null); const [linkedFeaturesByAssertion, setLinkedFeaturesByAssertion] = useState>(new Map()); const [linkingAssertions, setLinkingAssertions] = useState>(new Set()); const [unlinkingFeatures, setUnlinkingFeatures] = useState>(new Set()); const [featurePickerOpenForAssertion, setFeaturePickerOpenForAssertion] = useState(null); const [validationRollupByMilestone, setValidationRollupByMilestone] = useState>(new Map()); const [selectedMilestoneId, setSelectedMilestoneId] = useState(null); const [validationTelemetry, setValidationTelemetry] = useState(null); const [validationRoundsExpanded, setValidationRoundsExpanded] = useState(true); const [validatingFeatures, setValidatingFeatures] = useState>(new Set()); // Feature loop state const [featureLoopStates, setFeatureLoopStates] = useState>(new Map()); // Expanded feature for run history display const [expandedFeatureId, setExpandedFeatureId] = useState(null); // Validation runs by feature const [validationRunsByFeature, setValidationRunsByFeature] = useState>(new Map()); // Expanded run ID for showing details with failures const [expandedRunId, setExpandedRunId] = useState(null); // Run details with failures (keyed by runId) const [runDetailsByRunId, setRunDetailsByRunId] = useState }>>(new Map()); const [missionHealthById, setMissionHealthById] = useState>(new Map()); const [activeTab, setActiveTab] = useState<"structure" | "activity">("structure"); const [missionEvents, setMissionEvents] = useState([]); const missionEventsRef = useRef([]); const missionsRef = useRef([]); const selectedMissionRef = useRef(null); const selectedMilestoneIdRef = useRef(null); const activeTabRef = useRef<"structure" | "activity">("structure"); const eventsFilterRef = useRef<"all" | "errors" | "state_changes" | "tasks" | "slices" | "autopilot">("all"); const [eventsLoading, setEventsLoading] = useState(false); const [eventsTotal, setEventsTotal] = useState(0); const [eventsFilter, setEventsFilter] = useState< "all" | "errors" | "state_changes" | "tasks" | "slices" | "autopilot" >("all"); const [expandedEventMetadata, setExpandedEventMetadata] = useState>(new Set()); const activityEventsContainerRef = useRef(null); const activityEventsEndRef = useRef(null); // Keep latest state available to long-lived SSE handlers without reconnect churn. missionsRef.current = missions; selectedMissionRef.current = selectedMission; selectedMilestoneIdRef.current = selectedMilestoneId; activeTabRef.current = activeTab; eventsFilterRef.current = eventsFilter; const scrollActivityToLatest = useCallback((behavior: ScrollBehavior = "auto") => { const endNode = activityEventsEndRef.current; if (endNode && typeof endNode.scrollIntoView === "function") { endNode.scrollIntoView({ block: "end", behavior }); return; } const container = activityEventsContainerRef.current; if (container) { container.scrollTop = container.scrollHeight; } }, []); const isActivityScrolledNearBottom = useCallback(() => { const container = activityEventsContainerRef.current; if (!container) { return true; } const distanceToBottom = container.scrollHeight - container.scrollTop - container.clientHeight; return distanceToBottom <= 100; }, []); const loadMissionHealth = useCallback(async (missionList: MissionWithSummary[]) => { if (missionList.length === 0) { setMissionHealthById(new Map()); return; } // Use batched endpoint for optimal performance (1 request instead of N) const healthRecord = await fetchMissionsHealth(projectId); setMissionHealthById((prev) => { const next = new Map(prev); for (const [missionId, health] of Object.entries(healthRecord)) { if (isMissionHealth(health)) { next.set(missionId, health); } } return next; }); }, [projectId]); const loadMissions = useCallback(async () => { try { setLoading(true); const data = await fetchMissions(projectId); setMissions(data); void loadMissionHealth(data); } catch (err: any) { addToast(err.message || "Failed to load missions", "error"); } finally { setLoading(false); } }, [addToast, projectId, loadMissionHealth]); const loadMissionDetail = useCallback(async (missionId: string) => { try { setDetailLoading(true); const data = await fetchMission(missionId, projectId); setSelectedMission(data); // Auto-expand first milestone and slice if (data.milestones.length > 0) { const firstMilestoneId = data.milestones[0].id; setSelectedMilestoneId(firstMilestoneId); setValidationRoundsExpanded(true); setExpandedMilestones(new Set([firstMilestoneId])); // Load assertions and validation rollup for the first milestone (inline to avoid forward ref) fetchAssertions(firstMilestoneId, projectId).then((assertions) => { setAssertionsByMilestone((prev) => { const next = new Map(prev); next.set(firstMilestoneId, assertions); return next; }); }).catch(() => { /* silently fail */ }); fetchMilestoneValidation(firstMilestoneId, projectId).then((rollup) => { setValidationRollupByMilestone((prev) => { const next = new Map(prev); next.set(firstMilestoneId, rollup); return next; }); }).catch(() => { /* silently fail */ }); if (data.milestones[0].slices.length > 0) { setExpandedSlices(new Set([data.milestones[0].slices[0].id])); } } else { setSelectedMilestoneId(null); setValidationTelemetry(null); } } catch (err: any) { addToast(err.message || "Failed to load mission details", "error"); } finally { setDetailLoading(false); } }, [addToast, projectId]); useEffect(() => { if (!isActive || !selectedMilestoneId) { setValidationTelemetry(null); return; } let cancelled = false; setValidationTelemetry(null); fetchMilestoneValidationTelemetry(selectedMilestoneId, projectId) .then((telemetry) => { if (cancelled) { return; } if (!isMilestoneValidationTelemetry(telemetry)) { setValidationTelemetry(null); return; } setValidationTelemetry(telemetry); setValidationRollupByMilestone((prev) => { const next = new Map(prev); next.set(selectedMilestoneId, telemetry.rollup); return next; }); }) .catch(() => { if (!cancelled) { setValidationTelemetry(null); } }); return () => { cancelled = true; }; }, [isActive, selectedMilestoneId, projectId]); useEffect(() => { setValidationRoundsExpanded(true); }, [selectedMilestoneId]); const refreshValidationTelemetry = useCallback((milestoneId: string) => { if (!milestoneId || milestoneId !== selectedMilestoneIdRef.current) { return; } void fetchMilestoneValidationTelemetry(milestoneId, projectId) .then((telemetry) => { if (selectedMilestoneIdRef.current !== milestoneId || !isMilestoneValidationTelemetry(telemetry)) { return; } setValidationTelemetry(telemetry); setValidationRollupByMilestone((prev) => { const next = new Map(prev); next.set(milestoneId, telemetry.rollup); return next; }); }) .catch(() => { // Silently fail - telemetry is supplemental }); }, [projectId]); const loadMissionEvents = useCallback(async ( missionId: string, options?: { append?: boolean }, ) => { const append = options?.append ?? false; const offset = append ? missionEventsRef.current.length : 0; if (!append) { setEventsLoading(true); setExpandedEventMetadata(new Set()); } try { const response = await fetchMissionEvents( missionId, { limit: 50, offset, eventType: getActivityQueryEventType(eventsFilter), }, projectId, ); const incomingEvents = response.events.filter((event) => matchesEventFilter(event.eventType, eventsFilter)); setMissionEvents((prev) => { if (!append) { return incomingEvents; } const existing = new Set(prev.map((event) => event.id)); const merged = [...prev]; for (const event of incomingEvents) { if (!existing.has(event.id)) { merged.push(event); } } return merged; }); setEventsTotal(response.total); if (!append) { requestAnimationFrame(() => { scrollActivityToLatest("auto"); }); } } catch (err: any) { addToast(err.message || "Failed to load mission activity", "error"); } finally { if (!append) { setEventsLoading(false); } } }, [addToast, eventsFilter, projectId, scrollActivityToLatest]); useEffect(() => { missionEventsRef.current = missionEvents; }, [missionEvents]); useEffect(() => { if (isActive) { loadMissions(); setSelectedMission(null); setSelectedMilestoneId(null); setValidationTelemetry(null); setMissionEvents([]); setEventsTotal(0); setActiveTab("structure"); setEventsFilter("all"); setExpandedEventMetadata(new Set()); } }, [isActive, loadMissions]); // Auto-load target mission when specified const targetLoadedRef = useRef(null); useEffect(() => { if (isActive && targetMissionId && targetLoadedRef.current !== targetMissionId && missions.length > 0) { targetLoadedRef.current = targetMissionId; loadMissionDetail(targetMissionId); } }, [isActive, targetMissionId, missions, loadMissionDetail]); // Reset target tracking when modal closes useEffect(() => { if (!isActive) { targetLoadedRef.current = null; } }, [isActive]); useEffect(() => { if (!isActive || !selectedMission || activeTab !== "activity") { return; } void loadMissionEvents(selectedMission.id); }, [activeTab, isActive, loadMissionEvents, selectedMission, eventsFilter]); useEffect(() => { if (!isActive || typeof EventSource === "undefined") { return; } const search = new URLSearchParams(); if (projectId) { search.set("projectId", projectId); } const eventUrl = `/api/events${search.size > 0 ? `?${search.toString()}` : ""}`; const refreshHealth = () => { void loadMissionHealth(missionsRef.current); }; const handleMissionUpdated = (rawEvent: Event) => { refreshHealth(); // Update mission status in the list to keep badges in sync const messageEvent = rawEvent as MessageEvent; if (messageEvent.data) { try { const updatedMission = JSON.parse(messageEvent.data); if (updatedMission?.id) { setMissions((prev) => prev.map((m) => m.id === updatedMission.id ? { ...m, ...updatedMission } : m ) ); } } catch { // ignore invalid payloads } } // Reload the selected mission detail to reflect updated mission state (autopilot, status, etc.) if (selectedMissionRef.current) { void loadMissionDetail(selectedMissionRef.current.id); } }; const handleSliceUpdated = (rawEvent: Event) => { refreshHealth(); // Reload the selected mission detail to reflect updated slice status if (selectedMissionRef.current) { void loadMissionDetail(selectedMissionRef.current.id); } }; const handleFeatureUpdated = () => { refreshHealth(); // Reload the selected mission detail to reflect updated feature status if (selectedMissionRef.current) { void loadMissionDetail(selectedMissionRef.current.id); } }; const handleMilestoneUpdated = (_rawEvent: Event) => { refreshHealth(); // Reload the selected mission detail to reflect updated milestone status if (selectedMissionRef.current) { void loadMissionDetail(selectedMissionRef.current.id); } }; // Handler for validator run started - refresh feature loop state and validation runs const handleValidatorRunStarted = (rawEvent: Event) => { const messageEvent = rawEvent as MessageEvent; if (!messageEvent.data) return; try { const payload = JSON.parse(messageEvent.data); if (payload && payload.featureId) { // Refresh feature loop state void loadFeatureLoopState(payload.featureId); // Refresh validation runs void loadValidationRuns(payload.featureId); if (payload.milestoneId) { refreshValidationTelemetry(payload.milestoneId); } } } catch { // ignore invalid payloads } }; // Handler for validator run completed - refresh feature loop state, runs, mission detail, and telemetry const handleValidatorRunCompleted = (rawEvent: Event) => { const messageEvent = rawEvent as MessageEvent; if (!messageEvent.data) return; try { const payload = JSON.parse(messageEvent.data); if (payload && payload.featureId) { // Refresh feature loop state void loadFeatureLoopState(payload.featureId); // Refresh validation runs void loadValidationRuns(payload.featureId); if (payload.milestoneId) { refreshValidationTelemetry(payload.milestoneId); } // Refresh mission detail to update feature status if (selectedMissionRef.current) { void loadMissionDetail(selectedMissionRef.current.id); } } } catch { // ignore invalid payloads } }; // Handler for milestone validation updated - refresh validation rollup const handleMilestoneValidationUpdated = (rawEvent: Event) => { const messageEvent = rawEvent as MessageEvent; if (!messageEvent.data) return; try { const payload = JSON.parse(messageEvent.data); if (payload && payload.milestoneId) { void loadValidationRollup(payload.milestoneId); refreshValidationTelemetry(payload.milestoneId); } } catch { // ignore invalid payloads } }; // Handler for assertion mutations - refresh assertions and validation rollup const handleAssertionMutation = (rawEvent: Event) => { const messageEvent = rawEvent as MessageEvent; if (!messageEvent.data) return; try { const payload = JSON.parse(messageEvent.data); if (payload && payload.milestoneId) { void loadAssertionsForMilestone(payload.milestoneId); void loadValidationRollup(payload.milestoneId); refreshValidationTelemetry(payload.milestoneId); } } catch { // ignore invalid payloads } }; // Handler for fix-feature:created - refresh mission detail to show new fix feature with lineage const handleFixFeatureCreated = (rawEvent: Event) => { const messageEvent = rawEvent as MessageEvent; if (!messageEvent.data) return; try { const payload = JSON.parse(messageEvent.data); if (payload && payload.sourceFeatureId) { // Refresh feature loop state for the source feature void loadFeatureLoopState(payload.sourceFeatureId); const createdFeatureSliceId = payload?.feature?.sliceId as string | undefined; const selectedMission = selectedMissionRef.current; if (createdFeatureSliceId && selectedMission) { const containingMilestone = selectedMission.milestones.find((milestone) => milestone.slices.some((slice) => slice.id === createdFeatureSliceId) ); if (containingMilestone) { refreshValidationTelemetry(containingMilestone.id); } } // Refresh mission detail to show the new fix feature in the list if (selectedMissionRef.current) { void loadMissionDetail(selectedMissionRef.current.id); } } } catch { // ignore invalid payloads } }; const handleMissionEvent = (rawEvent: Event) => { refreshHealth(); const currentSelectedMission = selectedMissionRef.current; if (!currentSelectedMission || activeTabRef.current !== "activity") { return; } const shouldAutoScroll = isActivityScrolledNearBottom(); const messageEvent = rawEvent as MessageEvent; if (!messageEvent.data) { return; } try { const payload = JSON.parse(messageEvent.data); if (!isMissionEvent(payload)) { return; } if (payload.missionId !== currentSelectedMission.id) { return; } if (!matchesEventFilter(payload.eventType, eventsFilterRef.current)) { return; } setMissionEvents((prev) => { const withoutExisting = prev.filter((event) => event.id !== payload.id); return [payload, ...withoutExisting].slice(0, 100); }); setEventsTotal((prev) => prev + 1); if (shouldAutoScroll) { requestAnimationFrame(() => { const container = activityEventsContainerRef.current; if (container) { container.scrollTop = 0; } }); } } catch { // ignore invalid payloads } }; return subscribeSse(eventUrl, { events: { "mission:updated": handleMissionUpdated, "slice:updated": handleSliceUpdated, "feature:updated": handleFeatureUpdated, "milestone:updated": handleMilestoneUpdated, "mission:event": handleMissionEvent, "validator-run:started": handleValidatorRunStarted, "validator-run:completed": handleValidatorRunCompleted, "milestone:validation:updated": handleMilestoneValidationUpdated, "assertion:created": handleAssertionMutation, "assertion:updated": handleAssertionMutation, "assertion:deleted": handleAssertionMutation, "assertion:linked": handleAssertionMutation, "assertion:unlinked": handleAssertionMutation, "fix-feature:created": handleFixFeatureCreated, }, }); }, [ isActive, isActivityScrolledNearBottom, loadMissionDetail, loadMissionHealth, projectId, refreshValidationTelemetry, ]); // Mission handlers const handleCreateMission = useCallback(() => { setIsCreatingMission(true); setEditingMissionId(null); setMissionForm(EMPTY_MISSION_FORM); }, []); const handleEditMission = useCallback((mission: Mission) => { setEditingMissionId(mission.id); setIsCreatingMission(false); setMissionForm({ title: mission.title, description: mission.description || "", status: mission.status, autopilotEnabled: mission.autopilotEnabled ?? false, }); }, []); const handleCancelMission = useCallback(() => { setEditingMissionId(null); setIsCreatingMission(false); setMissionForm(EMPTY_MISSION_FORM); }, []); const handleSaveMission = useCallback(async () => { if (!missionForm.title.trim()) { addToast("Mission title is required", "error"); return; } try { setSaving(true); if (isCreatingMission) { await createMission({ title: missionForm.title.trim(), description: missionForm.description.trim() || undefined, autopilotEnabled: missionForm.autopilotEnabled, }, projectId); addToast("Mission created", "success"); } else if (editingMissionId) { // Build update payload - when autopilot is enabled, also set autoAdvance // for backward compat with the engine (though engine no longer reads it) const updates: Record = { title: missionForm.title.trim(), description: missionForm.description.trim() || undefined, status: missionForm.status, autopilotEnabled: missionForm.autopilotEnabled, }; if (missionForm.autopilotEnabled) { updates.autoAdvance = true; } await updateMission(editingMissionId, updates as Parameters[1], projectId); addToast("Mission updated", "success"); // Refresh detail view if viewing this mission if (selectedMission?.id === editingMissionId) { await loadMissionDetail(editingMissionId); } } await loadMissions(); handleCancelMission(); } catch (err: any) { addToast(err.message || "Failed to save mission", "error"); } finally { setSaving(false); } }, [missionForm, isCreatingMission, editingMissionId, addToast, loadMissions, loadMissionDetail, selectedMission, handleCancelMission, projectId]); const handleDeleteMission = useCallback(async (missionId: string) => { try { await deleteMission(missionId, projectId); addToast("Mission deleted", "success"); if (selectedMission?.id === missionId) { setSelectedMission(null); } await loadMissions(); setDeleteConfirmId(null); } catch (err: any) { addToast(err.message || "Failed to delete mission", "error"); } }, [addToast, loadMissions, selectedMission, projectId]); // Milestone handlers const handleCreateMilestone = useCallback(() => { setIsCreatingMilestone(true); setEditingMilestoneId(null); setMilestoneForm(EMPTY_MILESTONE_FORM); }, []); const handleEditMilestone = useCallback((milestone: Milestone) => { setEditingMilestoneId(milestone.id); setIsCreatingMilestone(false); setMilestoneForm({ title: milestone.title, description: milestone.description || "", status: milestone.status, dependencies: milestone.dependencies, }); }, []); const handleCancelMilestone = useCallback(() => { setEditingMilestoneId(null); setIsCreatingMilestone(false); setMilestoneForm(EMPTY_MILESTONE_FORM); }, []); const handleSaveMilestone = useCallback(async () => { if (!milestoneForm.title.trim()) { addToast("Milestone title is required", "error"); return; } try { setSaving(true); if (isCreatingMilestone && selectedMission) { await createMilestone(selectedMission.id, { title: milestoneForm.title.trim(), description: milestoneForm.description.trim() || undefined, dependencies: milestoneForm.dependencies, }, projectId); addToast("Milestone created", "success"); } else if (editingMilestoneId) { await updateMilestone(editingMilestoneId, { title: milestoneForm.title.trim(), description: milestoneForm.description.trim() || undefined, status: milestoneForm.status, dependencies: milestoneForm.dependencies, }, projectId); addToast("Milestone updated", "success"); } await loadMissionDetail(selectedMission!.id); handleCancelMilestone(); } catch (err: any) { addToast(err.message || "Failed to save milestone", "error"); } finally { setSaving(false); } }, [milestoneForm, isCreatingMilestone, editingMilestoneId, selectedMission, addToast, loadMissionDetail, handleCancelMilestone, missionForm.title, projectId]); const handleDeleteMilestone = useCallback(async (milestoneId: string) => { try { await deleteMilestone(milestoneId, projectId); addToast("Milestone deleted", "success"); await loadMissionDetail(selectedMission!.id); setDeleteConfirmId(null); } catch (err: any) { addToast(err.message || "Failed to delete milestone", "error"); } }, [addToast, loadMissionDetail, selectedMission, projectId]); const toggleMilestoneExpanded = useCallback((milestoneId: string) => { setSelectedMilestoneId(milestoneId); setValidationRoundsExpanded(true); setExpandedMilestones((prev) => { const next = new Set(prev); const isExpanding = !next.has(milestoneId); if (isExpanding) { next.add(milestoneId); // Load assertions and validation rollup when expanding milestone fetchAssertions(milestoneId, projectId).then((assertions) => { setAssertionsByMilestone((prev) => { const next = new Map(prev); next.set(milestoneId, assertions); return next; }); }).catch(() => { /* silently fail */ }); fetchMilestoneValidation(milestoneId, projectId).then((rollup) => { setValidationRollupByMilestone((prev) => { const next = new Map(prev); next.set(milestoneId, rollup); return next; }); }).catch(() => { /* silently fail */ }); } else { next.delete(milestoneId); } return next; }); }, [projectId]); // Slice handlers const handleCreateSlice = useCallback((milestoneId: string) => { setSelectedMilestoneIdForNewSlice(milestoneId); setIsCreatingSlice(true); setEditingSliceId(null); setSliceForm(EMPTY_SLICE_FORM); }, []); const handleEditSlice = useCallback((slice: Slice) => { setEditingSliceId(slice.id); setIsCreatingSlice(false); setSliceForm({ title: slice.title, description: slice.description || "", status: slice.status, }); }, []); const handleCancelSlice = useCallback(() => { setEditingSliceId(null); setIsCreatingSlice(false); setSelectedMilestoneIdForNewSlice(null); setSliceForm(EMPTY_SLICE_FORM); }, []); const handleSaveSlice = useCallback(async () => { if (!sliceForm.title.trim()) { addToast("Slice title is required", "error"); return; } try { setSaving(true); if (isCreatingSlice && selectedMilestoneIdForNewSlice) { await createSlice(selectedMilestoneIdForNewSlice, { title: sliceForm.title.trim(), description: sliceForm.description.trim() || undefined, }, projectId); addToast("Slice created", "success"); } else if (editingSliceId) { await updateSlice(editingSliceId, { title: sliceForm.title.trim(), description: sliceForm.description.trim() || undefined, status: sliceForm.status, }, projectId); addToast("Slice updated", "success"); } await loadMissionDetail(selectedMission!.id); handleCancelSlice(); } catch (err: any) { addToast(err.message || "Failed to save slice", "error"); } finally { setSaving(false); } }, [sliceForm, isCreatingSlice, editingSliceId, selectedMilestoneIdForNewSlice, selectedMission, addToast, loadMissionDetail, handleCancelSlice, projectId]); const handleDeleteSlice = useCallback(async (sliceId: string) => { try { await deleteSlice(sliceId, projectId); addToast("Slice deleted", "success"); await loadMissionDetail(selectedMission!.id); setDeleteConfirmId(null); } catch (err: any) { addToast(err.message || "Failed to delete slice", "error"); } }, [addToast, loadMissionDetail, selectedMission, projectId]); const handleActivateSlice = useCallback(async (sliceId: string) => { try { await activateSlice(sliceId, projectId); addToast("Slice activated", "success"); await loadMissionDetail(selectedMission!.id); } catch (err: any) { addToast(err.message || "Failed to activate slice", "error"); } }, [addToast, loadMissionDetail, selectedMission, projectId]); const toggleSliceExpanded = useCallback((sliceId: string) => { setExpandedSlices((prev) => { const next = new Set(prev); if (next.has(sliceId)) { next.delete(sliceId); } else { next.add(sliceId); } return next; }); }, []); // Feature handlers const handleCreateFeature = useCallback((sliceId: string) => { setSelectedSliceIdForNewFeature(sliceId); setIsCreatingFeature(true); setEditingFeatureId(null); setFeatureForm(EMPTY_FEATURE_FORM); }, []); const handleEditFeature = useCallback((feature: MissionFeature) => { setEditingFeatureId(feature.id); setIsCreatingFeature(false); setFeatureForm({ title: feature.title, description: feature.description || "", acceptanceCriteria: feature.acceptanceCriteria || "", status: feature.status, }); }, []); const handleCancelFeature = useCallback(() => { setEditingFeatureId(null); setIsCreatingFeature(false); setSelectedSliceIdForNewFeature(null); setFeatureForm(EMPTY_FEATURE_FORM); }, []); const handleSaveFeature = useCallback(async () => { if (!featureForm.title.trim()) { addToast("Feature title is required", "error"); return; } try { setSaving(true); if (isCreatingFeature && selectedSliceIdForNewFeature) { await createFeature(selectedSliceIdForNewFeature, { title: featureForm.title.trim(), description: featureForm.description.trim() || undefined, acceptanceCriteria: featureForm.acceptanceCriteria.trim() || undefined, }, projectId); addToast("Feature created", "success"); } else if (editingFeatureId) { await updateFeature(editingFeatureId, { title: featureForm.title.trim(), description: featureForm.description.trim() || undefined, acceptanceCriteria: featureForm.acceptanceCriteria.trim() || undefined, status: featureForm.status, }, projectId); addToast("Feature updated", "success"); } await loadMissionDetail(selectedMission!.id); handleCancelFeature(); } catch (err: any) { addToast(err.message || "Failed to save feature", "error"); } finally { setSaving(false); } }, [featureForm, isCreatingFeature, editingFeatureId, selectedSliceIdForNewFeature, selectedMission, addToast, loadMissionDetail, handleCancelFeature, projectId]); const handleDeleteFeature = useCallback(async (featureId: string) => { try { await deleteFeature(featureId, projectId); addToast("Feature deleted", "success"); await loadMissionDetail(selectedMission!.id); setDeleteConfirmId(null); } catch (err: any) { addToast(err.message || "Failed to delete feature", "error"); } }, [addToast, loadMissionDetail, selectedMission, projectId]); const handleLinkTask = useCallback(async () => { if (!linkTaskFeatureId || !selectedTaskId.trim()) { addToast("Task ID is required", "error"); return; } try { await linkFeatureToTask(linkTaskFeatureId, selectedTaskId.trim(), projectId); addToast("Feature linked to task", "success"); await loadMissionDetail(selectedMission!.id); setLinkTaskFeatureId(null); setSelectedTaskId(""); } catch (err: any) { addToast(err.message || "Failed to link feature to task", "error"); } }, [linkTaskFeatureId, selectedTaskId, addToast, loadMissionDetail, selectedMission, projectId]); const handleUnlinkTask = useCallback(async (featureId: string) => { try { await unlinkFeatureFromTask(featureId, projectId); addToast("Feature unlinked from task", "success"); await loadMissionDetail(selectedMission!.id); } catch (err: any) { addToast(err.message || "Failed to unlink feature", "error"); } }, [addToast, loadMissionDetail, selectedMission, projectId]); // Triage a single feature — creates a task and links it const handleTriageFeature = useCallback(async (featureId: string) => { try { setSaving(true); await triageFeature(featureId, undefined, undefined, projectId); addToast("Feature triaged — task created", "success"); await loadMissionDetail(selectedMission!.id); } catch (err: any) { addToast(err.message || "Failed to triage feature", "error"); } finally { setSaving(false); } }, [addToast, loadMissionDetail, selectedMission, projectId]); // Triage with preview — fetches enriched description first const handleTriageFeatureWithPreview = useCallback(async (featureId: string) => { setTriagePreviewLoading(featureId); try { const result = await previewEnrichedDescription(featureId, projectId); setTriagePreview({ featureId, enrichedDescription: result.description }); } catch { // Fallback to direct triage if preview endpoint not available await handleTriageFeature(featureId); } finally { setTriagePreviewLoading(null); } }, [handleTriageFeature, projectId]); // Confirm triage from preview const handleConfirmTriageFromPreview = useCallback(async () => { if (!triagePreview) return; setTriagePreview(null); await handleTriageFeature(triagePreview.featureId); }, [handleTriageFeature, triagePreview]); // Cancel triage preview const handleCancelTriagePreview = useCallback(() => { setTriagePreview(null); }, []); // Triage all defined features in a slice const handleTriageAllSliceFeatures = useCallback(async (sliceId: string) => { try { setSaving(true); const result = await triageAllSliceFeatures(sliceId, projectId); addToast(`Triaged ${result.count} feature${result.count !== 1 ? "s" : ""}`, "success"); await loadMissionDetail(selectedMission!.id); } catch (err: any) { addToast(err.message || "Failed to triage slice features", "error"); } finally { setSaving(false); } }, [addToast, loadMissionDetail, selectedMission, projectId]); // ── Assertion handlers ── const loadAssertionsForMilestone = useCallback(async (milestoneId: string) => { try { const assertions = await fetchAssertions(milestoneId, projectId); setAssertionsByMilestone((prev) => { const next = new Map(prev); next.set(milestoneId, assertions); return next; }); } catch (err: any) { // Silently fail - assertions are optional } }, [projectId]); const loadValidationRollup = useCallback(async (milestoneId: string) => { try { const rollup = await fetchMilestoneValidation(milestoneId, projectId); setValidationRollupByMilestone((prev) => { const next = new Map(prev); next.set(milestoneId, rollup); return next; }); } catch (err: any) { // Silently fail } }, [projectId]); const handleCreateAssertion = useCallback(async (milestoneId: string) => { if (!assertionForm.title.trim() || !assertionForm.assertion.trim()) { addToast("Title and assertion text are required", "error"); return; } try { setSaving(true); await createAssertion(milestoneId, { title: assertionForm.title.trim(), assertion: assertionForm.assertion.trim(), status: assertionForm.status, }, projectId); addToast("Assertion created", "success"); await loadAssertionsForMilestone(milestoneId); await loadValidationRollup(milestoneId); setIsCreatingAssertion(false); setAssertionForm({ title: "", assertion: "", status: "pending" }); } catch (err: any) { addToast(err.message || "Failed to create assertion", "error"); } finally { setSaving(false); } }, [assertionForm, addToast, loadAssertionsForMilestone, loadValidationRollup, projectId]); const handleEditAssertion = useCallback((assertion: MissionContractAssertion) => { setEditingAssertionId(assertion.id); setAssertionForm({ title: assertion.title, assertion: assertion.assertion, status: assertion.status, }); }, []); const handleCancelAssertion = useCallback(() => { setEditingAssertionId(null); setIsCreatingAssertion(false); setAssertionForm({ title: "", assertion: "", status: "pending" }); }, []); const handleSaveAssertion = useCallback(async (assertionId: string, milestoneId: string) => { if (!assertionForm.title.trim() || !assertionForm.assertion.trim()) { addToast("Title and assertion text are required", "error"); return; } try { setSaving(true); await updateAssertion(assertionId, { title: assertionForm.title.trim(), assertion: assertionForm.assertion.trim(), status: assertionForm.status, }, projectId); addToast("Assertion updated", "success"); await loadAssertionsForMilestone(milestoneId); await loadValidationRollup(milestoneId); handleCancelAssertion(); } catch (err: any) { addToast(err.message || "Failed to update assertion", "error"); } finally { setSaving(false); } }, [assertionForm, addToast, loadAssertionsForMilestone, loadValidationRollup, handleCancelAssertion, projectId]); const handleDeleteAssertion = useCallback(async (assertionId: string, milestoneId: string) => { try { await deleteAssertion(assertionId, projectId); addToast("Assertion deleted", "success"); await loadAssertionsForMilestone(milestoneId); await loadValidationRollup(milestoneId); setDeleteConfirmId(null); } catch (err: any) { addToast(err.message || "Failed to delete assertion", "error"); } }, [addToast, loadAssertionsForMilestone, loadValidationRollup, projectId]); const loadLinkedFeaturesForAssertion = useCallback(async (assertionId: string) => { try { const features = await fetchFeaturesForAssertion(assertionId, projectId); setLinkedFeaturesByAssertion((prev) => { const next = new Map(prev); next.set(assertionId, features); return next; }); } catch (err: any) { // Silently fail } }, [projectId]); const handleToggleAssertionExpanded = useCallback(async (assertionId: string) => { const isExpanding = expandedAssertionId !== assertionId; setExpandedAssertionId((prev) => (prev === assertionId ? null : assertionId)); if (isExpanding) { await loadLinkedFeaturesForAssertion(assertionId); } }, [expandedAssertionId, loadLinkedFeaturesForAssertion]); const focusAssertion = useCallback((assertionId: string) => { setExpandedAssertionId(assertionId); void loadLinkedFeaturesForAssertion(assertionId); requestAnimationFrame(() => { const assertionElement = document.querySelector(`[data-mission-assertion-id="${assertionId}"]`); if (assertionElement instanceof HTMLElement && typeof assertionElement.scrollIntoView === "function") { assertionElement.scrollIntoView({ behavior: "smooth", block: "center" }); } }); }, [loadLinkedFeaturesForAssertion]); const handleLinkFeatureToAssertion = useCallback(async (featureId: string, assertionId: string) => { try { setLinkingAssertions((prev) => new Set(prev).add(assertionId)); await linkFeatureToAssertion(featureId, assertionId, projectId); addToast("Feature linked to assertion", "success"); await loadLinkedFeaturesForAssertion(assertionId); setFeaturePickerOpenForAssertion(null); } catch (err: any) { addToast(err.message || "Failed to link feature", "error"); } finally { setLinkingAssertions((prev) => { const next = new Set(prev); next.delete(assertionId); return next; }); } }, [addToast, loadLinkedFeaturesForAssertion, projectId]); const handleUnlinkFeatureFromAssertion = useCallback(async (featureId: string, assertionId: string) => { const key = `${featureId}-${assertionId}`; try { setUnlinkingFeatures((prev) => new Set(prev).add(key)); await unlinkFeatureFromAssertion(featureId, assertionId, projectId); addToast("Feature unlinked from assertion", "success"); await loadLinkedFeaturesForAssertion(assertionId); } catch (err: any) { addToast(err.message || "Failed to unlink feature", "error"); } finally { setUnlinkingFeatures((prev) => { const next = new Set(prev); next.delete(key); return next; }); } }, [addToast, loadLinkedFeaturesForAssertion, projectId]); // ── Validation trigger ── const handleTriggerValidation = useCallback(async (featureId: string) => { try { setValidatingFeatures((prev) => new Set(prev).add(featureId)); await triggerValidation(featureId, projectId); addToast("Validation triggered", "success"); // Reload feature loop state const snapshot = await fetchValidationLoopState(featureId, projectId); setFeatureLoopStates((prev) => { const next = new Map(prev); next.set(featureId, snapshot); return next; }); } catch (err: any) { addToast(err.message || "Failed to trigger validation", "error"); } finally { setValidatingFeatures((prev) => { const next = new Set(prev); next.delete(featureId); return next; }); } }, [addToast, projectId]); const loadFeatureLoopState = useCallback(async (featureId: string) => { try { const snapshot = await fetchValidationLoopState(featureId, projectId); setFeatureLoopStates((prev) => { const next = new Map(prev); next.set(featureId, snapshot); return next; }); } catch (err: any) { // Silently fail } }, [projectId]); // Load validation runs for a feature const loadValidationRuns = useCallback(async (featureId: string) => { try { const runs = await fetchValidationRuns(featureId, { limit: 10 }, projectId); setValidationRunsByFeature((prev) => { const next = new Map(prev); next.set(featureId, runs); return next; }); } catch (err: any) { // Silently fail } }, [projectId]); const focusFeature = useCallback((featureId: string) => { const mission = selectedMissionRef.current; if (!mission) { return; } for (const milestone of mission.milestones) { for (const slice of milestone.slices) { const targetFeature = slice.features.find((feature) => feature.id === featureId); if (!targetFeature) { continue; } setExpandedMilestones((prev) => { const next = new Set(prev); next.add(milestone.id); return next; }); setExpandedSlices((prev) => { const next = new Set(prev); next.add(slice.id); return next; }); setExpandedFeatureId(featureId); setSelectedMilestoneId(milestone.id); void loadFeatureLoopState(featureId); void loadValidationRuns(featureId); requestAnimationFrame(() => { const featureElement = document.querySelector(`[data-mission-feature-id="${featureId}"]`); if (featureElement instanceof HTMLElement && typeof featureElement.scrollIntoView === "function") { featureElement.scrollIntoView({ behavior: "smooth", block: "center" }); } }); return; } } }, [loadFeatureLoopState, loadValidationRuns]); // Load run detail with failures const loadRunDetail = useCallback(async (runId: string) => { try { const detail = await fetchValidationRun(runId, projectId); setRunDetailsByRunId((prev) => { const next = new Map(prev); next.set(runId, detail); return next; }); } catch (err: any) { // Silently fail } }, [projectId]); // Toggle feature expansion to show run history const toggleFeatureExpanded = useCallback(async (featureId: string) => { if (expandedFeatureId === featureId) { setExpandedFeatureId(null); } else { setExpandedFeatureId(featureId); // Load loop state and validation runs when expanding await loadFeatureLoopState(featureId); await loadValidationRuns(featureId); } }, [expandedFeatureId, loadFeatureLoopState, loadValidationRuns]); // Toggle run expansion to show failures const toggleRunExpanded = useCallback(async (runId: string) => { if (expandedRunId === runId) { setExpandedRunId(null); } else { setExpandedRunId(runId); await loadRunDetail(runId); } }, [expandedRunId, loadRunDetail]); // Resume a paused mission — set status back to "active" const handleResumeMission = useCallback(async (missionId: string) => { try { await resumeMission(missionId, projectId); addToast("Mission resumed", "success"); await loadMissionDetail(missionId); loadMissions(); } catch (err: any) { addToast(err.message || "Failed to resume mission", "error"); } }, [addToast, loadMissionDetail, loadMissions, projectId]); // Stop mission — set status to "blocked" and pause all linked tasks const handleStopMission = useCallback(async (missionId: string) => { try { const result = await stopMission(missionId, projectId); const count = result.pausedTaskIds?.length ?? 0; addToast(`Mission stopped (${count} task${count !== 1 ? "s" : ""} paused)`, "success"); await loadMissionDetail(missionId); loadMissions(); } catch (err: any) { addToast(err.message || "Failed to stop mission", "error"); } }, [addToast, loadMissionDetail, loadMissions, projectId]); // Start a planning mission — set status to "active" and activate first slice const handleStartMission = useCallback(async (missionId: string) => { try { await startMission(missionId, projectId); addToast("Mission started — first slice activated", "success"); await loadMissionDetail(missionId); loadMissions(); } catch (err: any) { addToast(err.message || "Failed to start mission", "error"); } }, [addToast, loadMissionDetail, loadMissions, projectId]); // ── Autopilot handlers ── const handleToggleAutopilot = useCallback(async (missionId: string, enabled: boolean) => { try { await updateMissionAutopilot(missionId, { enabled }, projectId); addToast(enabled ? "Autopilot enabled" : "Autopilot disabled", "success"); // Reload mission detail to reflect updated fields await loadMissionDetail(missionId); loadMissions(); } catch (err: any) { addToast(err.message || "Failed to update autopilot", "error"); } }, [addToast, loadMissionDetail, loadMissions, projectId]); const handleSelectMission = useCallback((mission: Mission) => { setActiveTab("structure"); setSelectedMilestoneId(null); setValidationTelemetry(null); setMissionEvents([]); setEventsTotal(0); setEventsFilter("all"); setExpandedEventMetadata(new Set()); loadMissionDetail(mission.id); }, [loadMissionDetail]); const handleBackToList = useCallback(() => { setSelectedMission(null); setSelectedMilestoneId(null); setValidationTelemetry(null); setActiveTab("structure"); setMissionEvents([]); setEventsTotal(0); setEventsFilter("all"); setExpandedEventMetadata(new Set()); loadMissions(); }, [loadMissions]); const hasMoreEvents = missionEvents.length < eventsTotal; const autopilotState = (selectedMission?.autopilotState ?? "inactive") as AutopilotState; const autopilotPulseActive = autopilotState === "watching" || autopilotState === "activating"; const autopilotActivitySummary = getAutopilotActivitySummary( autopilotState, selectedMission?.lastAutopilotActivityAt, ); const selectedMilestoneTelemetry = useMemo(() => { if (!validationTelemetry || !selectedMilestoneId || !isMilestoneValidationTelemetry(validationTelemetry)) { return null; } return validationTelemetry.rollup.milestoneId === selectedMilestoneId ? validationTelemetry : null; }, [selectedMilestoneId, validationTelemetry]); const latestRoundsByFeatureId = useMemo(() => { const roundsByFeature = new Map(); for (const round of selectedMilestoneTelemetry?.validationTelemetry.validationRounds ?? []) { const existing = roundsByFeature.get(round.featureId); if (!existing || round.startedAt > existing.startedAt) { roundsByFeature.set(round.featureId, round); } } return roundsByFeature; }, [selectedMilestoneTelemetry]); const handleLoadMoreEvents = useCallback(() => { if (!selectedMission || eventsLoading || !hasMoreEvents) { return; } void loadMissionEvents(selectedMission.id, { append: true }); }, [eventsLoading, hasMoreEvents, loadMissionEvents, selectedMission]); const toggleEventMetadata = useCallback((eventId: string) => { setExpandedEventMetadata((prev) => { const next = new Set(prev); if (next.has(eventId)) { next.delete(eventId); } else { next.add(eventId); } return next; }); }, []); // Keyboard handler for mission form const handleMissionFormKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSaveMission(); } }, [handleSaveMission]); const handleMilestoneFormKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSaveMilestone(); } }, [handleSaveMilestone]); const handleSliceFormKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSaveSlice(); } }, [handleSaveSlice]); const handleFeatureFormKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSaveFeature(); } }, [handleSaveFeature]); // Ref for focus management const modalRef = useRef(null); // Escape key handling useEffect(() => { if (!isActive) return; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { onClose(); } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [isActive, onClose]); if (!isActive) return null; const manager = (
{/* ── Header ── */}
{selectedMission ? ( ) : null}

{selectedMission ? selectedMission.title : "Missions"}

{!isInline && ( /* Modal mode: show close button */ )}
{/* ── Body ── */}
{loading ? (
Loading missions...
) : detailLoading ? (
Loading mission details...
) : selectedMission ? ( /* ── Detail View ── */
{autopilotPulseActive && ( )}

{selectedMission.title}

{selectedMission.status}
{selectedMission.description && (

{selectedMission.description}

)}
{selectedMission.milestones.length} milestones
{/* ── Autopilot section ── */}
{autopilotPulseActive && } {autopilotState}
{autopilotActivitySummary && ( {autopilotActivitySummary} )}
{selectedMission.status === "active" && ( )} {selectedMission.status === "blocked" && ( )} {selectedMission.status === "planning" && ( )}
{/* Inline edit mission form (detail view) */} {editingMissionId === selectedMission.id && (
setMissionForm({ ...missionForm, title: e.target.value })} onKeyDown={handleMissionFormKeyDown} autoFocus />