feat: add useApiSettings hook for managing API settings, including providers, endpoints, cache, and model pools

- Implemented hooks for CRUD operations on providers and endpoints.
- Added cache management hooks for cache stats and settings.
- Introduced model pool management hooks for high availability and load balancing.
- Created localization files for English and Chinese translations of API settings.
This commit is contained in:
catlog22
2026-02-01 23:14:55 +08:00
parent b76424feef
commit e5252f8a77
27 changed files with 4370 additions and 201 deletions

View File

@@ -105,8 +105,12 @@ export function useActiveCliExecutions(
refetchInterval: number = 5000
) {
const upsertExecution = useCliStreamStore(state => state.upsertExecution);
const removeExecution = useCliStreamStore(state => state.removeExecution);
const executions = useCliStreamStore(state => state.executions);
const setCurrentExecution = useCliStreamStore(state => state.setCurrentExecution);
const markExecutionClosedByUser = useCliStreamStore(state => state.markExecutionClosedByUser);
const isExecutionClosedByUser = useCliStreamStore(state => state.isExecutionClosedByUser);
const cleanupUserClosedExecutions = useCliStreamStore(state => state.cleanupUserClosedExecutions);
return useQuery({
queryKey: ACTIVE_CLI_EXECUTIONS_QUERY_KEY,
@@ -117,11 +121,33 @@ export function useActiveCliExecutions(
}
const data: ActiveCliExecutionsResponse = await response.json();
// Get server execution IDs
const serverIds = new Set(data.executions.map(e => e.id));
// Clean up userClosedExecutions - remove those no longer on server
cleanupUserClosedExecutions(serverIds);
// Remove executions that are no longer on server and were closed by user
for (const [id, exec] of Object.entries(executions)) {
if (isExecutionClosedByUser(id)) {
// User closed this execution, remove from local state
removeExecution(id);
} else if (exec.status !== 'running' && !serverIds.has(id) && exec.recovered) {
// Not running, not on server, and was recovered (not user-created)
removeExecution(id);
}
}
// Process executions and sync to store
let hasNewExecution = false;
const now = Date.now();
for (const exec of data.executions) {
// Skip if user closed this execution
if (isExecutionClosedByUser(exec.id)) {
continue;
}
const existing = executions[exec.id];
const historicalOutput = parseHistoricalOutput(exec.output || '', exec.startTime);
@@ -175,7 +201,7 @@ export function useActiveCliExecutions(
// Set current execution to first running execution if none selected
if (hasNewExecution) {
const runningExec = data.executions.find(e => e.status === 'running');
const runningExec = data.executions.find(e => e.status === 'running' && !isExecutionClosedByUser(e.id));
if (runningExec && !executions[runningExec.id]) {
setCurrentExecution(runningExec.id);
}

View File

@@ -0,0 +1,623 @@
// ========================================
// useApiSettings Hook
// ========================================
// TanStack Query hooks for API Settings management
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
fetchProviders,
createProvider,
updateProvider,
deleteProvider,
testProvider,
testProviderKey,
getProviderHealthStatus,
triggerProviderHealthCheck,
fetchEndpoints,
createEndpoint,
updateEndpoint,
deleteEndpoint,
fetchCacheStats,
clearCache,
updateCacheSettings,
fetchModelPools,
fetchModelPool,
createModelPool,
updateModelPool,
deleteModelPool,
getAvailableModelsForPool,
discoverModelsForPool,
fetchApiConfig,
syncApiConfig,
previewYamlConfig,
checkCcwLitellmStatus,
installCcwLitellm,
uninstallCcwLitellm,
type ProviderCredential,
type CustomEndpoint,
type CacheStats,
type GlobalCacheSettings,
type ModelPoolConfig,
type ModelPoolType,
type DiscoveredProvider,
} from '../lib/api';
// Query key factory
export const apiSettingsKeys = {
all: ['apiSettings'] as const,
providers: () => [...apiSettingsKeys.all, 'providers'] as const,
provider: (id: string) => [...apiSettingsKeys.providers(), id] as const,
endpoints: () => [...apiSettingsKeys.all, 'endpoints'] as const,
endpoint: (id: string) => [...apiSettingsKeys.endpoints(), id] as const,
cache: () => [...apiSettingsKeys.all, 'cache'] as const,
modelPools: () => [...apiSettingsKeys.all, 'modelPools'] as const,
modelPool: (id: string) => [...apiSettingsKeys.modelPools(), id] as const,
ccwLitellm: () => [...apiSettingsKeys.all, 'ccwLitellm'] as const,
};
const STALE_TIME = 2 * 60 * 1000;
// ========================================
// Provider Hooks
// ========================================
export interface UseProvidersOptions {
staleTime?: number;
enabled?: boolean;
}
export interface UseProvidersReturn {
providers: ProviderCredential[];
totalCount: number;
isLoading: boolean;
isFetching: boolean;
error: Error | null;
refetch: () => Promise<void>;
invalidate: () => Promise<void>;
}
export function useProviders(options: UseProvidersOptions = {}): UseProvidersReturn {
const { staleTime = STALE_TIME, enabled = true } = options;
const queryClient = useQueryClient();
const query = useQuery({
queryKey: apiSettingsKeys.providers(),
queryFn: fetchProviders,
staleTime,
enabled,
retry: 2,
});
const providers = query.data?.providers ?? [];
const refetch = async () => {
await query.refetch();
};
const invalidate = async () => {
await queryClient.invalidateQueries({ queryKey: apiSettingsKeys.providers() });
};
return {
providers,
totalCount: providers.length,
isLoading: query.isLoading,
isFetching: query.isFetching,
error: query.error,
refetch,
invalidate,
};
}
export function useCreateProvider() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (provider: Omit<ProviderCredential, 'id' | 'createdAt' | 'updatedAt'>) =>
createProvider(provider),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.providers() });
},
});
return {
createProvider: mutation.mutateAsync,
isCreating: mutation.isPending,
error: mutation.error,
};
}
export function useUpdateProvider() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: ({ providerId, updates }: { providerId: string; updates: Partial<Omit<ProviderCredential, 'id' | 'createdAt' | 'updatedAt'>> }) =>
updateProvider(providerId, updates),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.providers() });
},
});
return {
updateProvider: (providerId: string, updates: Partial<Omit<ProviderCredential, 'id' | 'createdAt' | 'updatedAt'>>) =>
mutation.mutateAsync({ providerId, updates }),
isUpdating: mutation.isPending,
error: mutation.error,
};
}
export function useDeleteProvider() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (providerId: string) => deleteProvider(providerId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.providers() });
},
});
return {
deleteProvider: mutation.mutateAsync,
isDeleting: mutation.isPending,
error: mutation.error,
};
}
export function useTestProvider() {
const mutation = useMutation({
mutationFn: (providerId: string) => testProvider(providerId),
});
return {
testProvider: mutation.mutateAsync,
isTesting: mutation.isPending,
error: mutation.error,
};
}
export function useTestProviderKey() {
const mutation = useMutation({
mutationFn: ({ providerId, keyId }: { providerId: string; keyId: string }) =>
testProviderKey(providerId, keyId),
});
return {
testProviderKey: mutation.mutateAsync,
isTesting: mutation.isPending,
error: mutation.error,
};
}
export function useProviderHealthStatus(providerId: string) {
return useQuery({
queryKey: [...apiSettingsKeys.provider(providerId), 'health'],
queryFn: () => getProviderHealthStatus(providerId),
enabled: !!providerId,
staleTime: 30000, // 30 seconds
refetchInterval: 60000, // Refetch every minute
});
}
export function useTriggerProviderHealthCheck() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (providerId: string) => triggerProviderHealthCheck(providerId),
onSuccess: (_, providerId) => {
queryClient.invalidateQueries({ queryKey: [...apiSettingsKeys.provider(providerId), 'health'] });
},
});
return {
triggerHealthCheck: mutation.mutateAsync,
isChecking: mutation.isPending,
error: mutation.error,
};
}
// ========================================
// Endpoint Hooks
// ========================================
export interface UseEndpointsOptions {
staleTime?: number;
enabled?: boolean;
}
export interface UseEndpointsReturn {
endpoints: CustomEndpoint[];
totalCount: number;
cachedCount: number;
isLoading: boolean;
isFetching: boolean;
error: Error | null;
refetch: () => Promise<void>;
invalidate: () => Promise<void>;
}
export function useEndpoints(options: UseEndpointsOptions = {}): UseEndpointsReturn {
const { staleTime = STALE_TIME, enabled = true } = options;
const queryClient = useQueryClient();
const query = useQuery({
queryKey: apiSettingsKeys.endpoints(),
queryFn: fetchEndpoints,
staleTime,
enabled,
retry: 2,
});
const endpoints = query.data?.endpoints ?? [];
const cachedEndpoints = endpoints.filter((e) => e.cacheStrategy.enabled);
const refetch = async () => {
await query.refetch();
};
const invalidate = async () => {
await queryClient.invalidateQueries({ queryKey: apiSettingsKeys.endpoints() });
};
return {
endpoints,
totalCount: endpoints.length,
cachedCount: cachedEndpoints.length,
isLoading: query.isLoading,
isFetching: query.isFetching,
error: query.error,
refetch,
invalidate,
};
}
export function useCreateEndpoint() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (endpoint: Omit<CustomEndpoint, 'createdAt' | 'updatedAt'>) =>
createEndpoint(endpoint),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.endpoints() });
},
});
return {
createEndpoint: mutation.mutateAsync,
isCreating: mutation.isPending,
error: mutation.error,
};
}
export function useUpdateEndpoint() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: ({ endpointId, updates }: { endpointId: string; updates: Partial<Omit<CustomEndpoint, 'id' | 'createdAt' | 'updatedAt'>> }) =>
updateEndpoint(endpointId, updates),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.endpoints() });
},
});
return {
updateEndpoint: (endpointId: string, updates: Partial<Omit<CustomEndpoint, 'id' | 'createdAt' | 'updatedAt'>>) =>
mutation.mutateAsync({ endpointId, updates }),
isUpdating: mutation.isPending,
error: mutation.error,
};
}
export function useDeleteEndpoint() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (endpointId: string) => deleteEndpoint(endpointId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.endpoints() });
},
});
return {
deleteEndpoint: mutation.mutateAsync,
isDeleting: mutation.isPending,
error: mutation.error,
};
}
// ========================================
// Cache Hooks
// ========================================
export interface UseCacheStatsOptions {
staleTime?: number;
enabled?: boolean;
}
export interface UseCacheStatsReturn {
stats: CacheStats | null;
isLoading: boolean;
isFetching: boolean;
error: Error | null;
refetch: () => Promise<void>;
}
export function useCacheStats(options: UseCacheStatsOptions = {}): UseCacheStatsReturn {
const { staleTime = 30000, enabled = true } = options; // 30 seconds stale time for cache stats
const query = useQuery({
queryKey: apiSettingsKeys.cache(),
queryFn: fetchCacheStats,
staleTime,
enabled,
retry: 2,
});
const refetch = async () => {
await query.refetch();
};
return {
stats: query.data ?? null,
isLoading: query.isLoading,
isFetching: query.isFetching,
error: query.error,
refetch,
};
}
export function useClearCache() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: () => clearCache(),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.cache() });
},
});
return {
clearCache: mutation.mutateAsync,
isClearing: mutation.isPending,
error: mutation.error,
};
}
export function useUpdateCacheSettings() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (settings: Partial<{ enabled: boolean; cacheDir: string; maxTotalSizeMB: number }>) =>
updateCacheSettings(settings),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.cache() });
},
});
return {
updateCacheSettings: mutation.mutateAsync,
isUpdating: mutation.isPending,
error: mutation.error,
};
}
// ========================================
// Model Pool Hooks
// ========================================
export interface UseModelPoolsOptions {
staleTime?: number;
enabled?: boolean;
}
export interface UseModelPoolsReturn {
pools: ModelPoolConfig[];
totalCount: number;
enabledCount: number;
isLoading: boolean;
isFetching: boolean;
error: Error | null;
refetch: () => Promise<void>;
invalidate: () => Promise<void>;
}
export function useModelPools(options: UseModelPoolsOptions = {}): UseModelPoolsReturn {
const { staleTime = STALE_TIME, enabled = true } = options;
const queryClient = useQueryClient();
const query = useQuery({
queryKey: apiSettingsKeys.modelPools(),
queryFn: fetchModelPools,
staleTime,
enabled,
retry: 2,
});
const pools = query.data?.pools ?? [];
const enabledPools = pools.filter((p) => p.enabled);
const refetch = async () => {
await query.refetch();
};
const invalidate = async () => {
await queryClient.invalidateQueries({ queryKey: apiSettingsKeys.modelPools() });
};
return {
pools,
totalCount: pools.length,
enabledCount: enabledPools.length,
isLoading: query.isLoading,
isFetching: query.isFetching,
error: query.error,
refetch,
invalidate,
};
}
export function useModelPool(poolId: string) {
return useQuery({
queryKey: apiSettingsKeys.modelPool(poolId),
queryFn: () => fetchModelPool(poolId),
enabled: !!poolId,
staleTime: STALE_TIME,
retry: 2,
});
}
export function useCreateModelPool() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (pool: Omit<ModelPoolConfig, 'id'>) => createModelPool(pool),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.modelPools() });
},
});
return {
createModelPool: mutation.mutateAsync,
isCreating: mutation.isPending,
error: mutation.error,
};
}
export function useUpdateModelPool() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: ({ poolId, updates }: { poolId: string; updates: Partial<ModelPoolConfig> }) =>
updateModelPool(poolId, updates),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.modelPools() });
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.modelPool(variables.poolId) });
},
});
return {
updateModelPool: (poolId: string, updates: Partial<ModelPoolConfig>) =>
mutation.mutateAsync({ poolId, updates }),
isUpdating: mutation.isPending,
error: mutation.error,
};
}
export function useDeleteModelPool() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (poolId: string) => deleteModelPool(poolId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.modelPools() });
},
});
return {
deleteModelPool: mutation.mutateAsync,
isDeleting: mutation.isPending,
error: mutation.error,
};
}
export function useAvailableModelsForPool(modelType: ModelPoolType) {
return useQuery({
queryKey: [...apiSettingsKeys.modelPools(), 'available', modelType],
queryFn: () => getAvailableModelsForPool(modelType),
enabled: !!modelType,
staleTime: STALE_TIME,
});
}
export function useDiscoverModelsForPool() {
const mutation = useMutation({
mutationFn: ({ modelType, targetModel }: { modelType: ModelPoolType; targetModel: string }) =>
discoverModelsForPool(modelType, targetModel),
});
return {
discoverModels: (modelType: ModelPoolType, targetModel: string) =>
mutation.mutateAsync({ modelType, targetModel }),
isDiscovering: mutation.isPending,
error: mutation.error,
data: mutation.data,
};
}
// ========================================
// Config Hooks
// ========================================
export function useApiConfig() {
return useQuery({
queryKey: [...apiSettingsKeys.all, 'config'],
queryFn: fetchApiConfig,
staleTime: STALE_TIME,
});
}
export function useSyncApiConfig() {
return useMutation({
mutationFn: () => syncApiConfig(),
});
}
export function usePreviewYamlConfig() {
return useMutation({
mutationFn: () => previewYamlConfig(),
});
}
// ========================================
// CCW-LiteLLM Package Hooks
// ========================================
export interface UseCcwLitellmStatusOptions {
staleTime?: number;
enabled?: boolean;
refresh?: boolean;
}
export function useCcwLitellmStatus(options: UseCcwLitellmStatusOptions = {}) {
const { staleTime = 5 * 60 * 1000, enabled = true, refresh = false } = options;
return useQuery({
queryKey: [...apiSettingsKeys.ccwLitellm(), 'status', refresh],
queryFn: () => checkCcwLitellmStatus(refresh),
staleTime,
enabled,
});
}
export function useInstallCcwLitellm() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: () => installCcwLitellm(),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.ccwLitellm() });
},
});
return {
install: mutation.mutateAsync,
isInstalling: mutation.isPending,
error: mutation.error,
};
}
export function useUninstallCcwLitellm() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: () => uninstallCcwLitellm(),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.ccwLitellm() });
},
});
return {
uninstall: mutation.mutateAsync,
isUninstalling: mutation.isPending,
error: mutation.error,
};
}

