#261·ora

Spinner desyncs and corrupts terminal output when resized mid-animation (multi-line wrapped text)

Author: dpc00Created Sep 12, 2026Updated Sep 12, 2026

Ora#updateLineCount() computes how many terminal rows the current text wraps to using this.#stream.columns at the moment it runs, and caches the result in #lineCount. It's only ever called from the text/prefixText/ suffixText/indent setters — never in response to a terminal resize.

clear() (called at the top of every render(), i.e. every animation frame) uses that cached #linesToClear to move the cursor up and erase the previous frame. If the terminal is resized while the spinner is running and the text is long enough to wrap across multiple rows, the actual wrapped row count changes but the cached one doesn't — so every subsequent clear() erases the wrong region of the screen. This isn't a one-frame glitch: since #lineCount is never recomputed for the rest of that spinner's life, the desync is permanent and can progressively corrupt unrelated terminal content above/around the spinner.

Repro

javascript
import ora from 'ora';
const spinner = ora(
  'Waiting for browser authentication at https://example.com/some/very/long/callback/url/that/wraps/across/multiple/terminal/columns'
).start();
// While it's spinning, resize the terminal so this message's wrap
// count changes (e.g. drag narrower). A *single* resize is enough if
// it crosses the wrap-count boundary for this text at this width —
// no rapid resizing needed.

Observed: subsequent frames overwrite/interleave fragments of the message into adjacent rows instead of cleanly redrawing in place.

Environment

  • ora 8.2.0
  • Found via @sourcegraph/cody's cody auth login --web (real-world discovery), reproduced directly against ora in isolation
  • Windows 11, ConPTY-backed terminal — the bug itself isn't Windows-specific (it's pure stream.columns line-math), but I haven't personally reproduced on macOS/Linux

Suggested fix

Recompute #lineCount from the current stream.columns inside clear() right before using it (or listen for the stream's 'resize' event while spinning and call #updateLineCount() then), rather than relying on a value cached at the last text/prefixText/etc. assignment.