mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 20:43:36 +00:00
FINSIHED
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { CheckCircle2, Circle, Loader2, ChevronDown, ChevronRight } from "lucide-react";
|
import { CheckCircle2, Circle, Loader2, ChevronDown, ChevronRight } from "lucide-react";
|
||||||
import { getElectronAPI } from "@/lib/electron";
|
import { getElectronAPI } from "@/lib/electron";
|
||||||
@@ -16,15 +16,65 @@ interface TaskInfo {
|
|||||||
|
|
||||||
interface TaskProgressPanelProps {
|
interface TaskProgressPanelProps {
|
||||||
featureId: string;
|
featureId: string;
|
||||||
|
projectPath?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TaskProgressPanel({ featureId, className }: TaskProgressPanelProps) {
|
export function TaskProgressPanel({ featureId, projectPath, className }: TaskProgressPanelProps) {
|
||||||
const [tasks, setTasks] = useState<TaskInfo[]>([]);
|
const [tasks, setTasks] = useState<TaskInfo[]>([]);
|
||||||
const [isExpanded, setIsExpanded] = useState(true);
|
const [isExpanded, setIsExpanded] = useState(true);
|
||||||
const [currentTaskId, setCurrentTaskId] = useState<string | null>(null);
|
const [currentTaskId, setCurrentTaskId] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
// Listen to task events
|
// Load initial tasks from feature's planSpec
|
||||||
|
const loadInitialTasks = useCallback(async () => {
|
||||||
|
if (!projectPath) {
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (!api?.features) {
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await api.features.get(projectPath, featureId);
|
||||||
|
if (result.success && result.feature?.planSpec?.tasks) {
|
||||||
|
const planTasks = result.feature.planSpec.tasks;
|
||||||
|
const currentId = result.feature.planSpec.currentTaskId;
|
||||||
|
const completedCount = result.feature.planSpec.tasksCompleted || 0;
|
||||||
|
|
||||||
|
// Convert planSpec tasks to TaskInfo with proper status
|
||||||
|
const initialTasks: TaskInfo[] = planTasks.map((t: any, index: number) => ({
|
||||||
|
id: t.id,
|
||||||
|
description: t.description,
|
||||||
|
filePath: t.filePath,
|
||||||
|
phase: t.phase,
|
||||||
|
status: index < completedCount
|
||||||
|
? "completed" as const
|
||||||
|
: t.id === currentId
|
||||||
|
? "in_progress" as const
|
||||||
|
: "pending" as const,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setTasks(initialTasks);
|
||||||
|
setCurrentTaskId(currentId || null);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load initial tasks:", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [featureId, projectPath]);
|
||||||
|
|
||||||
|
// Load initial state on mount
|
||||||
|
useEffect(() => {
|
||||||
|
loadInitialTasks();
|
||||||
|
}, [loadInitialTasks]);
|
||||||
|
|
||||||
|
// Listen to task events for real-time updates
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
if (!api?.autoMode) return;
|
if (!api?.autoMode) return;
|
||||||
@@ -69,24 +119,25 @@ export function TaskProgressPanel({ featureId, className }: TaskProgressPanelPro
|
|||||||
t.id === taskEvent.taskId ? { ...t, status: "completed" as const } : t
|
t.id === taskEvent.taskId ? { ...t, status: "completed" as const } : t
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
// Clear current task if it was completed
|
setCurrentTaskId(null);
|
||||||
if (currentTaskId === taskEvent.taskId) {
|
|
||||||
setCurrentTaskId(null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
}, [featureId, currentTaskId]);
|
}, [featureId]);
|
||||||
|
|
||||||
// Calculate progress
|
// Calculate progress
|
||||||
const completedCount = tasks.filter((t) => t.status === "completed").length;
|
const completedCount = tasks.filter((t) => t.status === "completed").length;
|
||||||
const totalCount = tasks.length;
|
const totalCount = tasks.length;
|
||||||
const progressPercent = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0;
|
const progressPercent = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0;
|
||||||
|
|
||||||
// Don't render if no tasks
|
// Don't render if loading or no tasks
|
||||||
|
if (isLoading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (tasks.length === 0) {
|
if (tasks.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,6 +218,13 @@ export function AgentOutputModal({
|
|||||||
// Show when plan is auto-approved
|
// Show when plan is auto-approved
|
||||||
newContent = `\n✅ Plan auto-approved - continuing to implementation...\n`;
|
newContent = `\n✅ Plan auto-approved - continuing to implementation...\n`;
|
||||||
break;
|
break;
|
||||||
|
case "plan_revision_requested":
|
||||||
|
// Show when user requests plan revision
|
||||||
|
if ("planVersion" in event) {
|
||||||
|
const revisionEvent = event as Extract<AutoModeEvent, { type: "plan_revision_requested" }>;
|
||||||
|
newContent = `\n🔄 Revising plan based on your feedback (v${revisionEvent.planVersion})...\n`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case "auto_mode_task_started":
|
case "auto_mode_task_started":
|
||||||
// Show when a task starts
|
// Show when a task starts
|
||||||
if ("taskId" in event && "taskDescription" in event) {
|
if ("taskId" in event && "taskDescription" in event) {
|
||||||
@@ -362,7 +369,11 @@ export function AgentOutputModal({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{/* Task Progress Panel - shows when tasks are being executed */}
|
{/* Task Progress Panel - shows when tasks are being executed */}
|
||||||
<TaskProgressPanel featureId={featureId} className="flex-shrink-0 mx-1" />
|
<TaskProgressPanel
|
||||||
|
featureId={featureId}
|
||||||
|
projectPath={projectPath}
|
||||||
|
className="flex-shrink-0 mx-1"
|
||||||
|
/>
|
||||||
|
|
||||||
{viewMode === "changes" ? (
|
{viewMode === "changes" ? (
|
||||||
<div className="flex-1 min-h-[400px] max-h-[60vh] overflow-y-auto scrollbar-visible">
|
<div className="flex-1 min-h-[400px] max-h-[60vh] overflow-y-auto scrollbar-visible">
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { Markdown } from "@/components/ui/markdown";
|
import { Markdown } from "@/components/ui/markdown";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Feature } from "@/store/app-store";
|
import { Feature } from "@/store/app-store";
|
||||||
import { Check, X, Edit2, Eye, Loader2 } from "lucide-react";
|
import { Check, RefreshCw, Edit2, Eye, Loader2 } from "lucide-react";
|
||||||
|
|
||||||
interface PlanApprovalDialogProps {
|
interface PlanApprovalDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -143,18 +143,21 @@ export function PlanApprovalDialog({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Reject Feedback Section - Only show when not in viewOnly mode */}
|
{/* Revision Feedback Section - Only show when not in viewOnly mode */}
|
||||||
{showRejectFeedback && !viewOnly && (
|
{showRejectFeedback && !viewOnly && (
|
||||||
<div className="mt-4 space-y-2">
|
<div className="mt-4 space-y-2">
|
||||||
<Label htmlFor="reject-feedback">Feedback (optional)</Label>
|
<Label htmlFor="reject-feedback">What changes would you like?</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="reject-feedback"
|
id="reject-feedback"
|
||||||
value={rejectFeedback}
|
value={rejectFeedback}
|
||||||
onChange={(e) => setRejectFeedback(e.target.value)}
|
onChange={(e) => setRejectFeedback(e.target.value)}
|
||||||
placeholder="Provide feedback on why this plan is being rejected..."
|
placeholder="Describe the changes you'd like to see in the plan..."
|
||||||
className="min-h-[80px]"
|
className="min-h-[80px]"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Leave empty to cancel the feature, or provide feedback to regenerate the plan.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -171,30 +174,30 @@ export function PlanApprovalDialog({
|
|||||||
onClick={handleCancelReject}
|
onClick={handleCancelReject}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
Cancel
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="secondary"
|
||||||
onClick={handleReject}
|
onClick={handleReject}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<X className="w-4 h-4 mr-2" />
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
)}
|
)}
|
||||||
Confirm Reject
|
{rejectFeedback.trim() ? "Revise Plan" : "Cancel Feature"}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="outline"
|
||||||
onClick={handleReject}
|
onClick={handleReject}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
<X className="w-4 h-4 mr-2" />
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
Reject
|
Request Changes
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleApprove}
|
onClick={handleApprove}
|
||||||
|
|||||||
@@ -12,6 +12,15 @@ import { cn } from "@/lib/utils";
|
|||||||
|
|
||||||
export type PlanningMode = 'skip' | 'lite' | 'spec' | 'full';
|
export type PlanningMode = 'skip' | 'lite' | 'spec' | 'full';
|
||||||
|
|
||||||
|
// Parsed task from spec (for spec and full planning modes)
|
||||||
|
export interface ParsedTask {
|
||||||
|
id: string; // e.g., "T001"
|
||||||
|
description: string; // e.g., "Create user model"
|
||||||
|
filePath?: string; // e.g., "src/models/user.ts"
|
||||||
|
phase?: string; // e.g., "Phase 1: Foundation" (for full mode)
|
||||||
|
status: 'pending' | 'in_progress' | 'completed' | 'failed';
|
||||||
|
}
|
||||||
|
|
||||||
export interface PlanSpec {
|
export interface PlanSpec {
|
||||||
status: 'pending' | 'generating' | 'generated' | 'approved' | 'rejected';
|
status: 'pending' | 'generating' | 'generated' | 'approved' | 'rejected';
|
||||||
content?: string;
|
content?: string;
|
||||||
@@ -21,6 +30,8 @@ export interface PlanSpec {
|
|||||||
reviewedByUser: boolean;
|
reviewedByUser: boolean;
|
||||||
tasksCompleted?: number;
|
tasksCompleted?: number;
|
||||||
tasksTotal?: number;
|
tasksTotal?: number;
|
||||||
|
currentTaskId?: string; // ID of the task currently being worked on
|
||||||
|
tasks?: ParsedTask[]; // Parsed tasks from the spec
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PlanningModeSelectorProps {
|
interface PlanningModeSelectorProps {
|
||||||
|
|||||||
@@ -294,6 +294,20 @@ export function useAutoMode() {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "plan_revision_requested":
|
||||||
|
// Log when user requests plan revision with feedback
|
||||||
|
if (event.featureId) {
|
||||||
|
const revisionEvent = event as Extract<AutoModeEvent, { type: "plan_revision_requested" }>;
|
||||||
|
console.log(`[AutoMode] Plan revision requested for ${event.featureId} (v${revisionEvent.planVersion})`);
|
||||||
|
addAutoModeActivity({
|
||||||
|
featureId: event.featureId,
|
||||||
|
type: "planning",
|
||||||
|
message: `Revising plan based on feedback (v${revisionEvent.planVersion})...`,
|
||||||
|
phase: "planning",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case "auto_mode_task_started":
|
case "auto_mode_task_started":
|
||||||
// Task started - show which task is being worked on
|
// Task started - show which task is being worked on
|
||||||
if (event.featureId && "taskId" in event && "taskDescription" in event) {
|
if (event.featureId && "taskId" in event && "taskDescription" in event) {
|
||||||
|
|||||||
@@ -305,6 +305,15 @@ export interface Feature {
|
|||||||
requirePlanApproval?: boolean; // Whether to pause and require manual approval before implementation
|
requirePlanApproval?: boolean; // Whether to pause and require manual approval before implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parsed task from spec (for spec and full planning modes)
|
||||||
|
export interface ParsedTask {
|
||||||
|
id: string; // e.g., "T001"
|
||||||
|
description: string; // e.g., "Create user model"
|
||||||
|
filePath?: string; // e.g., "src/models/user.ts"
|
||||||
|
phase?: string; // e.g., "Phase 1: Foundation" (for full mode)
|
||||||
|
status: 'pending' | 'in_progress' | 'completed' | 'failed';
|
||||||
|
}
|
||||||
|
|
||||||
// PlanSpec status for feature planning/specification
|
// PlanSpec status for feature planning/specification
|
||||||
export interface PlanSpec {
|
export interface PlanSpec {
|
||||||
status: 'pending' | 'generating' | 'generated' | 'approved' | 'rejected';
|
status: 'pending' | 'generating' | 'generated' | 'approved' | 'rejected';
|
||||||
@@ -315,6 +324,8 @@ export interface PlanSpec {
|
|||||||
reviewedByUser: boolean; // True if user has seen the spec
|
reviewedByUser: boolean; // True if user has seen the spec
|
||||||
tasksCompleted?: number;
|
tasksCompleted?: number;
|
||||||
tasksTotal?: number;
|
tasksTotal?: number;
|
||||||
|
currentTaskId?: string; // ID of the task currently being worked on
|
||||||
|
tasks?: ParsedTask[]; // Parsed tasks from the spec
|
||||||
}
|
}
|
||||||
|
|
||||||
// File tree node for project analysis
|
// File tree node for project analysis
|
||||||
|
|||||||
10
apps/app/src/types/electron.d.ts
vendored
10
apps/app/src/types/electron.d.ts
vendored
@@ -244,6 +244,7 @@ export type AutoModeEvent =
|
|||||||
projectPath?: string;
|
projectPath?: string;
|
||||||
planContent: string;
|
planContent: string;
|
||||||
planningMode: "lite" | "spec" | "full";
|
planningMode: "lite" | "spec" | "full";
|
||||||
|
planVersion?: number;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "plan_auto_approved";
|
type: "plan_auto_approved";
|
||||||
@@ -257,6 +258,7 @@ export type AutoModeEvent =
|
|||||||
featureId: string;
|
featureId: string;
|
||||||
projectPath?: string;
|
projectPath?: string;
|
||||||
hasEdits: boolean;
|
hasEdits: boolean;
|
||||||
|
planVersion?: number;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "plan_rejected";
|
type: "plan_rejected";
|
||||||
@@ -264,6 +266,14 @@ export type AutoModeEvent =
|
|||||||
projectPath?: string;
|
projectPath?: string;
|
||||||
feedback?: string;
|
feedback?: string;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: "plan_revision_requested";
|
||||||
|
featureId: string;
|
||||||
|
projectPath?: string;
|
||||||
|
feedback?: string;
|
||||||
|
hasEdits?: boolean;
|
||||||
|
planVersion?: number;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: "planning_started";
|
type: "planning_started";
|
||||||
featureId: string;
|
featureId: string;
|
||||||
|
|||||||
@@ -1671,7 +1671,8 @@ When done, summarize what you implemented and any notes for the developer.`;
|
|||||||
const planContent = responseText.substring(0, markerIndex).trim();
|
const planContent = responseText.substring(0, markerIndex).trim();
|
||||||
|
|
||||||
// Parse tasks from the generated spec (for spec and full modes)
|
// Parse tasks from the generated spec (for spec and full modes)
|
||||||
const parsedTasks = parseTasksFromSpec(planContent);
|
// Use let since we may need to update this after plan revision
|
||||||
|
let parsedTasks = parseTasksFromSpec(planContent);
|
||||||
const tasksTotal = parsedTasks.length;
|
const tasksTotal = parsedTasks.length;
|
||||||
|
|
||||||
console.log(`[AutoMode] Parsed ${tasksTotal} tasks from spec for feature ${featureId}`);
|
console.log(`[AutoMode] Parsed ${tasksTotal} tasks from spec for feature ${featureId}`);
|
||||||
@@ -1693,60 +1694,173 @@ When done, summarize what you implemented and any notes for the developer.`;
|
|||||||
|
|
||||||
let approvedPlanContent = planContent;
|
let approvedPlanContent = planContent;
|
||||||
let userFeedback: string | undefined;
|
let userFeedback: string | undefined;
|
||||||
|
let currentPlanContent = planContent;
|
||||||
|
let planVersion = 1;
|
||||||
|
|
||||||
// Only pause for approval if requirePlanApproval is true
|
// Only pause for approval if requirePlanApproval is true
|
||||||
if (requiresApproval) {
|
if (requiresApproval) {
|
||||||
console.log(`[AutoMode] Spec generated for feature ${featureId}, waiting for approval`);
|
// ========================================
|
||||||
|
// PLAN REVISION LOOP
|
||||||
|
// Keep regenerating plan until user approves
|
||||||
|
// ========================================
|
||||||
|
let planApproved = false;
|
||||||
|
|
||||||
// CRITICAL: Register pending approval BEFORE emitting event
|
while (!planApproved) {
|
||||||
// This prevents race condition where frontend tries to approve before
|
console.log(`[AutoMode] Spec v${planVersion} generated for feature ${featureId}, waiting for approval`);
|
||||||
// the approval is registered in pendingApprovals Map
|
|
||||||
const approvalPromise = this.waitForPlanApproval(featureId, projectPath);
|
|
||||||
|
|
||||||
// NOW emit plan_approval_required event (approval is already registered)
|
// CRITICAL: Register pending approval BEFORE emitting event
|
||||||
this.emitAutoModeEvent('plan_approval_required', {
|
const approvalPromise = this.waitForPlanApproval(featureId, projectPath);
|
||||||
featureId,
|
|
||||||
projectPath,
|
|
||||||
planContent,
|
|
||||||
planningMode,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for user approval
|
// Emit plan_approval_required event
|
||||||
try {
|
this.emitAutoModeEvent('plan_approval_required', {
|
||||||
const approvalResult = await approvalPromise;
|
|
||||||
|
|
||||||
if (!approvalResult.approved) {
|
|
||||||
// User rejected the plan - abort feature execution
|
|
||||||
console.log(`[AutoMode] Plan rejected for feature ${featureId}`);
|
|
||||||
throw new Error('Plan rejected by user');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[AutoMode] Plan approved for feature ${featureId}, continuing with implementation`);
|
|
||||||
|
|
||||||
// If user provided an edited plan, update the planSpec content
|
|
||||||
if (approvalResult.editedPlan) {
|
|
||||||
approvedPlanContent = approvalResult.editedPlan;
|
|
||||||
await this.updateFeaturePlanSpec(projectPath, featureId, {
|
|
||||||
content: approvalResult.editedPlan,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Capture user feedback if provided
|
|
||||||
userFeedback = approvalResult.feedback;
|
|
||||||
|
|
||||||
// Emit event to notify implementation is starting
|
|
||||||
this.emitAutoModeEvent('plan_approved', {
|
|
||||||
featureId,
|
featureId,
|
||||||
projectPath,
|
projectPath,
|
||||||
hasEdits: !!approvalResult.editedPlan,
|
planContent: currentPlanContent,
|
||||||
|
planningMode,
|
||||||
|
planVersion,
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
// Wait for user response
|
||||||
if ((error as Error).message.includes('cancelled')) {
|
try {
|
||||||
throw error; // Re-throw cancellation errors
|
const approvalResult = await approvalPromise;
|
||||||
|
|
||||||
|
if (approvalResult.approved) {
|
||||||
|
// User approved the plan
|
||||||
|
console.log(`[AutoMode] Plan v${planVersion} approved for feature ${featureId}`);
|
||||||
|
planApproved = true;
|
||||||
|
|
||||||
|
// If user provided edits, use the edited version
|
||||||
|
if (approvalResult.editedPlan) {
|
||||||
|
approvedPlanContent = approvalResult.editedPlan;
|
||||||
|
await this.updateFeaturePlanSpec(projectPath, featureId, {
|
||||||
|
content: approvalResult.editedPlan,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
approvedPlanContent = currentPlanContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture any additional feedback for implementation
|
||||||
|
userFeedback = approvalResult.feedback;
|
||||||
|
|
||||||
|
// Emit approval event
|
||||||
|
this.emitAutoModeEvent('plan_approved', {
|
||||||
|
featureId,
|
||||||
|
projectPath,
|
||||||
|
hasEdits: !!approvalResult.editedPlan,
|
||||||
|
planVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// User rejected - check if they provided feedback for revision
|
||||||
|
const hasFeedback = approvalResult.feedback && approvalResult.feedback.trim().length > 0;
|
||||||
|
const hasEdits = approvalResult.editedPlan && approvalResult.editedPlan.trim().length > 0;
|
||||||
|
|
||||||
|
if (!hasFeedback && !hasEdits) {
|
||||||
|
// No feedback or edits = explicit cancel
|
||||||
|
console.log(`[AutoMode] Plan rejected without feedback for feature ${featureId}, cancelling`);
|
||||||
|
throw new Error('Plan cancelled by user');
|
||||||
|
}
|
||||||
|
|
||||||
|
// User wants revisions - regenerate the plan
|
||||||
|
console.log(`[AutoMode] Plan v${planVersion} rejected with feedback for feature ${featureId}, regenerating...`);
|
||||||
|
planVersion++;
|
||||||
|
|
||||||
|
// Emit revision event
|
||||||
|
this.emitAutoModeEvent('plan_revision_requested', {
|
||||||
|
featureId,
|
||||||
|
projectPath,
|
||||||
|
feedback: approvalResult.feedback,
|
||||||
|
hasEdits: !!hasEdits,
|
||||||
|
planVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build revision prompt
|
||||||
|
let revisionPrompt = `The user has requested revisions to the plan/specification.
|
||||||
|
|
||||||
|
## Previous Plan (v${planVersion - 1})
|
||||||
|
${hasEdits ? approvalResult.editedPlan : currentPlanContent}
|
||||||
|
|
||||||
|
## User Feedback
|
||||||
|
${approvalResult.feedback || 'Please revise the plan based on the edits above.'}
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
Please regenerate the specification incorporating the user's feedback.
|
||||||
|
Keep the same format with the \`\`\`tasks block for task definitions.
|
||||||
|
After generating the revised spec, output:
|
||||||
|
"[SPEC_GENERATED] Please review the revised specification above."
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Update status to regenerating
|
||||||
|
await this.updateFeaturePlanSpec(projectPath, featureId, {
|
||||||
|
status: 'generating',
|
||||||
|
version: planVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make revision call
|
||||||
|
const revisionStream = provider.executeQuery({
|
||||||
|
prompt: revisionPrompt,
|
||||||
|
model: finalModel,
|
||||||
|
maxTurns: maxTurns || 100,
|
||||||
|
cwd: workDir,
|
||||||
|
allowedTools: allowedTools,
|
||||||
|
abortController,
|
||||||
|
});
|
||||||
|
|
||||||
|
let revisionText = "";
|
||||||
|
for await (const msg of revisionStream) {
|
||||||
|
if (msg.type === "assistant" && msg.message?.content) {
|
||||||
|
for (const block of msg.message.content) {
|
||||||
|
if (block.type === "text") {
|
||||||
|
revisionText += block.text || "";
|
||||||
|
this.emitAutoModeEvent("auto_mode_progress", {
|
||||||
|
featureId,
|
||||||
|
content: block.text,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (msg.type === "error") {
|
||||||
|
throw new Error(msg.error || "Error during plan revision");
|
||||||
|
} else if (msg.type === "result" && msg.subtype === "success") {
|
||||||
|
revisionText += msg.result || "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract new plan content
|
||||||
|
const markerIndex = revisionText.indexOf('[SPEC_GENERATED]');
|
||||||
|
if (markerIndex > 0) {
|
||||||
|
currentPlanContent = revisionText.substring(0, markerIndex).trim();
|
||||||
|
} else {
|
||||||
|
currentPlanContent = revisionText.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-parse tasks from revised plan
|
||||||
|
const revisedTasks = parseTasksFromSpec(currentPlanContent);
|
||||||
|
console.log(`[AutoMode] Revised plan has ${revisedTasks.length} tasks`);
|
||||||
|
|
||||||
|
// Update planSpec with revised content
|
||||||
|
await this.updateFeaturePlanSpec(projectPath, featureId, {
|
||||||
|
status: 'generated',
|
||||||
|
content: currentPlanContent,
|
||||||
|
version: planVersion,
|
||||||
|
tasks: revisedTasks,
|
||||||
|
tasksTotal: revisedTasks.length,
|
||||||
|
tasksCompleted: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update parsedTasks for implementation
|
||||||
|
parsedTasks = revisedTasks;
|
||||||
|
|
||||||
|
responseText += revisionText;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as Error).message.includes('cancelled')) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new Error(`Plan approval failed: ${(error as Error).message}`);
|
||||||
}
|
}
|
||||||
throw new Error(`Plan approval failed: ${(error as Error).message}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Auto-approve: requirePlanApproval is false, just continue without pausing
|
// Auto-approve: requirePlanApproval is false, just continue without pausing
|
||||||
console.log(`[AutoMode] Spec generated for feature ${featureId}, auto-approving (requirePlanApproval=false)`);
|
console.log(`[AutoMode] Spec generated for feature ${featureId}, auto-approving (requirePlanApproval=false)`);
|
||||||
@@ -1758,6 +1872,8 @@ When done, summarize what you implemented and any notes for the developer.`;
|
|||||||
planContent,
|
planContent,
|
||||||
planningMode,
|
planningMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
approvedPlanContent = planContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CRITICAL: After approval, we need to make a second call to continue implementation
|
// CRITICAL: After approval, we need to make a second call to continue implementation
|
||||||
@@ -1771,142 +1887,163 @@ When done, summarize what you implemented and any notes for the developer.`;
|
|||||||
reviewedByUser: requiresApproval,
|
reviewedByUser: requiresApproval,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build continuation prompt
|
// ========================================
|
||||||
let continuationPrompt = `The plan/specification has been approved. `;
|
// MULTI-AGENT TASK EXECUTION
|
||||||
if (userFeedback) {
|
// Each task gets its own focused agent call
|
||||||
continuationPrompt += `\n\nUser feedback: ${userFeedback}\n\n`;
|
// ========================================
|
||||||
}
|
|
||||||
continuationPrompt += `Now proceed with the implementation as specified in the plan:\n\n${approvedPlanContent}\n\nImplement the feature now.`;
|
|
||||||
|
|
||||||
// Make continuation call
|
if (parsedTasks.length > 0) {
|
||||||
const continuationStream = provider.executeQuery({
|
console.log(`[AutoMode] Starting multi-agent execution: ${parsedTasks.length} tasks for feature ${featureId}`);
|
||||||
prompt: continuationPrompt,
|
|
||||||
model: finalModel,
|
|
||||||
maxTurns: maxTurns,
|
|
||||||
cwd: workDir,
|
|
||||||
allowedTools: allowedTools,
|
|
||||||
abortController,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Track task progress for events
|
// Execute each task with a separate agent
|
||||||
let startedTaskIds: string[] = [];
|
for (let taskIndex = 0; taskIndex < parsedTasks.length; taskIndex++) {
|
||||||
let completedTaskIds: string[] = [];
|
const task = parsedTasks[taskIndex];
|
||||||
let currentPhaseNum = 1;
|
|
||||||
let currentTaskId: string | null = null;
|
|
||||||
|
|
||||||
// Process continuation stream with task progress tracking
|
// Check for abort
|
||||||
for await (const contMsg of continuationStream) {
|
if (abortController.signal.aborted) {
|
||||||
if (contMsg.type === "assistant" && contMsg.message?.content) {
|
throw new Error('Feature execution aborted');
|
||||||
for (const contBlock of contMsg.message.content) {
|
}
|
||||||
if (contBlock.type === "text") {
|
|
||||||
responseText += contBlock.text || "";
|
|
||||||
|
|
||||||
// Check for [TASK_START] markers - detect when task begins
|
// Emit task started
|
||||||
const taskStartMatches = responseText.match(/\[TASK_START\]\s*(T\d{3})/g);
|
console.log(`[AutoMode] Starting task ${task.id}: ${task.description}`);
|
||||||
if (taskStartMatches) {
|
this.emitAutoModeEvent("auto_mode_task_started", {
|
||||||
for (const match of taskStartMatches) {
|
featureId,
|
||||||
const taskIdMatch = match.match(/T\d{3}/);
|
projectPath,
|
||||||
if (taskIdMatch && !startedTaskIds.includes(taskIdMatch[0])) {
|
taskId: task.id,
|
||||||
const taskId = taskIdMatch[0];
|
taskDescription: task.description,
|
||||||
startedTaskIds.push(taskId);
|
taskIndex,
|
||||||
currentTaskId = taskId;
|
tasksTotal: parsedTasks.length,
|
||||||
|
});
|
||||||
|
|
||||||
// Find task details from parsed tasks
|
// Update planSpec with current task
|
||||||
const taskInfo = parsedTasks.find(t => t.id === taskId);
|
await this.updateFeaturePlanSpec(projectPath, featureId, {
|
||||||
const taskDescription = taskInfo?.description || 'Working on task';
|
currentTaskId: task.id,
|
||||||
|
});
|
||||||
|
|
||||||
console.log(`[AutoMode] Task ${taskId} started for feature ${featureId}: ${taskDescription}`);
|
// Build focused prompt for this specific task
|
||||||
|
const taskPrompt = this.buildTaskPrompt(task, parsedTasks, taskIndex, approvedPlanContent, userFeedback);
|
||||||
|
|
||||||
// Emit task started event
|
// Execute task with dedicated agent
|
||||||
this.emitAutoModeEvent("auto_mode_task_started", {
|
const taskStream = provider.executeQuery({
|
||||||
featureId,
|
prompt: taskPrompt,
|
||||||
projectPath,
|
model: finalModel,
|
||||||
taskId,
|
maxTurns: Math.min(maxTurns || 100, 50), // Limit turns per task
|
||||||
taskDescription,
|
cwd: workDir,
|
||||||
taskIndex: startedTaskIds.length - 1,
|
allowedTools: allowedTools,
|
||||||
tasksTotal: parsedTasks.length,
|
abortController,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update planSpec with current task
|
let taskOutput = "";
|
||||||
await this.updateFeaturePlanSpec(projectPath, featureId, {
|
|
||||||
currentTaskId: taskId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for [TASK_COMPLETE] markers
|
// Process task stream
|
||||||
const taskCompleteMatches = responseText.match(/\[TASK_COMPLETE\]\s*(T\d{3})/g);
|
for await (const msg of taskStream) {
|
||||||
if (taskCompleteMatches) {
|
if (msg.type === "assistant" && msg.message?.content) {
|
||||||
for (const match of taskCompleteMatches) {
|
for (const block of msg.message.content) {
|
||||||
const taskIdMatch = match.match(/T\d{3}/);
|
if (block.type === "text") {
|
||||||
if (taskIdMatch && !completedTaskIds.includes(taskIdMatch[0])) {
|
taskOutput += block.text || "";
|
||||||
const taskId = taskIdMatch[0];
|
responseText += block.text || "";
|
||||||
completedTaskIds.push(taskId);
|
this.emitAutoModeEvent("auto_mode_progress", {
|
||||||
|
|
||||||
console.log(`[AutoMode] Task ${taskId} completed for feature ${featureId}`);
|
|
||||||
|
|
||||||
// Emit task completion event
|
|
||||||
this.emitAutoModeEvent("auto_mode_task_complete", {
|
|
||||||
featureId,
|
|
||||||
projectPath,
|
|
||||||
taskId,
|
|
||||||
tasksCompleted: completedTaskIds.length,
|
|
||||||
tasksTotal: parsedTasks.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update planSpec with task progress
|
|
||||||
await this.updateFeaturePlanSpec(projectPath, featureId, {
|
|
||||||
tasksCompleted: completedTaskIds.length,
|
|
||||||
currentTaskId: taskId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for [PHASE_COMPLETE] markers (for full mode)
|
|
||||||
const phaseCompleteMatch = contBlock.text?.match(/\[PHASE_COMPLETE\]\s*Phase\s*(\d+)/i);
|
|
||||||
if (phaseCompleteMatch) {
|
|
||||||
const phaseNum = parseInt(phaseCompleteMatch[1], 10);
|
|
||||||
if (phaseNum > currentPhaseNum) {
|
|
||||||
currentPhaseNum = phaseNum;
|
|
||||||
console.log(`[AutoMode] Phase ${phaseNum} completed for feature ${featureId}`);
|
|
||||||
|
|
||||||
this.emitAutoModeEvent("auto_mode_phase_complete", {
|
|
||||||
featureId,
|
featureId,
|
||||||
projectPath,
|
content: block.text,
|
||||||
phaseNumber: phaseNum,
|
});
|
||||||
|
} else if (block.type === "tool_use") {
|
||||||
|
this.emitAutoModeEvent("auto_mode_tool", {
|
||||||
|
featureId,
|
||||||
|
tool: block.name,
|
||||||
|
input: block.input,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (msg.type === "error") {
|
||||||
this.emitAutoModeEvent("auto_mode_progress", {
|
throw new Error(msg.error || `Error during task ${task.id}`);
|
||||||
featureId,
|
} else if (msg.type === "result" && msg.subtype === "success") {
|
||||||
content: contBlock.text,
|
taskOutput += msg.result || "";
|
||||||
});
|
responseText += msg.result || "";
|
||||||
} else if (contBlock.type === "tool_use") {
|
|
||||||
this.emitAutoModeEvent("auto_mode_tool", {
|
|
||||||
featureId,
|
|
||||||
tool: contBlock.name,
|
|
||||||
input: contBlock.input,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (contMsg.type === "error") {
|
|
||||||
throw new Error(contMsg.error || "Unknown error during implementation");
|
// Emit task completed
|
||||||
} else if (contMsg.type === "result" && contMsg.subtype === "success") {
|
console.log(`[AutoMode] Task ${task.id} completed for feature ${featureId}`);
|
||||||
responseText += contMsg.result || "";
|
this.emitAutoModeEvent("auto_mode_task_complete", {
|
||||||
|
featureId,
|
||||||
|
projectPath,
|
||||||
|
taskId: task.id,
|
||||||
|
tasksCompleted: taskIndex + 1,
|
||||||
|
tasksTotal: parsedTasks.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update planSpec with progress
|
||||||
|
await this.updateFeaturePlanSpec(projectPath, featureId, {
|
||||||
|
tasksCompleted: taskIndex + 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for phase completion (group tasks by phase)
|
||||||
|
if (task.phase) {
|
||||||
|
const nextTask = parsedTasks[taskIndex + 1];
|
||||||
|
if (!nextTask || nextTask.phase !== task.phase) {
|
||||||
|
// Phase changed, emit phase complete
|
||||||
|
const phaseMatch = task.phase.match(/Phase\s*(\d+)/i);
|
||||||
|
if (phaseMatch) {
|
||||||
|
this.emitAutoModeEvent("auto_mode_phase_complete", {
|
||||||
|
featureId,
|
||||||
|
projectPath,
|
||||||
|
phaseNumber: parseInt(phaseMatch[1], 10),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[AutoMode] All ${parsedTasks.length} tasks completed for feature ${featureId}`);
|
||||||
|
} else {
|
||||||
|
// No parsed tasks - fall back to single-agent execution
|
||||||
|
console.log(`[AutoMode] No parsed tasks, using single-agent execution for feature ${featureId}`);
|
||||||
|
|
||||||
|
const continuationPrompt = `The plan/specification has been approved. Now implement it.
|
||||||
|
${userFeedback ? `\n## User Feedback\n${userFeedback}\n` : ''}
|
||||||
|
## Approved Plan
|
||||||
|
|
||||||
|
${approvedPlanContent}
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
Implement all the changes described in the plan above.`;
|
||||||
|
|
||||||
|
const continuationStream = provider.executeQuery({
|
||||||
|
prompt: continuationPrompt,
|
||||||
|
model: finalModel,
|
||||||
|
maxTurns: maxTurns,
|
||||||
|
cwd: workDir,
|
||||||
|
allowedTools: allowedTools,
|
||||||
|
abortController,
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const msg of continuationStream) {
|
||||||
|
if (msg.type === "assistant" && msg.message?.content) {
|
||||||
|
for (const block of msg.message.content) {
|
||||||
|
if (block.type === "text") {
|
||||||
|
responseText += block.text || "";
|
||||||
|
this.emitAutoModeEvent("auto_mode_progress", {
|
||||||
|
featureId,
|
||||||
|
content: block.text,
|
||||||
|
});
|
||||||
|
} else if (block.type === "tool_use") {
|
||||||
|
this.emitAutoModeEvent("auto_mode_tool", {
|
||||||
|
featureId,
|
||||||
|
tool: block.name,
|
||||||
|
input: block.input,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (msg.type === "error") {
|
||||||
|
throw new Error(msg.error || "Unknown error during implementation");
|
||||||
|
} else if (msg.type === "result" && msg.subtype === "success") {
|
||||||
|
responseText += msg.result || "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark all tasks as completed when implementation finishes
|
console.log(`[AutoMode] Implementation completed for feature ${featureId}`);
|
||||||
if (parsedTasks.length > 0) {
|
|
||||||
await this.updateFeaturePlanSpec(projectPath, featureId, {
|
|
||||||
tasksCompleted: parsedTasks.length,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[AutoMode] Implementation completed for feature ${featureId} (${completedTaskIds.length}/${parsedTasks.length} tasks tracked)`);
|
|
||||||
// Exit the original stream loop since continuation is done
|
// Exit the original stream loop since continuation is done
|
||||||
break streamLoop;
|
break streamLoop;
|
||||||
}
|
}
|
||||||
@@ -1969,6 +2106,78 @@ Review the previous work and continue the implementation. If the feature appears
|
|||||||
return this.executeFeature(projectPath, featureId, useWorktrees, false, prompt);
|
return this.executeFeature(projectPath, featureId, useWorktrees, false, prompt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a focused prompt for executing a single task.
|
||||||
|
* Each task gets minimal context to keep the agent focused.
|
||||||
|
*/
|
||||||
|
private buildTaskPrompt(
|
||||||
|
task: ParsedTask,
|
||||||
|
allTasks: ParsedTask[],
|
||||||
|
taskIndex: number,
|
||||||
|
planContent: string,
|
||||||
|
userFeedback?: string
|
||||||
|
): string {
|
||||||
|
const completedTasks = allTasks.slice(0, taskIndex);
|
||||||
|
const remainingTasks = allTasks.slice(taskIndex + 1);
|
||||||
|
|
||||||
|
let prompt = `# Task Execution: ${task.id}
|
||||||
|
|
||||||
|
You are executing a specific task as part of a larger feature implementation.
|
||||||
|
|
||||||
|
## Your Current Task
|
||||||
|
|
||||||
|
**Task ID:** ${task.id}
|
||||||
|
**Description:** ${task.description}
|
||||||
|
${task.filePath ? `**Primary File:** ${task.filePath}` : ''}
|
||||||
|
${task.phase ? `**Phase:** ${task.phase}` : ''}
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Show what's already done
|
||||||
|
if (completedTasks.length > 0) {
|
||||||
|
prompt += `### Already Completed (${completedTasks.length} tasks)
|
||||||
|
${completedTasks.map(t => `- [x] ${t.id}: ${t.description}`).join('\n')}
|
||||||
|
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show remaining tasks
|
||||||
|
if (remainingTasks.length > 0) {
|
||||||
|
prompt += `### Coming Up Next (${remainingTasks.length} tasks remaining)
|
||||||
|
${remainingTasks.slice(0, 3).map(t => `- [ ] ${t.id}: ${t.description}`).join('\n')}
|
||||||
|
${remainingTasks.length > 3 ? `... and ${remainingTasks.length - 3} more tasks` : ''}
|
||||||
|
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add user feedback if any
|
||||||
|
if (userFeedback) {
|
||||||
|
prompt += `### User Feedback
|
||||||
|
${userFeedback}
|
||||||
|
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add relevant excerpt from plan (just the task-related part to save context)
|
||||||
|
prompt += `### Reference: Full Plan
|
||||||
|
<details>
|
||||||
|
${planContent}
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
1. Focus ONLY on completing task ${task.id}: "${task.description}"
|
||||||
|
2. Do not work on other tasks
|
||||||
|
3. Use the existing codebase patterns
|
||||||
|
4. When done, summarize what you implemented
|
||||||
|
|
||||||
|
Begin implementing task ${task.id} now.`;
|
||||||
|
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emit an auto-mode event wrapped in the correct format for the client.
|
* Emit an auto-mode event wrapped in the correct format for the client.
|
||||||
* All auto-mode events are sent as type "auto-mode:event" with the actual
|
* All auto-mode events are sent as type "auto-mode:event" with the actual
|
||||||
|
|||||||
Reference in New Issue
Block a user