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:
@@ -85,9 +85,9 @@ export class A2UIWebSocketHandler {
|
||||
components: unknown[];
|
||||
initialState: Record<string, unknown>;
|
||||
}): number {
|
||||
const message: A2UISurfaceMessage = {
|
||||
const message = {
|
||||
type: 'a2ui-surface',
|
||||
surfaceUpdate,
|
||||
payload: surfaceUpdate, // Frontend expects 'payload' not 'surfaceUpdate'
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
@@ -132,9 +132,9 @@ export class A2UIWebSocketHandler {
|
||||
initialState: Record<string, unknown>;
|
||||
}
|
||||
): boolean {
|
||||
const message: A2UISurfaceMessage = {
|
||||
const message = {
|
||||
type: 'a2ui-surface',
|
||||
surfaceUpdate,
|
||||
payload: surfaceUpdate, // Frontend expects 'payload' not 'surfaceUpdate'
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
|
||||
@@ -123,6 +123,7 @@ export async function csrfValidation(ctx: CsrfMiddlewareContext): Promise<boolea
|
||||
// Always allow token acquisition routes and webhook endpoints.
|
||||
if (pathname === '/api/auth/token') return true;
|
||||
if (pathname === '/api/hook') return true;
|
||||
if (pathname === '/api/test/ask-question') return true; // Temporary for E2E testing
|
||||
|
||||
// Requests authenticated via Authorization header do not require CSRF protection.
|
||||
const authorization = getHeaderValue(req.headers.authorization);
|
||||
|
||||
@@ -4,10 +4,14 @@
|
||||
*
|
||||
* Endpoints:
|
||||
* - GET /api/dashboard/init - Returns initial dashboard data (projectPath, recentPaths, platform, initialData)
|
||||
* - GET /api/workflow-status-counts - Returns workflow status distribution
|
||||
*/
|
||||
|
||||
import { existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import type { RouteContext } from './types.js';
|
||||
import { getRecentPaths, normalizePathForDisplay } from '../../utils/path-resolver.js';
|
||||
import { getRecentPaths, normalizePathForDisplay, resolvePath } from '../../utils/path-resolver.js';
|
||||
import { scanSessions } from '../session-scanner.js';
|
||||
|
||||
/**
|
||||
* Dashboard initialization response structure
|
||||
@@ -42,6 +46,15 @@ interface DashboardInitResponse {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Workflow status count structure
|
||||
*/
|
||||
interface WorkflowStatusCount {
|
||||
status: 'planning' | 'in_progress' | 'completed' | 'paused' | 'archived';
|
||||
count: number;
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle dashboard routes
|
||||
* @returns true if route was handled, false otherwise
|
||||
@@ -99,5 +112,61 @@ export async function handleDashboardRoutes(ctx: RouteContext): Promise<boolean>
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/workflow-status-counts - Return workflow status distribution
|
||||
if (pathname === '/api/workflow-status-counts' && req.method === 'GET') {
|
||||
try {
|
||||
const projectPath = ctx.url.searchParams.get('projectPath') || initialPath;
|
||||
const resolvedPath = resolvePath(projectPath);
|
||||
const workflowDir = join(resolvedPath, '.workflow');
|
||||
|
||||
// Initialize counts for all statuses
|
||||
const statusCounts: Record<string, number> = {
|
||||
planning: 0,
|
||||
in_progress: 0,
|
||||
completed: 0,
|
||||
paused: 0,
|
||||
archived: 0
|
||||
};
|
||||
|
||||
// Scan sessions if .workflow directory exists
|
||||
if (existsSync(workflowDir)) {
|
||||
const sessions = await scanSessions(workflowDir);
|
||||
|
||||
// Count active sessions by status
|
||||
// Map session statuses: 'active' -> 'in_progress' (frontend convention)
|
||||
for (const session of sessions.active) {
|
||||
const rawStatus = session.status || 'active';
|
||||
const status = rawStatus === 'active' ? 'in_progress' : rawStatus;
|
||||
if (status in statusCounts) {
|
||||
statusCounts[status]++;
|
||||
} else {
|
||||
statusCounts.in_progress++;
|
||||
}
|
||||
}
|
||||
|
||||
// All archived sessions count as 'archived'
|
||||
statusCounts.archived = sessions.archived.length;
|
||||
}
|
||||
|
||||
// Calculate total and percentages
|
||||
const total = Object.values(statusCounts).reduce((sum, count) => sum + count, 0);
|
||||
const result: WorkflowStatusCount[] = Object.entries(statusCounts)
|
||||
.map(([status, count]) => ({
|
||||
status: status as WorkflowStatusCount['status'],
|
||||
count,
|
||||
percentage: total > 0 ? Math.round((count / total) * 100) : 0
|
||||
}))
|
||||
.filter(item => item.count > 0); // Only include statuses with non-zero counts
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(result));
|
||||
return true;
|
||||
} catch (error) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (error as Error).message }));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -457,7 +457,7 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
||||
const tokenManager = getTokenManager();
|
||||
const secretKey = tokenManager.getSecretKey();
|
||||
tokenManager.getOrCreateAuthToken();
|
||||
const unauthenticatedPaths = new Set<string>(['/api/auth/token', '/api/csrf-token', '/api/hook']);
|
||||
const unauthenticatedPaths = new Set<string>(['/api/auth/token', '/api/csrf-token', '/api/hook', '/api/test/ask-question']);
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const url = new URL(req.url ?? '/', `http://localhost:${serverPort}`);
|
||||
@@ -527,6 +527,46 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
||||
// Try each route handler in order
|
||||
// Order matters: more specific routes should come before general ones
|
||||
|
||||
// Test endpoint for ask_question tool (temporary for E2E testing)
|
||||
if (pathname === '/api/test/ask-question' && req.method === 'POST') {
|
||||
const { executeTool } = await import('../tools/index.js');
|
||||
|
||||
// Get question params from request body if provided, or use default
|
||||
let questionParams = {
|
||||
question: {
|
||||
id: 'test-question-' + Date.now(),
|
||||
type: 'confirm',
|
||||
title: 'Test Question',
|
||||
message: 'This is a test of the ask_question tool integration',
|
||||
description: 'Click Confirm or Cancel to complete the test'
|
||||
},
|
||||
timeout: 30000
|
||||
};
|
||||
|
||||
if (req.headers['content-type']?.includes('application/json')) {
|
||||
try {
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of req) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
const body = JSON.parse(Buffer.concat(chunks).toString());
|
||||
if (body.question) {
|
||||
questionParams.question = { ...questionParams.question, ...body.question };
|
||||
}
|
||||
if (body.timeout) {
|
||||
questionParams.timeout = body.timeout;
|
||||
}
|
||||
} catch (e) {
|
||||
// Use defaults if parsing fails
|
||||
}
|
||||
}
|
||||
|
||||
const result = await executeTool('ask_question', questionParams);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(result));
|
||||
return;
|
||||
}
|
||||
|
||||
// Auth routes (/api/csrf-token)
|
||||
if (await handleAuthRoutes(routeContext)) return;
|
||||
|
||||
@@ -540,8 +580,8 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
||||
if (await handleNavStatusRoutes(routeContext)) return;
|
||||
}
|
||||
|
||||
// Dashboard routes (/api/dashboard/*) - Dashboard initialization
|
||||
if (pathname.startsWith('/api/dashboard/')) {
|
||||
// Dashboard routes (/api/dashboard/*, /api/workflow-status-counts)
|
||||
if (pathname.startsWith('/api/dashboard/') || pathname === '/api/workflow-status-counts') {
|
||||
if (await handleDashboardRoutes(routeContext)) return;
|
||||
}
|
||||
|
||||
|
||||
@@ -334,7 +334,7 @@ export async function execute(params: AskQuestionParams): Promise<ToolResult<Ask
|
||||
});
|
||||
|
||||
// Send A2UI surface via WebSocket to frontend
|
||||
const a2uiSurface = createA2UISurface(question, surfaceId);
|
||||
const a2uiSurface = generateQuestionSurface(question, surfaceId);
|
||||
a2uiWebSocketHandler.sendSurface(a2uiSurface.surfaceUpdate);
|
||||
|
||||
// Wait for answer
|
||||
@@ -474,3 +474,11 @@ export const schema: ToolSchema = {
|
||||
required: ['question'],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Tool handler for MCP integration
|
||||
* Wraps the execute function to match the expected handler signature
|
||||
*/
|
||||
export async function handler(params: Record<string, unknown>): Promise<ToolResult<AskQuestionResult>> {
|
||||
return execute(params as AskQuestionParams);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user