import { useEffect, useMemo, useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Link, useNavigate } from "@/lib/router"; import { Repeat, Plus, Play, Clock3, Webhook } from "lucide-react"; import { routinesApi } from "../api/routines"; import { agentsApi } from "../api/agents"; import { projectsApi } from "../api/projects"; import { useCompany } from "../context/CompanyContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { useToast } from "../context/ToastContext"; import { queryKeys } from "../lib/queryKeys"; import { EmptyState } from "../components/EmptyState"; import { PageSkeleton } from "../components/PageSkeleton"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Badge } from "@/components/ui/badge"; import { timeAgo } from "../lib/timeAgo"; const priorities = ["critical", "high", "medium", "low"]; const concurrencyPolicies = ["coalesce_if_active", "always_enqueue", "skip_if_active"]; const catchUpPolicies = ["skip_missed", "enqueue_missed_with_cap"]; const concurrencyPolicyDescriptions: Record = { coalesce_if_active: "If a run is already active, keep just one follow-up run queued.", always_enqueue: "Queue every trigger occurrence, even if the routine is already running.", skip_if_active: "Drop new trigger occurrences while a run is still active.", }; const catchUpPolicyDescriptions: Record = { skip_missed: "Ignore windows that were missed while the scheduler or routine was paused.", enqueue_missed_with_cap: "Catch up missed schedule windows with a capped backlog after recovery.", }; function triggerIcon(kind: string) { if (kind === "schedule") return ; if (kind === "webhook") return ; return ; } export function Routines() { const { selectedCompanyId } = useCompany(); const { setBreadcrumbs } = useBreadcrumbs(); const queryClient = useQueryClient(); const navigate = useNavigate(); const { pushToast } = useToast(); const [runningRoutineId, setRunningRoutineId] = useState(null); const [draft, setDraft] = useState({ title: "", description: "", projectId: "", assigneeAgentId: "", priority: "medium", concurrencyPolicy: "coalesce_if_active", catchUpPolicy: "skip_missed", }); useEffect(() => { setBreadcrumbs([{ label: "Routines" }]); }, [setBreadcrumbs]); const { data: routines, isLoading, error } = useQuery({ queryKey: queryKeys.routines.list(selectedCompanyId!), queryFn: () => routinesApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: agents } = useQuery({ queryKey: queryKeys.agents.list(selectedCompanyId!), queryFn: () => agentsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: projects } = useQuery({ queryKey: queryKeys.projects.list(selectedCompanyId!), queryFn: () => projectsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const createRoutine = useMutation({ mutationFn: () => routinesApi.create(selectedCompanyId!, { ...draft, description: draft.description.trim() || null, }), onSuccess: async (routine) => { setDraft({ title: "", description: "", projectId: "", assigneeAgentId: "", priority: "medium", concurrencyPolicy: "coalesce_if_active", catchUpPolicy: "skip_missed", }); await queryClient.invalidateQueries({ queryKey: queryKeys.routines.list(selectedCompanyId!) }); pushToast({ title: "Routine created", body: "Add the first trigger to turn it into a live workflow.", tone: "success", }); navigate(`/routines/${routine.id}?tab=triggers`); }, }); const runRoutine = useMutation({ mutationFn: (id: string) => routinesApi.run(id), onMutate: (id) => { setRunningRoutineId(id); }, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: queryKeys.routines.list(selectedCompanyId!) }); }, onSettled: () => { setRunningRoutineId(null); }, onError: (error) => { pushToast({ title: "Routine run failed", body: error instanceof Error ? error.message : "Paperclip could not start the routine run.", tone: "error", }); }, }); if (!selectedCompanyId) { return ; } if (isLoading) { return ; } const agentName = new Map((agents ?? []).map((agent) => [agent.id, agent.name])); const projectName = new Map((projects ?? []).map((project) => [project.id, project.name])); return (
Create Routine Define recurring work once, then add the first trigger on the next screen to make it live.
setDraft((current) => ({ ...current, title: event.target.value }))} placeholder="Review the last 24 hours of merged code" />