feat(cron): add command payload form (#1289)

This commit is contained in:
Zezae Oh authored and GitHub committed 2026-06-27 12:54:34 +07:00
1 parent a90127c3d6
commit 24a6b02506
6 files changed
+74 -24

No files matched your search

+2 -1
View File
@@ -147,7 +147,8 @@
"envVars": "{{count}} vars",
"commandJson": "Command JSON",
"commandJsonHelp": "Edit the command payload as JSON. argv is required and must be a non-empty string array.",
"invalidCommandJson": "Invalid command JSON: argv must be a non-empty array."
"invalidCommandJson": "Invalid command JSON: argv must be a non-empty array.",
"commandArgvHelp": "One argument per line. Use sh / -c / your script for shell syntax."
},
"stateless": "Stateless",
"statelessHelp": "Skip session persistence. Each run starts fresh without loading previous messages. Saves tokens for crons that don't need past context.",
+2 -1
View File
@@ -147,7 +147,8 @@
"envVars": "{{count}}개",
"commandJson": "명령 JSON",
"commandJsonHelp": "명령 payload를 JSON으로 수정합니다. argv는 필수이며 비어 있지 않은 문자열 배열이어야 합니다.",
"invalidCommandJson": "명령 JSON이 올바르지 않습니다: argv는 비어 있지 않은 배열이어야 합니다."
"invalidCommandJson": "명령 JSON이 올바르지 않습니다: argv는 비어 있지 않은 배열이어야 합니다.",
"commandArgvHelp": "한 줄에 인자 하나씩 입력하세요. shell 문법은 sh / -c / script 형태를 사용하세요."
},
"stateless": "상태 없음",
"statelessHelp": "세션 지속성 건너뜁니다. 각 실행은 이전 메시지 없이 새로 시작됩니다. 이전 컨텍스트가 필요 없는 cron의 토큰을 절약합니다.",
+2 -1
View File
@@ -147,7 +147,8 @@
"envVars": "{{count}} biến",
"commandJson": "JSON lệnh",
"commandJsonHelp": "Chỉnh sửa payload lệnh dưới dạng JSON. argv là bắt buộc và phải là mảng chuỗi không rỗng.",
"invalidCommandJson": "JSON lệnh không hợp lệ: argv phải là mảng không rỗng."
"invalidCommandJson": "JSON lệnh không hợp lệ: argv phải là mảng không rỗng.",
"commandArgvHelp": "Mỗi dòng là một đối số. Dùng sh / -c / script nếu cần cú pháp shell."
},
"stateless": "Không trạng thái",
"statelessHelp": "Bỏ qua lưu session. Mỗi lần chạy bắt đầu mới. Tiết kiệm token cho các cron không cần context cũ.",
+2 -1
View File
@@ -147,7 +147,8 @@
"envVars": "{{count}}个变量",
"commandJson": "命令 JSON",
"commandJsonHelp": "以 JSON 编辑命令 payload。argv 必填,且必须是非空字符串数组。",
"invalidCommandJson": "命令 JSON 无效:argv 必须是非空数组。"
"invalidCommandJson": "命令 JSON 无效:argv 必须是非空数组。",
"commandArgvHelp": "每行一个参数。如需 shell 语法,请使用 sh / -c / script。"
},
"stateless": "无状态",
"statelessHelp": "跳过会话持久化。每次运行从零开始。为不需要历史上下文的定时任务节省令牌。",
+50 -15
View File
@@ -41,7 +41,12 @@ export function CronFormDialog({ open, onOpenChange, onSubmit }: CronFormDialogP
name: "",
payloadKind: "agent_turn",
message: "",
commandJson: JSON.stringify({ argv: ["sh", "-c", "echo hello"] }, null, 2),
commandArgvText: "sh\n-c\necho hello",
commandCwd: "",
commandTimeoutSeconds: "",
commandNoOutputTimeoutSeconds: "",
commandOutputMaxBytes: "",
commandInput: "",
agentId: "",
scheduleKind: "every",
everyValue: "60",
@@ -63,7 +68,14 @@ export function CronFormDialog({ open, onOpenChange, onSubmit }: CronFormDialogP
}
const command = data.payloadKind === "command"
? JSON.parse(data.commandJson || "{}") as CronCommandSpec
? {
argv: (data.commandArgvText || "").split("\n").map((v) => v.trim()).filter(Boolean),
cwd: data.commandCwd?.trim() || undefined,
timeoutSeconds: data.commandTimeoutSeconds ? Number(data.commandTimeoutSeconds) : undefined,
noOutputTimeoutSeconds: data.commandNoOutputTimeoutSeconds ? Number(data.commandNoOutputTimeoutSeconds) : undefined,
outputMaxBytes: data.commandOutputMaxBytes ? Number(data.commandOutputMaxBytes) : undefined,
input: data.commandInput || undefined,
} satisfies CronCommandSpec
: undefined;
await onSubmit({
@@ -200,18 +212,41 @@ export function CronFormDialog({ open, onOpenChange, onSubmit }: CronFormDialogP
)}
</div>
) : (
<div className="space-y-2">
<Label>{t("detail.commandJson")}</Label>
<Textarea
{...register("commandJson")}
rows={8}
className="font-mono text-base md:text-sm"
/>
{errors.commandJson ? (
<p className="text-xs text-destructive">{errors.commandJson.message}</p>
) : (
<p className="text-xs text-muted-foreground">{t("detail.commandJsonHelp")}</p>
)}
<div className="space-y-3 rounded-md border p-3">
<div className="space-y-2">
<Label>{t("detail.commandArgv")}</Label>
<Textarea {...register("commandArgvText")} rows={4} className="font-mono text-base md:text-sm" />
{errors.commandArgvText ? (
<p className="text-xs text-destructive">{errors.commandArgvText.message}</p>
) : (
<p className="text-xs text-muted-foreground">{t("detail.commandArgvHelp")}</p>
)}
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div className="space-y-2">
<Label>{t("detail.commandCwd")}</Label>
<Input {...register("commandCwd")} placeholder={t("detail.defaultWorkingDirectory")} />
</div>
<div className="space-y-2">
<Label>{t("detail.commandTimeout")}</Label>
<Input type="number" min={0} {...register("commandTimeoutSeconds")} placeholder={t("detail.defaultValue")} />
{errors.commandTimeoutSeconds && <p className="text-xs text-destructive">{errors.commandTimeoutSeconds.message}</p>}
</div>
<div className="space-y-2">
<Label>{t("detail.commandNoOutputTimeout")}</Label>
<Input type="number" min={0} {...register("commandNoOutputTimeoutSeconds")} placeholder={t("detail.none")} />
{errors.commandNoOutputTimeoutSeconds && <p className="text-xs text-destructive">{errors.commandNoOutputTimeoutSeconds.message}</p>}
</div>
<div className="space-y-2">
<Label>{t("detail.commandOutputLimit")}</Label>
<Input type="number" min={0} {...register("commandOutputMaxBytes")} placeholder={t("detail.defaultValue")} />
{errors.commandOutputMaxBytes && <p className="text-xs text-destructive">{errors.commandOutputMaxBytes.message}</p>}
</div>
</div>
<div className="space-y-2">
<Label>{t("detail.commandInput")}</Label>
<Textarea {...register("commandInput")} rows={3} className="font-mono text-base md:text-sm" placeholder={t("detail.none")} />
</div>
</div>
)}
</div>
@@ -221,7 +256,7 @@ export function CronFormDialog({ open, onOpenChange, onSubmit }: CronFormDialogP
</Button>
<Button
onClick={handleSubmit(onFormSubmit)}
disabled={isSubmitting || !!errors.name || (payloadKind === "agent_turn" ? !!errors.message : !!errors.commandJson)}
disabled={isSubmitting || !!errors.name || (payloadKind === "agent_turn" ? !!errors.message : !!errors.commandArgvText)}
>
{isSubmitting ? t("create.creating") : t("create.create")}
</Button>
+16 -5
View File
@@ -8,7 +8,12 @@ export const cronCreateSchema = z.object({
.refine(isValidSlug, "Only lowercase letters, numbers, and hyphens"),
payloadKind: z.enum(["agent_turn", "command"]),
message: z.string().optional(),
commandJson: z.string().optional(),
commandArgvText: z.string().optional(),
commandCwd: z.string().optional(),
commandTimeoutSeconds: z.string().optional(),
commandNoOutputTimeoutSeconds: z.string().optional(),
commandOutputMaxBytes: z.string().optional(),
commandInput: z.string().optional(),
agentId: z.string().optional(),
scheduleKind: z.enum(["every", "cron", "at"]),
everyValue: z.string().min(1, "Required"),
@@ -19,12 +24,18 @@ export const cronCreateSchema = z.object({
}
if (data.payloadKind === "command") {
try {
const parsed = JSON.parse(data.commandJson || "");
if (!Array.isArray(parsed?.argv) || parsed.argv.length === 0 || !parsed.argv[0]) {
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["commandJson"], message: "argv must be a non-empty array" });
const argv = (data.commandArgvText || "").split("\n").map((v) => v.trim()).filter(Boolean);
if (argv.length === 0 || !argv[0]) {
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["commandArgvText"], message: "argv must be a non-empty list" });
}
for (const field of ["commandTimeoutSeconds", "commandNoOutputTimeoutSeconds", "commandOutputMaxBytes"] as const) {
const raw = data[field];
if (raw && (!/^\d+$/.test(raw) || Number(raw) < 0)) {
ctx.addIssue({ code: z.ZodIssueCode.custom, path: [field], message: "Must be a non-negative integer" });
}
}
} catch {
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["commandJson"], message: "Invalid JSON" });
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["commandArgvText"], message: "Invalid command" });
}
}
});