mission progress UI: auto-refresh + Team tab + Live events tab
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:
@@ -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 ? (
|
||||
|
||||
Reference in New Issue
Block a user