From 72e803b56d958763a25be94b10b377c3c7a89233 Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Sun, 14 Dec 2025 20:06:52 -0500 Subject: [PATCH 1/5] feat: implement completed features management in BoardView and KanbanCard - Added functionality to complete and unarchive features, allowing users to manage feature statuses effectively. - Introduced a modal to display completed features, enhancing user experience by providing a dedicated view for archived items. - Updated KanbanCard to include buttons for completing features and managing their states, improving interactivity and workflow. - Modified the Feature interface to include a new "completed" status, ensuring comprehensive state management across the application. --- apps/app/src/components/views/board-view.tsx | 234 ++++++++++++++++++ apps/app/src/components/views/kanban-card.tsx | 192 +++++++++++++- apps/app/src/store/app-store.ts | 7 +- 3 files changed, 421 insertions(+), 12 deletions(-) diff --git a/apps/app/src/components/views/board-view.tsx b/apps/app/src/components/views/board-view.tsx index 42c326e4..6891c1e4 100644 --- a/apps/app/src/components/views/board-view.tsx +++ b/apps/app/src/components/views/board-view.tsx @@ -90,6 +90,8 @@ import { Maximize2, Shuffle, ImageIcon, + Archive, + ArchiveRestore, } from "lucide-react"; import { toast } from "sonner"; import { Slider } from "@/components/ui/slider"; @@ -208,6 +210,9 @@ export function BoardView() { useState(false); const [showBoardBackgroundModal, setShowBoardBackgroundModal] = useState(false); + const [showCompletedModal, setShowCompletedModal] = useState(false); + const [deleteCompletedFeature, setDeleteCompletedFeature] = + useState(null); const [persistedCategories, setPersistedCategories] = useState([]); const [showFollowUpDialog, setShowFollowUpDialog] = useState(false); const [followUpFeature, setFollowUpFeature] = useState(null); @@ -1497,6 +1502,47 @@ export function BoardView() { } }; + // Complete a verified feature (move to completed/archived) + const handleCompleteFeature = (feature: Feature) => { + console.log("[Board] Completing feature:", { + id: feature.id, + description: feature.description, + }); + + const updates = { + status: "completed" as const, + }; + updateFeature(feature.id, updates); + persistFeatureUpdate(feature.id, updates); + + toast.success("Feature completed", { + description: `Archived: ${feature.description.slice(0, 50)}${ + feature.description.length > 50 ? "..." : "" + }`, + }); + }; + + // Unarchive a completed feature (move back to verified) + const handleUnarchiveFeature = (feature: Feature) => { + console.log("[Board] Unarchiving feature:", { + id: feature.id, + description: feature.description, + }); + + const updates = { + status: "verified" as const, + }; + updateFeature(feature.id, updates); + persistFeatureUpdate(feature.id, updates); + + toast.success("Feature restored", { + description: `Moved back to verified: ${feature.description.slice( + 0, + 50 + )}${feature.description.length > 50 ? "..." : ""}`, + }); + }; + const checkContextExists = async (featureId: string): Promise => { if (!currentProject) return false; @@ -1518,6 +1564,11 @@ export function BoardView() { } }; + // Memoize completed features for the archive modal + const completedFeatures = useMemo(() => { + return features.filter((f) => f.status === "completed"); + }, [features]); + // Memoize column features to prevent unnecessary re-renders const columnFeaturesMap = useMemo(() => { const map: Record = { @@ -1525,6 +1576,7 @@ export function BoardView() { in_progress: [], waiting_approval: [], verified: [], + completed: [], // Completed features are shown in the archive modal, not as a column }; // Filter features by search query (case-insensitive) @@ -1903,6 +1955,31 @@ export function BoardView() { + {/* Completed/Archived Features Button */} + + + + + +

Completed Features ({completedFeatures.length})

