mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
feat: add tests and implementation for issue discovery and queue pages
- Implemented `DiscoveryPage` with session management and findings display. - Added tests for `DiscoveryPage` to ensure proper rendering and functionality. - Created `QueuePage` for managing issue execution queues with stats and actions. - Added tests for `QueuePage` to verify UI elements and translations. - Introduced `useIssues` hooks for fetching and managing issue data. - Added loading skeletons and error handling for better user experience. - Created `vite-env.d.ts` for TypeScript support in Vite environment.
This commit is contained in:
@@ -18,7 +18,7 @@ interface ServeOptions {
|
||||
* @param {Object} options - Command options
|
||||
*/
|
||||
export async function serveCommand(options: ServeOptions): Promise<void> {
|
||||
const port = options.port || 3456;
|
||||
const port = Number(options.port) || 3456;
|
||||
const host = options.host || '127.0.0.1';
|
||||
const frontend = options.frontend || 'js';
|
||||
|
||||
@@ -75,9 +75,9 @@ export async function serveCommand(options: ServeOptions): Promise<void> {
|
||||
// Display frontend URLs
|
||||
if (frontend === 'both') {
|
||||
console.log(chalk.gray(` JS Frontend: ${boundUrl}`));
|
||||
console.log(chalk.gray(` React Frontend: ${boundUrl}/react`));
|
||||
console.log(chalk.gray(` React Frontend: http://${host}:${reactPort}`));
|
||||
} else if (frontend === 'react') {
|
||||
console.log(chalk.gray(` React Frontend: ${boundUrl}/react`));
|
||||
console.log(chalk.gray(` React Frontend: http://${host}:${reactPort}`));
|
||||
}
|
||||
|
||||
// Open browser
|
||||
@@ -86,10 +86,17 @@ export async function serveCommand(options: ServeOptions): Promise<void> {
|
||||
try {
|
||||
// Determine which URL to open based on frontend setting
|
||||
let openUrl = browserUrl;
|
||||
if (frontend === 'react') {
|
||||
openUrl = `${browserUrl}/react`;
|
||||
if (frontend === 'react' && reactPort) {
|
||||
// React frontend: access via proxy path /react/
|
||||
openUrl = `http://${host}:${port}/react/`;
|
||||
} else if (frontend === 'both') {
|
||||
// Both frontends: default to JS frontend at root
|
||||
openUrl = browserUrl;
|
||||
}
|
||||
await launchBrowser(openUrl);
|
||||
|
||||
// Add path query parameter for workspace switching
|
||||
const pathParam = initialPath ? `?path=${encodeURIComponent(initialPath)}` : '';
|
||||
await launchBrowser(openUrl + pathParam);
|
||||
console.log(chalk.green.bold('\n Dashboard opened in browser!'));
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
@@ -101,9 +108,9 @@ export async function serveCommand(options: ServeOptions): Promise<void> {
|
||||
console.log(chalk.gray('\n Press Ctrl+C to stop the server\n'));
|
||||
|
||||
// Handle graceful shutdown
|
||||
process.on('SIGINT', () => {
|
||||
process.on('SIGINT', async () => {
|
||||
console.log(chalk.yellow('\n Shutting down server...'));
|
||||
stopReactFrontend();
|
||||
await stopReactFrontend();
|
||||
server.close(() => {
|
||||
console.log(chalk.green(' Server stopped.\n'));
|
||||
process.exit(0);
|
||||
@@ -117,7 +124,7 @@ export async function serveCommand(options: ServeOptions): Promise<void> {
|
||||
console.error(chalk.yellow(` Port ${port} is already in use.`));
|
||||
console.error(chalk.gray(` Try a different port: ccw serve --port ${port + 1}\n`));
|
||||
}
|
||||
stopReactFrontend();
|
||||
await stopReactFrontend();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ async function killProcess(pid: string): Promise<boolean> {
|
||||
*/
|
||||
export async function stopCommand(options: StopOptions): Promise<void> {
|
||||
const port = options.port || 3456;
|
||||
const reactPort = port + 1; // React frontend runs on port + 1
|
||||
const force = options.force || false;
|
||||
|
||||
console.log(chalk.blue.bold('\n CCW Dashboard\n'));
|
||||
@@ -107,6 +108,23 @@ export async function stopCommand(options: StopOptions): Promise<void> {
|
||||
|
||||
if (!pid) {
|
||||
console.log(chalk.yellow(` No server running on port ${port}\n`));
|
||||
|
||||
// Also check and clean up React frontend if it's still running
|
||||
const reactPid = await findProcessOnPort(reactPort);
|
||||
if (reactPid) {
|
||||
console.log(chalk.yellow(` React frontend still running on port ${reactPort} (PID: ${reactPid})`));
|
||||
if (force) {
|
||||
console.log(chalk.cyan(' Cleaning up React frontend...'));
|
||||
const killed = await killProcess(reactPid);
|
||||
if (killed) {
|
||||
console.log(chalk.green(' React frontend stopped!\n'));
|
||||
} else {
|
||||
console.log(chalk.red(' Failed to stop React frontend.\n'));
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.gray(`\n Use --force to clean it up:\n ccw stop --force\n`));
|
||||
}
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -118,7 +136,17 @@ export async function stopCommand(options: StopOptions): Promise<void> {
|
||||
const killed = await killProcess(pid);
|
||||
|
||||
if (killed) {
|
||||
console.log(chalk.green.bold('\n Process killed successfully!\n'));
|
||||
console.log(chalk.green(' Main server killed successfully!'));
|
||||
|
||||
// Also try to kill React frontend
|
||||
const reactPid = await findProcessOnPort(reactPort);
|
||||
if (reactPid) {
|
||||
console.log(chalk.cyan(` Cleaning up React frontend on port ${reactPort}...`));
|
||||
await killProcess(reactPid);
|
||||
console.log(chalk.green(' React frontend stopped!'));
|
||||
}
|
||||
|
||||
console.log(chalk.green.bold('\n All processes stopped successfully!\n'));
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log(chalk.red('\n Failed to kill process. Try running as administrator.\n'));
|
||||
|
||||
@@ -9,6 +9,7 @@ interface ViewOptions {
|
||||
path?: string;
|
||||
host?: string;
|
||||
browser?: boolean;
|
||||
frontend?: 'js' | 'react' | 'both';
|
||||
}
|
||||
|
||||
interface SwitchWorkspaceResult {
|
||||
@@ -72,9 +73,10 @@ export async function viewCommand(options: ViewOptions): Promise<void> {
|
||||
// Check for updates (fire-and-forget, non-blocking)
|
||||
checkForUpdates().catch(() => { /* ignore errors */ });
|
||||
|
||||
const port = options.port || 3456;
|
||||
const port = Number(options.port) || 3456;
|
||||
const host = options.host || '127.0.0.1';
|
||||
const browserHost = host === '0.0.0.0' || host === '::' ? 'localhost' : host;
|
||||
const frontend = options.frontend || 'both';
|
||||
|
||||
// Resolve workspace path
|
||||
let workspacePath = process.cwd();
|
||||
@@ -101,8 +103,12 @@ export async function viewCommand(options: ViewOptions): Promise<void> {
|
||||
if (result.success) {
|
||||
console.log(chalk.green(` Workspace switched successfully`));
|
||||
|
||||
// Open browser with the new path
|
||||
const url = `http://${browserHost}:${port}/?path=${encodeURIComponent(result.path!)}`;
|
||||
// Determine URL based on frontend type
|
||||
let urlPath = '';
|
||||
if (frontend === 'react') {
|
||||
urlPath = '/react';
|
||||
}
|
||||
const url = `http://${browserHost}:${port}${urlPath}/?path=${encodeURIComponent(result.path!)}`;
|
||||
|
||||
if (options.browser !== false) {
|
||||
console.log(chalk.cyan(' Opening in browser...'));
|
||||
@@ -127,7 +133,8 @@ export async function viewCommand(options: ViewOptions): Promise<void> {
|
||||
path: workspacePath,
|
||||
port: port,
|
||||
host,
|
||||
browser: options.browser
|
||||
browser: options.browser,
|
||||
frontend: frontend
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ export class A2UIWebSocketHandler {
|
||||
// Convert to QuestionAnswer format
|
||||
const questionAnswer: QuestionAnswer = {
|
||||
questionId: answer.questionId,
|
||||
value: answer.value,
|
||||
value: answer.value as string | boolean | string[],
|
||||
cancelled: answer.cancelled,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
// A2UI Backend - Index
|
||||
// ========================================
|
||||
|
||||
export * from './A2UITypes';
|
||||
export * from './A2UIWebSocketHandler';
|
||||
export * from './A2UITypes.js';
|
||||
export * from './A2UIWebSocketHandler.js';
|
||||
|
||||
@@ -220,13 +220,13 @@ export async function handleSystemRoutes(ctx: SystemRouteContext): Promise<boole
|
||||
return true;
|
||||
}
|
||||
|
||||
// Track the path and return success
|
||||
trackRecentPath(resolved);
|
||||
// Get full workflow data for the new path
|
||||
const workflowData = await getWorkflowData(resolved);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
path: resolved,
|
||||
recentPaths: getRecentPaths()
|
||||
...workflowData
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,14 @@ import { getCliToolsStatus } from '../tools/cli-executor.js';
|
||||
import type { ServerConfig } from '../types/config.js';
|
||||
import type { PostRequestHandler } from './routes/types.js';
|
||||
|
||||
|
||||
interface ServerOptions {
|
||||
port?: number;
|
||||
initialPath?: string;
|
||||
host?: string;
|
||||
open?: boolean;
|
||||
frontend?: 'js' | 'react' | 'both';
|
||||
reactPort?: number;
|
||||
}
|
||||
|
||||
type PostHandler = PostRequestHandler;
|
||||
|
||||
@@ -413,6 +420,20 @@ window.INITIAL_PATH = '${normalizePathForDisplay(initialPath).replace(/\\/g, '/'
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read request body as text for proxy requests
|
||||
* @param req - HTTP request object
|
||||
* @returns Promise that resolves to body text
|
||||
*/
|
||||
async function readRequestBody(req: http.IncomingMessage): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let body = '';
|
||||
req.on('data', (chunk) => { body += chunk; });
|
||||
req.on('end', () => { resolve(body); });
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and start the dashboard server
|
||||
* @param {Object} options - Server options
|
||||
@@ -424,6 +445,14 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
||||
let serverPort = options.port ?? 3456;
|
||||
const initialPath = options.initialPath || process.cwd();
|
||||
const host = options.host ?? '127.0.0.1';
|
||||
const frontend = options.frontend || 'js';
|
||||
const reactPort = options.reactPort || serverPort + 1;
|
||||
|
||||
// Log frontend configuration
|
||||
console.log(`[Server] Frontend mode: ${frontend}`);
|
||||
if (frontend === 'react' || frontend === 'both') {
|
||||
console.log(`[Server] React proxy configured: /react/* -> http://localhost:${reactPort}`);
|
||||
}
|
||||
|
||||
const tokenManager = getTokenManager();
|
||||
const secretKey = tokenManager.getSecretKey();
|
||||
@@ -696,6 +725,69 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
||||
}
|
||||
}
|
||||
|
||||
// React frontend proxy - proxy requests to React dev server
|
||||
// Use the frontend and reactPort variables defined at startServer scope
|
||||
if (frontend === 'react' || frontend === 'both') {
|
||||
if (pathname === '/react' || pathname.startsWith('/react/')) {
|
||||
// Don't strip the /react prefix - Vite knows it's serving under /react/
|
||||
const reactUrl = `http://localhost:${reactPort}${pathname}${url.search}`;
|
||||
|
||||
console.log(`[React Proxy] Proxying ${pathname} -> ${reactUrl}`);
|
||||
|
||||
try {
|
||||
// Convert headers to plain object for fetch
|
||||
const proxyHeaders: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(req.headers)) {
|
||||
if (typeof value === 'string') {
|
||||
proxyHeaders[key] = value;
|
||||
} else if (Array.isArray(value)) {
|
||||
proxyHeaders[key] = value.join(', ');
|
||||
}
|
||||
}
|
||||
proxyHeaders['host'] = `localhost:${reactPort}`;
|
||||
|
||||
const reactResponse = await fetch(reactUrl, {
|
||||
method: req.method,
|
||||
headers: proxyHeaders,
|
||||
body: req.method !== 'GET' && req.method !== 'HEAD' ? await readRequestBody(req) : undefined,
|
||||
});
|
||||
|
||||
const contentType = reactResponse.headers.get('content-type') || 'text/html';
|
||||
const body = await reactResponse.text();
|
||||
|
||||
console.log(`[React Proxy] Response ${reactResponse.status}: ${contentType}`);
|
||||
|
||||
res.writeHead(reactResponse.status, {
|
||||
'Content-Type': contentType,
|
||||
'Cache-Control': 'no-cache',
|
||||
});
|
||||
res.end(body);
|
||||
return;
|
||||
} catch (err) {
|
||||
console.error(`[React Proxy] Failed to proxy to ${reactUrl}:`, err);
|
||||
console.error(`[React Proxy] Error details:`, (err as Error).message);
|
||||
res.writeHead(502, { 'Content-Type': 'text/plain' });
|
||||
res.end(`Bad Gateway: React frontend not available at ${reactUrl}\nError: ${(err as Error).message}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect root to React if react-only mode
|
||||
if (frontend === 'react' && (pathname === '/' || pathname === '/index.html')) {
|
||||
res.writeHead(302, { 'Location': `/react${url.search}` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Root path - serve JS frontend HTML (default or both mode)
|
||||
if (pathname === '/' || pathname === '/index.html') {
|
||||
const html = generateServerDashboard(initialPath);
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(html);
|
||||
return;
|
||||
}
|
||||
|
||||
// 404
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||
res.end('Not Found');
|
||||
|
||||
@@ -144,18 +144,56 @@ export async function startReactFrontend(port: number): Promise<void> {
|
||||
/**
|
||||
* Stop React frontend development server
|
||||
*/
|
||||
export function stopReactFrontend(): void {
|
||||
export async function stopReactFrontend(): Promise<void> {
|
||||
if (reactProcess) {
|
||||
console.log(chalk.yellow(' Stopping React frontend...'));
|
||||
|
||||
// Try graceful shutdown first
|
||||
reactProcess.kill('SIGTERM');
|
||||
|
||||
// Force kill after timeout
|
||||
setTimeout(() => {
|
||||
if (reactProcess && !reactProcess.killed) {
|
||||
|
||||
// Wait up to 5 seconds for graceful shutdown
|
||||
await new Promise<void>((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
resolve();
|
||||
}, 5000);
|
||||
|
||||
reactProcess?.once('exit', () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// Force kill if still running
|
||||
if (reactProcess && !reactProcess.killed) {
|
||||
// On Windows with shell: true, we need to kill the entire process group
|
||||
if (process.platform === 'win32') {
|
||||
try {
|
||||
// Use taskkill to forcefully terminate the process tree
|
||||
const { exec } = await import('child_process');
|
||||
const pid = reactProcess.pid;
|
||||
if (pid) {
|
||||
await new Promise<void>((resolve) => {
|
||||
exec(`taskkill /F /T /PID ${pid}`, (err) => {
|
||||
if (err) {
|
||||
// Fallback to SIGKILL if taskkill fails
|
||||
reactProcess?.kill('SIGKILL');
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Fallback to SIGKILL
|
||||
reactProcess.kill('SIGKILL');
|
||||
}
|
||||
} else {
|
||||
reactProcess.kill('SIGKILL');
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
}
|
||||
|
||||
// Wait a bit more for force kill to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
reactProcess = null;
|
||||
reactPort = null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user