import { useState, useEffect, useCallback, useRef } from "react"; import { X, Plus, Pencil, Trash2, ChevronRight, ChevronDown, ChevronLeft, Target, Layers, Package, Box, Check, Loader2, Link, Unlink, Play, Square, Sparkles, Zap, Activity, } from "lucide-react"; import type { ToastType } from "../hooks/useToast"; import { MissionInterviewModal } from "./MissionInterviewModal"; import type { Mission, MissionWithHierarchy, MissionWithSummary, Milestone, Slice, MissionFeature, MissionStatus, MilestoneStatus, SliceStatus, FeatureStatus, MilestoneWithSlices, SliceWithFeatures, MissionHealth, MissionEvent, MissionEventType, } from "./mission-types"; import { fetchMissions, createMission, fetchMission, updateMission, deleteMission, createMilestone, updateMilestone, deleteMilestone, createSlice, updateSlice, deleteSlice, activateSlice, createFeature, updateFeature, deleteFeature, linkFeatureToTask, unlinkFeatureFromTask, triageFeature, triageAllSliceFeatures, resumeMission, stopMission, startMission, updateMissionAutopilot, fetchMissionHealth, fetchMissionsHealth, fetchMissionEvents, } 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; } // 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)" }, }; // 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" ); } 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 }: 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); // Auto-open interview modal when resuming a session useEffect(() => { if (isActive && resumeSessionId) { setShowInterviewModal(true); } }, [isActive, resumeSessionId]); // Delete confirmation const [deleteConfirmId, setDeleteConfirmId] = useState<{ type: string; id: string } | null>(null); const [missionHealthById, setMissionHealthById] = useState>(new Map()); const [activeTab, setActiveTab] = useState<"structure" | "activity">("structure"); const [missionEvents, setMissionEvents] = useState([]); const missionEventsRef = useRef([]); 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); 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) { setExpandedMilestones(new Set([data.milestones[0].id])); if (data.milestones[0].slices.length > 0) { setExpandedSlices(new Set([data.milestones[0].slices[0].id])); } } } catch (err: any) { addToast(err.message || "Failed to load mission details", "error"); } finally { setDetailLoading(false); } }, [addToast, 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); 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 || missions.length === 0 || typeof EventSource === "undefined") { return; } const search = new URLSearchParams(); if (projectId) { search.set("projectId", projectId); } const eventUrl = `/api/events${search.size > 0 ? `?${search.toString()}` : ""}`; const eventSource = new EventSource(eventUrl); const refreshHealth = () => { void loadMissionHealth(missions); }; const handleMissionUpdated = (rawEvent: Event) => { refreshHealth(); // Reload the selected mission detail to reflect updated mission state (autopilot, status, etc.) if (selectedMission) { void loadMissionDetail(selectedMission.id); } }; const handleSliceUpdated = (rawEvent: Event) => { refreshHealth(); // Reload the selected mission detail to reflect updated slice status if (selectedMission) { void loadMissionDetail(selectedMission.id); } }; const handleFeatureUpdated = () => { refreshHealth(); // Reload the selected mission detail to reflect updated feature status if (selectedMission) { void loadMissionDetail(selectedMission.id); } }; const handleMissionEvent = (rawEvent: Event) => { refreshHealth(); if (!selectedMission || activeTab !== "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 !== selectedMission.id) { return; } if (!matchesEventFilter(payload.eventType, eventsFilter)) { 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 } }; eventSource.addEventListener("mission:updated", handleMissionUpdated); eventSource.addEventListener("slice:updated", handleSliceUpdated); eventSource.addEventListener("feature:updated", handleFeatureUpdated); eventSource.addEventListener("mission:event", handleMissionEvent); return () => { eventSource.removeEventListener("mission:updated", handleMissionUpdated); eventSource.removeEventListener("slice:updated", handleSliceUpdated); eventSource.removeEventListener("feature:updated", handleFeatureUpdated); eventSource.removeEventListener("mission:event", handleMissionEvent); eventSource.close(); }; }, [ activeTab, eventsFilter, isActive, isActivityScrolledNearBottom, loadMissionDetail, loadMissionHealth, missions, projectId, scrollActivityToLatest, selectedMission, ]); // 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) => { setExpandedMilestones((prev) => { const next = new Set(prev); if (next.has(milestoneId)) { next.delete(milestoneId); } else { next.add(milestoneId); } return next; }); }, []); // 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 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]); // 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"); setMissionEvents([]); setEventsTotal(0); setEventsFilter("all"); setExpandedEventMetadata(new Set()); loadMissionDetail(mission.id); }, [loadMissionDetail]); const handleBackToList = useCallback(() => { setSelectedMission(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 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"}

{/* ── 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 />