mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-12 02:37:45 +08:00
feat: add CLI Command Node and Prompt Node components for orchestrator
- Implemented CliCommandNode component for executing CLI tools with AI models. - Implemented PromptNode component for constructing AI prompts with context. - Added styling for mode and tool badges in both components. - Enhanced user experience with command and argument previews, execution status, and error handling. test: add comprehensive tests for ask_question tool - Created direct test for ask_question tool execution. - Developed end-to-end tests to validate ask_question tool integration with WebSocket and A2UI surfaces. - Implemented simple and integrated WebSocket tests to ensure proper message handling and surface reception. - Added tool registration test to verify ask_question tool is correctly registered. chore: add WebSocket listener and simulation tests - Added WebSocket listener for A2UI surfaces to facilitate testing. - Implemented frontend simulation test to validate complete flow from backend to frontend. - Created various test scripts to ensure robust testing of ask_question tool functionality.
This commit is contained in:
@@ -15,6 +15,9 @@ export type { UseConfigReturn } from './useConfig';
|
||||
export { useNotifications } from './useNotifications';
|
||||
export type { UseNotificationsReturn, ToastOptions } from './useNotifications';
|
||||
|
||||
export { useWebSocket } from './useWebSocket';
|
||||
export type { UseWebSocketOptions, UseWebSocketReturn } from './useWebSocket';
|
||||
|
||||
export { useWebSocketNotifications } from './useWebSocketNotifications';
|
||||
|
||||
export { useSystemNotifications } from './useSystemNotifications';
|
||||
@@ -140,7 +143,13 @@ export {
|
||||
useDeleteMcpServer,
|
||||
useToggleMcpServer,
|
||||
useMcpServerMutations,
|
||||
useMcpTemplates,
|
||||
useCreateTemplate,
|
||||
useDeleteTemplate,
|
||||
useInstallTemplate,
|
||||
useProjectOperations,
|
||||
mcpServersKeys,
|
||||
mcpTemplatesKeys,
|
||||
} from './useMcpServers';
|
||||
export type {
|
||||
UseMcpServersOptions,
|
||||
@@ -149,6 +158,12 @@ export type {
|
||||
UseCreateMcpServerReturn,
|
||||
UseDeleteMcpServerReturn,
|
||||
UseToggleMcpServerReturn,
|
||||
UseMcpTemplatesOptions,
|
||||
UseMcpTemplatesReturn,
|
||||
UseCreateTemplateReturn,
|
||||
UseDeleteTemplateReturn,
|
||||
UseInstallTemplateReturn,
|
||||
UseProjectOperationsReturn,
|
||||
} from './useMcpServers';
|
||||
|
||||
// ========== CLI ==========
|
||||
|
||||
@@ -10,8 +10,23 @@ import {
|
||||
createMcpServer,
|
||||
deleteMcpServer,
|
||||
toggleMcpServer,
|
||||
fetchMcpTemplates,
|
||||
saveMcpTemplate,
|
||||
deleteMcpTemplate,
|
||||
installMcpTemplate,
|
||||
codexRemoveServer,
|
||||
codexToggleServer,
|
||||
fetchAllProjects,
|
||||
fetchOtherProjectsServers,
|
||||
crossCliCopy,
|
||||
type McpServer,
|
||||
type McpServersResponse,
|
||||
type McpTemplate,
|
||||
type McpTemplateInstallRequest,
|
||||
type AllProjectsResponse,
|
||||
type OtherProjectsServersResponse,
|
||||
type CrossCliCopyRequest,
|
||||
type CrossCliCopyResponse,
|
||||
} from '../lib/api';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
|
||||
@@ -22,6 +37,22 @@ export const mcpServersKeys = {
|
||||
list: (scope?: 'project' | 'global') => [...mcpServersKeys.lists(), scope] as const,
|
||||
};
|
||||
|
||||
// Query key factory for MCP templates
|
||||
export const mcpTemplatesKeys = {
|
||||
all: ['mcpTemplates'] as const,
|
||||
lists: () => [...mcpTemplatesKeys.all, 'list'] as const,
|
||||
list: (category?: string) => [...mcpTemplatesKeys.lists(), category] as const,
|
||||
search: (query: string) => [...mcpTemplatesKeys.all, 'search', query] as const,
|
||||
categories: () => [...mcpTemplatesKeys.all, 'categories'] as const,
|
||||
};
|
||||
|
||||
// Query key factory for projects
|
||||
export const projectsKeys = {
|
||||
all: ['projects'] as const,
|
||||
list: () => [...projectsKeys.all, 'list'] as const,
|
||||
servers: (paths?: string[]) => [...projectsKeys.all, 'servers', ...(paths ?? [])] as const,
|
||||
};
|
||||
|
||||
// Default stale time: 2 minutes (MCP servers change occasionally)
|
||||
const STALE_TIME = 2 * 60 * 1000;
|
||||
|
||||
@@ -229,3 +260,267 @@ export function useMcpServerMutations() {
|
||||
isMutating: update.isUpdating || create.isCreating || remove.isDeleting || toggle.isToggling,
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MCP Template Hooks
|
||||
// ========================================
|
||||
|
||||
// Default stale time for templates: 5 minutes (templates change rarely)
|
||||
const TEMPLATES_STALE_TIME = 5 * 60 * 1000;
|
||||
|
||||
export interface UseMcpTemplatesOptions {
|
||||
category?: string;
|
||||
staleTime?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface UseMcpTemplatesReturn {
|
||||
templates: McpTemplate[];
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => Promise<void>;
|
||||
invalidate: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for fetching MCP templates with optional category filter
|
||||
*/
|
||||
export function useMcpTemplates(options: UseMcpTemplatesOptions = {}): UseMcpTemplatesReturn {
|
||||
const { category, staleTime = TEMPLATES_STALE_TIME, enabled = true } = options;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: mcpTemplatesKeys.list(category),
|
||||
queryFn: () => fetchMcpTemplates(),
|
||||
staleTime,
|
||||
enabled,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
const refetch = async () => {
|
||||
await query.refetch();
|
||||
};
|
||||
|
||||
const invalidate = async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: mcpTemplatesKeys.all });
|
||||
};
|
||||
|
||||
return {
|
||||
templates: category
|
||||
? query.data?.filter((t) => t.category === category) ?? []
|
||||
: query.data ?? [],
|
||||
isLoading: query.isLoading,
|
||||
isFetching: query.isFetching,
|
||||
error: query.error,
|
||||
refetch,
|
||||
invalidate,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseCreateTemplateReturn {
|
||||
createTemplate: (template: Omit<McpTemplate, 'id' | 'createdAt' | 'updatedAt'>) => Promise<{ success: boolean; id?: number; error?: string }>;
|
||||
isCreating: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for creating or updating MCP templates
|
||||
*/
|
||||
export function useCreateTemplate(): UseCreateTemplateReturn {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (template: Omit<McpTemplate, 'id' | 'createdAt' | 'updatedAt'>) =>
|
||||
saveMcpTemplate(template),
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: mcpTemplatesKeys.all });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
createTemplate: mutation.mutateAsync,
|
||||
isCreating: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseDeleteTemplateReturn {
|
||||
deleteTemplate: (templateName: string) => Promise<{ success: boolean; error?: string }>;
|
||||
isDeleting: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for deleting MCP templates
|
||||
*/
|
||||
export function useDeleteTemplate(): UseDeleteTemplateReturn {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (templateName: string) => deleteMcpTemplate(templateName),
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: mcpTemplatesKeys.all });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
deleteTemplate: mutation.mutateAsync,
|
||||
isDeleting: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseInstallTemplateReturn {
|
||||
installTemplate: (request: McpTemplateInstallRequest) => Promise<{ success: boolean; serverName?: string; error?: string }>;
|
||||
isInstalling: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for installing MCP templates to project or global scope
|
||||
*/
|
||||
export function useInstallTemplate(): UseInstallTemplateReturn {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (request: McpTemplateInstallRequest) => installMcpTemplate(request),
|
||||
onSettled: () => {
|
||||
// Invalidate both templates and servers since installation affects both
|
||||
queryClient.invalidateQueries({ queryKey: mcpTemplatesKeys.all });
|
||||
queryClient.invalidateQueries({ queryKey: mcpServersKeys.all });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
installTemplate: mutation.mutateAsync,
|
||||
isInstalling: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Codex MCP Hooks
|
||||
// ========================================
|
||||
|
||||
export interface UseCodexMutationsReturn {
|
||||
removeServer: (serverName: string) => Promise<{ success: boolean; error?: string }>;
|
||||
toggleServer: (serverName: string, enabled: boolean) => Promise<{ success: boolean; error?: string }>;
|
||||
isRemoving: boolean;
|
||||
isToggling: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined hook for Codex MCP mutations (remove and toggle)
|
||||
*/
|
||||
export function useCodexMutations(): UseCodexMutationsReturn {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const removeMutation = useMutation({
|
||||
mutationFn: (serverName: string) => codexRemoveServer(serverName),
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: mcpServersKeys.all });
|
||||
},
|
||||
});
|
||||
|
||||
const toggleMutation = useMutation({
|
||||
mutationFn: ({ serverName, enabled }: { serverName: string; enabled: boolean }) =>
|
||||
codexToggleServer(serverName, enabled),
|
||||
onMutate: async ({ serverName, enabled }) => {
|
||||
// Optimistic update could be added here if needed
|
||||
return { serverName, enabled };
|
||||
},
|
||||
onError: (_error, _vars, context) => {
|
||||
// Rollback on error
|
||||
console.error('Failed to toggle Codex MCP server:', _error);
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: mcpServersKeys.all });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
removeServer: removeMutation.mutateAsync,
|
||||
isRemoving: removeMutation.isPending,
|
||||
toggleServer: (serverName, enabled) => toggleMutation.mutateAsync({ serverName, enabled }),
|
||||
isToggling: toggleMutation.isPending,
|
||||
error: removeMutation.error || toggleMutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Project Operations Hooks
|
||||
// ========================================
|
||||
|
||||
export interface UseProjectOperationsReturn {
|
||||
projects: string[];
|
||||
currentProject?: string;
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => Promise<void>;
|
||||
copyToCodex: (request: CrossCliCopyRequest) => Promise<CrossCliCopyResponse>;
|
||||
copyFromCodex: (request: CrossCliCopyRequest) => Promise<CrossCliCopyResponse>;
|
||||
isCopying: boolean;
|
||||
fetchOtherServers: (projectPaths?: string[]) => Promise<OtherProjectsServersResponse>;
|
||||
isFetchingServers: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined hook for project operations (all projects, cross-CLI copy, other projects' servers)
|
||||
*/
|
||||
export function useProjectOperations(): UseProjectOperationsReturn {
|
||||
const queryClient = useQueryClient();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
// Fetch all projects
|
||||
const projectsQuery = useQuery({
|
||||
queryKey: projectsKeys.list(),
|
||||
queryFn: () => fetchAllProjects(),
|
||||
staleTime: STALE_TIME,
|
||||
enabled: true,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
// Cross-CLI copy mutation
|
||||
const copyMutation = useMutation({
|
||||
mutationFn: (request: CrossCliCopyRequest) => crossCliCopy(request),
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: mcpServersKeys.all });
|
||||
},
|
||||
});
|
||||
|
||||
// Other projects servers query
|
||||
const serversQuery = useQuery({
|
||||
queryKey: projectsKeys.servers(),
|
||||
queryFn: () => fetchOtherProjectsServers(),
|
||||
staleTime: STALE_TIME,
|
||||
enabled: false, // Manual trigger only
|
||||
retry: 1,
|
||||
});
|
||||
|
||||
const refetch = async () => {
|
||||
await projectsQuery.refetch();
|
||||
};
|
||||
|
||||
const fetchOtherServers = async (projectPaths?: string[]) => {
|
||||
return await queryClient.fetchQuery({
|
||||
queryKey: projectsKeys.servers(projectPaths),
|
||||
queryFn: () => fetchOtherProjectsServers(projectPaths),
|
||||
staleTime: STALE_TIME,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
projects: projectsQuery.data?.projects ?? [],
|
||||
currentProject: projectsQuery.data?.currentProject ?? projectPath ?? undefined,
|
||||
isLoading: projectsQuery.isLoading,
|
||||
error: projectsQuery.error,
|
||||
refetch,
|
||||
copyToCodex: (request) => copyMutation.mutateAsync({ ...request, source: 'claude', target: 'codex' }),
|
||||
copyFromCodex: (request) => copyMutation.mutateAsync({ ...request, source: 'codex', target: 'claude' }),
|
||||
isCopying: copyMutation.isPending,
|
||||
fetchOtherServers,
|
||||
isFetchingServers: serversQuery.isFetching,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user