View File

@@ -26,6 +26,14 @@ import {
resetCodexLensGpu,
fetchCodexLensIgnorePatterns,
updateCodexLensIgnorePatterns,
searchCodexLens,
searchFilesCodexLens,
searchSymbolCodexLens,
fetchCodexLensIndexes,
rebuildCodexLensIndex,
updateCodexLensIndex,
cancelCodexLensIndexing,
checkCodexLensIndexingStatus,
type CodexLensDashboardInitResponse,
type CodexLensVenvStatus,
type CodexLensConfig,
@@ -39,6 +47,11 @@ import {
type CodexLensUpdateEnvRequest,
type CodexLensUpdateIgnorePatternsRequest,
type CodexLensWorkspaceStatus,
type CodexLensSearchParams,
type CodexLensSearchResponse,
type CodexLensSymbolSearchResponse,
type CodexLensIndexesResponse,
type CodexLensIndexingStatusResponse,
} from '../lib/api';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
@@ -56,6 +69,11 @@ export const codexLensKeys = {
gpuList: () => [...codexLensKeys.gpu(), 'list'] as const,
gpuDetect: () => [...codexLensKeys.gpu(), 'detect'] as const,
ignorePatterns: () => [...codexLensKeys.all, 'ignorePatterns'] as const,
indexes: () => [...codexLensKeys.all, 'indexes'] as const,
indexingStatus: () => [...codexLensKeys.all, 'indexingStatus'] as const,
search: (params: CodexLensSearchParams) => [...codexLensKeys.all, 'search', params] as const,
filesSearch: (params: CodexLensSearchParams) => [...codexLensKeys.all, 'filesSearch', params] as const,
symbolSearch: (params: Pick<CodexLensSearchParams, 'query' | 'limit'>) => [...codexLensKeys.all, 'symbolSearch', params] as const,
};
// Default stale times
@@ -715,6 +733,189 @@ export function useUpdateIgnorePatterns(): UseUpdateIgnorePatternsReturn {
};
}
// ========== Index Management Hooks ==========
export interface UseCodexLensIndexesOptions {
enabled?: boolean;
staleTime?: number;
}
export interface UseCodexLensIndexesReturn {
data: CodexLensIndexesResponse | undefined;
indexes: CodexLensIndexesResponse['indexes'] | undefined;
isLoading: boolean;
error: Error | null;
refetch: () => Promise<void>;
}
/**
* Hook for fetching CodexLens indexes
*/
export function useCodexLensIndexes(options: UseCodexLensIndexesOptions = {}): UseCodexLensIndexesReturn {
const { enabled = true, staleTime = STALE_TIME_MEDIUM } = options;
const query = useQuery({
queryKey: codexLensKeys.indexes(),
queryFn: fetchCodexLensIndexes,
staleTime,
enabled,
retry: 2,
});
const refetch = async () => {
await query.refetch();
};
return {
data: query.data,
indexes: query.data?.indexes,
isLoading: query.isLoading,
error: query.error,
refetch,
};
}
export interface UseCodexLensIndexingStatusReturn {
data: CodexLensIndexingStatusResponse | undefined;
inProgress: boolean;
isLoading: boolean;
error: Error | null;
}
/**
* Hook for checking CodexLens indexing status
*/
export function useCodexLensIndexingStatus(): UseCodexLensIndexingStatusReturn {
const query = useQuery({
queryKey: codexLensKeys.indexingStatus(),
queryFn: checkCodexLensIndexingStatus,
staleTime: STALE_TIME_SHORT,
refetchInterval: (data) => (data?.inProgress ? 2000 : false), // Poll every 2s when indexing
retry: false,
});
return {
data: query.data,
inProgress: query.data?.inProgress ?? false,
isLoading: query.isLoading,
error: query.error,
};
}
export interface UseRebuildIndexReturn {
rebuildIndex: (projectPath: string, options?: {
indexType?: 'normal' | 'vector';
embeddingModel?: string;
embeddingBackend?: 'fastembed' | 'litellm';
maxWorkers?: number;
}) => Promise<{ success: boolean; message?: string; error?: string }>;
isRebuilding: boolean;
error: Error | null;
}
/**
* Hook for rebuilding CodexLens index (full rebuild)
*/
export function useRebuildIndex(): UseRebuildIndexReturn {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: async ({
projectPath,
options = {},
}: {
projectPath: string;
options?: {
indexType?: 'normal' | 'vector';
embeddingModel?: string;
embeddingBackend?: 'fastembed' | 'litellm';
maxWorkers?: number;
};
}) => rebuildCodexLensIndex(projectPath, options),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: codexLensKeys.indexes() });
queryClient.invalidateQueries({ queryKey: codexLensKeys.dashboard() });
},
});
return {
rebuildIndex: (projectPath, options) =>
mutation.mutateAsync({ projectPath, options }),
isRebuilding: mutation.isPending,
error: mutation.error,
};
}
export interface UseUpdateIndexReturn {
updateIndex: (projectPath: string, options?: {
indexType?: 'normal' | 'vector';
embeddingModel?: string;
embeddingBackend?: 'fastembed' | 'litellm';
maxWorkers?: number;
}) => Promise<{ success: boolean; message?: string; error?: string }>;
isUpdating: boolean;
error: Error | null;
}
/**
* Hook for updating CodexLens index (incremental update)
*/
export function useUpdateIndex(): UseUpdateIndexReturn {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: async ({
projectPath,
options = {},
}: {
projectPath: string;
options?: {
indexType?: 'normal' | 'vector';
embeddingModel?: string;
embeddingBackend?: 'fastembed' | 'litellm';
maxWorkers?: number;
};
}) => updateCodexLensIndex(projectPath, options),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: codexLensKeys.indexes() });
queryClient.invalidateQueries({ queryKey: codexLensKeys.dashboard() });
},
});
return {
updateIndex: (projectPath, options) =>
mutation.mutateAsync({ projectPath, options }),
isUpdating: mutation.isPending,
error: mutation.error,
};
}
export interface UseCancelIndexingReturn {
cancelIndexing: () => Promise<{ success: boolean; error?: string }>;
isCancelling: boolean;
error: Error | null;
}
/**
* Hook for canceling CodexLens indexing
*/
export function useCancelIndexing(): UseCancelIndexingReturn {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: cancelCodexLensIndexing,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: codexLensKeys.indexingStatus() });
},
});
return {
cancelIndexing: mutation.mutateAsync,
isCancelling: mutation.isPending,
error: mutation.error,
};
}
/**
* Combined hook for all CodexLens mutations
*/
@@ -727,6 +928,9 @@ export function useCodexLensMutations() {
const updateEnv = useUpdateCodexLensEnv();
const gpu = useSelectGpu();
const updatePatterns = useUpdateIgnorePatterns();
const rebuildIndex = useRebuildIndex();
const updateIndex = useUpdateIndex();
const cancelIndexing = useCancelIndexing();
return {
updateConfig: updateConfig.updateConfig,
@@ -748,6 +952,12 @@ export function useCodexLensMutations() {
isSelectingGpu: gpu.isSelecting || gpu.isResetting,
updatePatterns: updatePatterns.updatePatterns,
isUpdatingPatterns: updatePatterns.isUpdating,
rebuildIndex: rebuildIndex.rebuildIndex,
isRebuildingIndex: rebuildIndex.isRebuilding,
updateIndex: updateIndex.updateIndex,
isUpdatingIndex: updateIndex.isUpdating,
cancelIndexing: cancelIndexing.cancelIndexing,
isCancellingIndexing: cancelIndexing.isCancelling,
isMutating:
updateConfig.isUpdating ||
bootstrap.isBootstrapping ||
@@ -757,6 +967,119 @@ export function useCodexLensMutations() {
updateEnv.isUpdating ||
gpu.isSelecting ||
gpu.isResetting ||
updatePatterns.isUpdating,
updatePatterns.isUpdating ||
rebuildIndex.isRebuilding ||
updateIndex.isUpdating ||
cancelIndexing.isCancelling,
};
}
// ========== Search Hooks ==========
export interface UseCodexLensSearchOptions {
enabled?: boolean;
}
export interface UseCodexLensSearchReturn {
data: CodexLensSearchResponse | undefined;
results: CodexLensSearchResponse['results'] | undefined;
isLoading: boolean;
error: Error | null;
refetch: () => Promise<void>;
}
/**
* Hook for content search using CodexLens
*/
export function useCodexLensSearch(params: CodexLensSearchParams, options: UseCodexLensSearchOptions = {}): UseCodexLensSearchReturn {
const { enabled = false } = options;
const query = useQuery({
queryKey: codexLensKeys.search(params),
queryFn: () => searchCodexLens(params),
enabled,
staleTime: STALE_TIME_SHORT,
retry: 1,
});
const refetch = async () => {
await query.refetch();
};
return {
data: query.data,
results: query.data?.results,
isLoading: query.isLoading,
error: query.error,
refetch,
};
}
/**
* Hook for file search using CodexLens
*/
export function useCodexLensFilesSearch(params: CodexLensSearchParams, options: UseCodexLensSearchOptions = {}): UseCodexLensSearchReturn {
const { enabled = false } = options;
const query = useQuery({
queryKey: codexLensKeys.filesSearch(params),
queryFn: () => searchFilesCodexLens(params),
enabled,
staleTime: STALE_TIME_SHORT,
retry: 1,
});
const refetch = async () => {
await query.refetch();
};
return {
data: query.data,
results: query.data?.results,
isLoading: query.isLoading,
error: query.error,
refetch,
};
}
export interface UseCodexLensSymbolSearchOptions {
enabled?: boolean;
}
export interface UseCodexLensSymbolSearchReturn {
data: CodexLensSymbolSearchResponse | undefined;
symbols: CodexLensSymbolSearchResponse['symbols'] | undefined;
isLoading: boolean;
error: Error | null;
refetch: () => Promise<void>;
}
/**
* Hook for symbol search using CodexLens
*/
export function useCodexLensSymbolSearch(
params: Pick<CodexLensSearchParams, 'query' | 'limit'>,
options: UseCodexLensSymbolSearchOptions = {}
): UseCodexLensSymbolSearchReturn {
const { enabled = false } = options;
const query = useQuery({
queryKey: codexLensKeys.symbolSearch(params),
queryFn: () => searchSymbolCodexLens(params),
enabled,
staleTime: STALE_TIME_SHORT,
retry: 1,
});
const refetch = async () => {
await query.refetch();
};
return {
data: query.data,
symbols: query.data?.symbols,
isLoading: query.isLoading,
error: query.error,
refetch,
};
}

View File

@@ -3,8 +3,8 @@
// ========================================
// TanStack Query hook for project overview data
import { useQuery } from '@tanstack/react-query';
import { fetchProjectOverview } from '../lib/api';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { fetchProjectOverview, updateProjectGuidelines, type ProjectGuidelines } from '../lib/api';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
// Query key factory
@@ -53,3 +53,39 @@ export function useProjectOverview(options: UseProjectOverviewOptions = {}) {
refetch: query.refetch,
};
}
// ========== Mutations ==========
export interface UseUpdateGuidelinesReturn {
updateGuidelines: (guidelines: ProjectGuidelines) => Promise<{ success: boolean; guidelines?: ProjectGuidelines; error?: string }>;
isUpdating: boolean;
error: Error | null;
}
/**
* Hook for updating project guidelines
*
* @example
* ```tsx
* const { updateGuidelines, isUpdating } = useUpdateGuidelines();
* await updateGuidelines({ conventions: { ... }, constraints: { ... } });
* ```
*/
export function useUpdateGuidelines(): UseUpdateGuidelinesReturn {
const queryClient = useQueryClient();
const projectPath = useWorkflowStore(selectProjectPath);
const mutation = useMutation({
mutationFn: (guidelines: ProjectGuidelines) => updateProjectGuidelines(guidelines, projectPath),
onSuccess: () => {
// Invalidate project overview cache to trigger refetch
queryClient.invalidateQueries({ queryKey: projectOverviewKeys.detail() });
},
});
return {
updateGuidelines: mutation.mutateAsync,
isUpdating: mutation.isPending,
error: mutation.error,
};
}