Files
Claude-Code-Workflow/ccw/frontend/src/hooks/useCli.ts
catlog22 715ef12c92 feat(a2ui): Implement A2UI backend with question handling and WebSocket support
- Added A2UITypes for defining question structures and answers.
- Created A2UIWebSocketHandler for managing WebSocket connections and message handling.
- Developed ask-question tool for interactive user questions via A2UI.
- Introduced platformUtils for platform detection and shell command handling.
- Centralized TypeScript types in index.ts for better organization.
- Implemented compatibility checks for hook templates based on platform requirements.
2026-01-31 15:27:12 +08:00

513 lines
13 KiB
TypeScript

// ========================================
// useCliEndpoints Hook
// ========================================
// TanStack Query hooks for CLI endpoint management
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
fetchCliEndpoints,
toggleCliEndpoint,
type CliEndpoint,
type CliEndpointsResponse,
} from '../lib/api';
// Query key factory
export const cliEndpointsKeys = {
all: ['cliEndpoints'] as const,
lists: () => [...cliEndpointsKeys.all, 'list'] as const,
};
const STALE_TIME = 2 * 60 * 1000;
export interface UseCliEndpointsOptions {
staleTime?: number;
enabled?: boolean;
}
export interface UseCliEndpointsReturn {
endpoints: CliEndpoint[];
litellmEndpoints: CliEndpoint[];
customEndpoints: CliEndpoint[];
wrapperEndpoints: CliEndpoint[];
totalCount: number;
enabledCount: number;
isLoading: boolean;
isFetching: boolean;
error: Error | null;
refetch: () => Promise<void>;
invalidate: () => Promise<void>;
}
export function useCliEndpoints(options: UseCliEndpointsOptions = {}): UseCliEndpointsReturn {
const { staleTime = STALE_TIME, enabled = true } = options;
const queryClient = useQueryClient();
const query = useQuery({
queryKey: cliEndpointsKeys.lists(),
queryFn: fetchCliEndpoints,
staleTime,
enabled,
retry: 2,
});
const endpoints = query.data?.endpoints ?? [];
const enabledEndpoints = endpoints.filter((e) => e.enabled);
const litellmEndpoints = endpoints.filter((e) => e.type === 'litellm');
const customEndpoints = endpoints.filter((e) => e.type === 'custom');
const wrapperEndpoints = endpoints.filter((e) => e.type === 'wrapper');
const refetch = async () => {
await query.refetch();
};
const invalidate = async () => {
await queryClient.invalidateQueries({ queryKey: cliEndpointsKeys.all });
};
return {
endpoints,
litellmEndpoints,
customEndpoints,
wrapperEndpoints,
totalCount: endpoints.length,
enabledCount: enabledEndpoints.length,
isLoading: query.isLoading,
isFetching: query.isFetching,
error: query.error,
refetch,
invalidate,
};
}
export function useToggleCliEndpoint() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: ({ endpointId, enabled }: { endpointId: string; enabled: boolean }) =>
toggleCliEndpoint(endpointId, enabled),
onMutate: async ({ endpointId, enabled }) => {
await queryClient.cancelQueries({ queryKey: cliEndpointsKeys.all });
const previousEndpoints = queryClient.getQueryData<CliEndpointsResponse>(cliEndpointsKeys.lists());
queryClient.setQueryData<CliEndpointsResponse>(cliEndpointsKeys.lists(), (old) => {
if (!old) return old;
return {
endpoints: old.endpoints.map((e) => (e.id === endpointId ? { ...e, enabled } : e)),
};
});
return { previousEndpoints };
},
onError: (_error, _vars, context) => {
if (context?.previousEndpoints) {
queryClient.setQueryData(cliEndpointsKeys.lists(), context.previousEndpoints);
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: cliEndpointsKeys.all });
},
});
return {
toggleEndpoint: (endpointId: string, enabled: boolean) => mutation.mutateAsync({ endpointId, enabled }),
isToggling: mutation.isPending,
error: mutation.error,
};
}
// ========================================
// useCliInstallations Hook
// ========================================
import {
fetchCliInstallations,
installCliTool,
uninstallCliTool,
upgradeCliTool,
type CliInstallation,
} from '../lib/api';
export const cliInstallationsKeys = {
all: ['cliInstallations'] as const,
lists: () => [...cliInstallationsKeys.all, 'list'] as const,
};
export interface UseCliInstallationsOptions {
staleTime?: number;
enabled?: boolean;
}
export interface UseCliInstallationsReturn {
installations: CliInstallation[];
installedTools: CliInstallation[];
totalCount: number;
installedCount: number;
isLoading: boolean;
isFetching: boolean;
error: Error | null;
refetch: () => Promise<void>;
invalidate: () => Promise<void>;
}
export function useCliInstallations(options: UseCliInstallationsOptions = {}): UseCliInstallationsReturn {
const { staleTime = STALE_TIME, enabled = true } = options;
const queryClient = useQueryClient();
const query = useQuery({
queryKey: cliInstallationsKeys.lists(),
queryFn: fetchCliInstallations,
staleTime,
enabled,
retry: 2,
});
const installations = query.data?.tools ?? [];
const installedTools = installations.filter((t) => t.installed);
const refetch = async () => {
await query.refetch();
};
const invalidate = async () => {
await queryClient.invalidateQueries({ queryKey: cliInstallationsKeys.all });
};
return {
installations,
installedTools,
totalCount: installations.length,
installedCount: installedTools.length,
isLoading: query.isLoading,
isFetching: query.isFetching,
error: query.error,
refetch,
invalidate,
};
}
export function useInstallCliTool() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (toolName: string) => installCliTool(toolName),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: cliInstallationsKeys.all });
},
});
return {
installTool: mutation.mutateAsync,
isInstalling: mutation.isPending,
error: mutation.error,
};
}
export function useUninstallCliTool() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (toolName: string) => uninstallCliTool(toolName),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: cliInstallationsKeys.all });
},
});
return {
uninstallTool: mutation.mutateAsync,
isUninstalling: mutation.isPending,
error: mutation.error,
};
}
export function useUpgradeCliTool() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (toolName: string) => upgradeCliTool(toolName),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: cliInstallationsKeys.all });
},
});
return {
upgradeTool: mutation.mutateAsync,
isUpgrading: mutation.isPending,
error: mutation.error,
};
}
// ========================================
// useHooks Hook
// ========================================
import {
fetchHooks,
toggleHook,
type Hook,
type HooksResponse,
} from '../lib/api';
export const hooksKeys = {
all: ['hooks'] as const,
lists: () => [...hooksKeys.all, 'list'] as const,
};
export interface UseHooksOptions {
staleTime?: number;
enabled?: boolean;
}
export interface UseHooksReturn {
hooks: Hook[];
enabledHooks: Hook[];
totalCount: number;
enabledCount: number;
isLoading: boolean;
isFetching: boolean;
error: Error | null;
refetch: () => Promise<void>;
invalidate: () => Promise<void>;
}
export function useHooks(options: UseHooksOptions = {}): UseHooksReturn {
const { staleTime = STALE_TIME, enabled = true } = options;
const queryClient = useQueryClient();
const query = useQuery({
queryKey: hooksKeys.lists(),
queryFn: fetchHooks,
staleTime,
enabled,
retry: 2,
});
const hooks = query.data?.hooks ?? [];
const enabledHooks = hooks.filter((h) => h.enabled);
const refetch = async () => {
await query.refetch();
};
const invalidate = async () => {
await queryClient.invalidateQueries({ queryKey: hooksKeys.all });
};
return {
hooks,
enabledHooks,
totalCount: hooks.length,
enabledCount: enabledHooks.length,
isLoading: query.isLoading,
isFetching: query.isFetching,
error: query.error,
refetch,
invalidate,
};
}
export function useToggleHook() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: ({ hookName, enabled }: { hookName: string; enabled: boolean }) =>
toggleHook(hookName, enabled),
onMutate: async ({ hookName, enabled }) => {
await queryClient.cancelQueries({ queryKey: hooksKeys.all });
const previousHooks = queryClient.getQueryData<HooksResponse>(hooksKeys.lists());
queryClient.setQueryData<HooksResponse>(hooksKeys.lists(), (old) => {
if (!old) return old;
return {
hooks: old.hooks.map((h) => (h.name === hookName ? { ...h, enabled } : h)),
};
});
return { previousHooks };
},
onError: (_error, _vars, context) => {
if (context?.previousHooks) {
queryClient.setQueryData(hooksKeys.lists(), context.previousHooks);
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: hooksKeys.all });
},
});
return {
toggleHook: (hookName: string, enabled: boolean) => mutation.mutateAsync({ hookName, enabled }),
isToggling: mutation.isPending,
error: mutation.error,
};
}
// ========================================
// useRules Hook
// ========================================
import {
fetchRules,
toggleRule,
createRule as createRuleApi,
deleteRule as deleteRuleApi,
type Rule,
type RulesResponse,
type RuleCreateInput,
} from '../lib/api';
export const rulesKeys = {
all: ['rules'] as const,
lists: () => [...rulesKeys.all, 'list'] as const,
};
export interface UseRulesOptions {
staleTime?: number;
enabled?: boolean;
}
export interface UseRulesReturn {
rules: Rule[];
enabledRules: Rule[];
totalCount: number;
enabledCount: number;
isLoading: boolean;
isFetching: boolean;
error: Error | null;
refetch: () => Promise<void>;
invalidate: () => Promise<void>;
}
export function useRules(options: UseRulesOptions = {}): UseRulesReturn {
const { staleTime = STALE_TIME, enabled = true } = options;
const queryClient = useQueryClient();
const query = useQuery({
queryKey: rulesKeys.lists(),
queryFn: fetchRules,
staleTime,
enabled,
retry: 2,
});
const rules = query.data?.rules ?? [];
const enabledRules = rules.filter((r) => r.enabled);
const refetch = async () => {
await query.refetch();
};
const invalidate = async () => {
await queryClient.invalidateQueries({ queryKey: rulesKeys.all });
};
return {
rules,
enabledRules,
totalCount: rules.length,
enabledCount: enabledRules.length,
isLoading: query.isLoading,
isFetching: query.isFetching,
error: query.error,
refetch,
invalidate,
};
}
export function useToggleRule() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: ({ ruleId, enabled }: { ruleId: string; enabled: boolean }) =>
toggleRule(ruleId, enabled),
onMutate: async ({ ruleId, enabled }) => {
await queryClient.cancelQueries({ queryKey: rulesKeys.all });
const previousRules = queryClient.getQueryData<RulesResponse>(rulesKeys.lists());
queryClient.setQueryData<RulesResponse>(rulesKeys.lists(), (old) => {
if (!old) return old;
return {
rules: old.rules.map((r) => (r.id === ruleId ? { ...r, enabled } : r)),
};
});
return { previousRules };
},
onError: (_error, _vars, context) => {
if (context?.previousRules) {
queryClient.setQueryData(rulesKeys.lists(), context.previousRules);
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: rulesKeys.all });
},
});
return {
toggleRule: (ruleId: string, enabled: boolean) => mutation.mutateAsync({ ruleId, enabled }),
isToggling: mutation.isPending,
error: mutation.error,
};
}
export function useCreateRule() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (input: RuleCreateInput) => createRuleApi(input),
onSuccess: (newRule) => {
queryClient.setQueryData<RulesResponse>(rulesKeys.lists(), (old) => {
if (!old) return { rules: [newRule] };
return {
rules: [newRule, ...old.rules],
};
});
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: rulesKeys.all });
},
});
return {
createRule: mutation.mutateAsync,
isCreating: mutation.isPending,
error: mutation.error,
};
}
export function useDeleteRule() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: ({ ruleId, location }: { ruleId: string; location?: string }) =>
deleteRuleApi(ruleId, location),
onMutate: async ({ ruleId }) => {
await queryClient.cancelQueries({ queryKey: rulesKeys.all });
const previousRules = queryClient.getQueryData<RulesResponse>(rulesKeys.lists());
queryClient.setQueryData<RulesResponse>(rulesKeys.lists(), (old) => {
if (!old) return old;
return {
rules: old.rules.filter((r) => r.id !== ruleId),
};
});
return { previousRules };
},
onError: (_error, _vars, context) => {
if (context?.previousRules) {
queryClient.setQueryData(rulesKeys.lists(), context.previousRules);
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: rulesKeys.all });
},
});
return {
deleteRule: (ruleId: string, location?: string) => mutation.mutateAsync({ ruleId, location }),
isDeleting: mutation.isPending,
error: mutation.error,
};
}