diff --git a/apps/app/src/components/views/board-view/shared/planning-mode-selector.tsx b/apps/app/src/components/views/board-view/shared/planning-mode-selector.tsx index d4d14a5e..98ce3855 100644 --- a/apps/app/src/components/views/board-view/shared/planning-mode-selector.tsx +++ b/apps/app/src/components/views/board-view/shared/planning-mode-selector.tsx @@ -9,30 +9,12 @@ import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { cn } from "@/lib/utils"; +import type { PlanSpec } from "@/store/app-store"; 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; - version: number; - generatedAt?: string; - approvedAt?: string; - reviewedByUser: boolean; - tasksCompleted?: number; - tasksTotal?: number; - currentTaskId?: string; // ID of the task currently being worked on - tasks?: ParsedTask[]; // Parsed tasks from the spec -} +// Re-export for backwards compatibility +export type { ParsedTask, PlanSpec } from "@/store/app-store"; interface PlanningModeSelectorProps { mode: PlanningMode; diff --git a/apps/app/src/hooks/use-auto-mode.ts b/apps/app/src/hooks/use-auto-mode.ts index 9106ee4f..ef838bca 100644 --- a/apps/app/src/hooks/use-auto-mode.ts +++ b/apps/app/src/hooks/use-auto-mode.ts @@ -156,12 +156,7 @@ export function useAutoMode() { case "auto_mode_error": if (event.featureId && event.error) { // Check if this is a user-initiated cancellation (not a real error) - const isCancellation = - event.error.includes("cancelled") || - event.error.includes("stopped") || - event.error.includes("aborted"); - - if (isCancellation) { + if (event.errorType === "cancellation") { // User cancelled the feature - just log as info, not an error console.log("[AutoMode] Feature cancelled:", event.error); // Remove from running tasks diff --git a/apps/app/src/types/electron.d.ts b/apps/app/src/types/electron.d.ts index 6044b5f0..99905bf2 100644 --- a/apps/app/src/types/electron.d.ts +++ b/apps/app/src/types/electron.d.ts @@ -193,7 +193,7 @@ export type AutoModeEvent = | { type: "auto_mode_error"; error: string; - errorType?: "authentication" | "execution"; + errorType?: "authentication" | "cancellation" | "execution"; featureId?: string; projectId?: string; projectPath?: string; diff --git a/apps/server/src/lib/error-handler.ts b/apps/server/src/lib/error-handler.ts index b1e0c3ac..8f6dbc00 100644 --- a/apps/server/src/lib/error-handler.ts +++ b/apps/server/src/lib/error-handler.ts @@ -21,6 +21,22 @@ export function isAbortError(error: unknown): boolean { ); } +/** + * Check if an error is a user-initiated cancellation + * + * @param errorMessage - The error message to check + * @returns True if the error is a user-initiated cancellation + */ +export function isCancellationError(errorMessage: string): boolean { + const lowerMessage = errorMessage.toLowerCase(); + return ( + lowerMessage.includes("cancelled") || + lowerMessage.includes("canceled") || + lowerMessage.includes("stopped") || + lowerMessage.includes("aborted") + ); +} + /** * Check if an error is an authentication/API key error * @@ -39,7 +55,7 @@ export function isAuthenticationError(errorMessage: string): boolean { /** * Error type classification */ -export type ErrorType = "authentication" | "abort" | "execution" | "unknown"; +export type ErrorType = "authentication" | "cancellation" | "abort" | "execution" | "unknown"; /** * Classified error information @@ -49,6 +65,7 @@ export interface ErrorInfo { message: string; isAbort: boolean; isAuth: boolean; + isCancellation: boolean; originalError: unknown; } @@ -62,12 +79,13 @@ export function classifyError(error: unknown): ErrorInfo { const message = error instanceof Error ? error.message : String(error || "Unknown error"); const isAbort = isAbortError(error); const isAuth = isAuthenticationError(message); + const isCancellation = isCancellationError(message); let type: ErrorType; if (isAuth) { type = "authentication"; - } else if (isAbort) { - type = "abort"; + } else if (isCancellation || isAbort) { + type = "cancellation"; } else if (error instanceof Error) { type = "execution"; } else { @@ -79,6 +97,7 @@ export function classifyError(error: unknown): ErrorInfo { message, isAbort, isAuth, + isCancellation, originalError: error, }; } diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index 7f47b1ba..721d69da 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -19,7 +19,7 @@ import type { EventEmitter } from "../lib/events.js"; import { buildPromptWithImages } from "../lib/prompt-builder.js"; import { resolveModelString, DEFAULT_MODELS } from "../lib/model-resolver.js"; import { createAutoModeOptions } from "../lib/sdk-options.js"; -import { isAbortError, classifyError } from "../lib/error-handler.js"; +import { classifyError } from "../lib/error-handler.js"; import { resolveDependencies, areDependenciesSatisfied } from "../lib/dependency-resolver.js"; import type { Feature } from "./feature-loader.js"; import { @@ -362,8 +362,10 @@ export class AutoModeService { // Run the loop in the background this.runAutoLoop().catch((error) => { console.error("[AutoMode] Loop error:", error); + const errorInfo = classifyError(error); this.emitAutoModeEvent("auto_mode_error", { - error: error.message, + error: errorInfo.message, + errorType: errorInfo.type, }); }); } @@ -457,7 +459,10 @@ export class AutoModeService { featureId: string, useWorktrees = false, isAutoMode = false, - providedWorktreePath?: string + providedWorktreePath?: string, + options?: { + continuationPrompt?: string; + } ): Promise { if (this.runningFeatures.has(featureId)) { throw new Error(`Feature ${featureId} is already running`); @@ -531,18 +536,27 @@ export class AutoModeService { // Update feature status to in_progress await this.updateFeatureStatus(projectPath, featureId, "in_progress"); - // Build the prompt with planning phase - const featurePrompt = this.buildFeaturePrompt(feature); - const planningPrefix = this.getPlanningPromptPrefix(feature); - const prompt = planningPrefix + featurePrompt; + // Build the prompt - use continuation prompt if provided (for recovery after plan approval) + let prompt: string; + if (options?.continuationPrompt) { + // Continuation prompt is used when recovering from a plan approval + // The plan was already approved, so skip the planning phase + prompt = options.continuationPrompt; + console.log(`[AutoMode] Using continuation prompt for feature ${featureId}`); + } else { + // Normal flow: build prompt with planning phase + const featurePrompt = this.buildFeaturePrompt(feature); + const planningPrefix = this.getPlanningPromptPrefix(feature); + prompt = planningPrefix + featurePrompt; - // Emit planning mode info - if (feature.planningMode && feature.planningMode !== 'skip') { - this.emitAutoModeEvent('planning_started', { - featureId: feature.id, - mode: feature.planningMode, - message: `Starting ${feature.planningMode} planning phase` - }); + // Emit planning mode info + if (feature.planningMode && feature.planningMode !== 'skip') { + this.emitAutoModeEvent('planning_started', { + featureId: feature.id, + mode: feature.planningMode, + message: `Starting ${feature.planningMode} planning phase` + }); + } } // Extract image paths from feature @@ -602,7 +616,7 @@ export class AutoModeService { this.emitAutoModeEvent("auto_mode_error", { featureId, error: errorInfo.message, - errorType: errorInfo.isAuth ? "authentication" : "execution", + errorType: errorInfo.type, projectPath, }); } @@ -862,10 +876,12 @@ Address the follow-up instructions above. Review the previous work and make the projectPath, }); } catch (error) { - if (!isAbortError(error)) { + const errorInfo = classifyError(error); + if (!errorInfo.isCancellation) { this.emitAutoModeEvent("auto_mode_error", { featureId, - error: (error as Error).message, + error: errorInfo.message, + errorType: errorInfo.type, projectPath, }); } @@ -1109,9 +1125,11 @@ Format your response as a structured markdown document.`; projectPath, }); } catch (error) { + const errorInfo = classifyError(error); this.emitAutoModeEvent("auto_mode_error", { featureId: analysisFeatureId, - error: (error as Error).message, + error: errorInfo.message, + errorType: errorInfo.type, projectPath, }); } @@ -1219,7 +1237,10 @@ Format your response as a structured markdown document.`; console.log(`[AutoMode] Starting recovery execution for feature ${featureId}`); // Start feature execution with the continuation prompt (async, don't await) - this.executeFeature(projectPathFromClient, featureId, true, false, continuationPrompt) + // Pass undefined for providedWorktreePath, use options for continuation prompt + this.executeFeature(projectPathFromClient, featureId, true, false, undefined, { + continuationPrompt, + }) .catch((error) => { console.error(`[AutoMode] Recovery execution failed for feature ${featureId}:`, error); }); @@ -2306,7 +2327,9 @@ ${context} ## Instructions Review the previous work and continue the implementation. If the feature appears complete, verify it works correctly.`; - return this.executeFeature(projectPath, featureId, useWorktrees, false, prompt); + return this.executeFeature(projectPath, featureId, useWorktrees, false, undefined, { + continuationPrompt: prompt, + }); } /**