Spinner desyncs and corrupts terminal output when resized mid-animation (multi-line wrapped text)
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
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.columnsline-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.
Source: sindresorhus/ora