feat: enhance spec regeneration management by project

- Refactored spec regeneration status tracking to support multiple projects using a Map for running states and abort controllers.
- Updated `getSpecRegenerationStatus` to accept a project path, allowing retrieval of status specific to a project.
- Modified `setRunningState` to manage running states and abort controllers per project.
- Adjusted related route handlers to utilize project-specific status checks and updates.
- Introduced a new Graph View page and integrated it into the routing structure.
- Enhanced UI components to reflect the current project’s spec generation state.
This commit is contained in:
webdevcody
2026-01-11 01:37:26 -05:00
parent 7115460804
commit 5f3db1f25e
25 changed files with 890 additions and 588 deletions

View File

@@ -6,26 +6,57 @@ import { createLogger } from '@automaker/utils';
const logger = createLogger('SpecRegeneration');
// Shared state for tracking generation status - private
let isRunning = false;
let currentAbortController: AbortController | null = null;
// Shared state for tracking generation status - scoped by project path
const runningProjects = new Map<string, boolean>();
const abortControllers = new Map<string, AbortController>();
/**
* Get the current running state
* Get the running state for a specific project
*/
export function getSpecRegenerationStatus(): {
export function getSpecRegenerationStatus(projectPath?: string): {
isRunning: boolean;
currentAbortController: AbortController | null;
projectPath?: string;
} {
return { isRunning, currentAbortController };
if (projectPath) {
return {
isRunning: runningProjects.get(projectPath) || false,
currentAbortController: abortControllers.get(projectPath) || null,
projectPath,
};
}
// Fallback: check if any project is running (for backward compatibility)
const isAnyRunning = Array.from(runningProjects.values()).some((running) => running);
return { isRunning: isAnyRunning, currentAbortController: null };
}
/**
* Set the running state and abort controller
* Get the project path that is currently running (if any)
*/
export function setRunningState(running: boolean, controller: AbortController | null = null): void {
isRunning = running;
currentAbortController = controller;
export function getRunningProjectPath(): string | null {
for (const [path, running] of runningProjects.entries()) {
if (running) return path;
}
return null;
}
/**
* Set the running state and abort controller for a specific project
*/
export function setRunningState(
projectPath: string,
running: boolean,
controller: AbortController | null = null
): void {
if (running) {
runningProjects.set(projectPath, true);
if (controller) {
abortControllers.set(projectPath, controller);
}
} else {
runningProjects.delete(projectPath);
abortControllers.delete(projectPath);
}
}
/**

View File

@@ -47,17 +47,17 @@ export function createCreateHandler(events: EventEmitter) {
return;
}
const { isRunning } = getSpecRegenerationStatus();
const { isRunning } = getSpecRegenerationStatus(projectPath);
if (isRunning) {
logger.warn('Generation already running, rejecting request');
res.json({ success: false, error: 'Spec generation already running' });
logger.warn('Generation already running for project:', projectPath);
res.json({ success: false, error: 'Spec generation already running for this project' });
return;
}
logAuthStatus('Before starting generation');
const abortController = new AbortController();
setRunningState(true, abortController);
setRunningState(projectPath, true, abortController);
logger.info('Starting background generation task...');
// Start generation in background
@@ -80,7 +80,7 @@ export function createCreateHandler(events: EventEmitter) {
})
.finally(() => {
logger.info('Generation task finished (success or error)');
setRunningState(false, null);
setRunningState(projectPath, false, null);
});
logger.info('Returning success response (generation running in background)');

View File

@@ -40,17 +40,17 @@ export function createGenerateFeaturesHandler(
return;
}
const { isRunning } = getSpecRegenerationStatus();
const { isRunning } = getSpecRegenerationStatus(projectPath);
if (isRunning) {
logger.warn('Generation already running, rejecting request');
res.json({ success: false, error: 'Generation already running' });
logger.warn('Generation already running for project:', projectPath);
res.json({ success: false, error: 'Generation already running for this project' });
return;
}
logAuthStatus('Before starting feature generation');
const abortController = new AbortController();
setRunningState(true, abortController);
setRunningState(projectPath, true, abortController);
logger.info('Starting background feature generation task...');
generateFeaturesFromSpec(projectPath, events, abortController, maxFeatures, settingsService)
@@ -63,7 +63,7 @@ export function createGenerateFeaturesHandler(
})
.finally(() => {
logger.info('Feature generation task finished (success or error)');
setRunningState(false, null);
setRunningState(projectPath, false, null);
});
logger.info('Returning success response (generation running in background)');

View File

@@ -48,17 +48,17 @@ export function createGenerateHandler(events: EventEmitter, settingsService?: Se
return;
}
const { isRunning } = getSpecRegenerationStatus();
const { isRunning } = getSpecRegenerationStatus(projectPath);
if (isRunning) {
logger.warn('Generation already running, rejecting request');
res.json({ success: false, error: 'Spec generation already running' });
logger.warn('Generation already running for project:', projectPath);
res.json({ success: false, error: 'Spec generation already running for this project' });
return;
}
logAuthStatus('Before starting generation');
const abortController = new AbortController();
setRunningState(true, abortController);
setRunningState(projectPath, true, abortController);
logger.info('Starting background generation task...');
generateSpec(
@@ -81,7 +81,7 @@ export function createGenerateHandler(events: EventEmitter, settingsService?: Se
})
.finally(() => {
logger.info('Generation task finished (success or error)');
setRunningState(false, null);
setRunningState(projectPath, false, null);
});
logger.info('Returning success response (generation running in background)');

View File

@@ -6,10 +6,11 @@ import type { Request, Response } from 'express';
import { getSpecRegenerationStatus, getErrorMessage } from '../common.js';
export function createStatusHandler() {
return async (_req: Request, res: Response): Promise<void> => {
return async (req: Request, res: Response): Promise<void> => {
try {
const { isRunning } = getSpecRegenerationStatus();
res.json({ success: true, isRunning });
const projectPath = req.query.projectPath as string | undefined;
const { isRunning } = getSpecRegenerationStatus(projectPath);
res.json({ success: true, isRunning, projectPath });
} catch (error) {
res.status(500).json({ success: false, error: getErrorMessage(error) });
}

View File

@@ -6,13 +6,16 @@ import type { Request, Response } from 'express';
import { getSpecRegenerationStatus, setRunningState, getErrorMessage } from '../common.js';
export function createStopHandler() {
return async (_req: Request, res: Response): Promise<void> => {
return async (req: Request, res: Response): Promise<void> => {
try {
const { currentAbortController } = getSpecRegenerationStatus();
const { projectPath } = req.body as { projectPath?: string };
const { currentAbortController } = getSpecRegenerationStatus(projectPath);
if (currentAbortController) {
currentAbortController.abort();
}
setRunningState(false, null);
if (projectPath) {
setRunningState(projectPath, false, null);
}
res.json({ success: true });
} catch (error) {
res.status(500).json({ success: false, error: getErrorMessage(error) });