mirror of
https://github.com/0xWheatyz/handler.git
synced 2026-08-30 04:26:24 +00:00
Extend the model picker to schedules
schedules.model_id (migration 0013) pins every fired run of a recurring spawn to a registered model backend, exactly like a hand-spawned agent: the worker copies it into each firing's spawn payload, the launched agent records the pin, and resumes stay on the same backend. The Schedules form gets the same Model dropdown as the spawn form (Claude subscription by default), with a badge in the schedules table. Create/update routes fail fast on a missing or disabled backend so a stale selection bounces immediately instead of every firing failing asynchronously in Activity; a backend deleted later still fails each firing visibly rather than silently falling back to the subscription. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DzDofD7gP63WpeLG8vEdZu
This commit is contained in:
@@ -36,7 +36,7 @@ function intervalLabel(seconds: number): string {
|
||||
return `every ${seconds}s`;
|
||||
}
|
||||
|
||||
const emptyForm = { name_prefix: "", task: "", interval: "3600", role: "" };
|
||||
const emptyForm = { name_prefix: "", task: "", interval: "3600", role: "", model_id: "" };
|
||||
|
||||
export function SchedulesSection() {
|
||||
const s = useDashboard();
|
||||
@@ -46,6 +46,21 @@ export function SchedulesSection() {
|
||||
() => s.projects.map((p) => ({ value: p.id, label: p.id })),
|
||||
[s.projects],
|
||||
);
|
||||
/* Same choice the spawn form offers: Claude subscription by default, plus every
|
||||
* enabled backend from the Claude page's Models tab. Every fired run spawns on it. */
|
||||
const modelOpts = useMemo(
|
||||
() => [
|
||||
{ value: "", label: "Claude (subscription)" },
|
||||
...s.claudeModels
|
||||
.filter((m) => m.enabled)
|
||||
.map((m) => ({ value: String(m.id), label: `${m.name} (${m.model})` })),
|
||||
],
|
||||
[s.claudeModels],
|
||||
);
|
||||
const modelName = useMemo(() => {
|
||||
const byId = new Map(s.claudeModels.map((m) => [m.id, m.name]));
|
||||
return (id: number | null | undefined) => (id == null ? null : byId.get(id) ?? `#${id}`);
|
||||
}, [s.claudeModels]);
|
||||
|
||||
const create = async () => {
|
||||
const ok = await s.createSchedule(s.selectedProjectId, {
|
||||
@@ -53,6 +68,7 @@ export function SchedulesSection() {
|
||||
task: form.task,
|
||||
interval_seconds: Number(form.interval),
|
||||
role: form.role,
|
||||
model_id: form.model_id,
|
||||
});
|
||||
if (ok) setForm(emptyForm);
|
||||
};
|
||||
@@ -107,6 +123,12 @@ export function SchedulesSection() {
|
||||
onChange={(v) => setForm({ ...form, role: v })}
|
||||
options={ROLE_OPTS}
|
||||
/>
|
||||
<Select
|
||||
label="Model"
|
||||
value={form.model_id}
|
||||
onChange={(v) => setForm({ ...form, model_id: v })}
|
||||
options={modelOpts}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt14">
|
||||
<Textarea
|
||||
@@ -167,6 +189,12 @@ export function SchedulesSection() {
|
||||
<Badge tone="info">{sc.role}</Badge>
|
||||
</>
|
||||
) : null}
|
||||
{sc.model_id != null ? (
|
||||
<>
|
||||
{" "}
|
||||
<Badge tone="warning">{modelName(sc.model_id)}</Badge>
|
||||
</>
|
||||
) : null}
|
||||
</td>
|
||||
<td className="mono faint">{sc.project_id}</td>
|
||||
<td className="nowrap">{intervalLabel(sc.interval_seconds)}</td>
|
||||
|
||||
@@ -231,6 +231,8 @@ export interface ScheduleBody {
|
||||
task: string;
|
||||
interval_seconds: number;
|
||||
role: string;
|
||||
/* Model backend id as a select value; "" = the Claude subscription. */
|
||||
model_id: string;
|
||||
}
|
||||
export interface ApprovalBody {
|
||||
branch: string;
|
||||
@@ -496,7 +498,8 @@ export function DashboardProvider({
|
||||
const s = sectionRef.current;
|
||||
const run = selectedRunRef.current;
|
||||
if (run) await loadRun(run.projectId, run.name);
|
||||
if (s === "agents") await loadClaudeModels(); // the spawn form's model dropdown
|
||||
// The spawn/schedule forms' model dropdowns.
|
||||
if (s === "agents" || s === "schedules") await loadClaudeModels();
|
||||
if (s === "approvals") await loadApprovals(selectedProjectRef.current);
|
||||
if (s === "servers") await loadHosts();
|
||||
if (s === "activity") await loadCommands();
|
||||
@@ -527,7 +530,7 @@ export function DashboardProvider({
|
||||
(s: Section) => {
|
||||
setSectionRaw(s);
|
||||
setCmd({ text: "", error: false, busy: false });
|
||||
if (s === "agents") void loadClaudeModels();
|
||||
if (s === "agents" || s === "schedules") void loadClaudeModels();
|
||||
if (s === "approvals") void loadApprovals(selectedProjectRef.current);
|
||||
if (s === "servers") void loadHosts();
|
||||
if (s === "activity") void loadCommands();
|
||||
@@ -894,6 +897,7 @@ export function DashboardProvider({
|
||||
task: b.task.trim(),
|
||||
interval_seconds: b.interval_seconds,
|
||||
role: b.role || null,
|
||||
model_id: b.model_id ? Number(b.model_id) : null,
|
||||
},
|
||||
});
|
||||
setCmd({
|
||||
|
||||
Reference in New Issue
Block a user