import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { useNavigate } from "@tanstack/react-router" import { useEffect } from "react" import { agentsApi } from "./api" import type { ScheduleUpdateRequest } from "./api" import type { AgentThread, Chunk, ImageChunk, Message } from "./types" export const agentThreadKeys = { all: ["agent-threads"] as const, detail: (threadId: string) => ["agent-threads", threadId] as const, prDiff: (threadId: string) => ["agent-threads", threadId, "pr-diff"] as const, } export const agentScheduleKeys = { all: ["agent-schedules"] as const, } // The list endpoint (`GET /threads`) and the detail endpoint // (`GET /threads/{id}`) return the same per-thread summary, so warming the // detail cache from the already-fetched list avoids a fan-out of one request // per sidebar thread. Navigation stays instant; the real (mark-viewed) fetch // fires only when a thread is actually opened. The active thread is skipped so // its live detail query stays the source of truth. export function useSeedAgentThreadDetails( threads: Array, activeThreadId?: string ) { const queryClient = useQueryClient() useEffect(() => { for (const thread of threads) { if (thread.id === activeThreadId) continue queryClient.setQueryData(agentThreadKeys.detail(thread.id), thread) } }, [activeThreadId, queryClient, threads]) } export function useAgentThreads() { return useQuery({ queryKey: agentThreadKeys.all, queryFn: () => agentsApi.listThreads(), refetchInterval: (query) => query.state.data?.some((thread) => thread.status === "running") ? 2000 : false, }) } export function useAgentThread(threadId: string) { return useQuery({ queryKey: agentThreadKeys.detail(threadId), queryFn: () => agentsApi.getThread(threadId), }) } export function useAgentThreadPrDiff(threadId: string, enabled: boolean) { return useQuery({ queryKey: agentThreadKeys.prDiff(threadId), queryFn: () => agentsApi.getThreadPrDiff(threadId), enabled, staleTime: 30_000, retry: false, }) } export function useAgentSchedules() { return useQuery({ queryKey: agentScheduleKeys.all, queryFn: () => agentsApi.listSchedules(), }) } export function useCreateAgentSchedule() { const queryClient = useQueryClient() return useMutation({ mutationFn: agentsApi.createSchedule, onSuccess: () => { queryClient.invalidateQueries({ queryKey: agentScheduleKeys.all }) }, }) } export function useUpdateAgentSchedule() { const queryClient = useQueryClient() return useMutation({ mutationFn: (vars: { scheduleId: string; body: ScheduleUpdateRequest }) => agentsApi.updateSchedule(vars.scheduleId, vars.body), onSuccess: () => { queryClient.invalidateQueries({ queryKey: agentScheduleKeys.all }) }, }) } export function useDeleteAgentSchedule() { const queryClient = useQueryClient() return useMutation({ mutationFn: agentsApi.deleteSchedule, onSuccess: () => { queryClient.invalidateQueries({ queryKey: agentScheduleKeys.all }) }, }) } export interface CreateAgentThreadVariables { prompt: string images?: Array repo?: string | null repo_explicitly_none?: boolean model_id?: string | null effort?: string | null } /** * Build the placeholder thread shown the instant a run is started from the * home page — before the server has stamped the thread record. Seeded into * the detail + list caches by `AgentsHome` so the `$threadId` route renders * immediately (the 30s `staleTime` keeps it from refetching into a 404), then * reconciled to server truth by the list's running refetch + the stream's * `onCreated` / `onCompleted` invalidations. */ export function optimisticThread( threadId: string, vars: CreateAgentThreadVariables ): AgentThread { const now = Date.now() const text = vars.prompt.trim() const repoFullName = vars.repo ?? "" const chunks: Array = [ ...(vars.images ?? []), ...(text ? [{ kind: "text", text } satisfies Chunk] : []), ] const message: Message = { id: `optimistic-user-${threadId}`, author: "user", timestamp: new Date(now).toISOString(), chunks, } return { id: threadId, title: text.slice(0, 80) || "New agent", repo: repoFullName.split("/")[1] ?? "", repoFullName, branch: "main", model: vars.model_id ?? "Default", effort: vars.effort ?? null, source: "dashboard", status: "running", viewed: true, viewedAt: now, createdAt: now, updatedAt: now, traceUrl: null, messages: message.chunks.length > 0 ? [message] : [], } } export interface SendAgentMessageVariables { content: string images?: Array model_id?: string | null effort?: string | null } export function useCancelAgentThread(threadId: string) { const queryClient = useQueryClient() return useMutation({ mutationFn: () => agentsApi.cancelThread(threadId), onSuccess: (thread) => { queryClient.setQueryData(agentThreadKeys.detail(threadId), thread) queryClient.invalidateQueries({ queryKey: agentThreadKeys.all, exact: true }) }, }) } export function useDeleteAgentThread() { const queryClient = useQueryClient() const navigate = useNavigate() return useMutation({ mutationFn: (threadId: string) => agentsApi.deleteThread(threadId), onSuccess: (_, threadId) => { queryClient.removeQueries({ queryKey: agentThreadKeys.detail(threadId) }) queryClient.invalidateQueries({ queryKey: agentThreadKeys.all, exact: true }) const path = window.location.pathname if (path.includes(`/agents/${threadId}`)) { navigate({ to: "/agents" }) } }, }) }