"use client"; // Team roster for a mission. Fetches the mission's team (via // /api/teams/{id}) and lists each claw with its role + a link that // hands the operator over to AGENT tier with that claw selected — // where the existing WorkingOnNow / ReasoningStream / metrics live. import { useEffect, useState } from "react"; import { ArrowUpRight, User } from "lucide-react"; const mono = "ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace"; interface Member { claw_id: string; role: string; node_id?: string; } interface TeamDetail { name?: string; members: Member[]; } interface Claw { id: string; name: string; job_title?: string | null; } interface MissionTeamRow { team_id: string; purpose: string; team_name: string; } export function MissionTeamTab({ missionId, teamId, onOpenClaw, }: { missionId: string; teamId: string | null; onOpenClaw?: (clawId: string) => void; }) { const [rows, setRows] = useState([]); const [teams, setTeams] = useState>({}); const [claws, setClaws] = useState>({}); const [error, setError] = useState(null); useEffect(() => { let alive = true; (async () => { try { const [mtRes, cRes] = await Promise.all([ fetch(`/api/missions/${missionId}/teams`), fetch(`/api/team/claws`), ]); if (!mtRes.ok) throw new Error(`teams ${mtRes.status}`); const data = (await mtRes.json()) as { teams: MissionTeamRow[] }; // Fallback for legacy single-team missions: mission_teams empty // but mission.team_id is set — synthesize one "mission" row so // the UI doesn't look empty. const list = data.teams.length > 0 ? data.teams : teamId ? [{ team_id: teamId, purpose: "mission", team_name: "Team" }] : []; const details = await Promise.all( list.map(async (r) => { const t = await fetch(`/api/teams/${r.team_id}`); return t.ok ? ([r.team_id, (await t.json()) as TeamDetail] as const) : null; }), ); const clawList = cRes.ok ? ((await cRes.json()) as Claw[]) : []; if (!alive) return; setRows(list); setTeams(Object.fromEntries(details.filter(Boolean) as (readonly [string, TeamDetail])[])); setClaws(Object.fromEntries(clawList.map((c) => [c.id, c]))); } catch (e) { if (alive) setError(e instanceof Error ? e.message : "load failed"); } })(); return () => { alive = false; }; }, [missionId, teamId]); if (!teamId && rows.length === 0) { return (
No teams yet. Launch the mission to materialize teams from the picked templates.
); } if (error) { return
{error}
; } // Group rows by purpose so each section renders as one card. const grouped = rows.reduce>((acc, r) => { (acc[r.purpose] = acc[r.purpose] ?? []).push(r); return acc; }, {}); return (
{Object.entries(grouped).map(([purpose, purposeRows]) => (
{purpose} · {purposeRows.length} team{purposeRows.length === 1 ? "" : "s"}
{purposeRows.map((r) => { const detail = teams[r.team_id]; const members = detail?.members ?? []; return (
{r.team_name} {members.length} member{members.length === 1 ? "" : "s"}
{members.length === 0 ? (
Materializing…
) : ( members.map((m) => { const claw = claws[m.claw_id]; return (
{claw?.name ?? `claw ${m.claw_id.slice(0, 8)}`}
{m.role}
{onOpenClaw && ( )}
); }) )}
); })}
))}
); }