/** * phases.ts — phase-log accumulator + stdout progress + completion-message * helpers. * * The live *detail* view of a check run is the tool-event stream piped into * the chat (see `pygienium-stream` in `index.ts`): each `tool_execution_start * /end` and assistant turn becomes its own chat message. This module no longer * owns a TUI widget — it only records phase transitions for the completion * message and forwards phase headers to stdout in print/JSON mode. * * The *overview* view is the footer widget (see `footer.ts`): a multi-line * `belowEditor` strip listing the full pipeline with the cursor and what's to * come. The two never overlap: the chat is the per-event detail, the footer * is the static overview. * * The strip accumulates a phase log (`getPhaseLog`) that the check-runner * turns into the expandable completion message rendered by * `registerMessageRenderer("pygienium-progress")` in `index.ts`. * * @module pygienium/phases */ import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent"; /** Callback to post a message into the chat history (see `index.ts` renderer). */ export type SendChatMessage = ( content: string, /** Extra data passed to the message renderer (toolCalls, completion, …). */ meta?: { phase?: string; /** Tool calls captured during this agent execution (ralpi-style tree). */ toolCalls?: never; [meta: string]: unknown; }, ) => void; /** Phase display metadata for a check run's phases. */ export const PHASE_LABELS: Record = { recon: "Recon", analysis: "Scanning", fix: "Fixing", verify: "Verifying", cleanup: "Cleaning up", }; /** Status of a single phase as recorded in the completion log. */ export type PhaseLogStatus = "running" | "complete" | "failed" | "skipped"; /** One phase entry carried into the completion message's `details.phases`. */ export interface PhaseLogEntry { /** Phase id (e.g. "analysis"). */ id: string; /** Human-readable label (e.g. "Scanning"). */ label: string; /** Terminal/running status. */ status: PhaseLogStatus; /** Optional note shown on the branch (e.g. "findings: 12 lines"). */ note?: string; } export interface PhaseStripOptions { /** Check label shown alongside the phase, e.g. "comments". */ checkLabel?: string; /** Whether dialog-capable UI is available. */ hasUI?: boolean; /** UI context (unused for widget rendering since the stream owns the chat). */ ui?: ExtensionUIContext; } /** * A handle that records phase transitions for the completion message and * forwards phase headers to stdout when no TUI is present. Created by * {@link createPhaseStrip}; the check-runner drives it. */ export interface PhaseStrip { /** Set the current phase (id or a pre-rendered header string). */ setPhase(phaseId: string): void; /** Annotate the most recent phase (e.g. "findings: 12 lines"). */ setPhaseNote(note: string): void; /** Append a plain-text progress line (forwarded to stdout in print mode). */ log(line: string): void; /** Snapshot of phase transitions for the completion message. */ getPhaseLog(): PhaseLogEntry[]; /** Mark the strip terminal. Safe to call repeatedly. */ done(): void; } /** * Simple write lock for stdout in print mode to prevent interleaved output * from parallel checks. */ let stdoutLock: Promise = Promise.resolve(); /** Acquire the stdout write lock and execute the write function. */ async function withStdoutLock(fn: () => void): Promise { const prev = stdoutLock; stdoutLock = prev.then(() => { fn(); return Promise.resolve(); }); return stdoutLock; } /** Create a phase-strip UI adapter. */ export function createPhaseStrip(opts: PhaseStripOptions): PhaseStrip { const checkLabel = opts.checkLabel; const hasUI = opts.hasUI ?? false; const phaseLog: PhaseLogEntry[] = []; let disposed = false; let currentHeader = checkLabel ? `pygienium ${checkLabel}: starting…` : "pygienium: starting…"; function phaseLabel(id: string): string { return PHASE_LABELS[id] ?? id; } function writeStdout(text: string): void { if (!disposed && !hasUI) { process.stdout.write(`${text}\n`); } } return { setPhase(phaseId) { if (disposed) return; const label = phaseLabel(phaseId); currentHeader = checkLabel ? `pygienium ${checkLabel}: ${label}` : `pygienium: ${phaseId}`; phaseLog.push({ id: phaseId, label, status: "running" }); if (!hasUI) { withStdoutLock(() => writeStdout(currentHeader)).catch(() => {}); } }, setPhaseNote(note) { const last = phaseLog[phaseLog.length - 1]; if (!last) return; last.note = note; }, log(line) { if (disposed || hasUI) return; withStdoutLock(() => writeStdout(line)).catch(() => {}); }, getPhaseLog() { return phaseLog; }, done() { if (disposed) return; disposed = true; }, }; } /** Phase-log detail carried into a completion message's `details`. */ export interface CheckCompletionDetails { checkLabel: string; status: "complete" | "failed" | "skipped"; fix?: boolean; durationMs?: number; phases: PhaseLogEntry[]; error?: string; } /** Re-exported for index.ts renderer convenience. */ export const PHASE_GLYPH: Record = { running: "~", complete: "✓", failed: "✗", skipped: "-", };