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:
catlog22
2026-02-03 23:10:36 +08:00
parent a806d70d9b
commit c6093ef741
134 changed files with 6392 additions and 634 deletions

View File

@@ -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 ==========

View File

@@ -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,
};
}