Skip to content

Regression in 11.8.0: interactive reporter flickers and never settles on run --parallel when the frame is taller than the terminal #12634

Description

@francois-codes

Summary

Since v11.8.0, running multiple long-running scripts with pnpm run --parallel (e.g. pnpm -r --parallel dev) in an interactive terminal causes the reporter to continuously repaint the entire screen and never settle — a relentless flicker/scroll that makes the output unusable. The underlying processes start exactly once and run fine; only the reporter is broken.

It is a regression introduced by #12351 / commit 3f0fb21 ("erase trailing characters on progress line"), which replaced the height-aware ansi-diff renderer with a manual in-place redraw whose cursor-up arithmetic is not clamped to the terminal height.

  • 11.5.2 / 11.7.0: fine.
  • 11.8.0: flickers / never settles.
  • Workaround: --reporter=append-only (sidesteps the interactive renderer entirely).

Environment

  • pnpm: 11.8.0 (broken) vs 11.5.2 (fine) — same machine, same repo, same terminal
  • Node.js: 24.13.0
  • OS: macOS 15 (Apple Silicon)
  • Terminal: Ghostty (TERM=xterm-ghostty); reproduces in any interactive TTY whose height is smaller than the rendered frame
  • Workspace: 8 packages, run via pnpm -r --parallel dev (each script is long-running and verbose on startup)

Steps to reproduce

  1. A workspace with several packages, each with a long-running, verbose dev script. Minimal version — root pnpm-workspace.yaml listing packages/*, and ~6 packages each with:
    { "scripts": { "dev": "node -e \"for(let i=0;i<40;i++)console.log('pkg line '+i); setInterval(()=>{}, 1e9)\"" } }
    The trigger is simply that the combined live frame the reporter draws is taller than the terminal window; many parallel projects + multi-line startup output is the common real-world case.
  2. In an interactive terminal (e.g. 24–50 rows), run:
    pnpm -r --parallel dev

Expected

The reporter renders the live multi-line view and settles once the processes are up (as in 11.5.2 / 11.7.0).

Actual

The reporter repaints the whole screen on every frame indefinitely — the startup summary (Scope: N of M workspace projects + each pkg dev$ … line + each process's buffered output) is reprinted thousands of times and the terminal scrolls/flickers without ever settling.

Evidence

Captured the same run through a pipe (no TTY → append-only fallback) vs a pty (script, interactive reporter):

marker (printed once per process) piped (real exec count) pty / interactive
process banner 4 11,123
uv sync "Checked …" 1 2,815
server "startup complete" 3 1,794

Same single execution; the interactive reporter re-emits each buffered frame thousands of times. The emitted ESC[<N>A cursor-up value grows without bound across frames (observed climbing 2659 → 2823), i.e. it tracks cumulative lines printed rather than a value bounded by the terminal height.

Root cause

cli/default-reporter/src/index.ts, changed in #12351 / 3f0fb21:

Before (height-aware):

const diff = createDiffer({ height: proc.stdout.rows, outputMaxWidth })
// ...
write(diff.update(view))

ansi-diff was constructed with height: proc.stdout.rows, so its redraw was clamped to the visible viewport.

After (not height-aware):

let prevRows = 0
function logUpdate (view) {
  if (!view.endsWith(EOL)) view += EOL
  const moveToFrameTop = prevRows > 0 ? `\x1b[${prevRows}A\r` : '\r'
  write(`${moveToFrameTop}${ERASE_TO_END_OF_DISPLAY}${view}`)
  prevRows = countRows(view) // counts '\n' only
}
// countRows: "Lines are assumed not to soft-wrap"

Two problems, both because prevRows no longer relates to the terminal height:

  1. Frame taller than the terminal. Once the frame has more lines than proc.stdout.rows, printing it scrolls the viewport, so the top of the logical frame moves into scrollback. The next frame emits ESC[<prevRows>A, but a terminal cannot move the cursor up past the top of the viewport into scrollback — so it lands at the top of the screen, not the true frame top. ESC[0J then erases the visible region and the oversized frame is reprinted, scrolling again. Every frame rewrites the whole screen and prevRows keeps growing → permanent flicker that never converges.
  2. Soft-wrapping. countRows counts \n only and explicitly assumes no soft-wrap, but run --parallel interleaves long child-output lines that wrap. prevRows then undercounts physical rows, so even sub-screen frames can misplace the cursor.

The old ansi-diff differ avoided both by being constructed with the terminal height.

Suggested fix

Restore height-awareness in the in-place redraw. Options:

  • Clamp the redraw to the viewport: cap prevRows (and the cursor-up move) at proc.stdout.rows, and erase/reprint only the visible region — matching what ansi-diff did via height.
  • When the frame would exceed proc.stdout.rows, stop the in-place redraw and append (or render only the last rows - 1 lines) instead of emitting an out-of-range ESC[<N>A.
  • Account for soft-wrap when computing rows (use outputMaxWidth / cli-truncate width math, not a raw \n count).

The #12350 fix (erasing external-process remnants) can be preserved while clamping the cursor math to the terminal height.

Workaround

pnpm -r --parallel --reporter=append-only dev — disables the interactive renderer; output streams cleanly regardless of frame/terminal height.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions