/** * Schedule Modal Component * * Modal for managing agent schedules (create, edit, delete). */ import { useState, useEffect, useRef } from 'react' import { Clock, GitBranch, Trash2 } from 'lucide-react' import { useSchedules, useCreateSchedule, useDeleteSchedule, useToggleSchedule, } from '../hooks/useSchedules' import { utcToLocalWithDayShift, localToUTCWithDayShift, adjustDaysForDayShift, formatDuration, DAYS, isDayActive, toggleDay, } from '../lib/timeUtils' import type { ScheduleCreate } from '../lib/types' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Alert, AlertDescription } from '@/components/ui/alert' import { Card, CardContent } from '@/components/ui/card' import { Checkbox } from '@/components/ui/checkbox' import { Separator } from '@/components/ui/separator' interface ScheduleModalProps { projectName: string isOpen: boolean onClose: () => void } export function ScheduleModal({ projectName, isOpen, onClose }: ScheduleModalProps) { const modalRef = useRef(null) const firstFocusableRef = useRef(null) // Queries and mutations const { data: schedulesData, isLoading } = useSchedules(projectName) const createSchedule = useCreateSchedule(projectName) const deleteSchedule = useDeleteSchedule(projectName) const toggleSchedule = useToggleSchedule(projectName) // Form state for new schedule const [newSchedule, setNewSchedule] = useState({ start_time: '22:00', duration_minutes: 240, days_of_week: 31, // Weekdays by default enabled: true, yolo_mode: false, model: null, max_concurrency: 3, }) const [error, setError] = useState(null) // Focus trap useEffect(() => { if (isOpen && firstFocusableRef.current) { firstFocusableRef.current.focus() } }, [isOpen]) const schedules = schedulesData?.schedules || [] const handleCreateSchedule = async () => { try { setError(null) // Validate if (newSchedule.days_of_week === 0) { setError('Please select at least one day') return } // Validate duration if (newSchedule.duration_minutes < 1 || newSchedule.duration_minutes > 1440) { setError('Duration must be between 1 and 1440 minutes') return } // Convert local time to UTC and get day shift const { time: utcTime, dayShift } = localToUTCWithDayShift(newSchedule.start_time) // Adjust days_of_week based on day shift const adjustedDays = adjustDaysForDayShift(newSchedule.days_of_week, dayShift) const scheduleToCreate = { ...newSchedule, start_time: utcTime, days_of_week: adjustedDays, } await createSchedule.mutateAsync(scheduleToCreate) // Reset form setNewSchedule({ start_time: '22:00', duration_minutes: 240, days_of_week: 31, enabled: true, yolo_mode: false, model: null, max_concurrency: 3, }) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to create schedule') } } const handleToggleSchedule = async (scheduleId: number, enabled: boolean) => { try { setError(null) await toggleSchedule.mutateAsync({ scheduleId, enabled: !enabled }) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to toggle schedule') } } const handleDeleteSchedule = async (scheduleId: number) => { if (!confirm('Are you sure you want to delete this schedule?')) return try { setError(null) await deleteSchedule.mutateAsync(scheduleId) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to delete schedule') } } const handleToggleDay = (dayBit: number) => { setNewSchedule((prev) => ({ ...prev, days_of_week: toggleDay(prev.days_of_week, dayBit), })) } return ( !open && onClose()}> {/* Header */} Agent Schedules
{/* Error display */} {error && ( {error} )} {/* Loading state */} {isLoading && (
Loading schedules...
)} {/* Existing schedules */} {!isLoading && schedules.length > 0 && (
{schedules.map((schedule) => { // Convert UTC time to local and get day shift for display const { time: localTime, dayShift } = utcToLocalWithDayShift(schedule.start_time) const duration = formatDuration(schedule.duration_minutes) const displayDays = adjustDaysForDayShift(schedule.days_of_week, dayShift) return (
{/* Time and duration */}
{localTime} for {duration}
{/* Days */}
{DAYS.map((day) => { const isActive = isDayActive(displayDays, day.bit) return ( {day.label} ) })}
{/* Metadata */}
{schedule.yolo_mode && ( YOLO mode )} {schedule.max_concurrency}x {schedule.model && Model: {schedule.model}} {schedule.crash_count > 0 && ( Crashes: {schedule.crash_count} )}
{/* Actions */}
{/* Enable/disable toggle */} {/* Delete button */}
) })}
)} {/* Empty state */} {!isLoading && schedules.length === 0 && (

No schedules configured yet

)} {/* Add new schedule form */}

Add New Schedule

{/* Time and duration */}
setNewSchedule((prev) => ({ ...prev, start_time: e.target.value })) } />
{ const parsed = parseInt(e.target.value, 10) const value = isNaN(parsed) ? 1 : Math.max(1, Math.min(1440, parsed)) setNewSchedule((prev) => ({ ...prev, duration_minutes: value, })) }} />

{formatDuration(newSchedule.duration_minutes)}

{/* Days of week */}
{DAYS.map((day) => { const isActive = isDayActive(newSchedule.days_of_week, day.bit) return ( ) })}
{/* YOLO mode toggle */}
setNewSchedule((prev) => ({ ...prev, yolo_mode: checked === true })) } />
{/* Concurrency slider */}
1 ? 'text-primary' : 'text-muted-foreground'} /> setNewSchedule((prev) => ({ ...prev, max_concurrency: Number(e.target.value) })) } className="flex-1 h-2 accent-primary cursor-pointer" /> {newSchedule.max_concurrency}x

Run {newSchedule.max_concurrency} agent{newSchedule.max_concurrency > 1 ? 's' : ''} in parallel for faster feature completion

{/* Model selection (optional) */}
setNewSchedule((prev) => ({ ...prev, model: e.target.value || null })) } />
{/* Actions */}
) }