feat(web): integrate TanStack Query for state management

Replace manual state management with TanStack Query (React Query) for more robust server state handling. This refactoring introduces:

- New hooks for queries: useSessions, useSession, useMessages, useMachines
- New hooks for mutations: useSendMessage, useSessionActions, useSpawnSession
- Centralized query client with optimized configuration (5s staleTime, disabled window focus refetch)
- Query key factory for consistent cache invalidation
- Improved message synchronization via socket events with cache updates
- Optimistic updates for message sending with retry capability
- Simplified App.tsx by removing manual state management logic
- Integrated React Query devtools in development mode

This enables automatic cache management, better error handling, and a foundation for more sophisticated data fetching patterns.
This commit is contained in:
weishu
2025-12-19 18:25:13 +08:00
parent 10bed01358
commit 3a7272d03d
17 changed files with 673 additions and 355 deletions
@@ -0,0 +1,73 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import type { ApiClient } from '@/api/client'
import type { ModelMode, PermissionMode } from '@/types/api'
import { queryKeys } from '@/lib/query-keys'
type PermissionModeValue = 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan'
type ModelModeValue = 'default' | 'sonnet' | 'opus'
function toPermissionMode(mode: PermissionMode): PermissionModeValue {
if (mode === 'acceptEdits' || mode === 'bypassPermissions' || mode === 'plan') {
return mode
}
return 'default'
}
function toModelMode(mode: ModelMode): ModelModeValue {
if (mode === 'sonnet' || mode === 'opus') {
return mode
}
return 'default'
}
export function useSessionActions(api: ApiClient | null, sessionId: string | null): {
abortSession: () => Promise<void>
setPermissionMode: (mode: PermissionMode) => Promise<void>
setModelMode: (mode: ModelMode) => Promise<void>
isPending: boolean
} {
const queryClient = useQueryClient()
const invalidateSession = async () => {
if (!sessionId) return
await queryClient.invalidateQueries({ queryKey: queryKeys.session(sessionId) })
await queryClient.invalidateQueries({ queryKey: queryKeys.sessions })
}
const abortMutation = useMutation({
mutationFn: async () => {
if (!api || !sessionId) {
throw new Error('Session unavailable')
}
await api.abortSession(sessionId)
},
onSuccess: () => void invalidateSession(),
})
const permissionMutation = useMutation({
mutationFn: async (mode: PermissionMode) => {
if (!api || !sessionId) {
throw new Error('Session unavailable')
}
await api.setPermissionMode(sessionId, toPermissionMode(mode))
},
onSuccess: () => void invalidateSession(),
})
const modelMutation = useMutation({
mutationFn: async (mode: ModelMode) => {
if (!api || !sessionId) {
throw new Error('Session unavailable')
}
await api.setModelMode(sessionId, toModelMode(mode))
},
onSuccess: () => void invalidateSession(),
})
return {
abortSession: abortMutation.mutateAsync,
setPermissionMode: permissionMutation.mutateAsync,
setModelMode: modelMutation.mutateAsync,
isPending: abortMutation.isPending || permissionMutation.isPending || modelMutation.isPending,
}
}