mission canvas: add Refine button + markdown-rendered description
Adds a Refine button to the left of Refresh + Launch on the mission
detail toolbar (draft-only). Clicking it POSTs to a new endpoint that
calls Gemini 2.5 Flash to rewrite the user's freeform description into
a coherent, sectioned Markdown brief (Objective / Context / Scope /
Constraints / Acceptance Criteria / Open Questions) ready for the
research + coding agents to ingest cleanly.
Backend:
- crates/cm-api/src/mission_refiner.rs — Gemini call with a
system prompt that preserves user-provided facts, avoids
invention, and emits raw markdown (not JSON).
- POST /api/missions/{id}/refine — draft-only, 400 on empty
description or non-draft state.
- cm-db::repo::missions::set_description helper.
Frontend:
- MarkdownBlock — tiny zero-dep renderer for h1/h2/h3, bullet +
numbered lists, **bold**, `code`, paragraphs. Deliberately
small; the refiner emits a bounded subset.
- MissionCanvas — Refine button (Sparkles icon, secondary style)
to the left of Refresh; description now renders through
MarkdownBlock instead of a single <p>. Disabled while
description is empty or a refine is in flight.
- lib/api/missions — refineMission client.
This commit is contained in:
@@ -9,10 +9,11 @@
|
||||
// This replaces ResearchCanvas + LoopsCanvas after Slice 9's cutover.
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { FileText, Play, RefreshCw } from "lucide-react";
|
||||
import { FileText, Play, RefreshCw, Sparkles } from "lucide-react";
|
||||
|
||||
import {
|
||||
getMission,
|
||||
refineMission,
|
||||
setMissionStatus,
|
||||
type MissionDetail,
|
||||
type MissionStatus,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
type TaskStatus,
|
||||
type TemplateKind,
|
||||
} from "@/lib/api/missions";
|
||||
import { MarkdownBlock } from "./MarkdownBlock";
|
||||
|
||||
const mono =
|
||||
"ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace";
|
||||
@@ -77,6 +79,7 @@ export function MissionCanvas({
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [tab, setTab] = useState<Tab>("overview");
|
||||
const [launching, setLaunching] = useState(false);
|
||||
const [refining, setRefining] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!selectedId) {
|
||||
@@ -100,6 +103,21 @@ export function MissionCanvas({
|
||||
void load();
|
||||
}, [load, refreshKey]);
|
||||
|
||||
const refine = useCallback(async () => {
|
||||
if (!mission) return;
|
||||
setRefining(true);
|
||||
setError(null);
|
||||
try {
|
||||
await refineMission(mission.id);
|
||||
onChanged();
|
||||
await load();
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "refine failed");
|
||||
} finally {
|
||||
setRefining(false);
|
||||
}
|
||||
}, [mission, onChanged, load]);
|
||||
|
||||
const launch = useCallback(async () => {
|
||||
if (!mission) return;
|
||||
setLaunching(true);
|
||||
@@ -198,6 +216,26 @@ export function MissionCanvas({
|
||||
{TEMPLATE_LABEL[mission.template_kind] ?? mission.template_kind}
|
||||
</span>
|
||||
<div style={{ marginLeft: "auto", display: "flex", gap: 6 }}>
|
||||
{mission.status === "draft" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={refine}
|
||||
disabled={refining || !mission.description?.trim()}
|
||||
title={
|
||||
mission.description?.trim()
|
||||
? "Refine the description into a sectioned brief"
|
||||
: "Add a description first"
|
||||
}
|
||||
aria-label="Refine"
|
||||
style={{
|
||||
...secondaryBtn,
|
||||
opacity: refining || !mission.description?.trim() ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
<Sparkles size={13} style={{ marginRight: 4 }} />
|
||||
{refining ? "Refining…" : "Refine"}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={load}
|
||||
@@ -225,9 +263,9 @@ export function MissionCanvas({
|
||||
</div>
|
||||
<h1 style={{ margin: 0, fontSize: 20, color: "#f3f3f5" }}>{mission.title}</h1>
|
||||
{mission.description && (
|
||||
<p style={{ margin: 0, fontSize: 13, color: "#cfcfd5", lineHeight: 1.5 }}>
|
||||
{mission.description}
|
||||
</p>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<MarkdownBlock source={mission.description} />
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: "flex", gap: 4, marginTop: 4 }}>
|
||||
{(["overview", "phases", "tasks", "artifacts", "benchmarks"] as Tab[]).map((t) => {
|
||||
|
||||
Reference in New Issue
Block a user