/** * StdoutProgress is a ProgressPort that prints to stdout — the always-on, * zero-config progress sink. It adapts to the resolved StdoutCapabilities: * * - DISCRETE events print as their own newline-terminated line, the durable * scrollback record of the run, rendered from the semantic markdown that * `formatEvent` produces via `renderAnsi` (ANSI when colour is on, stripped * to clean plain text when off — no leaked `**` asterisks). On a TTY the * `stage-started` line is augmented with a block progress bar derived from * the event's index/total; a non-TTY keeps the plain `N/total` text. * * - HEARTBEAT ticks animate a spinner ON THE SAME LINE when `animate` is set. * The line is COMPOSED here (not via formatEvent) because it is presentation * heavy: a braille spinner, the activity's glyph, the activity label (or a * "working" fallback), and an elapsed timer coloured by how close the step * is to its timeout ceiling. Each tick rewrites the line with a carriage * return; a following discrete event breaks the line first so the spinner's * last frame survives in scrollback. When `animate` is OFF (a non-TTY: * piped, redirected, CI), heartbeats are SUPPRESSED entirely. * * The write sink, capabilities, and step-timeout ceiling are injected so the * rendering and animation are unit-tested without a real stdout or terminal. */ import type { ProgressEvent, ProgressPort } from "../ports/progress.ts"; import type { StdoutCapabilities } from "../domain/stdout-capabilities.ts"; export declare class StdoutProgress implements ProgressPort { private readonly capabilities; private readonly write; /** The per-step timeout ceiling, for colouring the elapsed timer. 0 = none. */ private readonly stepTimeoutMs; private spinnerFrame; /** True while the current terminal line holds an unterminated spinner. */ private spinnerActive; constructor(capabilities: StdoutCapabilities, write?: (s: string) => void, stepTimeoutMs?: number); emit(event: ProgressEvent): Promise; /** * Renders a discrete event line. When `fun` is on, the tier-running, verdict, * and retry events get persona-flavored wording (stdout-only personality); * otherwise they fall through to the shared neutral markdown formatter — which * is also exactly what the Telegram sink renders. `stage-started` additionally * gets a block progress bar on a TTY, keeping the plain `N/total` text the * formatter produced for a non-TTY. */ private renderDiscrete; /** * The persona-flavored wording for the events that have a personality, or * undefined for events that keep their neutral rendering. Only consulted when * `fun` is on. The verdict/retry SEMANTICS any log scraping relies on are not * the stdout sink's concern — Telegram and progress.md keep the canonical * "VERDICT PASS" / attempt wording. */ private funLine; private writeSpinner; /** * Composes the heartbeat line: spinner + the activity's glyph + the activity * label + a colour-shifting elapsed timer. The spinner is the only leading * glyph — the old bare `⏳` is gone. When no real activity is known, the label * is rotating flavor text (when `fun` is on) or a neutral "working" otherwise; * a known activity always takes precedence over flavor text. The rotation * bucket is a ~15s slice of elapsed time, so the phrase changes slowly rather * than flickering on every 1s tick. */ private heartbeatLine; }