import { useState, useEffect } from 'react' import { useTaskStore } from '../../stores/taskStore' import { useSessionStore } from '../../stores/sessionStore' import { useAdapterStore } from '../../stores/adapterStore' import { Modal } from '../shared/Modal' import { Input } from '../shared/Input' import { Button } from '../shared/Button' import { PromptEditor } from './PromptEditor' import { DayOfWeekPicker } from './DayOfWeekPicker' import { useTranslation } from '../../i18n' import { describeCron, isValidCron, parseCron, type FrequencyKey } from '../../lib/cronDescribe' import type { CronTask } from '../../types/task' type NotificationChannel = 'desktop' | 'telegram' | 'feishu' type Props = { open: boolean onClose: () => void editTask?: CronTask } const MINUTE_INTERVALS = [5, 10, 15, 20, 30] const HOUR_INTERVALS = [1, 2, 3, 4, 6, 8, 12] const MINUTE_OFFSETS = [0, 15, 30, 45] function buildCron( freq: FrequencyKey, time: string, opts: { minuteInterval: number hourInterval: number minuteOffset: number selectedDays: number[] monthDay: number customCron: string }, ): string { const [hours, minutes] = time.split(':').map(Number) switch (freq) { case 'everyNMinutes': return `*/${opts.minuteInterval} * * * *` case 'everyNHours': return `${opts.minuteOffset} */${opts.hourInterval} * * *` case 'daily': return `${minutes} ${hours} * * *` case 'weekdays': return `${minutes} ${hours} * * 1-5` case 'specificDays': return `${minutes} ${hours} * * ${[...opts.selectedDays].sort((a, b) => a - b).join(',')}` case 'monthly': return `${minutes} ${hours} ${opts.monthDay} * *` case 'customCron': return opts.customCron.trim() } } export function NewTaskModal({ open, onClose, editTask }: Props) { const t = useTranslation() const { createTask, updateTask } = useTaskStore() const sessions = useSessionStore((s) => s.sessions) const activeSessionId = useSessionStore((s) => s.activeSessionId) const activeSession = sessions.find((s) => s.id === activeSessionId) const defaultWorkDir = activeSession?.workDir || '' const adapterConfig = useAdapterStore((s) => s.config) const fetchAdapterConfig = useAdapterStore((s) => s.fetchConfig) useEffect(() => { if (open) fetchAdapterConfig() }, [open]) const isFeishuConfigured = !!(adapterConfig.feishu?.appId && adapterConfig.feishu?.appSecret && ((adapterConfig.feishu?.pairedUsers?.length ?? 0) > 0 || (adapterConfig.feishu?.allowedUsers?.length ?? 0) > 0)) const isTelegramConfigured = !!(adapterConfig.telegram?.botToken && ((adapterConfig.telegram?.pairedUsers?.length ?? 0) > 0 || (adapterConfig.telegram?.allowedUsers?.length ?? 0) > 0)) const isEdit = !!editTask const parsed = editTask ? parseCron(editTask.cron) : null const FREQUENCY_OPTIONS: Array<{ value: FrequencyKey; label: string }> = [ { value: 'everyNMinutes', label: t('newTask.everyNMinutes') }, { value: 'everyNHours', label: t('newTask.everyNHours') }, { value: 'daily', label: t('newTask.daily') }, { value: 'weekdays', label: t('newTask.weekdays') }, { value: 'specificDays', label: t('newTask.specificDays') }, { value: 'monthly', label: t('newTask.monthly') }, { value: 'customCron', label: t('newTask.customCron') }, ] const [name, setName] = useState(editTask?.name || '') const [description, setDescription] = useState(editTask?.description || '') const [prompt, setPrompt] = useState(editTask?.prompt || '') const [frequency, setFrequency] = useState(parsed?.frequency || 'daily') const [time, setTime] = useState(parsed?.time || '09:00') const [model, setModel] = useState(editTask?.model || '') const [providerId, setProviderId] = useState(editTask?.providerId) const [folderPath, setFolderPath] = useState(editTask?.folderPath || defaultWorkDir) const [useWorktree, setUseWorktree] = useState(editTask?.useWorktree || false) const [notifyEnabled, setNotifyEnabled] = useState(editTask?.notification?.enabled || false) const [notifyChannels, setNotifyChannels] = useState(editTask?.notification?.channels || []) const [isSubmitting, setIsSubmitting] = useState(false) // Enhanced scheduling state const [minuteInterval, setMinuteInterval] = useState(parsed?.minuteInterval || 15) const [hourInterval, setHourInterval] = useState(parsed?.hourInterval || 1) const [minuteOffset, setMinuteOffset] = useState(parsed?.minuteOffset || 0) const [selectedDays, setSelectedDays] = useState(parsed?.selectedDays || [1]) const [monthDay, setMonthDay] = useState(parsed?.monthDay || 1) const [customCron, setCustomCron] = useState(parsed?.customCron || '0 9 * * *') const showTime = ['daily', 'weekdays', 'specificDays', 'monthly'].includes(frequency) const cronValue = buildCron(frequency, time, { minuteInterval, hourInterval, minuteOffset, selectedDays, monthDay, customCron, }) const canSubmit = name.trim() && description.trim() && prompt.trim() && (frequency !== 'customCron' || isValidCron(customCron)) && (frequency !== 'specificDays' || selectedDays.length > 0) && (!notifyEnabled || notifyChannels.length > 0) const handleSubmit = async () => { if (!canSubmit) return setIsSubmitting(true) try { const payload = { name: name.trim(), description: description.trim(), cron: cronValue, prompt: prompt.trim(), model: model || undefined, providerId, permissionMode: 'bypassPermissions', folderPath: folderPath.trim() || undefined, useWorktree: useWorktree || undefined, notification: notifyEnabled && notifyChannels.length > 0 ? { enabled: true, channels: notifyChannels } : undefined, } if (isEdit) { await updateTask(editTask!.id, payload) } else { await createTask({ ...payload, enabled: true, recurring: true }) } onClose() } catch (err) { console.error(`Failed to ${isEdit ? 'update' : 'create'} task:`, err) } finally { setIsSubmitting(false) } } const selectClass = 'w-full h-10 px-3 pr-8 rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] text-sm text-[var(--color-text-primary)] outline-none focus:border-[var(--color-border-focus)] appearance-none cursor-pointer' return ( } > {/* Info banner */}
info {t('newTask.localWarning')}
setName(e.target.value)} placeholder={t('newTask.namePlaceholder')} /> setDescription(e.target.value)} placeholder={t('newTask.descPlaceholder')} /> {/* Prompt editor with embedded controls */} {/* Frequency */}
expand_more
{/* Sub-controls based on frequency */} {frequency === 'everyNMinutes' && (
expand_more
)} {frequency === 'everyNHours' && (
expand_more
expand_more
)} {frequency === 'specificDays' && ( )} {frequency === 'monthly' && (
expand_more
)} {frequency === 'customCron' && (
setCustomCron(e.target.value)} placeholder={t('newTask.cronFormatHint')} className="w-full h-10 px-3 rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] text-sm text-[var(--color-text-primary)] font-[var(--font-mono)] outline-none focus:border-[var(--color-border-focus)]" /> {t('newTask.cronFormatHint')} {customCron.trim() && !isValidCron(customCron) && ( {t('newTask.invalidCron')} )}
)} {/* Time picker — shown for daily, weekdays, specificDays, monthly */} {showTime && (
setTime(e.target.value)} className="w-auto h-10 px-3 rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] text-sm text-[var(--color-text-primary)] outline-none focus:border-[var(--color-border-focus)]" style={{ maxWidth: 120 }} />
)} {/* Notification */}
{notifyEnabled && (
{notifyChannels.length === 0 && (

warning {t('newTask.noChannelSelected')}

)}
)}
{/* Cron preview */}
schedule {frequency === 'customCron' && customCron.trim() && !isValidCron(customCron) ? t('newTask.invalidCron') : describeCron(cronValue, t) }

{t('newTask.delayNote')}

) }