"use client"; import { useEffect, useState, useCallback, useRef } from "react"; import { useAppStore } from "@/store/app-store"; import { getElectronAPI } from "@/lib/electron"; import { Button } from "@/components/ui/button"; import { HotkeyButton } from "@/components/ui/hotkey-button"; import { Card } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Save, RefreshCw, FileText, Sparkles, Loader2, FilePlus2, AlertCircle, CheckCircle2, } from "lucide-react"; import { toast } from "sonner"; import { Checkbox } from "@/components/ui/checkbox"; import { XmlSyntaxEditor } from "@/components/ui/xml-syntax-editor"; import type { SpecRegenerationEvent } from "@/types/electron"; // Delay before reloading spec file to ensure it's written to disk const SPEC_FILE_WRITE_DELAY = 500; // Interval for polling backend status during generation const STATUS_CHECK_INTERVAL_MS = 2000; export function SpecView() { const { currentProject, appSpec, setAppSpec } = useAppStore(); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [hasChanges, setHasChanges] = useState(false); const [specExists, setSpecExists] = useState(true); // Regeneration state const [showRegenerateDialog, setShowRegenerateDialog] = useState(false); const [projectDefinition, setProjectDefinition] = useState(""); const [isRegenerating, setIsRegenerating] = useState(false); const [generateFeaturesOnRegenerate, setGenerateFeaturesOnRegenerate] = useState(true); const [analyzeProjectOnRegenerate, setAnalyzeProjectOnRegenerate] = useState(true); // Create spec state const [showCreateDialog, setShowCreateDialog] = useState(false); const [projectOverview, setProjectOverview] = useState(""); const [isCreating, setIsCreating] = useState(false); const [generateFeatures, setGenerateFeatures] = useState(true); const [analyzeProjectOnCreate, setAnalyzeProjectOnCreate] = useState(true); // Generate features only state const [isGeneratingFeatures, setIsGeneratingFeatures] = useState(false); // Logs state (kept for internal tracking, but UI removed) const [logs, setLogs] = useState(""); const logsRef = useRef(""); // Phase tracking and status const [currentPhase, setCurrentPhase] = useState(""); const [errorMessage, setErrorMessage] = useState(""); const statusCheckRef = useRef(false); const stateRestoredRef = useRef(false); const pendingStatusTimeoutRef = useRef(null); // Load spec from file const loadSpec = useCallback(async () => { if (!currentProject) return; setIsLoading(true); try { const api = getElectronAPI(); const result = await api.readFile( `${currentProject.path}/.automaker/app_spec.txt` ); if (result.success && result.content) { setAppSpec(result.content); setSpecExists(true); setHasChanges(false); } else { // File doesn't exist setAppSpec(""); setSpecExists(false); } } catch (error) { console.error("Failed to load spec:", error); setSpecExists(false); } finally { setIsLoading(false); } }, [currentProject, setAppSpec]); useEffect(() => { loadSpec(); }, [loadSpec]); // Reset all spec regeneration state when project changes useEffect(() => { // Clear all state when switching projects setIsCreating(false); setIsRegenerating(false); setIsGeneratingFeatures(false); setCurrentPhase(""); setErrorMessage(""); setLogs(""); logsRef.current = ""; stateRestoredRef.current = false; statusCheckRef.current = false; // Clear any pending timeout if (pendingStatusTimeoutRef.current) { clearTimeout(pendingStatusTimeoutRef.current); pendingStatusTimeoutRef.current = null; } }, [currentProject?.path]); // Check if spec regeneration is running when component mounts or project changes useEffect(() => { const checkStatus = async () => { if (!currentProject || statusCheckRef.current) return; statusCheckRef.current = true; try { const api = getElectronAPI(); if (!api.specRegeneration) { statusCheckRef.current = false; return; } const status = await api.specRegeneration.status(); console.log( "[SpecView] Status check on mount:", status, "for project:", currentProject.path ); if (status.success && status.isRunning) { // Something is running globally, but we can't verify it's for this project // since the backend doesn't track projectPath in status // Tentatively show loader - events will confirm if it's for this project console.log( "[SpecView] Spec generation is running globally. Tentatively showing loader, waiting for events to confirm project match." ); // Tentatively set state - events will confirm or clear it setIsCreating(true); setIsRegenerating(true); if (status.currentPhase) { setCurrentPhase(status.currentPhase); } else { setCurrentPhase("initialization"); } // Set a timeout to clear state if no events arrive for this project within 3 seconds if (pendingStatusTimeoutRef.current) { clearTimeout(pendingStatusTimeoutRef.current); } pendingStatusTimeoutRef.current = setTimeout(() => { // If no events confirmed this is for current project, clear state console.log( "[SpecView] No events received for current project - clearing tentative state" ); setIsCreating(false); setIsRegenerating(false); setCurrentPhase(""); pendingStatusTimeoutRef.current = null; }, 3000); } else if (status.success && !status.isRunning) { // Not running - clear all state setIsCreating(false); setIsRegenerating(false); setCurrentPhase(""); stateRestoredRef.current = false; } } catch (error) { console.error("[SpecView] Failed to check status:", error); } finally { statusCheckRef.current = false; } }; // Reset restoration flag when project changes stateRestoredRef.current = false; checkStatus(); }, [currentProject]); // Sync state when tab becomes visible (user returns to spec editor) useEffect(() => { const handleVisibilityChange = async () => { if ( !document.hidden && currentProject && (isCreating || isRegenerating || isGeneratingFeatures) ) { // Tab became visible and we think we're still generating - verify status from backend try { const api = getElectronAPI(); if (!api.specRegeneration) return; const status = await api.specRegeneration.status(); console.log("[SpecView] Visibility change - status check:", status); if (!status.isRunning) { // Backend says not running - clear state console.log( "[SpecView] Visibility change: Backend indicates generation complete - clearing state" ); setIsCreating(false); setIsRegenerating(false); setIsGeneratingFeatures(false); setCurrentPhase(""); stateRestoredRef.current = false; loadSpec(); } else if (status.currentPhase) { // Still running - update phase from backend setCurrentPhase(status.currentPhase); } } catch (error) { console.error( "[SpecView] Failed to check status on visibility change:", error ); } } }; document.addEventListener("visibilitychange", handleVisibilityChange); return () => { document.removeEventListener("visibilitychange", handleVisibilityChange); }; }, [ currentProject, isCreating, isRegenerating, isGeneratingFeatures, loadSpec, ]); // Periodic status check to ensure state stays in sync (only when we think we're running) useEffect(() => { if ( !currentProject || (!isCreating && !isRegenerating && !isGeneratingFeatures) ) return; const intervalId = setInterval(async () => { try { const api = getElectronAPI(); if (!api.specRegeneration) return; const status = await api.specRegeneration.status(); if (!status.isRunning) { // Backend says not running - clear state console.log( "[SpecView] Periodic check: Backend indicates generation complete - clearing state" ); setIsCreating(false); setIsRegenerating(false); setIsGeneratingFeatures(false); setCurrentPhase(""); stateRestoredRef.current = false; loadSpec(); } else if ( status.currentPhase && status.currentPhase !== currentPhase ) { // Still running but phase changed - update from backend console.log("[SpecView] Periodic check: Phase updated from backend", { old: currentPhase, new: status.currentPhase, }); setCurrentPhase(status.currentPhase); } } catch (error) { console.error("[SpecView] Periodic status check error:", error); } }, STATUS_CHECK_INTERVAL_MS); return () => { clearInterval(intervalId); }; }, [ currentProject, isCreating, isRegenerating, isGeneratingFeatures, currentPhase, loadSpec, ]); // Subscribe to spec regeneration events useEffect(() => { if (!currentProject) return; const api = getElectronAPI(); if (!api.specRegeneration) return; const unsubscribe = api.specRegeneration.onEvent( (event: SpecRegenerationEvent) => { console.log( "[SpecView] Regeneration event:", event.type, "for project:", event.projectPath, "current project:", currentProject?.path ); // Only handle events for the current project if (event.projectPath !== currentProject?.path) { console.log("[SpecView] Ignoring event - not for current project"); return; } // Clear any pending timeout since we received an event for this project if (pendingStatusTimeoutRef.current) { clearTimeout(pendingStatusTimeoutRef.current); pendingStatusTimeoutRef.current = null; console.log( "[SpecView] Event confirmed this is for current project - clearing timeout" ); } if (event.type === "spec_regeneration_progress") { // Ensure state is set when we receive events for this project setIsCreating(true); setIsRegenerating(true); // Extract phase from content if present const phaseMatch = event.content.match(/\[Phase:\s*([^\]]+)\]/); if (phaseMatch) { const phase = phaseMatch[1]; setCurrentPhase(phase); console.log(`[SpecView] Phase updated: ${phase}`); // If phase is "complete", clear running state immediately if (phase === "complete") { console.log("[SpecView] Phase is complete - clearing state"); setIsCreating(false); setIsRegenerating(false); stateRestoredRef.current = false; // Small delay to ensure spec file is written setTimeout(() => { loadSpec(); }, SPEC_FILE_WRITE_DELAY); } } // Check for completion indicators in content if ( event.content.includes("All tasks completed") || event.content.includes("✓ All tasks completed") ) { // This indicates everything is done - clear state immediately console.log( "[SpecView] Detected completion in progress message - clearing state" ); setIsCreating(false); setIsRegenerating(false); setCurrentPhase(""); stateRestoredRef.current = false; setTimeout(() => { loadSpec(); }, SPEC_FILE_WRITE_DELAY); } // Append progress to logs const newLog = logsRef.current + event.content; logsRef.current = newLog; setLogs(newLog); console.log("[SpecView] Progress:", event.content.substring(0, 100)); // Clear error message when we get new progress if (errorMessage) { setErrorMessage(""); } } else if (event.type === "spec_regeneration_tool") { // Check if this is a feature creation tool const isFeatureTool = event.tool === "mcp__automaker-tools__UpdateFeatureStatus" || event.tool === "UpdateFeatureStatus" || event.tool?.includes("Feature"); if (isFeatureTool) { // Ensure we're in feature generation phase if (currentPhase !== "feature_generation") { setCurrentPhase("feature_generation"); setIsCreating(true); setIsRegenerating(true); console.log( "[SpecView] Detected feature creation tool - setting phase to feature_generation" ); } } // Log tool usage with details const toolInput = event.input ? ` (${JSON.stringify(event.input).substring(0, 100)}...)` : ""; const toolLog = `\n[Tool] ${event.tool}${toolInput}\n`; const newLog = logsRef.current + toolLog; logsRef.current = newLog; setLogs(newLog); console.log("[SpecView] Tool:", event.tool, event.input); } else if (event.type === "spec_regeneration_complete") { // Add completion message to logs first const completionLog = logsRef.current + `\n[Complete] ${event.message}\n`; logsRef.current = completionLog; setLogs(completionLog); // --- Completion Detection Logic --- // The backend sends explicit signals for completion: // 1. "All tasks completed" in the message // 2. [Phase: complete] marker in logs // 3. "Spec regeneration complete!" for regeneration // 4. "Initial spec creation complete!" for creation without features const isFinalCompletionMessage = event.message?.includes("All tasks completed") || event.message === "All tasks completed!" || event.message === "All tasks completed" || event.message === "Spec regeneration complete!" || event.message === "Initial spec creation complete!"; const hasCompletePhase = logsRef.current.includes("[Phase: complete]"); // Intermediate completion means features are being generated after spec creation const isIntermediateCompletion = event.message?.includes("Features are being generated") || event.message?.includes("features are being generated"); // Rely solely on explicit backend signals const shouldComplete = (isFinalCompletionMessage || hasCompletePhase) && !isIntermediateCompletion; if (shouldComplete) { // Fully complete - clear all states immediately console.log( "[SpecView] Final completion detected - clearing state", { isFinalCompletionMessage, hasCompletePhase, message: event.message, } ); setIsRegenerating(false); setIsCreating(false); setIsGeneratingFeatures(false); setCurrentPhase(""); setShowRegenerateDialog(false); setShowCreateDialog(false); setProjectDefinition(""); setProjectOverview(""); setErrorMessage(""); stateRestoredRef.current = false; // Reload the spec with delay to ensure file is written to disk setTimeout(() => { loadSpec(); }, SPEC_FILE_WRITE_DELAY); // Show success toast notification 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", { description: isFeatureGeneration ? "Features have been created from the app specification." : "Your app specification has been saved.", icon: , } ); } else if (isIntermediateCompletion) { // Intermediate completion - keep state active for feature generation setIsCreating(true); setIsRegenerating(true); setCurrentPhase("feature_generation"); console.log( "[SpecView] Intermediate completion, continuing with feature generation" ); } console.log("[SpecView] Spec generation event:", event.message); } else if (event.type === "spec_regeneration_error") { setIsRegenerating(false); setIsCreating(false); setIsGeneratingFeatures(false); setCurrentPhase("error"); setErrorMessage(event.error); stateRestoredRef.current = false; // Reset restoration flag // Add error to logs const errorLog = logsRef.current + `\n\n[ERROR] ${event.error}\n`; logsRef.current = errorLog; setLogs(errorLog); console.error("[SpecView] Regeneration error:", event.error); } } ); return () => { unsubscribe(); }; }, [currentProject?.path, loadSpec, errorMessage, currentPhase]); // Save spec to file const saveSpec = async () => { if (!currentProject) return; setIsSaving(true); try { const api = getElectronAPI(); await api.writeFile( `${currentProject.path}/.automaker/app_spec.txt`, appSpec ); setHasChanges(false); } catch (error) { console.error("Failed to save spec:", error); } finally { setIsSaving(false); } }; const handleChange = (value: string) => { setAppSpec(value); setHasChanges(true); }; const handleRegenerate = async () => { if (!currentProject || !projectDefinition.trim()) return; setIsRegenerating(true); setShowRegenerateDialog(false); setCurrentPhase("initialization"); setErrorMessage(""); // Reset logs when starting new regeneration logsRef.current = ""; setLogs(""); console.log( "[SpecView] Starting spec regeneration, generateFeatures:", generateFeaturesOnRegenerate ); try { const api = getElectronAPI(); if (!api.specRegeneration) { console.error("[SpecView] Spec regeneration not available"); setIsRegenerating(false); return; } const result = await api.specRegeneration.generate( currentProject.path, projectDefinition.trim(), generateFeaturesOnRegenerate, analyzeProjectOnRegenerate ); if (!result.success) { const errorMsg = result.error || "Unknown error"; console.error("[SpecView] Failed to start regeneration:", errorMsg); setIsRegenerating(false); setCurrentPhase("error"); setErrorMessage(errorMsg); const errorLog = `[Error] Failed to start regeneration: ${errorMsg}\n`; logsRef.current = errorLog; setLogs(errorLog); } // If successful, we'll wait for the events to update the state } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); console.error("[SpecView] Failed to regenerate spec:", errorMsg); setIsRegenerating(false); setCurrentPhase("error"); setErrorMessage(errorMsg); const errorLog = `[Error] Failed to regenerate spec: ${errorMsg}\n`; logsRef.current = errorLog; setLogs(errorLog); } }; const handleCreateSpec = async () => { if (!currentProject || !projectOverview.trim()) return; setIsCreating(true); setShowCreateDialog(false); setCurrentPhase("initialization"); setErrorMessage(""); // Reset logs when starting new generation logsRef.current = ""; setLogs(""); console.log( "[SpecView] Starting spec creation, generateFeatures:", generateFeatures ); try { const api = getElectronAPI(); if (!api.specRegeneration) { console.error("[SpecView] Spec regeneration not available"); setIsCreating(false); return; } const result = await api.specRegeneration.create( currentProject.path, projectOverview.trim(), generateFeatures, analyzeProjectOnCreate ); if (!result.success) { const errorMsg = result.error || "Unknown error"; console.error("[SpecView] Failed to start spec creation:", errorMsg); setIsCreating(false); setCurrentPhase("error"); setErrorMessage(errorMsg); const errorLog = `[Error] Failed to start spec creation: ${errorMsg}\n`; logsRef.current = errorLog; setLogs(errorLog); } // If successful, we'll wait for the events to update the state } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); console.error("[SpecView] Failed to create spec:", errorMsg); setIsCreating(false); setCurrentPhase("error"); setErrorMessage(errorMsg); const errorLog = `[Error] Failed to create spec: ${errorMsg}\n`; logsRef.current = errorLog; setLogs(errorLog); } }; const handleGenerateFeatures = async () => { if (!currentProject) return; setIsGeneratingFeatures(true); setShowRegenerateDialog(false); setCurrentPhase("initialization"); setErrorMessage(""); // Reset logs when starting feature generation logsRef.current = ""; setLogs(""); console.log("[SpecView] Starting feature generation from existing spec"); try { const api = getElectronAPI(); if (!api.specRegeneration) { console.error("[SpecView] Spec regeneration not available"); setIsGeneratingFeatures(false); return; } const result = await api.specRegeneration.generateFeatures( currentProject.path ); if (!result.success) { const errorMsg = result.error || "Unknown error"; console.error( "[SpecView] Failed to start feature generation:", errorMsg ); setIsGeneratingFeatures(false); setCurrentPhase("error"); setErrorMessage(errorMsg); const errorLog = `[Error] Failed to start feature generation: ${errorMsg}\n`; logsRef.current = errorLog; setLogs(errorLog); } // If successful, we'll wait for the events to update the state } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); console.error("[SpecView] Failed to generate features:", errorMsg); setIsGeneratingFeatures(false); setCurrentPhase("error"); setErrorMessage(errorMsg); const errorLog = `[Error] Failed to generate features: ${errorMsg}\n`; logsRef.current = errorLog; setLogs(errorLog); } }; if (!currentProject) { return (

