import { randomUUID } from "node:crypto"; import { readFile } from "node:fs/promises"; import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, } from "@earendil-works/pi-coding-agent"; import { isValidContextContinuation } from "./continuation.ts"; import type { GateDecision, PrototypeCandidate, WorkflowRun, } from "./domain.ts"; import { executeHostDecision, type ContextTransitionRuntime, } from "./context-transition.ts"; import { deriveRunLabels } from "./run-label.ts"; import { readVerifiedPlanArtifact, readVerifiedReportArtifact, readVerifiedTranscriptArtifact, } from "./store.ts"; import { workflowConfirm, workflowEditor, workflowInput, workflowPlanReader, workflowSelect, } from "./tui.ts"; const CORE_STAGES = [ "discover", "design", "prototype", "implement", "verify", "review", ] as const; function activeStage(run: WorkflowRun) { return ( [...run.stages] .reverse() .find((stage) => stage.status === "running" || stage.status === "waiting") ?.kind ?? run.status ); } function selectedGate(run: WorkflowRun) { return [...run.gates].reverse().find((gate) => gate.status === "open"); } function stageSummary(run: WorkflowRun) { return run.stages .filter((stage) => CORE_STAGES.includes(stage.kind as (typeof CORE_STAGES)[number])) .map((stage) => { const marker = stage.status === "completed" ? "✓" : stage.status === "failed" ? "×" : stage.status === "running" || stage.status === "waiting" ? "●" : "·"; return `${marker} ${stage.kind}`; }) .join(" "); } function runSummary(run: WorkflowRun, label: string) { const gate = selectedGate(run); return [ `${label} · ${run.status} · revision ${run.revision}`, `Stage: ${activeStage(run)}`, `Progress: ${stageSummary(run)}`, ...(gate ? [`Gate: ${gate.kind} (${gate.id})`] : []), ...(run.workspace.branch ? [`Branch: ${run.workspace.branch}`] : []), ...(run.workspace.root ? [`Worktree: ${run.workspace.root}`] : []), ...(run.planArtifact ? [`Plan: ${run.planArtifact.path}`] : []), ...(run.reportArtifact ? [`Report: ${run.reportArtifact.path}`] : []), ].join("\n"); } function idempotencyKey(run: WorkflowRun, decision: GateDecision) { return `dashboard-${run.runId}-${run.revision}-${decision.kind}-${randomUUID()}`; } async function decide( pi: Pick, runtime: ContextTransitionRuntime, ctx: ExtensionContext, run: WorkflowRun, decision: GateDecision, ) { const gate = selectedGate(run); if (!gate) throw new Error("Workflow Run has no open Gate"); const engine = await runtime.engineForContext(ctx); return executeHostDecision(pi, engine, { action: "decide", runId: run.runId, gateId: gate.id, expectedRevision: run.revision, decision, idempotencyKey: idempotencyKey(run, decision), }); } async function questionDecision(ctx: ExtensionContext, run: WorkflowRun) { const payload = selectedGate(run)?.payload; if (!payload || typeof payload !== "object") return undefined; const questions = (payload as { questions?: unknown }).questions; if (!Array.isArray(questions)) return undefined; const answers: Record = {}; for (const item of questions) { if (!item || typeof item !== "object") continue; const id = (item as { id?: unknown }).id; const prompt = (item as { prompt?: unknown }).prompt; if (typeof id !== "string" || typeof prompt !== "string") continue; const answer = await workflowInput(ctx, "Required question", prompt); if (!answer?.trim()) return undefined; answers[id] = answer.trim(); } return { kind: "answer-questions" as const, answers }; } async function planDecision(ctx: ExtensionContext, run: WorkflowRun) { if (!run.planArtifact || !run.uiImpact) return undefined; while (true) { const action = await workflowSelect(ctx, "Plan Approval", [ { value: "inspect" as const, label: "Review verified plan" }, { value: "approve" as const, label: `Approve plan · UI impact ${run.uiImpact}`, }, { value: "revise" as const, label: "Request revision" }, ]); if (!action) return undefined; if (action === "inspect") { const plan = await readVerifiedPlanArtifact(run.planArtifact); await workflowPlanReader(ctx, run.featureName ?? run.runId, plan); continue; } if (action === "revise") { const feedback = await workflowEditor( ctx, "Plan revision feedback", "Describe the required plan changes.", ); return feedback?.trim() ? { kind: "revise-plan" as const, feedback: feedback.trim() } : undefined; } const approved = await workflowConfirm( ctx, "Approve plan", `Approve plan revision ${run.planArtifact.revision} and explicitly accept UI impact ${run.uiImpact}?`, ); return approved ? { kind: "approve-plan" as const, approved: true as const, acceptedUiImpact: run.uiImpact, } : undefined; } } async function prototypeDecision(ctx: ExtensionContext, run: WorkflowRun) { const candidates = run.prototypeCandidates.slice(-3); while (true) { const action = await workflowSelect< | { kind: "candidate"; candidate: PrototypeCandidate } | { kind: "refine" } | { kind: "abort" } >(ctx, "Prototype Acceptance", [ ...candidates.map((candidate, index) => ({ value: { kind: "candidate" as const, candidate }, label: `${index + 1}. ${candidate.structuralRationale}`, description: candidate.route, })), { value: { kind: "refine" as const }, label: "Request refinement" }, { value: { kind: "abort" as const }, label: "Abort run" }, ]); if (!action) return undefined; if (action.kind === "refine") { const feedback = await workflowEditor( ctx, "Prototype refinement feedback", "Describe what the next candidates must change.", ); return feedback?.trim() ? { kind: "refine-prototypes" as const, feedback: feedback.trim() } : undefined; } if (action.kind === "abort") { return (await workflowConfirm( ctx, "Abort Workflow Run", "Abort this run and preserve its current evidence?", )) ? ("abort" as const) : undefined; } const candidate = action.candidate; while (true) { const candidateAction = await workflowSelect( ctx, `Prototype ${candidate.revision}`, [ { value: "review" as const, label: "Review candidate evidence" }, { value: "accept" as const, label: "Accept this candidate" }, { value: "back" as const, label: "Back to candidates" }, ], ); if (!candidateAction || candidateAction === "back") break; if (candidateAction === "review") { await workflowPlanReader( ctx, `prototype ${candidate.revision}`, [ candidate.launchInstructions, `Route: ${candidate.route}`, `Evidence: ${candidate.retainedArtifacts.join(", ")}`, ].join("\n\n"), ); continue; } const browserReviewed = await workflowConfirm( ctx, "Accept prototype", "Confirm you reviewed this exact candidate in the browser or application at mobile and desktop sizes.", ); if (!browserReviewed) continue; const feedback = await workflowInput( ctx, "Acceptance feedback", "Optional feedback. Submit an empty value for none.", ); return { kind: "accept-prototype" as const, candidateId: candidate.id, candidateRevision: candidate.revision, browserReviewed: true as const, ...(feedback?.trim() ? { feedback: feedback.trim() } : {}), }; } } } async function implementationContextDecision(ctx: ExtensionContext) { const strategy = await workflowSelect(ctx, "Implementation Context", [ { value: "fresh-session" as const, label: "Fresh plan-seeded session", description: "Replace this session and continue immediately.", }, { value: "smart-compact" as const, label: "Compact current session", }, { value: "current-session" as const, label: "Keep current context unchanged", }, ]); return strategy ? { kind: "select-implementation-context" as const, strategy } : undefined; } async function recoveryDecision(ctx: ExtensionContext) { const resolution = await workflowSelect(ctx, "Recovery", [ { value: "retry" as const, label: "Retry" }, { value: "inspect" as const, label: "Inspect" }, { value: "abort" as const, label: "Abort" }, ]); return resolution ? { kind: "recover" as const, resolution } : undefined; } async function gateDecision(ctx: ExtensionContext, run: WorkflowRun) { const gate = selectedGate(run); if (!gate) return undefined; if (gate.kind === "questions") return questionDecision(ctx, run); if (gate.kind === "plan-approval") return planDecision(ctx, run); if (gate.kind === "prototype-acceptance") return prototypeDecision(ctx, run); if (gate.kind === "implementation-context") { return implementationContextDecision(ctx); } return recoveryDecision(ctx); } function validContinuation(run: WorkflowRun, sessionId?: string) { return isValidContextContinuation( { ...run, openGate: selectedGate(run) }, sessionId, ); } function isActiveParent(run: WorkflowRun, sessionId?: string) { return Boolean(sessionId && run.lease?.activeParentSessionId === sessionId); } async function browsePrototypeArtifacts(ctx: ExtensionContext, run: WorkflowRun) { while (true) { const candidate = await workflowSelect( ctx, "Prototype evidence", run.prototypeCandidates.map((entry, index) => ({ value: entry, label: `${index + 1}. ${entry.structuralRationale}`, description: entry.route, })), ); if (!candidate) return; while (true) { const artifact = await workflowSelect( ctx, "Retained artifact", [candidate.artifactPath, ...candidate.retainedArtifacts].map((entry) => ({ value: entry, label: entry, })), ); if (!artifact) break; if (artifact.endsWith(".json")) { await workflowPlanReader( ctx, "prototype evidence", await readFile(artifact, "utf8"), ); } else { await workflowPlanReader( ctx, "prototype evidence", `${candidate.launchInstructions}\n\nRoute: ${candidate.route}\n\nArtifact: ${artifact}`, ); } } } } async function browseAssignmentTranscripts(ctx: ExtensionContext, run: WorkflowRun) { const assignments = run.assignments.filter( (assignment) => assignment.transcriptArtifact, ); while (true) { const assignment = await workflowSelect( ctx, "Assignment transcripts", assignments.map((entry, index) => ({ value: entry, label: `${index + 1}. ${entry.role}/${entry.profileName}`, description: entry.status, })), ); if (!assignment?.transcriptArtifact) return; const transcript = await readVerifiedTranscriptArtifact( assignment.transcriptArtifact, ); await workflowPlanReader(ctx, `${assignment.role} transcript`, transcript); } } type DashboardActionResult = | { kind: "back" } | { kind: "continue"; runId: string } | { kind: "closed" }; async function actionSheet( pi: ExtensionAPI, runtime: ContextTransitionRuntime, ctx: ExtensionCommandContext, initialRun: WorkflowRun, label: string, ): Promise { let run = initialRun; while (true) { const actions = [ { value: "inspect" as const, label: "Inspect run", description: runSummary(run, label) }, ...(selectedGate(run) && isActiveParent(run, ctx.sessionManager.getSessionId()) ? [{ value: "decide" as const, label: "Record Gate Decision", description: selectedGate(run)?.kind }] : []), ...(validContinuation(run, ctx.sessionManager.getSessionId()) ? [{ value: "continue" as const, label: "Continue context transition", description: "Enter the selected implementation context now." }] : []), ...(run.planArtifact ? [{ value: "plan" as const, label: "Review verified plan" }] : []), ...(run.reportArtifact ? [{ value: "report" as const, label: "Open report" }] : []), ...(run.prototypeCandidates.length > 0 ? [{ value: "prototypes" as const, label: "Browse prototype evidence" }] : []), ...(run.assignments.some((assignment) => assignment.transcriptArtifact) ? [{ value: "transcripts" as const, label: "Browse Assignment transcripts" }] : []), ...(run.workspace.root ? [{ value: "worktree" as const, label: "Show worktree path" }] : []), ]; const action = await workflowSelect(ctx, label, actions); if (!action) return { kind: "back" }; if (action === "continue") return { kind: "continue", runId: run.runId }; if (action === "inspect") { await workflowPlanReader(ctx, label, runSummary(run, label)); } else if (action === "plan") { await workflowPlanReader( ctx, label, await readVerifiedPlanArtifact(run.planArtifact!), ); } else if (action === "report") { await workflowPlanReader( ctx, `${label} report`, await readVerifiedReportArtifact(run.reportArtifact!), ); } else if (action === "prototypes") { await browsePrototypeArtifacts(ctx, run); } else if (action === "transcripts") { await browseAssignmentTranscripts(ctx, run); } else if (action === "worktree") { await workflowPlanReader(ctx, "worktree", run.workspace.root!); } else if (action === "decide") { const decision = await gateDecision(ctx, run); if (decision === "abort") { const engine = await runtime.engineForContext(ctx); await engine.execute({ action: "abort", runId: run.runId, expectedRevision: run.revision, idempotencyKey: `dashboard-abort-${run.runId}-${run.revision}-${randomUUID()}`, }); } else if (decision) { const result = await decide(pi, runtime, ctx, run, decision); if ( decision.kind === "select-implementation-context" && decision.strategy === "smart-compact" ) { ctx.ui.notify( `Workflow ${label} is ready for smart_compact. Ask the assistant to invoke it and report the Gate-bound outcome through workflow.`, "info", ); } else if ( decision.kind === "select-implementation-context" && decision.strategy === "fresh-session" ) { return { kind: "continue", runId: run.runId }; } else { ctx.ui.notify(`Workflow ${label}: ${result.status}`, "info"); } } const refreshed = (await runtime.store.listRuns()).find((candidate) => candidate.runId === run.runId); if (!refreshed || ["completed", "failed", "aborted"].includes(refreshed.status)) { return { kind: "closed" }; } run = refreshed; } } } export interface DashboardResult { continueRunId?: string; } export async function openWorkflowDashboard( pi: ExtensionAPI, runtime: ContextTransitionRuntime, ctx: ExtensionCommandContext, initialRunId?: string, ): Promise { const sessionId = ctx.sessionManager.getSessionId(); const runs = (await runtime.store.listRuns()) .filter((run) => !sessionId || run.parentSessionIds.includes(sessionId)) .sort((left, right) => { const leftTime = Math.max(left.lease?.heartbeatAt ?? 0, ...left.gates.map((gate) => gate.openedAt)); const rightTime = Math.max(right.lease?.heartbeatAt ?? 0, ...right.gates.map((gate) => gate.openedAt)); return rightTime - leftTime; }); if (initialRunId && !runs.some((run) => run.runId === initialRunId)) { ctx.ui.notify(`Workflow Run ${initialRunId} was not found for this session.`, "error"); return {}; } const labels = deriveRunLabels(runs); if (!ctx.hasUI || ctx.mode !== "tui") { const visibleRuns = runs.slice(0, 20); const omitted = runs.length - visibleRuns.length; ctx.ui.notify( runs.length > 0 ? `${visibleRuns.map((run, index) => runSummary(run, labels[index]!)).join("\n\n")}${omitted > 0 ? `\n\n[${omitted} older runs omitted]` : ""}` : "No Workflow Runs.", "info", ); return {}; } let selectedRunId = initialRunId; while (true) { if (!selectedRunId) { selectedRunId = await workflowSelect( ctx, "workflow", runs.map((run, index) => ({ value: run.runId, label: `${labels[index]} ${run.status.toUpperCase()} ${String(activeStage(run))}`, description: selectedGate(run)?.kind, })), ); if (!selectedRunId) return {}; } const index = runs.findIndex((candidate) => candidate.runId === selectedRunId); const run = runs[index]; if (!run) return {}; const result = await actionSheet(pi, runtime, ctx, run, labels[index]!); if (result.kind === "continue") return { continueRunId: result.runId }; if (result.kind === "closed") return {}; selectedRunId = undefined; } }