From 254bb7ceeef071c24be8432ed06133288d9dc316 Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Mon, 2 Mar 2026 07:24:33 -0600 Subject: [PATCH] ui(cron): add advanced controls for run-if-due and routing (#31244) * ui(cron): add advanced run controls and routing fields * ui(cron): gate delivery account id to announce mode * ui(cron): allow clearing delivery account id in editor * cron: persist payload lightContext updates * tests(cron): fix payload lightContext assertion typing --- src/cron/service.jobs.test.ts | 47 ++++++ src/cron/service/jobs.ts | 4 + ui/src/ui/app-defaults.ts | 3 + ui/src/ui/app-render.ts | 4 +- ui/src/ui/controllers/cron.test.ts | 234 ++++++++++++++++++++++++++++- ui/src/ui/controllers/cron.ts | 31 +++- ui/src/ui/types.ts | 3 + ui/src/ui/ui-types.ts | 3 + ui/src/ui/views/cron.test.ts | 34 ++++- ui/src/ui/views/cron.ts | 62 +++++++- 10 files changed, 418 insertions(+), 7 deletions(-) diff --git a/src/cron/service.jobs.test.ts b/src/cron/service.jobs.test.ts index 18eef9240..4d0819ab9 100644 --- a/src/cron/service.jobs.test.ts +++ b/src/cron/service.jobs.test.ts @@ -137,6 +137,53 @@ describe("applyJobPatch", () => { expect(job.delivery?.accountId).toBeUndefined(); }); + it("persists agentTurn payload.lightContext updates when editing existing jobs", () => { + const job = createIsolatedAgentTurnJob("job-light-context", { + mode: "announce", + channel: "telegram", + }); + job.payload = { + kind: "agentTurn", + message: "do it", + lightContext: true, + }; + + applyJobPatch(job, { + payload: { + kind: "agentTurn", + message: "do it", + lightContext: false, + }, + }); + + expect(job.payload.kind).toBe("agentTurn"); + if (job.payload.kind === "agentTurn") { + expect(job.payload.lightContext).toBe(false); + } + }); + + it("applies payload.lightContext when replacing payload kind via patch", () => { + const job = createIsolatedAgentTurnJob("job-light-context-switch", { + mode: "announce", + channel: "telegram", + }); + job.payload = { kind: "systemEvent", text: "ping" }; + + applyJobPatch(job, { + payload: { + kind: "agentTurn", + message: "do it", + lightContext: true, + }, + }); + + const payload = job.payload as CronJob["payload"]; + expect(payload.kind).toBe("agentTurn"); + if (payload.kind === "agentTurn") { + expect(payload.lightContext).toBe(true); + } + }); + it("rejects webhook delivery without a valid http(s) target URL", () => { const expectedError = "cron webhook delivery requires delivery.to to be a valid http(s) URL"; const cases = [ diff --git a/src/cron/service/jobs.ts b/src/cron/service/jobs.ts index 3808be03e..8c602cd62 100644 --- a/src/cron/service/jobs.ts +++ b/src/cron/service/jobs.ts @@ -564,6 +564,9 @@ function mergeCronPayload(existing: CronPayload, patch: CronPayloadPatch): CronP if (typeof patch.timeoutSeconds === "number") { next.timeoutSeconds = patch.timeoutSeconds; } + if (typeof patch.lightContext === "boolean") { + next.lightContext = patch.lightContext; + } if (typeof patch.allowUnsafeExternalContent === "boolean") { next.allowUnsafeExternalContent = patch.allowUnsafeExternalContent; } @@ -641,6 +644,7 @@ function buildPayloadFromPatch(patch: CronPayloadPatch): CronPayload { model: patch.model, thinking: patch.thinking, timeoutSeconds: patch.timeoutSeconds, + lightContext: patch.lightContext, allowUnsafeExternalContent: patch.allowUnsafeExternalContent, deliver: patch.deliver, channel: patch.channel, diff --git a/ui/src/ui/app-defaults.ts b/ui/src/ui/app-defaults.ts index b3661b18e..451e95b4e 100644 --- a/ui/src/ui/app-defaults.ts +++ b/ui/src/ui/app-defaults.ts @@ -14,6 +14,7 @@ export const DEFAULT_CRON_FORM: CronFormState = { name: "", description: "", agentId: "", + sessionKey: "", clearAgent: false, enabled: true, deleteAfterRun: true, @@ -32,9 +33,11 @@ export const DEFAULT_CRON_FORM: CronFormState = { payloadText: "", payloadModel: "", payloadThinking: "", + payloadLightContext: false, deliveryMode: "announce", deliveryChannel: "last", deliveryTo: "", + deliveryAccountId: "", deliveryBestEffort: false, failureAlertMode: "inherit", failureAlertAfter: "2", diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index e7958ea3b..7bf0665de 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -214,6 +214,7 @@ export function renderApp(state: AppViewState) { ...jobToSuggestions, ...accountToSuggestions, ]); + const accountSuggestions = uniquePreserveOrder(accountToSuggestions); const deliveryToSuggestions = state.cronForm.deliveryMode === "webhook" ? rawDeliveryToSuggestions.filter((value) => isHttpUrl(value)) @@ -482,6 +483,7 @@ export function renderApp(state: AppViewState) { thinkingSuggestions: CRON_THINKING_SUGGESTIONS, timezoneSuggestions: CRON_TIMEZONE_SUGGESTIONS, deliveryToSuggestions, + accountSuggestions, onFormChange: (patch) => { state.cronForm = normalizeCronFormState({ ...state.cronForm, ...patch }); state.cronFieldErrors = validateCronForm(state.cronForm); @@ -492,7 +494,7 @@ export function renderApp(state: AppViewState) { onClone: (job) => startCronClone(state, job), onCancelEdit: () => cancelCronEdit(state), onToggle: (job, enabled) => toggleCronJob(state, job, enabled), - onRun: (job) => runCronJob(state, job), + onRun: (job, mode) => runCronJob(state, job, mode ?? "force"), onRemove: (job) => removeCronJob(state, job), onLoadRuns: async (jobId) => { updateCronRunsFilter(state, { cronRunsScope: "job" }); diff --git a/ui/src/ui/controllers/cron.test.ts b/ui/src/ui/controllers/cron.test.ts index 50bdf5811..d3f6b7157 100644 --- a/ui/src/ui/controllers/cron.test.ts +++ b/ui/src/ui/controllers/cron.test.ts @@ -7,6 +7,7 @@ import { loadCronRuns, loadMoreCronRuns, normalizeCronFormState, + runCronJob, startCronEdit, startCronClone, validateCronForm, @@ -119,6 +120,83 @@ describe("cron controller", () => { }); }); + it("forwards sessionKey and delivery accountId in cron.add payload", async () => { + const request = vi.fn(async (method: string, _payload?: unknown) => { + if (method === "cron.add") { + return { id: "job-3" }; + } + if (method === "cron.list") { + return { jobs: [] }; + } + if (method === "cron.status") { + return { enabled: true, jobs: 0, nextWakeAtMs: null }; + } + return {}; + }); + + const state = createState({ + client: { request } as unknown as CronState["client"], + cronForm: { + ...DEFAULT_CRON_FORM, + name: "account-routed", + scheduleKind: "cron", + cronExpr: "0 * * * *", + sessionTarget: "isolated", + payloadKind: "agentTurn", + payloadText: "run this", + sessionKey: "agent:ops:main", + deliveryMode: "announce", + deliveryAccountId: "ops-bot", + }, + }); + + await addCronJob(state); + + const addCall = request.mock.calls.find(([method]) => method === "cron.add"); + expect(addCall).toBeDefined(); + expect(addCall?.[1]).toMatchObject({ + sessionKey: "agent:ops:main", + delivery: { mode: "announce", accountId: "ops-bot" }, + }); + }); + + it("forwards lightContext in cron payload", async () => { + const request = vi.fn(async (method: string, _payload?: unknown) => { + if (method === "cron.add") { + return { id: "job-light" }; + } + if (method === "cron.list") { + return { jobs: [] }; + } + if (method === "cron.status") { + return { enabled: true, jobs: 0, nextWakeAtMs: null }; + } + return {}; + }); + + const state = createState({ + client: { request } as unknown as CronState["client"], + cronForm: { + ...DEFAULT_CRON_FORM, + name: "light-context job", + scheduleKind: "cron", + cronExpr: "0 * * * *", + sessionTarget: "isolated", + payloadKind: "agentTurn", + payloadText: "run this", + payloadLightContext: true, + }, + }); + + await addCronJob(state); + + const addCall = request.mock.calls.find(([method]) => method === "cron.add"); + expect(addCall).toBeDefined(); + expect(addCall?.[1]).toMatchObject({ + payload: { kind: "agentTurn", lightContext: true }, + }); + }); + it('sends delivery: { mode: "none" } explicitly in cron.add payload', async () => { const request = vi.fn(async (method: string, _payload?: unknown) => { if (method === "cron.add") { @@ -306,12 +384,74 @@ describe("cron controller", () => { expect(state.cronEditingJobId).toBeNull(); }); + it("sends empty delivery.accountId in cron.update to clear persisted account routing", async () => { + const request = vi.fn(async (method: string, _payload?: unknown) => { + if (method === "cron.update") { + return { id: "job-clear-account-id" }; + } + if (method === "cron.list") { + return { jobs: [{ id: "job-clear-account-id" }] }; + } + if (method === "cron.status") { + return { enabled: true, jobs: 1, nextWakeAtMs: null }; + } + return {}; + }); + + const state = createState({ + client: { request } as unknown as CronState["client"], + cronEditingJobId: "job-clear-account-id", + cronJobs: [ + { + id: "job-clear-account-id", + name: "clear account", + enabled: true, + createdAtMs: 0, + updatedAtMs: 0, + schedule: { kind: "cron", expr: "0 * * * *" }, + sessionTarget: "isolated", + wakeMode: "next-heartbeat", + payload: { kind: "agentTurn", message: "run" }, + delivery: { mode: "announce", accountId: "ops-bot" }, + state: {}, + }, + ], + cronForm: { + ...DEFAULT_CRON_FORM, + name: "clear account", + scheduleKind: "cron", + cronExpr: "0 * * * *", + sessionTarget: "isolated", + wakeMode: "next-heartbeat", + payloadKind: "agentTurn", + payloadText: "run", + deliveryMode: "announce", + deliveryAccountId: " ", + }, + }); + + await addCronJob(state); + + const updateCall = request.mock.calls.find(([method]) => method === "cron.update"); + expect(updateCall).toBeDefined(); + expect(updateCall?.[1]).toMatchObject({ + id: "job-clear-account-id", + patch: { + delivery: { + mode: "announce", + accountId: "", + }, + }, + }); + }); + it("maps a cron job into editable form fields", () => { const state = createState(); const job = { id: "job-9", name: "Weekly report", description: "desc", + sessionKey: "agent:ops:main", enabled: false, createdAtMs: 0, updatedAtMs: 0, @@ -319,7 +459,7 @@ describe("cron controller", () => { sessionTarget: "isolated" as const, wakeMode: "next-heartbeat" as const, payload: { kind: "agentTurn" as const, message: "ship it", timeoutSeconds: 45 }, - delivery: { mode: "announce" as const, channel: "telegram", to: "123" }, + delivery: { mode: "announce" as const, channel: "telegram", to: "123", accountId: "bot-2" }, state: {}, }; @@ -328,6 +468,7 @@ describe("cron controller", () => { expect(state.cronEditingJobId).toBe("job-9"); expect(state.cronRunsJobId).toBe("job-9"); expect(state.cronForm.name).toBe("Weekly report"); + expect(state.cronForm.sessionKey).toBe("agent:ops:main"); expect(state.cronForm.enabled).toBe(false); expect(state.cronForm.scheduleKind).toBe("every"); expect(state.cronForm.everyAmount).toBe("2"); @@ -338,6 +479,7 @@ describe("cron controller", () => { expect(state.cronForm.deliveryMode).toBe("announce"); expect(state.cronForm.deliveryChannel).toBe("telegram"); expect(state.cronForm.deliveryTo).toBe("123"); + expect(state.cronForm.deliveryAccountId).toBe("bot-2"); }); it("includes model/thinking/stagger/bestEffort in cron.update patch", async () => { @@ -391,6 +533,62 @@ describe("cron controller", () => { }); }); + it("sends lightContext=false in cron.update when clearing prior light-context setting", async () => { + const request = vi.fn(async (method: string, _payload?: unknown) => { + if (method === "cron.update") { + return { id: "job-clear-light" }; + } + if (method === "cron.list") { + return { jobs: [{ id: "job-clear-light" }] }; + } + if (method === "cron.status") { + return { enabled: true, jobs: 1, nextWakeAtMs: null }; + } + return {}; + }); + const state = createState({ + client: { request } as unknown as CronState["client"], + cronEditingJobId: "job-clear-light", + cronJobs: [ + { + id: "job-clear-light", + name: "Light job", + enabled: true, + createdAtMs: 0, + updatedAtMs: 0, + schedule: { kind: "cron", expr: "0 9 * * *" }, + sessionTarget: "isolated", + wakeMode: "now", + payload: { kind: "agentTurn", message: "run", lightContext: true }, + state: {}, + }, + ], + cronForm: { + ...DEFAULT_CRON_FORM, + name: "Light job", + scheduleKind: "cron", + cronExpr: "0 9 * * *", + payloadKind: "agentTurn", + payloadText: "run", + payloadLightContext: false, + }, + }); + + await addCronJob(state); + + const updateCall = request.mock.calls.find(([method]) => method === "cron.update"); + expect(updateCall).toBeDefined(); + expect(updateCall?.[1]).toMatchObject({ + id: "job-clear-light", + patch: { + payload: { + kind: "agentTurn", + lightContext: false, + }, + }, + }); + }); + it("includes custom failureAlert fields in cron.update patch", async () => { const request = vi.fn(async (method: string, _payload?: unknown) => { if (method === "cron.update") { @@ -787,4 +985,38 @@ describe("cron controller", () => { expect(state.cronRuns[0]?.summary).toBe("newest"); expect(state.cronRuns[1]?.summary).toBe("older"); }); + + it("runs cron job in due mode when requested", async () => { + const request = vi.fn(async (method: string, payload?: unknown) => { + if (method === "cron.run") { + expect(payload).toMatchObject({ id: "job-due", mode: "due" }); + return { ok: true }; + } + if (method === "cron.runs") { + return { entries: [], total: 0, hasMore: false, nextOffset: null }; + } + return {}; + }); + const state = createState({ + client: { request } as unknown as CronState["client"], + cronRunsScope: "job", + cronRunsJobId: "job-due", + }); + const job = { + id: "job-due", + name: "Due test", + enabled: true, + createdAtMs: 0, + updatedAtMs: 0, + schedule: { kind: "cron" as const, expr: "0 * * * *" }, + sessionTarget: "isolated" as const, + wakeMode: "now" as const, + payload: { kind: "agentTurn" as const, message: "run" }, + state: {}, + }; + + await runCronJob(state, job, "due"); + + expect(request).toHaveBeenCalledWith("cron.run", { id: "job-due", mode: "due" }); + }); }); diff --git a/ui/src/ui/controllers/cron.ts b/ui/src/ui/controllers/cron.ts index 79417fbfe..151f62a68 100644 --- a/ui/src/ui/controllers/cron.ts +++ b/ui/src/ui/controllers/cron.ts @@ -434,6 +434,7 @@ function jobToForm(job: CronJob, prev: CronFormState): CronFormState { name: job.name, description: job.description ?? "", agentId: job.agentId ?? "", + sessionKey: job.sessionKey ?? "", clearAgent: false, enabled: job.enabled, deleteAfterRun: job.deleteAfterRun ?? false, @@ -452,9 +453,12 @@ function jobToForm(job: CronJob, prev: CronFormState): CronFormState { payloadText: job.payload.kind === "systemEvent" ? job.payload.text : job.payload.message, payloadModel: job.payload.kind === "agentTurn" ? (job.payload.model ?? "") : "", payloadThinking: job.payload.kind === "agentTurn" ? (job.payload.thinking ?? "") : "", + payloadLightContext: + job.payload.kind === "agentTurn" ? job.payload.lightContext === true : false, deliveryMode: job.delivery?.mode ?? "none", deliveryChannel: job.delivery?.channel ?? CRON_CHANNEL_LAST, deliveryTo: job.delivery?.to ?? "", + deliveryAccountId: job.delivery?.accountId ?? "", deliveryBestEffort: job.delivery?.bestEffort ?? false, failureAlertMode: failureAlert === false @@ -555,6 +559,7 @@ export function buildCronPayload(form: CronFormState) { model?: string; thinking?: string; timeoutSeconds?: number; + lightContext?: boolean; } = { kind: "agentTurn", message }; const model = form.payloadModel.trim(); if (model) { @@ -568,6 +573,9 @@ export function buildCronPayload(form: CronFormState) { if (timeoutSeconds > 0) { payload.timeoutSeconds = timeoutSeconds; } + if (form.payloadLightContext) { + payload.lightContext = true; + } return payload; } @@ -612,6 +620,20 @@ export async function addCronJob(state: CronState) { const schedule = buildCronSchedule(form); const payload = buildCronPayload(form); + const editingJob = state.cronEditingJobId + ? state.cronJobs.find((job) => job.id === state.cronEditingJobId) + : undefined; + if (payload.kind === "agentTurn") { + const existingLightContext = + editingJob?.payload.kind === "agentTurn" ? editingJob.payload.lightContext : undefined; + if ( + !form.payloadLightContext && + state.cronEditingJobId && + existingLightContext !== undefined + ) { + payload.lightContext = false; + } + } const selectedDeliveryMode = form.deliveryMode; const delivery = selectedDeliveryMode && selectedDeliveryMode !== "none" @@ -622,6 +644,8 @@ export async function addCronJob(state: CronState) { ? form.deliveryChannel.trim() || "last" : undefined, to: form.deliveryTo.trim() || undefined, + accountId: + selectedDeliveryMode === "announce" ? form.deliveryAccountId.trim() : undefined, bestEffort: form.deliveryBestEffort, } : selectedDeliveryMode === "none" @@ -629,10 +653,13 @@ export async function addCronJob(state: CronState) { : undefined; const failureAlert = buildFailureAlert(form); const agentId = form.clearAgent ? null : form.agentId.trim(); + const sessionKeyRaw = form.sessionKey.trim(); + const sessionKey = sessionKeyRaw || (editingJob?.sessionKey ? null : undefined); const job = { name: form.name.trim(), description: form.description.trim(), agentId: agentId === null ? null : agentId || undefined, + sessionKey, enabled: form.enabled, deleteAfterRun: form.deleteAfterRun, schedule, @@ -681,14 +708,14 @@ export async function toggleCronJob(state: CronState, job: CronJob, enabled: boo } } -export async function runCronJob(state: CronState, job: CronJob) { +export async function runCronJob(state: CronState, job: CronJob, mode: "force" | "due" = "force") { if (!state.client || !state.connected || state.cronBusy) { return; } state.cronBusy = true; state.cronError = null; try { - await state.client.request("cron.run", { id: job.id, mode: "force" }); + await state.client.request("cron.run", { id: job.id, mode }); if (state.cronRunsScope === "all") { await loadCronRuns(state, null); } else { diff --git a/ui/src/ui/types.ts b/ui/src/ui/types.ts index 23b34bde6..0432efa93 100644 --- a/ui/src/ui/types.ts +++ b/ui/src/ui/types.ts @@ -482,12 +482,14 @@ export type CronPayload = model?: string; thinking?: string; timeoutSeconds?: number; + lightContext?: boolean; }; export type CronDelivery = { mode: "none" | "announce" | "webhook"; channel?: string; to?: string; + accountId?: string; bestEffort?: boolean; }; @@ -511,6 +513,7 @@ export type CronJobState = { export type CronJob = { id: string; agentId?: string; + sessionKey?: string; name: string; description?: string; enabled: boolean; diff --git a/ui/src/ui/ui-types.ts b/ui/src/ui/ui-types.ts index c179bdea1..2b837067e 100644 --- a/ui/src/ui/ui-types.ts +++ b/ui/src/ui/ui-types.ts @@ -18,6 +18,7 @@ export type CronFormState = { name: string; description: string; agentId: string; + sessionKey: string; clearAgent: boolean; enabled: boolean; deleteAfterRun: boolean; @@ -36,9 +37,11 @@ export type CronFormState = { payloadText: string; payloadModel: string; payloadThinking: string; + payloadLightContext: boolean; deliveryMode: "none" | "announce" | "webhook"; deliveryChannel: string; deliveryTo: string; + deliveryAccountId: string; deliveryBestEffort: boolean; failureAlertMode: "inherit" | "disabled" | "custom"; failureAlertAfter: string; diff --git a/ui/src/ui/views/cron.test.ts b/ui/src/ui/views/cron.test.ts index 95509b5f3..1fdfd8364 100644 --- a/ui/src/ui/views/cron.test.ts +++ b/ui/src/ui/views/cron.test.ts @@ -57,6 +57,7 @@ function createProps(overrides: Partial = {}): CronProps { thinkingSuggestions: [], timezoneSuggestions: [], deliveryToSuggestions: [], + accountSuggestions: [], onFormChange: () => undefined, onRefresh: () => undefined, onAdd: () => undefined, @@ -423,6 +424,7 @@ describe("cron view", () => { expect(container.textContent).toContain("Advanced"); expect(container.textContent).toContain("Exact timing (no stagger)"); expect(container.textContent).toContain("Stagger window"); + expect(container.textContent).toContain("Light context"); expect(container.textContent).toContain("Model"); expect(container.textContent).toContain("Thinking"); expect(container.textContent).toContain("Best effort delivery"); @@ -671,7 +673,7 @@ describe("cron view", () => { removeButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); expect(onToggle).toHaveBeenCalledWith(job, false); - expect(onRun).toHaveBeenCalledWith(job); + expect(onRun).toHaveBeenCalledWith(job, "force"); expect(onRemove).toHaveBeenCalledWith(job); expect(onLoadRuns).toHaveBeenCalledTimes(3); expect(onLoadRuns).toHaveBeenNthCalledWith(1, "job-actions"); @@ -679,6 +681,31 @@ describe("cron view", () => { expect(onLoadRuns).toHaveBeenNthCalledWith(3, "job-actions"); }); + it("wires Run if due action with due mode", () => { + const container = document.createElement("div"); + const onRun = vi.fn(); + const onLoadRuns = vi.fn(); + const job = createJob("job-due"); + render( + renderCron( + createProps({ + jobs: [job], + onRun, + onLoadRuns, + }), + ), + container, + ); + + const runDueButton = Array.from(container.querySelectorAll("button")).find( + (btn) => btn.textContent?.trim() === "Run if due", + ); + expect(runDueButton).not.toBeUndefined(); + runDueButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + + expect(onRun).toHaveBeenCalledWith(job, "due"); + }); + it("renders suggestion datalists for agent/model/thinking/timezone", () => { const container = document.createElement("div"); render( @@ -690,6 +717,7 @@ describe("cron view", () => { thinkingSuggestions: ["low"], timezoneSuggestions: ["UTC"], deliveryToSuggestions: ["+15551234567"], + accountSuggestions: ["default"], }), ), container, @@ -700,10 +728,14 @@ describe("cron view", () => { expect(container.querySelector("datalist#cron-thinking-suggestions")).not.toBeNull(); expect(container.querySelector("datalist#cron-tz-suggestions")).not.toBeNull(); expect(container.querySelector("datalist#cron-delivery-to-suggestions")).not.toBeNull(); + expect(container.querySelector("datalist#cron-delivery-account-suggestions")).not.toBeNull(); expect(container.querySelector('input[list="cron-agent-suggestions"]')).not.toBeNull(); expect(container.querySelector('input[list="cron-model-suggestions"]')).not.toBeNull(); expect(container.querySelector('input[list="cron-thinking-suggestions"]')).not.toBeNull(); expect(container.querySelector('input[list="cron-tz-suggestions"]')).not.toBeNull(); expect(container.querySelector('input[list="cron-delivery-to-suggestions"]')).not.toBeNull(); + expect( + container.querySelector('input[list="cron-delivery-account-suggestions"]'), + ).not.toBeNull(); }); }); diff --git a/ui/src/ui/views/cron.ts b/ui/src/ui/views/cron.ts index b13929f9c..2907f7f6c 100644 --- a/ui/src/ui/views/cron.ts +++ b/ui/src/ui/views/cron.ts @@ -61,6 +61,7 @@ export type CronProps = { thinkingSuggestions: string[]; timezoneSuggestions: string[]; deliveryToSuggestions: string[]; + accountSuggestions: string[]; onFormChange: (patch: Partial) => void; onRefresh: () => void; onAdd: () => void; @@ -68,7 +69,7 @@ export type CronProps = { onClone: (job: CronJob) => void; onCancelEdit: () => void; onToggle: (job: CronJob, enabled: boolean) => void; - onRun: (job: CronJob) => void; + onRun: (job: CronJob, mode?: "force" | "due") => void; onRemove: (job: CronJob) => void; onLoadRuns: (jobId: string) => void; onLoadMoreJobs: () => void; @@ -1037,6 +1038,21 @@ export function renderCron(props: CronProps) { ${t("cron.form.clearAgentOverride")}
${t("cron.form.clearAgentHelp")}
+ ${ isCronSchedule ? html` @@ -1098,6 +1114,37 @@ export function renderCron(props: CronProps) { ${ isAgentTurn ? html` + +