mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 06:42:03 +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:
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { createLogger } from '@automaker/utils/logger';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
|
||||
@@ -6,6 +6,7 @@ const logger = createLogger('RunningAgents');
|
||||
|
||||
export function useRunningAgents() {
|
||||
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
|
||||
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
|
||||
useEffect(() => {
|
||||
const api = getElectronAPI();
|
||||
@@ -80,6 +91,41 @@ export function useRunningAgents() {
|
||||
};
|
||||
}, [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 {
|
||||
runningAgentsCount,
|
||||
};
|
||||
|
||||
@@ -56,6 +56,9 @@ export function SpecView() {
|
||||
// Feature generation
|
||||
isGeneratingFeatures,
|
||||
|
||||
// Sync
|
||||
isSyncing,
|
||||
|
||||
// Status
|
||||
currentPhase,
|
||||
errorMessage,
|
||||
@@ -63,6 +66,8 @@ export function SpecView() {
|
||||
// Handlers
|
||||
handleCreateSpec,
|
||||
handleRegenerate,
|
||||
handleGenerateFeatures,
|
||||
handleSync,
|
||||
} = useSpecGeneration({ loadSpec });
|
||||
|
||||
// Reset hasChanges when spec is reloaded
|
||||
@@ -86,10 +91,9 @@ export function SpecView() {
|
||||
);
|
||||
}
|
||||
|
||||
// Empty state - no spec exists or generation is running
|
||||
// When generation is running, we skip loading the spec to avoid 500 errors,
|
||||
// so we show the empty state with generation indicator
|
||||
if (!specExists || isGenerationRunning) {
|
||||
// Empty state - only show when spec doesn't exist AND no generation is running
|
||||
// If generation is running but no spec exists, show the generating UI
|
||||
if (!specExists) {
|
||||
// If generation is running (from loading hook check), ensure we show the generating UI
|
||||
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">
|
||||
<SpecHeader
|
||||
projectPath={currentProject.path}
|
||||
isRegenerating={isRegenerating}
|
||||
isRegenerating={isRegenerating || isGenerationRunning}
|
||||
isCreating={isCreating}
|
||||
isGeneratingFeatures={isGeneratingFeatures}
|
||||
isSyncing={isSyncing}
|
||||
isSaving={isSaving}
|
||||
hasChanges={hasChanges}
|
||||
currentPhase={currentPhase}
|
||||
currentPhase={currentPhase || (isGenerationRunning ? 'working' : '')}
|
||||
errorMessage={errorMessage}
|
||||
onRegenerateClick={() => setShowRegenerateDialog(true)}
|
||||
onGenerateFeaturesClick={handleGenerateFeatures}
|
||||
onSyncClick={handleSync}
|
||||
onSaveClick={saveSpec}
|
||||
showActionsPanel={showActionsPanel}
|
||||
onToggleActionsPanel={() => setShowActionsPanel(!showActionsPanel)}
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
HeaderActionsPanel,
|
||||
HeaderActionsPanelTrigger,
|
||||
} 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';
|
||||
|
||||
interface SpecHeaderProps {
|
||||
@@ -11,11 +11,14 @@ interface SpecHeaderProps {
|
||||
isRegenerating: boolean;
|
||||
isCreating: boolean;
|
||||
isGeneratingFeatures: boolean;
|
||||
isSyncing: boolean;
|
||||
isSaving: boolean;
|
||||
hasChanges: boolean;
|
||||
currentPhase: string;
|
||||
errorMessage: string;
|
||||
onRegenerateClick: () => void;
|
||||
onGenerateFeaturesClick: () => void;
|
||||
onSyncClick: () => void;
|
||||
onSaveClick: () => void;
|
||||
showActionsPanel: boolean;
|
||||
onToggleActionsPanel: () => void;
|
||||
@@ -26,16 +29,19 @@ export function SpecHeader({
|
||||
isRegenerating,
|
||||
isCreating,
|
||||
isGeneratingFeatures,
|
||||
isSyncing,
|
||||
isSaving,
|
||||
hasChanges,
|
||||
currentPhase,
|
||||
errorMessage,
|
||||
onRegenerateClick,
|
||||
onGenerateFeaturesClick,
|
||||
onSyncClick,
|
||||
onSaveClick,
|
||||
showActionsPanel,
|
||||
onToggleActionsPanel,
|
||||
}: SpecHeaderProps) {
|
||||
const isProcessing = isRegenerating || isCreating || isGeneratingFeatures;
|
||||
const isProcessing = isRegenerating || isCreating || isGeneratingFeatures || isSyncing;
|
||||
const phaseLabel = PHASE_LABELS[currentPhase] || currentPhase;
|
||||
|
||||
return (
|
||||
@@ -58,11 +64,13 @@ export function SpecHeader({
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 min-w-0">
|
||||
<span className="text-sm font-semibold text-primary leading-tight tracking-tight">
|
||||
{isGeneratingFeatures
|
||||
? 'Generating Features'
|
||||
: isCreating
|
||||
? 'Generating Specification'
|
||||
: 'Regenerating Specification'}
|
||||
{isSyncing
|
||||
? 'Syncing Specification'
|
||||
: isGeneratingFeatures
|
||||
? 'Generating Features'
|
||||
: isCreating
|
||||
? 'Generating Specification'
|
||||
: 'Regenerating Specification'}
|
||||
</span>
|
||||
{currentPhase && (
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
{/* Desktop: show actions inline */}
|
||||
<div className="hidden lg:flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onRegenerateClick}
|
||||
disabled={isProcessing}
|
||||
data-testid="regenerate-spec"
|
||||
>
|
||||
{isRegenerating ? (
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
{/* Desktop: show actions inline - hidden when processing since status card shows progress */}
|
||||
{!isProcessing && (
|
||||
<div className="hidden lg:flex gap-2">
|
||||
<Button size="sm" variant="outline" onClick={onSyncClick} data-testid="sync-spec">
|
||||
<RefreshCcw className="w-4 h-4 mr-2" />
|
||||
Sync
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onRegenerateClick}
|
||||
data-testid="regenerate-spec"
|
||||
>
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
{isRegenerating ? 'Regenerating...' : 'Regenerate'}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={onSaveClick}
|
||||
disabled={!hasChanges || isSaving || isProcessing}
|
||||
data-testid="save-spec"
|
||||
>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
{isSaving ? 'Saving...' : hasChanges ? 'Save Changes' : 'Saved'}
|
||||
</Button>
|
||||
</div>
|
||||
Regenerate
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onGenerateFeaturesClick}
|
||||
data-testid="generate-features"
|
||||
>
|
||||
<ListPlus className="w-4 h-4 mr-2" />
|
||||
Generate Features
|
||||
</Button>
|
||||
<Button
|
||||
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 */}
|
||||
<HeaderActionsPanelTrigger isOpen={showActionsPanel} onToggle={onToggleActionsPanel} />
|
||||
</div>
|
||||
@@ -142,11 +160,13 @@ export function SpecHeader({
|
||||
<Loader2 className="w-4 h-4 animate-spin text-primary shrink-0" />
|
||||
<div className="flex flex-col gap-0.5 min-w-0">
|
||||
<span className="text-sm font-medium text-primary">
|
||||
{isGeneratingFeatures
|
||||
? 'Generating Features'
|
||||
: isCreating
|
||||
? 'Generating Specification'
|
||||
: 'Regenerating Specification'}
|
||||
{isSyncing
|
||||
? 'Syncing Specification'
|
||||
: isGeneratingFeatures
|
||||
? 'Generating Features'
|
||||
: isCreating
|
||||
? 'Generating Specification'
|
||||
: 'Regenerating Specification'}
|
||||
</span>
|
||||
{currentPhase && <span className="text-xs text-muted-foreground">{phaseLabel}</span>}
|
||||
</div>
|
||||
@@ -161,29 +181,47 @@ export function SpecHeader({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start"
|
||||
onClick={onRegenerateClick}
|
||||
disabled={isProcessing}
|
||||
data-testid="regenerate-spec-mobile"
|
||||
>
|
||||
{isRegenerating ? (
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
{isRegenerating ? 'Regenerating...' : 'Regenerate'}
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full justify-start"
|
||||
onClick={onSaveClick}
|
||||
disabled={!hasChanges || isSaving || isProcessing}
|
||||
data-testid="save-spec-mobile"
|
||||
>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
{isSaving ? 'Saving...' : hasChanges ? 'Save Changes' : 'Saved'}
|
||||
</Button>
|
||||
{/* Hide action buttons when processing - status card shows progress */}
|
||||
{!isProcessing && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start"
|
||||
onClick={onSyncClick}
|
||||
data-testid="sync-spec-mobile"
|
||||
>
|
||||
<RefreshCcw className="w-4 h-4 mr-2" />
|
||||
Sync
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start"
|
||||
onClick={onRegenerateClick}
|
||||
data-testid="regenerate-spec-mobile"
|
||||
>
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
Regenerate
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -24,6 +24,7 @@ export const PHASE_LABELS: Record<string, string> = {
|
||||
analysis: 'Analyzing project structure...',
|
||||
spec_complete: 'Spec created! Generating features...',
|
||||
feature_generation: 'Creating features from roadmap...',
|
||||
working: 'Working...',
|
||||
complete: 'Complete!',
|
||||
error: 'Error occurred',
|
||||
};
|
||||
|
||||
@@ -39,6 +39,9 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
||||
// Generate features only state
|
||||
const [isGeneratingFeatures, setIsGeneratingFeatures] = useState(false);
|
||||
|
||||
// Sync state
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
|
||||
// Logs state (kept for internal tracking)
|
||||
const [logs, setLogs] = useState<string>('');
|
||||
const logsRef = useRef<string>('');
|
||||
@@ -55,6 +58,7 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
||||
setIsCreating(false);
|
||||
setIsRegenerating(false);
|
||||
setIsGeneratingFeatures(false);
|
||||
setIsSyncing(false);
|
||||
setCurrentPhase('');
|
||||
setErrorMessage('');
|
||||
setLogs('');
|
||||
@@ -135,7 +139,7 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
||||
if (
|
||||
!document.hidden &&
|
||||
currentProject &&
|
||||
(isCreating || isRegenerating || isGeneratingFeatures)
|
||||
(isCreating || isRegenerating || isGeneratingFeatures || isSyncing)
|
||||
) {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
@@ -151,6 +155,7 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
||||
setIsCreating(false);
|
||||
setIsRegenerating(false);
|
||||
setIsGeneratingFeatures(false);
|
||||
setIsSyncing(false);
|
||||
setCurrentPhase('');
|
||||
stateRestoredRef.current = false;
|
||||
loadSpec();
|
||||
@@ -167,11 +172,12 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, [currentProject, isCreating, isRegenerating, isGeneratingFeatures, loadSpec]);
|
||||
}, [currentProject, isCreating, isRegenerating, isGeneratingFeatures, isSyncing, loadSpec]);
|
||||
|
||||
// Periodic status check
|
||||
useEffect(() => {
|
||||
if (!currentProject || (!isCreating && !isRegenerating && !isGeneratingFeatures)) return;
|
||||
if (!currentProject || (!isCreating && !isRegenerating && !isGeneratingFeatures && !isSyncing))
|
||||
return;
|
||||
|
||||
const intervalId = setInterval(async () => {
|
||||
try {
|
||||
@@ -187,6 +193,7 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
||||
setIsCreating(false);
|
||||
setIsRegenerating(false);
|
||||
setIsGeneratingFeatures(false);
|
||||
setIsSyncing(false);
|
||||
setCurrentPhase('');
|
||||
stateRestoredRef.current = false;
|
||||
loadSpec();
|
||||
@@ -205,7 +212,15 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, [currentProject, isCreating, isRegenerating, isGeneratingFeatures, currentPhase, loadSpec]);
|
||||
}, [
|
||||
currentProject,
|
||||
isCreating,
|
||||
isRegenerating,
|
||||
isGeneratingFeatures,
|
||||
isSyncing,
|
||||
currentPhase,
|
||||
loadSpec,
|
||||
]);
|
||||
|
||||
// Subscribe to spec regeneration events
|
||||
useEffect(() => {
|
||||
@@ -317,7 +332,8 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
||||
event.message === 'All tasks completed!' ||
|
||||
event.message === 'All tasks completed' ||
|
||||
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]');
|
||||
|
||||
@@ -337,6 +353,7 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
||||
setIsRegenerating(false);
|
||||
setIsCreating(false);
|
||||
setIsGeneratingFeatures(false);
|
||||
setIsSyncing(false);
|
||||
setCurrentPhase('');
|
||||
setShowRegenerateDialog(false);
|
||||
setShowCreateDialog(false);
|
||||
@@ -349,18 +366,23 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
||||
loadSpec();
|
||||
}, SPEC_FILE_WRITE_DELAY);
|
||||
|
||||
const isSyncComplete = event.message?.includes('sync');
|
||||
const isRegeneration = event.message?.includes('regeneration');
|
||||
const isFeatureGeneration = event.message?.includes('Feature generation');
|
||||
toast.success(
|
||||
isFeatureGeneration
|
||||
? 'Feature Generation Complete'
|
||||
: isRegeneration
|
||||
? 'Spec Regeneration Complete'
|
||||
: 'Spec Creation Complete',
|
||||
isSyncComplete
|
||||
? 'Spec Sync Complete'
|
||||
: isFeatureGeneration
|
||||
? 'Feature Generation Complete'
|
||||
: isRegeneration
|
||||
? 'Spec Regeneration Complete'
|
||||
: 'Spec Creation Complete',
|
||||
{
|
||||
description: isFeatureGeneration
|
||||
? 'Features have been created from the app specification.'
|
||||
: 'Your app specification has been saved.',
|
||||
description: isSyncComplete
|
||||
? 'Your spec has been updated with the latest changes.'
|
||||
: isFeatureGeneration
|
||||
? 'Features have been created from the app specification.'
|
||||
: 'Your app specification has been saved.',
|
||||
icon: createElement(CheckCircle2, { className: 'w-4 h-4' }),
|
||||
}
|
||||
);
|
||||
@@ -378,6 +400,7 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
||||
setIsRegenerating(false);
|
||||
setIsCreating(false);
|
||||
setIsGeneratingFeatures(false);
|
||||
setIsSyncing(false);
|
||||
setCurrentPhase('error');
|
||||
setErrorMessage(event.error);
|
||||
stateRestoredRef.current = false;
|
||||
@@ -544,6 +567,46 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
||||
}
|
||||
}, [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 {
|
||||
// Dialog state
|
||||
showCreateDialog,
|
||||
@@ -576,6 +639,9 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
||||
// Feature generation state
|
||||
isGeneratingFeatures,
|
||||
|
||||
// Sync state
|
||||
isSyncing,
|
||||
|
||||
// Status state
|
||||
currentPhase,
|
||||
errorMessage,
|
||||
@@ -584,6 +650,7 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
||||
// Handlers
|
||||
handleCreateSpec,
|
||||
handleRegenerate,
|
||||
handleSync,
|
||||
handleGenerateFeatures,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,20 +18,21 @@ export function useSpecLoading() {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
|
||||
// Check if spec generation is running before trying to load
|
||||
// This prevents showing "No App Specification Found" during generation
|
||||
// Check if spec generation is running
|
||||
if (api.specRegeneration) {
|
||||
const status = await api.specRegeneration.status(currentProject.path);
|
||||
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);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
} else {
|
||||
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`);
|
||||
|
||||
if (result.success && result.content) {
|
||||
|
||||
@@ -437,6 +437,10 @@ export interface SpecRegenerationAPI {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
sync: (projectPath: string) => Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
stop: (projectPath?: string) => Promise<{ success: boolean; error?: string }>;
|
||||
status: (projectPath?: string) => Promise<{
|
||||
success: boolean;
|
||||
@@ -2742,6 +2746,30 @@ function createMockSpecRegenerationAPI(): SpecRegenerationAPI {
|
||||
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) => {
|
||||
mockSpecRegenerationRunning = false;
|
||||
mockSpecRegenerationPhase = '';
|
||||
|
||||
@@ -1882,6 +1882,7 @@ export class HttpApiClient implements ElectronAPI {
|
||||
projectPath,
|
||||
maxFeatures,
|
||||
}),
|
||||
sync: (projectPath: string) => this.post('/api/spec-regeneration/sync', { projectPath }),
|
||||
stop: (projectPath?: string) => this.post('/api/spec-regeneration/stop', { projectPath }),
|
||||
status: (projectPath?: string) =>
|
||||
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;
|
||||
}>;
|
||||
|
||||
sync: (projectPath: string) => Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
stop: (projectPath?: string) => Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
|
||||
@@ -67,6 +67,7 @@ export default defineConfig(({ command }) => {
|
||||
server: {
|
||||
host: process.env.HOST || '0.0.0.0',
|
||||
port: parseInt(process.env.TEST_PORT || '3007', 10),
|
||||
allowedHosts: true,
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
|
||||
Reference in New Issue
Block a user