Explorar el Código

feat: improve team management frontend

Jax Docker hace 1 mes
padre
commit
f1f5fb419c
Se han modificado 2 ficheros con 463 adiciones y 118 borrados
  1. 33 3
      web/src/components/ui/dialog.tsx
  2. 430 115
      web/src/pages/teams/TeamsPage.tsx

+ 33 - 3
web/src/components/ui/dialog.tsx

@@ -13,9 +13,39 @@ interface DialogProps {
 }
 
 export function Dialog({ open, onOpenChange, title, description, children, className }: DialogProps) {
+  const titleId = React.useId();
+
+  React.useEffect(() => {
+    if (!open) return;
+
+    const previousOverflow = document.body.style.overflow;
+    document.body.style.overflow = "hidden";
+
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (event.key === "Escape") {
+        onOpenChange(false);
+      }
+    };
+
+    window.addEventListener("keydown", handleKeyDown);
+    return () => {
+      window.removeEventListener("keydown", handleKeyDown);
+      document.body.style.overflow = previousOverflow;
+    };
+  }, [onOpenChange, open]);
+
   if (!open) return null;
+
   return (
-    <div className="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/55 p-3 sm:p-6" role="presentation">
+    <div
+      className="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/55 p-3 sm:p-6"
+      role="presentation"
+      onMouseDown={(event) => {
+        if (event.target === event.currentTarget) {
+          onOpenChange(false);
+        }
+      }}
+    >
       <div
         className={cn(
           "my-auto max-h-[calc(100dvh-1.5rem)] w-full max-w-2xl overflow-auto rounded-md border border-border bg-surface-elevated shadow-glow sm:max-h-[calc(100dvh-3rem)]",
@@ -23,11 +53,11 @@ export function Dialog({ open, onOpenChange, title, description, children, class
         )}
         role="dialog"
         aria-modal="true"
-        aria-labelledby="dialog-title"
+        aria-labelledby={titleId}
       >
         <div className="sticky top-0 z-10 flex items-start justify-between gap-4 border-b border-border bg-surface-elevated p-4 sm:p-5">
           <div>
-            <h2 id="dialog-title" className="text-lg font-semibold">{title}</h2>
+            <h2 id={titleId} className="text-lg font-semibold">{title}</h2>
             {description ? <p className="mt-1 text-sm text-muted-foreground">{description}</p> : null}
           </div>
           <Button variant="ghost" size="icon" onClick={() => onOpenChange(false)} aria-label="Close">

+ 430 - 115
web/src/pages/teams/TeamsPage.tsx

@@ -22,7 +22,6 @@ import { createTeam, createTeamRun, createTeamVersion, listTeamRuns, listTeamVer
 import { ApiErrorState } from "@/components/shared/ApiErrorState";
 import { EmptyState } from "@/components/shared/EmptyState";
 import { EntityListItem } from "@/components/shared/EntityListItem";
-import { JsonViewer } from "@/components/shared/JsonViewer";
 import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
 import { MetricCard } from "@/components/shared/MetricCard";
 import { PageHeader } from "@/components/shared/PageHeader";
@@ -44,6 +43,16 @@ type StatusFilter = "all" | TeamStatus;
 type RunStatusFilter = "all" | TeamRunStatus;
 type SortMode = "recent" | "name" | "status";
 type ViewMode = "list" | "grid";
+type MemberDraft = {
+  role: string;
+  agent_id: string;
+  responsibility: string;
+};
+type PolicyDraft = {
+  max_rounds: string;
+  handoff: string;
+  failure_mode: string;
+};
 
 const TEAM_TYPE_OPTIONS = [
   { value: "collaborative", label: "Collaborative" },
@@ -65,6 +74,25 @@ const RUN_TEMPLATE_OPTIONS = [
   "Compare two implementation options and return a decision with tradeoffs.",
 ];
 
+const DEFAULT_MEMBER_DRAFTS: MemberDraft[] = [
+  {
+    role: "supervisor",
+    agent_id: "agent_supervisor",
+    responsibility: "Plan work, route tasks, and merge final output.",
+  },
+  {
+    role: "worker",
+    agent_id: "agent_worker",
+    responsibility: "Execute assigned specialist tasks.",
+  },
+];
+
+const DEFAULT_POLICY_DRAFT: PolicyDraft = {
+  max_rounds: "3",
+  handoff: "supervisor",
+  failure_mode: "stop_on_critical",
+};
+
 export function TeamsPage() {
   const [teams, setTeams] = React.useState<TeamDefinition[]>([]);
   const [versions, setVersions] = React.useState<TeamVersion[]>([]);
@@ -80,9 +108,11 @@ export function TeamsPage() {
   const [loading, setLoading] = React.useState(true);
   const [relatedLoading, setRelatedLoading] = React.useState(false);
   const [error, setError] = React.useState<string>();
+  const [relatedError, setRelatedError] = React.useState<string>();
   const [createOpen, setCreateOpen] = React.useState(false);
   const [versionOpen, setVersionOpen] = React.useState(false);
   const [runOpen, setRunOpen] = React.useState(false);
+  const detailsRequestRef = React.useRef(0);
 
   const selectedTeam = teams.find((team) => team.id === selectedTeamId);
   const teamTypes = React.useMemo(() => Array.from(new Set(teams.map((team) => team.team_type))).sort(), [teams]);
@@ -132,13 +162,28 @@ export function TeamsPage() {
 
   const reloadTeamDetails = React.useCallback(async () => {
     if (!selectedTeamId) return;
+    const requestId = detailsRequestRef.current + 1;
+    detailsRequestRef.current = requestId;
     setRelatedLoading(true);
+    setRelatedError(undefined);
+    setVersions([]);
+    setRuns([]);
     try {
       const [versionData, runData] = await Promise.all([listTeamVersions(selectedTeamId), listTeamRuns(selectedTeamId)]);
-      setVersions(versionData);
-      setRuns(runData);
+      if (detailsRequestRef.current === requestId) {
+        setVersions(versionData);
+        setRuns(runData);
+      }
+    } catch (err) {
+      if (detailsRequestRef.current === requestId) {
+        const message = err instanceof Error ? err.message : "Failed to load team details";
+        setRelatedError(message);
+        toast.error(message);
+      }
     } finally {
-      setRelatedLoading(false);
+      if (detailsRequestRef.current === requestId) {
+        setRelatedLoading(false);
+      }
     }
   }, [selectedTeamId]);
 
@@ -148,10 +193,13 @@ export function TeamsPage() {
 
   React.useEffect(() => {
     if (!selectedTeamId) {
+      detailsRequestRef.current += 1;
       setVersions([]);
       setRuns([]);
+      setRelatedError(undefined);
       return;
     }
+    setRunStatusFilter("all");
     void reloadTeamDetails();
   }, [reloadTeamDetails, selectedTeamId]);
 
@@ -174,6 +222,22 @@ export function TeamsPage() {
     setSortMode("recent");
   }
 
+  function openVersionCreator() {
+    if (!selectedTeam) return;
+    setVersionOpen(true);
+  }
+
+  function openRunStarter() {
+    if (!selectedTeam) return;
+    if (!latestVersion) {
+      setActiveTab("versions");
+      setVersionOpen(true);
+      toast.info("Create a team version before starting a run");
+      return;
+    }
+    setRunOpen(true);
+  }
+
   if (loading) return <LoadingSpinner label="Loading teams" />;
   if (error) return <ApiErrorState message={error} onRetry={() => void load()} />;
 
@@ -326,78 +390,89 @@ export function TeamsPage() {
                 <Button variant="outline" disabled={!selectedTeam} onClick={() => void copyTeamCode()}>
                   <Copy className="h-4 w-4" /> Copy Code
                 </Button>
-                <Button variant="secondary" disabled={!selectedTeam} onClick={() => setVersionOpen(true)}>
+                <Button variant="secondary" disabled={!selectedTeam} onClick={openVersionCreator}>
                   <FileCode2 className="h-4 w-4" /> New Version
                 </Button>
-                <Button disabled={!selectedTeam || !latestVersion} onClick={() => setRunOpen(true)}>
-                  <Play className="h-4 w-4" /> Run
+                <Button disabled={!selectedTeam} onClick={openRunStarter}>
+                  {latestVersion ? <Play className="h-4 w-4" /> : <FileCode2 className="h-4 w-4" />}
+                  {latestVersion ? "Run" : "Create Version"}
                 </Button>
               </div>
             </div>
           </CardHeader>
           <CardContent>
             {selectedTeam ? (
-              <Tabs
-                value={activeTab}
-                onChange={setActiveTab}
-                tabs={[
-                  {
-                    value: "overview",
-                    label: "Overview",
-                    content: (
-                      <TeamOverview
-                        team={selectedTeam}
-                        latestVersion={latestVersion}
-                        versionCount={versions.length}
-                        runCount={sortedRuns.length}
-                        failedRunCount={failedRuns}
-                        latestRun={sortedRuns[0]}
-                      />
-                    ),
-                  },
-                  {
-                    value: "members",
-                    label: "Members",
-                    content: <TeamMembers version={latestVersion} loading={relatedLoading} onCreateVersion={() => setVersionOpen(true)} />,
-                  },
-                  {
-                    value: "versions",
-                    label: "Versions",
-                    content: <TeamVersions versions={sortedVersions} loading={relatedLoading} onCreateVersion={() => setVersionOpen(true)} />,
-                  },
-                  {
-                    value: "runs",
-                    label: "Runs",
-                    content: (
-                      <TeamRuns
-                        runs={filteredRuns}
-                        loading={relatedLoading}
-                        statusFilter={runStatusFilter}
-                        onStatusFilterChange={setRunStatusFilter}
-                        onStartRun={() => setRunOpen(true)}
-                      />
-                    ),
-                  },
-                  {
-                    value: "console",
-                    label: "Run Console",
-                    content: (
-                      <TeamRunConsole
-                        team={selectedTeam}
-                        versions={sortedVersions}
-                        runs={sortedRuns}
-                        loading={relatedLoading}
-                        onCreated={() => void reloadTeamDetails()}
-                      />
-                    ),
-                  },
-                  {
-                    value: "policy",
-                    label: "Policy",
-                    content: <TeamPolicy version={latestVersion} loading={relatedLoading} onCreateVersion={() => setVersionOpen(true)} />,
-                  },
-                ]}
-              />
+              <div className="space-y-4">
+                {relatedError ? <InlineError message={relatedError} onRetry={() => void reloadTeamDetails()} /> : null}
+                <Tabs
+                  value={activeTab}
+                  onChange={setActiveTab}
+                  tabs={[
+                    {
+                      value: "overview",
+                      label: "Overview",
+                      content: (
+                        <TeamOverview
+                          team={selectedTeam}
+                          latestVersion={latestVersion}
+                          versionCount={versions.length}
+                          runCount={sortedRuns.length}
+                          failedRunCount={failedRuns}
+                          latestRun={sortedRuns[0]}
+                          loading={relatedLoading}
+                        />
+                      ),
+                    },
+                    {
+                      value: "members",
+                      label: "Members",
+                      content: <TeamMembers version={latestVersion} loading={relatedLoading} onCreateVersion={openVersionCreator} />,
+                    },
+                    {
+                      value: "versions",
+                      label: "Versions",
+                      content: <TeamVersions versions={sortedVersions} loading={relatedLoading} onCreateVersion={openVersionCreator} />,
+                    },
+                    {
+                      value: "runs",
+                      label: "Runs",
+                      content: (
+                        <TeamRuns
+                          runs={filteredRuns}
+                          loading={relatedLoading}
+                          statusFilter={runStatusFilter}
+                          onStatusFilterChange={setRunStatusFilter}
+                          onStartRun={openRunStarter}
+                          canStartRun={Boolean(latestVersion)}
+                        />
+                      ),
+                    },
+                    {
+                      value: "console",
+                      label: "Run Console",
+                      content: (
+                        <TeamRunConsole
+                          team={selectedTeam}
+                          versions={sortedVersions}
+                          runs={sortedRuns}
+                          loading={relatedLoading}
+                          onCreated={(run) => {
+                            setRuns((current) => [run, ...current]);
+                            setActiveTab("runs");
+                            void reloadTeamDetails();
+                          }}
+                          onCreateVersion={openVersionCreator}
+                        />
+                      ),
+                    },
+                    {
+                      value: "policy",
+                      label: "Policy",
+                      content: <TeamPolicy version={latestVersion} loading={relatedLoading} onCreateVersion={openVersionCreator} />,
+                    },
+                  ]}
+                />
+              </div>
             ) : (
               <EmptyState icon={Bot} title="No teams" description="Create a team to start coordinating multiple specialized agents." actionLabel="New Team" onAction={() => setCreateOpen(true)} />
             )}
@@ -405,19 +480,37 @@ export function TeamsPage() {
         </Card>
       </div>
 
-      <CreateTeamDialog open={createOpen} onOpenChange={setCreateOpen} onCreated={() => void load()} />
+      <CreateTeamDialog
+        open={createOpen}
+        onOpenChange={setCreateOpen}
+        onCreated={(team) => {
+          setSelectedTeamId(team.id);
+          setActiveTab("overview");
+          setSearch("");
+          void load();
+        }}
+      />
       <CreateTeamVersionDialog
         open={versionOpen}
         onOpenChange={setVersionOpen}
         teamId={selectedTeamId}
-        onCreated={() => void reloadTeamDetails()}
+        onCreated={(version) => {
+          setVersions((current) => [version, ...current]);
+          setActiveTab("console");
+          void reloadTeamDetails();
+        }}
       />
       <CreateTeamRunDialog
         open={runOpen}
         onOpenChange={setRunOpen}
         teamId={selectedTeamId}
         versions={sortedVersions}
-        onCreated={() => void reloadTeamDetails()}
+        onCreateVersion={openVersionCreator}
+        onCreated={(run) => {
+          setRuns((current) => [run, ...current]);
+          setActiveTab("runs");
+          void reloadTeamDetails();
+        }}
       />
     </div>
   );
@@ -448,6 +541,17 @@ function TeamCard({ team, active, onOpen }: { team: TeamDefinition; active: bool
   );
 }
 
+function InlineError({ message, onRetry }: { message: string; onRetry: () => void }) {
+  return (
+    <div className="flex flex-col gap-3 rounded-md border border-red-500/25 bg-red-500/5 p-3 text-sm sm:flex-row sm:items-center sm:justify-between">
+      <p className="text-foreground">{message}</p>
+      <Button type="button" size="sm" variant="outline" onClick={onRetry}>
+        <RefreshCw className="h-4 w-4" /> Retry
+      </Button>
+    </div>
+  );
+}
+
 function TeamOverview({
   team,
   latestVersion,
@@ -455,6 +559,7 @@ function TeamOverview({
   runCount,
   failedRunCount,
   latestRun,
+  loading,
 }: {
   team: TeamDefinition;
   latestVersion?: TeamVersion;
@@ -462,14 +567,15 @@ function TeamOverview({
   runCount: number;
   failedRunCount: number;
   latestRun?: TeamRun;
+  loading: boolean;
 }) {
   return (
     <div className="space-y-5">
       <div className="grid gap-4 md:grid-cols-4">
-        <SummaryTile label="Versions" value={versionCount} />
-        <SummaryTile label="Runs" value={runCount} />
-        <SummaryTile label="Failures" value={failedRunCount} />
-        <SummaryTile label="Latest" value={latestVersion ? `v${latestVersion.version_no}` : "None"} />
+        <SummaryTile label="Versions" value={loading ? "..." : versionCount} />
+        <SummaryTile label="Runs" value={loading ? "..." : runCount} />
+        <SummaryTile label="Failures" value={loading ? "..." : failedRunCount} />
+        <SummaryTile label="Latest" value={loading ? "..." : latestVersion ? `v${latestVersion.version_no}` : "None"} />
       </div>
       <div className="grid gap-4 text-sm md:grid-cols-2">
         <Detail label="Code" value={team.code} mono />
@@ -506,29 +612,29 @@ function TeamMembers({
   }
   if (!version.member_refs_json.length) {
     return (
-      <div className="space-y-4">
-        <EmptyState icon={Users} title="No members configured" description="This version has no member references yet. The policy and members can still be inspected from the raw version payload." />
-        <JsonViewer value={version.member_refs_json} collapsed={false} />
-      </div>
+      <EmptyState
+        icon={Users}
+        title="No members configured"
+        description="Create a new version with member rows to define who participates and what each role is responsible for."
+        actionLabel="New Version"
+        onAction={onCreateVersion}
+      />
     );
   }
   return (
-    <div className="space-y-4">
-      <div className="grid gap-3 md:grid-cols-2">
-        {version.member_refs_json.map((member, index) => (
-          <div key={index} className="rounded-md border border-border bg-muted/30 p-4">
-            <div className="flex items-start justify-between gap-3">
-              <div className="min-w-0">
-                <p className="truncate text-sm font-semibold">{getJsonString(member, "name") ?? getJsonString(member, "agent_name") ?? `Member ${index + 1}`}</p>
-                <p className="mt-1 truncate font-mono text-xs text-muted-foreground">{getJsonString(member, "agent_id") ?? getJsonString(member, "id") ?? "unbound-agent"}</p>
-              </div>
-              <Badge className="border-border bg-muted/60 text-muted-foreground">{getJsonString(member, "role") ?? "member"}</Badge>
+    <div className="grid gap-3 md:grid-cols-2">
+      {version.member_refs_json.map((member, index) => (
+        <div key={index} className="rounded-md border border-border bg-muted/30 p-4">
+          <div className="flex items-start justify-between gap-3">
+            <div className="min-w-0">
+              <p className="truncate text-sm font-semibold">{getJsonString(member, "name") ?? getJsonString(member, "agent_name") ?? `Member ${index + 1}`}</p>
+              <p className="mt-1 truncate font-mono text-xs text-muted-foreground">{getJsonString(member, "agent_id") ?? getJsonString(member, "id") ?? "unbound-agent"}</p>
             </div>
-            <p className="mt-3 text-sm leading-6 text-muted-foreground">{getJsonString(member, "description") ?? getJsonString(member, "responsibility") ?? "No responsibility summary provided."}</p>
+            <Badge className="border-border bg-muted/60 text-muted-foreground">{getJsonString(member, "role") ?? "member"}</Badge>
           </div>
-        ))}
-      </div>
-      <JsonViewer value={version.member_refs_json} />
+          <p className="mt-3 text-sm leading-6 text-muted-foreground">{getJsonString(member, "description") ?? getJsonString(member, "responsibility") ?? "No responsibility summary provided."}</p>
+        </div>
+      ))}
     </div>
   );
 }
@@ -578,12 +684,14 @@ function TeamRuns({
   statusFilter,
   onStatusFilterChange,
   onStartRun,
+  canStartRun,
 }: {
   runs: TeamRun[];
   loading: boolean;
   statusFilter: RunStatusFilter;
   onStatusFilterChange: (status: RunStatusFilter) => void;
   onStartRun: () => void;
+  canStartRun: boolean;
 }) {
   if (loading) return <LoadingSpinner label="Loading runs" />;
   return (
@@ -604,7 +712,8 @@ function TeamRuns({
           ]}
         />
         <Button type="button" variant="secondary" onClick={onStartRun}>
-          <Play className="h-4 w-4" /> Start Run
+          {canStartRun ? <Play className="h-4 w-4" /> : <FileCode2 className="h-4 w-4" />}
+          {canStartRun ? "Start Run" : "Create Version"}
         </Button>
       </div>
       {runs.length ? (
@@ -627,7 +736,13 @@ function TeamRuns({
           ))}
         </div>
       ) : (
-        <EmptyState icon={Activity} title="No runs" description="No run records match this team and status filter." actionLabel="Start Run" onAction={onStartRun} />
+        <EmptyState
+          icon={Activity}
+          title="No runs"
+          description={canStartRun ? "No run records match this team and status filter." : "Create a version before this team can run work."}
+          actionLabel={canStartRun ? "Start Run" : "Create Version"}
+          onAction={onStartRun}
+        />
       )}
     </div>
   );
@@ -639,12 +754,14 @@ function TeamRunConsole({
   runs,
   loading,
   onCreated,
+  onCreateVersion,
 }: {
   team: TeamDefinition;
   versions: TeamVersion[];
   runs: TeamRun[];
   loading: boolean;
-  onCreated: () => void;
+  onCreated: (run: TeamRun) => void;
+  onCreateVersion: () => void;
 }) {
   const tenantId = useAuthStore((state) => state.tenantId);
   const [versionId, setVersionId] = React.useState(versions[0]?.id ?? "");
@@ -660,10 +777,12 @@ function TeamRunConsole({
     if (!versionId || !inputText.trim()) return;
     setSubmitting(true);
     try {
-      await createTeamRun({ tenant_id: tenantId, team_id: team.id, team_version_id: versionId, input_text: inputText });
+      const run = await createTeamRun({ tenant_id: tenantId, team_id: team.id, team_version_id: versionId, input_text: inputText });
       toast.success("Team run started");
       setInputText("");
-      onCreated();
+      onCreated(run);
+    } catch (err) {
+      toast.error(err instanceof Error ? err.message : "Failed to start team run");
     } finally {
       setSubmitting(false);
     }
@@ -671,7 +790,15 @@ function TeamRunConsole({
 
   if (loading) return <LoadingSpinner label="Preparing console" />;
   if (!versions.length) {
-    return <EmptyState icon={Play} title="No runnable version" description="Create a team version before starting a collaborative run." />;
+    return (
+      <EmptyState
+        icon={Play}
+        title="No runnable version"
+        description="Create a team version before starting a collaborative run."
+        actionLabel="Create Version"
+        onAction={onCreateVersion}
+      />
+    );
   }
 
   return (
@@ -739,7 +866,7 @@ function TeamPolicy({
     return <EmptyState icon={Settings2} title="No policy" description="Create a team version to inspect member policy and coordination settings." actionLabel="New Version" onAction={onCreateVersion} />;
   }
   return (
-    <div className="grid gap-4 lg:grid-cols-2">
+    <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
       <section className="rounded-md border border-border bg-muted/30 p-4">
         <div className="mb-3 flex items-center gap-2">
           <Layers3 className="h-4 w-4 text-muted-foreground" />
@@ -751,11 +878,22 @@ function TeamPolicy({
           <Detail label="Status" value={version.status} />
         </div>
       </section>
-      <JsonViewer value={version.policy_json} collapsed={false} />
+      <PolicyTile label="Max Rounds" value={getJsonString(version.policy_json, "max_rounds") ?? "Not set"} />
+      <PolicyTile label="Handoff" value={readableLabel(getJsonString(version.policy_json, "handoff") ?? "Not set")} />
+      <PolicyTile label="Failure Mode" value={readableLabel(getJsonString(version.policy_json, "failure_mode") ?? "Not set")} />
     </div>
   );
 }
 
+function PolicyTile({ label, value }: { label: string; value: string }) {
+  return (
+    <section className="rounded-md border border-border bg-muted/30 p-4">
+      <p className="text-sm text-muted-foreground">{label}</p>
+      <p className="mt-2 text-lg font-semibold">{value}</p>
+    </section>
+  );
+}
+
 function Detail({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
   return (
     <div className="min-w-0">
@@ -783,22 +921,43 @@ function CreateTeamVersionDialog({
   open: boolean;
   onOpenChange: (open: boolean) => void;
   teamId?: string;
-  onCreated: () => void;
+  onCreated: (version: TeamVersion) => void;
 }) {
   const tenantId = useAuthStore((state) => state.tenantId);
   const [form, setForm] = React.useState({ coordination_mode: "supervisor", objective: "" });
+  const [members, setMembers] = React.useState<MemberDraft[]>(DEFAULT_MEMBER_DRAFTS);
+  const [policy, setPolicy] = React.useState<PolicyDraft>(DEFAULT_POLICY_DRAFT);
+  const [formError, setFormError] = React.useState<string>();
   const [submitting, setSubmitting] = React.useState(false);
 
   async function submit(event: React.FormEvent) {
     event.preventDefault();
     if (!teamId) return;
+    const versionPayload = buildVersionConfig(members, policy);
+    if (!versionPayload.ok) {
+      setFormError(versionPayload.message);
+      return;
+    }
     setSubmitting(true);
+    setFormError(undefined);
     try {
-      await createTeamVersion({ tenant_id: tenantId, team_id: teamId, ...form, status: "draft" });
+      const version = await createTeamVersion({
+        tenant_id: tenantId,
+        team_id: teamId,
+        coordination_mode: form.coordination_mode,
+        objective: form.objective,
+        member_refs: versionPayload.memberRefs,
+        policy_json: versionPayload.policyJson,
+        status: "draft",
+      });
       toast.success("Team version created");
       onOpenChange(false);
       setForm({ coordination_mode: "supervisor", objective: "" });
-      onCreated();
+      setMembers(DEFAULT_MEMBER_DRAFTS);
+      setPolicy(DEFAULT_POLICY_DRAFT);
+      onCreated(version);
+    } catch (err) {
+      toast.error(err instanceof Error ? err.message : "Failed to create team version");
     } finally {
       setSubmitting(false);
     }
@@ -821,9 +980,9 @@ function CreateTeamVersionDialog({
             onChange={(event) => setForm({ ...form, objective: event.target.value })}
           />
         </Field>
-        <div className="rounded-md border border-border bg-muted/40 p-3 text-sm text-muted-foreground">
-          Members and policy start empty so the version can be refined without blocking creation.
-        </div>
+        <MemberDraftEditor members={members} onChange={setMembers} />
+        <PolicyDraftEditor policy={policy} onChange={setPolicy} />
+        {formError ? <p className="rounded-md border border-red-500/25 bg-red-500/5 p-3 text-sm text-foreground">{formError}</p> : null}
         <div className="flex justify-end gap-2">
           <Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
             Cancel
@@ -835,18 +994,122 @@ function CreateTeamVersionDialog({
   );
 }
 
+function MemberDraftEditor({ members, onChange }: { members: MemberDraft[]; onChange: (members: MemberDraft[]) => void }) {
+  function updateMember(index: number, patch: Partial<MemberDraft>) {
+    onChange(members.map((member, memberIndex) => (memberIndex === index ? { ...member, ...patch } : member)));
+  }
+
+  function removeMember(index: number) {
+    onChange(members.filter((_, memberIndex) => memberIndex !== index));
+  }
+
+  return (
+    <section className="space-y-3">
+      <div className="flex items-center justify-between gap-3">
+        <h3 className="text-sm font-medium">Members</h3>
+        <Button
+          type="button"
+          size="sm"
+          variant="outline"
+          onClick={() => onChange([...members, { role: "worker", agent_id: "", responsibility: "" }])}
+        >
+          <Users className="h-4 w-4" /> Add Member
+        </Button>
+      </div>
+      <div className="space-y-3">
+        {members.map((member, index) => (
+          <div key={index} className="rounded-md border border-border bg-muted/30 p-3">
+            <div className="grid gap-3 md:grid-cols-[150px_1fr_auto]">
+              <Select
+                aria-label={`Role for member ${index + 1}`}
+                value={member.role}
+                onChange={(event) => updateMember(index, { role: event.target.value })}
+                options={[
+                  { value: "supervisor", label: "Supervisor" },
+                  { value: "worker", label: "Worker" },
+                  { value: "reviewer", label: "Reviewer" },
+                  { value: "planner", label: "Planner" },
+                ]}
+              />
+              <Input
+                aria-label={`Agent id for member ${index + 1}`}
+                value={member.agent_id}
+                placeholder="Agent ID"
+                onChange={(event) => updateMember(index, { agent_id: event.target.value })}
+              />
+              <Button type="button" size="sm" variant="ghost" disabled={members.length <= 1} onClick={() => removeMember(index)}>
+                Remove
+              </Button>
+            </div>
+            <Textarea
+              className="mt-3 min-h-20"
+              aria-label={`Responsibility for member ${index + 1}`}
+              value={member.responsibility}
+              placeholder="Responsibility"
+              onChange={(event) => updateMember(index, { responsibility: event.target.value })}
+            />
+          </div>
+        ))}
+      </div>
+    </section>
+  );
+}
+
+function PolicyDraftEditor({ policy, onChange }: { policy: PolicyDraft; onChange: (policy: PolicyDraft) => void }) {
+  return (
+    <section className="space-y-3">
+      <h3 className="text-sm font-medium">Policy</h3>
+      <div className="grid gap-3 md:grid-cols-3">
+        <Field label="Max rounds">
+          <Input
+            type="number"
+            min={1}
+            max={20}
+            value={policy.max_rounds}
+            onChange={(event) => onChange({ ...policy, max_rounds: event.target.value })}
+          />
+        </Field>
+        <Field label="Handoff">
+          <Select
+            value={policy.handoff}
+            onChange={(event) => onChange({ ...policy, handoff: event.target.value })}
+            options={[
+              { value: "supervisor", label: "Supervisor" },
+              { value: "round_robin", label: "Round Robin" },
+              { value: "parallel_merge", label: "Parallel Merge" },
+            ]}
+          />
+        </Field>
+        <Field label="Failure mode">
+          <Select
+            value={policy.failure_mode}
+            onChange={(event) => onChange({ ...policy, failure_mode: event.target.value })}
+            options={[
+              { value: "stop_on_critical", label: "Stop on Critical" },
+              { value: "continue_with_warning", label: "Continue with Warning" },
+              { value: "retry_once", label: "Retry Once" },
+            ]}
+          />
+        </Field>
+      </div>
+    </section>
+  );
+}
+
 function CreateTeamRunDialog({
   open,
   onOpenChange,
   teamId,
   versions,
+  onCreateVersion,
   onCreated,
 }: {
   open: boolean;
   onOpenChange: (open: boolean) => void;
   teamId?: string;
   versions: TeamVersion[];
-  onCreated: () => void;
+  onCreateVersion: () => void;
+  onCreated: (run: TeamRun) => void;
 }) {
   const tenantId = useAuthStore((state) => state.tenantId);
   const [versionId, setVersionId] = React.useState(versions[0]?.id ?? "");
@@ -859,19 +1122,38 @@ function CreateTeamRunDialog({
 
   async function submit(event: React.FormEvent) {
     event.preventDefault();
-    if (!teamId || !versionId) return;
+    if (!teamId || !versionId || !inputText.trim()) return;
     setSubmitting(true);
     try {
-      await createTeamRun({ tenant_id: tenantId, team_id: teamId, team_version_id: versionId, input_text: inputText });
+      const run = await createTeamRun({ tenant_id: tenantId, team_id: teamId, team_version_id: versionId, input_text: inputText });
       toast.success("Team run started");
       onOpenChange(false);
       setInputText("");
-      onCreated();
+      onCreated(run);
+    } catch (err) {
+      toast.error(err instanceof Error ? err.message : "Failed to start team run");
     } finally {
       setSubmitting(false);
     }
   }
 
+  if (!versions.length) {
+    return (
+      <Dialog open={open} onOpenChange={onOpenChange} title="Start Team Run">
+        <EmptyState
+          icon={Play}
+          title="No runnable version"
+          description="Create a team version before starting a collaborative run."
+          actionLabel="Create Version"
+          onAction={() => {
+            onOpenChange(false);
+            onCreateVersion();
+          }}
+        />
+      </Dialog>
+    );
+  }
+
   return (
     <Dialog open={open} onOpenChange={onOpenChange} title="Start Team Run">
       <form className="space-y-4" onSubmit={submit}>
@@ -892,14 +1174,14 @@ function CreateTeamRunDialog({
           <Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
             Cancel
           </Button>
-          <Button disabled={submitting || !teamId || !versionId}>{submitting ? "Starting..." : "Start Run"}</Button>
+          <Button disabled={submitting || !teamId || !versionId || !inputText.trim()}>{submitting ? "Starting..." : "Start Run"}</Button>
         </div>
       </form>
     </Dialog>
   );
 }
 
-function CreateTeamDialog({ open, onOpenChange, onCreated }: { open: boolean; onOpenChange: (open: boolean) => void; onCreated: () => void }) {
+function CreateTeamDialog({ open, onOpenChange, onCreated }: { open: boolean; onOpenChange: (open: boolean) => void; onCreated: (team: TeamDefinition) => void }) {
   const { tenantId, userId } = useAuthStore();
   const [form, setForm] = React.useState({ name: "", description: "", team_type: "collaborative" });
   const [submitting, setSubmitting] = React.useState(false);
@@ -908,11 +1190,13 @@ function CreateTeamDialog({ open, onOpenChange, onCreated }: { open: boolean; on
     event.preventDefault();
     setSubmitting(true);
     try {
-      await createTeam({ tenant_id: tenantId, owner_user_id: userId, ...form });
+      const team = await createTeam({ tenant_id: tenantId, owner_user_id: userId, ...form });
       toast.success("Team created");
       onOpenChange(false);
       setForm({ name: "", description: "", team_type: "collaborative" });
-      onCreated();
+      onCreated(team);
+    } catch (err) {
+      toast.error(err instanceof Error ? err.message : "Failed to create team");
     } finally {
       setSubmitting(false);
     }
@@ -963,3 +1247,34 @@ function getJsonString(value: JSONObject, key: string) {
   if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") return String(item);
   return undefined;
 }
+
+function buildVersionConfig(members: MemberDraft[], policy: PolicyDraft):
+  | { ok: true; memberRefs: JSONObject[]; policyJson: JSONObject }
+  | { ok: false; message: string } {
+  const normalizedMembers = members
+    .map((member) => ({
+      role: member.role.trim(),
+      agent_id: member.agent_id.trim(),
+      responsibility: member.responsibility.trim(),
+    }))
+    .filter((member) => member.role || member.agent_id || member.responsibility);
+  if (!normalizedMembers.length) {
+    return { ok: false, message: "Add at least one team member." };
+  }
+  if (normalizedMembers.some((member) => !member.agent_id)) {
+    return { ok: false, message: "Each member needs an Agent ID." };
+  }
+  const maxRounds = Number(policy.max_rounds);
+  if (!Number.isInteger(maxRounds) || maxRounds < 1 || maxRounds > 20) {
+    return { ok: false, message: "Max rounds must be a whole number from 1 to 20." };
+  }
+  return {
+    ok: true,
+    memberRefs: normalizedMembers,
+    policyJson: {
+      max_rounds: maxRounds,
+      handoff: policy.handoff,
+      failure_mode: policy.failure_mode,
+    },
+  };
+}