diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 21a2444b..00000000 Binary files a/.DS_Store and /dev/null differ diff --git a/apps/app/src/components/layout/project-setup-dialog.tsx b/apps/app/src/components/layout/project-setup-dialog.tsx index 3683b5cc..82453203 100644 --- a/apps/app/src/components/layout/project-setup-dialog.tsx +++ b/apps/app/src/components/layout/project-setup-dialog.tsx @@ -1,6 +1,6 @@ "use client"; -import { Sparkles } from "lucide-react"; +import { Sparkles, Clock } from "lucide-react"; import { Dialog, DialogContent, @@ -11,6 +11,19 @@ import { } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; +import { cn } from "@/lib/utils"; + +// Feature count options +export type FeatureCount = 20 | 50 | 100; +const FEATURE_COUNT_OPTIONS: { + value: FeatureCount; + label: string; + warning?: string; +}[] = [ + { value: 20, label: "20" }, + { value: 50, label: "50", warning: "May take up to 5 minutes" }, + { value: 100, label: "100", warning: "May take up to 5 minutes" }, +]; interface ProjectSetupDialogProps { open: boolean; @@ -19,6 +32,8 @@ interface ProjectSetupDialogProps { onProjectOverviewChange: (value: string) => void; generateFeatures: boolean; onGenerateFeaturesChange: (value: boolean) => void; + featureCount: FeatureCount; + onFeatureCountChange: (value: FeatureCount) => void; onCreateSpec: () => void; onSkip: () => void; isCreatingSpec: boolean; @@ -31,6 +46,8 @@ export function ProjectSetupDialog({ onProjectOverviewChange, generateFeatures, onGenerateFeaturesChange, + featureCount, + onFeatureCountChange, onCreateSpec, onSkip, isCreatingSpec, @@ -94,16 +111,52 @@ export function ProjectSetupDialog({

+ + {/* Feature Count Selection - only shown when generateFeatures is enabled */} + {generateFeatures && ( +
+ +
+ {FEATURE_COUNT_OPTIONS.map((option) => ( + + ))} +
+ {FEATURE_COUNT_OPTIONS.find((o) => o.value === featureCount) + ?.warning && ( +

+ + { + FEATURE_COUNT_OPTIONS.find((o) => o.value === featureCount) + ?.warning + } +

+ )} +
+ )} - @@ -112,4 +165,3 @@ export function ProjectSetupDialog({ ); } - diff --git a/apps/app/src/components/layout/sidebar.tsx b/apps/app/src/components/layout/sidebar.tsx index d45519ef..f33bce56 100644 --- a/apps/app/src/components/layout/sidebar.tsx +++ b/apps/app/src/components/layout/sidebar.tsx @@ -82,7 +82,10 @@ import { themeOptions } from "@/config/theme-options"; import type { SpecRegenerationEvent } from "@/types/electron"; import { DeleteProjectDialog } from "@/components/views/settings-view/components/delete-project-dialog"; import { NewProjectModal } from "@/components/new-project-modal"; -import { ProjectSetupDialog } from "@/components/layout/project-setup-dialog"; +import { + ProjectSetupDialog, + type FeatureCount, +} from "@/components/layout/project-setup-dialog"; import { DndContext, DragEndEvent, @@ -261,6 +264,7 @@ export function Sidebar() { const [setupProjectPath, setSetupProjectPath] = useState(""); const [projectOverview, setProjectOverview] = useState(""); const [generateFeatures, setGenerateFeatures] = useState(true); + const [featureCount, setFeatureCount] = useState(50); const [showSpecIndicator, setShowSpecIndicator] = useState(true); // Derive isCreatingSpec from store state @@ -466,7 +470,9 @@ export function Sidebar() { const result = await api.specRegeneration.create( setupProjectPath, projectOverview.trim(), - generateFeatures + generateFeatures, + undefined, // analyzeProject - use default + generateFeatures ? featureCount : undefined // only pass maxFeatures if generating features ); if (!result.success) { @@ -490,7 +496,13 @@ export function Sidebar() { description: error instanceof Error ? error.message : "Unknown error", }); } - }, [setupProjectPath, projectOverview, setSpecCreatingForProject]); + }, [ + setupProjectPath, + projectOverview, + generateFeatures, + featureCount, + setSpecCreatingForProject, + ]); // Handle skipping setup const handleSkipSetup = useCallback(() => { @@ -1453,7 +1465,7 @@ export function Sidebar() { onClick={() => setShowTrashDialog(true)} className={cn( "group flex items-center justify-center px-3 h-[42px] rounded-xl", - "relative overflow-hidden", + "relative", "text-muted-foreground hover:text-destructive", // Subtle background that turns red on hover "bg-accent/20 hover:bg-destructive/15", @@ -1467,7 +1479,7 @@ export function Sidebar() { > {trashedProjects.length > 0 && ( - + {trashedProjects.length > 9 ? "9+" : trashedProjects.length} )} @@ -2248,6 +2260,8 @@ export function Sidebar() { onProjectOverviewChange={setProjectOverview} generateFeatures={generateFeatures} onGenerateFeaturesChange={setGenerateFeatures} + featureCount={featureCount} + onFeatureCountChange={setFeatureCount} onCreateSpec={handleCreateInitialSpec} onSkip={handleSkipSetup} isCreatingSpec={isCreatingSpec} diff --git a/apps/app/src/components/ui/dropdown-menu.tsx b/apps/app/src/components/ui/dropdown-menu.tsx index 9a15f9e3..cdefc5c7 100644 --- a/apps/app/src/components/ui/dropdown-menu.tsx +++ b/apps/app/src/components/ui/dropdown-menu.tsx @@ -43,14 +43,16 @@ const DropdownMenuSubContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + + + )) DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName diff --git a/apps/app/src/components/views/board-view.tsx b/apps/app/src/components/views/board-view.tsx index dc8acfca..3554276f 100644 --- a/apps/app/src/components/views/board-view.tsx +++ b/apps/app/src/components/views/board-view.tsx @@ -2180,7 +2180,7 @@ export function BoardView() { data-testid="start-next-button" > - Pull Top + Make )} diff --git a/apps/app/src/lib/electron.ts b/apps/app/src/lib/electron.ts index 89ff570f..3e7d3fe1 100644 --- a/apps/app/src/lib/electron.ts +++ b/apps/app/src/lib/electron.ts @@ -148,15 +148,17 @@ export interface SpecRegenerationAPI { projectPath: string, projectOverview: string, generateFeatures?: boolean, - analyzeProject?: boolean + analyzeProject?: boolean, + maxFeatures?: number ) => Promise<{ success: boolean; error?: string }>; generate: ( projectPath: string, projectDefinition: string, generateFeatures?: boolean, - analyzeProject?: boolean + analyzeProject?: boolean, + maxFeatures?: number ) => Promise<{ success: boolean; error?: string }>; - generateFeatures: (projectPath: string) => Promise<{ + generateFeatures: (projectPath: string, maxFeatures?: number) => Promise<{ success: boolean; error?: string; }>; @@ -1836,7 +1838,9 @@ function createMockSpecRegenerationAPI(): SpecRegenerationAPI { create: async ( projectPath: string, projectOverview: string, - generateFeatures = true + generateFeatures = true, + _analyzeProject?: boolean, + maxFeatures?: number ) => { if (mockSpecRegenerationRunning) { return { success: false, error: "Spec creation is already running" }; @@ -1844,7 +1848,7 @@ function createMockSpecRegenerationAPI(): SpecRegenerationAPI { mockSpecRegenerationRunning = true; console.log( - `[Mock] Creating initial spec for: ${projectPath}, generateFeatures: ${generateFeatures}` + `[Mock] Creating initial spec for: ${projectPath}, generateFeatures: ${generateFeatures}, maxFeatures: ${maxFeatures}` ); // Simulate async spec creation @@ -1856,7 +1860,9 @@ function createMockSpecRegenerationAPI(): SpecRegenerationAPI { generate: async ( projectPath: string, projectDefinition: string, - generateFeatures = false + generateFeatures = false, + _analyzeProject?: boolean, + maxFeatures?: number ) => { if (mockSpecRegenerationRunning) { return { @@ -1867,7 +1873,7 @@ function createMockSpecRegenerationAPI(): SpecRegenerationAPI { mockSpecRegenerationRunning = true; console.log( - `[Mock] Regenerating spec for: ${projectPath}, generateFeatures: ${generateFeatures}` + `[Mock] Regenerating spec for: ${projectPath}, generateFeatures: ${generateFeatures}, maxFeatures: ${maxFeatures}` ); // Simulate async spec regeneration @@ -1880,7 +1886,7 @@ function createMockSpecRegenerationAPI(): SpecRegenerationAPI { return { success: true }; }, - generateFeatures: async (projectPath: string) => { + generateFeatures: async (projectPath: string, maxFeatures?: number) => { if (mockSpecRegenerationRunning) { return { success: false, @@ -1890,7 +1896,7 @@ function createMockSpecRegenerationAPI(): SpecRegenerationAPI { mockSpecRegenerationRunning = true; console.log( - `[Mock] Generating features from existing spec for: ${projectPath}` + `[Mock] Generating features from existing spec for: ${projectPath}, maxFeatures: ${maxFeatures}` ); // Simulate async feature generation diff --git a/apps/app/src/lib/http-api-client.ts b/apps/app/src/lib/http-api-client.ts index b0cdfe35..513d0f60 100644 --- a/apps/app/src/lib/http-api-client.ts +++ b/apps/app/src/lib/http-api-client.ts @@ -582,28 +582,32 @@ export class HttpApiClient implements ElectronAPI { projectPath: string, projectOverview: string, generateFeatures?: boolean, - analyzeProject?: boolean + analyzeProject?: boolean, + maxFeatures?: number ) => this.post("/api/spec-regeneration/create", { projectPath, projectOverview, generateFeatures, analyzeProject, + maxFeatures, }), generate: ( projectPath: string, projectDefinition: string, generateFeatures?: boolean, - analyzeProject?: boolean + analyzeProject?: boolean, + maxFeatures?: number ) => this.post("/api/spec-regeneration/generate", { projectPath, projectDefinition, generateFeatures, analyzeProject, + maxFeatures, }), - generateFeatures: (projectPath: string) => - this.post("/api/spec-regeneration/generate-features", { projectPath }), + generateFeatures: (projectPath: string, maxFeatures?: number) => + this.post("/api/spec-regeneration/generate-features", { projectPath, maxFeatures }), stop: () => this.post("/api/spec-regeneration/stop"), status: () => this.get("/api/spec-regeneration/status"), onEvent: (callback: (event: SpecRegenerationEvent) => void) => { diff --git a/apps/app/src/types/electron.d.ts b/apps/app/src/types/electron.d.ts index d8004ff0..9f112568 100644 --- a/apps/app/src/types/electron.d.ts +++ b/apps/app/src/types/electron.d.ts @@ -267,7 +267,8 @@ export interface SpecRegenerationAPI { projectPath: string, projectOverview: string, generateFeatures?: boolean, - analyzeProject?: boolean + analyzeProject?: boolean, + maxFeatures?: number ) => Promise<{ success: boolean; error?: string; @@ -277,13 +278,14 @@ export interface SpecRegenerationAPI { projectPath: string, projectDefinition: string, generateFeatures?: boolean, - analyzeProject?: boolean + analyzeProject?: boolean, + maxFeatures?: number ) => Promise<{ success: boolean; error?: string; }>; - generateFeatures: (projectPath: string) => Promise<{ + generateFeatures: (projectPath: string, maxFeatures?: number) => Promise<{ success: boolean; error?: string; }>; diff --git a/apps/server/src/routes/app-spec/generate-features-from-spec.ts b/apps/server/src/routes/app-spec/generate-features-from-spec.ts index 98355a68..84964a9c 100644 --- a/apps/server/src/routes/app-spec/generate-features-from-spec.ts +++ b/apps/server/src/routes/app-spec/generate-features-from-spec.ts @@ -13,15 +13,18 @@ import { parseAndCreateFeatures } from "./parse-and-create-features.js"; const logger = createLogger("SpecRegeneration"); -const MAX_FEATURES = 100; +const DEFAULT_MAX_FEATURES = 50; export async function generateFeaturesFromSpec( projectPath: string, events: EventEmitter, - abortController: AbortController + abortController: AbortController, + maxFeatures?: number ): Promise { + const featureCount = maxFeatures ?? DEFAULT_MAX_FEATURES; logger.debug("========== generateFeaturesFromSpec() started =========="); logger.debug("projectPath:", projectPath); + logger.debug("maxFeatures:", featureCount); // Read existing spec const specPath = path.join(projectPath, ".automaker", "app_spec.txt"); @@ -73,7 +76,7 @@ Format as JSON: ] } -Generate ${MAX_FEATURES} features that build on each other logically. +Generate ${featureCount} features that build on each other logically. IMPORTANT: Do not ask for clarification. The specification is provided above. Generate the JSON immediately.`; diff --git a/apps/server/src/routes/app-spec/generate-spec.ts b/apps/server/src/routes/app-spec/generate-spec.ts index c17082ae..4b638935 100644 --- a/apps/server/src/routes/app-spec/generate-spec.ts +++ b/apps/server/src/routes/app-spec/generate-spec.ts @@ -20,7 +20,8 @@ export async function generateSpec( events: EventEmitter, abortController: AbortController, generateFeatures?: boolean, - analyzeProject?: boolean + analyzeProject?: boolean, + maxFeatures?: number ): Promise { logger.info("========== generateSpec() started =========="); logger.info("projectPath:", projectPath); @@ -28,6 +29,7 @@ export async function generateSpec( logger.info("projectOverview preview:", projectOverview.substring(0, 300)); logger.info("generateFeatures:", generateFeatures); logger.info("analyzeProject:", analyzeProject); + logger.info("maxFeatures:", maxFeatures); // Build the prompt based on whether we should analyze the project let analysisInstructions = ""; @@ -252,7 +254,8 @@ ${getAppSpecFormatInstruction()}`; await generateFeaturesFromSpec( projectPath, events, - featureAbortController + featureAbortController, + maxFeatures ); // Final completion will be emitted by generateFeaturesFromSpec -> parseAndCreateFeatures } catch (featureError) { diff --git a/apps/server/src/routes/app-spec/routes/create.ts b/apps/server/src/routes/app-spec/routes/create.ts index 89d6430e..2ac1b032 100644 --- a/apps/server/src/routes/app-spec/routes/create.ts +++ b/apps/server/src/routes/app-spec/routes/create.ts @@ -22,12 +22,13 @@ export function createCreateHandler(events: EventEmitter) { logger.debug("Request body:", JSON.stringify(req.body, null, 2)); try { - const { projectPath, projectOverview, generateFeatures, analyzeProject } = + const { projectPath, projectOverview, generateFeatures, analyzeProject, maxFeatures } = req.body as { projectPath: string; projectOverview: string; generateFeatures?: boolean; analyzeProject?: boolean; + maxFeatures?: number; }; logger.debug("Parsed params:"); @@ -38,6 +39,7 @@ export function createCreateHandler(events: EventEmitter) { ); logger.debug(" generateFeatures:", generateFeatures); logger.debug(" analyzeProject:", analyzeProject); + logger.debug(" maxFeatures:", maxFeatures); if (!projectPath || !projectOverview) { logger.error("Missing required parameters"); @@ -68,7 +70,8 @@ export function createCreateHandler(events: EventEmitter) { events, abortController, generateFeatures, - analyzeProject + analyzeProject, + maxFeatures ) .catch((error) => { logError(error, "Generation failed with error"); diff --git a/apps/server/src/routes/app-spec/routes/generate-features.ts b/apps/server/src/routes/app-spec/routes/generate-features.ts index 91f3b9b5..e527da0a 100644 --- a/apps/server/src/routes/app-spec/routes/generate-features.ts +++ b/apps/server/src/routes/app-spec/routes/generate-features.ts @@ -22,9 +22,13 @@ export function createGenerateFeaturesHandler(events: EventEmitter) { logger.debug("Request body:", JSON.stringify(req.body, null, 2)); try { - const { projectPath } = req.body as { projectPath: string }; + const { projectPath, maxFeatures } = req.body as { + projectPath: string; + maxFeatures?: number; + }; logger.debug("projectPath:", projectPath); + logger.debug("maxFeatures:", maxFeatures); if (!projectPath) { logger.error("Missing projectPath parameter"); @@ -45,7 +49,12 @@ export function createGenerateFeaturesHandler(events: EventEmitter) { setRunningState(true, abortController); logger.info("Starting background feature generation task..."); - generateFeaturesFromSpec(projectPath, events, abortController) + generateFeaturesFromSpec( + projectPath, + events, + abortController, + maxFeatures + ) .catch((error) => { logError(error, "Feature generation failed with error"); events.emit("spec-regeneration:event", { diff --git a/apps/server/src/routes/app-spec/routes/generate.ts b/apps/server/src/routes/app-spec/routes/generate.ts index 428de409..15f46c52 100644 --- a/apps/server/src/routes/app-spec/routes/generate.ts +++ b/apps/server/src/routes/app-spec/routes/generate.ts @@ -27,11 +27,13 @@ export function createGenerateHandler(events: EventEmitter) { projectDefinition, generateFeatures, analyzeProject, + maxFeatures, } = req.body as { projectPath: string; projectDefinition: string; generateFeatures?: boolean; analyzeProject?: boolean; + maxFeatures?: number; }; logger.debug("Parsed params:"); @@ -42,6 +44,7 @@ export function createGenerateHandler(events: EventEmitter) { ); logger.debug(" generateFeatures:", generateFeatures); logger.debug(" analyzeProject:", analyzeProject); + logger.debug(" maxFeatures:", maxFeatures); if (!projectPath || !projectDefinition) { logger.error("Missing required parameters"); @@ -71,7 +74,8 @@ export function createGenerateHandler(events: EventEmitter) { events, abortController, generateFeatures, - analyzeProject + analyzeProject, + maxFeatures ) .catch((error) => { logError(error, "Generation failed with error");