import { randomUUID } from "node:crypto"; import { mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { ExtensionCommandContext, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent"; import type { ImplementationPlanRetention } from "./settings.js"; import type { PlanCompletionSource, PlanModeState } from "./state.js"; type NewSessionOptions = Exclude[0], undefined>; type ReplacementContext = Parameters>[0]; export interface FreshImplementationRequest { plan: string; source: PlanCompletionSource; retention: ImplementationPlanRetention; stateEntryType: string; isCurrent(): boolean; } interface FreshImplementationFromStateOptions { getState(): PlanModeState; menuIsCurrent(): boolean; retention: ImplementationPlanRetention; stateEntryType: string; } export type FreshImplementationResult = | { kind: "started" } | { kind: "cancelled" } | { kind: "partial" } | { kind: "rejected" } | { kind: "stale" }; export function formatImplementationHandoff(plan: string) { return `Plan mode is now disabled. Full tool access is restored. Implement this proposed plan now:\n\n${plan}`; } export async function startFreshImplementationFromState( ctx: ExtensionContext, options: FreshImplementationFromStateOptions, ) { if (!isCommandContext(ctx)) { ctx.ui.notify( "Fresh implementation requires the interactive /plan command. Reopen /plan and try again.", "warning", ); return { kind: "rejected" } as const; } const initialState = options.getState(); const savedPlan = initialState.enabled ? undefined : initialState.savedPlan; const plan = (initialState.enabled ? initialState.latestPlan : savedPlan?.plan)?.trim(); const source = initialState.enabled ? initialState.latestPlanSource : savedPlan?.source; if (!plan || !source) { ctx.ui.notify("No completed plan is available to implement.", "warning"); return { kind: "rejected" } as const; } const wasEnabled = initialState.enabled; const isCurrent = () => { const current = options.getState(); return ( options.menuIsCurrent() && current.enabled === wasEnabled && (wasEnabled ? current.latestPlan === plan && current.latestPlanSource === source : current.savedPlan === savedPlan) ); }; return startFreshImplementationSession(ctx, { plan, source, retention: options.retention, stateEntryType: options.stateEntryType, isCurrent, }); } export async function startFreshImplementationSession( ctx: ExtensionCommandContext, request: FreshImplementationRequest, ): Promise { if (ctx.mode === "print" || ctx.mode === "json") { throw new Error("Fresh plan implementation is unavailable in print/JSON mode. Use TUI or RPC."); } await ctx.waitForIdle(); if (!request.isCurrent()) return { kind: "stale" }; const sourceModel = await preflightModel(ctx, request.isCurrent); if (!sourceModel) return { kind: "rejected" }; if (!request.isCurrent()) return { kind: "stale" }; const currentModel = ctx.model; if (currentModel?.provider !== sourceModel.provider || currentModel?.id !== sourceModel.id) { ctx.ui.notify( "The selected model changed during authentication. Confirm the intended model and retry /plan.", "warning", ); return { kind: "rejected" }; } const activeImplementation = { id: randomUUID(), plan: request.plan, source: request.source, startedAt: Date.now(), retention: request.retention, }; const destinationState: PlanModeState = { enabled: false, awaitingAction: false, activeImplementation, }; const handoff = formatImplementationHandoff(request.plan); let sourceBranch: SessionEntry[]; try { sourceBranch = typeof ctx.sessionManager.getBranch === "function" ? structuredClone(ctx.sessionManager.getBranch()) : []; } catch (error: unknown) { safeNotify( ctx, `Unable to preserve the planning session before replacement: ${safeErrorDetail(error)}. The source plan remains available; retry or resume the planning session.`, "error", ); return { kind: "rejected" }; } let parentSession = typeof ctx.sessionManager.getSessionFile === "function" ? ctx.sessionManager.getSessionFile() : undefined; if (!parentSession && sourceBranch.length > 0) { try { parentSession = snapshotInMemoryBranch(ctx, sourceBranch); } catch (error: unknown) { safeNotify( ctx, `Unable to preserve the in-memory planning session before replacement: ${safeErrorDetail(error)}. The source plan remains available; retry or resume the planning session.`, "error", ); return { kind: "rejected" }; } } let setupError: string | undefined; let kickoffError: string | undefined; let modelNeedsConfirmation = false; let replaced = false; if (ctx.mode === "rpc") ctx.ui.notify("Starting fresh implementation session…", "info"); let result: Awaited>; try { result = await ctx.newSession({ ...(parentSession ? { parentSession } : {}), setup: async (sessionManager) => { try { sessionManager.appendCustomEntry(request.stateEntryType, destinationState); } catch (error: unknown) { setupError = safeErrorDetail(error); } }, withSession: async (replacementCtx) => { replaced = true; if (setupError) { recoverSetupFailure(replacementCtx, handoff, setupError); return; } const destinationModel = replacementCtx.model; if (destinationModel?.provider !== sourceModel.provider || destinationModel?.id !== sourceModel.id) { modelNeedsConfirmation = true; replacementCtx.ui.setEditorText(handoff); replacementCtx.ui.notify( `Fresh session model differs: source ${safeErrorDetail(`${sourceModel.provider}/${sourceModel.id}`)}; destination ${destinationModel ? safeErrorDetail(`${destinationModel.provider}/${destinationModel.id}`) : "none (no model selected)"}. Implementation was not submitted. Select or confirm the intended model, then submit the request in the editor.`, "warning", ); return; } try { await replacementCtx.sendUserMessage(handoff); replacementCtx.ui.notify( "Fresh implementation session started. Only the approved plan was transferred.", "info", ); } catch (error: unknown) { kickoffError = safeErrorDetail(error); replacementCtx.ui.notify( `Fresh session created, but implementation did not start: ${kickoffError}. Send a message to continue, use /plan exit to clear the active plan, or resume the parent planning session.`, "error", ); } }, }); } catch (error: unknown) { if (replaced) return { kind: "partial" }; safeNotify( ctx, `Unable to start a fresh implementation session: ${safeErrorDetail(error)}. The source plan remains available; retry or resume the planning session.`, "error", ); return { kind: "rejected" }; } if (result.cancelled) { ctx.ui.notify("Fresh implementation cancelled. The plan remains available.", "info"); return { kind: "cancelled" }; } return setupError || kickoffError || modelNeedsConfirmation ? { kind: "partial" } : { kind: "started" }; } async function preflightModel(ctx: ExtensionCommandContext, isCurrent: () => boolean) { const model = ctx.model; if (!model) { ctx.ui.notify("Unable to implement the plan: no model is selected.", "warning"); return false; } // Keep only plain identity data across authentication and session replacement. const identity = { provider: model.provider, id: model.id }; let auth: Awaited>; try { auth = await ctx.modelRegistry.getApiKeyAndHeaders(model); } catch (error: unknown) { if (isCurrent()) { ctx.ui.notify(`Unable to implement the plan: ${safeErrorDetail(error)}`, "error"); } return false; } if (!isCurrent()) return false; if (!auth.ok) { ctx.ui.notify(`Unable to implement the plan: ${safeErrorDetail(auth.error)}`, "warning"); return false; } return identity; } function recoverSetupFailure(ctx: ReplacementContext, handoff: string, setupError: string) { ctx.ui.setEditorText(handoff); ctx.ui.notify( `Fresh session created, but the active plan could not be saved: ${setupError}. The implementation request is in the editor; submit it to continue or resume the parent planning session.`, "error", ); } function safeNotify(ctx: ExtensionCommandContext, message: string, level: "info" | "warning" | "error") { try { ctx.ui.notify(message, level); } catch { // The source context can become stale if Pi fails after replacement teardown. } } function snapshotInMemoryBranch(ctx: ExtensionCommandContext, branch: readonly SessionEntry[]): string { const directory = mkdtempSync(join(tmpdir(), "pi-plan-mode-")); const sessionId = randomUUID(); const sessionFile = join(directory, `${sessionId}.jsonl`); const header = { type: "session" as const, version: 3, id: sessionId, timestamp: new Date().toISOString(), cwd: typeof ctx.sessionManager.getCwd === "function" ? ctx.sessionManager.getCwd() : ctx.cwd, }; writeFileSync(sessionFile, `${[header, ...branch].map((entry) => JSON.stringify(entry)).join("\n")}\n`, { encoding: "utf8", mode: 0o600, }); return sessionFile; } function isCommandContext(ctx: ExtensionContext): ctx is ExtensionCommandContext { return typeof (ctx as Partial).newSession === "function"; } function safeErrorDetail(error: unknown) { const detail = error instanceof Error ? error.message : String(error); const normalized = [...detail] .map((character) => { const codePoint = character.codePointAt(0) ?? 0; return codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f) ? " " : character; }) .join("") .replace(/\s+/gu, " ") .trim() || "unknown error"; const characters = [...normalized]; return characters.length > 500 ? `${characters.slice(0, 499).join("")}…` : normalized; }