feat(feature-suggestions): implement feature suggestions and spec regeneration functionality

- Introduced a new `FeatureSuggestionsService` to analyze projects and generate feature suggestions based on the project structure and existing features.
- Added IPC handlers for generating and stopping feature suggestions, as well as checking their status.
- Implemented a `SpecRegenerationService` to create and regenerate application specifications based on user-defined project overviews and definitions.
- Enhanced the UI with a `FeatureSuggestionsDialog` for displaying generated suggestions and allowing users to import them into their project.
- Updated the sidebar and board view components to integrate feature suggestions and spec regeneration functionalities, improving project management capabilities.

These changes significantly enhance the application's ability to assist users in feature planning and specification management.
This commit is contained in:
Cody Seibert
2025-12-10 08:51:33 -05:00
parent e9a4dd0319
commit 72cc43d02f
16 changed files with 2923 additions and 187 deletions

View File

@@ -5,13 +5,35 @@ import { useAppStore } from "@/store/app-store";
import { getElectronAPI } from "@/lib/electron";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Save, RefreshCw, FileText } from "lucide-react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Save, RefreshCw, FileText, Sparkles, Loader2, FilePlus2 } from "lucide-react";
import { Checkbox } from "@/components/ui/checkbox";
import type { SpecRegenerationEvent } from "@/types/electron";
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);
// Create spec state
const [showCreateDialog, setShowCreateDialog] = useState(false);
const [projectOverview, setProjectOverview] = useState("");
const [isCreating, setIsCreating] = useState(false);
const [generateFeatures, setGenerateFeatures] = useState(true);
// Load spec from file
const loadSpec = useCallback(async () => {
@@ -26,10 +48,16 @@ export function SpecView() {
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);
}
@@ -39,6 +67,35 @@ export function SpecView() {
loadSpec();
}, [loadSpec]);
// Subscribe to spec regeneration events
useEffect(() => {
const api = getElectronAPI();
if (!api.specRegeneration) return;
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();
} else if (event.type === "spec_regeneration_error") {
setIsRegenerating(false);
setIsCreating(false);
console.error("[SpecView] Regeneration error:", event.error);
}
});
return () => {
unsubscribe();
};
}, [loadSpec]);
// Save spec to file
const saveSpec = async () => {
if (!currentProject) return;
@@ -63,6 +120,62 @@ export function SpecView() {
setHasChanges(true);
};
const handleRegenerate = async () => {
if (!currentProject || !projectDefinition.trim()) return;
setIsRegenerating(true);
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()
);
if (!result.success) {
console.error("[SpecView] Failed to start regeneration:", result.error);
setIsRegenerating(false);
}
// If successful, we'll wait for the events to update the state
} catch (error) {
console.error("[SpecView] Failed to regenerate spec:", error);
setIsRegenerating(false);
}
};
const handleCreateSpec = async () => {
if (!currentProject || !projectOverview.trim()) return;
setIsCreating(true);
setShowCreateDialog(false);
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
);
if (!result.success) {
console.error("[SpecView] Failed to start spec creation:", result.error);
setIsCreating(false);
}
// If successful, we'll wait for the events to update the state
} catch (error) {
console.error("[SpecView] Failed to create spec:", error);
setIsCreating(false);
}
};
if (!currentProject) {
return (
<div
@@ -85,6 +198,121 @@ export function SpecView() {
);
}
// Show empty state when no spec exists (isCreating is handled by bottom-right indicator in sidebar)
if (!specExists) {
return (
<div
className="flex-1 flex flex-col overflow-hidden content-bg"
data-testid="spec-view-empty"
>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-border bg-glass backdrop-blur-md">
<div className="flex items-center gap-3">
<FileText className="w-5 h-5 text-muted-foreground" />
<div>
<h1 className="text-xl font-bold">App Specification</h1>
<p className="text-sm text-muted-foreground">
{currentProject.path}/.automaker/app_spec.txt
</p>
</div>
</div>
</div>
{/* Empty State */}
<div className="flex-1 flex items-center justify-center p-8">
<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" />
</div>
</div>
<h2 className="text-2xl font-semibold mb-3">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.
</p>
<Button
size="lg"
onClick={() => setShowCreateDialog(true)}
>
<FilePlus2 className="w-5 h-5 mr-2" />
Create app_spec
</Button>
</div>
</div>
{/* Create Dialog */}
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Create App Specification</DialogTitle>
<DialogDescription className="text-muted-foreground">
We didn&apos;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&apos;ll analyze your project&apos;s
tech stack and create a comprehensive specification.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium">
Project Overview
</label>
<p className="text-xs text-muted-foreground">
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.
</p>
<textarea
className="w-full h-48 p-3 rounded-md border border-border bg-background font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
value={projectOverview}
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
/>
</div>
<div className="flex items-start space-x-3 pt-2">
<Checkbox
id="generate-features"
checked={generateFeatures}
onCheckedChange={(checked) => setGenerateFeatures(checked === true)}
/>
<div className="space-y-1">
<label
htmlFor="generate-features"
className="text-sm font-medium cursor-pointer"
>
Generate feature list
</label>
<p className="text-xs text-muted-foreground">
Automatically populate feature_list.json with all features from the
implementation roadmap after the spec is generated.
</p>
</div>
</div>
</div>
<DialogFooter>
<Button
variant="ghost"
onClick={() => setShowCreateDialog(false)}
>
Cancel
</Button>
<Button
onClick={handleCreateSpec}
disabled={!projectOverview.trim()}
>
<Sparkles className="w-4 h-4 mr-2" />
Generate Spec
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
return (
<div
className="flex-1 flex flex-col overflow-hidden content-bg"
@@ -102,6 +330,20 @@ export function SpecView() {
</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}
@@ -127,6 +369,65 @@ export function SpecView() {
/>
</Card>
</div>
{/* Regenerate Dialog */}
<Dialog open={showRegenerateDialog} onOpenChange={setShowRegenerateDialog}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Regenerate App Specification</DialogTitle>
<DialogDescription className="text-muted-foreground">
We will regenerate your app spec based on a short project definition and the
current tech stack found in your project. The agent will analyze your codebase
to understand your existing technologies and create a comprehensive specification.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium">
Describe your project
</label>
<p className="text-xs text-muted-foreground">
Provide a clear description of what your app should do. Be as detailed as you
want - the more context you provide, the more comprehensive the spec will be.
</p>
<textarea
className="w-full h-40 p-3 rounded-md border border-border bg-background font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
value={projectDefinition}
onChange={(e) => setProjectDefinition(e.target.value)}
placeholder="e.g., A task management app where users can create projects, add tasks with due dates, assign tasks to team members, track progress with a kanban board, and receive notifications for upcoming deadlines..."
disabled={isRegenerating}
/>
</div>
</div>
<DialogFooter>
<Button
variant="ghost"
onClick={() => setShowRegenerateDialog(false)}
disabled={isRegenerating}
>
Cancel
</Button>
<Button
onClick={handleRegenerate}
disabled={!projectDefinition.trim() || isRegenerating}
>
{isRegenerating ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Regenerating...
</>
) : (
<>
<Sparkles className="w-4 h-4 mr-2" />
Regenerate Spec
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}