mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
263 lines
6.7 KiB
TypeScript
263 lines
6.7 KiB
TypeScript
// ========================================
|
|
// 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<Loop['status'], Loop[]>;
|
|
runningCount: number;
|
|
completedCount: number;
|
|
failedCount: number;
|
|
isLoading: boolean;
|
|
isFetching: boolean;
|
|
error: Error | null;
|
|
refetch: () => Promise<void>;
|
|
invalidate: () => Promise<void>;
|
|
}
|
|
|
|
/**
|
|
* 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<Loop['status'], Loop[]> = {
|
|
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<Loop>;
|
|
isCreating: boolean;
|
|
error: Error | null;
|
|
}
|
|
|
|
export function useCreateLoop(): UseCreateLoopReturn {
|
|
const queryClient = useQueryClient();
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: createLoop,
|
|
onSuccess: (newLoop) => {
|
|
queryClient.setQueryData<LoopsResponse>(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<Loop>;
|
|
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<LoopsResponse>(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<void>;
|
|
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<LoopsResponse>(loopsKeys.list());
|
|
|
|
queryClient.setQueryData<LoopsResponse>(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,
|
|
};
|
|
}
|