From 6dac91eca45fd6df17ec6d11ead89aa046443d3a Mon Sep 17 00:00:00 2001 From: Nadia Nujovich Date: Thu, 18 Jun 2026 14:56:03 +0200 Subject: [PATCH] feat(cron): expose per-job Model field in Create/Edit Job modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cron scheduler already resolves a per-job model override (cron/scheduler.py: model = job.get("model") or env or default), but the dashboard never exposed it, so users had to hand-edit ~/.hermes/cron/jobs.json — which the gateway can overwrite on the next tick. This adds a free-text "Model (optional)" input to the Create and Edit Job modals, wired into the existing create/update payload under the model key (English-only string via the existing optional-i18n-key + fallback pattern; other locales fill in later). The Edit/update path already accepted model end-to-end (CronJobUpdate forwards the full updates dict). The Create path did NOT: the POST handler whitelisted fields and dropped model, and CronJobCreate had no such field — so a frontend-only change would have been silently discarded on create. Added the minimal forward: model on CronJobCreate plus passing it through to create_job(). Draft until maintainer direction on UX: free text vs dropdown vs autocomplete. --- hermes_cli/web_server.py | 2 ++ web/src/i18n/en.ts | 2 ++ web/src/i18n/types.ts | 2 ++ web/src/lib/api.ts | 5 +++-- web/src/pages/CronPage.tsx | 36 ++++++++++++++++++++++++++++++++++++ 5 files changed, 45 insertions(+), 2 deletions(-) diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index ed619979bfb7..df710fd2b046 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -7046,6 +7046,7 @@ class CronJobCreate(BaseModel): name: str = "" deliver: str = "local" skills: Optional[List[str]] = None + model: str = "" class CronJobUpdate(BaseModel): @@ -7218,6 +7219,7 @@ async def create_cron_job(body: CronJobCreate, profile: str = "default"): name=body.name, deliver=body.deliver, skills=body.skills, + model=body.model, ) except Exception as e: _log.exception("POST /api/cron/jobs failed") diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 10fd8df43005..d9f9bc668cf7 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -230,6 +230,8 @@ export const en: Translations = { newJob: "New Cron Job", nameOptional: "Name (optional)", namePlaceholder: "e.g. Daily summary", + modelOptional: "Model (optional)", + modelPlaceholder: "e.g. moonshotai/kimi-k2.6 (leaves the gateway default if empty)", prompt: "Prompt", promptPlaceholder: "What should the agent do on each run?", schedule: "Schedule (cron expression)", diff --git a/web/src/i18n/types.ts b/web/src/i18n/types.ts index 68a5c5693772..c902b9654147 100644 --- a/web/src/i18n/types.ts +++ b/web/src/i18n/types.ts @@ -249,6 +249,8 @@ export interface Translations { newJob: string; nameOptional: string; namePlaceholder: string; + modelOptional?: string; + modelPlaceholder?: string; prompt: string; promptPlaceholder: string; schedule: string; diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 9b5b7afc835f..2433bba1378a 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -509,7 +509,7 @@ export const api = { fetchJSON(`/api/cron/jobs?profile=${encodeURIComponent(profile)}`), getCronDeliveryTargets: () => fetchJSON<{ targets: CronDeliveryTarget[] }>("/api/cron/delivery-targets"), - createCronJob: (job: { prompt: string; schedule: string; name?: string; deliver?: string; skills?: string[] }, profile = "default") => + createCronJob: (job: { prompt: string; schedule: string; name?: string; deliver?: string; skills?: string[]; model?: string }, profile = "default") => fetchJSON(`/api/cron/jobs?profile=${encodeURIComponent(profile)}`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -519,7 +519,7 @@ export const api = { fetchJSON(`/api/cron/jobs/${encodeURIComponent(id)}/pause?profile=${encodeURIComponent(profile)}`, { method: "POST" }), updateCronJob: ( id: string, - updates: { prompt?: string; schedule?: string; name?: string; deliver?: string; skills?: string[] }, + updates: { prompt?: string; schedule?: string; name?: string; deliver?: string; skills?: string[]; model?: string | null }, profile = "default", ) => fetchJSON( @@ -1878,6 +1878,7 @@ export interface CronJob { enabled: boolean; state?: string | null; deliver?: string | null; + model?: string | null; last_run_at?: string | null; next_run_at?: string | null; last_error?: string | null; diff --git a/web/src/pages/CronPage.tsx b/web/src/pages/CronPage.tsx index d69af7cbaf88..cf9d9db2d4e4 100644 --- a/web/src/pages/CronPage.tsx +++ b/web/src/pages/CronPage.tsx @@ -211,6 +211,7 @@ export default function CronPage() { DEFAULT_SCHEDULE_STATE, ); const [name, setName] = useState(""); + const [model, setModel] = useState(""); const closeCreateModal = useCallback(() => setCreateModalOpen(false), []); const createModalRef = useModalBehavior({ open: createModalOpen, @@ -229,6 +230,7 @@ export default function CronPage() { const [editPrompt, setEditPrompt] = useState(""); const [editSchedule, setEditSchedule] = useState(""); const [editName, setEditName] = useState(""); + const [editModel, setEditModel] = useState(""); const [editDeliver, setEditDeliver] = useState("local"); const [editSkills, setEditSkills] = useState([]); const [saving, setSaving] = useState(false); @@ -251,6 +253,7 @@ export default function CronPage() { asText(job.schedule?.expr) || asText(job.schedule_display) || "", ); setEditName(getJobName(job)); + setEditModel(asText(job.model)); setEditDeliver(asText(job.deliver) || "local"); setEditSkills(Array.isArray(job.skills) ? job.skills.filter(Boolean) : []); }, []); @@ -373,6 +376,7 @@ export default function CronPage() { name: name.trim() || undefined, deliver, skills: jobSkills.length > 0 ? jobSkills : undefined, + model: model.trim() || undefined, }, createProfile, ); @@ -380,6 +384,7 @@ export default function CronPage() { setPrompt(""); setScheduleState(DEFAULT_SCHEDULE_STATE); setName(""); + setModel(""); setDeliver("local"); setJobSkills([]); setCreateModalOpen(false); @@ -407,6 +412,7 @@ export default function CronPage() { name: editName.trim(), deliver: editDeliver, skills: editSkills, + model: editModel.trim() || null, }, getJobProfile(editJob), ); @@ -610,6 +616,21 @@ export default function CronPage() { /> +
+ + setModel(e.target.value)} + /> +
+ +
+ + setEditModel(e.target.value)} + /> +
+