loops: repo picker + agent/team/org staffing + sidebar edit/delete
Adds the missing pieces the wizard needed and the sidebar controls around it: - LoopsWizard is now a 6-step flow (identity → repo → task/topology → triggers → repeat → assign agents) plus the existing secrets card. ResearchWizard picks up the same repo step and a hard gate when the workspace has zero agents. - New LoopStaffingStep with three tabs — Individual / Team / Organization — that mix freely per loop; selections persist via new loop_agents / loop_teams / loop_orgs join tables (0035 migration), each cascading on loop_id so hard-delete stays a single-row DELETE. - Backend CreateLoopRequest / UpdateLoopRequest accept the three lists and apply_staffing does a transactional replace-all; list_loops / get_loop hydrate the lists via a flattened LoopWithStaffing response. - LoopsList sidebar gains per-row enable/disable, edit (reopens the wizard prefilled with the current loop, PATCHes on submit), and delete with an inline confirm. - NoAgentsGate blocks launching a loop or research topic from a workspace with no roster; the sidebar `+` buttons also disable with a tooltip pointing at the TEAM tier. Not yet wired: the run driver still fills role slots from the workspace-wide pool; teaching enqueue_iteration to prefer loop_agents/loop_teams/loop_orgs is a follow-up.
This commit is contained in:
@@ -15,6 +15,8 @@ import {
|
||||
wizardRefine,
|
||||
type OutcomeKind,
|
||||
} from "@/lib/api/research";
|
||||
import { RepoPicker, type PickedRepo } from "./RepoPicker";
|
||||
import { NoAgentsGate } from "./NoAgentsGate";
|
||||
|
||||
const mono =
|
||||
"ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace";
|
||||
@@ -48,8 +50,9 @@ export function ResearchWizard({
|
||||
onClose: () => void;
|
||||
onCreated: (topicId: string) => void;
|
||||
}) {
|
||||
const [step, setStep] = useState<1 | 2 | 3 | 4>(1);
|
||||
const [step, setStep] = useState<1 | 2 | 3 | 4 | 5>(1);
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [repo, setRepo] = useState<PickedRepo | null>(null);
|
||||
const [outcome, setOutcome] = useState<OutcomeKind>("spec");
|
||||
const [refining, setRefining] = useState(false);
|
||||
const [refineError, setRefineError] = useState<string | null>(null);
|
||||
@@ -67,6 +70,7 @@ export function ResearchWizard({
|
||||
prompt,
|
||||
outcome_kind: outcome,
|
||||
prior_description: prior,
|
||||
...(repo ? { repo } : {}),
|
||||
});
|
||||
setTitle(out.suggested_title);
|
||||
setDescription(out.description);
|
||||
@@ -89,6 +93,7 @@ export function ResearchWizard({
|
||||
agent_id: s.agent_id,
|
||||
role_slot: s.role_slot.trim() || undefined,
|
||||
})),
|
||||
...(repo ? { repo } : {}),
|
||||
});
|
||||
onCreated(id);
|
||||
} catch (e) {
|
||||
@@ -100,9 +105,10 @@ export function ResearchWizard({
|
||||
|
||||
const canNext =
|
||||
(step === 1 && prompt.trim().length > 0) ||
|
||||
(step === 2 && title.trim().length > 0 && description.trim().length > 0) ||
|
||||
step === 3 ||
|
||||
step === 4;
|
||||
step === 2 ||
|
||||
(step === 3 && title.trim().length > 0 && description.trim().length > 0) ||
|
||||
step === 4 ||
|
||||
step === 5;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -143,7 +149,7 @@ export function ResearchWizard({
|
||||
}}
|
||||
>
|
||||
<span style={{ fontFamily: mono, fontSize: 10, color: "#5a5a62" }}>
|
||||
STEP {step} / 4
|
||||
STEP {step} / 5
|
||||
</span>
|
||||
<span style={{ flex: 1, fontSize: 16, fontWeight: 700, color: "#f3f3f5" }}>
|
||||
New research topic
|
||||
@@ -171,7 +177,10 @@ export function ResearchWizard({
|
||||
|
||||
{/* Body */}
|
||||
<div style={{ flex: 1, minHeight: 0, overflow: "auto", padding: 20 }}>
|
||||
{step === 1 && (
|
||||
{agents.length === 0 ? (
|
||||
<NoAgentsGate what="research topic" onDismiss={onClose} />
|
||||
) : null}
|
||||
{agents.length > 0 && step === 1 && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
<label htmlFor="topic-prompt" style={{ fontFamily: mono, fontSize: 11, color: "#b5b5bd" }}>
|
||||
Topic prompt
|
||||
@@ -191,7 +200,18 @@ export function ResearchWizard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
{agents.length > 0 && step === 2 && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
<span style={labelStyle}>Repository (optional)</span>
|
||||
<p style={hintStyle}>
|
||||
Anchor the topic to a connected repo — the refine step will
|
||||
use it as context, and downstream outcomes can cite it.
|
||||
</p>
|
||||
<RepoPicker value={repo} onChange={setRepo} optional />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{agents.length > 0 && step === 3 && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
{!title && !refining ? (
|
||||
<button
|
||||
@@ -247,7 +267,7 @@ export function ResearchWizard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 3 && (
|
||||
{agents.length > 0 && step === 4 && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
<span style={labelStyle}>Outcome kind</span>
|
||||
{OUTCOMES.map((o) => (
|
||||
@@ -279,7 +299,7 @@ export function ResearchWizard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 4 && (
|
||||
{agents.length > 0 && step === 5 && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
<span style={labelStyle}>Assign agents</span>
|
||||
<p style={hintStyle}>Zero or more. Each optional role slot ("lead", "critic") groups avatars on the canvas.</p>
|
||||
@@ -346,6 +366,7 @@ export function ResearchWizard({
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
{agents.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
padding: 14,
|
||||
@@ -357,16 +378,16 @@ export function ResearchWizard({
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep((s) => (s > 1 ? ((s - 1) as 1 | 2 | 3 | 4) : s))}
|
||||
onClick={() => setStep((s) => (s > 1 ? ((s - 1) as 1 | 2 | 3 | 4 | 5) : s))}
|
||||
disabled={step === 1}
|
||||
style={{ ...secondaryBtn, opacity: step === 1 ? 0.4 : 1 }}
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
{step < 4 ? (
|
||||
{step < 5 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep((s) => ((s + 1) as 1 | 2 | 3 | 4))}
|
||||
onClick={() => setStep((s) => ((s + 1) as 1 | 2 | 3 | 4 | 5))}
|
||||
disabled={!canNext || refining}
|
||||
style={{ ...primaryBtn, opacity: !canNext || refining ? 0.4 : 1 }}
|
||||
>
|
||||
@@ -383,6 +404,7 @@ export function ResearchWizard({
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user