import { useId, useMemo, useRef, useState } from "react"; import type { Project, ScheduledTask, ScheduledTaskInput, ScheduleKind } from "../../../lib/types"; import { addScheduledTask, updateScheduledTask } from "../../../lib/tauri-commands"; import { effectivePermissionMode, PERMISSION_MODES } from "../PermissionModeControl"; import Button from "../../ui/Button"; import Modal from "../../ui/Modal"; import SegmentedControl from "../../ui/SegmentedControl"; import { inputClass, monoInputClass } from "../../ui/Field"; import { atTimestampIsPast, CRON_PRESETS, DEFAULT_WORKING_DIR, describeCron, MAX_TASK_PROMPT_LEN, validateAtTimestamp, validateCronExpression, validateTaskName, validateTaskPrompt, validateWorkingDir, } from "./taskValidation"; interface Props { project: Project; /** `null` creates a new task; a task edits it in place. */ task: ScheduledTask | null; onClose: () => void; /** Called after the scheduler accepted the change, to refresh the list. */ onSaved: () => void; } const DEFAULT_CRON = "0 9 * * *"; /** `YYYY-MM-DD HH:MM`, one hour from now, as the one-shot default. */ function defaultAtTimestamp(now = new Date()): string { const at = new Date(now.getTime() + 60 * 60 * 1000); at.setSeconds(0, 0); const pad = (n: number) => String(n).padStart(2, "0"); return `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())} ${pad( at.getHours(), )}:${pad(at.getMinutes())}`; } /** * Create or edit a `triple-c-scheduler` task. * * Validation here mirrors the backend so mistakes surface before a round trip; * the backend re-checks everything regardless. */ export default function TaskEditorModal({ project, task, onClose, onSaved }: Props) { const formId = useId(); const nameRef = useRef(null); const [name, setName] = useState(task?.name ?? ""); const [prompt, setPrompt] = useState(task?.prompt ?? ""); const [workingDir, setWorkingDir] = useState(task?.working_dir ?? DEFAULT_WORKING_DIR); const [kind, setKind] = useState( task?.task_type === "once" ? "once" : "recurring", ); const [cron, setCron] = useState( task && task.task_type !== "once" ? task.schedule : DEFAULT_CRON, ); const [at, setAt] = useState(task?.at ?? defaultAtTimestamp()); const [showAllErrors, setShowAllErrors] = useState(false); const [touched, setTouched] = useState>({}); const [saving, setSaving] = useState(false); const [submitError, setSubmitError] = useState(null); const errors = { name: validateTaskName(name), prompt: validateTaskPrompt(prompt), workingDir: validateWorkingDir(workingDir), schedule: kind === "recurring" ? validateCronExpression(cron) : validateAtTimestamp(at), }; const hasErrors = Object.values(errors).some(Boolean); const show = (field: keyof typeof errors) => (showAllErrors || touched[field]) && errors[field] ? errors[field] : null; const cronReading = useMemo(() => describeCron(cron), [cron]); const atIsPast = kind === "once" && atTimestampIsPast(at); const mode = effectivePermissionMode(project); const modeLabel = PERMISSION_MODES.find((m) => m.value === mode)?.label ?? mode; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setShowAllErrors(true); setSubmitError(null); if (hasErrors) return; const input: ScheduledTaskInput = { name: name.trim(), prompt: prompt.trim(), scheduleKind: kind, schedule: kind === "recurring" ? cron.trim() : at.trim(), workingDir: workingDir.trim() || DEFAULT_WORKING_DIR, }; setSaving(true); try { if (task) { await updateScheduledTask(project.id, task.id, input, task.enabled); } else { await addScheduledTask(project.id, input); } onSaved(); onClose(); } catch (err) { setSubmitError(String(err)); } finally { setSaving(false); } }; const errorText = (message: string | null) => message ? (

{message}

) : null; return ( } >
{/* Name */}
setName(e.target.value)} onBlur={() => setTouched((t) => ({ ...t, name: true }))} placeholder="nightly-tests" aria-invalid={show("name") ? true : undefined} className={inputClass} /> {errorText(show("name"))}
{/* Prompt */}

What Claude Code is asked to do on each run.