diff --git a/README.md b/README.md index 9634693a..29e61eb1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,36 @@ +> **[!TIP]** +> +> **Learn more about Agentic Coding!** +> +> Automaker itself was built by a group of engineers using AI and agentic coding techniques to build features faster than ever. By leveraging tools like Cursor IDE and Claude Code CLI, the team orchestrated AI agents to implement complex functionality in days instead of weeks. +> +> **Learn how:** Master these same techniques and workflows in the [Agentic Jumpstart course](https://agenticjumpstart.com/). + # Automaker -Automaker is an autonomous AI development studio that helps you build software faster using AI-powered agents. It provides a visual Kanban board interface to manage features, automatically assigns AI agents to implement them, and tracks progress through an intuitive workflow from backlog to verified completion. +**Stop typing code. Start directing AI agents.** + +Automaker is an autonomous AI development studio that transforms how you build software. Instead of manually writing every line of code, you describe features on a Kanban board and watch as AI agents powered by Claude Code automatically implement them. + +## What Makes Automaker Different? + +Traditional development tools help you write code. Automaker helps you **orchestrate AI agents** to build entire features autonomously. Think of it as having a team of AI developers working for you—you define what needs to be built, and Automaker handles the implementation. + +### The Workflow + +1. **Add Features** - Describe features you want built (with text, images, or screenshots) +2. **Move to "In Progress"** - Automaker automatically assigns an AI agent to implement the feature +3. **Watch It Build** - See real-time progress as the agent writes code, runs tests, and makes changes +4. **Review & Verify** - Review the changes, run tests, and approve when ready +5. **Ship Faster** - Build entire applications in days, not weeks + +### Powered by Claude Code + +Automaker leverages the [Claude Agent SDK](https://docs.anthropic.com/en/docs/claude-code) to give AI agents full access to your codebase. Agents can read files, write code, execute commands, run tests, and make git commits—all while working in isolated git worktrees to keep your main branch safe. + +### Why This Matters + +The future of software development is **agentic coding**—where developers become architects directing AI agents rather than manual coders. Automaker puts this future in your hands today, letting you experience what it's like to build software 10x faster with AI agents handling the implementation while you focus on architecture and business logic. --- @@ -36,20 +66,22 @@ cd automaker # 2. Install dependencies npm install -# 3. Get your Claude Code OAuth token -claude setup-token -# ⚠️ This prints your token - don't share your screen! - -# 4. Set the token and run -export CLAUDE_CODE_OAUTH_TOKEN="sk-ant-oat01-..." -npm run dev:electron +# 3. Run Automaker (pick your mode) +npm run dev +# Then choose your run mode when prompted, or use specific commands below ``` ## How to Run -### Development Modes +### Development Mode -Automaker can be run in several modes: +Start Automaker in development mode: + +```bash +npm run dev +``` + +This will prompt you to choose your run mode, or you can specify a mode directly: #### Electron Desktop App (Recommended) @@ -72,8 +104,6 @@ npm run dev:electron:wsl:gpu ```bash # Run in web browser (http://localhost:3007) npm run dev:web -# or -npm run dev ``` ### Building for Production diff --git a/apps/app/src/components/layout/sidebar.tsx b/apps/app/src/components/layout/sidebar.tsx index 26ad3e69..cf163102 100644 --- a/apps/app/src/components/layout/sidebar.tsx +++ b/apps/app/src/components/layout/sidebar.tsx @@ -381,8 +381,6 @@ export function Sidebar() { toast.success("App specification created", { description: "Your project is now set up and ready to go!", }); - // Navigate to spec view to show the new spec - setCurrentView("spec"); } else if (event.type === "spec_regeneration_error") { setSpecCreatingForProject(null); toast.error("Failed to create specification", { diff --git a/apps/app/src/components/new-project-modal.tsx b/apps/app/src/components/new-project-modal.tsx index addefb73..55d690b8 100644 --- a/apps/app/src/components/new-project-modal.tsx +++ b/apps/app/src/components/new-project-modal.tsx @@ -31,6 +31,8 @@ import { getHttpApiClient } from "@/lib/http-api-client"; import { cn } from "@/lib/utils"; import { useFileBrowser } from "@/contexts/file-browser-context"; +const LAST_PROJECT_DIR_KEY = "automaker:lastProjectDir"; + interface ValidationErrors { projectName?: boolean; workspaceDir?: boolean; @@ -80,6 +82,14 @@ export function NewProjectModal({ // Fetch workspace directory when modal opens useEffect(() => { if (open) { + // First, check localStorage for last used directory + const lastUsedDir = localStorage.getItem(LAST_PROJECT_DIR_KEY); + if (lastUsedDir) { + setWorkspaceDir(lastUsedDir); + return; + } + + // Fall back to server config if no saved directory setIsLoadingWorkspace(true); const httpClient = getHttpApiClient(); httpClient.workspace @@ -201,6 +211,8 @@ export function NewProjectModal({ }); if (selectedPath) { setWorkspaceDir(selectedPath); + // Save to localStorage for next time + localStorage.setItem(LAST_PROJECT_DIR_KEY, selectedPath); // Clear any workspace error when a valid directory is selected if (errors.workspaceDir) { setErrors((prev) => ({ ...prev, workspaceDir: false })); diff --git a/apps/app/src/components/views/board-view.tsx b/apps/app/src/components/views/board-view.tsx index b39bb953..42c326e4 100644 --- a/apps/app/src/components/views/board-view.tsx +++ b/apps/app/src/components/views/board-view.tsx @@ -1554,6 +1554,13 @@ export function BoardView() { } }); + // Sort backlog by priority: 1 (high) -> 2 (medium) -> 3 (low) -> no priority + map.backlog.sort((a, b) => { + const aPriority = a.priority ?? 999; // Features without priority go last + const bPriority = b.priority ?? 999; + return aPriority - bPriority; + }); + return map; }, [features, runningAutoTasks, searchQuery]); diff --git a/apps/app/src/components/views/kanban-card.tsx b/apps/app/src/components/views/kanban-card.tsx index a49ca9b6..17c4fe1f 100644 --- a/apps/app/src/components/views/kanban-card.tsx +++ b/apps/app/src/components/views/kanban-card.tsx @@ -57,6 +57,7 @@ import { ChevronDown, ChevronUp, Brain, + Flag, } from "lucide-react"; import { CountUpTimer } from "@/components/ui/count-up-timer"; import { getElectronAPI } from "@/lib/electron"; @@ -89,6 +90,33 @@ function formatThinkingLevel(level: ThinkingLevel | undefined): string { return labels[level]; } +/** + * Formats priority for display + */ +function formatPriority(priority: number | undefined): string | null { + if (!priority) return null; + const labels: Record = { + 1: "High", + 2: "Medium", + 3: "Low", + }; + return labels[priority] || null; +} + +/** + * Gets priority badge color classes + */ +function getPriorityBadgeClasses(priority: number | undefined): string { + if (priority === 1) { + return "bg-red-500/20 border border-red-500/50 text-red-400"; + } else if (priority === 2) { + return "bg-yellow-500/20 border border-yellow-500/50 text-yellow-400"; + } else if (priority === 3) { + return "bg-blue-500/20 border border-blue-500/50 text-blue-400"; + } + return ""; +} + interface KanbanCardProps { feature: Feature; onEdit: () => void; @@ -198,6 +226,34 @@ export const KanbanCard = memo(function KanbanCard({ return () => clearInterval(interval); }, [feature.justFinishedAt, feature.status, currentTime]); + // Calculate priority badge position + const priorityLabel = formatPriority(feature.priority); + const hasPriority = !!priorityLabel; + + // Calculate top position for badges (stacking vertically) + const getBadgeTopPosition = (badgeIndex: number) => { + return badgeIndex === 0 + ? "top-2" + : badgeIndex === 1 + ? "top-8" + : badgeIndex === 2 + ? "top-14" + : "top-20"; + }; + + // Determine badge positions (must be after isJustFinished is defined) + let badgeIndex = 0; + const priorityBadgeIndex = hasPriority ? badgeIndex++ : -1; + const skipTestsBadgeIndex = + feature.skipTests && !feature.error ? badgeIndex++ : -1; + const errorBadgeIndex = feature.error ? badgeIndex++ : -1; + const justFinishedBadgeIndex = isJustFinished ? badgeIndex++ : -1; + const branchBadgeIndex = + hasWorktree && !isCurrentAutoTask ? badgeIndex++ : -1; + + // Total number of badges displayed + const totalBadgeCount = badgeIndex; + // Load context file for in_progress, waiting_approval, and verified features useEffect(() => { const loadContext = async () => { @@ -353,12 +409,29 @@ export const KanbanCard = memo(function KanbanCard({ style={{ opacity: opacity / 100 }} /> )} + {/* Priority badge */} + {hasPriority && ( +
+ + {priorityLabel} +
+ )} {/* Skip Tests indicator badge */} {feature.skipTests && !feature.error && (
@@ -432,11 +505,11 @@ export const KanbanCard = memo(function KanbanCard({ className={cn( "p-3 pb-2 block", // Reset grid layout to block for custom kanban card layout // Add extra top padding when badges are present to prevent text overlap - (feature.skipTests || feature.error || isJustFinished) && "pt-10", - // Add even more top padding when both badges and branch are shown - hasWorktree && - (feature.skipTests || feature.error || isJustFinished) && - "pt-14" + // Calculate padding based on number of badges + totalBadgeCount === 1 && "pt-10", + totalBadgeCount === 2 && "pt-14", + totalBadgeCount === 3 && "pt-20", + totalBadgeCount >= 4 && "pt-24" )} > {isCurrentAutoTask && ( diff --git a/apps/app/src/components/views/spec-view.tsx b/apps/app/src/components/views/spec-view.tsx index ea9fe36d..935d2c72 100644 --- a/apps/app/src/components/views/spec-view.tsx +++ b/apps/app/src/components/views/spec-view.tsx @@ -22,7 +22,6 @@ import { Loader2, FilePlus2, AlertCircle, - ListPlus, CheckCircle2, } from "lucide-react"; import { toast } from "sonner"; @@ -47,12 +46,17 @@ export function SpecView() { 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); @@ -66,6 +70,7 @@ export function SpecView() { const [errorMessage, setErrorMessage] = useState(""); const statusCheckRef = useRef(false); const stateRestoredRef = useRef(false); + const pendingStatusTimeoutRef = useRef(null); // Load spec from file const loadSpec = useCallback(async () => { @@ -99,6 +104,26 @@ export function SpecView() { 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 () => { @@ -113,40 +138,44 @@ export function SpecView() { } const status = await api.specRegeneration.status(); - console.log("[SpecView] Status check on mount:", status); + console.log( + "[SpecView] Status check on mount:", + status, + "for project:", + currentProject.path + ); if (status.success && status.isRunning) { - // Something is running - restore state using backend's authoritative phase + // 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 - restoring state", - { phase: status.currentPhase } + "[SpecView] Spec generation is running globally. Tentatively showing loader, waiting for events to confirm project match." ); - if (!stateRestoredRef.current) { - setIsCreating(true); - setIsRegenerating(true); - stateRestoredRef.current = true; - } - - // Use the backend's currentPhase directly - single source of truth + // Tentatively set state - events will confirm or clear it + setIsCreating(true); + setIsRegenerating(true); if (status.currentPhase) { setCurrentPhase(status.currentPhase); } else { - setCurrentPhase("in progress"); + setCurrentPhase("initialization"); } - // Add resume message to logs if needed - 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")) { - const resumeMessage = - "\n[Status] Resumed monitoring existing spec generation process...\n"; - logsRef.current = logsRef.current + resumeMessage; - setLogs(logsRef.current); + // 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); @@ -274,6 +303,8 @@ export function SpecView() { // Subscribe to spec regeneration events useEffect(() => { + if (!currentProject) return; + const api = getElectronAPI(); if (!api.specRegeneration) return; @@ -283,7 +314,9 @@ export function SpecView() { "[SpecView] Regeneration event:", event.type, "for project:", - event.projectPath + event.projectPath, + "current project:", + currentProject?.path ); // Only handle events for the current project @@ -292,7 +325,20 @@ export function SpecView() { 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) { @@ -475,7 +521,7 @@ export function SpecView() { return () => { unsubscribe(); }; - }, [loadSpec]); + }, [currentProject?.path, loadSpec, errorMessage, currentPhase]); // Save spec to file const saveSpec = async () => { @@ -505,12 +551,16 @@ export function SpecView() { 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"); + console.log( + "[SpecView] Starting spec regeneration, generateFeatures:", + generateFeaturesOnRegenerate + ); try { const api = getElectronAPI(); if (!api.specRegeneration) { @@ -520,7 +570,9 @@ export function SpecView() { } const result = await api.specRegeneration.generate( currentProject.path, - projectDefinition.trim() + projectDefinition.trim(), + generateFeaturesOnRegenerate, + analyzeProjectOnRegenerate ); if (!result.success) { @@ -570,7 +622,8 @@ export function SpecView() { const result = await api.specRegeneration.create( currentProject.path, projectOverview.trim(), - generateFeatures + generateFeatures, + analyzeProjectOnCreate ); if (!result.success) { @@ -839,6 +892,33 @@ export function SpecView() { />
+
+ + setAnalyzeProjectOnCreate(checked === true) + } + disabled={isCreating} + /> +
+ +

+ If checked, the agent will research your existing codebase + to understand the tech stack. If unchecked, defaults to + TanStack Start, Drizzle ORM, PostgreSQL, shadcn/ui, Tailwind + CSS, and React. +

+
+
+
+ +
+ + setAnalyzeProjectOnRegenerate(checked === true) + } + disabled={isRegenerating} + /> +
+ +

+ If checked, the agent will research your existing codebase to + understand the tech stack. If unchecked, defaults to TanStack + Start, Drizzle ORM, PostgreSQL, shadcn/ui, Tailwind CSS, and + React. +

+
+
+ +
+ + setGenerateFeaturesOnRegenerate(checked === true) + } + disabled={isRegenerating} + /> +
+ +

+ Automatically create features in the features folder from the + implementation roadmap after the spec is regenerated. +

+
+
- - +