mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 08:33:36 +00:00
feat: implement spec synchronization feature for improved project management
- Added a new `/sync` endpoint to synchronize the project specification with the current codebase and feature state. - Introduced `syncSpec` function to handle the synchronization logic, updating technology stack, implemented features, and roadmap phases. - Enhanced the running state management to track synchronization tasks alongside existing generation tasks. - Updated UI components to support synchronization actions, including loading indicators and status updates. - Improved logging and error handling for better visibility during sync operations. These changes enhance project management capabilities by ensuring that the specification remains up-to-date with the latest code and feature developments.
This commit is contained in:
@@ -463,3 +463,149 @@ export function fromSpecOutputFeatures(
|
|||||||
: {}),
|
: {}),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a roadmap phase extracted from XML
|
||||||
|
*/
|
||||||
|
export interface RoadmapPhase {
|
||||||
|
name: string;
|
||||||
|
status: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the technology stack from app_spec.txt XML content
|
||||||
|
*
|
||||||
|
* @param specContent - The full XML content
|
||||||
|
* @param options - Optional extraction options
|
||||||
|
* @returns Array of technology names
|
||||||
|
*/
|
||||||
|
export function extractTechnologyStack(
|
||||||
|
specContent: string,
|
||||||
|
options: ExtractXmlOptions = {}
|
||||||
|
): string[] {
|
||||||
|
const log = options.logger || logger;
|
||||||
|
|
||||||
|
const techSection = extractXmlSection(specContent, 'technology_stack', options);
|
||||||
|
if (!techSection) {
|
||||||
|
log.debug('No technology_stack section found');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const technologies = extractXmlElements(techSection, 'technology', options);
|
||||||
|
log.debug(`Extracted ${technologies.length} technologies`);
|
||||||
|
return technologies;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the technology_stack section in XML content
|
||||||
|
*
|
||||||
|
* @param specContent - The full XML content
|
||||||
|
* @param technologies - The new technology list
|
||||||
|
* @param options - Optional extraction options
|
||||||
|
* @returns Updated XML content
|
||||||
|
*/
|
||||||
|
export function updateTechnologyStack(
|
||||||
|
specContent: string,
|
||||||
|
technologies: string[],
|
||||||
|
options: ExtractXmlOptions = {}
|
||||||
|
): string {
|
||||||
|
const log = options.logger || logger;
|
||||||
|
const indent = ' ';
|
||||||
|
const i2 = indent.repeat(2);
|
||||||
|
|
||||||
|
// Generate new section content
|
||||||
|
const techXml = technologies
|
||||||
|
.map((t) => `${i2}<technology>${escapeXml(t)}</technology>`)
|
||||||
|
.join('\n');
|
||||||
|
const newSection = `<technology_stack>\n${techXml}\n${indent}</technology_stack>`;
|
||||||
|
|
||||||
|
// Check if section exists
|
||||||
|
const sectionRegex = /<technology_stack>[\s\S]*?<\/technology_stack>/;
|
||||||
|
|
||||||
|
if (sectionRegex.test(specContent)) {
|
||||||
|
log.debug('Replacing existing technology_stack section');
|
||||||
|
return specContent.replace(sectionRegex, newSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug('No technology_stack section found to update');
|
||||||
|
return specContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract roadmap phases from app_spec.txt XML content
|
||||||
|
*
|
||||||
|
* @param specContent - The full XML content
|
||||||
|
* @param options - Optional extraction options
|
||||||
|
* @returns Array of roadmap phases
|
||||||
|
*/
|
||||||
|
export function extractRoadmapPhases(
|
||||||
|
specContent: string,
|
||||||
|
options: ExtractXmlOptions = {}
|
||||||
|
): RoadmapPhase[] {
|
||||||
|
const log = options.logger || logger;
|
||||||
|
const phases: RoadmapPhase[] = [];
|
||||||
|
|
||||||
|
const roadmapSection = extractXmlSection(specContent, 'implementation_roadmap', options);
|
||||||
|
if (!roadmapSection) {
|
||||||
|
log.debug('No implementation_roadmap section found');
|
||||||
|
return phases;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract individual phase blocks
|
||||||
|
const phaseRegex = /<phase>([\s\S]*?)<\/phase>/g;
|
||||||
|
const phaseMatches = roadmapSection.matchAll(phaseRegex);
|
||||||
|
|
||||||
|
for (const phaseMatch of phaseMatches) {
|
||||||
|
const phaseContent = phaseMatch[1];
|
||||||
|
|
||||||
|
const nameMatch = phaseContent.match(/<name>([\s\S]*?)<\/name>/);
|
||||||
|
const name = nameMatch ? unescapeXml(nameMatch[1].trim()) : '';
|
||||||
|
|
||||||
|
const statusMatch = phaseContent.match(/<status>([\s\S]*?)<\/status>/);
|
||||||
|
const status = statusMatch ? unescapeXml(statusMatch[1].trim()) : 'pending';
|
||||||
|
|
||||||
|
const descMatch = phaseContent.match(/<description>([\s\S]*?)<\/description>/);
|
||||||
|
const description = descMatch ? unescapeXml(descMatch[1].trim()) : undefined;
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
phases.push({ name, status, description });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug(`Extracted ${phases.length} roadmap phases`);
|
||||||
|
return phases;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a roadmap phase status in XML content
|
||||||
|
*
|
||||||
|
* @param specContent - The full XML content
|
||||||
|
* @param phaseName - The name of the phase to update
|
||||||
|
* @param newStatus - The new status value
|
||||||
|
* @param options - Optional extraction options
|
||||||
|
* @returns Updated XML content
|
||||||
|
*/
|
||||||
|
export function updateRoadmapPhaseStatus(
|
||||||
|
specContent: string,
|
||||||
|
phaseName: string,
|
||||||
|
newStatus: string,
|
||||||
|
options: ExtractXmlOptions = {}
|
||||||
|
): string {
|
||||||
|
const log = options.logger || logger;
|
||||||
|
|
||||||
|
// Find the phase and update its status
|
||||||
|
// Match the phase block containing the specific name
|
||||||
|
const phaseRegex = new RegExp(
|
||||||
|
`(<phase>\\s*<name>\\s*${escapeXml(phaseName)}\\s*<\\/name>\\s*<status>)[\\s\\S]*?(<\\/status>)`,
|
||||||
|
'i'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (phaseRegex.test(specContent)) {
|
||||||
|
log.debug(`Updating phase "${phaseName}" status to "${newStatus}"`);
|
||||||
|
return specContent.replace(phaseRegex, `$1${escapeXml(newStatus)}$2`);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug(`Phase "${phaseName}" not found`);
|
||||||
|
return specContent;
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,8 +6,17 @@ import { createLogger } from '@automaker/utils';
|
|||||||
|
|
||||||
const logger = createLogger('SpecRegeneration');
|
const logger = createLogger('SpecRegeneration');
|
||||||
|
|
||||||
|
// Types for running generation
|
||||||
|
export type GenerationType = 'spec_regeneration' | 'feature_generation' | 'sync';
|
||||||
|
|
||||||
|
interface RunningGeneration {
|
||||||
|
isRunning: boolean;
|
||||||
|
type: GenerationType;
|
||||||
|
startedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Shared state for tracking generation status - scoped by project path
|
// Shared state for tracking generation status - scoped by project path
|
||||||
const runningProjects = new Map<string, boolean>();
|
const runningProjects = new Map<string, RunningGeneration>();
|
||||||
const abortControllers = new Map<string, AbortController>();
|
const abortControllers = new Map<string, AbortController>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,16 +26,21 @@ export function getSpecRegenerationStatus(projectPath?: string): {
|
|||||||
isRunning: boolean;
|
isRunning: boolean;
|
||||||
currentAbortController: AbortController | null;
|
currentAbortController: AbortController | null;
|
||||||
projectPath?: string;
|
projectPath?: string;
|
||||||
|
type?: GenerationType;
|
||||||
|
startedAt?: string;
|
||||||
} {
|
} {
|
||||||
if (projectPath) {
|
if (projectPath) {
|
||||||
|
const generation = runningProjects.get(projectPath);
|
||||||
return {
|
return {
|
||||||
isRunning: runningProjects.get(projectPath) || false,
|
isRunning: generation?.isRunning || false,
|
||||||
currentAbortController: abortControllers.get(projectPath) || null,
|
currentAbortController: abortControllers.get(projectPath) || null,
|
||||||
projectPath,
|
projectPath,
|
||||||
|
type: generation?.type,
|
||||||
|
startedAt: generation?.startedAt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// Fallback: check if any project is running (for backward compatibility)
|
// Fallback: check if any project is running (for backward compatibility)
|
||||||
const isAnyRunning = Array.from(runningProjects.values()).some((running) => running);
|
const isAnyRunning = Array.from(runningProjects.values()).some((g) => g.isRunning);
|
||||||
return { isRunning: isAnyRunning, currentAbortController: null };
|
return { isRunning: isAnyRunning, currentAbortController: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,10 +60,15 @@ export function getRunningProjectPath(): string | null {
|
|||||||
export function setRunningState(
|
export function setRunningState(
|
||||||
projectPath: string,
|
projectPath: string,
|
||||||
running: boolean,
|
running: boolean,
|
||||||
controller: AbortController | null = null
|
controller: AbortController | null = null,
|
||||||
|
type: GenerationType = 'spec_regeneration'
|
||||||
): void {
|
): void {
|
||||||
if (running) {
|
if (running) {
|
||||||
runningProjects.set(projectPath, true);
|
runningProjects.set(projectPath, {
|
||||||
|
isRunning: true,
|
||||||
|
type,
|
||||||
|
startedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
if (controller) {
|
if (controller) {
|
||||||
abortControllers.set(projectPath, controller);
|
abortControllers.set(projectPath, controller);
|
||||||
}
|
}
|
||||||
@@ -59,6 +78,33 @@ export function setRunningState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all running spec/feature generations for the running agents view
|
||||||
|
*/
|
||||||
|
export function getAllRunningGenerations(): Array<{
|
||||||
|
projectPath: string;
|
||||||
|
type: GenerationType;
|
||||||
|
startedAt: string;
|
||||||
|
}> {
|
||||||
|
const results: Array<{
|
||||||
|
projectPath: string;
|
||||||
|
type: GenerationType;
|
||||||
|
startedAt: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
for (const [projectPath, generation] of runningProjects.entries()) {
|
||||||
|
if (generation.isRunning) {
|
||||||
|
results.push({
|
||||||
|
projectPath,
|
||||||
|
type: generation.type,
|
||||||
|
startedAt: generation.startedAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to log authentication status
|
* Helper to log authentication status
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { parseAndCreateFeatures } from './parse-and-create-features.js';
|
|||||||
import { getAppSpecPath } from '@automaker/platform';
|
import { getAppSpecPath } from '@automaker/platform';
|
||||||
import type { SettingsService } from '../../services/settings-service.js';
|
import type { SettingsService } from '../../services/settings-service.js';
|
||||||
import { getAutoLoadClaudeMdSetting, getPromptCustomization } from '../../lib/settings-helpers.js';
|
import { getAutoLoadClaudeMdSetting, getPromptCustomization } from '../../lib/settings-helpers.js';
|
||||||
|
import { FeatureLoader } from '../../services/feature-loader.js';
|
||||||
|
|
||||||
const logger = createLogger('SpecRegeneration');
|
const logger = createLogger('SpecRegeneration');
|
||||||
|
|
||||||
@@ -56,13 +57,45 @@ export async function generateFeaturesFromSpec(
|
|||||||
// Get customized prompts from settings
|
// Get customized prompts from settings
|
||||||
const prompts = await getPromptCustomization(settingsService, '[FeatureGeneration]');
|
const prompts = await getPromptCustomization(settingsService, '[FeatureGeneration]');
|
||||||
|
|
||||||
|
// Load existing features to prevent duplicates
|
||||||
|
const featureLoader = new FeatureLoader();
|
||||||
|
const existingFeatures = await featureLoader.getAll(projectPath);
|
||||||
|
|
||||||
|
logger.info(`Found ${existingFeatures.length} existing features to exclude from generation`);
|
||||||
|
|
||||||
|
// Build existing features context for the prompt
|
||||||
|
let existingFeaturesContext = '';
|
||||||
|
if (existingFeatures.length > 0) {
|
||||||
|
const featuresList = existingFeatures
|
||||||
|
.map(
|
||||||
|
(f) =>
|
||||||
|
`- "${f.title}" (ID: ${f.id}): ${f.description?.substring(0, 100) || 'No description'}`
|
||||||
|
)
|
||||||
|
.join('\n');
|
||||||
|
existingFeaturesContext = `
|
||||||
|
|
||||||
|
## EXISTING FEATURES (DO NOT REGENERATE THESE)
|
||||||
|
|
||||||
|
The following ${existingFeatures.length} features already exist in the project. You MUST NOT generate features that duplicate or overlap with these:
|
||||||
|
|
||||||
|
${featuresList}
|
||||||
|
|
||||||
|
CRITICAL INSTRUCTIONS:
|
||||||
|
- DO NOT generate any features with the same or similar titles as the existing features listed above
|
||||||
|
- DO NOT generate features that cover the same functionality as existing features
|
||||||
|
- ONLY generate NEW features that are not yet in the system
|
||||||
|
- If a feature from the roadmap already exists, skip it entirely
|
||||||
|
- Generate unique feature IDs that do not conflict with existing IDs: ${existingFeatures.map((f) => f.id).join(', ')}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
const prompt = `Based on this project specification:
|
const prompt = `Based on this project specification:
|
||||||
|
|
||||||
${spec}
|
${spec}
|
||||||
|
${existingFeaturesContext}
|
||||||
${prompts.appSpec.generateFeaturesFromSpecPrompt}
|
${prompts.appSpec.generateFeaturesFromSpecPrompt}
|
||||||
|
|
||||||
Generate ${featureCount} features that build on each other logically.`;
|
Generate ${featureCount} NEW features that build on each other logically. Remember: ONLY generate features that DO NOT already exist.`;
|
||||||
|
|
||||||
logger.info('========== PROMPT BEING SENT ==========');
|
logger.info('========== PROMPT BEING SENT ==========');
|
||||||
logger.info(`Prompt length: ${prompt.length} chars`);
|
logger.info(`Prompt length: ${prompt.length} chars`);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { EventEmitter } from '../../lib/events.js';
|
|||||||
import { createCreateHandler } from './routes/create.js';
|
import { createCreateHandler } from './routes/create.js';
|
||||||
import { createGenerateHandler } from './routes/generate.js';
|
import { createGenerateHandler } from './routes/generate.js';
|
||||||
import { createGenerateFeaturesHandler } from './routes/generate-features.js';
|
import { createGenerateFeaturesHandler } from './routes/generate-features.js';
|
||||||
|
import { createSyncHandler } from './routes/sync.js';
|
||||||
import { createStopHandler } from './routes/stop.js';
|
import { createStopHandler } from './routes/stop.js';
|
||||||
import { createStatusHandler } from './routes/status.js';
|
import { createStatusHandler } from './routes/status.js';
|
||||||
import type { SettingsService } from '../../services/settings-service.js';
|
import type { SettingsService } from '../../services/settings-service.js';
|
||||||
@@ -20,6 +21,7 @@ export function createSpecRegenerationRoutes(
|
|||||||
router.post('/create', createCreateHandler(events));
|
router.post('/create', createCreateHandler(events));
|
||||||
router.post('/generate', createGenerateHandler(events, settingsService));
|
router.post('/generate', createGenerateHandler(events, settingsService));
|
||||||
router.post('/generate-features', createGenerateFeaturesHandler(events, settingsService));
|
router.post('/generate-features', createGenerateFeaturesHandler(events, settingsService));
|
||||||
|
router.post('/sync', createSyncHandler(events, settingsService));
|
||||||
router.post('/stop', createStopHandler());
|
router.post('/stop', createStopHandler());
|
||||||
router.get('/status', createStatusHandler());
|
router.get('/status', createStatusHandler());
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export function createGenerateFeaturesHandler(
|
|||||||
logAuthStatus('Before starting feature generation');
|
logAuthStatus('Before starting feature generation');
|
||||||
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
setRunningState(projectPath, true, abortController);
|
setRunningState(projectPath, true, abortController, 'feature_generation');
|
||||||
logger.info('Starting background feature generation task...');
|
logger.info('Starting background feature generation task...');
|
||||||
|
|
||||||
generateFeaturesFromSpec(projectPath, events, abortController, maxFeatures, settingsService)
|
generateFeaturesFromSpec(projectPath, events, abortController, maxFeatures, settingsService)
|
||||||
|
|||||||
76
apps/server/src/routes/app-spec/routes/sync.ts
Normal file
76
apps/server/src/routes/app-spec/routes/sync.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* POST /sync endpoint - Sync spec with codebase and features
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Request, Response } from 'express';
|
||||||
|
import type { EventEmitter } from '../../../lib/events.js';
|
||||||
|
import { createLogger } from '@automaker/utils';
|
||||||
|
import {
|
||||||
|
getSpecRegenerationStatus,
|
||||||
|
setRunningState,
|
||||||
|
logAuthStatus,
|
||||||
|
logError,
|
||||||
|
getErrorMessage,
|
||||||
|
} from '../common.js';
|
||||||
|
import { syncSpec } from '../sync-spec.js';
|
||||||
|
import type { SettingsService } from '../../../services/settings-service.js';
|
||||||
|
|
||||||
|
const logger = createLogger('SpecSync');
|
||||||
|
|
||||||
|
export function createSyncHandler(events: EventEmitter, settingsService?: SettingsService) {
|
||||||
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
|
logger.info('========== /sync endpoint called ==========');
|
||||||
|
logger.debug('Request body:', JSON.stringify(req.body, null, 2));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { projectPath } = req.body as {
|
||||||
|
projectPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.debug('projectPath:', projectPath);
|
||||||
|
|
||||||
|
if (!projectPath) {
|
||||||
|
logger.error('Missing projectPath parameter');
|
||||||
|
res.status(400).json({ success: false, error: 'projectPath required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isRunning } = getSpecRegenerationStatus(projectPath);
|
||||||
|
if (isRunning) {
|
||||||
|
logger.warn('Generation/sync already running for project:', projectPath);
|
||||||
|
res.json({ success: false, error: 'Operation already running for this project' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logAuthStatus('Before starting spec sync');
|
||||||
|
|
||||||
|
const abortController = new AbortController();
|
||||||
|
setRunningState(projectPath, true, abortController, 'sync');
|
||||||
|
logger.info('Starting background spec sync task...');
|
||||||
|
|
||||||
|
syncSpec(projectPath, events, abortController, settingsService)
|
||||||
|
.then((result) => {
|
||||||
|
logger.info('Spec sync completed successfully');
|
||||||
|
logger.info('Result:', JSON.stringify(result, null, 2));
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logError(error, 'Spec sync failed with error');
|
||||||
|
events.emit('spec-regeneration:event', {
|
||||||
|
type: 'spec_regeneration_error',
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
projectPath,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
logger.info('Spec sync task finished (success or error)');
|
||||||
|
setRunningState(projectPath, false, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('Returning success response (sync running in background)');
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
logError(error, 'Sync route handler failed');
|
||||||
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
307
apps/server/src/routes/app-spec/sync-spec.ts
Normal file
307
apps/server/src/routes/app-spec/sync-spec.ts
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
/**
|
||||||
|
* Sync spec with current codebase and feature state
|
||||||
|
*
|
||||||
|
* Updates the spec file based on:
|
||||||
|
* - Completed Automaker features
|
||||||
|
* - Code analysis for tech stack and implementations
|
||||||
|
* - Roadmap phase status updates
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as secureFs from '../../lib/secure-fs.js';
|
||||||
|
import type { EventEmitter } from '../../lib/events.js';
|
||||||
|
import { createLogger } from '@automaker/utils';
|
||||||
|
import { DEFAULT_PHASE_MODELS } from '@automaker/types';
|
||||||
|
import { resolvePhaseModel } from '@automaker/model-resolver';
|
||||||
|
import { streamingQuery } from '../../providers/simple-query-service.js';
|
||||||
|
import { getAppSpecPath } from '@automaker/platform';
|
||||||
|
import type { SettingsService } from '../../services/settings-service.js';
|
||||||
|
import { getAutoLoadClaudeMdSetting } from '../../lib/settings-helpers.js';
|
||||||
|
import { FeatureLoader } from '../../services/feature-loader.js';
|
||||||
|
import {
|
||||||
|
extractImplementedFeatures,
|
||||||
|
extractTechnologyStack,
|
||||||
|
extractRoadmapPhases,
|
||||||
|
updateImplementedFeaturesSection,
|
||||||
|
updateTechnologyStack,
|
||||||
|
updateRoadmapPhaseStatus,
|
||||||
|
type ImplementedFeature,
|
||||||
|
type RoadmapPhase,
|
||||||
|
} from '../../lib/xml-extractor.js';
|
||||||
|
import { getNotificationService } from '../../services/notification-service.js';
|
||||||
|
|
||||||
|
const logger = createLogger('SpecSync');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of a sync operation
|
||||||
|
*/
|
||||||
|
export interface SyncResult {
|
||||||
|
techStackUpdates: {
|
||||||
|
added: string[];
|
||||||
|
removed: string[];
|
||||||
|
};
|
||||||
|
implementedFeaturesUpdates: {
|
||||||
|
addedFromFeatures: string[];
|
||||||
|
removed: string[];
|
||||||
|
};
|
||||||
|
roadmapUpdates: Array<{ phaseName: string; newStatus: string }>;
|
||||||
|
summary: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync the spec with current codebase and feature state
|
||||||
|
*/
|
||||||
|
export async function syncSpec(
|
||||||
|
projectPath: string,
|
||||||
|
events: EventEmitter,
|
||||||
|
abortController: AbortController,
|
||||||
|
settingsService?: SettingsService
|
||||||
|
): Promise<SyncResult> {
|
||||||
|
logger.info('========== syncSpec() started ==========');
|
||||||
|
logger.info('projectPath:', projectPath);
|
||||||
|
|
||||||
|
const result: SyncResult = {
|
||||||
|
techStackUpdates: { added: [], removed: [] },
|
||||||
|
implementedFeaturesUpdates: { addedFromFeatures: [], removed: [] },
|
||||||
|
roadmapUpdates: [],
|
||||||
|
summary: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read existing spec
|
||||||
|
const specPath = getAppSpecPath(projectPath);
|
||||||
|
let specContent: string;
|
||||||
|
|
||||||
|
try {
|
||||||
|
specContent = (await secureFs.readFile(specPath, 'utf-8')) as string;
|
||||||
|
logger.info(`Spec loaded successfully (${specContent.length} chars)`);
|
||||||
|
} catch (readError) {
|
||||||
|
logger.error('Failed to read spec file:', readError);
|
||||||
|
events.emit('spec-regeneration:event', {
|
||||||
|
type: 'spec_regeneration_error',
|
||||||
|
error: 'No project spec found. Create or regenerate spec first.',
|
||||||
|
projectPath,
|
||||||
|
});
|
||||||
|
throw new Error('No project spec found');
|
||||||
|
}
|
||||||
|
|
||||||
|
events.emit('spec-regeneration:event', {
|
||||||
|
type: 'spec_regeneration_progress',
|
||||||
|
content: '[Phase: sync] Starting spec sync...\n',
|
||||||
|
projectPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract current state from spec
|
||||||
|
const currentImplementedFeatures = extractImplementedFeatures(specContent);
|
||||||
|
const currentTechStack = extractTechnologyStack(specContent);
|
||||||
|
const currentRoadmapPhases = extractRoadmapPhases(specContent);
|
||||||
|
|
||||||
|
logger.info(`Current spec has ${currentImplementedFeatures.length} implemented features`);
|
||||||
|
logger.info(`Current spec has ${currentTechStack.length} technologies`);
|
||||||
|
logger.info(`Current spec has ${currentRoadmapPhases.length} roadmap phases`);
|
||||||
|
|
||||||
|
// Load completed Automaker features
|
||||||
|
const featureLoader = new FeatureLoader();
|
||||||
|
const allFeatures = await featureLoader.getAll(projectPath);
|
||||||
|
const completedFeatures = allFeatures.filter(
|
||||||
|
(f) => f.status === 'completed' || f.status === 'verified'
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`Found ${completedFeatures.length} completed/verified features in Automaker`);
|
||||||
|
|
||||||
|
events.emit('spec-regeneration:event', {
|
||||||
|
type: 'spec_regeneration_progress',
|
||||||
|
content: `Found ${completedFeatures.length} completed features to sync...\n`,
|
||||||
|
projectPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build new implemented features list from completed Automaker features
|
||||||
|
const newImplementedFeatures: ImplementedFeature[] = [];
|
||||||
|
const existingNames = new Set(currentImplementedFeatures.map((f) => f.name.toLowerCase()));
|
||||||
|
|
||||||
|
for (const feature of completedFeatures) {
|
||||||
|
const name = feature.title || `Feature: ${feature.id}`;
|
||||||
|
if (!existingNames.has(name.toLowerCase())) {
|
||||||
|
newImplementedFeatures.push({
|
||||||
|
name,
|
||||||
|
description: feature.description || '',
|
||||||
|
});
|
||||||
|
result.implementedFeaturesUpdates.addedFromFeatures.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge: keep existing + add new from completed features
|
||||||
|
const mergedFeatures = [...currentImplementedFeatures, ...newImplementedFeatures];
|
||||||
|
|
||||||
|
// Update spec with merged features
|
||||||
|
if (result.implementedFeaturesUpdates.addedFromFeatures.length > 0) {
|
||||||
|
specContent = updateImplementedFeaturesSection(specContent, mergedFeatures);
|
||||||
|
logger.info(
|
||||||
|
`Added ${result.implementedFeaturesUpdates.addedFromFeatures.length} features to spec`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analyze codebase for tech stack updates using AI
|
||||||
|
events.emit('spec-regeneration:event', {
|
||||||
|
type: 'spec_regeneration_progress',
|
||||||
|
content: 'Analyzing codebase for technology updates...\n',
|
||||||
|
projectPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting(
|
||||||
|
projectPath,
|
||||||
|
settingsService,
|
||||||
|
'[SpecSync]'
|
||||||
|
);
|
||||||
|
|
||||||
|
const settings = await settingsService?.getGlobalSettings();
|
||||||
|
const phaseModelEntry =
|
||||||
|
settings?.phaseModels?.specGenerationModel || DEFAULT_PHASE_MODELS.specGenerationModel;
|
||||||
|
const { model, thinkingLevel } = resolvePhaseModel(phaseModelEntry);
|
||||||
|
|
||||||
|
// Use AI to analyze tech stack
|
||||||
|
const techAnalysisPrompt = `Analyze this project and return ONLY a JSON object with the current technology stack.
|
||||||
|
|
||||||
|
Current known technologies: ${currentTechStack.join(', ')}
|
||||||
|
|
||||||
|
Look at package.json, config files, and source code to identify:
|
||||||
|
- Frameworks (React, Vue, Express, etc.)
|
||||||
|
- Languages (TypeScript, JavaScript, Python, etc.)
|
||||||
|
- Build tools (Vite, Webpack, etc.)
|
||||||
|
- Databases (PostgreSQL, MongoDB, etc.)
|
||||||
|
- Key libraries and tools
|
||||||
|
|
||||||
|
Return ONLY this JSON format, no other text:
|
||||||
|
{
|
||||||
|
"technologies": ["Technology 1", "Technology 2", ...]
|
||||||
|
}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const techResult = await streamingQuery({
|
||||||
|
prompt: techAnalysisPrompt,
|
||||||
|
model,
|
||||||
|
cwd: projectPath,
|
||||||
|
maxTurns: 10,
|
||||||
|
allowedTools: ['Read', 'Glob', 'Grep'],
|
||||||
|
abortController,
|
||||||
|
thinkingLevel,
|
||||||
|
readOnly: true,
|
||||||
|
settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined,
|
||||||
|
onText: (text) => {
|
||||||
|
logger.debug(`Tech analysis text: ${text.substring(0, 100)}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Parse tech stack from response
|
||||||
|
const jsonMatch = techResult.text.match(/\{[\s\S]*"technologies"[\s\S]*\}/);
|
||||||
|
if (jsonMatch) {
|
||||||
|
const parsed = JSON.parse(jsonMatch[0]);
|
||||||
|
if (Array.isArray(parsed.technologies)) {
|
||||||
|
const newTechStack = parsed.technologies as string[];
|
||||||
|
|
||||||
|
// Calculate differences
|
||||||
|
const currentSet = new Set(currentTechStack.map((t) => t.toLowerCase()));
|
||||||
|
const newSet = new Set(newTechStack.map((t) => t.toLowerCase()));
|
||||||
|
|
||||||
|
for (const tech of newTechStack) {
|
||||||
|
if (!currentSet.has(tech.toLowerCase())) {
|
||||||
|
result.techStackUpdates.added.push(tech);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const tech of currentTechStack) {
|
||||||
|
if (!newSet.has(tech.toLowerCase())) {
|
||||||
|
result.techStackUpdates.removed.push(tech);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update spec with new tech stack if there are changes
|
||||||
|
if (
|
||||||
|
result.techStackUpdates.added.length > 0 ||
|
||||||
|
result.techStackUpdates.removed.length > 0
|
||||||
|
) {
|
||||||
|
specContent = updateTechnologyStack(specContent, newTechStack);
|
||||||
|
logger.info(
|
||||||
|
`Updated tech stack: +${result.techStackUpdates.added.length}, -${result.techStackUpdates.removed.length}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Failed to analyze tech stack:', error);
|
||||||
|
// Continue with other sync operations
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update roadmap phase statuses based on completed features
|
||||||
|
events.emit('spec-regeneration:event', {
|
||||||
|
type: 'spec_regeneration_progress',
|
||||||
|
content: 'Checking roadmap phase statuses...\n',
|
||||||
|
projectPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
// For each phase, check if all its features are completed
|
||||||
|
// This is a heuristic - we check if the phase name appears in any feature titles/descriptions
|
||||||
|
for (const phase of currentRoadmapPhases) {
|
||||||
|
if (phase.status === 'completed') continue; // Already completed
|
||||||
|
|
||||||
|
// Check if this phase should be marked as completed
|
||||||
|
// A phase is considered complete if we have completed features that mention it
|
||||||
|
const phaseNameLower = phase.name.toLowerCase();
|
||||||
|
const relatedCompletedFeatures = completedFeatures.filter(
|
||||||
|
(f) =>
|
||||||
|
f.title?.toLowerCase().includes(phaseNameLower) ||
|
||||||
|
f.description?.toLowerCase().includes(phaseNameLower) ||
|
||||||
|
f.category?.toLowerCase().includes(phaseNameLower)
|
||||||
|
);
|
||||||
|
|
||||||
|
// If we have related completed features and the phase is still pending/in_progress,
|
||||||
|
// update it to in_progress or completed based on feature count
|
||||||
|
if (relatedCompletedFeatures.length > 0 && phase.status !== 'completed') {
|
||||||
|
const newStatus = 'in_progress';
|
||||||
|
specContent = updateRoadmapPhaseStatus(specContent, phase.name, newStatus);
|
||||||
|
result.roadmapUpdates.push({ phaseName: phase.name, newStatus });
|
||||||
|
logger.info(`Updated phase "${phase.name}" to ${newStatus}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save updated spec
|
||||||
|
await secureFs.writeFile(specPath, specContent, 'utf-8');
|
||||||
|
logger.info('Spec saved successfully');
|
||||||
|
|
||||||
|
// Build summary
|
||||||
|
const summaryParts: string[] = [];
|
||||||
|
if (result.implementedFeaturesUpdates.addedFromFeatures.length > 0) {
|
||||||
|
summaryParts.push(
|
||||||
|
`Added ${result.implementedFeaturesUpdates.addedFromFeatures.length} implemented features`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (result.techStackUpdates.added.length > 0) {
|
||||||
|
summaryParts.push(`Added ${result.techStackUpdates.added.length} technologies`);
|
||||||
|
}
|
||||||
|
if (result.techStackUpdates.removed.length > 0) {
|
||||||
|
summaryParts.push(`Removed ${result.techStackUpdates.removed.length} technologies`);
|
||||||
|
}
|
||||||
|
if (result.roadmapUpdates.length > 0) {
|
||||||
|
summaryParts.push(`Updated ${result.roadmapUpdates.length} roadmap phases`);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.summary = summaryParts.length > 0 ? summaryParts.join(', ') : 'Spec is already up to date';
|
||||||
|
|
||||||
|
// Create notification
|
||||||
|
const notificationService = getNotificationService();
|
||||||
|
await notificationService.createNotification({
|
||||||
|
type: 'spec_regeneration_complete',
|
||||||
|
title: 'Spec Sync Complete',
|
||||||
|
message: result.summary,
|
||||||
|
projectPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
events.emit('spec-regeneration:event', {
|
||||||
|
type: 'spec_regeneration_complete',
|
||||||
|
message: `Spec sync complete! ${result.summary}`,
|
||||||
|
projectPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('========== syncSpec() completed ==========');
|
||||||
|
logger.info('Summary:', result.summary);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import type { AutoModeService } from '../../../services/auto-mode-service.js';
|
import type { AutoModeService } from '../../../services/auto-mode-service.js';
|
||||||
import { getBacklogPlanStatus, getRunningDetails } from '../../backlog-plan/common.js';
|
import { getBacklogPlanStatus, getRunningDetails } from '../../backlog-plan/common.js';
|
||||||
|
import { getAllRunningGenerations } from '../../app-spec/common.js';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
@@ -26,6 +27,36 @@ export function createIndexHandler(autoModeService: AutoModeService) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add spec/feature generation tasks
|
||||||
|
const specGenerations = getAllRunningGenerations();
|
||||||
|
for (const generation of specGenerations) {
|
||||||
|
let title: string;
|
||||||
|
let description: string;
|
||||||
|
|
||||||
|
switch (generation.type) {
|
||||||
|
case 'feature_generation':
|
||||||
|
title = 'Generating features from spec';
|
||||||
|
description = 'Creating features from the project specification';
|
||||||
|
break;
|
||||||
|
case 'sync':
|
||||||
|
title = 'Syncing spec with code';
|
||||||
|
description = 'Updating spec from codebase and completed features';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
title = 'Regenerating spec';
|
||||||
|
description = 'Analyzing project and generating specification';
|
||||||
|
}
|
||||||
|
|
||||||
|
runningAgents.push({
|
||||||
|
featureId: `spec-generation:${generation.projectPath}`,
|
||||||
|
projectPath: generation.projectPath,
|
||||||
|
projectName: path.basename(generation.projectPath),
|
||||||
|
isAutoMode: false,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
runningAgents,
|
runningAgents,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { createLogger } from '@automaker/utils/logger';
|
import { createLogger } from '@automaker/utils/logger';
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
import { getElectronAPI } from '@/lib/electron';
|
||||||
|
|
||||||
@@ -6,6 +6,7 @@ const logger = createLogger('RunningAgents');
|
|||||||
|
|
||||||
export function useRunningAgents() {
|
export function useRunningAgents() {
|
||||||
const [runningAgentsCount, setRunningAgentsCount] = useState(0);
|
const [runningAgentsCount, setRunningAgentsCount] = useState(0);
|
||||||
|
const fetchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
// Fetch running agents count function - used for initial load and event-driven updates
|
// Fetch running agents count function - used for initial load and event-driven updates
|
||||||
const fetchRunningAgentsCount = useCallback(async () => {
|
const fetchRunningAgentsCount = useCallback(async () => {
|
||||||
@@ -32,6 +33,16 @@ export function useRunningAgents() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Debounced fetch to avoid excessive API calls from frequent events
|
||||||
|
const debouncedFetchRunningAgentsCount = useCallback(() => {
|
||||||
|
if (fetchTimeoutRef.current) {
|
||||||
|
clearTimeout(fetchTimeoutRef.current);
|
||||||
|
}
|
||||||
|
fetchTimeoutRef.current = setTimeout(() => {
|
||||||
|
fetchRunningAgentsCount();
|
||||||
|
}, 300);
|
||||||
|
}, [fetchRunningAgentsCount]);
|
||||||
|
|
||||||
// Subscribe to auto-mode events to update running agents count in real-time
|
// Subscribe to auto-mode events to update running agents count in real-time
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
@@ -80,6 +91,41 @@ export function useRunningAgents() {
|
|||||||
};
|
};
|
||||||
}, [fetchRunningAgentsCount]);
|
}, [fetchRunningAgentsCount]);
|
||||||
|
|
||||||
|
// Subscribe to spec regeneration events to update running agents count
|
||||||
|
useEffect(() => {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (!api.specRegeneration) return;
|
||||||
|
|
||||||
|
fetchRunningAgentsCount();
|
||||||
|
|
||||||
|
const unsubscribe = api.specRegeneration.onEvent((event) => {
|
||||||
|
logger.debug('Spec regeneration event for running agents hook', {
|
||||||
|
type: event.type,
|
||||||
|
});
|
||||||
|
// When spec regeneration completes or errors, refresh immediately
|
||||||
|
if (event.type === 'spec_regeneration_complete' || event.type === 'spec_regeneration_error') {
|
||||||
|
fetchRunningAgentsCount();
|
||||||
|
}
|
||||||
|
// For progress events, use debounced fetch to avoid excessive calls
|
||||||
|
else if (event.type === 'spec_regeneration_progress') {
|
||||||
|
debouncedFetchRunningAgentsCount();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [fetchRunningAgentsCount, debouncedFetchRunningAgentsCount]);
|
||||||
|
|
||||||
|
// Cleanup timeout on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (fetchTimeoutRef.current) {
|
||||||
|
clearTimeout(fetchTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
runningAgentsCount,
|
runningAgentsCount,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ export function SpecView() {
|
|||||||
// Feature generation
|
// Feature generation
|
||||||
isGeneratingFeatures,
|
isGeneratingFeatures,
|
||||||
|
|
||||||
|
// Sync
|
||||||
|
isSyncing,
|
||||||
|
|
||||||
// Status
|
// Status
|
||||||
currentPhase,
|
currentPhase,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@@ -63,6 +66,8 @@ export function SpecView() {
|
|||||||
// Handlers
|
// Handlers
|
||||||
handleCreateSpec,
|
handleCreateSpec,
|
||||||
handleRegenerate,
|
handleRegenerate,
|
||||||
|
handleGenerateFeatures,
|
||||||
|
handleSync,
|
||||||
} = useSpecGeneration({ loadSpec });
|
} = useSpecGeneration({ loadSpec });
|
||||||
|
|
||||||
// Reset hasChanges when spec is reloaded
|
// Reset hasChanges when spec is reloaded
|
||||||
@@ -86,10 +91,9 @@ export function SpecView() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty state - no spec exists or generation is running
|
// Empty state - only show when spec doesn't exist AND no generation is running
|
||||||
// When generation is running, we skip loading the spec to avoid 500 errors,
|
// If generation is running but no spec exists, show the generating UI
|
||||||
// so we show the empty state with generation indicator
|
if (!specExists) {
|
||||||
if (!specExists || isGenerationRunning) {
|
|
||||||
// If generation is running (from loading hook check), ensure we show the generating UI
|
// If generation is running (from loading hook check), ensure we show the generating UI
|
||||||
const showAsGenerating = isCreating || isGenerationRunning;
|
const showAsGenerating = isCreating || isGenerationRunning;
|
||||||
|
|
||||||
@@ -127,14 +131,17 @@ export function SpecView() {
|
|||||||
<div className="flex-1 flex flex-col overflow-hidden content-bg" data-testid="spec-view">
|
<div className="flex-1 flex flex-col overflow-hidden content-bg" data-testid="spec-view">
|
||||||
<SpecHeader
|
<SpecHeader
|
||||||
projectPath={currentProject.path}
|
projectPath={currentProject.path}
|
||||||
isRegenerating={isRegenerating}
|
isRegenerating={isRegenerating || isGenerationRunning}
|
||||||
isCreating={isCreating}
|
isCreating={isCreating}
|
||||||
isGeneratingFeatures={isGeneratingFeatures}
|
isGeneratingFeatures={isGeneratingFeatures}
|
||||||
|
isSyncing={isSyncing}
|
||||||
isSaving={isSaving}
|
isSaving={isSaving}
|
||||||
hasChanges={hasChanges}
|
hasChanges={hasChanges}
|
||||||
currentPhase={currentPhase}
|
currentPhase={currentPhase || (isGenerationRunning ? 'working' : '')}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
onRegenerateClick={() => setShowRegenerateDialog(true)}
|
onRegenerateClick={() => setShowRegenerateDialog(true)}
|
||||||
|
onGenerateFeaturesClick={handleGenerateFeatures}
|
||||||
|
onSyncClick={handleSync}
|
||||||
onSaveClick={saveSpec}
|
onSaveClick={saveSpec}
|
||||||
showActionsPanel={showActionsPanel}
|
showActionsPanel={showActionsPanel}
|
||||||
onToggleActionsPanel={() => setShowActionsPanel(!showActionsPanel)}
|
onToggleActionsPanel={() => setShowActionsPanel(!showActionsPanel)}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
HeaderActionsPanel,
|
HeaderActionsPanel,
|
||||||
HeaderActionsPanelTrigger,
|
HeaderActionsPanelTrigger,
|
||||||
} from '@/components/ui/header-actions-panel';
|
} from '@/components/ui/header-actions-panel';
|
||||||
import { Save, Sparkles, Loader2, FileText, AlertCircle } from 'lucide-react';
|
import { Save, Sparkles, Loader2, FileText, AlertCircle, ListPlus, RefreshCcw } from 'lucide-react';
|
||||||
import { PHASE_LABELS } from '../constants';
|
import { PHASE_LABELS } from '../constants';
|
||||||
|
|
||||||
interface SpecHeaderProps {
|
interface SpecHeaderProps {
|
||||||
@@ -11,11 +11,14 @@ interface SpecHeaderProps {
|
|||||||
isRegenerating: boolean;
|
isRegenerating: boolean;
|
||||||
isCreating: boolean;
|
isCreating: boolean;
|
||||||
isGeneratingFeatures: boolean;
|
isGeneratingFeatures: boolean;
|
||||||
|
isSyncing: boolean;
|
||||||
isSaving: boolean;
|
isSaving: boolean;
|
||||||
hasChanges: boolean;
|
hasChanges: boolean;
|
||||||
currentPhase: string;
|
currentPhase: string;
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
onRegenerateClick: () => void;
|
onRegenerateClick: () => void;
|
||||||
|
onGenerateFeaturesClick: () => void;
|
||||||
|
onSyncClick: () => void;
|
||||||
onSaveClick: () => void;
|
onSaveClick: () => void;
|
||||||
showActionsPanel: boolean;
|
showActionsPanel: boolean;
|
||||||
onToggleActionsPanel: () => void;
|
onToggleActionsPanel: () => void;
|
||||||
@@ -26,16 +29,19 @@ export function SpecHeader({
|
|||||||
isRegenerating,
|
isRegenerating,
|
||||||
isCreating,
|
isCreating,
|
||||||
isGeneratingFeatures,
|
isGeneratingFeatures,
|
||||||
|
isSyncing,
|
||||||
isSaving,
|
isSaving,
|
||||||
hasChanges,
|
hasChanges,
|
||||||
currentPhase,
|
currentPhase,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
onRegenerateClick,
|
onRegenerateClick,
|
||||||
|
onGenerateFeaturesClick,
|
||||||
|
onSyncClick,
|
||||||
onSaveClick,
|
onSaveClick,
|
||||||
showActionsPanel,
|
showActionsPanel,
|
||||||
onToggleActionsPanel,
|
onToggleActionsPanel,
|
||||||
}: SpecHeaderProps) {
|
}: SpecHeaderProps) {
|
||||||
const isProcessing = isRegenerating || isCreating || isGeneratingFeatures;
|
const isProcessing = isRegenerating || isCreating || isGeneratingFeatures || isSyncing;
|
||||||
const phaseLabel = PHASE_LABELS[currentPhase] || currentPhase;
|
const phaseLabel = PHASE_LABELS[currentPhase] || currentPhase;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -58,11 +64,13 @@ export function SpecHeader({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1 min-w-0">
|
<div className="flex flex-col gap-1 min-w-0">
|
||||||
<span className="text-sm font-semibold text-primary leading-tight tracking-tight">
|
<span className="text-sm font-semibold text-primary leading-tight tracking-tight">
|
||||||
{isGeneratingFeatures
|
{isSyncing
|
||||||
? 'Generating Features'
|
? 'Syncing Specification'
|
||||||
: isCreating
|
: isGeneratingFeatures
|
||||||
? 'Generating Specification'
|
? 'Generating Features'
|
||||||
: 'Regenerating Specification'}
|
: isCreating
|
||||||
|
? 'Generating Specification'
|
||||||
|
: 'Regenerating Specification'}
|
||||||
</span>
|
</span>
|
||||||
{currentPhase && (
|
{currentPhase && (
|
||||||
<span className="text-xs text-muted-foreground/90 leading-tight font-medium">
|
<span className="text-xs text-muted-foreground/90 leading-tight font-medium">
|
||||||
@@ -99,32 +107,42 @@ export function SpecHeader({
|
|||||||
<span className="text-xs font-medium text-destructive">Error</span>
|
<span className="text-xs font-medium text-destructive">Error</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Desktop: show actions inline */}
|
{/* Desktop: show actions inline - hidden when processing since status card shows progress */}
|
||||||
<div className="hidden lg:flex gap-2">
|
{!isProcessing && (
|
||||||
<Button
|
<div className="hidden lg:flex gap-2">
|
||||||
size="sm"
|
<Button size="sm" variant="outline" onClick={onSyncClick} data-testid="sync-spec">
|
||||||
variant="outline"
|
<RefreshCcw className="w-4 h-4 mr-2" />
|
||||||
onClick={onRegenerateClick}
|
Sync
|
||||||
disabled={isProcessing}
|
</Button>
|
||||||
data-testid="regenerate-spec"
|
<Button
|
||||||
>
|
size="sm"
|
||||||
{isRegenerating ? (
|
variant="outline"
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
onClick={onRegenerateClick}
|
||||||
) : (
|
data-testid="regenerate-spec"
|
||||||
|
>
|
||||||
<Sparkles className="w-4 h-4 mr-2" />
|
<Sparkles className="w-4 h-4 mr-2" />
|
||||||
)}
|
Regenerate
|
||||||
{isRegenerating ? 'Regenerating...' : 'Regenerate'}
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
<Button
|
size="sm"
|
||||||
size="sm"
|
variant="outline"
|
||||||
onClick={onSaveClick}
|
onClick={onGenerateFeaturesClick}
|
||||||
disabled={!hasChanges || isSaving || isProcessing}
|
data-testid="generate-features"
|
||||||
data-testid="save-spec"
|
>
|
||||||
>
|
<ListPlus className="w-4 h-4 mr-2" />
|
||||||
<Save className="w-4 h-4 mr-2" />
|
Generate Features
|
||||||
{isSaving ? 'Saving...' : hasChanges ? 'Save Changes' : 'Saved'}
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
</div>
|
size="sm"
|
||||||
|
onClick={onSaveClick}
|
||||||
|
disabled={!hasChanges || isSaving}
|
||||||
|
data-testid="save-spec"
|
||||||
|
>
|
||||||
|
<Save className="w-4 h-4 mr-2" />
|
||||||
|
{isSaving ? 'Saving...' : hasChanges ? 'Save Changes' : 'Saved'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* Tablet/Mobile: show trigger for actions panel */}
|
{/* Tablet/Mobile: show trigger for actions panel */}
|
||||||
<HeaderActionsPanelTrigger isOpen={showActionsPanel} onToggle={onToggleActionsPanel} />
|
<HeaderActionsPanelTrigger isOpen={showActionsPanel} onToggle={onToggleActionsPanel} />
|
||||||
</div>
|
</div>
|
||||||
@@ -142,11 +160,13 @@ export function SpecHeader({
|
|||||||
<Loader2 className="w-4 h-4 animate-spin text-primary shrink-0" />
|
<Loader2 className="w-4 h-4 animate-spin text-primary shrink-0" />
|
||||||
<div className="flex flex-col gap-0.5 min-w-0">
|
<div className="flex flex-col gap-0.5 min-w-0">
|
||||||
<span className="text-sm font-medium text-primary">
|
<span className="text-sm font-medium text-primary">
|
||||||
{isGeneratingFeatures
|
{isSyncing
|
||||||
? 'Generating Features'
|
? 'Syncing Specification'
|
||||||
: isCreating
|
: isGeneratingFeatures
|
||||||
? 'Generating Specification'
|
? 'Generating Features'
|
||||||
: 'Regenerating Specification'}
|
: isCreating
|
||||||
|
? 'Generating Specification'
|
||||||
|
: 'Regenerating Specification'}
|
||||||
</span>
|
</span>
|
||||||
{currentPhase && <span className="text-xs text-muted-foreground">{phaseLabel}</span>}
|
{currentPhase && <span className="text-xs text-muted-foreground">{phaseLabel}</span>}
|
||||||
</div>
|
</div>
|
||||||
@@ -161,29 +181,47 @@ export function SpecHeader({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Button
|
{/* Hide action buttons when processing - status card shows progress */}
|
||||||
variant="outline"
|
{!isProcessing && (
|
||||||
className="w-full justify-start"
|
<>
|
||||||
onClick={onRegenerateClick}
|
<Button
|
||||||
disabled={isProcessing}
|
variant="outline"
|
||||||
data-testid="regenerate-spec-mobile"
|
className="w-full justify-start"
|
||||||
>
|
onClick={onSyncClick}
|
||||||
{isRegenerating ? (
|
data-testid="sync-spec-mobile"
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
>
|
||||||
) : (
|
<RefreshCcw className="w-4 h-4 mr-2" />
|
||||||
<Sparkles className="w-4 h-4 mr-2" />
|
Sync
|
||||||
)}
|
</Button>
|
||||||
{isRegenerating ? 'Regenerating...' : 'Regenerate'}
|
<Button
|
||||||
</Button>
|
variant="outline"
|
||||||
<Button
|
className="w-full justify-start"
|
||||||
className="w-full justify-start"
|
onClick={onRegenerateClick}
|
||||||
onClick={onSaveClick}
|
data-testid="regenerate-spec-mobile"
|
||||||
disabled={!hasChanges || isSaving || isProcessing}
|
>
|
||||||
data-testid="save-spec-mobile"
|
<Sparkles className="w-4 h-4 mr-2" />
|
||||||
>
|
Regenerate
|
||||||
<Save className="w-4 h-4 mr-2" />
|
</Button>
|
||||||
{isSaving ? 'Saving...' : hasChanges ? 'Save Changes' : 'Saved'}
|
<Button
|
||||||
</Button>
|
variant="outline"
|
||||||
|
className="w-full justify-start"
|
||||||
|
onClick={onGenerateFeaturesClick}
|
||||||
|
data-testid="generate-features-mobile"
|
||||||
|
>
|
||||||
|
<ListPlus className="w-4 h-4 mr-2" />
|
||||||
|
Generate Features
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="w-full justify-start"
|
||||||
|
onClick={onSaveClick}
|
||||||
|
disabled={!hasChanges || isSaving}
|
||||||
|
data-testid="save-spec-mobile"
|
||||||
|
>
|
||||||
|
<Save className="w-4 h-4 mr-2" />
|
||||||
|
{isSaving ? 'Saving...' : hasChanges ? 'Save Changes' : 'Saved'}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</HeaderActionsPanel>
|
</HeaderActionsPanel>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const PHASE_LABELS: Record<string, string> = {
|
|||||||
analysis: 'Analyzing project structure...',
|
analysis: 'Analyzing project structure...',
|
||||||
spec_complete: 'Spec created! Generating features...',
|
spec_complete: 'Spec created! Generating features...',
|
||||||
feature_generation: 'Creating features from roadmap...',
|
feature_generation: 'Creating features from roadmap...',
|
||||||
|
working: 'Working...',
|
||||||
complete: 'Complete!',
|
complete: 'Complete!',
|
||||||
error: 'Error occurred',
|
error: 'Error occurred',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
|||||||
// Generate features only state
|
// Generate features only state
|
||||||
const [isGeneratingFeatures, setIsGeneratingFeatures] = useState(false);
|
const [isGeneratingFeatures, setIsGeneratingFeatures] = useState(false);
|
||||||
|
|
||||||
|
// Sync state
|
||||||
|
const [isSyncing, setIsSyncing] = useState(false);
|
||||||
|
|
||||||
// Logs state (kept for internal tracking)
|
// Logs state (kept for internal tracking)
|
||||||
const [logs, setLogs] = useState<string>('');
|
const [logs, setLogs] = useState<string>('');
|
||||||
const logsRef = useRef<string>('');
|
const logsRef = useRef<string>('');
|
||||||
@@ -55,6 +58,7 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
|||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
setIsRegenerating(false);
|
setIsRegenerating(false);
|
||||||
setIsGeneratingFeatures(false);
|
setIsGeneratingFeatures(false);
|
||||||
|
setIsSyncing(false);
|
||||||
setCurrentPhase('');
|
setCurrentPhase('');
|
||||||
setErrorMessage('');
|
setErrorMessage('');
|
||||||
setLogs('');
|
setLogs('');
|
||||||
@@ -135,7 +139,7 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
|||||||
if (
|
if (
|
||||||
!document.hidden &&
|
!document.hidden &&
|
||||||
currentProject &&
|
currentProject &&
|
||||||
(isCreating || isRegenerating || isGeneratingFeatures)
|
(isCreating || isRegenerating || isGeneratingFeatures || isSyncing)
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
@@ -151,6 +155,7 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
|||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
setIsRegenerating(false);
|
setIsRegenerating(false);
|
||||||
setIsGeneratingFeatures(false);
|
setIsGeneratingFeatures(false);
|
||||||
|
setIsSyncing(false);
|
||||||
setCurrentPhase('');
|
setCurrentPhase('');
|
||||||
stateRestoredRef.current = false;
|
stateRestoredRef.current = false;
|
||||||
loadSpec();
|
loadSpec();
|
||||||
@@ -167,11 +172,12 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
|||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||||
};
|
};
|
||||||
}, [currentProject, isCreating, isRegenerating, isGeneratingFeatures, loadSpec]);
|
}, [currentProject, isCreating, isRegenerating, isGeneratingFeatures, isSyncing, loadSpec]);
|
||||||
|
|
||||||
// Periodic status check
|
// Periodic status check
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentProject || (!isCreating && !isRegenerating && !isGeneratingFeatures)) return;
|
if (!currentProject || (!isCreating && !isRegenerating && !isGeneratingFeatures && !isSyncing))
|
||||||
|
return;
|
||||||
|
|
||||||
const intervalId = setInterval(async () => {
|
const intervalId = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -187,6 +193,7 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
|||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
setIsRegenerating(false);
|
setIsRegenerating(false);
|
||||||
setIsGeneratingFeatures(false);
|
setIsGeneratingFeatures(false);
|
||||||
|
setIsSyncing(false);
|
||||||
setCurrentPhase('');
|
setCurrentPhase('');
|
||||||
stateRestoredRef.current = false;
|
stateRestoredRef.current = false;
|
||||||
loadSpec();
|
loadSpec();
|
||||||
@@ -205,7 +212,15 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
|||||||
return () => {
|
return () => {
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
};
|
};
|
||||||
}, [currentProject, isCreating, isRegenerating, isGeneratingFeatures, currentPhase, loadSpec]);
|
}, [
|
||||||
|
currentProject,
|
||||||
|
isCreating,
|
||||||
|
isRegenerating,
|
||||||
|
isGeneratingFeatures,
|
||||||
|
isSyncing,
|
||||||
|
currentPhase,
|
||||||
|
loadSpec,
|
||||||
|
]);
|
||||||
|
|
||||||
// Subscribe to spec regeneration events
|
// Subscribe to spec regeneration events
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -317,7 +332,8 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
|||||||
event.message === 'All tasks completed!' ||
|
event.message === 'All tasks completed!' ||
|
||||||
event.message === 'All tasks completed' ||
|
event.message === 'All tasks completed' ||
|
||||||
event.message === 'Spec regeneration complete!' ||
|
event.message === 'Spec regeneration complete!' ||
|
||||||
event.message === 'Initial spec creation complete!';
|
event.message === 'Initial spec creation complete!' ||
|
||||||
|
event.message?.includes('Spec sync complete');
|
||||||
|
|
||||||
const hasCompletePhase = logsRef.current.includes('[Phase: complete]');
|
const hasCompletePhase = logsRef.current.includes('[Phase: complete]');
|
||||||
|
|
||||||
@@ -337,6 +353,7 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
|||||||
setIsRegenerating(false);
|
setIsRegenerating(false);
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
setIsGeneratingFeatures(false);
|
setIsGeneratingFeatures(false);
|
||||||
|
setIsSyncing(false);
|
||||||
setCurrentPhase('');
|
setCurrentPhase('');
|
||||||
setShowRegenerateDialog(false);
|
setShowRegenerateDialog(false);
|
||||||
setShowCreateDialog(false);
|
setShowCreateDialog(false);
|
||||||
@@ -349,18 +366,23 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
|||||||
loadSpec();
|
loadSpec();
|
||||||
}, SPEC_FILE_WRITE_DELAY);
|
}, SPEC_FILE_WRITE_DELAY);
|
||||||
|
|
||||||
|
const isSyncComplete = event.message?.includes('sync');
|
||||||
const isRegeneration = event.message?.includes('regeneration');
|
const isRegeneration = event.message?.includes('regeneration');
|
||||||
const isFeatureGeneration = event.message?.includes('Feature generation');
|
const isFeatureGeneration = event.message?.includes('Feature generation');
|
||||||
toast.success(
|
toast.success(
|
||||||
isFeatureGeneration
|
isSyncComplete
|
||||||
? 'Feature Generation Complete'
|
? 'Spec Sync Complete'
|
||||||
: isRegeneration
|
: isFeatureGeneration
|
||||||
? 'Spec Regeneration Complete'
|
? 'Feature Generation Complete'
|
||||||
: 'Spec Creation Complete',
|
: isRegeneration
|
||||||
|
? 'Spec Regeneration Complete'
|
||||||
|
: 'Spec Creation Complete',
|
||||||
{
|
{
|
||||||
description: isFeatureGeneration
|
description: isSyncComplete
|
||||||
? 'Features have been created from the app specification.'
|
? 'Your spec has been updated with the latest changes.'
|
||||||
: 'Your app specification has been saved.',
|
: isFeatureGeneration
|
||||||
|
? 'Features have been created from the app specification.'
|
||||||
|
: 'Your app specification has been saved.',
|
||||||
icon: createElement(CheckCircle2, { className: 'w-4 h-4' }),
|
icon: createElement(CheckCircle2, { className: 'w-4 h-4' }),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -378,6 +400,7 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
|||||||
setIsRegenerating(false);
|
setIsRegenerating(false);
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
setIsGeneratingFeatures(false);
|
setIsGeneratingFeatures(false);
|
||||||
|
setIsSyncing(false);
|
||||||
setCurrentPhase('error');
|
setCurrentPhase('error');
|
||||||
setErrorMessage(event.error);
|
setErrorMessage(event.error);
|
||||||
stateRestoredRef.current = false;
|
stateRestoredRef.current = false;
|
||||||
@@ -544,6 +567,46 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
|||||||
}
|
}
|
||||||
}, [currentProject]);
|
}, [currentProject]);
|
||||||
|
|
||||||
|
const handleSync = useCallback(async () => {
|
||||||
|
if (!currentProject) return;
|
||||||
|
|
||||||
|
setIsSyncing(true);
|
||||||
|
setCurrentPhase('sync');
|
||||||
|
setErrorMessage('');
|
||||||
|
logsRef.current = '';
|
||||||
|
setLogs('');
|
||||||
|
logger.debug('[useSpecGeneration] Starting spec sync');
|
||||||
|
try {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (!api.specRegeneration) {
|
||||||
|
logger.error('[useSpecGeneration] Spec regeneration not available');
|
||||||
|
setIsSyncing(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await api.specRegeneration.sync(currentProject.path);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
const errorMsg = result.error || 'Unknown error';
|
||||||
|
logger.error('[useSpecGeneration] Failed to start spec sync:', errorMsg);
|
||||||
|
setIsSyncing(false);
|
||||||
|
setCurrentPhase('error');
|
||||||
|
setErrorMessage(errorMsg);
|
||||||
|
const errorLog = `[Error] Failed to start spec sync: ${errorMsg}\n`;
|
||||||
|
logsRef.current = errorLog;
|
||||||
|
setLogs(errorLog);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
|
logger.error('[useSpecGeneration] Failed to sync spec:', errorMsg);
|
||||||
|
setIsSyncing(false);
|
||||||
|
setCurrentPhase('error');
|
||||||
|
setErrorMessage(errorMsg);
|
||||||
|
const errorLog = `[Error] Failed to sync spec: ${errorMsg}\n`;
|
||||||
|
logsRef.current = errorLog;
|
||||||
|
setLogs(errorLog);
|
||||||
|
}
|
||||||
|
}, [currentProject]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Dialog state
|
// Dialog state
|
||||||
showCreateDialog,
|
showCreateDialog,
|
||||||
@@ -576,6 +639,9 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
|||||||
// Feature generation state
|
// Feature generation state
|
||||||
isGeneratingFeatures,
|
isGeneratingFeatures,
|
||||||
|
|
||||||
|
// Sync state
|
||||||
|
isSyncing,
|
||||||
|
|
||||||
// Status state
|
// Status state
|
||||||
currentPhase,
|
currentPhase,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@@ -584,6 +650,7 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
|||||||
// Handlers
|
// Handlers
|
||||||
handleCreateSpec,
|
handleCreateSpec,
|
||||||
handleRegenerate,
|
handleRegenerate,
|
||||||
|
handleSync,
|
||||||
handleGenerateFeatures,
|
handleGenerateFeatures,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,20 +18,21 @@ export function useSpecLoading() {
|
|||||||
try {
|
try {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
|
|
||||||
// Check if spec generation is running before trying to load
|
// Check if spec generation is running
|
||||||
// This prevents showing "No App Specification Found" during generation
|
|
||||||
if (api.specRegeneration) {
|
if (api.specRegeneration) {
|
||||||
const status = await api.specRegeneration.status(currentProject.path);
|
const status = await api.specRegeneration.status(currentProject.path);
|
||||||
if (status.success && status.isRunning) {
|
if (status.success && status.isRunning) {
|
||||||
logger.debug('Spec generation is running for this project, skipping load');
|
logger.debug('Spec generation is running for this project');
|
||||||
setIsGenerationRunning(true);
|
setIsGenerationRunning(true);
|
||||||
setIsLoading(false);
|
} else {
|
||||||
return;
|
setIsGenerationRunning(false);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
setIsGenerationRunning(false);
|
||||||
}
|
}
|
||||||
// Always reset when generation is not running (handles edge case where api.specRegeneration might not be available)
|
|
||||||
setIsGenerationRunning(false);
|
|
||||||
|
|
||||||
|
// Always try to load the spec file, even if generation is running
|
||||||
|
// This allows users to view their existing spec while generating features
|
||||||
const result = await api.readFile(`${currentProject.path}/.automaker/app_spec.txt`);
|
const result = await api.readFile(`${currentProject.path}/.automaker/app_spec.txt`);
|
||||||
|
|
||||||
if (result.success && result.content) {
|
if (result.success && result.content) {
|
||||||
|
|||||||
@@ -437,6 +437,10 @@ export interface SpecRegenerationAPI {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
}>;
|
}>;
|
||||||
|
sync: (projectPath: string) => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
stop: (projectPath?: string) => Promise<{ success: boolean; error?: string }>;
|
stop: (projectPath?: string) => Promise<{ success: boolean; error?: string }>;
|
||||||
status: (projectPath?: string) => Promise<{
|
status: (projectPath?: string) => Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -2742,6 +2746,30 @@ function createMockSpecRegenerationAPI(): SpecRegenerationAPI {
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
sync: async (projectPath: string) => {
|
||||||
|
if (mockSpecRegenerationRunning) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Spec sync is already running',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mockSpecRegenerationRunning = true;
|
||||||
|
console.log(`[Mock] Syncing spec for: ${projectPath}`);
|
||||||
|
|
||||||
|
// Simulate async spec sync (similar to feature generation but simpler)
|
||||||
|
setTimeout(() => {
|
||||||
|
emitSpecRegenerationEvent({
|
||||||
|
type: 'spec_regeneration_complete',
|
||||||
|
message: 'Spec synchronized successfully',
|
||||||
|
projectPath,
|
||||||
|
});
|
||||||
|
mockSpecRegenerationRunning = false;
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
|
||||||
stop: async (_projectPath?: string) => {
|
stop: async (_projectPath?: string) => {
|
||||||
mockSpecRegenerationRunning = false;
|
mockSpecRegenerationRunning = false;
|
||||||
mockSpecRegenerationPhase = '';
|
mockSpecRegenerationPhase = '';
|
||||||
|
|||||||
@@ -1882,6 +1882,7 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
projectPath,
|
projectPath,
|
||||||
maxFeatures,
|
maxFeatures,
|
||||||
}),
|
}),
|
||||||
|
sync: (projectPath: string) => this.post('/api/spec-regeneration/sync', { projectPath }),
|
||||||
stop: (projectPath?: string) => this.post('/api/spec-regeneration/stop', { projectPath }),
|
stop: (projectPath?: string) => this.post('/api/spec-regeneration/stop', { projectPath }),
|
||||||
status: (projectPath?: string) =>
|
status: (projectPath?: string) =>
|
||||||
this.get(
|
this.get(
|
||||||
|
|||||||
5
apps/ui/src/types/electron.d.ts
vendored
5
apps/ui/src/types/electron.d.ts
vendored
@@ -367,6 +367,11 @@ export interface SpecRegenerationAPI {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
sync: (projectPath: string) => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
stop: (projectPath?: string) => Promise<{
|
stop: (projectPath?: string) => Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export default defineConfig(({ command }) => {
|
|||||||
server: {
|
server: {
|
||||||
host: process.env.HOST || '0.0.0.0',
|
host: process.env.HOST || '0.0.0.0',
|
||||||
port: parseInt(process.env.TEST_PORT || '3007', 10),
|
port: parseInt(process.env.TEST_PORT || '3007', 10),
|
||||||
|
allowedHosts: true,
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
|
|||||||
@@ -685,6 +685,12 @@ Format as JSON:
|
|||||||
|
|
||||||
Generate features that build on each other logically.
|
Generate features that build on each other logically.
|
||||||
|
|
||||||
|
CRITICAL RULES:
|
||||||
|
- If an "EXISTING FEATURES" section is provided above, you MUST NOT generate any features that duplicate or overlap with those existing features
|
||||||
|
- Check each feature you generate against the existing features list - if it already exists, DO NOT include it
|
||||||
|
- Only generate truly NEW features that add value beyond what already exists
|
||||||
|
- Generate unique IDs that don't conflict with existing feature IDs
|
||||||
|
|
||||||
IMPORTANT: Do not ask for clarification. The specification is provided above. Generate the JSON immediately.`;
|
IMPORTANT: Do not ask for clarification. The specification is provided above. Generate the JSON immediately.`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1049,9 +1049,9 @@ fi
|
|||||||
case $MODE in
|
case $MODE in
|
||||||
web)
|
web)
|
||||||
export TEST_PORT="$WEB_PORT"
|
export TEST_PORT="$WEB_PORT"
|
||||||
export VITE_SERVER_URL="http://localhost:$SERVER_PORT"
|
export VITE_SERVER_URL="http://$HOSTNAME:$SERVER_PORT"
|
||||||
export PORT="$SERVER_PORT"
|
export PORT="$SERVER_PORT"
|
||||||
export CORS_ORIGIN="http://localhost:$WEB_PORT,http://127.0.0.1:$WEB_PORT"
|
export CORS_ORIGIN="http://$HOSTNAME:$WEB_PORT,http://127.0.0.1:$WEB_PORT"
|
||||||
export VITE_APP_MODE="1"
|
export VITE_APP_MODE="1"
|
||||||
|
|
||||||
if [ "$PRODUCTION_MODE" = true ]; then
|
if [ "$PRODUCTION_MODE" = true ]; then
|
||||||
@@ -1067,7 +1067,7 @@ case $MODE in
|
|||||||
max_retries=30
|
max_retries=30
|
||||||
server_ready=false
|
server_ready=false
|
||||||
for ((i=0; i<max_retries; i++)); do
|
for ((i=0; i<max_retries; i++)); do
|
||||||
if curl -s "http://localhost:$SERVER_PORT/api/health" > /dev/null 2>&1; then
|
if curl -s "http://$HOSTNAME:$SERVER_PORT/api/health" > /dev/null 2>&1; then
|
||||||
server_ready=true
|
server_ready=true
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user