Complete overhaul for app spec system. Created logic to auto generate kanban stories after the fact as well as added logging logic and visual aids to tell what stage of the process the app spec creation is in. May need refinement for state-based updates as the menu doesnt update as dynamicly as id like

This commit is contained in:
trueheads
2025-12-11 03:01:45 -06:00
parent acae5526b7
commit c198c10244
11 changed files with 1508 additions and 137 deletions

View File

@@ -208,7 +208,19 @@ export function LogViewer({ output, className }: LogViewerProps) {
};
if (entries.length === 0) {
return null;
return (
<div className="flex items-center justify-center p-8 text-muted-foreground">
<div className="text-center">
<Info className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p className="text-sm">No log entries yet. Logs will appear here as the process runs.</p>
{output && output.trim() && (
<div className="mt-4 p-3 bg-zinc-900/50 rounded text-xs font-mono text-left max-h-40 overflow-auto">
<pre className="whitespace-pre-wrap">{output}</pre>
</div>
)}
</div>
</div>
);
}
// Count entries by type

View File

@@ -393,10 +393,10 @@ export const KanbanCard = memo(function KanbanCard({
!isDescriptionExpanded && "line-clamp-3"
)}
>
{feature.description}
{feature.description || feature.summary || feature.title || feature.id}
</CardTitle>
{/* Show More/Less toggle - only show when description is likely truncated */}
{feature.description.length > 100 && (
{(feature.description || feature.summary || feature.title || "").length > 100 && (
<button
onClick={(e) => {
e.stopPropagation();
@@ -427,7 +427,7 @@ export const KanbanCard = memo(function KanbanCard({
</CardHeader>
<CardContent className="p-3 pt-0">
{/* Steps Preview - Show in Standard and Detailed modes */}
{showSteps && feature.steps.length > 0 && (
{showSteps && feature.steps && feature.steps.length > 0 && (
<div className="mb-3 space-y-1">
{feature.steps.slice(0, 3).map((step, index) => (
<div
@@ -844,10 +844,13 @@ export const KanbanCard = memo(function KanbanCard({
<Sparkles className="w-5 h-5 text-green-400" />
Implementation Summary
</DialogTitle>
<DialogDescription className="text-sm" title={feature.description}>
{feature.description.length > 100
? `${feature.description.slice(0, 100)}...`
: feature.description}
<DialogDescription className="text-sm" title={feature.description || feature.summary || feature.title || ""}>
{(() => {
const displayText = feature.description || feature.summary || feature.title || "No description";
return displayText.length > 100
? `${displayText.slice(0, 100)}...`
: displayText;
})()}
</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-y-auto p-4 bg-card rounded-lg border border-border">

View File

@@ -1,6 +1,6 @@
"use client";
import { useEffect, useState, useCallback } from "react";
import { useEffect, useState, useCallback, useRef } from "react";
import { useAppStore } from "@/store/app-store";
import { getElectronAPI } from "@/lib/electron";
import { Button } from "@/components/ui/button";
@@ -14,7 +14,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Save, RefreshCw, FileText, Sparkles, Loader2, FilePlus2 } from "lucide-react";
import { Save, RefreshCw, FileText, Sparkles, Loader2, FilePlus2, AlertCircle, ListPlus } from "lucide-react";
import { Checkbox } from "@/components/ui/checkbox";
import { XmlSyntaxEditor } from "@/components/ui/xml-syntax-editor";
import type { SpecRegenerationEvent } from "@/types/electron";
@@ -36,6 +36,19 @@ export function SpecView() {
const [projectOverview, setProjectOverview] = useState("");
const [isCreating, setIsCreating] = useState(false);
const [generateFeatures, setGenerateFeatures] = useState(true);
// Generate features only state
const [isGeneratingFeatures, setIsGeneratingFeatures] = useState(false);
// Logs state (kept for internal tracking, but UI removed)
const [logs, setLogs] = useState<string>("");
const logsRef = useRef<string>("");
// Phase tracking and status
const [currentPhase, setCurrentPhase] = useState<string>("");
const [errorMessage, setErrorMessage] = useState<string>("");
const statusCheckRef = useRef<boolean>(false);
const stateRestoredRef = useRef<boolean>(false);
// Load spec from file
const loadSpec = useCallback(async () => {
@@ -69,6 +82,244 @@ export function SpecView() {
loadSpec();
}, [loadSpec]);
// 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);
if (status.success && status.isRunning) {
// Something is running - restore state
console.log("[SpecView] Spec generation is running - restoring state");
// Only restore state if we haven't already done so for this project
// This prevents resetting state when switching tabs
if (!stateRestoredRef.current) {
setIsCreating(true);
setIsRegenerating(true);
stateRestoredRef.current = true;
}
// Try to extract the current phase from existing logs
let detectedPhase = "";
if (logsRef.current) {
// Look for the most recent phase in the logs
const phaseMatches = logsRef.current.matchAll(/\[Phase:\s*([^\]]+)\]/g);
const phases = Array.from(phaseMatches);
if (phases.length > 0) {
// Get the last phase mentioned in the logs
detectedPhase = phases[phases.length - 1][1];
console.log(`[SpecView] Detected phase from logs: ${detectedPhase}`);
}
// Also check for feature generation indicators in logs
const hasFeatureGeneration = logsRef.current.includes("Feature Generation") ||
logsRef.current.includes("Feature Creation") ||
logsRef.current.includes("Creating feature") ||
logsRef.current.includes("feature_generation");
if (hasFeatureGeneration && !detectedPhase) {
detectedPhase = "feature_generation";
console.log("[SpecView] Detected feature generation from logs");
}
}
// Update phase from logs if we found one and don't have a specific phase set
// This allows the phase to update as new events come in
if (detectedPhase) {
setCurrentPhase((prevPhase) => {
// Only update if we don't have a phase or if the detected phase is more recent
if (!prevPhase || prevPhase === "unknown" || prevPhase === "in progress") {
return detectedPhase;
}
return prevPhase;
});
} else if (!currentPhase) {
// Use a more descriptive default instead of "unknown"
setCurrentPhase("in progress");
}
// Don't clear logs - they may have been persisted
if (!logsRef.current) {
const resumeMessage = "[Status] Resumed monitoring existing spec generation process...\n";
logsRef.current = resumeMessage;
setLogs(resumeMessage);
} else if (!logsRef.current.includes("Resumed monitoring")) {
// Add a resume message to existing logs only if not already present
const resumeMessage = "\n[Status] Resumed monitoring existing spec generation process...\n";
logsRef.current = logsRef.current + resumeMessage;
setLogs(logsRef.current);
}
} else if (status.success && !status.isRunning) {
// Check if we might still be in feature generation phase based on logs
const mightBeGeneratingFeatures = logsRef.current && (
logsRef.current.includes("Feature Generation") ||
logsRef.current.includes("Feature Creation") ||
logsRef.current.includes("Creating feature") ||
logsRef.current.includes("feature_generation") ||
currentPhase === "feature_generation"
);
if (mightBeGeneratingFeatures && specExists) {
// Spec exists and we might still be generating features - keep state active
console.log("[SpecView] Detected potential feature generation - keeping state active");
if (!isCreating && !isRegenerating) {
setIsCreating(true);
}
if (currentPhase !== "feature_generation") {
setCurrentPhase("feature_generation");
}
} else {
// Not running - clear running 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
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) {
// Not running but we think we are - check if we're truly done
// Look for recent activity in logs (within last 30 seconds worth of content)
const recentLogs = logsRef.current.slice(-5000); // Last ~5000 chars
const hasRecentFeatureActivity = recentLogs.includes("Feature Creation") ||
recentLogs.includes("Creating feature") ||
recentLogs.match(/\[Feature Creation\].*$/m);
// Check if we have a completion message or complete phase
const hasCompletion = logsRef.current.includes("All tasks completed") ||
logsRef.current.includes("[Complete] All tasks completed") ||
logsRef.current.includes("[Phase: complete]");
if (hasCompletion || (!hasRecentFeatureActivity && currentPhase !== "feature_generation")) {
// No recent activity and not running - we're done
console.log("[SpecView] Visibility change: Generation appears complete - clearing state");
setIsCreating(false);
setIsRegenerating(false);
setIsGeneratingFeatures(false);
setCurrentPhase("");
stateRestoredRef.current = false;
loadSpec();
} else if (currentPhase === "feature_generation" && !hasRecentFeatureActivity) {
// We were in feature generation but no recent activity - might be done
// Wait a moment and check again
setTimeout(async () => {
if (api.specRegeneration) {
const recheckStatus = await api.specRegeneration.status();
if (!recheckStatus.isRunning) {
console.log("[SpecView] Re-check after visibility: Still not running - clearing state");
setIsCreating(false);
setIsRegenerating(false);
setIsGeneratingFeatures(false);
setCurrentPhase("");
stateRestoredRef.current = false;
loadSpec();
}
}
}, 2000);
}
}
} 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, currentPhase, 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 not running but we think we are, verify we're truly done
if (!status.isRunning && (isCreating || isRegenerating || isGeneratingFeatures)) {
// Check logs for completion indicators
const hasCompletion = logsRef.current.includes("All tasks completed") ||
logsRef.current.includes("[Complete] All tasks completed") ||
logsRef.current.includes("[Phase: complete]") ||
currentPhase === "complete";
// Also check if we haven't seen feature activity recently
const recentLogs = logsRef.current.slice(-3000); // Last 3000 chars (more context)
const hasRecentFeatureActivity = recentLogs.includes("Feature Creation") ||
recentLogs.includes("Creating feature") ||
recentLogs.includes("UpdateFeatureStatus") ||
recentLogs.includes("[Tool]") && recentLogs.includes("UpdateFeatureStatus");
// If we're in feature_generation phase and not running, we're likely done
// (features are created via tool calls, so when stream ends, they're done)
const isFeatureGenComplete = currentPhase === "feature_generation" &&
!hasRecentFeatureActivity;
if (hasCompletion || isFeatureGenComplete) {
console.log("[SpecView] Periodic check: Generation complete - clearing state", {
hasCompletion,
hasRecentFeatureActivity,
currentPhase,
isFeatureGenComplete
});
setIsCreating(false);
setIsRegenerating(false);
setIsGeneratingFeatures(false);
setCurrentPhase("");
stateRestoredRef.current = false;
loadSpec();
}
}
} catch (error) {
console.error("[SpecView] Periodic status check error:", error);
}
}, 2000); // Check every 2 seconds (more frequent)
return () => {
clearInterval(intervalId);
};
}, [currentProject, isCreating, isRegenerating, isGeneratingFeatures, currentPhase, loadSpec]);
// Subscribe to spec regeneration events
useEffect(() => {
const api = getElectronAPI();
@@ -77,18 +328,151 @@ export function SpecView() {
const unsubscribe = api.specRegeneration.onEvent((event: SpecRegenerationEvent) => {
console.log("[SpecView] Regeneration event:", event.type);
if (event.type === "spec_regeneration_complete") {
setIsRegenerating(false);
setIsCreating(false);
setShowRegenerateDialog(false);
setShowCreateDialog(false);
setProjectDefinition("");
setProjectOverview("");
// Reload the spec to show the new content
loadSpec();
if (event.type === "spec_regeneration_progress") {
// 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();
}, 500);
}
}
// 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();
}, 500);
}
// 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 (before checking)
const completionLog = logsRef.current + `\n[Complete] ${event.message}\n`;
logsRef.current = completionLog;
setLogs(completionLog);
// Check if this is the final completion
const isFinalCompletion = event.message?.includes("All tasks completed") ||
event.message === "All tasks completed!" ||
event.message === "All tasks completed";
// Check if we've already seen a completion phase in logs (including the message we just added)
const hasSeenCompletePhase = logsRef.current.includes("[Phase: complete]");
// Check recent logs for feature activity
const recentLogs = logsRef.current.slice(-2000);
const hasRecentFeatureActivity = recentLogs.includes("Feature Creation") ||
recentLogs.includes("Creating feature") ||
recentLogs.includes("UpdateFeatureStatus");
// Check if we're still generating features (only for intermediate completion)
const isGeneratingFeatures = !isFinalCompletion &&
!hasSeenCompletePhase &&
(event.message?.includes("Features are being generated") ||
event.message?.includes("features are being generated"));
// If we're in feature_generation but no recent activity and we see completion, we're done
const shouldComplete = isFinalCompletion ||
hasSeenCompletePhase ||
(currentPhase === "feature_generation" && !hasRecentFeatureActivity && !isGeneratingFeatures);
if (shouldComplete) {
// Fully complete - clear all states immediately
console.log("[SpecView] Final completion detected - clearing state", {
isFinalCompletion,
hasSeenCompletePhase,
shouldComplete,
hasRecentFeatureActivity,
currentPhase,
message: event.message
});
setIsRegenerating(false);
setIsCreating(false);
setIsGeneratingFeatures(false);
setCurrentPhase("");
setShowRegenerateDialog(false);
setShowCreateDialog(false);
setProjectDefinition("");
setProjectOverview("");
setErrorMessage("");
stateRestoredRef.current = false;
// Reload the spec to show the new content
loadSpec();
} else {
// Intermediate completion - keep state active for feature generation
setIsCreating(true);
setIsRegenerating(true);
setCurrentPhase("feature_generation");
console.log("[SpecView] Spec complete, continuing with feature generation", {
isGeneratingFeatures,
hasRecentFeatureActivity,
currentPhase
});
}
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);
}
});
@@ -126,6 +510,12 @@ export function SpecView() {
if (!currentProject || !projectDefinition.trim()) return;
setIsRegenerating(true);
setCurrentPhase("initialization");
setErrorMessage("");
// Reset logs when starting new regeneration
logsRef.current = "";
setLogs("");
console.log("[SpecView] Starting spec regeneration");
try {
const api = getElectronAPI();
if (!api.specRegeneration) {
@@ -139,13 +529,25 @@ export function SpecView() {
);
if (!result.success) {
console.error("[SpecView] Failed to start regeneration:", result.error);
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) {
console.error("[SpecView] Failed to regenerate spec:", 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);
}
};
@@ -154,6 +556,12 @@ export function SpecView() {
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) {
@@ -168,13 +576,70 @@ export function SpecView() {
);
if (!result.success) {
console.error("[SpecView] Failed to start spec creation:", result.error);
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) {
console.error("[SpecView] Failed to create spec:", 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);
}
};
@@ -218,6 +683,36 @@ export function SpecView() {
</p>
</div>
</div>
{(isCreating || isRegenerating) && (
<div className="flex items-center gap-3 px-6 py-3.5 rounded-xl bg-gradient-to-r from-primary/15 to-primary/5 border border-primary/30 shadow-lg backdrop-blur-md">
<div className="relative">
<Loader2 className="w-5 h-5 animate-spin text-primary flex-shrink-0" />
<div className="absolute inset-0 w-5 h-5 animate-ping text-primary/20" />
</div>
<div className="flex flex-col gap-1 min-w-0">
<span className="text-sm font-semibold text-primary leading-tight tracking-tight">
{isCreating ? "Generating Specification" : "Regenerating Specification"}
</span>
{currentPhase && (
<span className="text-xs text-muted-foreground/90 leading-tight font-medium">
{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}
</span>
)}
</div>
</div>
)}
{errorMessage && (
<div className="flex items-center gap-2 text-destructive">
<span className="text-sm font-medium">Error: {errorMessage}</span>
</div>
)}
</div>
{/* Empty State */}
@@ -225,26 +720,74 @@ export function SpecView() {
<div className="text-center max-w-md">
<div className="mb-6 flex justify-center">
<div className="p-4 rounded-full bg-primary/10">
<FilePlus2 className="w-12 h-12 text-primary" />
{isCreating ? (
<Loader2 className="w-12 h-12 text-primary animate-spin" />
) : (
<FilePlus2 className="w-12 h-12 text-primary" />
)}
</div>
</div>
<h2 className="text-2xl font-semibold mb-3">No App Specification Found</h2>
<h2 className="text-2xl font-semibold mb-4">
{isCreating ? (
<>
<div className="mb-4">
<span>Generating App Specification</span>
</div>
{currentPhase && (
<div className="px-6 py-3.5 rounded-xl bg-gradient-to-r from-primary/15 to-primary/5 border border-primary/30 shadow-lg backdrop-blur-md inline-flex items-center justify-center">
<span className="text-sm font-semibold text-primary text-center tracking-tight">
{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}
</span>
</div>
)}
</>
) : (
"No App Specification Found"
)}
</h2>
<p className="text-muted-foreground mb-6">
Create an app specification to help our system understand your project.
We&apos;ll analyze your codebase and generate a comprehensive spec based on your description.
{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."}
</p>
<Button
size="lg"
onClick={() => setShowCreateDialog(true)}
>
<FilePlus2 className="w-5 h-5 mr-2" />
Create app_spec
</Button>
{errorMessage && (
<div className="mb-4 p-3 rounded-md bg-destructive/10 border border-destructive/20">
<p className="text-sm text-destructive font-medium">Error:</p>
<p className="text-sm text-destructive">{errorMessage}</p>
</div>
)}
{!isCreating && (
<div className="flex gap-2 justify-center">
<Button
size="lg"
onClick={() => setShowCreateDialog(true)}
>
<FilePlus2 className="w-5 h-5 mr-2" />
Create app_spec
</Button>
</div>
)}
</div>
</div>
{/* Create Dialog */}
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
<Dialog
open={showCreateDialog}
onOpenChange={(open) => {
if (!open && !isCreating) {
setShowCreateDialog(false);
}
}}
>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Create App Specification</DialogTitle>
@@ -270,6 +813,7 @@ export function SpecView() {
onChange={(e) => setProjectOverview(e.target.value)}
placeholder="e.g., A project management tool that allows teams to track tasks, manage sprints, and visualize progress through kanban boards. It should support user authentication, real-time updates, and file attachments..."
autoFocus
disabled={isCreating}
/>
</div>
@@ -278,11 +822,12 @@ export function SpecView() {
id="generate-features"
checked={generateFeatures}
onCheckedChange={(checked) => setGenerateFeatures(checked === true)}
disabled={isCreating}
/>
<div className="space-y-1">
<label
htmlFor="generate-features"
className="text-sm font-medium cursor-pointer"
className={`text-sm font-medium ${isCreating ? "" : "cursor-pointer"}`}
>
Generate feature list
</label>
@@ -298,17 +843,27 @@ export function SpecView() {
<Button
variant="ghost"
onClick={() => setShowCreateDialog(false)}
disabled={isCreating}
>
Cancel
</Button>
<HotkeyButton
onClick={handleCreateSpec}
disabled={!projectOverview.trim()}
disabled={!projectOverview.trim() || isCreating}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkeyActive={showCreateDialog}
hotkeyActive={showCreateDialog && !isCreating}
>
<Sparkles className="w-4 h-4 mr-2" />
Generate Spec
{isCreating ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Generating...
</>
) : (
<>
<Sparkles className="w-4 h-4 mr-2" />
Generate Spec
</>
)}
</HotkeyButton>
</DialogFooter>
</DialogContent>
@@ -333,30 +888,66 @@ export function SpecView() {
</p>
</div>
</div>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => setShowRegenerateDialog(true)}
disabled={isRegenerating}
data-testid="regenerate-spec"
>
{isRegenerating ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Sparkles className="w-4 h-4 mr-2" />
)}
{isRegenerating ? "Regenerating..." : "Regenerate"}
</Button>
<Button
size="sm"
onClick={saveSpec}
disabled={!hasChanges || isSaving}
data-testid="save-spec"
>
<Save className="w-4 h-4 mr-2" />
{isSaving ? "Saving..." : hasChanges ? "Save Changes" : "Saved"}
</Button>
<div className="flex items-center gap-3">
{(isRegenerating || isCreating || isGeneratingFeatures) && (
<div className="flex items-center gap-3 px-6 py-3.5 rounded-xl bg-gradient-to-r from-primary/15 to-primary/5 border border-primary/30 shadow-lg backdrop-blur-md">
<div className="relative">
<Loader2 className="w-5 h-5 animate-spin text-primary flex-shrink-0" />
<div className="absolute inset-0 w-5 h-5 animate-ping text-primary/20" />
</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"}
</span>
{currentPhase && (
<span className="text-xs text-muted-foreground/90 leading-tight font-medium">
{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}
</span>
)}
</div>
</div>
)}
{errorMessage && (
<div className="flex items-center gap-3 px-6 py-3.5 rounded-xl bg-gradient-to-r from-destructive/15 to-destructive/5 border border-destructive/30 shadow-lg backdrop-blur-md">
<AlertCircle className="w-5 h-5 text-destructive flex-shrink-0" />
<div className="flex flex-col gap-1 min-w-0">
<span className="text-sm font-semibold text-destructive leading-tight tracking-tight">Error</span>
<span className="text-xs text-destructive/90 leading-tight font-medium">{errorMessage}</span>
</div>
</div>
)}
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => setShowRegenerateDialog(true)}
disabled={isRegenerating || isCreating || isGeneratingFeatures}
data-testid="regenerate-spec"
>
{isRegenerating ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Sparkles className="w-4 h-4 mr-2" />
)}
{isRegenerating ? "Regenerating..." : "Regenerate"}
</Button>
<Button
size="sm"
onClick={saveSpec}
disabled={!hasChanges || isSaving || isCreating || isRegenerating || isGeneratingFeatures}
data-testid="save-spec"
>
<Save className="w-4 h-4 mr-2" />
{isSaving ? "Saving..." : hasChanges ? "Save Changes" : "Saved"}
</Button>
</div>
</div>
</div>
@@ -373,7 +964,14 @@ export function SpecView() {
</div>
{/* Regenerate Dialog */}
<Dialog open={showRegenerateDialog} onOpenChange={setShowRegenerateDialog}>
<Dialog
open={showRegenerateDialog}
onOpenChange={(open) => {
if (!open && !isRegenerating) {
setShowRegenerateDialog(false);
}
}}
>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Regenerate App Specification</DialogTitle>
@@ -403,35 +1001,56 @@ export function SpecView() {
</div>
</div>
<DialogFooter>
<DialogFooter className="flex justify-between sm:justify-between">
<Button
variant="ghost"
onClick={() => setShowRegenerateDialog(false)}
disabled={isRegenerating}
variant="outline"
onClick={handleGenerateFeatures}
disabled={isRegenerating || isGeneratingFeatures}
title="Generate features from the existing app_spec.txt without regenerating the spec"
>
Cancel
</Button>
<HotkeyButton
onClick={handleRegenerate}
disabled={!projectDefinition.trim() || isRegenerating}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkeyActive={showRegenerateDialog}
>
{isRegenerating ? (
{isGeneratingFeatures ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Regenerating...
Generating...
</>
) : (
<>
<Sparkles className="w-4 h-4 mr-2" />
Regenerate Spec
<ListPlus className="w-4 h-4 mr-2" />
Generate Features
</>
)}
</HotkeyButton>
</Button>
<div className="flex gap-2">
<Button
variant="ghost"
onClick={() => setShowRegenerateDialog(false)}
disabled={isRegenerating || isGeneratingFeatures}
>
Cancel
</Button>
<HotkeyButton
onClick={handleRegenerate}
disabled={!projectDefinition.trim() || isRegenerating || isGeneratingFeatures}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkeyActive={showRegenerateDialog && !isRegenerating && !isGeneratingFeatures}
>
{isRegenerating ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Regenerating...
</>
) : (
<>
<Sparkles className="w-4 h-4 mr-2" />
Regenerate Spec
</>
)}
</HotkeyButton>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -1843,6 +1843,23 @@ function createMockSpecRegenerationAPI(): SpecRegenerationAPI {
return { success: true };
},
generateFeatures: async (projectPath: string) => {
if (mockSpecRegenerationRunning) {
return {
success: false,
error: "Feature generation is already running",
};
}
mockSpecRegenerationRunning = true;
console.log(`[Mock] Generating features from existing spec for: ${projectPath}`);
// Simulate async feature generation
simulateFeatureGeneration(projectPath);
return { success: true };
},
stop: async () => {
mockSpecRegenerationRunning = false;
if (mockSpecRegenerationTimeout) {
@@ -2007,6 +2024,51 @@ async function simulateSpecRegeneration(
mockSpecRegenerationTimeout = null;
}
async function simulateFeatureGeneration(projectPath: string) {
emitSpecRegenerationEvent({
type: "spec_regeneration_progress",
content: "[Phase: initialization] Starting feature generation from existing app_spec.txt...\n",
});
await new Promise((resolve) => {
mockSpecRegenerationTimeout = setTimeout(resolve, 500);
});
if (!mockSpecRegenerationRunning) return;
emitSpecRegenerationEvent({
type: "spec_regeneration_progress",
content: "[Phase: feature_generation] Reading implementation roadmap...\n",
});
await new Promise((resolve) => {
mockSpecRegenerationTimeout = setTimeout(resolve, 500);
});
if (!mockSpecRegenerationRunning) return;
emitSpecRegenerationEvent({
type: "spec_regeneration_progress",
content: "[Feature Creation] Creating features from roadmap...\n",
});
await new Promise((resolve) => {
mockSpecRegenerationTimeout = setTimeout(resolve, 1000);
});
if (!mockSpecRegenerationRunning) return;
emitSpecRegenerationEvent({
type: "spec_regeneration_progress",
content: "[Phase: complete] All tasks completed!\n",
});
emitSpecRegenerationEvent({
type: "spec_regeneration_complete",
message: "All tasks completed!",
});
mockSpecRegenerationRunning = false;
mockSpecRegenerationTimeout = null;
}
// Mock Features API implementation
function createMockFeaturesAPI(): FeaturesAPI {
// Store features in mock file system using features/{id}/feature.json pattern

View File

@@ -72,10 +72,21 @@ function detectEntryType(content: string): LogEntryType {
trimmed.startsWith("📋") ||
trimmed.startsWith("⚡") ||
trimmed.startsWith("✅") ||
trimmed.match(/^(Planning|Action|Verification)/i)
trimmed.match(/^(Planning|Action|Verification)/i) ||
trimmed.match(/\[Phase:\s*([^\]]+)\]/) ||
trimmed.match(/Phase:\s*\w+/i)
) {
return "phase";
}
// Feature creation events
if (
trimmed.match(/\[Feature Creation\]/i) ||
trimmed.match(/Feature Creation/i) ||
trimmed.match(/Creating feature/i)
) {
return "success";
}
// Errors
if (trimmed.startsWith("❌") || trimmed.toLowerCase().includes("error:")) {
@@ -138,6 +149,12 @@ function extractPhase(content: string): string | undefined {
if (content.includes("⚡")) return "action";
if (content.includes("✅")) return "verification";
// Extract from [Phase: ...] format
const phaseMatch = content.match(/\[Phase:\s*([^\]]+)\]/);
if (phaseMatch) {
return phaseMatch[1].toLowerCase();
}
const match = content.match(/^(Planning|Action|Verification)/i);
return match?.[1]?.toLowerCase();
}
@@ -155,7 +172,14 @@ function generateTitle(type: LogEntryType, content: string): string {
return "Tool Input/Result";
case "phase": {
const phase = extractPhase(content);
return phase ? `Phase: ${phase.charAt(0).toUpperCase() + phase.slice(1)}` : "Phase Change";
if (phase) {
// Capitalize first letter of each word
const formatted = phase.split(/\s+/).map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(" ");
return `Phase: ${formatted}`;
}
return "Phase Change";
}
case "error":
return "Error";
@@ -224,6 +248,13 @@ export function parseLogOutput(rawOutput: string): LogEntry[] {
trimmedLine.startsWith("❌") ||
trimmedLine.startsWith("⚠️") ||
trimmedLine.startsWith("🧠") ||
trimmedLine.match(/\[Phase:\s*([^\]]+)\]/) ||
trimmedLine.match(/\[Feature Creation\]/i) ||
trimmedLine.match(/\[Tool\]/i) ||
trimmedLine.match(/\[Agent\]/i) ||
trimmedLine.match(/\[Complete\]/i) ||
trimmedLine.match(/\[ERROR\]/i) ||
trimmedLine.match(/\[Status\]/i) ||
trimmedLine.toLowerCase().includes("ultrathink preparation") ||
trimmedLine.toLowerCase().includes("thinking level") ||
(trimmedLine.startsWith("Input:") && currentEntry?.type === "tool_call");

View File

@@ -257,6 +257,11 @@ export interface SpecRegenerationAPI {
error?: string;
}>;
generateFeatures: (projectPath: string) => Promise<{
success: boolean;
error?: string;
}>;
stop: () => Promise<{
success: boolean;
error?: string;