+
+
+ {/* Kanban Card Detail Level Toggle */}
handleCommitFeature(feature)} onRevert={() => handleRevertFeature(feature)} onMerge={() => handleMergeFeature(feature)} + onComplete={() => + handleCompleteFeature(feature) + } + onImplement={async () => { + // Check concurrency limit + if (!autoMode.canStartNewTask) { + toast.error("Concurrency limit reached", { + description: `You can only have ${ + autoMode.maxConcurrency + } task${ + autoMode.maxConcurrency > 1 ? "s" : "" + } running at a time. Wait for a task to complete or increase the limit.`, + }); + return; + } + // Update with startedAt timestamp + const updates = { + status: "in_progress" as const, + startedAt: new Date().toISOString(), + }; + updateFeature(feature.id, updates); + persistFeatureUpdate(feature.id, updates); + console.log( + "[Board] Feature moved to in_progress via Implement button, starting agent..." + ); + await handleRunFeature(feature); + }} hasContext={featuresWithContext.has(feature.id)} isCurrentAutoTask={runningAutoTasks.includes( feature.id @@ -2157,6 +2261,136 @@ export function BoardView() { onOpenChange={setShowBoardBackgroundModal} /> + {/* Completed Features Modal */} + + + + + + Completed Features + + + {completedFeatures.length === 0 + ? "No completed features yet. Features you complete will appear here." + : `${completedFeatures.length} completed feature${ + completedFeatures.length === 1 ? "" : "s" + }`} + + +
+ {completedFeatures.length === 0 ? ( +
+ +

No completed features

+

+ Complete features from the Verified column to archive them + here. +

+
+ ) : ( +
+ {completedFeatures.map((feature) => ( + + + + {feature.description || feature.summary || feature.id} + + + {feature.category || "Uncategorized"} + + +
+ + +
+
+ ))} +
+ )} +
+ + + +
+
+ + {/* Delete Completed Feature Confirmation Dialog */} + !open && setDeleteCompletedFeature(null)} + > + + + + + Delete Feature + + + Are you sure you want to permanently delete this feature? + + "{deleteCompletedFeature?.description?.slice(0, 100)} + {(deleteCompletedFeature?.description?.length ?? 0) > 100 + ? "..." + : ""} + " + + + This action cannot be undone. + + + + + + + + + + {/* Add Feature Dialog */} void; onRevert?: () => void; onMerge?: () => void; + onImplement?: () => void; + onComplete?: () => void; hasContext?: boolean; isCurrentAutoTask?: boolean; shortcutKey?: string; @@ -162,6 +166,8 @@ export const KanbanCard = memo(function KanbanCard({ onCommit, onRevert, onMerge, + onImplement, + onComplete, hasContext, isCurrentAutoTask, shortcutKey, @@ -526,7 +532,120 @@ export const KanbanCard = memo(function KanbanCard({ )}
)} - {!isCurrentAutoTask && ( + {!isCurrentAutoTask && feature.status === "backlog" && ( +
+ +
+ )} + {!isCurrentAutoTask && feature.status === "waiting_approval" && ( +
+ + {onViewOutput && ( + + )} + +
+ )} + {!isCurrentAutoTask && feature.status === "verified" && ( +
+ + {onViewOutput && ( + + )} + +
+ )} + {!isCurrentAutoTask && feature.status === "in_progress" && (
@@ -552,7 +671,7 @@ export const KanbanCard = memo(function KanbanCard({ Edit - {onViewOutput && feature.status !== "backlog" && ( + {onViewOutput && ( { e.stopPropagation(); @@ -926,12 +1045,12 @@ export const KanbanCard = memo(function KanbanCard({ )} {!isCurrentAutoTask && feature.status === "verified" && ( <> - {/* Logs button if context exists */} - {hasContext && onViewOutput && ( + {/* Logs button - styled like Refine */} + {onViewOutput && ( + )} + {/* Complete button */} + {onComplete && ( + )} @@ -972,7 +1108,7 @@ export const KanbanCard = memo(function KanbanCard({ )} - {/* Follow-up prompt button */} + {/* Refine prompt button */} {onFollowUp && ( )} {/* Merge button - only show when worktree exists */} @@ -1026,6 +1162,40 @@ export const KanbanCard = memo(function KanbanCard({ )} )} + {!isCurrentAutoTask && feature.status === "backlog" && ( + <> + + {onImplement && ( + + )} + + )}
diff --git a/apps/app/src/store/app-store.ts b/apps/app/src/store/app-store.ts index a4625932..3c2aeaa4 100644 --- a/apps/app/src/store/app-store.ts +++ b/apps/app/src/store/app-store.ts @@ -277,7 +277,12 @@ export interface Feature { category: string; description: string; steps: string[]; - status: "backlog" | "in_progress" | "waiting_approval" | "verified"; + status: + | "backlog" + | "in_progress" + | "waiting_approval" + | "verified" + | "completed"; images?: FeatureImage[]; imagePaths?: FeatureImagePath[]; // Paths to temp files for agent context startedAt?: string; // ISO timestamp for when the card moved to in_progress From 919e08689a79bf612f7613a21199ea0bd2eadd5f Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Sun, 14 Dec 2025 20:21:42 -0500 Subject: [PATCH 2/5] refactor: streamline feature implementation handling in BoardView and KanbanCard - Introduced a helper function, handleStartImplementation, to manage concurrency checks and feature status updates when moving features from backlog to in_progress. - Simplified the onImplement callback in KanbanCard to utilize the new helper function, enhancing code readability and maintainability. - Removed redundant concurrency checks from multiple locations, centralizing the logic for better consistency and reducing code duplication. --- apps/app/src/components/views/board-view.tsx | 79 +++++------- apps/app/src/components/views/kanban-card.tsx | 116 ++++++------------ 2 files changed, 68 insertions(+), 127 deletions(-) diff --git a/apps/app/src/components/views/board-view.tsx b/apps/app/src/components/views/board-view.tsx index 6891c1e4..f7587f59 100644 --- a/apps/app/src/components/views/board-view.tsx +++ b/apps/app/src/components/views/board-view.tsx @@ -849,34 +849,12 @@ export function BoardView() { // Same column, nothing to do if (targetStatus === draggedFeature.status) return; - // Check concurrency limit before moving to in_progress (only for backlog -> in_progress and if running agent) - if ( - targetStatus === "in_progress" && - draggedFeature.status === "backlog" && - !autoMode.canStartNewTask - ) { - console.log("[Board] Cannot start new task - at max concurrency limit"); - toast.error("Concurrency limit reached", { - description: `You can only have ${autoMode.maxConcurrency} task${ - autoMode.maxConcurrency > 1 ? "s" : "" - } running at a time. Wait for a task to complete or increase the limit.`, - }); - return; - } - // Handle different drag scenarios if (draggedFeature.status === "backlog") { // From backlog if (targetStatus === "in_progress") { - // Update with startedAt timestamp - const updates = { - status: targetStatus, - startedAt: new Date().toISOString(), - }; - updateFeature(featureId, updates); - persistFeatureUpdate(featureId, updates); - console.log("[Board] Feature moved to in_progress, starting agent..."); - await handleRunFeature(draggedFeature); + // Use helper function to handle concurrency check and start implementation + await handleStartImplementation(draggedFeature); } else { moveFeature(featureId, targetStatus); persistFeatureUpdate(featureId, { status: targetStatus }); @@ -1149,6 +1127,28 @@ export function BoardView() { } }; + // Helper function to start implementing a feature (from backlog to in_progress) + const handleStartImplementation = async (feature: Feature) => { + if (!autoMode.canStartNewTask) { + toast.error("Concurrency limit reached", { + description: `You can only have ${autoMode.maxConcurrency} task${ + autoMode.maxConcurrency > 1 ? "s" : "" + } running at a time. Wait for a task to complete or increase the limit.`, + }); + return false; + } + + const updates = { + status: "in_progress" as const, + startedAt: new Date().toISOString(), + }; + updateFeature(feature.id, updates); + persistFeatureUpdate(feature.id, updates); + console.log("[Board] Feature moved to in_progress, starting agent..."); + await handleRunFeature(feature); + return true; + }; + const handleVerifyFeature = async (feature: Feature) => { if (!currentProject) return; @@ -2187,30 +2187,9 @@ export function BoardView() { onComplete={() => handleCompleteFeature(feature) } - onImplement={async () => { - // Check concurrency limit - if (!autoMode.canStartNewTask) { - toast.error("Concurrency limit reached", { - description: `You can only have ${ - autoMode.maxConcurrency - } task${ - autoMode.maxConcurrency > 1 ? "s" : "" - } running at a time. Wait for a task to complete or increase the limit.`, - }); - return; - } - // Update with startedAt timestamp - const updates = { - status: "in_progress" as const, - startedAt: new Date().toISOString(), - }; - updateFeature(feature.id, updates); - persistFeatureUpdate(feature.id, updates); - console.log( - "[Board] Feature moved to in_progress via Implement button, starting agent..." - ); - await handleRunFeature(feature); - }} + onImplement={() => + handleStartImplementation(feature) + } hasContext={featuresWithContext.has(feature.id)} isCurrentAutoTask={runningAutoTasks.includes( feature.id @@ -2376,9 +2355,9 @@ export function BoardView() { )} - {!isCurrentAutoTask && feature.status === "waiting_approval" && ( -
- - {onViewOutput && ( + {!isCurrentAutoTask && + (feature.status === "waiting_approval" || + feature.status === "verified") && ( +
- )} - -
- )} - {!isCurrentAutoTask && feature.status === "verified" && ( -
- - {onViewOutput && ( + {onViewOutput && ( + + )} - )} - -
- )} +
+ )} {!isCurrentAutoTask && feature.status === "in_progress" && (
From f25d62fe25f893a2ab628bfd1e84b255f67dadbe Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Mon, 15 Dec 2025 01:07:47 -0500 Subject: [PATCH 3/5] 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. --- apps/app/src/app/api/chat/route.ts | 172 ----------- .../layout/project-setup-dialog.tsx | 114 +++++++ apps/app/src/components/layout/sidebar.tsx | 90 +----- apps/app/src/components/views/board-view.tsx | 26 +- apps/app/src/config/model-config.ts | 93 ++++++ apps/server/src/lib/model-resolver.ts | 9 +- apps/server/src/lib/sdk-options.ts | 291 ++++++++++++++++++ .../app-spec/generate-features-from-spec.ts | 33 +- .../src/routes/app-spec/generate-spec.ts | 147 ++++++--- .../app-spec/parse-and-create-features.ts | 19 +- .../suggestions/generate-suggestions.ts | 11 +- apps/server/src/services/agent-service.ts | 30 +- apps/server/src/services/auto-mode-service.ts | 21 +- 13 files changed, 724 insertions(+), 332 deletions(-) delete mode 100644 apps/app/src/app/api/chat/route.ts create mode 100644 apps/app/src/components/layout/project-setup-dialog.tsx create mode 100644 apps/app/src/config/model-config.ts create mode 100644 apps/server/src/lib/sdk-options.ts diff --git a/apps/app/src/app/api/chat/route.ts b/apps/app/src/app/api/chat/route.ts deleted file mode 100644 index 11f3db6a..00000000 --- a/apps/app/src/app/api/chat/route.ts +++ /dev/null @@ -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 }); - } -} diff --git a/apps/app/src/components/layout/project-setup-dialog.tsx b/apps/app/src/components/layout/project-setup-dialog.tsx new file mode 100644 index 00000000..4b4ce15e --- /dev/null +++ b/apps/app/src/components/layout/project-setup-dialog.tsx @@ -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 ( + { + if (!open && !isCreatingSpec) { + onSkip(); + } + }} + > + + + Set Up Your Project + + We didn'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'll analyze your project's tech stack and create a + comprehensive specification. + + + +
+
+ +

+ 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. +

+