"use client"; // MissionWizard — unified 5-step wizard replacing ResearchWizard + // LoopsWizard. // 1. Template — pick one of the 5 workflow templates (or custom) // 2. Target — repo picker (when template requires) + subject/prompt // 3. Team — bind existing team (later slices: team template + auto-provision) // 4. Schedule — one-shot / cron / on-event // 5. Review — plan preview + launch // // Slice 2 ships a minimal implementation that creates a mission with // the template's canned phase composition; Slice 4 threads the TOML // recipe engine through here for real template dispatch. import { useEffect, useMemo, useState } from "react"; import { X } from "lucide-react"; import { createMission, presetForKind, TEMPLATE_PRESETS, type Schedule, type TemplateKind, type TemplatePreset, } from "@/lib/api/missions"; import { listTeamTemplates, type TeamTemplate, } from "@/lib/api/team-templates"; import { RepoPicker, type PickedRepo } from "./RepoPicker"; const mono = "ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace"; type Step = 1 | 2 | 3 | 4 | 5; export function MissionWizard({ onClose, onCreated, }: { onClose: () => void; onCreated: (id: string) => void; }) { const [step, setStep] = useState(1); const [templateKind, setTemplateKind] = useState("research_only"); const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); const [repo, setRepo] = useState(null); const [teamTemplateId, setTeamTemplateId] = useState(""); const [teamTemplates, setTeamTemplates] = useState([]); useEffect(() => { (async () => { try { setTeamTemplates(await listTeamTemplates()); } catch { // Non-fatal: user can still create a mission without a team template. } })(); }, []); const [scheduleKind, setScheduleKind] = useState<"one_shot" | "cron">("one_shot"); const [cron, setCron] = useState("0 */6 * * *"); const [runtimeKind, setRuntimeKind] = useState<"zeroclaw" | "local_herdr">("zeroclaw"); const [targetNodeId, setTargetNodeId] = useState(""); const [onlineNodes, setOnlineNodes] = useState< Array<{ id: string; name: string }> >([]); useEffect(() => { (async () => { try { const r = await fetch("/api/nodes"); if (!r.ok) return; const data = (await r.json()) as { nodes?: Array<{ id: string; name: string; status: string }>; }; setOnlineNodes( (data.nodes ?? []) .filter((n) => n.status === "online") .map((n) => ({ id: n.id, name: n.name })), ); } catch { // Non-fatal — user can still pick zeroclaw runtime without nodes. } })(); }, []); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const preset: TemplatePreset = useMemo( () => presetForKind(templateKind) ?? TEMPLATE_PRESETS[0], [templateKind], ); const canNext = (step === 1 && !!templateKind) || (step === 2 && title.trim().length > 0 && (!preset.requiresRepo || repo !== null)) || step === 3 || (step === 4 && (runtimeKind === "zeroclaw" || targetNodeId !== "")); async function submit() { setError(null); setSubmitting(true); try { const schedule: Schedule = scheduleKind === "cron" ? { kind: "cron", cron } : { kind: "one_shot" }; const created = await createMission({ title: title.trim(), template_kind: templateKind, repo_id: repo?.repo_id, team_template_id: teamTemplateId || undefined, schedule, description: description.trim() || undefined, phases: preset.phases, runtime_kind: runtimeKind, target_node_id: runtimeKind === "local_herdr" ? targetNodeId : undefined, }); onCreated(created.id); } catch (e) { setError(e instanceof Error ? e.message : "create failed"); } finally { setSubmitting(false); } } return (
e.stopPropagation()} role="dialog" aria-modal="true" aria-label="New mission" style={{ width: "100%", maxWidth: 720, maxHeight: "88vh", display: "flex", flexDirection: "column", borderRadius: 16, background: "#0d0d10", border: "1px solid rgba(255,255,255,.1)", boxShadow: "0 30px 90px rgba(0,0,0,.6)", overflow: "hidden", animation: "scale-in .18s ease", }} >
STEP {step} / 5 New mission
{step === 1 && (
Template

Pick the workflow shape. This decides which phases run and what the coding/research agents are asked to do.

{TEMPLATE_PRESETS.map((p) => { const active = p.kind === templateKind; return ( ); })}
)} {step === 2 && (
setTitle(e.target.value)} placeholder="e.g. ClawHDF5 spec — memory engine v3" style={fieldStyle} />