import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; import { recordContextMetric } from "../../context-metrics/utils.ts"; import { buildPlanExecutionDecision, planModeFollowUpDeliveryOptions, type PlanReviewChoice, } from "../plan-review.ts"; import { buildPlanReviewRefinePrompt } from "../plans.ts"; import type { ContextShapingController } from "./context-shaping.js"; import type { ExecutionProgressController } from "./execution-progress.js"; import type { ModeLifecycleController } from "./mode-lifecycle.js"; import type { PlanArtifactController } from "./plan-artifact.js"; interface ExecutionFlowOptions { getFlag: (name: string) => unknown; appendEntry: (customType: string, data: unknown) => void; sendUserMessage: ( message: string, options?: ReturnType, ) => void; setNormalTools: () => void; updateStatus: (ctx: ExtensionContext) => void; persistState: () => void; } export class ExecutionFlowController { private readonly modeLifecycle: ModeLifecycleController; private readonly planArtifact: PlanArtifactController; private readonly contextShaping: ContextShapingController; private readonly executionProgress: ExecutionProgressController; private readonly options: ExecutionFlowOptions; constructor( modeLifecycle: ModeLifecycleController, planArtifact: PlanArtifactController, contextShaping: ContextShapingController, executionProgress: ExecutionProgressController, options: ExecutionFlowOptions, ) { this.modeLifecycle = modeLifecycle; this.planArtifact = planArtifact; this.contextShaping = contextShaping; this.executionProgress = executionProgress; this.options = options; } async handleReviewChoice( ctx: ExtensionContext, choice: PlanReviewChoice | undefined, planPath: string | undefined, reviewGeneration: number, ): Promise { this.planArtifact.pendingReviewPath = undefined; const decision = buildPlanExecutionDecision( choice, this.executionProgress.todos, planPath, ); if (decision.shouldExecute && decision.handoff) { if (!this.modeLifecycle.startExecution(reviewGeneration)) return; this.contextShaping.shapePending = false; recordContextMetric(ctx, this.options.getFlag, "plan-mode:execution-start", { todoCount: this.executionProgress.todos.length, planPath, }); this.options.setNormalTools(); this.options.updateStatus(ctx); const handoff = decision.handoff; this.options.appendEntry("plan-mode-execute", handoff.marker); this.options.persistState(); setTimeout(() => { this.options.sendUserMessage( handoff.message, planModeFollowUpDeliveryOptions(), ); }, 0); return; } this.modeLifecycle.returnToPlanning(); if (choice === "refine") { const refinement = await ctx.ui.editor( "What should the agent change before execution?", "", ); if (refinement?.trim()) { this.options.sendUserMessage( buildPlanReviewRefinePrompt({ planPath, userFeedback: refinement.trim(), context: this.executionProgress.todos.length > 0 ? `Extracted execution steps: ${this.executionProgress.todos.map((todo) => `${todo.step}. ${todo.text}`).join("; ")}` : undefined, }), planModeFollowUpDeliveryOptions(), ); } } this.options.updateStatus(ctx); this.options.persistState(); } completeExecutionIfDone(ctx: ExtensionContext): boolean { if (!this.modeLifecycle.executing) return false; if (this.executionProgress.todos.length > 0 && !this.executionProgress.isComplete()) return true; this.modeLifecycle.completeExecution(); this.executionProgress.clear(); this.options.setNormalTools(); this.options.updateStatus(ctx); this.options.persistState(); return true; } }