Skip to content

Inline multi-line ANSI progress redraws can accumulate duplicate transcript lines #388

Open

Description

@raiseCatError

Problem / observed symptom

During a Homebrew-driven install inside NMSh, the maintainer observed progress output accumulating/spamming transcript lines instead of redrawing in place. The exact brew command and byte trace have not yet been captured; this is a report requiring physical reproduction, not a confirmed Homebrew-specific regression.

The generic source-level gap is narrower and independently visible on release/v0.18.0:

  • src/output/AnsiOutputParser.ts handles carriage return (\r) by resetting the column and can clear/replace the current line with CSI K/G/C/D. It does not implement upward/downward or absolute row movement (e.g. CSI A/B/H/f), yet it appends on every newline.
  • src/output/Classifier.ts detects cursor positioning as hasCursorMovement, but moves ordinary output to LIVE after time has passed; cursor motion alone is deliberately not enough for PASSTHROUGH.
  • Consequently, inline multi-line progress displays that redraw previous rows using CSI A/2K or H/K are liable to become multiple permanent transcript lines, even though single-line \r rewriting works.
  • tests/outputBuffer.test.ts covers 10%\r20%\r100%\n, and tests/interactiveCli.test.ts verifies progress output does not incorrectly steal the terminal; neither proves multi-row redraws remain bounded and visually correct.

This needs an explicit rendering contract and controlled reproduction, not an assumption that all ANSI output should be printed literally.

Minimal diagnostic fixture

Compare the same command in plain Ghostty/Terminal.app versus NMSh, with normal and narrow width, and record the actual byte stream (safely, without private output):

node -e 'for (let i=0;i<=10;i++){process.stdout.write((i?"\x1b[1A\x1b[2K":"")+"Downloading "+i*10+"%\n")}'

A single progress line rewritten via CR is an existing passing control:

node -e 'process.stdout.write("10%\r20%\r100%\n")'

Also test two-or-more-line redraws, chunk boundaries split inside CSI, ANSI colors/hyperlinks, multiple progress components, window resize, completion, ctrl-C, persistent shell, detach/reattach, transcript copy/search/folding, NO_COLOR and Safe mode. Distinguish actual tool output from NMSh's own progress UI.

Design / acceptance questions

  1. Decide how NMSh's bounded inline transcript representation supports multi-row in-place overwrites without growing a history entry per visual frame. Avoid turning arbitrary ANSI into a full terminal emulator; if unsupported constructs are encountered, choose an explicit safe degradation.
  2. Preserve the distinction between raw PTY event data and the current presented screen. Do not silently delete useful logs or corrupt /copy, transcript search, saved sessions, and replay. Document what copy means for transient redraws and expose raw recorded output where supported.
  3. Don't auto-enter PASSTHROUGH solely on progress bars, CSI A, cursor hiding, or high volume: interactive terminal ownership needs independent evidence and the composer should stay usable.
  4. Add focused parser/classifier/real-PTY regressions reproducing the reported repeated-line failure; include representative brew install/brew update when possible, plus another independent renderer (e.g. package installer progress).
  5. Benchmark frequent updates and long hostile escape sequences; bound memory, CPU, rendering frequency, retained history and row count.
  6. Test normal/narrow Ghostty and Terminal.app visually; record the exact before/after. Distinguish ordinary progress from genuine interactive TUIs and preserve existing mode/passthrough behavior.

Coordination

Follow-up to ongoing compatibility coverage #14 and structured presentation #4; related inline-UI handover distinction #211. No duplicate architecture or per-program parsing heuristics. Shared frontend files (src/app/TerminalApp.ts) are owned by the active #359 stabilization track: do not edit those files or disrupt Codex's work until #359 gives an integration checkpoint.

The Reddit discussion was shared as context (https://www.reddit.com/r/commandline/s/WCZeIyUbCh), but its short URL could not be resolved during this review. Do not infer the post's content, comments, or linked repository from the share token. Add the canonical post and any upstream evidence once accessible.

Activity

  1. raiseCatError commented on Oct 12, 2026

    @raiseCatError
    OwnerAuthor

    Source follow-up — Reddit discussion verified (2026-10-12)

    The linked post is now resolved: Veitangie, Why terminal animations can eat actual output and how to stop them and the spinq repository.

    Important distinction: spinq is a producer-side Go library. It coordinates a program's own stdout/stderr wrappers via a shared writer mutex and actor, clears a single spinner row before application writes, redraws afterward, and uses newline-aware rendering. It deliberately recommends self-sizing, one-row-only cleanup during terminal resize. An opt-in multi-row cleanup is documented as dangerous because stale TTY width/reflow assumptions can erase real log lines. The commenter recommendation is to tolerate stale spinner fragments rather than risk deleting output.

    NMSh is instead a consumer-side PTY/frontend. We cannot require programs such as Homebrew to use coordinated wrappers, and their stdout/stderr have already been combined into PTY terminal bytes. spinq should not be added as an NMSh dependency or treated as a drop-in fix. Its valuable principles are: data over animation, keep transient progress separate from durable command history where possible, do not erase unowned rows to clean up after resize, and test concurrent stdout/stderr + resizing with real PTYs.

    Source review of NMSh release/v0.18.0: TerminalRenderer.render() already draws its own owned alternate-screen frame by absolute row, diffs changed rows, and can use synchronized output. Thus spinq's own \r + CSI K spinner coordination problem is not automatically an NMSh-chrome problem. The outstanding, separately justified gap in this issue remains interpreting progress control sequences emitted by a child program. AnsiOutputParser handles \r and current-row operations but not upward/absolute row addressing; CommandClassifier deliberately doesn't promote ordinary progress alone into passthrough.

    Acceptance should include: a child streaming a two-row progress animation while interleaving informational stdout and stderr output; resize mid-animation, including rapid narrow/wide oscillation; verify no historical log or command gets erased/reassigned and ephemeral redraw frames do not balloon saved transcript; verify copy/search/folding and reattach behavior; compare ordinary and passthrough modes. Note that if the child itself sends control codes that erase previous content, a frontend cannot necessarily recover the erased logical data from a final screen snapshot; whether raw PTY bytes remain independently archived must be verified rather than assumed.

    The original Reddit thread provides architectural guidance, not proof that the maintainer's earlier Homebrew command failed for that exact reason. Capture the actual command and terminal byte stream before claiming a Homebrew-specific fix.

    No changes to #359-owned TerminalApp.ts until its stabilization checkpoint.

  2. raiseCatError commented on Oct 18, 2026

    @raiseCatError
    OwnerAuthor

    v0.21: #412 adds a bounded live region to AnsiOutputParser. Programs can redraw their own recent rows (CSI A/B/E/F, ESC M/D/E, save/restore, CSI J 0/1, CSI K 0/1/2). The cursor never moves above NMSh's lines or earlier commands, and never more than the PTY's height. Absolute row positioning keeps the old behavior. Tests: 10, including the fixture from this issue run through a real zsh PTY; 9 of the 10 fail on the old parser. The Homebrew/Docker physical checks are in docs/testing/v021-physical-qa.md section 1. Not run yet.

  3. added 2 commits that reference this issue on Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions