Files
autocoder/ui/src/hooks/useProjects.ts
Auto f31ea403ea feat: add GLM/alternative API support via environment variables
Add support for using alternative API endpoints (like Zhipu AI's GLM models)
without affecting the user's global Claude Code settings. Configuration is
done via AutoCoder's .env file.

Changes:
- Add API_ENV_VARS constant and pass through ClaudeAgentOptions.env parameter
  in client.py and all server service files (spec, expand, assistant sessions)
- Add glm_mode to settings API response to indicate when GLM is configured
- Add purple "GLM" badge in UI header when GLM mode is active
- Update setup status to accept GLM credentials as valid authentication
- Update .env.example with GLM configuration documentation
- Update README.md with AutoCoder-scoped GLM setup instructions

Supported environment variables:
- ANTHROPIC_BASE_URL: Custom API endpoint (e.g., https://api.z.ai/api/anthropic)
- ANTHROPIC_AUTH_TOKEN: API authentication token
- API_TIMEOUT_MS: Request timeout in milliseconds
- ANTHROPIC_DEFAULT_SONNET_MODEL: Model override for Sonnet
- ANTHROPIC_DEFAULT_OPUS_MODEL: Model override for Opus
- ANTHROPIC_DEFAULT_HAIKU_MODEL: Model override for Haiku

This approach routes API requests through the alternative endpoint while
keeping all Claude Code features (MCP servers, hooks, permissions) intact.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 12:25:13 +02:00

275 lines
7.5 KiB
TypeScript

/**
* React Query hooks for project data
*/
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import * as api from '../lib/api'
import type { FeatureCreate, ModelsResponse, Settings, SettingsUpdate } from '../lib/types'
// ============================================================================
// Projects
// ============================================================================
export function useProjects() {
return useQuery({
queryKey: ['projects'],
queryFn: api.listProjects,
})
}
export function useProject(name: string | null) {
return useQuery({
queryKey: ['project', name],
queryFn: () => api.getProject(name!),
enabled: !!name,
})
}
export function useCreateProject() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ name, path, specMethod }: { name: string; path: string; specMethod?: 'claude' | 'manual' }) =>
api.createProject(name, path, specMethod),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['projects'] })
},
})
}
export function useDeleteProject() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (name: string) => api.deleteProject(name),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['projects'] })
},
})
}
// ============================================================================
// Features
// ============================================================================
export function useFeatures(projectName: string | null) {
return useQuery({
queryKey: ['features', projectName],
queryFn: () => api.listFeatures(projectName!),
enabled: !!projectName,
refetchInterval: 5000, // Refetch every 5 seconds for real-time updates
})
}
export function useCreateFeature(projectName: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (feature: FeatureCreate) => api.createFeature(projectName, feature),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['features', projectName] })
},
})
}
export function useDeleteFeature(projectName: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (featureId: number) => api.deleteFeature(projectName, featureId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['features', projectName] })
},
})
}
export function useSkipFeature(projectName: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (featureId: number) => api.skipFeature(projectName, featureId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['features', projectName] })
},
})
}
// ============================================================================
// Agent
// ============================================================================
export function useAgentStatus(projectName: string | null) {
return useQuery({
queryKey: ['agent-status', projectName],
queryFn: () => api.getAgentStatus(projectName!),
enabled: !!projectName,
refetchInterval: 3000, // Poll every 3 seconds
})
}
export function useStartAgent(projectName: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (yoloMode: boolean = false) => api.startAgent(projectName, yoloMode),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['agent-status', projectName] })
},
})
}
export function useStopAgent(projectName: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: () => api.stopAgent(projectName),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['agent-status', projectName] })
},
})
}
export function usePauseAgent(projectName: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: () => api.pauseAgent(projectName),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['agent-status', projectName] })
},
})
}
export function useResumeAgent(projectName: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: () => api.resumeAgent(projectName),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['agent-status', projectName] })
},
})
}
// ============================================================================
// Setup
// ============================================================================
export function useSetupStatus() {
return useQuery({
queryKey: ['setup-status'],
queryFn: api.getSetupStatus,
staleTime: 60000, // Cache for 1 minute
})
}
export function useHealthCheck() {
return useQuery({
queryKey: ['health'],
queryFn: api.healthCheck,
retry: false,
})
}
// ============================================================================
// Filesystem
// ============================================================================
export function useListDirectory(path?: string) {
return useQuery({
queryKey: ['filesystem', 'list', path],
queryFn: () => api.listDirectory(path),
})
}
export function useCreateDirectory() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (path: string) => api.createDirectory(path),
onSuccess: (_, path) => {
// Invalidate parent directory listing
const parentPath = path.split('/').slice(0, -1).join('/') || undefined
queryClient.invalidateQueries({ queryKey: ['filesystem', 'list', parentPath] })
},
})
}
export function useValidatePath() {
return useMutation({
mutationFn: (path: string) => api.validatePath(path),
})
}
// ============================================================================
// Settings
// ============================================================================
// Default models response for placeholder (until API responds)
const DEFAULT_MODELS: ModelsResponse = {
models: [
{ id: 'claude-opus-4-5-20251101', name: 'Claude Opus 4.5' },
{ id: 'claude-sonnet-4-5-20250929', name: 'Claude Sonnet 4.5' },
],
default: 'claude-opus-4-5-20251101',
}
const DEFAULT_SETTINGS: Settings = {
yolo_mode: false,
model: 'claude-opus-4-5-20251101',
glm_mode: false,
}
export function useAvailableModels() {
return useQuery({
queryKey: ['available-models'],
queryFn: api.getAvailableModels,
staleTime: 300000, // Cache for 5 minutes - models don't change often
retry: 1,
placeholderData: DEFAULT_MODELS,
})
}
export function useSettings() {
return useQuery({
queryKey: ['settings'],
queryFn: api.getSettings,
staleTime: 60000, // Cache for 1 minute
retry: 1,
placeholderData: DEFAULT_SETTINGS,
})
}
export function useUpdateSettings() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (settings: SettingsUpdate) => api.updateSettings(settings),
onMutate: async (newSettings) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['settings'] })
// Snapshot previous value
const previous = queryClient.getQueryData<Settings>(['settings'])
// Optimistically update
queryClient.setQueryData<Settings>(['settings'], (old) => ({
...DEFAULT_SETTINGS,
...old,
...newSettings,
}))
return { previous }
},
onError: (_err, _newSettings, context) => {
// Rollback on error
if (context?.previous) {
queryClient.setQueryData(['settings'], context.previous)
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['settings'] })
},
})
}