feat(cli): NDJSON render progress event stream for agents (--progress-format) - #3755
Open
mvanhorn wants to merge 1 commit into
Open
feat(cli): NDJSON render progress event stream for agents (--progress-format)#3755mvanhorn wants to merge 1 commit into
mvanhorn wants to merge 1 commit into
Conversation
Add --progress-format <tty|ndjson|none> to hyperframes render. ndjson streams one JSON object per producer progress tick to stdout (or an inherited fd via --progress-fd N): render.progress ticks plus exactly one terminal render.completed / render.failed carrying failedStage and the producer's structured errorDetails. Batch rows multiplex into the same stream stamped with their row index. When the stream owns stdout, human output is quiet-suppressed and stdout-bound console diagnostics are rerouted to stderr so pipes stay clean; --quiet keeps silencing human output only, never the stream. Docs: reference/cli-render + guides/agents (agent recipe).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds
--progress-format <tty|ndjson|none>(defaulttty) tohyperframes render, plus--progress-fd N.With
ndjson, every producer progress tick becomes one newline-terminated JSON object on stdout (or on an inherited file descriptor via--progress-fd):render.progressticks plus exactly one terminalrender.completed/render.failedevent per render. The terminal failure event carries the producer's structured failure contract —failedStageanderrorDetails(message, elapsed time, free memory, browser console tail, per-stage timings). Batch rows multiplex into the same stream, each event stamped with itsrowindex.nonedisables progress output without touching the rest of the human output.{"type":"render.progress","ts":"2026-09-08T00:49:12.599Z","progress":0.25,"status":"rendering","stage":"Starting frame capture","message":"Starting frame capture","framesRendered":0,"totalFrames":90,"failedStage":null}Why
hyperframes renderonly drives a TTY progress bar, and--quietremoves even that. Agents and CI pipelines driving renders need machine-readable progress: is the render moving, what stage is it in, and — on failure — which stage died and why, in a form a program can branch on instead of regex-matching human log text. Remotion exposes this viaonProgresscallbacks; the producer already has aProgressCallbackcontract internally — this PR surfaces it at the CLI boundary as an NDJSON stream.How
packages/cli/src/ui/progressNdjson.ts— newProgressNdjsonWriter, extracted next toprogress.ts. It isProgressCallback-shaped (publish(job, message)), maps the producer's terminalfailed/completestatus ticks torender.failed/render.completed, and latches after one terminal event per writer so the double-report paths (the producer's own terminal tick viapublishRenderFailure+ the CLI'shandleRenderError) can't duplicate the terminal line. Sink failures (EPIPE when the consumer exits early) disable the stream instead of killing the render — same containment rule as the producer'sOrderedRenderEventPublisher.render.ts— flags,resolveRenderProgressCallback(precedence: active NDJSON writer → streams even under--quiet, since quiet governs human output; then--quiet/nonedisable; then the TTY bar), a guaranteedrender.completedafter a successfulexecuteRenderJob, and arender.failedemission at the top ofhandleRenderErrorsothrowOnErrorpaths (batch rows) still close the stream.render/plan.ts— flag parsing/validation:ndjsonis rejected with--docker(the container's output is opaque to the host CLI) and with--jsonunless--progress-fdmoves the stream off stdout;--progress-fdrequiresndjson.render/execute.ts— one shared sink per command, one writer per render; batch rows each get a writer stamped withrow.index. When the stream owns stdout, human output is quiet-suppressed (the rule--batch --jsonalready applies viaeffectiveQuiet) and stdout-boundconsole.log/info/debugare rerouted to stderr — caught live: the engine's[BrowserManager] Browser launchedline otherwise lands mid-stream.ProgressCallback/OrderedRenderEventPublisher/publishRenderFailurecontract already delivers everything the stream needs.docs/reference/cli-render.mdx(event schema, flag interactions) anddocs/guides/agents.mdx(the agent recipe), both added to the nav.Demo
HyperFrames-rendered walkthrough — the demo below is itself a HyperFrames composition (
walkthrough/index.html, 25s · 1920×1080 · five scenes: title → NDJSON stream with live progress HUD → jq consumption ending inrender.completed→ the failure path withfailedStageextraction → flag end card). It passednpx hyperframes lintandnpx hyperframes check(0 errors, 139/139 WCAG contrast checks) and was rendered with:npx hyperframes render --quality high --output demo.mp4 # hyperframes 0.8.31The GIF is derived from that HyperFrames-rendered MP4 (ffmpeg palette two-pass, 960px/12fps).
(MP4 version)
Previous demo (terminal screen recording — superseded by the HyperFrames-rendered walkthrough above)
(MP4 version)
Test plan
Unit tests added/updated
Manual testing performed
Documentation updated (if applicable)
packages/cli/src/ui/progressNdjson.test.ts(17 tests): event shape, publish ordering, terminal mapping + exactly-one-terminal dedupe, cancelled latch, batchrowstamping, progress clamping, EPIPE containment, console redirect, stdout sink.packages/cli/src/commands/render.progressNdjson.test.ts(16 tests):resolveRenderProgressCallbackprecedence with a fake ProgressCallback tick sequence (including quiet interaction and a stray post-terminal tick — no browser required), andcreateRenderPlanflag validation (--docker/--json/--progress-fdinteractions,effectiveQuiet).Full CLI suite: 3042 passed | 3 skipped (
bunx vitest runinpackages/cli).bunx oxlint/bunx oxfmt --checkclean on all touched files;tsc --noEmitclean for the CLI package.Live end-to-end on Linux: success stream (pure JSON on stdout — zero non-JSON lines),
--quietkeeps the stream flowing,--progress-fd 3keeps human output on stdout with events on fd 3, a failing render exits 1 withrender.failedcarryingfailedStage: "Extracting video frames"+errorDetails, and a 2-row--batchstampsrow: 0/1with per-row terminal events.AI assistance disclosure