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