(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,
};
diff --git a/apps/ui/src/components/views/spec-view.tsx b/apps/ui/src/components/views/spec-view.tsx
index 67725a64..616dc4dd 100644
--- a/apps/ui/src/components/views/spec-view.tsx
+++ b/apps/ui/src/components/views/spec-view.tsx
@@ -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() {
setShowRegenerateDialog(true)}
+ onGenerateFeaturesClick={handleGenerateFeatures}
+ onSyncClick={handleSync}
onSaveClick={saveSpec}
showActionsPanel={showActionsPanel}
onToggleActionsPanel={() => setShowActionsPanel(!showActionsPanel)}
diff --git a/apps/ui/src/components/views/spec-view/components/spec-header.tsx b/apps/ui/src/components/views/spec-view/components/spec-header.tsx
index 37132701..b38a6579 100644
--- a/apps/ui/src/components/views/spec-view/components/spec-header.tsx
+++ b/apps/ui/src/components/views/spec-view/components/spec-header.tsx
@@ -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({
- {isGeneratingFeatures
- ? 'Generating Features'
- : isCreating
- ? 'Generating Specification'
- : 'Regenerating Specification'}
+ {isSyncing
+ ? 'Syncing Specification'
+ : isGeneratingFeatures
+ ? 'Generating Features'
+ : isCreating
+ ? 'Generating Specification'
+ : 'Regenerating Specification'}
{currentPhase && (
@@ -99,32 +107,42 @@ export function SpecHeader({
Error
)}
- {/* Desktop: show actions inline */}
-
-
+
+
+ Generate Features
+
+
+
+ {isSaving ? 'Saving...' : hasChanges ? 'Save Changes' : 'Saved'}
+
+
+ )}
{/* Tablet/Mobile: show trigger for actions panel */}
@@ -142,11 +160,13 @@ export function SpecHeader({
- {isGeneratingFeatures
- ? 'Generating Features'
- : isCreating
- ? 'Generating Specification'
- : 'Regenerating Specification'}
+ {isSyncing
+ ? 'Syncing Specification'
+ : isGeneratingFeatures
+ ? 'Generating Features'
+ : isCreating
+ ? 'Generating Specification'
+ : 'Regenerating Specification'}
{currentPhase && {phaseLabel}}
@@ -161,29 +181,47 @@ export function SpecHeader({
)}
-
- {isRegenerating ? (
-
- ) : (
-
- )}
- {isRegenerating ? 'Regenerating...' : 'Regenerate'}
-
-
-
- {isSaving ? 'Saving...' : hasChanges ? 'Save Changes' : 'Saved'}
-
+ {/* Hide action buttons when processing - status card shows progress */}
+ {!isProcessing && (
+ <>
+
+
+ Sync
+
+
+
+ Regenerate
+
+
+
+ Generate Features
+
+
+
+ {isSaving ? 'Saving...' : hasChanges ? 'Save Changes' : 'Saved'}
+
+ >
+ )}
>
);
diff --git a/apps/ui/src/components/views/spec-view/constants.ts b/apps/ui/src/components/views/spec-view/constants.ts
index 5b4a5a4a..f6c32a80 100644
--- a/apps/ui/src/components/views/spec-view/constants.ts
+++ b/apps/ui/src/components/views/spec-view/constants.ts
@@ -24,6 +24,7 @@ export const PHASE_LABELS: Record = {
analysis: 'Analyzing project structure...',
spec_complete: 'Spec created! Generating features...',
feature_generation: 'Creating features from roadmap...',
+ working: 'Working...',
complete: 'Complete!',
error: 'Error occurred',
};
diff --git a/apps/ui/src/components/views/spec-view/hooks/use-spec-generation.ts b/apps/ui/src/components/views/spec-view/hooks/use-spec-generation.ts
index 69054125..30a8150f 100644
--- a/apps/ui/src/components/views/spec-view/hooks/use-spec-generation.ts
+++ b/apps/ui/src/components/views/spec-view/hooks/use-spec-generation.ts
@@ -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('');
const logsRef = useRef('');
@@ -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,
};
}
diff --git a/apps/ui/src/components/views/spec-view/hooks/use-spec-loading.ts b/apps/ui/src/components/views/spec-view/hooks/use-spec-loading.ts
index 4343e300..9fc09b81 100644
--- a/apps/ui/src/components/views/spec-view/hooks/use-spec-loading.ts
+++ b/apps/ui/src/components/views/spec-view/hooks/use-spec-loading.ts
@@ -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) {
diff --git a/apps/ui/src/lib/electron.ts b/apps/ui/src/lib/electron.ts
index fd9f8588..97167e89 100644
--- a/apps/ui/src/lib/electron.ts
+++ b/apps/ui/src/lib/electron.ts
@@ -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 = '';
diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts
index 1b13baae..547fee7f 100644
--- a/apps/ui/src/lib/http-api-client.ts
+++ b/apps/ui/src/lib/http-api-client.ts
@@ -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(
diff --git a/apps/ui/src/types/electron.d.ts b/apps/ui/src/types/electron.d.ts
index 054f7d4b..42e4200d 100644
--- a/apps/ui/src/types/electron.d.ts
+++ b/apps/ui/src/types/electron.d.ts
@@ -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;
diff --git a/apps/ui/vite.config.mts b/apps/ui/vite.config.mts
index 71a70cda..0d18997e 100644
--- a/apps/ui/vite.config.mts
+++ b/apps/ui/vite.config.mts
@@ -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',
diff --git a/libs/prompts/src/defaults.ts b/libs/prompts/src/defaults.ts
index a582313d..f9849813 100644
--- a/libs/prompts/src/defaults.ts
+++ b/libs/prompts/src/defaults.ts
@@ -685,6 +685,12 @@ Format as JSON:
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.`;
/**
diff --git a/start-automaker.sh b/start-automaker.sh
index b0664716..cff17b87 100755
--- a/start-automaker.sh
+++ b/start-automaker.sh
@@ -1049,9 +1049,9 @@ fi
case $MODE in
web)
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 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"
if [ "$PRODUCTION_MODE" = true ]; then
@@ -1067,7 +1067,7 @@ case $MODE in
max_retries=30
server_ready=false
for ((i=0; i /dev/null 2>&1; then
+ if curl -s "http://$HOSTNAME:$SERVER_PORT/api/health" > /dev/null 2>&1; then
server_ready=true
break
fi