mission progress UI: auto-refresh + Team tab + Live events tab
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 36s
ci / rust (push) Successful in 2m59s
ci / e2e (push) Skipped
ci / publish (push) Successful in 3m9s

Fills the biggest UX gap surfaced during the deploy walk: hosted
missions had no live-progress surface at all. Now they do.

Auto-refresh:
  - MissionCanvas grows a second useEffect that polls getMission
    every 3s while mission.status === 'running'. Stops immediately
    on terminal state (completed / failed / cancelled). Phases,
    Tasks, Artifacts, Benchmarks all update without a manual click.

Team tab (new):
  - MissionTeamTab.tsx — fetches /api/teams/{id} + /api/team/claws,
    shows a card per member with role slot + an "Open" pill that
    calls onOpenClaw(clawId) → Dashboard flips to AGENT tier with
    that claw selected, dropping the operator into the existing
    ClawCommandCenter surface (WorkingOnNow, ReasoningStream, etc).

Live events tab (new):
  - MissionLiveEvents.tsx — polls /api/missions/{id}/runs every 5s
    for the topology_runs bound to this mission, opens one
    EventSource per active run against /api/topology-runs/{id}/events,
    renders as a chronological scrolling feed with per-event kind
    pills + per-run short-id badges. Auto-scrolls unless the
    operator scrolled up. New runs auto-attach; terminal runs
    close cleanly.

Backend:
  - cm-db::repo::topology_runs::list_by_mission — SELECT ... FROM
    topology_runs WHERE mission_id = $1 ORDER BY created_at DESC.
    Uses runtime sqlx::query (not the macro) to avoid a sqlx cache
    regen just for this route.
  - TopologyRunSummary gains #[derive(Serialize)] + rfc3339 codecs.
  - GET /api/missions/{id}/runs — workspace-scoped, returns
    { runs: [...] }.

Dashboard wires onOpenClaw on MissionCanvas → setAgentId + setTier("claw").

Verified: cargo check --workspace + tsc --noEmit + eslint --quiet
all green.
This commit is contained in:
Omar Sobh
2026-07-20 15:31:41 -07:00
parent cf735312f8
commit 3ba0485e7d
8 changed files with 534 additions and 1 deletions
@@ -29,7 +29,9 @@ import {
} from "@/lib/api/missions";
import { EditMissionModal } from "./EditMissionModal";
import { MarkdownBlock } from "./MarkdownBlock";
import { MissionLiveEvents } from "./MissionLiveEvents";
import { MissionLivePane } from "./MissionLivePane";
import { MissionTeamTab } from "./MissionTeamTab";
import { MissionWizard } from "./MissionWizard";
import { RefineDiffModal } from "./RefineDiffModal";
@@ -72,7 +74,15 @@ const TEMPLATE_LABEL: Record<TemplateKind, string> = {
custom: "Custom",
};
type Tab = "overview" | "phases" | "tasks" | "artifacts" | "benchmarks" | "pane";
type Tab =
| "overview"
| "phases"
| "tasks"
| "team"
| "live"
| "artifacts"
| "benchmarks"
| "pane";
export function MissionCanvas({
selectedId,
@@ -80,12 +90,15 @@ export function MissionCanvas({
onChanged,
onSelect,
onDeleted,
onOpenClaw,
}: {
selectedId: string | null;
refreshKey: number;
onChanged: () => void;
onSelect?: (id: string) => void;
onDeleted?: () => void;
/** Cross-tier navigation — jumps to AGENT tier with this claw selected. */
onOpenClaw?: (clawId: string) => void;
}) {
const [mission, setMission] = useState<MissionDetail | null>(null);
const [loading, setLoading] = useState(false);
@@ -123,6 +136,17 @@ export function MissionCanvas({
void load();
}, [load, refreshKey]);
// Auto-refresh every 3s while the mission is running so Phases +
// Tasks + Artifacts tabs surface progress without hitting Refresh.
// Stops polling once the mission reaches a terminal state.
useEffect(() => {
if (mission?.status !== "running") return;
const t = setInterval(() => {
void load();
}, 3000);
return () => clearInterval(t);
}, [mission?.status, load]);
const refine = useCallback(async () => {
if (!mission) return;
setRefining(true);
@@ -452,6 +476,8 @@ export function MissionCanvas({
"overview",
"phases",
"tasks",
"team",
"live",
"artifacts",
"benchmarks",
...(mission.runtime_kind === "local_herdr" ? (["pane"] as const) : []),
@@ -708,6 +734,14 @@ export function MissionCanvas({
</div>
)}
{tab === "team" && (
<MissionTeamTab teamId={mission.team_id} onOpenClaw={onOpenClaw} />
)}
{tab === "live" && (
<MissionLiveEvents missionId={mission.id} visible={tab === "live"} />
)}
{tab === "artifacts" && (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{mission.artifacts.length === 0 ? (