feat: implement follow-up and commit features for waiting_approval status

- Added functionality to allow users to send follow-up prompts for features in the waiting_approval status, enabling continued work with additional instructions.
- Implemented a commit feature that allows users to mark waiting_approval features as verified and commit changes directly.
- Updated the UI to include buttons for follow-up and commit actions on Kanban cards and integrated dialogs for user interaction.
- Enhanced the feature loader and executor to handle the new status and actions appropriately.

This update improves the workflow for managing features that require manual review and enhances user experience in the auto mode.
This commit is contained in:
Kacper
2025-12-09 22:25:20 +01:00
parent 66951f2b94
commit bfc0934ce9
15 changed files with 1079 additions and 80 deletions

View File

@@ -205,7 +205,7 @@ export function AgentOutputModal({
className="max-w-4xl max-h-[80vh] flex flex-col"
data-testid="agent-output-modal"
>
<DialogHeader>
<DialogHeader className="flex-shrink-0">
<div className="flex items-center justify-between">
<DialogTitle className="flex items-center gap-2">
<Loader2 className="w-5 h-5 text-purple-500 animate-spin" />
@@ -238,7 +238,10 @@ export function AgentOutputModal({
</button>
</div>
</div>
<DialogDescription className="mt-1">
<DialogDescription
className="mt-1 max-h-24 overflow-y-auto break-words"
data-testid="agent-output-description"
>
{featureDescription}
</DialogDescription>
</DialogHeader>
@@ -266,7 +269,7 @@ export function AgentOutputModal({
)}
</div>
<div className="text-xs text-muted-foreground text-center">
<div className="text-xs text-muted-foreground text-center flex-shrink-0">
{autoScrollRef.current
? "Auto-scrolling enabled"
: "Scroll to bottom to enable auto-scroll"}

View File

@@ -44,7 +44,7 @@ import { KanbanColumn } from "./kanban-column";
import { KanbanCard } from "./kanban-card";
import { AutoModeLog } from "./auto-mode-log";
import { AgentOutputModal } from "./agent-output-modal";
import { Plus, RefreshCw, Play, StopCircle, Loader2, ChevronUp, ChevronDown, Users, Trash2, FastForward, FlaskConical, CheckCircle2 } from "lucide-react";
import { Plus, RefreshCw, Play, StopCircle, Loader2, ChevronUp, ChevronDown, Users, Trash2, FastForward, FlaskConical, CheckCircle2, MessageSquare, GitCommit } from "lucide-react";
import { toast } from "sonner";
import { Slider } from "@/components/ui/slider";
import { Checkbox } from "@/components/ui/checkbox";
@@ -60,6 +60,7 @@ type ColumnId = Feature["status"];
const COLUMNS: { id: ColumnId; title: string; color: string }[] = [
{ id: "backlog", title: "Backlog", color: "bg-zinc-500" },
{ id: "in_progress", title: "In Progress", color: "bg-yellow-500" },
{ id: "waiting_approval", title: "Waiting Approval", color: "bg-orange-500" },
{ id: "verified", title: "Verified", color: "bg-green-500" },
];
@@ -95,6 +96,10 @@ export function BoardView() {
const [featuresWithContext, setFeaturesWithContext] = useState<Set<string>>(new Set());
const [showDeleteAllVerifiedDialog, setShowDeleteAllVerifiedDialog] = useState(false);
const [persistedCategories, setPersistedCategories] = useState<string[]>([]);
const [showFollowUpDialog, setShowFollowUpDialog] = useState(false);
const [followUpFeature, setFollowUpFeature] = useState<Feature | null>(null);
const [followUpPrompt, setFollowUpPrompt] = useState("");
const [followUpImagePaths, setFollowUpImagePaths] = useState<DescriptionImagePath[]>([]);
// Make current project available globally for modal
useEffect(() => {
@@ -688,6 +693,125 @@ export function BoardView() {
});
};
// Open follow-up dialog for waiting_approval features
const handleOpenFollowUp = (feature: Feature) => {
console.log("[Board] Opening follow-up dialog for feature:", { id: feature.id, description: feature.description });
setFollowUpFeature(feature);
setFollowUpPrompt("");
setFollowUpImagePaths([]);
setShowFollowUpDialog(true);
};
// Handle sending follow-up prompt
const handleSendFollowUp = async () => {
if (!currentProject || !followUpFeature || !followUpPrompt.trim()) return;
console.log("[Board] Sending follow-up prompt for feature:", {
id: followUpFeature.id,
prompt: followUpPrompt,
imagePaths: followUpImagePaths
});
try {
const api = getElectronAPI();
if (!api?.autoMode?.followUpFeature) {
console.error("Follow-up feature API not available");
toast.error("Follow-up not available", {
description: "This feature is not available in the current version.",
});
return;
}
// Move feature back to in_progress before sending follow-up
updateFeature(followUpFeature.id, { status: "in_progress", startedAt: new Date().toISOString() });
// Call the API to send follow-up prompt
const result = await api.autoMode.followUpFeature(
currentProject.path,
followUpFeature.id,
followUpPrompt,
followUpImagePaths.map(img => img.path)
);
if (result.success) {
console.log("[Board] Follow-up started successfully");
toast.success("Follow-up started", {
description: `Continuing work on: ${followUpFeature.description.slice(0, 50)}${followUpFeature.description.length > 50 ? "..." : ""}`,
});
setShowFollowUpDialog(false);
setFollowUpFeature(null);
setFollowUpPrompt("");
setFollowUpImagePaths([]);
} else {
console.error("[Board] Failed to send follow-up:", result.error);
toast.error("Failed to send follow-up", {
description: result.error || "An error occurred",
});
await loadFeatures();
}
} catch (error) {
console.error("[Board] Error sending follow-up:", error);
toast.error("Failed to send follow-up", {
description: error instanceof Error ? error.message : "An error occurred",
});
await loadFeatures();
}
};
// Handle commit-only for waiting_approval features (marks as verified and commits)
const handleCommitFeature = async (feature: Feature) => {
if (!currentProject) return;
console.log("[Board] Committing feature:", { id: feature.id, description: feature.description });
try {
const api = getElectronAPI();
if (!api?.autoMode?.commitFeature) {
console.error("Commit feature API not available");
toast.error("Commit not available", {
description: "This feature is not available in the current version.",
});
return;
}
// Call the API to commit this feature
const result = await api.autoMode.commitFeature(
currentProject.path,
feature.id
);
if (result.success) {
console.log("[Board] Feature committed successfully");
// Move to verified status
moveFeature(feature.id, "verified");
toast.success("Feature committed", {
description: `Committed and verified: ${feature.description.slice(0, 50)}${feature.description.length > 50 ? "..." : ""}`,
});
} else {
console.error("[Board] Failed to commit feature:", result.error);
toast.error("Failed to commit feature", {
description: result.error || "An error occurred",
});
await loadFeatures();
}
} catch (error) {
console.error("[Board] Error committing feature:", error);
toast.error("Failed to commit feature", {
description: error instanceof Error ? error.message : "An error occurred",
});
await loadFeatures();
}
};
// Move feature to waiting_approval (for skipTests features when agent completes)
const handleMoveToWaitingApproval = (feature: Feature) => {
console.log("[Board] Moving feature to waiting_approval:", { id: feature.id, description: feature.description });
updateFeature(feature.id, { status: "waiting_approval" });
toast.info("Feature ready for review", {
description: `Ready for approval: ${feature.description.slice(0, 50)}${feature.description.length > 50 ? "..." : ""}`,
});
};
const checkContextExists = async (featureId: string): Promise<boolean> => {
if (!currentProject) return false;
@@ -1000,6 +1124,8 @@ export function BoardView() {
onForceStop={() => handleForceStopFeature(feature)}
onManualVerify={() => handleManualVerify(feature)}
onMoveBackToInProgress={() => handleMoveBackToInProgress(feature)}
onFollowUp={() => handleOpenFollowUp(feature)}
onCommit={() => handleCommitFeature(feature)}
hasContext={featuresWithContext.has(feature.id)}
isCurrentAutoTask={runningAutoTasks.includes(feature.id)}
shortcutKey={shortcutKey}
@@ -1318,6 +1444,77 @@ export function BoardView() {
</DialogFooter>
</DialogContent>
</Dialog>
{/* Follow-Up Prompt Dialog */}
<Dialog open={showFollowUpDialog} onOpenChange={(open) => {
if (!open) {
setShowFollowUpDialog(false);
setFollowUpFeature(null);
setFollowUpPrompt("");
setFollowUpImagePaths([]);
}
}}>
<DialogContent
data-testid="follow-up-dialog"
onKeyDown={(e) => {
if ((e.metaKey || e.ctrlKey) && e.key === "Enter" && followUpPrompt.trim()) {
e.preventDefault();
handleSendFollowUp();
}
}}
>
<DialogHeader>
<DialogTitle>Follow-Up Prompt</DialogTitle>
<DialogDescription>
Send additional instructions to continue working on this feature.
{followUpFeature && (
<span className="block mt-2 text-primary">
Feature: {followUpFeature.description.slice(0, 100)}{followUpFeature.description.length > 100 ? "..." : ""}
</span>
)}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="follow-up-prompt">Instructions</Label>
<DescriptionImageDropZone
value={followUpPrompt}
onChange={setFollowUpPrompt}
images={followUpImagePaths}
onImagesChange={setFollowUpImagePaths}
placeholder="Describe what needs to be fixed or changed..."
/>
</div>
<p className="text-xs text-muted-foreground">
The agent will continue from where it left off, using the existing context.
You can attach screenshots to help explain the issue.
</p>
</div>
<DialogFooter>
<Button variant="ghost" onClick={() => {
setShowFollowUpDialog(false);
setFollowUpFeature(null);
setFollowUpPrompt("");
setFollowUpImagePaths([]);
}}>
Cancel
</Button>
<Button
onClick={handleSendFollowUp}
disabled={!followUpPrompt.trim()}
data-testid="confirm-follow-up"
>
<MessageSquare className="w-4 h-4 mr-2" />
Send Follow-Up
<span
className="ml-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-white/10 border border-white/20"
>
</span>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -34,6 +34,8 @@ import {
StopCircle,
FlaskConical,
ArrowLeft,
MessageSquare,
GitCommit,
} from "lucide-react";
import { CountUpTimer } from "@/components/ui/count-up-timer";
@@ -47,6 +49,8 @@ interface KanbanCardProps {
onForceStop?: () => void;
onManualVerify?: () => void;
onMoveBackToInProgress?: () => void;
onFollowUp?: () => void;
onCommit?: () => void;
hasContext?: boolean;
isCurrentAutoTask?: boolean;
shortcutKey?: string;
@@ -62,6 +66,8 @@ export function KanbanCard({
onForceStop,
onManualVerify,
onMoveBackToInProgress,
onFollowUp,
onCommit,
hasContext,
isCurrentAutoTask,
shortcutKey,
@@ -364,6 +370,51 @@ export function KanbanCard({
</Button>
</>
)}
{!isCurrentAutoTask && feature.status === "waiting_approval" && (
<>
{/* Follow-up prompt button */}
{onFollowUp && (
<Button
variant="default"
size="sm"
className="flex-1 h-7 text-xs bg-blue-600 hover:bg-blue-700"
onClick={(e) => {
e.stopPropagation();
onFollowUp();
}}
data-testid={`follow-up-${feature.id}`}
>
<MessageSquare className="w-3 h-3 mr-1" />
Follow-up
</Button>
)}
{/* Commit and verify button */}
{onCommit && (
<Button
variant="default"
size="sm"
className="flex-1 h-7 text-xs bg-green-600 hover:bg-green-700"
onClick={(e) => {
e.stopPropagation();
onCommit();
}}
data-testid={`commit-${feature.id}`}
>
<GitCommit className="w-3 h-3 mr-1" />
Commit
</Button>
)}
<Button
variant="ghost"
size="sm"
className="h-7 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={handleDeleteClick}
data-testid={`delete-waiting-feature-${feature.id}`}
>
<Trash2 className="w-3 h-3" />
</Button>
</>
)}
{!isCurrentAutoTask && feature.status === "backlog" && (
<>
<Button