feat(ui): three-screen mission wizard that asks only what the type needs

Five fixed steps for every mission type, and getting a research document out of
it meant naming a team, choosing a runtime, and writing per-phase completion
conditions under a paragraph explaining what a model checker can and cannot
prove. Two of those steps asked for things the mission does not use, and one of
them blocked outright.

  1  What do you want to do?
  2  Title, a description with a Polish button, repo ONLY if the type needs one
  3  Review -> Launch, plus one collapsed Advanced section

Two hard defects fixed on the way:

- The microVM runtime could not be selected AT ALL. Step 4 gated Next on
  `targetNodeId`, which microVM deliberately never sets because placement picks
  the node per phase. Everything shipped today, the local-GPU backend included,
  was unreachable from the UI.
- Step 3 required a team while every workflow TOML already names one in
  `default_team_template` — which this file ignored. The answer was always
  available and the question was always asked. It is now resolved by key, with a
  category fallback, and shown under Advanced so an operator can see WHICH
  default rather than having to supply one.

A failed `/api/team-templates` request and a genuinely empty list rendered the
identical red banner, which sends the reader looking for missing template files
when the request had 401'd. They now say different things.

`phases[]` is no longer sent unless someone set a completion condition.
`recipeToPreset` strips each phase's `config`, so posting the stripped list
overrode the recipe's real settings — tools, commit policy, loop mode — with
nothing. Omitting it lets `phases_for_create` use the recipe, which is both
simpler and more correct.

Launch keeps its own gate, since Advanced can still produce an unlaunchable
combination — but it names what is missing instead of greying out in silence.

Artifacts get a Download link. Deliberately a plain link to the streaming route
rather than a Blob built from what "Read" already fetched: that content is
capped at 2 MiB and UTF-8-decoded, so reusing it would silently produce a
truncated or undownloadable file for exactly the artifacts worth downloading.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-09 18:50:13 -07:00
co-authored by Claude Opus 5
parent 25f075a8be
commit e4bddeb1ba
3 changed files with 568 additions and 404 deletions
@@ -11,7 +11,7 @@
// research phase can leave dozens of documents.
import { useCallback, useState } from "react";
import { FileText, GitMerge } from "lucide-react";
import { Download, FileText, GitMerge } from "lucide-react";
import {
getArtifactContent,
@@ -172,6 +172,28 @@ export function MissionArtifacts({
>
{isOpen ? "Hide" : "Read"}
</button>
{/* A plain link, not a fetch-and-Blob.
The read endpoint caps at 2 MiB and decodes as UTF-8, so
building the download from what "Read" already fetched would
inherit both limits and silently produce a truncated or
undownloadable file for exactly the artifacts worth
downloading. This hits the streaming route instead. */}
<a
href={`/api/missions/${missionId}/artifacts/${a.id}/download`}
download
style={{
...secondaryBtn,
padding: "5px 10px",
fontSize: 11,
textDecoration: "none",
display: "inline-flex",
alignItems: "center",
gap: 5,
}}
>
<Download aria-hidden size={12} />
Download
</a>
</div>
{isOpen && (
<div
@@ -194,7 +216,8 @@ export function MissionArtifacts({
</div>
) : text?.truncated ? (
<div style={{ fontFamily: mono, fontSize: 11.5, color: "#8a8a92" }}>
too large to display inline ({text.bytes.toLocaleString()} bytes)
too large to display inline ({text.bytes.toLocaleString()}{" "}
bytes) — use Download
</div>
) : text ? (
<MarkdownBlock source={text.content} />