Repository Public
1'use client';2import '@/lib/zod-config';3import { useQuery, useQueryClient, type QueryClient } from '@tanstack/react-query';4import type {5 AgentTask, AuditEvent, ClaudeConversation, ClusterOverview, Device, FileEntry, GitStatus, LocalModel,6 McpServerInfo, Notification, Project, SkillInfo, SystemStatus, TerminalSession,7} from '@cr/shared';8import { UserSettings } from '@cr/shared';9import { api, asList, fetchAuthStatus, qs } from './api';1011export const qk = {12 auth: ['auth'] as const,13 sessions: (archived = false) => ['sessions', archived] as const,14 projects: ['projects'] as const,15 conversations: (projectId: string) => ['conversations', projectId] as const,16 files: (projectId: string, path: string) => ['files', projectId, path] as const,17 git: (projectId: string) => ['git', projectId] as const,18 tasks: ['tasks'] as const,19 task: (id: string) => ['task', id] as const,20 cluster: ['cluster'] as const,21 models: ['models'] as const,22 skills: ['skills'] as const,23 mcp: ['mcp'] as const,24 system: ['system'] as const,25 notifications: ['notifications'] as const,26 settings: ['settings'] as const,27 devices: ['devices'] as const,28};2930export const useAuthStatus = () => useQuery({ queryKey: qk.auth, queryFn: fetchAuthStatus, staleTime: 60_000 });3132export const useSessions = (archived = false, enabled = true) =>33 useQuery({ queryKey: qk.sessions(archived), queryFn: async () => asList<TerminalSession>(await api.get(`/sessions${qs({ archived: archived ? 1 : undefined })}`)), enabled });3435export const useProjects = () => useQuery({ queryKey: qk.projects, queryFn: async () => asList<Project>(await api.get('/projects')) });3637export const useConversations = (projectId: string | null, enabled: boolean) =>38 useQuery({39 queryKey: qk.conversations(projectId ?? ''),40 queryFn: async () => asList<ClaudeConversation>(await api.get(`/projects/${projectId}/conversations`)),41 enabled: enabled && !!projectId,42 });4344export const useFiles = (projectId: string | null, path: string, enabled = true) =>45 useQuery({46 queryKey: qk.files(projectId ?? '', path),47 queryFn: async () => asList<FileEntry>(await api.get(`/projects/${projectId}/files${qs({ path })}`)),48 enabled: enabled && !!projectId,49 });5051export const useGitStatus = (projectId: string | null) =>52 useQuery({ queryKey: qk.git(projectId ?? ''), queryFn: () => api.get<GitStatus>(`/projects/${projectId}/git/status`), enabled: !!projectId, refetchOnWindowFocus: true });5354export const useTasks = () => useQuery({ queryKey: qk.tasks, queryFn: async () => asList<AgentTask>(await api.get('/tasks')) });55export const useTask = (id: string | null) => useQuery({ queryKey: qk.task(id ?? ''), queryFn: () => api.get<AgentTask>(`/tasks/${id}`), enabled: !!id });5657/** Cluster and system have no push channel: 15–20 s polling, paused while the tab is hidden. */58export const useCluster = () =>59 useQuery({ queryKey: qk.cluster, queryFn: () => api.get<ClusterOverview>('/cluster/nodes'), refetchInterval: 20_000, refetchIntervalInBackground: false });60export const useModels = () => useQuery({ queryKey: qk.models, queryFn: async () => asList<LocalModel>(await api.get('/cluster/models')) });61export const useSystem = () =>62 useQuery({ queryKey: qk.system, queryFn: () => api.get<SystemStatus>('/system/status'), refetchInterval: 15_000, refetchIntervalInBackground: false });63export const useAudit = (before: string | null, limit = 50) =>64 useQuery({ queryKey: ['audit', before, limit], queryFn: async () => asList<AuditEvent>(await api.get(`/system/audit${qs({ limit, before })}`)), placeholderData: (p) => p });6566/** `projectId` adds the project-scoped entries (.claude/ of that project) to the user-scoped ones. */67export const useSkills = (projectId?: string | null) =>68 useQuery({ queryKey: [...qk.skills, projectId ?? null], queryFn: async () => asList<SkillInfo>(await api.get(`/skills${qs({ projectId })}`)) });69export const useMcp = (projectId?: string | null) =>70 useQuery({ queryKey: [...qk.mcp, projectId ?? null], queryFn: async () => asList<McpServerInfo>(await api.get(`/mcp${qs({ projectId })}`)) });71/** Live MCP statuses: slow (up to ~45 s) — only run on demand, never blocks the configuration list. */72export const useMcpLive = (projectId: string | null | undefined, enabled: boolean, refresh: number) =>73 useQuery({74 queryKey: ['mcp-live', projectId ?? null, refresh],75 queryFn: async () => asList<McpServerInfo>(await api.get(`/mcp${qs({ projectId, live: 1, refresh: refresh > 0 ? 1 : undefined })}`)),76 enabled,77 staleTime: Infinity,78 retry: false,79 });80export const useNotifications = () => useQuery({ queryKey: qk.notifications, queryFn: async () => asList<Notification>(await api.get('/notifications')) });81export const useDevices = () => useQuery({ queryKey: qk.devices, queryFn: async () => asList<Device>(await api.get('/auth/devices')) });8283export const DEFAULT_SETTINGS: UserSettings = UserSettings.parse({});84export const useSettingsQuery = () =>85 useQuery({ queryKey: qk.settings, queryFn: async () => UserSettings.parse(await api.get('/settings')), staleTime: 5 * 60_000 });86/** Settings with defaults while loading (never blocks rendering). */87export function useSettings(): UserSettings {88 return useSettingsQuery().data ?? DEFAULT_SETTINGS;89}90export async function saveSettings(qc: QueryClient, patch: Partial<UserSettings>): Promise<UserSettings> {91 const current = qc.getQueryData<UserSettings>(qk.settings) ?? DEFAULT_SETTINGS;92 const next = { ...current, ...patch };93 qc.setQueryData(qk.settings, next);94 try {95 const saved = UserSettings.parse(await api.put('/settings', next));96 qc.setQueryData(qk.settings, saved);97 return saved;98 } catch (e) {99 qc.setQueryData(qk.settings, current);100 throw e;101 }102}103export function useSaveSettings(): (patch: Partial<UserSettings>) => Promise<UserSettings> {104 const qc = useQueryClient();105 return (patch) => saveSettings(qc, patch);106}107108export function upsertSession(qc: QueryClient, s: TerminalSession): void {109 for (const archived of [false, true]) {110 qc.setQueryData<TerminalSession[]>(qk.sessions(archived), (list) => {111 if (!list) return list;112 const rest = list.filter((x) => x.id !== s.id);113 return s.archived === archived ? [...rest, s] : rest;114 });115 }116}117export function upsertTask(qc: QueryClient, t: AgentTask): void {118 qc.setQueryData<AgentTask[]>(qk.tasks, (list) => (list ? [t, ...list.filter((x) => x.id !== t.id)].sort((a, b) => b.createdAt.localeCompare(a.createdAt)) : list));119 qc.setQueryData(qk.task(t.id), t);120}121