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 ? ( +