feat: implement project setup dialog and refactor sidebar integration

- Added a new ProjectSetupDialog component to facilitate project specification generation, enhancing user experience by guiding users through project setup.
- Refactored the Sidebar component to integrate the new ProjectSetupDialog, replacing the previous inline dialog implementation for improved code organization and maintainability.
- Updated the sidebar to handle project overview and feature generation options, streamlining the project setup process.
- Removed the old dialog implementation from the Sidebar, reducing code duplication and improving clarity.
This commit is contained in:
Cody Seibert
2025-12-15 01:07:47 -05:00
parent 919e08689a
commit f25d62fe25
13 changed files with 724 additions and 332 deletions

View File

@@ -1,172 +0,0 @@
import {
query,
Options,
SDKAssistantMessage,
} from "@anthropic-ai/claude-agent-sdk";
import { NextRequest, NextResponse } from "next/server";
import path from "path";
const systemPrompt = `You are an AI assistant helping users build software. You are part of the Automaker application,
which is designed to help developers plan, design, and implement software projects autonomously.
Your role is to:
- Help users define their project requirements and specifications
- Ask clarifying questions to better understand their needs
- Suggest technical approaches and architectures
- Guide them through the development process
- Be conversational and helpful
- Write, edit, and modify code files as requested
- Execute commands and tests
- Search and analyze the codebase
When discussing projects, help users think through:
- Core functionality and features
- Technical stack choices
- Data models and architecture
- User experience considerations
- Testing strategies
You have full access to the codebase and can:
- Read files to understand existing code
- Write new files
- Edit existing files
- Run bash commands
- Search for code patterns
- Execute tests and builds`;
export async function POST(request: NextRequest) {
try {
const { messages, workingDirectory } = await request.json();
console.log(
"[API] CLAUDE_CODE_OAUTH_TOKEN present:",
!!process.env.CLAUDE_CODE_OAUTH_TOKEN
);
if (!process.env.CLAUDE_CODE_OAUTH_TOKEN) {
return NextResponse.json(
{ error: "CLAUDE_CODE_OAUTH_TOKEN not configured" },
{ status: 500 }
);
}
// Get the last user message
const lastMessage = messages[messages.length - 1];
// Determine working directory - default to parent of app directory
const cwd = workingDirectory || path.resolve(process.cwd(), "..");
console.log("[API] Working directory:", cwd);
// Create query with options that enable code modification
const options: Options = {
// model: "claude-sonnet-4-20250514",
model: "claude-opus-4-5-20251101",
systemPrompt,
maxTurns: 20,
cwd,
// Enable all core tools for code modification
allowedTools: [
"Read",
"Write",
"Edit",
"Glob",
"Grep",
"Bash",
"WebSearch",
"WebFetch",
],
// Auto-accept file edits within the working directory
permissionMode: "acceptEdits",
// Enable sandbox for safer bash execution
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true,
},
};
// Convert message history to SDK format to preserve conversation context
// Include both user and assistant messages for full context
const sessionId = `api-session-${Date.now()}`;
const conversationMessages = messages.map(
(msg: { role: string; content: string }) => {
if (msg.role === "user") {
return {
type: "user" as const,
message: {
role: "user" as const,
content: msg.content,
},
parent_tool_use_id: null,
session_id: sessionId,
};
} else {
// Assistant message
return {
type: "assistant" as const,
message: {
role: "assistant" as const,
content: [
{
type: "text" as const,
text: msg.content,
},
],
},
session_id: sessionId,
};
}
}
);
// Execute query with full conversation context
const queryResult = query({
prompt:
conversationMessages.length > 0
? conversationMessages
: lastMessage.content,
options,
});
let responseText = "";
const toolUses: Array<{ name: string; input: unknown }> = [];
// Collect the response from the async generator
for await (const msg of queryResult) {
if (msg.type === "assistant") {
const assistantMsg = msg as SDKAssistantMessage;
if (assistantMsg.message.content) {
for (const block of assistantMsg.message.content) {
if (block.type === "text") {
responseText += block.text;
} else if (block.type === "tool_use") {
// Track tool usage for transparency
toolUses.push({
name: block.name,
input: block.input,
});
}
}
}
} else if (msg.type === "result") {
if (msg.subtype === "success") {
if (msg.result) {
responseText = msg.result;
}
}
}
}
return NextResponse.json({
content: responseText || "Sorry, I couldn't generate a response.",
toolUses: toolUses.length > 0 ? toolUses : undefined,
});
} catch (error: unknown) {
console.error("Claude API error:", error);
const errorMessage =
error instanceof Error
? error.message
: "Failed to get response from Claude";
return NextResponse.json({ error: errorMessage }, { status: 500 });
}
}

