Files
Claude-Code-Workflow/ccw/src/core/routes/dashboard-routes.ts
catlog22 c6093ef741 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.
2026-02-03 23:10:36 +08:00

173 lines
5.1 KiB
TypeScript

/**
* Dashboard Routes Module
* Provides API endpoints for dashboard initialization and configuration
*
* 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, resolvePath } from '../../utils/path-resolver.js';
import { scanSessions } from '../session-scanner.js';
/**
* Dashboard initialization response structure
*/
interface DashboardInitResponse {
projectPath: string;
recentPaths: string[];
platform: string;
initialData: {
generatedAt: string;
activeSessions: unknown[];
archivedSessions: unknown[];
liteTasks: {
litePlan: unknown[];
liteFix: unknown[];
multiCliPlan: unknown[];
};
reviewData: {
dimensions: Record<string, unknown>;
};
projectOverview: null;
statistics: {
totalSessions: number;
activeSessions: number;
totalTasks: number;
completedTasks: number;
reviewFindings: number;
litePlanCount: number;
liteFixCount: number;
multiCliPlanCount: number;
};
};
}
/**
* 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
*/
export async function handleDashboardRoutes(ctx: RouteContext): Promise<boolean> {
const { pathname, req, res, initialPath } = ctx;
// GET /api/dashboard/init - Return initial dashboard data
if (pathname === '/api/dashboard/init' && req.method === 'GET') {
try {
const response: DashboardInitResponse = {
projectPath: normalizePathForDisplay(initialPath),
recentPaths: getRecentPaths(),
platform: process.platform,
initialData: {
generatedAt: new Date().toISOString(),
activeSessions: [],
archivedSessions: [],
liteTasks: {
litePlan: [],
liteFix: [],
multiCliPlan: []
},
reviewData: {
dimensions: {}
},
projectOverview: null,
statistics: {
totalSessions: 0,
activeSessions: 0,
totalTasks: 0,
completedTasks: 0,
reviewFindings: 0,
litePlanCount: 0,
liteFixCount: 0,
multiCliPlanCount: 0
}
}
};
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
data: response,
timestamp: new Date().toISOString()
}));
return true;
} catch (error) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: false,
error: (error as Error).message
}));
return true;
}
}
// 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;
}