loops wizard: auto-provision team in-wizard (parity with research)
The LoopsWizard hard-blocked on an empty roster via NoAgentsGate — new users had to bounce to the Agents page first. Match ResearchWizard's flow: remove the top-level gate, add an auto-provision panel to the staffing step (6) that derives 3-5 roles from the loop's task via Claude Sonnet 5 and preselects the returned agents into selectedAgents so the existing submit path publishes them onto the loop with zero extra clicks. Existing rosters still work — the hand-pick panel now sits below the auto-provision card and only renders when the workspace has agents and no team was auto-provisioned this session. Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
657b666219
commit
9cab32c354
@@ -23,7 +23,10 @@ import {
|
|||||||
// production build. Call /api/topologies with plain fetch instead.
|
// production build. Call /api/topologies with plain fetch instead.
|
||||||
import type { CatalogEntry, TopologyGraph } from "@/lib/api/topology";
|
import type { CatalogEntry, TopologyGraph } from "@/lib/api/topology";
|
||||||
import { RepoPicker, type PickedRepo } from "./RepoPicker";
|
import { RepoPicker, type PickedRepo } from "./RepoPicker";
|
||||||
import { NoAgentsGate } from "./NoAgentsGate";
|
import {
|
||||||
|
autoProvisionTeam,
|
||||||
|
type AutoProvisionResponse,
|
||||||
|
} from "@/lib/api/research";
|
||||||
// Do NOT import @/lib/api/team here — it transitively pulls bearer.ts
|
// Do NOT import @/lib/api/team here — it transitively pulls bearer.ts
|
||||||
// (next/headers, server-only) into the client bundle and breaks the
|
// (next/headers, server-only) into the client bundle and breaks the
|
||||||
// production build. Call /api/team/claws with plain fetch instead.
|
// production build. Call /api/team/claws with plain fetch instead.
|
||||||
@@ -39,7 +42,7 @@ const mono =
|
|||||||
|
|
||||||
export function LoopsWizard({
|
export function LoopsWizard({
|
||||||
initial,
|
initial,
|
||||||
agentsCount,
|
agentsCount: _agentsCount,
|
||||||
onClose,
|
onClose,
|
||||||
onCreated,
|
onCreated,
|
||||||
onSaved,
|
onSaved,
|
||||||
@@ -53,9 +56,6 @@ export function LoopsWizard({
|
|||||||
onSaved?: (id: string) => void;
|
onSaved?: (id: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const editing = !!initial;
|
const editing = !!initial;
|
||||||
const [rosterCount, setRosterCount] = useState<number | null>(
|
|
||||||
agentsCount ?? null,
|
|
||||||
);
|
|
||||||
const [rosterAgents, setRosterAgents] = useState<Agent[]>([]);
|
const [rosterAgents, setRosterAgents] = useState<Agent[]>([]);
|
||||||
const [selectedAgents, setSelectedAgents] = useState<AgentSelection[]>(
|
const [selectedAgents, setSelectedAgents] = useState<AgentSelection[]>(
|
||||||
initial?.agents?.map((a) => ({
|
initial?.agents?.map((a) => ({
|
||||||
@@ -155,17 +155,53 @@ export function LoopsWizard({
|
|||||||
const claws = (await r.json()) as Agent[];
|
const claws = (await r.json()) as Agent[];
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setRosterAgents(claws);
|
setRosterAgents(claws);
|
||||||
if (agentsCount === undefined) setRosterCount(claws.length);
|
|
||||||
} catch {
|
} catch {
|
||||||
if (!cancelled && agentsCount === undefined) setRosterCount(0);
|
// Roster is optional — auto-provision covers empty rosters.
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [agentsCount]);
|
}, []);
|
||||||
|
|
||||||
const noAgents = rosterCount !== null && rosterCount === 0;
|
// Roster-empty state no longer blocks the wizard — step 6 offers
|
||||||
|
// auto-provision so a new user can pick a topology + kick off a team
|
||||||
|
// without visiting the Agents page first (parity with ResearchWizard).
|
||||||
|
|
||||||
|
// Auto-provision state: mirrors ResearchWizard's `runAutoProvision`
|
||||||
|
// flow. On success we preselect the returned agents so submit flows
|
||||||
|
// through the existing `agents:` submit path.
|
||||||
|
const [autoTeam, setAutoTeam] = useState<AutoProvisionResponse | null>(null);
|
||||||
|
const [autoProvisioning, setAutoProvisioning] = useState(false);
|
||||||
|
const [autoProvisionError, setAutoProvisionError] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
async function runAutoProvision() {
|
||||||
|
setAutoProvisionError(null);
|
||||||
|
setAutoProvisioning(true);
|
||||||
|
try {
|
||||||
|
const r = await autoProvisionTeam({
|
||||||
|
title: title.trim() || task.trim().slice(0, 60) || "Loop team",
|
||||||
|
description: task.trim(),
|
||||||
|
outcome_kind: "loop",
|
||||||
|
topology_kind: kind,
|
||||||
|
model: "claude-sonnet-5",
|
||||||
|
});
|
||||||
|
setAutoTeam(r);
|
||||||
|
setSelectedAgents(
|
||||||
|
r.agents.map((a) => ({
|
||||||
|
agent_id: a.agent_id,
|
||||||
|
role_slot: a.role_slot,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
setAutoProvisionError(
|
||||||
|
e instanceof Error ? e.message : "auto-provision failed",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setAutoProvisioning(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const selectedEntry = useMemo(
|
const selectedEntry = useMemo(
|
||||||
() => catalog.find((c) => c.kind === kind),
|
() => catalog.find((c) => c.kind === kind),
|
||||||
@@ -363,10 +399,7 @@ export function LoopsWizard({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ flex: 1, minHeight: 0, overflow: "auto", padding: 20 }}>
|
<div style={{ flex: 1, minHeight: 0, overflow: "auto", padding: 20 }}>
|
||||||
{noAgents ? (
|
{step === 1 && (
|
||||||
<NoAgentsGate what="loop" onDismiss={onClose} />
|
|
||||||
) : null}
|
|
||||||
{!noAgents && step === 1 && (
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||||
<label style={labelStyle} htmlFor="loop-title">Title</label>
|
<label style={labelStyle} htmlFor="loop-title">Title</label>
|
||||||
<input
|
<input
|
||||||
@@ -387,7 +420,7 @@ export function LoopsWizard({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!noAgents && step === 2 && (
|
{step === 2 && (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||||
<span style={labelStyle}>Repository (optional)</span>
|
<span style={labelStyle}>Repository (optional)</span>
|
||||||
<p style={hintStyle}>
|
<p style={hintStyle}>
|
||||||
@@ -399,7 +432,7 @@ export function LoopsWizard({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!noAgents && step === 3 && (
|
{step === 3 && (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
||||||
<label style={labelStyle} htmlFor="loop-task">Task template</label>
|
<label style={labelStyle} htmlFor="loop-task">Task template</label>
|
||||||
@@ -499,7 +532,7 @@ export function LoopsWizard({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!noAgents && step === 4 && (
|
{step === 4 && (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||||
<span style={labelStyle}>Triggers (pick any combination)</span>
|
<span style={labelStyle}>Triggers (pick any combination)</span>
|
||||||
<TriggerToggle
|
<TriggerToggle
|
||||||
@@ -564,7 +597,7 @@ export function LoopsWizard({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!noAgents && step === 5 && (
|
{step === 5 && (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||||
<span style={labelStyle}>Repeat policy</span>
|
<span style={labelStyle}>Repeat policy</span>
|
||||||
<label style={radioRowStyle(repeatKind === "infinite")}>
|
<label style={radioRowStyle(repeatKind === "infinite")}>
|
||||||
@@ -671,7 +704,83 @@ export function LoopsWizard({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!noAgents && step === 6 && (
|
{step === 6 && (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: 12,
|
||||||
|
borderRadius: 8,
|
||||||
|
border: `1px solid ${autoTeam ? "rgba(95,208,138,.35)" : "rgba(94,200,216,.35)"}`,
|
||||||
|
background: autoTeam
|
||||||
|
? "rgba(95,208,138,.06)"
|
||||||
|
: "rgba(94,200,216,.06)",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontFamily: mono,
|
||||||
|
fontSize: 12,
|
||||||
|
color: autoTeam ? "#83e6a5" : "#7cd6e0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{autoTeam
|
||||||
|
? `Team ready · ${autoTeam.agents.length} agents · ${autoTeam.topology_kind}`
|
||||||
|
: "No team yet? Auto-provision one from the loop's task."}
|
||||||
|
</span>
|
||||||
|
{!autoTeam && (
|
||||||
|
<p style={hintStyle}>
|
||||||
|
We ask Claude Sonnet 5 to derive 3–5 roles from the task
|
||||||
|
above, then create each agent + wire them into this loop.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{autoTeam ? (
|
||||||
|
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
|
||||||
|
{autoTeam.agents.map((a) => (
|
||||||
|
<span
|
||||||
|
key={a.agent_id}
|
||||||
|
style={{
|
||||||
|
fontFamily: mono,
|
||||||
|
fontSize: 11,
|
||||||
|
padding: "3px 8px",
|
||||||
|
borderRadius: 999,
|
||||||
|
border: "1px solid rgba(255,255,255,.14)",
|
||||||
|
background: "rgba(255,255,255,.04)",
|
||||||
|
color: "#d7d7db",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{a.role_slot} · {a.display_name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={runAutoProvision}
|
||||||
|
disabled={autoProvisioning || task.trim().length === 0}
|
||||||
|
style={{
|
||||||
|
...primaryBtn,
|
||||||
|
opacity:
|
||||||
|
autoProvisioning || task.trim().length === 0 ? 0.5 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{autoProvisioning
|
||||||
|
? "Provisioning…"
|
||||||
|
: "Auto-provision team"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{autoProvisionError && (
|
||||||
|
<p style={{ ...hintStyle, color: "#ff8a7a" }}>
|
||||||
|
{autoProvisionError}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{rosterAgents.length > 0 && !autoTeam ? (
|
||||||
|
<>
|
||||||
|
<span style={labelStyle}>Or hand-pick from your roster</span>
|
||||||
<LoopStaffingStep
|
<LoopStaffingStep
|
||||||
agents={rosterAgents}
|
agents={rosterAgents}
|
||||||
selectedAgents={selectedAgents}
|
selectedAgents={selectedAgents}
|
||||||
@@ -681,9 +790,12 @@ export function LoopsWizard({
|
|||||||
selectedOrgIds={selectedOrgIds}
|
selectedOrgIds={selectedOrgIds}
|
||||||
onOrgsChange={setSelectedOrgIds}
|
onOrgsChange={setSelectedOrgIds}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!noAgents && step === 7 && created && (
|
{step === 7 && created && (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||||
<p style={{ fontFamily: mono, fontSize: 12, color: "#ffb44a", lineHeight: 1.6 }}>
|
<p style={{ fontFamily: mono, fontSize: 12, color: "#ffb44a", lineHeight: 1.6 }}>
|
||||||
Save these now — they're never shown again. To rotate, disable
|
Save these now — they're never shown again. To rotate, disable
|
||||||
@@ -700,7 +812,6 @@ export function LoopsWizard({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!noAgents && (
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: 14,
|
padding: 14,
|
||||||
@@ -759,7 +870,6 @@ export function LoopsWizard({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{pickerOpen ? (
|
{pickerOpen ? (
|
||||||
<ResearchArtifactPicker
|
<ResearchArtifactPicker
|
||||||
|
|||||||
Reference in New Issue
Block a user