"use client"; import { Circle, Eye, Pause, Pencil, Play, Plus, RefreshCw, Trash2, Zap, } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Button, buttonVariants } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@/components/ui/combobox"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; import { desktopClient } from "@/lib/desktop-client"; import { readModelSelectionStorageFromWindow } from "@/lib/model-selection"; import { normalizeProviderId } from "@/lib/provider-id"; import { loadProviderModelCatalog, loadProviderModels, } from "@/lib/provider-model-catalog"; import { cn } from "@/lib/utils"; import { CommandBadge, PageEmptyState, PageFrame, PageHeader, } from "../page-layout"; type DateTimeValue = number | string; interface RoutineSchedule { scheduleId: string; name: string; cronPattern: string; prompt: string; provider?: string; model?: string; modelSelection?: { providerId?: string; modelId?: string; }; mode: "act" | "plan" | "yolo"; workspaceRoot?: string; cwd?: string; systemPrompt?: string; maxIterations?: number; timeoutSeconds?: number; maxParallel: number; enabled: boolean; createdAt: DateTimeValue; updatedAt: DateTimeValue; lastRunAt?: DateTimeValue; nextRunAt?: DateTimeValue; tags?: string[]; } interface RoutineExecution { executionId: string; scheduleId: string; sessionId?: string; triggeredAt?: DateTimeValue; startedAt?: DateTimeValue; endedAt?: DateTimeValue; timeoutAt?: DateTimeValue; status?: string; errorMessage?: string; } interface RoutineUpcomingRun { scheduleId: string; name: string; nextRunAt: DateTimeValue; } interface RoutineOverviewResponse { schedules: RoutineSchedule[]; activeExecutions: RoutineExecution[]; upcomingRuns: RoutineUpcomingRun[]; lastExecutions: RoutineExecution[]; } const ROUTINE_OVERVIEW_CACHE_TTL_MS = 30_000; let routineOverviewCache: | (RoutineOverviewResponse & { fetchedAt: number; }) | null = null; async function fetchRoutineOverview(): Promise { const response = await desktopClient.invoke( "list_routine_schedules", ); return { schedules: response.schedules ?? [], activeExecutions: response.activeExecutions ?? [], upcomingRuns: response.upcomingRuns ?? [], lastExecutions: response.lastExecutions ?? [], }; } interface ProcessContext { workspaceRoot: string; cwd: string; } const FALLBACK_PROVIDER_MODELS: Record = { cline: ["anthropic/claude-sonnet-4.6"], anthropic: ["claude-sonnet-4-6"], "openai-native": ["gpt-5.3-codex"], openrouter: ["anthropic/claude-sonnet-4.6"], gemini: ["gemini-2.5-pro"], }; const WEEKDAY_OPTIONS = [ { label: "Mon", value: "MON" }, { label: "Tue", value: "TUE" }, { label: "Wed", value: "WED" }, { label: "Thu", value: "THU" }, { label: "Fri", value: "FRI" }, { label: "Sat", value: "SAT" }, { label: "Sun", value: "SUN" }, ] as const; interface RoutineFormState { name: string; scheduleHour: string; scheduleMinute: string; scheduleDays: string[]; prompt: string; provider: string; model: string; mode: "act" | "plan"; workspaceRoot: string; cwd: string; systemPrompt: string; maxIterations: string; timeoutSeconds: string; maxParallel: string; tags: string; enabled: boolean; } function formatDateTime(value?: DateTimeValue | null): string { if (value === undefined || value === null) { return "-"; } if (typeof value === "number") { if (!Number.isFinite(value)) { return "-"; } const parsed = new Date(value); return Number.isNaN(parsed.getTime()) ? "-" : parsed.toLocaleString(); } const trimmed = value.trim(); if (trimmed.length === 0) { return "-"; } const parsed = new Date(trimmed); if (Number.isNaN(parsed.getTime())) { return trimmed; } return parsed.toLocaleString(); } function formatScheduleModel(schedule: RoutineSchedule): string { const provider = schedule.modelSelection?.providerId?.trim() || schedule.provider?.trim(); const model = schedule.modelSelection?.modelId?.trim() || schedule.model?.trim(); if (provider && model) { return `${provider}/${model}`; } return model || provider || "-"; } function getScheduleProviderModel(schedule: RoutineSchedule): { provider: string; model: string; } { return { provider: schedule.modelSelection?.providerId?.trim() || schedule.provider?.trim() || "cline", model: schedule.modelSelection?.modelId?.trim() || schedule.model?.trim() || "openai/gpt-5.3-codex", }; } function formatExecutionResult(execution?: RoutineExecution): string { if (!execution) { return "-"; } const status = execution.status?.trim() || "unknown"; const timestamp = execution.endedAt ?? execution.startedAt ?? execution.triggeredAt; const when = formatDateTime(timestamp); return when === "-" ? status : `${status} at ${when}`; } function parseOptionalPositiveInt(text: string): number | undefined { const trimmed = text.trim(); if (!trimmed) { return undefined; } const value = Number.parseInt(trimmed, 10); if (!Number.isFinite(value) || value <= 0) { return undefined; } return value; } function parseTags(text: string): string[] | undefined { const tags = text .split(",") .map((value) => value.trim()) .filter((value) => value.length > 0); return tags.length > 0 ? tags : undefined; } function normalizeScheduleDays(days: string[]): string[] { const selected = new Set(days); return WEEKDAY_OPTIONS.map((option) => option.value).filter((value) => selected.has(value), ); } function buildCronPattern( days: string[], hour: string, minute: string, ): string { const normalizedDays = normalizeScheduleDays(days); if (normalizedDays.length === 0) { return ""; } const normalizedHour = Number.parseInt(hour, 10); const normalizedMinute = Number.parseInt(minute, 10); const cronHour = Number.isFinite(normalizedHour) ? Math.min(Math.max(normalizedHour, 0), 23) : 9; const cronMinute = Number.isFinite(normalizedMinute) ? Math.min(Math.max(normalizedMinute, 0), 59) : 0; if (normalizedDays.length === WEEKDAY_OPTIONS.length) { return `${cronMinute} ${cronHour} * * *`; } return `${cronMinute} ${cronHour} * * ${normalizedDays.join(",")}`; } function expandCronDays(dayExpression: string | undefined): string[] { const raw = dayExpression?.trim().toUpperCase(); if (!raw || raw === "*") { return WEEKDAY_OPTIONS.map((option) => option.value); } const values = new Set(); for (const part of raw.split(",")) { const trimmed = part.trim(); if (!trimmed) continue; const rangeMatch = /^([A-Z]{3})-([A-Z]{3})$/.exec(trimmed); if (rangeMatch) { const start = WEEKDAY_OPTIONS.findIndex( (option) => option.value === rangeMatch[1], ); const end = WEEKDAY_OPTIONS.findIndex( (option) => option.value === rangeMatch[2], ); if (start >= 0 && end >= start) { for (let index = start; index <= end; index += 1) { values.add(WEEKDAY_OPTIONS[index].value); } } continue; } if (WEEKDAY_OPTIONS.some((option) => option.value === trimmed)) { values.add(trimmed); } } return normalizeScheduleDays([...values]); } function parseCronPattern( cronPattern: string, ): Pick { const parts = cronPattern.trim().split(/\s+/); const minute = Number.parseInt(parts[0] ?? "", 10); const hour = Number.parseInt(parts[1] ?? "", 10); const days = expandCronDays(parts[4]); return { scheduleHour: Number.isInteger(hour) && hour >= 0 && hour <= 23 ? String(hour) : "9", scheduleMinute: Number.isInteger(minute) && minute >= 0 && minute <= 59 ? String(minute) : "0", scheduleDays: days.length > 0 ? days : ["MON", "TUE", "WED", "THU", "FRI"], }; } export function RoutineSchedulesContent() { const [schedules, setSchedules] = useState( () => routineOverviewCache?.schedules ?? [], ); const [activeExecutions, setActiveExecutions] = useState( () => routineOverviewCache?.activeExecutions ?? [], ); const [upcomingRuns, setUpcomingRuns] = useState( () => routineOverviewCache?.upcomingRuns ?? [], ); const [lastExecutions, setLastExecutions] = useState( () => routineOverviewCache?.lastExecutions ?? [], ); const [isLoading, setIsLoading] = useState(() => !routineOverviewCache); const [errorMessage, setErrorMessage] = useState(null); const [busyScheduleId, setBusyScheduleId] = useState(null); const [schedulePendingDelete, setSchedulePendingDelete] = useState(null); const [isCreateOpen, setIsCreateOpen] = useState(false); const [editingSchedule, setEditingSchedule] = useState(null); const [isCreating, setIsCreating] = useState(false); const [createFormError, setCreateFormError] = useState(null); const [providerModels, setProviderModels] = useState< Record >(FALLBACK_PROVIDER_MODELS); const [enabledProviderIds, setEnabledProviderIds] = useState(() => Object.keys(FALLBACK_PROVIDER_MODELS), ); const [lastModelSelection] = useState(() => readModelSelectionStorageFromWindow(), ); const rememberedProvider = normalizeProviderId( lastModelSelection.lastProvider, ); const [createForm, setCreateForm] = useState({ name: "", scheduleHour: "9", scheduleMinute: "0", scheduleDays: ["MON", "TUE", "WED", "THU", "FRI"], prompt: "Review PRs opened yesterday and summarize issues.", provider: "cline", model: "openai/gpt-5.3-codex", mode: "act", workspaceRoot: "", cwd: "", systemPrompt: "", maxIterations: "", timeoutSeconds: "", maxParallel: "1", tags: "", enabled: true, }); const visibleProviderModels = useMemo(() => { if (enabledProviderIds.length === 0) { return providerModels; } const next: Record = {}; for (const providerId of enabledProviderIds) { next[providerId] = providerModels[providerId] ?? []; } return next; }, [enabledProviderIds, providerModels]); const availableProviders = useMemo( () => Object.keys(visibleProviderModels), [visibleProviderModels], ); const availableModelsForProvider = useMemo( () => visibleProviderModels[createForm.provider] ?? [], [createForm.provider, visibleProviderModels], ); const cronPreview = useMemo( () => buildCronPattern( createForm.scheduleDays, createForm.scheduleHour, createForm.scheduleMinute, ), [ createForm.scheduleDays, createForm.scheduleHour, createForm.scheduleMinute, ], ); useEffect(() => { let cancelled = false; async function loadCatalog() { try { const payload = await loadProviderModelCatalog(); if (cancelled) { return; } setProviderModels(payload.providerModels); setEnabledProviderIds((current) => { const nextProviderIds = new Set(payload.enabledProviderIds); const normalizedCurrentProvider = normalizeProviderId( createForm.provider, ); if (normalizedCurrentProvider) { nextProviderIds.add(normalizedCurrentProvider); } for (const providerId of current) { if (providerId in payload.providerModels) { nextProviderIds.add(providerId); } } return Array.from(nextProviderIds); }); } catch { // Keep fallback values if provider catalog is unavailable. } } void loadCatalog(); return () => { cancelled = true; }; }, [createForm.provider]); useEffect(() => { const normalizedProvider = normalizeProviderId(createForm.provider); if (!normalizedProvider) { return; } if ((providerModels[normalizedProvider] ?? []).length > 0) { return; } let cancelled = false; async function loadModelsForProvider() { try { const models = await loadProviderModels(normalizedProvider); if (cancelled || models.length === 0) { return; } setProviderModels((current) => ({ ...current, [normalizedProvider]: models.map((entry) => entry.id), })); setEnabledProviderIds((current) => current.includes(normalizedProvider) ? current : [...current, normalizedProvider], ); } catch { // Keep existing values when provider-specific model loading fails. } } void loadModelsForProvider(); return () => { cancelled = true; }; }, [createForm.provider, providerModels]); useEffect(() => { if (availableProviders.length === 0) { return; } let nextSelection: { provider: string; model: string } | null = null; const normalizedFormProvider = normalizeProviderId(createForm.provider); if (!availableProviders.includes(normalizedFormProvider)) { const nextProvider = rememberedProvider && availableProviders.includes(rememberedProvider) ? rememberedProvider : availableProviders[0]; const models = visibleProviderModels[nextProvider] ?? []; const rememberedModel = lastModelSelection.lastModelByProvider[nextProvider] ?? lastModelSelection.lastModelByProvider[lastModelSelection.lastProvider]; const nextModel = rememberedModel && models.includes(rememberedModel) ? rememberedModel : (models[0] ?? ""); nextSelection = { provider: nextProvider, model: nextModel }; } else { const models = visibleProviderModels[normalizedFormProvider] ?? []; if (models.length === 0 || models.includes(createForm.model)) { return; } const rememberedModel = lastModelSelection.lastModelByProvider[normalizedFormProvider] ?? lastModelSelection.lastModelByProvider[lastModelSelection.lastProvider]; const nextModel = rememberedModel && models.includes(rememberedModel) ? rememberedModel : models[0]; nextSelection = { provider: normalizedFormProvider, model: nextModel }; } const timeoutId = window.setTimeout(() => { setCreateForm((prev) => ({ ...prev, ...nextSelection, })); }, 0); return () => window.clearTimeout(timeoutId); }, [ availableProviders, createForm.model, createForm.provider, lastModelSelection.lastModelByProvider, lastModelSelection.lastProvider, rememberedProvider, visibleProviderModels, ]); const refreshSchedules = useCallback( async (options?: { force?: boolean; showLoading?: boolean }) => { const now = Date.now(); if ( !options?.force && routineOverviewCache && now - routineOverviewCache.fetchedAt < ROUTINE_OVERVIEW_CACHE_TTL_MS ) { setSchedules(routineOverviewCache.schedules); setActiveExecutions(routineOverviewCache.activeExecutions); setUpcomingRuns(routineOverviewCache.upcomingRuns); setLastExecutions(routineOverviewCache.lastExecutions); setErrorMessage(null); setIsLoading(false); return; } if (options?.showLoading !== false) { setIsLoading(true); } setErrorMessage(null); try { const response = await fetchRoutineOverview(); const schedules = response.schedules; const activeExecutions = response.activeExecutions; const upcomingRuns = response.upcomingRuns; const lastExecutions = response.lastExecutions; setSchedules(schedules); setActiveExecutions(activeExecutions); setUpcomingRuns(upcomingRuns); setLastExecutions(lastExecutions); routineOverviewCache = { schedules, activeExecutions, upcomingRuns, lastExecutions, fetchedAt: now, }; } catch (error) { const message = error instanceof Error ? error.message : String(error); setErrorMessage(message); } finally { setIsLoading(false); } }, [], ); useEffect(() => { const timeoutId = window.setTimeout(() => { void refreshSchedules(); }, 0); return () => window.clearTimeout(timeoutId); }, [refreshSchedules]); const upsertScheduleEnabled = async ( schedule: RoutineSchedule, enabled: boolean, ) => { setBusyScheduleId(schedule.scheduleId); setErrorMessage(null); try { if (enabled) { await desktopClient.invoke("resume_routine_schedule", { schedule_id: schedule.scheduleId, }); } else { await desktopClient.invoke("pause_routine_schedule", { schedule_id: schedule.scheduleId, }); } await refreshSchedules({ force: true, showLoading: false }); } catch (error) { const message = error instanceof Error ? error.message : String(error); setErrorMessage(message); } finally { setBusyScheduleId(null); } }; const triggerSchedule = async (scheduleId: string) => { setBusyScheduleId(scheduleId); setErrorMessage(null); try { await desktopClient.invoke("trigger_routine_schedule", { schedule_id: scheduleId, }); await refreshSchedules({ force: true, showLoading: false }); window.setTimeout(() => { void refreshSchedules({ force: true, showLoading: false }); }, 1_000); } catch (error) { const message = error instanceof Error ? error.message : String(error); setErrorMessage(message); } finally { setBusyScheduleId(null); } }; const deleteSchedule = async (scheduleId: string) => { setBusyScheduleId(scheduleId); setErrorMessage(null); try { await desktopClient.invoke("delete_routine_schedule", { schedule_id: scheduleId, }); setSchedulePendingDelete(null); await refreshSchedules({ force: true, showLoading: false }); } catch (error) { const message = error instanceof Error ? error.message : String(error); setErrorMessage(message); } finally { setBusyScheduleId(null); } }; const openCreateDialog = async () => { setEditingSchedule(null); setErrorMessage(null); setCreateFormError(null); let context: ProcessContext = { workspaceRoot: "", cwd: "" }; try { context = await desktopClient.invoke( "get_process_context", ); } catch { // Use empty defaults when context lookup fails. } const preferredProvider = rememberedProvider && availableProviders.includes(rememberedProvider) ? rememberedProvider : (availableProviders[0] ?? "cline"); const modelsForProvider = visibleProviderModels[preferredProvider] ?? []; const rememberedModel = lastModelSelection.lastModelByProvider[preferredProvider] ?? lastModelSelection.lastModelByProvider[lastModelSelection.lastProvider]; const preferredModel = rememberedModel && modelsForProvider.includes(rememberedModel) ? rememberedModel : (modelsForProvider[0] ?? createForm.model); setCreateForm({ name: "", scheduleHour: "9", scheduleMinute: "0", scheduleDays: ["MON", "TUE", "WED", "THU", "FRI"], prompt: "Review PRs opened yesterday and summarize issues.", provider: preferredProvider, model: preferredModel, mode: "act", workspaceRoot: context.workspaceRoot || context.cwd, cwd: context.cwd || "", systemPrompt: "", maxIterations: "", timeoutSeconds: "", maxParallel: "1", tags: "", enabled: true, }); setIsCreateOpen(true); }; const openEditDialog = (schedule: RoutineSchedule) => { const { provider, model } = getScheduleProviderModel(schedule); const parsedCron = parseCronPattern(schedule.cronPattern); setEditingSchedule(schedule); setErrorMessage(null); setCreateFormError(null); setEnabledProviderIds((current) => current.includes(provider) ? current : [...current, provider], ); setProviderModels((current) => current[provider]?.includes(model) ? current : { ...current, [provider]: [...(current[provider] ?? []), model], }, ); setCreateForm({ name: schedule.name, ...parsedCron, prompt: schedule.prompt, provider, model, mode: schedule.mode === "plan" ? "plan" : "act", workspaceRoot: schedule.workspaceRoot ?? "", cwd: schedule.cwd ?? "", systemPrompt: schedule.systemPrompt ?? "", maxIterations: typeof schedule.maxIterations === "number" ? String(schedule.maxIterations) : "", timeoutSeconds: typeof schedule.timeoutSeconds === "number" ? String(schedule.timeoutSeconds) : "", maxParallel: typeof schedule.maxParallel === "number" ? String(schedule.maxParallel) : "1", tags: schedule.tags?.join(",") ?? "", enabled: schedule.enabled, }); setIsCreateOpen(true); }; const submitCreateForm = async () => { const name = createForm.name.trim(); if (!name) { setCreateFormError("Routine name is required."); return; } const cronPattern = buildCronPattern( createForm.scheduleDays, createForm.scheduleHour, createForm.scheduleMinute, ); if (!cronPattern) { setCreateFormError("Select at least one day and a valid time."); return; } const prompt = createForm.prompt.trim(); if (!prompt) { setCreateFormError("Prompt is required."); return; } const workspaceRoot = createForm.workspaceRoot.trim(); if (!workspaceRoot) { setCreateFormError("Workspace root is required."); return; } setCreateFormError(null); setIsCreating(true); try { const provider = normalizeProviderId(createForm.provider) || availableProviders[0] || "cline"; const model = createForm.model.trim() || (visibleProviderModels[provider] ?? [])[0] || "openai/gpt-5.3-codex"; const maxIterations = parseOptionalPositiveInt(createForm.maxIterations); const timeoutSeconds = parseOptionalPositiveInt( createForm.timeoutSeconds, ); const maxParallel = parseOptionalPositiveInt(createForm.maxParallel) ?? 1; const tags = parseTags(createForm.tags); const command = editingSchedule ? "update_routine_schedule" : "create_routine_schedule"; await desktopClient.invoke(command, { ...(editingSchedule ? { schedule_id: editingSchedule.scheduleId } : undefined), name, cron_pattern: cronPattern, prompt, provider, model, mode: createForm.mode, workspace_root: workspaceRoot, cwd: createForm.cwd.trim() || undefined, system_prompt: editingSchedule ? createForm.systemPrompt.trim() || null : createForm.systemPrompt.trim() || undefined, max_iterations: editingSchedule ? (maxIterations ?? null) : maxIterations, timeout_seconds: editingSchedule ? (timeoutSeconds ?? null) : timeoutSeconds, max_parallel: maxParallel, enabled: createForm.enabled, tags: tags ?? [], }); await refreshSchedules({ force: true, showLoading: false }); setIsCreateOpen(false); setEditingSchedule(null); } catch (error) { const message = error instanceof Error ? error.message : String(error); setCreateFormError(message); } finally { setIsCreating(false); } }; const executionBySchedule = useMemo(() => { const map = new Map(); for (const execution of activeExecutions) { if (!execution.scheduleId) { continue; } if (!map.has(execution.scheduleId)) { map.set(execution.scheduleId, execution); } } return map; }, [activeExecutions]); const lastExecutionBySchedule = useMemo(() => { const map = new Map(); for (const execution of lastExecutions) { if (!execution.scheduleId) { continue; } if (!map.has(execution.scheduleId)) { map.set(execution.scheduleId, execution); } } return map; }, [lastExecutions]); const sortedSchedules = useMemo( () => [...schedules].sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()), ), [schedules], ); return ( cline schedule} actions={ <> } /> {errorMessage && (
{errorMessage}
)} {isLoading ? ( Loading schedules... ) : sortedSchedules.length === 0 ? ( No schedules found. Create a schedule to run routines on a recurring basis. ) : (
{sortedSchedules.map((schedule) => { const isBusy = busyScheduleId === schedule.scheduleId; const activeExecution = executionBySchedule.get( schedule.scheduleId, ); const lastExecution = lastExecutionBySchedule.get( schedule.scheduleId, ); const upcoming = upcomingRuns.find( (item) => item.scheduleId === schedule.scheduleId, ); return (

{schedule.name}

{schedule.mode} {schedule.cronPattern}
void upsertScheduleEnabled(schedule, checked) } disabled={isBusy} aria-label={`Enable ${schedule.name}`} />

ID:{" "} {schedule.scheduleId}

Prompt:{" "} {schedule.prompt}

Model:{" "} {formatScheduleModel(schedule)}

{schedule.workspaceRoot && (

Workspace: {" "} {schedule.workspaceRoot}

)} {schedule.cwd && (

CWD:{" "} {schedule.cwd}

)}

Last run:{" "} {formatDateTime(schedule.lastRunAt)}

Last result: {" "} {formatExecutionResult(lastExecution)}

{lastExecution?.sessionId && (

Last session: {" "} {lastExecution.sessionId}

)} {lastExecution?.errorMessage && (

Last error: {" "} {lastExecution.errorMessage}

)}

Next run:{" "} {formatDateTime(schedule.nextRunAt || upcoming?.nextRunAt)}

{activeExecution && (

Active:{" "} {activeExecution.executionId} since{" "} {formatDateTime(activeExecution.startedAt)}

)} {schedule.tags && schedule.tags.length > 0 && (

Tags:{" "} {schedule.tags.join(", ")}

)}
); })}
)} { if (!open) { setSchedulePendingDelete(null); } }} > Delete Routine This will delete "{schedulePendingDelete?.name ?? "this routine"}" and remove future scheduled runs. Cancel { if (schedulePendingDelete) { void deleteSchedule(schedulePendingDelete.scheduleId); } }} className={buttonVariants({ variant: "destructive" })} > Delete { setIsCreateOpen(open); if (!open) { setCreateFormError(null); setEditingSchedule(null); } }} > {editingSchedule ? "Edit Routine" : "Create Routine"} {editingSchedule ? "Update this scheduler routine." : "Create a scheduler routine."}
setCreateForm((prev) => ({ ...prev, name: event.target.value, })) } placeholder="Daily code review" />
{WEEKDAY_OPTIONS.map((day) => { const inputId = `routine-day-${day.value.toLowerCase()}`; const checked = createForm.scheduleDays.includes( day.value, ); return ( ); })}
Cron:{" "} {cronPreview || "Select one or more days"}