// ======================================== // useLoops Hook // ======================================== // TanStack Query hooks for loops with real-time updates import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { fetchLoops, fetchLoop, createLoop, updateLoopStatus, deleteLoop, type Loop, type LoopsResponse, } from '../lib/api'; // Query key factory export const loopsKeys = { all: ['loops'] as const, lists: () => [...loopsKeys.all, 'list'] as const, list: (filters?: LoopsFilter) => [...loopsKeys.lists(), filters] as const, details: () => [...loopsKeys.all, 'detail'] as const, detail: (id: string) => [...loopsKeys.details(), id] as const, }; // Default stale time: 10 seconds (loops update frequently) const STALE_TIME = 10 * 1000; export interface LoopsFilter { status?: Loop['status'][]; search?: string; } export interface UseLoopsOptions { filter?: LoopsFilter; staleTime?: number; enabled?: boolean; refetchInterval?: number; } export interface UseLoopsReturn { loops: Loop[]; loopsByStatus: Record; runningCount: number; completedCount: number; failedCount: number; isLoading: boolean; isFetching: boolean; error: Error | null; refetch: () => Promise; invalidate: () => Promise; } /** * Hook for fetching and filtering loops */ export function useLoops(options: UseLoopsOptions = {}): UseLoopsReturn { const { filter, staleTime = STALE_TIME, enabled = true, refetchInterval = 0 } = options; const queryClient = useQueryClient(); const query = useQuery({ queryKey: loopsKeys.list(filter), queryFn: fetchLoops, staleTime, enabled, refetchInterval: refetchInterval > 0 ? refetchInterval : false, retry: 2, }); const allLoops = query.data?.loops ?? []; // Apply filters const filteredLoops = (() => { let loops = allLoops; if (filter?.status && filter.status.length > 0) { loops = loops.filter((l) => filter.status!.includes(l.status)); } if (filter?.search) { const searchLower = filter.search.toLowerCase(); loops = loops.filter( (l) => l.id.toLowerCase().includes(searchLower) || l.name?.toLowerCase().includes(searchLower) || l.prompt?.toLowerCase().includes(searchLower) ); } return loops; })(); // Group by status for Kanban const loopsByStatus: Record = { created: [], running: [], paused: [], completed: [], failed: [], }; for (const loop of allLoops) { loopsByStatus[loop.status].push(loop); } const refetch = async () => { await query.refetch(); }; const invalidate = async () => { await queryClient.invalidateQueries({ queryKey: loopsKeys.all }); }; return { loops: filteredLoops, loopsByStatus, runningCount: loopsByStatus.running.length, completedCount: loopsByStatus.completed.length, failedCount: loopsByStatus.failed.length, isLoading: query.isLoading, isFetching: query.isFetching, error: query.error, refetch, invalidate, }; } /** * Hook for fetching a single loop */ export function useLoop(loopId: string, options: { enabled?: boolean } = {}) { return useQuery({ queryKey: loopsKeys.detail(loopId), queryFn: () => fetchLoop(loopId), enabled: options.enabled ?? !!loopId, staleTime: STALE_TIME, }); } // ========== Mutations ========== export interface UseCreateLoopReturn { createLoop: (input: { prompt: string; tool?: string; mode?: string }) => Promise; isCreating: boolean; error: Error | null; } export function useCreateLoop(): UseCreateLoopReturn { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: createLoop, onSuccess: (newLoop) => { queryClient.setQueryData(loopsKeys.list(), (old) => { if (!old) return { loops: [newLoop], total: 1 }; return { loops: [newLoop, ...old.loops], total: old.total + 1, }; }); }, }); return { createLoop: mutation.mutateAsync, isCreating: mutation.isPending, error: mutation.error, }; } export interface UseUpdateLoopStatusReturn { updateStatus: (loopId: string, action: 'pause' | 'resume' | 'stop') => Promise; isUpdating: boolean; error: Error | null; } export function useUpdateLoopStatus(): UseUpdateLoopStatusReturn { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: ({ loopId, action }: { loopId: string; action: 'pause' | 'resume' | 'stop' }) => updateLoopStatus(loopId, action), onSuccess: (updatedLoop) => { queryClient.setQueryData(loopsKeys.list(), (old) => { if (!old) return old; return { ...old, loops: old.loops.map((l) => (l.id === updatedLoop.id ? updatedLoop : l)), }; }); queryClient.setQueryData(loopsKeys.detail(updatedLoop.id), updatedLoop); }, }); return { updateStatus: (loopId, action) => mutation.mutateAsync({ loopId, action }), isUpdating: mutation.isPending, error: mutation.error, }; } export interface UseDeleteLoopReturn { deleteLoop: (loopId: string) => Promise; isDeleting: boolean; error: Error | null; } export function useDeleteLoop(): UseDeleteLoopReturn { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: deleteLoop, onMutate: async (loopId) => { await queryClient.cancelQueries({ queryKey: loopsKeys.all }); const previousLoops = queryClient.getQueryData(loopsKeys.list()); queryClient.setQueryData(loopsKeys.list(), (old) => { if (!old) return old; return { ...old, loops: old.loops.filter((l) => l.id !== loopId), total: old.total - 1, }; }); return { previousLoops }; }, onError: (_error, _loopId, context) => { if (context?.previousLoops) { queryClient.setQueryData(loopsKeys.list(), context.previousLoops); } }, onSettled: () => { queryClient.invalidateQueries({ queryKey: loopsKeys.all }); }, }); return { deleteLoop: mutation.mutateAsync, isDeleting: mutation.isPending, error: mutation.error, }; } /** * Combined hook for all loop mutations */ export function useLoopMutations() { const create = useCreateLoop(); const update = useUpdateLoopStatus(); const remove = useDeleteLoop(); return { createLoop: create.createLoop, updateStatus: update.updateStatus, deleteLoop: remove.deleteLoop, isCreating: create.isCreating, isUpdating: update.isUpdating, isDeleting: remove.isDeleting, isMutating: create.isCreating || update.isUpdating || remove.isDeleting, }; }