mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 20:43:36 +00:00
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.
This commit is contained in:
@@ -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 */}
|
||||
<div className="flex gap-1.5">
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{isCurrentAutoTask && (
|
||||
<>
|
||||
{/* Approve Plan button - PRIORITY: shows even when agent is "running" (paused for approval) */}
|
||||
{feature.planSpec?.status === 'generated' && onApprovePlan && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 min-w-0 h-7 text-[11px] bg-purple-600 hover:bg-purple-700 text-white animate-pulse"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onApprovePlan();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`approve-plan-running-${feature.id}`}
|
||||
>
|
||||
<FileText className="w-3 h-3 mr-1 shrink-0" />
|
||||
<span className="truncate">Approve Plan</span>
|
||||
</Button>
|
||||
)}
|
||||
{onViewOutput && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-[11px] bg-[var(--status-info)] hover:bg-[var(--status-info)]/90"
|
||||
className="flex-1 min-w-0 h-7 text-[11px] bg-[var(--status-info)] hover:bg-[var(--status-info)]/90"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onViewOutput();
|
||||
@@ -873,11 +894,11 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`view-output-${feature.id}`}
|
||||
>
|
||||
<FileText className="w-3 h-3 mr-1" />
|
||||
Logs
|
||||
<FileText className="w-3 h-3 mr-1 shrink-0" />
|
||||
<span className="truncate">Logs</span>
|
||||
{shortcutKey && (
|
||||
<span
|
||||
className="ml-1.5 px-1 py-0.5 text-[9px] font-mono rounded bg-white/20"
|
||||
className="ml-1.5 px-1 py-0.5 text-[9px] font-mono rounded bg-white/20 shrink-0"
|
||||
data-testid={`shortcut-key-${feature.id}`}
|
||||
>
|
||||
{shortcutKey}
|
||||
@@ -889,7 +910,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="h-7 text-[11px] px-2"
|
||||
className="h-7 text-[11px] px-2 shrink-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onForceStop();
|
||||
@@ -904,6 +925,23 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
)}
|
||||
{!isCurrentAutoTask && feature.status === "in_progress" && (
|
||||
<>
|
||||
{/* Approve Plan button - shows when plan is generated and waiting for approval */}
|
||||
{feature.planSpec?.status === 'generated' && onApprovePlan && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-[11px] bg-purple-600 hover:bg-purple-700 text-white animate-pulse"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onApprovePlan();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`approve-plan-${feature.id}`}
|
||||
>
|
||||
<FileText className="w-3 h-3 mr-1" />
|
||||
Approve Plan
|
||||
</Button>
|
||||
)}
|
||||
{feature.skipTests && onManualVerify ? (
|
||||
<Button
|
||||
variant="default"
|
||||
@@ -1099,6 +1137,22 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
<Edit className="w-3 h-3 mr-1" />
|
||||
Edit
|
||||
</Button>
|
||||
{feature.planSpec?.content && onViewPlan && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 text-xs px-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onViewPlan();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`view-plan-${feature.id}`}
|
||||
title="View Plan"
|
||||
>
|
||||
<Eye className="w-3 h-3" />
|
||||
</Button>
|
||||
)}
|
||||
{onImplement && (
|
||||
<Button
|
||||
variant="default"
|
||||
|
||||
@@ -60,6 +60,7 @@ interface AddFeatureDialogProps {
|
||||
thinkingLevel: ThinkingLevel;
|
||||
priority: number;
|
||||
planningMode: PlanningMode;
|
||||
requirePlanApproval: boolean;
|
||||
}) => void;
|
||||
categorySuggestions: string[];
|
||||
defaultSkipTests: boolean;
|
||||
@@ -96,9 +97,10 @@ export function AddFeatureDialog({
|
||||
const [isEnhancing, setIsEnhancing] = useState(false);
|
||||
const [enhancementMode, setEnhancementMode] = useState<'improve' | 'technical' | 'simplify' | 'acceptance'>('improve');
|
||||
const [planningMode, setPlanningMode] = useState<PlanningMode>('skip');
|
||||
const [requirePlanApproval, setRequirePlanApproval] = useState(false);
|
||||
|
||||
// Get enhancement model and default planning mode from store
|
||||
const { enhancementModel, defaultPlanningMode } = useAppStore();
|
||||
const { enhancementModel, defaultPlanningMode, defaultRequirePlanApproval } = useAppStore();
|
||||
|
||||
// Sync defaults when dialog opens
|
||||
useEffect(() => {
|
||||
@@ -108,8 +110,9 @@ export function AddFeatureDialog({
|
||||
skipTests: defaultSkipTests,
|
||||
}));
|
||||
setPlanningMode(defaultPlanningMode);
|
||||
setRequirePlanApproval(defaultRequirePlanApproval);
|
||||
}
|
||||
}, [open, defaultSkipTests, defaultPlanningMode]);
|
||||
}, [open, defaultSkipTests, defaultPlanningMode, defaultRequirePlanApproval]);
|
||||
|
||||
const handleAdd = () => {
|
||||
if (!newFeature.description.trim()) {
|
||||
@@ -134,6 +137,7 @@ export function AddFeatureDialog({
|
||||
thinkingLevel: normalizedThinking,
|
||||
priority: newFeature.priority,
|
||||
planningMode,
|
||||
requirePlanApproval,
|
||||
});
|
||||
|
||||
// Reset form
|
||||
@@ -149,6 +153,7 @@ export function AddFeatureDialog({
|
||||
thinkingLevel: "none",
|
||||
});
|
||||
setPlanningMode(defaultPlanningMode);
|
||||
setRequirePlanApproval(defaultRequirePlanApproval);
|
||||
setNewFeaturePreviewMap(new Map());
|
||||
setShowAdvancedOptions(false);
|
||||
setDescriptionError(false);
|
||||
@@ -408,6 +413,8 @@ export function AddFeatureDialog({
|
||||
<PlanningModeSelector
|
||||
mode={planningMode}
|
||||
onModeChange={setPlanningMode}
|
||||
requireApproval={requirePlanApproval}
|
||||
onRequireApprovalChange={setRequirePlanApproval}
|
||||
featureDescription={newFeature.description}
|
||||
testIdPrefix="add-feature"
|
||||
compact
|
||||
|
||||
@@ -187,6 +187,36 @@ export function AgentOutputModal({
|
||||
|
||||
newContent = prepContent;
|
||||
break;
|
||||
case "planning_started":
|
||||
// Show when planning mode begins
|
||||
if ("mode" in event && "message" in event) {
|
||||
const modeLabel =
|
||||
event.mode === "lite"
|
||||
? "Lite"
|
||||
: event.mode === "spec"
|
||||
? "Spec"
|
||||
: "Full";
|
||||
newContent = `\n📋 Planning Mode: ${modeLabel}\n${event.message}\n`;
|
||||
}
|
||||
break;
|
||||
case "plan_approval_required":
|
||||
// Show when plan requires approval
|
||||
if ("planningMode" in event) {
|
||||
newContent = `\n⏸️ Plan generated - waiting for your approval...\n`;
|
||||
}
|
||||
break;
|
||||
case "plan_approved":
|
||||
// Show when plan is manually approved
|
||||
if ("hasEdits" in event) {
|
||||
newContent = event.hasEdits
|
||||
? `\n✅ Plan approved (with edits) - continuing to implementation...\n`
|
||||
: `\n✅ Plan approved - continuing to implementation...\n`;
|
||||
}
|
||||
break;
|
||||
case "plan_auto_approved":
|
||||
// Show when plan is auto-approved
|
||||
newContent = `\n✅ Plan auto-approved - continuing to implementation...\n`;
|
||||
break;
|
||||
case "auto_mode_feature_complete":
|
||||
const emoji = event.passes ? "✅" : "⚠️";
|
||||
newContent = `\n${emoji} Task completed: ${event.message}\n`;
|
||||
|
||||
@@ -62,6 +62,7 @@ interface EditFeatureDialogProps {
|
||||
imagePaths: DescriptionImagePath[];
|
||||
priority: number;
|
||||
planningMode: PlanningMode;
|
||||
requirePlanApproval: boolean;
|
||||
}
|
||||
) => void;
|
||||
categorySuggestions: string[];
|
||||
@@ -89,6 +90,7 @@ export function EditFeatureDialog({
|
||||
const [enhancementMode, setEnhancementMode] = useState<'improve' | 'technical' | 'simplify' | 'acceptance'>('improve');
|
||||
const [showDependencyTree, setShowDependencyTree] = useState(false);
|
||||
const [planningMode, setPlanningMode] = useState<PlanningMode>(feature?.planningMode ?? 'skip');
|
||||
const [requirePlanApproval, setRequirePlanApproval] = useState(feature?.requirePlanApproval ?? false);
|
||||
|
||||
// Get enhancement model from store
|
||||
const { enhancementModel } = useAppStore();
|
||||
@@ -97,6 +99,7 @@ export function EditFeatureDialog({
|
||||
setEditingFeature(feature);
|
||||
if (feature) {
|
||||
setPlanningMode(feature.planningMode ?? 'skip');
|
||||
setRequirePlanApproval(feature.requirePlanApproval ?? false);
|
||||
} else {
|
||||
setEditFeaturePreviewMap(new Map());
|
||||
setShowEditAdvancedOptions(false);
|
||||
@@ -121,6 +124,7 @@ export function EditFeatureDialog({
|
||||
imagePaths: editingFeature.imagePaths ?? [],
|
||||
priority: editingFeature.priority ?? 2,
|
||||
planningMode,
|
||||
requirePlanApproval,
|
||||
};
|
||||
|
||||
onUpdate(editingFeature.id, updates);
|
||||
@@ -394,6 +398,8 @@ export function EditFeatureDialog({
|
||||
<PlanningModeSelector
|
||||
mode={planningMode}
|
||||
onModeChange={setPlanningMode}
|
||||
requireApproval={requirePlanApproval}
|
||||
onRequireApprovalChange={setRequirePlanApproval}
|
||||
featureDescription={editingFeature.description}
|
||||
testIdPrefix="edit-feature"
|
||||
compact
|
||||
|
||||
@@ -6,3 +6,4 @@ export { DeleteCompletedFeatureDialog } from "./delete-completed-feature-dialog"
|
||||
export { EditFeatureDialog } from "./edit-feature-dialog";
|
||||
export { FeatureSuggestionsDialog } from "./feature-suggestions-dialog";
|
||||
export { FollowUpDialog } from "./follow-up-dialog";
|
||||
export { PlanApprovalDialog } from "./plan-approval-dialog";
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Markdown } from "@/components/ui/markdown";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Feature } from "@/store/app-store";
|
||||
import { Check, X, Edit2, Eye, Loader2 } from "lucide-react";
|
||||
|
||||
interface PlanApprovalDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
feature: Feature | null;
|
||||
planContent: string;
|
||||
onApprove: (editedPlan?: string) => void;
|
||||
onReject: (feedback?: string) => void;
|
||||
isLoading?: boolean;
|
||||
viewOnly?: boolean;
|
||||
}
|
||||
|
||||
export function PlanApprovalDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
feature,
|
||||
planContent,
|
||||
onApprove,
|
||||
onReject,
|
||||
isLoading = false,
|
||||
viewOnly = false,
|
||||
}: PlanApprovalDialogProps) {
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
const [editedPlan, setEditedPlan] = useState(planContent);
|
||||
const [showRejectFeedback, setShowRejectFeedback] = useState(false);
|
||||
const [rejectFeedback, setRejectFeedback] = useState("");
|
||||
|
||||
// Reset state when dialog opens or plan content changes
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setEditedPlan(planContent);
|
||||
setIsEditMode(false);
|
||||
setShowRejectFeedback(false);
|
||||
setRejectFeedback("");
|
||||
}
|
||||
}, [open, planContent]);
|
||||
|
||||
const handleApprove = () => {
|
||||
// Only pass edited plan if it was modified
|
||||
const wasEdited = editedPlan !== planContent;
|
||||
onApprove(wasEdited ? editedPlan : undefined);
|
||||
};
|
||||
|
||||
const handleReject = () => {
|
||||
if (showRejectFeedback) {
|
||||
onReject(rejectFeedback.trim() || undefined);
|
||||
} else {
|
||||
setShowRejectFeedback(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelReject = () => {
|
||||
setShowRejectFeedback(false);
|
||||
setRejectFeedback("");
|
||||
};
|
||||
|
||||
const handleClose = (open: boolean) => {
|
||||
if (!open && !isLoading) {
|
||||
onOpenChange(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent
|
||||
className="max-w-4xl"
|
||||
data-testid="plan-approval-dialog"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{viewOnly ? "View Plan" : "Review Plan"}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{viewOnly
|
||||
? "View the generated plan for this feature."
|
||||
: "Review the generated plan before implementation begins."}
|
||||
{feature && (
|
||||
<span className="block mt-2 text-primary">
|
||||
Feature: {feature.description.slice(0, 150)}
|
||||
{feature.description.length > 150 ? "..." : ""}
|
||||
</span>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-hidden flex flex-col min-h-0">
|
||||
{/* Mode Toggle - Only show when not in viewOnly mode */}
|
||||
{!viewOnly && (
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<Label className="text-sm text-muted-foreground">
|
||||
{isEditMode ? "Edit Mode" : "View Mode"}
|
||||
</Label>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsEditMode(!isEditMode)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isEditMode ? (
|
||||
<>
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
View
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Edit2 className="w-4 h-4 mr-2" />
|
||||
Edit
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Plan Content */}
|
||||
<div className="flex-1 overflow-y-auto max-h-[70vh] border border-border rounded-lg">
|
||||
{isEditMode && !viewOnly ? (
|
||||
<Textarea
|
||||
value={editedPlan}
|
||||
onChange={(e) => setEditedPlan(e.target.value)}
|
||||
className="min-h-[400px] h-full w-full border-0 rounded-lg resize-none font-mono text-sm"
|
||||
placeholder="Enter plan content..."
|
||||
disabled={isLoading}
|
||||
/>
|
||||
) : (
|
||||
<div className="p-4 overflow-auto">
|
||||
<Markdown>{editedPlan || "No plan content available."}</Markdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Reject Feedback Section - Only show when not in viewOnly mode */}
|
||||
{showRejectFeedback && !viewOnly && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<Label htmlFor="reject-feedback">Feedback (optional)</Label>
|
||||
<Textarea
|
||||
id="reject-feedback"
|
||||
value={rejectFeedback}
|
||||
onChange={(e) => setRejectFeedback(e.target.value)}
|
||||
placeholder="Provide feedback on why this plan is being rejected..."
|
||||
className="min-h-[80px]"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex-shrink-0 gap-2">
|
||||
{viewOnly ? (
|
||||
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
||||
Close
|
||||
</Button>
|
||||
) : showRejectFeedback ? (
|
||||
<>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={handleCancelReject}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleReject}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
Confirm Reject
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleReject}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
Reject
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleApprove}
|
||||
disabled={isLoading}
|
||||
className="bg-green-600 hover:bg-green-700 text-white"
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
Approve
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { Feature, FeatureImage, AgentModel, ThinkingLevel, useAppStore } from "@/store/app-store";
|
||||
import { Feature, FeatureImage, AgentModel, ThinkingLevel, PlanningMode, useAppStore } from "@/store/app-store";
|
||||
import { FeatureImagePath as DescriptionImagePath } from "@/components/ui/description-image-dropzone";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import { toast } from "sonner";
|
||||
@@ -67,6 +67,8 @@ export function useBoardActions({
|
||||
model: AgentModel;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
priority: number;
|
||||
planningMode: PlanningMode;
|
||||
requirePlanApproval: boolean;
|
||||
}) => {
|
||||
const newFeatureData = {
|
||||
...featureData,
|
||||
@@ -91,6 +93,8 @@ export function useBoardActions({
|
||||
thinkingLevel: ThinkingLevel;
|
||||
imagePaths: DescriptionImagePath[];
|
||||
priority: number;
|
||||
planningMode?: PlanningMode;
|
||||
requirePlanApproval?: boolean;
|
||||
}
|
||||
) => {
|
||||
updateFeature(featureId, updates);
|
||||
|
||||
@@ -210,6 +210,11 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
|
||||
.play()
|
||||
.catch((err) => console.warn("Could not play ding sound:", err));
|
||||
}
|
||||
} else if (event.type === "plan_approval_required") {
|
||||
// Reload features when plan is generated and requires approval
|
||||
// This ensures the feature card shows the "Approve Plan" button
|
||||
console.log("[Board] Plan approval required, reloading features...");
|
||||
loadFeatures();
|
||||
} else if (event.type === "auto_mode_error") {
|
||||
// Reload features when an error occurs (feature moved to waiting_approval)
|
||||
console.log(
|
||||
|
||||
@@ -49,6 +49,8 @@ interface KanbanBoardProps {
|
||||
onMerge: (feature: Feature) => void;
|
||||
onComplete: (feature: Feature) => void;
|
||||
onImplement: (feature: Feature) => void;
|
||||
onViewPlan: (feature: Feature) => void;
|
||||
onApprovePlan: (feature: Feature) => void;
|
||||
featuresWithContext: Set<string>;
|
||||
runningAutoTasks: string[];
|
||||
shortcuts: ReturnType<typeof useKeyboardShortcutsConfig>;
|
||||
@@ -81,6 +83,8 @@ export function KanbanBoard({
|
||||
onMerge,
|
||||
onComplete,
|
||||
onImplement,
|
||||
onViewPlan,
|
||||
onApprovePlan,
|
||||
featuresWithContext,
|
||||
runningAutoTasks,
|
||||
shortcuts,
|
||||
@@ -195,6 +199,8 @@ export function KanbanBoard({
|
||||
onMerge={() => onMerge(feature)}
|
||||
onComplete={() => onComplete(feature)}
|
||||
onImplement={() => onImplement(feature)}
|
||||
onViewPlan={() => onViewPlan(feature)}
|
||||
onApprovePlan={() => onApprovePlan(feature)}
|
||||
hasContext={featuresWithContext.has(feature.id)}
|
||||
isCurrentAutoTask={runningAutoTasks.includes(
|
||||
feature.id
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type PlanningMode = 'skip' | 'lite' | 'spec' | 'full';
|
||||
@@ -25,6 +26,8 @@ export interface PlanSpec {
|
||||
interface PlanningModeSelectorProps {
|
||||
mode: PlanningMode;
|
||||
onModeChange: (mode: PlanningMode) => void;
|
||||
requireApproval?: boolean;
|
||||
onRequireApprovalChange?: (require: boolean) => void;
|
||||
planSpec?: PlanSpec;
|
||||
onGenerateSpec?: () => void;
|
||||
onApproveSpec?: () => void;
|
||||
@@ -81,6 +84,8 @@ const modes = [
|
||||
export function PlanningModeSelector({
|
||||
mode,
|
||||
onModeChange,
|
||||
requireApproval,
|
||||
onRequireApprovalChange,
|
||||
planSpec,
|
||||
onGenerateSpec,
|
||||
onApproveSpec,
|
||||
@@ -195,6 +200,24 @@ export function PlanningModeSelector({
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Require Approval Checkbox - Only show when mode !== 'skip' */}
|
||||
{mode !== 'skip' && onRequireApprovalChange && (
|
||||
<div className="flex items-center gap-2 mt-3 pt-3 border-t border-border">
|
||||
<Checkbox
|
||||
id="require-approval"
|
||||
checked={requireApproval}
|
||||
onCheckedChange={(checked) => onRequireApprovalChange(checked === true)}
|
||||
data-testid={`${testIdPrefix}-require-approval-checkbox`}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="require-approval"
|
||||
className="text-sm text-muted-foreground cursor-pointer"
|
||||
>
|
||||
Manually approve plan before implementation
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Spec Preview/Actions Panel - Only for spec/full modes */}
|
||||
{requiresApproval && (
|
||||
<div className={cn(
|
||||
|
||||
Reference in New Issue
Block a user