From b112747073602291c9e5a426018021a2a89ef6ec Mon Sep 17 00:00:00 2001 From: SuperComboGamer Date: Wed, 17 Dec 2025 19:39:09 -0500 Subject: [PATCH] feat: implement plan approval functionality in board view - Introduced PlanApprovalDialog for reviewing and approving feature plans. - Added state management for pending plan approvals and loading states. - Enhanced BoardView to handle plan approval actions, including approve and reject functionalities. - Updated KanbanCard and KanbanBoard components to include buttons for viewing and approving plans. - Integrated plan approval logic into the auto mode service, allowing for user feedback and plan edits. - Updated app state to manage default plan approval settings and integrate with existing feature workflows. --- apps/app/src/components/views/board-view.tsx | 162 ++++++ .../board-view/components/kanban-card.tsx | 66 ++- .../board-view/dialogs/add-feature-dialog.tsx | 11 +- .../board-view/dialogs/agent-output-modal.tsx | 30 ++ .../dialogs/edit-feature-dialog.tsx | 6 + .../views/board-view/dialogs/index.ts | 1 + .../dialogs/plan-approval-dialog.tsx | 217 ++++++++ .../board-view/hooks/use-board-actions.ts | 6 +- .../board-view/hooks/use-board-features.ts | 5 + .../views/board-view/kanban-board.tsx | 6 + .../shared/planning-mode-selector.tsx | 23 + .../src/components/views/settings-view.tsx | 4 + .../feature-defaults-section.tsx | 40 +- apps/app/src/hooks/use-auto-mode.ts | 87 +++- apps/app/src/lib/electron.ts | 24 + apps/app/src/lib/http-api-client.ts | 14 + apps/app/src/store/app-store.ts | 27 + apps/app/src/types/electron.d.ts | 43 ++ apps/server/src/routes/auto-mode/index.ts | 2 + .../routes/auto-mode/routes/approve-plan.ts | 78 +++ apps/server/src/services/auto-mode-service.ts | 474 ++++++++++++++++-- apps/server/src/services/feature-loader.ts | 21 +- 22 files changed, 1290 insertions(+), 57 deletions(-) create mode 100644 apps/app/src/components/views/board-view/dialogs/plan-approval-dialog.tsx create mode 100644 apps/server/src/routes/auto-mode/routes/approve-plan.ts diff --git a/apps/app/src/components/views/board-view.tsx b/apps/app/src/components/views/board-view.tsx index 2836c917..7bf3fe37 100644 --- a/apps/app/src/components/views/board-view.tsx +++ b/apps/app/src/components/views/board-view.tsx @@ -29,6 +29,7 @@ import { EditFeatureDialog, FeatureSuggestionsDialog, FollowUpDialog, + PlanApprovalDialog, } from "./board-view/dialogs"; import { COLUMNS } from "./board-view/constants"; import { @@ -56,6 +57,9 @@ export function BoardView() { setKanbanCardDetailLevel, specCreatingForProject, setSpecCreatingForProject, + pendingPlanApproval, + setPendingPlanApproval, + updateFeature, } = useAppStore(); const shortcuts = useKeyboardShortcutsConfig(); const { @@ -80,6 +84,8 @@ export function BoardView() { const [showCompletedModal, setShowCompletedModal] = useState(false); const [deleteCompletedFeature, setDeleteCompletedFeature] = useState(null); + // State for viewing plan in read-only mode + const [viewPlanFeature, setViewPlanFeature] = useState(null); // Follow-up state hook const { @@ -111,6 +117,8 @@ export function BoardView() { } = useSuggestionsState(); // Search filter for Kanban cards const [searchQuery, setSearchQuery] = useState(""); + // Plan approval loading state + const [isPlanApprovalLoading, setIsPlanApprovalLoading] = useState(false); // Derive spec creation state from store - check if current project is the one being created const isCreatingSpec = specCreatingForProject === currentProject?.path; const creatingSpecProjectPath = specCreatingForProject ?? undefined; @@ -297,6 +305,130 @@ export function BoardView() { currentProject, }); + // Find feature for pending plan approval + const pendingApprovalFeature = useMemo(() => { + if (!pendingPlanApproval) return null; + return hookFeatures.find((f) => f.id === pendingPlanApproval.featureId) || null; + }, [pendingPlanApproval, hookFeatures]); + + // Handle plan approval + const handlePlanApprove = useCallback( + async (editedPlan?: string) => { + if (!pendingPlanApproval || !currentProject) return; + + const featureId = pendingPlanApproval.featureId; + setIsPlanApprovalLoading(true); + try { + const api = getElectronAPI(); + if (!api?.autoMode?.approvePlan) { + throw new Error("Plan approval API not available"); + } + + const result = await api.autoMode.approvePlan( + pendingPlanApproval.projectPath, + pendingPlanApproval.featureId, + true, + editedPlan + ); + + if (result.success) { + // Immediately update local feature state to hide "Approve Plan" button + // Get current feature to preserve version + const currentFeature = hookFeatures.find(f => f.id === featureId); + updateFeature(featureId, { + planSpec: { + status: 'approved', + content: editedPlan || pendingPlanApproval.planContent, + version: currentFeature?.planSpec?.version || 1, + approvedAt: new Date().toISOString(), + reviewedByUser: true, + }, + }); + // Reload features from server to ensure sync + loadFeatures(); + } else { + console.error("[Board] Failed to approve plan:", result.error); + } + } catch (error) { + console.error("[Board] Error approving plan:", error); + } finally { + setIsPlanApprovalLoading(false); + setPendingPlanApproval(null); + } + }, + [pendingPlanApproval, currentProject, setPendingPlanApproval, updateFeature, loadFeatures, hookFeatures] + ); + + // Handle plan rejection + const handlePlanReject = useCallback( + async (feedback?: string) => { + if (!pendingPlanApproval || !currentProject) return; + + const featureId = pendingPlanApproval.featureId; + setIsPlanApprovalLoading(true); + try { + const api = getElectronAPI(); + if (!api?.autoMode?.approvePlan) { + throw new Error("Plan approval API not available"); + } + + const result = await api.autoMode.approvePlan( + pendingPlanApproval.projectPath, + pendingPlanApproval.featureId, + false, + undefined, + feedback + ); + + if (result.success) { + // Immediately update local feature state + // Get current feature to preserve version + const currentFeature = hookFeatures.find(f => f.id === featureId); + updateFeature(featureId, { + status: 'backlog', + planSpec: { + status: 'rejected', + content: pendingPlanApproval.planContent, + version: currentFeature?.planSpec?.version || 1, + reviewedByUser: true, + }, + }); + // Reload features from server to ensure sync + loadFeatures(); + } else { + console.error("[Board] Failed to reject plan:", result.error); + } + } catch (error) { + console.error("[Board] Error rejecting plan:", error); + } finally { + setIsPlanApprovalLoading(false); + setPendingPlanApproval(null); + } + }, + [pendingPlanApproval, currentProject, setPendingPlanApproval, updateFeature, loadFeatures, hookFeatures] + ); + + // Handle opening approval dialog from feature card button + const handleOpenApprovalDialog = useCallback( + (feature: Feature) => { + if (!feature.planSpec?.content || !currentProject) return; + + // Determine the planning mode for approval (skip should never have a plan requiring approval) + const mode = feature.planningMode; + const approvalMode: "lite" | "spec" | "full" = + mode === 'lite' || mode === 'spec' || mode === 'full' ? mode : 'spec'; + + // Re-open the approval dialog with the feature's plan data + setPendingPlanApproval({ + featureId: feature.id, + projectPath: currentProject.path, + planContent: feature.planSpec.content, + planningMode: approvalMode, + }); + }, + [currentProject, setPendingPlanApproval] + ); + if (!currentProject) { return (
setViewPlanFeature(feature)} + onApprovePlan={handleOpenApprovalDialog} featuresWithContext={featuresWithContext} runningAutoTasks={runningAutoTasks} shortcuts={shortcuts} @@ -494,6 +628,34 @@ export function BoardView() { isGenerating={isGeneratingSuggestions} setIsGenerating={setIsGeneratingSuggestions} /> + + {/* Plan Approval Dialog */} + { + if (!open) { + setPendingPlanApproval(null); + } + }} + feature={pendingApprovalFeature} + planContent={pendingPlanApproval?.planContent || ""} + onApprove={handlePlanApprove} + onReject={handlePlanReject} + isLoading={isPlanApprovalLoading} + /> + + {/* View Plan Dialog (read-only) */} + {viewPlanFeature && viewPlanFeature.planSpec?.content && ( + !open && setViewPlanFeature(null)} + feature={viewPlanFeature} + planContent={viewPlanFeature.planSpec.content} + onApprove={() => setViewPlanFeature(null)} + onReject={() => setViewPlanFeature(null)} + viewOnly={true} + /> + )}
); } diff --git a/apps/app/src/components/views/board-view/components/kanban-card.tsx b/apps/app/src/components/views/board-view/components/kanban-card.tsx index 4d23a31f..c4b80e78 100644 --- a/apps/app/src/components/views/board-view/components/kanban-card.tsx +++ b/apps/app/src/components/views/board-view/components/kanban-card.tsx @@ -107,6 +107,8 @@ interface KanbanCardProps { onMerge?: () => void; onImplement?: () => void; onComplete?: () => void; + onViewPlan?: () => void; + onApprovePlan?: () => void; hasContext?: boolean; isCurrentAutoTask?: boolean; shortcutKey?: string; @@ -134,6 +136,8 @@ export const KanbanCard = memo(function KanbanCard({ onMerge, onImplement, onComplete, + onViewPlan, + onApprovePlan, hasContext, isCurrentAutoTask, shortcutKey, @@ -858,14 +862,31 @@ export const KanbanCard = memo(function KanbanCard({ )} {/* Actions */} -
+
{isCurrentAutoTask && ( <> + {/* Approve Plan button - PRIORITY: shows even when agent is "running" (paused for approval) */} + {feature.planSpec?.status === 'generated' && onApprovePlan && ( + + )} {onViewOutput && ( + )} {feature.skipTests && onManualVerify ? ( + {feature.planSpec?.content && onViewPlan && ( + + )} {onImplement && ( +
+ )} + + {/* Plan Content */} +
+ {isEditMode && !viewOnly ? ( +