mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
FINSIHED
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckCircle2, Circle, Loader2, ChevronDown, ChevronRight } from "lucide-react";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
@@ -16,15 +16,65 @@ interface TaskInfo {
|
||||
|
||||
interface TaskProgressPanelProps {
|
||||
featureId: string;
|
||||
projectPath?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function TaskProgressPanel({ featureId, className }: TaskProgressPanelProps) {
|
||||
export function TaskProgressPanel({ featureId, projectPath, className }: TaskProgressPanelProps) {
|
||||
const [tasks, setTasks] = useState<TaskInfo[]>([]);
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
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(() => {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode) return;
|
||||
@@ -69,24 +119,25 @@ export function TaskProgressPanel({ featureId, className }: TaskProgressPanelPro
|
||||
t.id === taskEvent.taskId ? { ...t, status: "completed" as const } : t
|
||||
)
|
||||
);
|
||||
// Clear current task if it was completed
|
||||
if (currentTaskId === taskEvent.taskId) {
|
||||
setCurrentTaskId(null);
|
||||
}
|
||||
setCurrentTaskId(null);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [featureId, currentTaskId]);
|
||||
}, [featureId]);
|
||||
|
||||
// Calculate progress
|
||||
const completedCount = tasks.filter((t) => t.status === "completed").length;
|
||||
const totalCount = tasks.length;
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -218,6 +218,13 @@ export function AgentOutputModal({
|
||||
// Show when plan is auto-approved
|
||||
newContent = `\n✅ Plan auto-approved - continuing to implementation...\n`;
|
||||
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":
|
||||
// Show when a task starts
|
||||
if ("taskId" in event && "taskDescription" in event) {
|
||||
@@ -362,7 +369,11 @@ export function AgentOutputModal({
|
||||
</DialogHeader>
|
||||
|
||||
{/* 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" ? (
|
||||
<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 { Label } from "@/components/ui/label";
|
||||
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 {
|
||||
open: boolean;
|
||||
@@ -143,18 +143,21 @@ export function PlanApprovalDialog({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Reject Feedback Section - Only show when not in viewOnly mode */}
|
||||
{/* Revision 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>
|
||||
<Label htmlFor="reject-feedback">What changes would you like?</Label>
|
||||
<Textarea
|
||||
id="reject-feedback"
|
||||
value={rejectFeedback}
|
||||
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]"
|
||||
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>
|
||||
@@ -171,30 +174,30 @@ export function PlanApprovalDialog({
|
||||
onClick={handleCancelReject}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Cancel
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
variant="secondary"
|
||||
onClick={handleReject}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<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
|
||||
variant="destructive"
|
||||
variant="outline"
|
||||
onClick={handleReject}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
Reject
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
Request Changes
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleApprove}
|
||||
|
||||
@@ -12,6 +12,15 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
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 {
|
||||
status: 'pending' | 'generating' | 'generated' | 'approved' | 'rejected';
|
||||
content?: string;
|
||||
@@ -21,6 +30,8 @@ export interface PlanSpec {
|
||||
reviewedByUser: boolean;
|
||||
tasksCompleted?: number;
|
||||
tasksTotal?: number;
|
||||
currentTaskId?: string; // ID of the task currently being worked on
|
||||
tasks?: ParsedTask[]; // Parsed tasks from the spec
|
||||
}
|
||||
|
||||
interface PlanningModeSelectorProps {
|
||||
|
||||
@@ -294,6 +294,20 @@ export function useAutoMode() {
|
||||
}
|
||||
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":
|
||||
// Task started - show which task is being worked on
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
export interface PlanSpec {
|
||||
status: 'pending' | 'generating' | 'generated' | 'approved' | 'rejected';
|
||||
@@ -315,6 +324,8 @@ export interface PlanSpec {
|
||||
reviewedByUser: boolean; // True if user has seen the spec
|
||||
tasksCompleted?: 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
|
||||
|
||||
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;
|
||||
planContent: string;
|
||||
planningMode: "lite" | "spec" | "full";
|
||||
planVersion?: number;
|
||||
}
|
||||
| {
|
||||
type: "plan_auto_approved";
|
||||
@@ -257,6 +258,7 @@ export type AutoModeEvent =
|
||||
featureId: string;
|
||||
projectPath?: string;
|
||||
hasEdits: boolean;
|
||||
planVersion?: number;
|
||||
}
|
||||
| {
|
||||
type: "plan_rejected";
|
||||
@@ -264,6 +266,14 @@ export type AutoModeEvent =
|
||||
projectPath?: string;
|
||||
feedback?: string;
|
||||
}
|
||||
| {
|
||||
type: "plan_revision_requested";
|
||||
featureId: string;
|
||||
projectPath?: string;
|
||||
feedback?: string;
|
||||
hasEdits?: boolean;
|
||||
planVersion?: number;
|
||||
}
|
||||
| {
|
||||
type: "planning_started";
|
||||
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();
|
||||
|
||||
// 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;
|
||||
|
||||
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 userFeedback: string | undefined;
|
||||
let currentPlanContent = planContent;
|
||||
let planVersion = 1;
|
||||
|
||||
// Only pause for approval if requirePlanApproval is true
|
||||
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
|
||||
// This prevents race condition where frontend tries to approve before
|
||||
// the approval is registered in pendingApprovals Map
|
||||
const approvalPromise = this.waitForPlanApproval(featureId, projectPath);
|
||||
while (!planApproved) {
|
||||
console.log(`[AutoMode] Spec v${planVersion} generated for feature ${featureId}, waiting for approval`);
|
||||
|
||||
// NOW emit plan_approval_required event (approval is already registered)
|
||||
this.emitAutoModeEvent('plan_approval_required', {
|
||||
featureId,
|
||||
projectPath,
|
||||
planContent,
|
||||
planningMode,
|
||||
});
|
||||
// CRITICAL: Register pending approval BEFORE emitting event
|
||||
const approvalPromise = this.waitForPlanApproval(featureId, projectPath);
|
||||
|
||||
// Wait for user approval
|
||||
try {
|
||||
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', {
|
||||
// Emit plan_approval_required event
|
||||
this.emitAutoModeEvent('plan_approval_required', {
|
||||
featureId,
|
||||
projectPath,
|
||||
hasEdits: !!approvalResult.editedPlan,
|
||||
planContent: currentPlanContent,
|
||||
planningMode,
|
||||
planVersion,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
if ((error as Error).message.includes('cancelled')) {
|
||||
throw error; // Re-throw cancellation errors
|
||||
// Wait for user response
|
||||
try {
|
||||
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 {
|
||||
// Auto-approve: requirePlanApproval is false, just continue without pausing
|
||||
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,
|
||||
planningMode,
|
||||
});
|
||||
|
||||
approvedPlanContent = planContent;
|
||||
}
|
||||
|
||||
// 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,
|
||||
});
|
||||
|
||||
// Build continuation prompt
|
||||
let continuationPrompt = `The plan/specification has been approved. `;
|
||||
if (userFeedback) {
|
||||
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.`;
|
||||
// ========================================
|
||||
// MULTI-AGENT TASK EXECUTION
|
||||
// Each task gets its own focused agent call
|
||||
// ========================================
|
||||
|
||||
// Make continuation call
|
||||
const continuationStream = provider.executeQuery({
|
||||
prompt: continuationPrompt,
|
||||
model: finalModel,
|
||||
maxTurns: maxTurns,
|
||||
cwd: workDir,
|
||||
allowedTools: allowedTools,
|
||||
abortController,
|
||||
});
|
||||
if (parsedTasks.length > 0) {
|
||||
console.log(`[AutoMode] Starting multi-agent execution: ${parsedTasks.length} tasks for feature ${featureId}`);
|
||||
|
||||
// Track task progress for events
|
||||
let startedTaskIds: string[] = [];
|
||||
let completedTaskIds: string[] = [];
|
||||
let currentPhaseNum = 1;
|
||||
let currentTaskId: string | null = null;
|
||||
// Execute each task with a separate agent
|
||||
for (let taskIndex = 0; taskIndex < parsedTasks.length; taskIndex++) {
|
||||
const task = parsedTasks[taskIndex];
|
||||
|
||||
// Process continuation stream with task progress tracking
|
||||
for await (const contMsg of continuationStream) {
|
||||
if (contMsg.type === "assistant" && contMsg.message?.content) {
|
||||
for (const contBlock of contMsg.message.content) {
|
||||
if (contBlock.type === "text") {
|
||||
responseText += contBlock.text || "";
|
||||
// Check for abort
|
||||
if (abortController.signal.aborted) {
|
||||
throw new Error('Feature execution aborted');
|
||||
}
|
||||
|
||||
// Check for [TASK_START] markers - detect when task begins
|
||||
const taskStartMatches = responseText.match(/\[TASK_START\]\s*(T\d{3})/g);
|
||||
if (taskStartMatches) {
|
||||
for (const match of taskStartMatches) {
|
||||
const taskIdMatch = match.match(/T\d{3}/);
|
||||
if (taskIdMatch && !startedTaskIds.includes(taskIdMatch[0])) {
|
||||
const taskId = taskIdMatch[0];
|
||||
startedTaskIds.push(taskId);
|
||||
currentTaskId = taskId;
|
||||
// Emit task started
|
||||
console.log(`[AutoMode] Starting task ${task.id}: ${task.description}`);
|
||||
this.emitAutoModeEvent("auto_mode_task_started", {
|
||||
featureId,
|
||||
projectPath,
|
||||
taskId: task.id,
|
||||
taskDescription: task.description,
|
||||
taskIndex,
|
||||
tasksTotal: parsedTasks.length,
|
||||
});
|
||||
|
||||
// Find task details from parsed tasks
|
||||
const taskInfo = parsedTasks.find(t => t.id === taskId);
|
||||
const taskDescription = taskInfo?.description || 'Working on task';
|
||||
// Update planSpec with current task
|
||||
await this.updateFeaturePlanSpec(projectPath, featureId, {
|
||||
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
|
||||
this.emitAutoModeEvent("auto_mode_task_started", {
|
||||
featureId,
|
||||
projectPath,
|
||||
taskId,
|
||||
taskDescription,
|
||||
taskIndex: startedTaskIds.length - 1,
|
||||
tasksTotal: parsedTasks.length,
|
||||
});
|
||||
// Execute task with dedicated agent
|
||||
const taskStream = provider.executeQuery({
|
||||
prompt: taskPrompt,
|
||||
model: finalModel,
|
||||
maxTurns: Math.min(maxTurns || 100, 50), // Limit turns per task
|
||||
cwd: workDir,
|
||||
allowedTools: allowedTools,
|
||||
abortController,
|
||||
});
|
||||
|
||||
// Update planSpec with current task
|
||||
await this.updateFeaturePlanSpec(projectPath, featureId, {
|
||||
currentTaskId: taskId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
let taskOutput = "";
|
||||
|
||||
// Check for [TASK_COMPLETE] markers
|
||||
const taskCompleteMatches = responseText.match(/\[TASK_COMPLETE\]\s*(T\d{3})/g);
|
||||
if (taskCompleteMatches) {
|
||||
for (const match of taskCompleteMatches) {
|
||||
const taskIdMatch = match.match(/T\d{3}/);
|
||||
if (taskIdMatch && !completedTaskIds.includes(taskIdMatch[0])) {
|
||||
const taskId = taskIdMatch[0];
|
||||
completedTaskIds.push(taskId);
|
||||
|
||||
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", {
|
||||
// Process task stream
|
||||
for await (const msg of taskStream) {
|
||||
if (msg.type === "assistant" && msg.message?.content) {
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === "text") {
|
||||
taskOutput += block.text || "";
|
||||
responseText += block.text || "";
|
||||
this.emitAutoModeEvent("auto_mode_progress", {
|
||||
featureId,
|
||||
projectPath,
|
||||
phaseNumber: phaseNum,
|
||||
content: block.text,
|
||||
});
|
||||
} else if (block.type === "tool_use") {
|
||||
this.emitAutoModeEvent("auto_mode_tool", {
|
||||
featureId,
|
||||
tool: block.name,
|
||||
input: block.input,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.emitAutoModeEvent("auto_mode_progress", {
|
||||
featureId,
|
||||
content: contBlock.text,
|
||||
});
|
||||
} else if (contBlock.type === "tool_use") {
|
||||
this.emitAutoModeEvent("auto_mode_tool", {
|
||||
featureId,
|
||||
tool: contBlock.name,
|
||||
input: contBlock.input,
|
||||
});
|
||||
} else if (msg.type === "error") {
|
||||
throw new Error(msg.error || `Error during task ${task.id}`);
|
||||
} else if (msg.type === "result" && msg.subtype === "success") {
|
||||
taskOutput += msg.result || "";
|
||||
responseText += msg.result || "";
|
||||
}
|
||||
}
|
||||
} else if (contMsg.type === "error") {
|
||||
throw new Error(contMsg.error || "Unknown error during implementation");
|
||||
} else if (contMsg.type === "result" && contMsg.subtype === "success") {
|
||||
responseText += contMsg.result || "";
|
||||
|
||||
// Emit task completed
|
||||
console.log(`[AutoMode] Task ${task.id} completed for feature ${featureId}`);
|
||||
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
|
||||
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)`);
|
||||
console.log(`[AutoMode] Implementation completed for feature ${featureId}`);
|
||||
// Exit the original stream loop since continuation is done
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* All auto-mode events are sent as type "auto-mode:event" with the actual
|
||||
|
||||
Reference in New Issue
Block a user