View File

@@ -0,0 +1,114 @@
"use client";
import { Sparkles } from "lucide-react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
interface ProjectSetupDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
projectOverview: string;
onProjectOverviewChange: (value: string) => void;
generateFeatures: boolean;
onGenerateFeaturesChange: (value: boolean) => void;
onCreateSpec: () => void;
onSkip: () => void;
isCreatingSpec: boolean;
}
export function ProjectSetupDialog({
open,
onOpenChange,
projectOverview,
onProjectOverviewChange,
generateFeatures,
onGenerateFeaturesChange,
onCreateSpec,
onSkip,
isCreatingSpec,
}: ProjectSetupDialogProps) {
return (
<Dialog
open={open}
onOpenChange={(open) => {
if (!open && !isCreatingSpec) {
onSkip();
}
}}
>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Set Up Your Project</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) => onProjectOverviewChange(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="sidebar-generate-features"
checked={generateFeatures}
onCheckedChange={(checked) =>
onGenerateFeaturesChange(checked === true)
}
/>
<div className="space-y-1">
<label
htmlFor="sidebar-generate-features"
className="text-sm font-medium cursor-pointer"
>
Generate feature list
</label>
<p className="text-xs text-muted-foreground">
Automatically create features in the features folder from the
implementation roadmap after the spec is generated.
</p>
</div>
</div>
</div>
<DialogFooter>
<Button variant="ghost" onClick={onSkip}>
Skip for now
</Button>
<Button
onClick={onCreateSpec}
disabled={!projectOverview.trim()}
>
<Sparkles className="w-4 h-4 mr-2" />
Generate Spec
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -79,10 +79,10 @@ import {
} from "@/lib/project-init";
import { toast } from "sonner";
import { themeOptions } from "@/config/theme-options";
import { Checkbox } from "@/components/ui/checkbox";
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 {
DndContext,
DragEndEvent,
@@ -471,6 +471,12 @@ export function Sidebar() {
toast.error("Failed to create specification", {
description: result.error,
});
} else {
// Show processing toast to inform user
toast.info("Generating app specification...", {
description:
"This may take a minute. You'll be notified when complete.",
});
}
// If successful, we'll wait for the events to update the state
} catch (error) {
@@ -1904,79 +1910,17 @@ export function Sidebar() {
</Dialog>
{/* New Project Setup Dialog */}
<Dialog
<ProjectSetupDialog
open={showSetupDialog}
onOpenChange={(open) => {
if (!open && !isCreatingSpec) {
handleSkipSetup();
}
}}
>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Set Up Your Project</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="sidebar-generate-features"
checked={generateFeatures}
onCheckedChange={(checked) =>
setGenerateFeatures(checked === true)
}
/>
<div className="space-y-1">
<label
htmlFor="sidebar-generate-features"
className="text-sm font-medium cursor-pointer"
>
Generate feature list
</label>
<p className="text-xs text-muted-foreground">
Automatically create features in the features folder from the
implementation roadmap after the spec is generated.
</p>
</div>
</div>
</div>
<DialogFooter>
<Button variant="ghost" onClick={handleSkipSetup}>
Skip for now
</Button>
<Button
onClick={handleCreateInitialSpec}
disabled={!projectOverview.trim()}
>
<Sparkles className="w-4 h-4 mr-2" />
Generate Spec
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
onOpenChange={setShowSetupDialog}
projectOverview={projectOverview}
onProjectOverviewChange={setProjectOverview}
generateFeatures={generateFeatures}
onGenerateFeaturesChange={setGenerateFeatures}
onCreateSpec={handleCreateInitialSpec}
onSkip={handleSkipSetup}
isCreatingSpec={isCreatingSpec}
/>
{/* New Project Onboarding Dialog */}
<Dialog

View File

@@ -275,7 +275,7 @@ export function BoardView() {
};
}, []);
// Subscribe to spec regeneration events to clear state on completion
// Subscribe to spec regeneration events to clear creating state on completion
useEffect(() => {
const api = getElectronAPI();
if (!api.specRegeneration) return;
@@ -495,6 +495,30 @@ export function BoardView() {
}
}, [currentProject, setFeatures]);
// Subscribe to spec regeneration complete events to refresh kanban board
useEffect(() => {
const api = getElectronAPI();
if (!api.specRegeneration) return;
const unsubscribe = api.specRegeneration.onEvent((event) => {
// Refresh the kanban board when spec regeneration completes for the current project
if (
event.type === "spec_regeneration_complete" &&
currentProject &&
event.projectPath === currentProject.path
) {
console.log(
"[BoardView] Spec regeneration complete, refreshing features"
);
loadFeatures();
}
});
return () => {
unsubscribe();
};
}, [currentProject, loadFeatures]);
// Load persisted categories from file
const loadCategories = useCallback(async () => {
if (!currentProject) return;

View File

@@ -0,0 +1,93 @@
/**
* Model Configuration - Centralized model settings for the app
*
* Models can be overridden via environment variables:
* - AUTOMAKER_MODEL_CHAT: Model for chat interactions
* - AUTOMAKER_MODEL_DEFAULT: Fallback model for all operations
*/
/**
* Claude model aliases for convenience
*/
export const CLAUDE_MODEL_MAP: Record<string, string> = {
haiku: "claude-haiku-4-5",
sonnet: "claude-sonnet-4-20250514",
opus: "claude-opus-4-5-20251101",
} as const;
/**
* Default models per use case
*/
export const DEFAULT_MODELS = {
chat: "claude-opus-4-5-20251101",
default: "claude-opus-4-5-20251101",
} as const;
/**
* Resolve a model alias to a full model string
*/
export function resolveModelString(
modelKey?: string,
defaultModel: string = DEFAULT_MODELS.default
): string {
if (!modelKey) {
return defaultModel;
}
// Full Claude model string - pass through
if (modelKey.includes("claude-")) {
return modelKey;
}
// Check alias map
const resolved = CLAUDE_MODEL_MAP[modelKey];
if (resolved) {
return resolved;
}
// Unknown key - use default
return defaultModel;
}
/**
* Get the model for chat operations
*
* Priority:
* 1. Explicit model parameter
* 2. AUTOMAKER_MODEL_CHAT environment variable
* 3. AUTOMAKER_MODEL_DEFAULT environment variable
* 4. Default chat model
*/
export function getChatModel(explicitModel?: string): string {
if (explicitModel) {
return resolveModelString(explicitModel);
}
const envModel =
process.env.AUTOMAKER_MODEL_CHAT || process.env.AUTOMAKER_MODEL_DEFAULT;
if (envModel) {
return resolveModelString(envModel);
}
return DEFAULT_MODELS.chat;
}
/**
* Default allowed tools for chat interactions
*/
export const CHAT_TOOLS = [
"Read",
"Write",
"Edit",
"Glob",
"Grep",
"Bash",
"WebSearch",
"WebFetch",
] as const;
/**
* Default max turns for chat
*/
export const CHAT_MAX_TURNS = 1000;