"use client"; // MissionCanvas — unified detail view for the missions tier. Tabs: // Overview · title, description, template kind, team, repo, schedule // Phases · ordered timeline of phase kinds + statuses // Tasks · INT-XX cards (Slice 5 fills these; Slice 2 renders shell) // Artifacts · MD/PDF/benchmark/security files with PDF-render status // // This replaces ResearchCanvas + LoopsCanvas after Slice 9's cutover. import React, { useCallback, useEffect, useMemo, useState } from "react"; import { FileText, Pencil, Play, Plus, RefreshCw, Sparkles, Trash2 } from "lucide-react"; import { deleteMission, getMission, refineMission, setMissionDescription, setMissionStatus, triggerBenchmark, triggerSecurityScan, updateMission, type MissionDetail, type MissionStatus, type PhaseKind, type PhaseStatus, type RefineResult, type TaskStatus, type TemplateKind, } from "@/lib/api/missions"; import { MarkdownBlock } from "./MarkdownBlock"; import { MissionWizard } from "./MissionWizard"; const mono = "ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace"; const STATUS_COLOR: Record = { draft: "#8a8a92", running: "#5ec8d8", completed: "#5fd08a", failed: "#ff8a7a", cancelled: "#6a6a72", }; const PHASE_STATUS_COLOR: Record = { pending: "#6a6a72", running: "#5ec8d8", completed: "#5fd08a", failed: "#ff8a7a", skipped: "#8a8a92", }; const TASK_STATUS_COLOR: Record = { created: "#8a8a92", working: "#5ec8d8", validating: "#ffb44a", complete: "#5fd08a", failed: "#ff8a7a", }; const PHASE_LABEL: Record = { research: "Research", coding: "Coding", benchmark: "Benchmark", security_scan: "Security scan", }; const TEMPLATE_LABEL: Record = { research_only: "Research only", research_and_code: "Research + Coding Loop", security_hardening: "Security Hardening", refactor: "Refactor", benchmark: "Benchmark", custom: "Custom", }; type Tab = "overview" | "phases" | "tasks" | "artifacts" | "benchmarks"; export function MissionCanvas({ selectedId, refreshKey, onChanged, onSelect, onDeleted, }: { selectedId: string | null; refreshKey: number; onChanged: () => void; onSelect?: (id: string) => void; onDeleted?: () => void; }) { const [mission, setMission] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [tab, setTab] = useState("overview"); const [launching, setLaunching] = useState(false); const [refining, setRefining] = useState(false); const [refineDiff, setRefineDiff] = useState(null); const [accepting, setAccepting] = useState(false); const [phaseBusy, setPhaseBusy] = useState(null); const [pdfPreviewId, setPdfPreviewId] = useState(null); const [wizardOpen, setWizardOpen] = useState(false); const [editOpen, setEditOpen] = useState(false); const [deleteBusy, setDeleteBusy] = useState(false); const load = useCallback(async () => { if (!selectedId) { setMission(null); return; } setLoading(true); setError(null); try { const m = await getMission(selectedId); setMission(m); } catch (e) { setError(e instanceof Error ? e.message : "load failed"); } finally { setLoading(false); } }, [selectedId]); useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect void load(); }, [load, refreshKey]); const refine = useCallback(async () => { if (!mission) return; setRefining(true); setError(null); try { const result = await refineMission(mission.id); setRefineDiff(result); } catch (e) { setError(e instanceof Error ? e.message : "refine failed"); } finally { setRefining(false); } }, [mission]); const acceptRefine = useCallback(async () => { if (!mission || !refineDiff) return; setAccepting(true); setError(null); try { await setMissionDescription(mission.id, refineDiff.refined); setRefineDiff(null); onChanged(); await load(); } catch (e) { setError(e instanceof Error ? e.message : "accept failed"); } finally { setAccepting(false); } }, [mission, refineDiff, onChanged, load]); const undoRefine = useCallback(async () => { if (!mission || !refineDiff) return; setAccepting(true); setError(null); try { await setMissionDescription(mission.id, refineDiff.original); setRefineDiff(null); onChanged(); await load(); } catch (e) { setError(e instanceof Error ? e.message : "undo failed"); } finally { setAccepting(false); } }, [mission, refineDiff, onChanged, load]); const runSecurityScan = useCallback( async (phaseId: string) => { if (!mission) return; setPhaseBusy(`sec:${phaseId}`); setError(null); try { await triggerSecurityScan(mission.id, phaseId); await load(); } catch (e) { setError(e instanceof Error ? e.message : "security scan failed"); } finally { setPhaseBusy(null); } }, [mission, load], ); const runBenchmark = useCallback( async (phaseId: string, slot: "baseline" | "after") => { if (!mission) return; setPhaseBusy(`bench:${phaseId}:${slot}`); setError(null); try { await triggerBenchmark( mission.id, phaseId, slot, slot === "after" ? Math.max(1, mission.benchmarks.length) : undefined, ); await load(); } catch (e) { setError(e instanceof Error ? e.message : "benchmark failed"); } finally { setPhaseBusy(null); } }, [mission, load], ); const launch = useCallback(async () => { if (!mission) return; setLaunching(true); try { await setMissionStatus(mission.id, "running"); onChanged(); await load(); } catch (e) { setError(e instanceof Error ? e.message : "launch failed"); } finally { setLaunching(false); } }, [mission, onChanged, load]); const orderedPhases = useMemo( () => mission ? [...mission.phases].sort((a, b) => a.order_idx - b.order_idx) : [], [mission], ); if (!selectedId) { return (
Pick a mission from the left, or hit + to create one.
); } if (loading && !mission) { return (
Loading…
); } if (error) { return (
{error}
); } if (!mission) return null; return (
{mission.status} {TEMPLATE_LABEL[mission.template_kind] ?? mission.template_kind}
{mission.status === "draft" && ( )} {mission.status === "draft" && ( )} {mission.status === "draft" && ( )}

{mission.title}

{mission.description && (
)} {refineDiff && ( setRefineDiff(null)} onUndoAfterAccept={undoRefine} /> )} {wizardOpen && ( setWizardOpen(false)} onCreated={(id) => { setWizardOpen(false); onSelect?.(id); onChanged(); }} /> )} {editOpen && ( setEditOpen(false)} onSaved={async () => { setEditOpen(false); onChanged(); await load(); }} /> )}
{(["overview", "phases", "tasks", "artifacts", "benchmarks"] as Tab[]).map((t) => { const active = tab === t; const badge = t === "tasks" ? mission.tasks.length : t === "artifacts" ? mission.artifacts.length : t === "phases" ? mission.phases.length : t === "benchmarks" ? mission.benchmarks.length : null; return ( ); })}
{tab === "overview" && (
{mission.completed_at && ( )}
)} {tab === "phases" && (
{orderedPhases.length === 0 ? ( ) : ( orderedPhases.map((p) => (
{p.status} #{p.order_idx + 1} · {PHASE_LABEL[p.kind] ?? p.kind}
{(p.started_at || p.completed_at) && ( {p.started_at ? `started ${new Date(p.started_at).toLocaleTimeString()}` : "—"} {p.completed_at ? ` · finished ${new Date(p.completed_at).toLocaleTimeString()}` : ""} )} {mission.status === "running" || mission.status === "completed" ? (
{p.kind === "security_scan" && ( )} {p.kind === "benchmark" && ( <> )}
) : null}
)) )}
)} {tab === "tasks" && (
{mission.tasks.length === 0 ? ( ) : ( mission.tasks.map((t) => (
{t.status} {t.external_id && ( {t.external_id} )} {t.title}
{t.artifact_paths.length > 0 && (
{t.artifact_paths.join(" · ")}
)}
)) )}
)} {tab === "artifacts" && (
{mission.artifacts.length === 0 ? ( ) : ( mission.artifacts.map((a) => { const isPreviewing = pdfPreviewId === a.id; return (
{a.title ?? a.path.split("/").pop() ?? a.path}
{a.kind} · {a.path}
{a.rendered_pdf_path ? ( <> Open ) : a.render_pdf_status !== "skip" ? ( pdf: {a.render_pdf_status} ) : null}
{isPreviewing && a.rendered_pdf_path && (