No project selected

); } if (isLoading) { return (
); } // Show empty state when no spec exists (isCreating is handled by bottom-right indicator in sidebar) if (!specExists) { return (
{/* Header */}

App Specification

{currentProject.path}/.automaker/app_spec.txt

{(isCreating || isRegenerating) && (
{isCreating ? "Generating Specification" : "Regenerating Specification"} {currentPhase && ( {currentPhase === "initialization" && "Initializing..."} {currentPhase === "setup" && "Setting up tools..."} {currentPhase === "analysis" && "Analyzing project structure..."} {currentPhase === "spec_complete" && "Spec created! Generating features..."} {currentPhase === "feature_generation" && "Creating features from roadmap..."} {currentPhase === "complete" && "Complete!"} {currentPhase === "error" && "Error occurred"} {![ "initialization", "setup", "analysis", "spec_complete", "feature_generation", "complete", "error", ].includes(currentPhase) && currentPhase} )}
)} {errorMessage && (
Error: {errorMessage}
)}
{/* Empty State */}
{isCreating ? ( ) : ( )}

{isCreating ? ( <>
Generating App Specification
{currentPhase && (
{currentPhase === "initialization" && "Initializing..."} {currentPhase === "setup" && "Setting up tools..."} {currentPhase === "analysis" && "Analyzing project structure..."} {currentPhase === "spec_complete" && "Spec created! Generating features..."} {currentPhase === "feature_generation" && "Creating features from roadmap..."} {currentPhase === "complete" && "Complete!"} {currentPhase === "error" && "Error occurred"} {![ "initialization", "setup", "analysis", "spec_complete", "feature_generation", "complete", "error", ].includes(currentPhase) && currentPhase}
)} ) : ( "No App Specification Found" )}

{isCreating ? currentPhase === "feature_generation" ? "The app specification has been created! Now generating features from the implementation roadmap..." : "We're analyzing your project and generating a comprehensive specification. This may take a few moments..." : "Create an app specification to help our system understand your project. We'll analyze your codebase and generate a comprehensive spec based on your description."}

{errorMessage && (

Error:

{errorMessage}

)} {!isCreating && (
)}
{/* Create Dialog */} { if (!open && !isCreating) { setShowCreateDialog(false); } }} > Create App Specification We didn't find an app_spec.txt file. Let us help you generate your app_spec.txt to help describe your project for our system. We'll analyze your project's tech stack and create a comprehensive specification.

Describe what your project does and what features you want to build. Be as detailed as you want - this will help us create a better specification.