slice 2: MissionWizard + MissionCanvas + MissionsList frontend
Adds the unified missions tier to the dashboard. Runs in parallel
with Research + Loops tabs until Slice 9's big-bang cutover.
New files:
- lib/api/missions.ts — typed API client + 5 template presets
(research_only, research_and_code,
security_hardening, refactor, benchmark)
- dashboard/MissionsList.tsx — sidebar list with status pills +
template-kind badges + new-mission CTA
- dashboard/MissionWizard.tsx — 5-step adaptive wizard:
1) template picker (5 cards)
2) title/description + repo (when required)
3) team (placeholder — Slice 3 wires templates + auto-provision)
4) schedule (one-shot or cron)
5) review + launch
- dashboard/MissionCanvas.tsx — 4-tab detail view (overview/phases/
tasks/artifacts), draft→running launch
Dashboard.tsx gets a new "missions" tier + crumb + rail icon (flag).
Slice 2 ships a minimal implementation that creates missions with the
template's canned phase composition. Slice 4 replaces the preset table
with real TOML-recipe dispatch on the server; the client's fallback
presets keep offline preview working.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
a72da9dff0
commit
fc67936e33
@@ -0,0 +1,274 @@
|
||||
"use client";
|
||||
|
||||
// Sidebar for the Missions tier — the unified list that replaces
|
||||
// ResearchList + LoopsList after Slice 9's cutover. During Slice 2 it
|
||||
// runs in parallel with the old two lists so we can iterate on the
|
||||
// UX without breaking existing workflows.
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Plus, RotateCw } from "lucide-react";
|
||||
|
||||
import {
|
||||
listMissions,
|
||||
type Mission,
|
||||
type MissionStatus,
|
||||
type TemplateKind,
|
||||
} from "@/lib/api/missions";
|
||||
import { MissionWizard } from "./MissionWizard";
|
||||
|
||||
const mono =
|
||||
"ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace";
|
||||
|
||||
const STATUS_COLOR: Record<MissionStatus, string> = {
|
||||
draft: "#8a8a92",
|
||||
running: "#5ec8d8",
|
||||
completed: "#5fd08a",
|
||||
failed: "#ff8a7a",
|
||||
cancelled: "#6a6a72",
|
||||
};
|
||||
|
||||
const TEMPLATE_BADGE: Record<TemplateKind, { label: string; color: string }> = {
|
||||
research_only: { label: "RESEARCH", color: "#7cd6e0" },
|
||||
research_and_code: { label: "R+CODE", color: "#c98af0" },
|
||||
security_hardening: { label: "SECURITY", color: "#ff8a7a" },
|
||||
refactor: { label: "REFACTOR", color: "#ffb44a" },
|
||||
benchmark: { label: "BENCHMARK", color: "#83e6a5" },
|
||||
custom: { label: "CUSTOM", color: "#8a8a92" },
|
||||
};
|
||||
|
||||
export function MissionsList({
|
||||
selectedId,
|
||||
onSelect,
|
||||
refreshKey,
|
||||
onCreated,
|
||||
}: {
|
||||
selectedId: string | null;
|
||||
onSelect: (id: string) => void;
|
||||
refreshKey: number;
|
||||
onCreated: (id: string) => void;
|
||||
}) {
|
||||
const [missions, setMissions] = useState<Mission[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [wizardOpen, setWizardOpen] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setError(null);
|
||||
try {
|
||||
const rows = await listMissions(100);
|
||||
setMissions(rows);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "load failed");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
(async () => {
|
||||
if (alive) await load();
|
||||
})();
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, [load, refreshKey]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
flex: "none",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "12px 14px 8px",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: mono,
|
||||
fontSize: 10,
|
||||
letterSpacing: ".16em",
|
||||
color: "#8a8a92",
|
||||
}}
|
||||
>
|
||||
MISSIONS · {missions.length}
|
||||
</span>
|
||||
<div style={{ display: "flex", gap: 6 }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={load}
|
||||
title="Refresh"
|
||||
aria-label="Refresh"
|
||||
style={iconBtn}
|
||||
>
|
||||
<RotateCw size={13} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setWizardOpen(true)}
|
||||
title="New mission"
|
||||
aria-label="New mission"
|
||||
style={{
|
||||
...iconBtn,
|
||||
color: "#ff8a7a",
|
||||
borderColor: "rgba(255,138,122,.5)",
|
||||
}}
|
||||
>
|
||||
<Plus size={13} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
overflowY: "auto",
|
||||
padding: "4px 10px 12px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<div style={{ fontFamily: mono, fontSize: 11, color: "#5ec8d8", padding: 8 }}>
|
||||
Loading…
|
||||
</div>
|
||||
) : error ? (
|
||||
<div style={{ fontSize: 12, color: "#ff8a7a", padding: 8 }}>{error}</div>
|
||||
) : missions.length === 0 ? (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 10,
|
||||
padding: 12,
|
||||
alignItems: "flex-start",
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 12.5, color: "#cfcfd5", lineHeight: 1.5 }}>
|
||||
No missions yet. Pick a workflow template to get started.
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setWizardOpen(true)}
|
||||
style={ctaBtn}
|
||||
>
|
||||
<Plus size={13} /> New mission
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
missions.map((m) => {
|
||||
const active = selectedId === m.id;
|
||||
const badge = TEMPLATE_BADGE[m.template_kind] ?? TEMPLATE_BADGE.custom;
|
||||
return (
|
||||
<button
|
||||
key={m.id}
|
||||
type="button"
|
||||
onClick={() => onSelect(m.id)}
|
||||
style={{
|
||||
width: "100%",
|
||||
textAlign: "left",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 4,
|
||||
padding: "10px 11px",
|
||||
borderRadius: 9,
|
||||
border: `1px solid ${active ? "rgba(94,200,216,.5)" : "rgba(255,255,255,.07)"}`,
|
||||
background: active ? "rgba(94,200,216,.08)" : "#101013",
|
||||
cursor: "pointer",
|
||||
color: "#eaeaee",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||||
<span
|
||||
style={{
|
||||
width: 7,
|
||||
height: 7,
|
||||
borderRadius: "50%",
|
||||
background: STATUS_COLOR[m.status],
|
||||
flex: "none",
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: mono,
|
||||
fontSize: 9,
|
||||
color: STATUS_COLOR[m.status],
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
{m.status}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
marginLeft: "auto",
|
||||
fontFamily: mono,
|
||||
fontSize: 9,
|
||||
color: badge.color,
|
||||
}}
|
||||
>
|
||||
{badge.label}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: "#f3f3f5",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{m.title}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{wizardOpen && (
|
||||
<MissionWizard
|
||||
onClose={() => setWizardOpen(false)}
|
||||
onCreated={(id) => {
|
||||
setWizardOpen(false);
|
||||
onCreated(id);
|
||||
void load();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const iconBtn: React.CSSProperties = {
|
||||
width: 26,
|
||||
height: 26,
|
||||
borderRadius: 7,
|
||||
border: "1px solid rgba(255,255,255,.12)",
|
||||
background: "transparent",
|
||||
color: "#9a9aa2",
|
||||
cursor: "pointer",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
};
|
||||
|
||||
const ctaBtn: React.CSSProperties = {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
padding: "8px 12px",
|
||||
borderRadius: 9,
|
||||
border: 0,
|
||||
background: "#ff8a7a",
|
||||
color: "#1a0d0b",
|
||||
fontSize: 12.5,
|
||||
fontWeight: 700,
|
||||
cursor: "pointer",
|
||||
};
|
||||
Reference in New Issue
Block a user