loops-wizard: initial_burst + on_artifact_update trigger UI
Surfaces the two new trigger fields shipped in the previous commit so
users can compose the new loop patterns without editing raw jsonb.
Step 4 (Triggers) now has five toggles instead of three:
1. **Run at create-time (initial burst)** — number input, defaults to 1
so every new loop runs at least once. Toggle off = classic behavior
(no auto-fire).
2. Cron schedule (existing)
3. On previous completion (existing) — pairs with initial_burst > 1
for a back-to-back burst chain, and with cron for continuous
iteration between schedule ticks.
4. **On new research artifact** — visible always; auto-disabled with
an explanatory label when no source topic is bound. Coalesced
server-side against in-flight runs so rapid revisions don't spawn
duplicates.
5. Webhook (existing)
Body wiring:
- Sends initial_burst > 0 → { initial_burst: N } in triggers jsonb.
- Sends on_artifact_update only when a source topic is bound (safety
belt matching the backend's fan-out filter).
Step gate:
- Advancing from step 4 now requires ANY one of the five triggers
(was just the three legacy toggles). The initial_burst default of 1
means users can happy-path through this step without deliberate
action, which is the intended nudge — every loop should have at
least one path to run.
Read/write parity:
- readInitialTriggers grows two fields so edit-mode reflects existing
loops' triggers correctly.
Next commit lands ResearchWizard's "When should this run?" step +
optional paired coding loop, and after that the kind='research'
dispatch path.
This commit is contained in:
@@ -103,6 +103,18 @@ export function LoopsWizard({
|
|||||||
const [webhookEnabled, setWebhookEnabled] = useState(
|
const [webhookEnabled, setWebhookEnabled] = useState(
|
||||||
!!initialTriggers.webhook_enabled,
|
!!initialTriggers.webhook_enabled,
|
||||||
);
|
);
|
||||||
|
// NEW — initial_burst: how many iterations to fire back-to-back on
|
||||||
|
// create. Default 1 so every loop runs at least once (D1).
|
||||||
|
const [initialBurst, setInitialBurst] = useState<number>(
|
||||||
|
typeof initialTriggers.initial_burst === "number"
|
||||||
|
? initialTriggers.initial_burst
|
||||||
|
: 1,
|
||||||
|
);
|
||||||
|
// NEW — on_artifact_update: wake up when the source topic gets a new
|
||||||
|
// outcome. Only meaningful when a source_research_topic is bound.
|
||||||
|
const [onArtifactUpdate, setOnArtifactUpdate] = useState<boolean>(
|
||||||
|
!!initialTriggers.on_artifact_update,
|
||||||
|
);
|
||||||
const [repeatKind, setRepeatKind] = useState<"infinite" | "iters" | "until">(
|
const [repeatKind, setRepeatKind] = useState<"infinite" | "iters" | "until">(
|
||||||
initialRepeat.kind,
|
initialRepeat.kind,
|
||||||
);
|
);
|
||||||
@@ -211,7 +223,12 @@ export function LoopsWizard({
|
|||||||
(step === 1 && title.trim().length > 0) ||
|
(step === 1 && title.trim().length > 0) ||
|
||||||
step === 2 ||
|
step === 2 ||
|
||||||
(step === 3 && task.trim().length > 0 && graphReady) ||
|
(step === 3 && task.trim().length > 0 && graphReady) ||
|
||||||
(step === 4 && (cronEnabled || onCompletion || webhookEnabled)) ||
|
(step === 4 &&
|
||||||
|
(cronEnabled ||
|
||||||
|
onCompletion ||
|
||||||
|
webhookEnabled ||
|
||||||
|
initialBurst > 0 ||
|
||||||
|
(onArtifactUpdate && !!sourceTopicId))) ||
|
||||||
step === 5 ||
|
step === 5 ||
|
||||||
step === 6;
|
step === 6;
|
||||||
|
|
||||||
@@ -262,6 +279,10 @@ export function LoopsWizard({
|
|||||||
...(cronEnabled ? { cron: cron.trim() } : {}),
|
...(cronEnabled ? { cron: cron.trim() } : {}),
|
||||||
...(onCompletion ? { on_completion: true } : {}),
|
...(onCompletion ? { on_completion: true } : {}),
|
||||||
...(webhookEnabled ? { webhook_enabled: true } : {}),
|
...(webhookEnabled ? { webhook_enabled: true } : {}),
|
||||||
|
...(initialBurst > 0 ? { initial_burst: initialBurst } : {}),
|
||||||
|
...(onArtifactUpdate && sourceTopicId
|
||||||
|
? { on_artifact_update: true }
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
repeat_policy,
|
repeat_policy,
|
||||||
...(repo ? { repo } : {}),
|
...(repo ? { repo } : {}),
|
||||||
@@ -481,6 +502,28 @@ export function LoopsWizard({
|
|||||||
{!noAgents && step === 4 && (
|
{!noAgents && 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
|
||||||
|
on={initialBurst > 0}
|
||||||
|
onToggle={(on) => setInitialBurst(on ? Math.max(1, initialBurst) : 0)}
|
||||||
|
label="Run at create-time (initial burst)"
|
||||||
|
hint="Fire N iterations back-to-back when the loop is created. Default 1 = runs at least once."
|
||||||
|
>
|
||||||
|
{initialBurst > 0 && (
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={100}
|
||||||
|
value={initialBurst}
|
||||||
|
onChange={(e) => setInitialBurst(Math.max(1, Number(e.target.value) || 1))}
|
||||||
|
style={{ ...fieldStyle, width: 90, fontFamily: mono }}
|
||||||
|
/>
|
||||||
|
<span style={{ fontFamily: mono, fontSize: 11, color: "#8a8a92" }}>
|
||||||
|
iteration{initialBurst === 1 ? "" : "s"} back-to-back
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TriggerToggle>
|
||||||
<TriggerToggle
|
<TriggerToggle
|
||||||
on={cronEnabled}
|
on={cronEnabled}
|
||||||
onToggle={setCronEnabled}
|
onToggle={setCronEnabled}
|
||||||
@@ -502,6 +545,16 @@ export function LoopsWizard({
|
|||||||
label="On previous completion"
|
label="On previous completion"
|
||||||
hint="Enqueue the next iteration as soon as the previous one emits run_completed."
|
hint="Enqueue the next iteration as soon as the previous one emits run_completed."
|
||||||
/>
|
/>
|
||||||
|
<TriggerToggle
|
||||||
|
on={onArtifactUpdate && !!sourceTopicId}
|
||||||
|
onToggle={(on) => setOnArtifactUpdate(on)}
|
||||||
|
label={
|
||||||
|
sourceTopicId
|
||||||
|
? "On new research artifact"
|
||||||
|
: "On new research artifact (bind a source topic first)"
|
||||||
|
}
|
||||||
|
hint="Wake up whenever the bound research topic writes a fresh outcome version. Coalesced with any in-flight run."
|
||||||
|
/>
|
||||||
<TriggerToggle
|
<TriggerToggle
|
||||||
on={webhookEnabled}
|
on={webhookEnabled}
|
||||||
onToggle={setWebhookEnabled}
|
onToggle={setWebhookEnabled}
|
||||||
@@ -1041,6 +1094,8 @@ function readInitialTriggers(t: unknown): {
|
|||||||
cron?: string;
|
cron?: string;
|
||||||
on_completion?: boolean;
|
on_completion?: boolean;
|
||||||
webhook_enabled?: boolean;
|
webhook_enabled?: boolean;
|
||||||
|
initial_burst?: number;
|
||||||
|
on_artifact_update?: boolean;
|
||||||
} {
|
} {
|
||||||
if (!t || typeof t !== "object") return {};
|
if (!t || typeof t !== "object") return {};
|
||||||
const o = t as Record<string, unknown>;
|
const o = t as Record<string, unknown>;
|
||||||
@@ -1048,6 +1103,9 @@ function readInitialTriggers(t: unknown): {
|
|||||||
cron: typeof o.cron === "string" ? o.cron : undefined,
|
cron: typeof o.cron === "string" ? o.cron : undefined,
|
||||||
on_completion: !!o.on_completion,
|
on_completion: !!o.on_completion,
|
||||||
webhook_enabled: !!o.webhook_enabled,
|
webhook_enabled: !!o.webhook_enabled,
|
||||||
|
initial_burst:
|
||||||
|
typeof o.initial_burst === "number" ? o.initial_burst : undefined,
|
||||||
|
on_artifact_update: !!o.on_artifact_update,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user