Skip to content

fix(default-reporter): erase trailing characters on progress line#12351

Merged
zkochan merged 4 commits into
pnpm:mainfrom
Mayank-Maurya:fix/progress-line-remnants
Jun 17, 2026
Merged

fix(default-reporter): erase trailing characters on progress line#12351
zkochan merged 4 commits into
pnpm:mainfrom
Mayank-Maurya:fix/progress-line-remnants

Conversation

@Mayank-Maurya

@Mayank-Maurya Mayank-Maurya commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

External processes like SSH passphrase prompts can write to the terminal between progress updates. The previous renderer used ansi-diff, which only overwrites the characters it knows changed, so leftover characters from the external output stayed visible on the progress line — e.g. added 0sa':, where sa': is a fragment of Enter passphrase for key '.../.ssh/id_rsa':.

Closes #12350

Summary

The interactive (non-append-only) reporter now redraws the whole frame in place on each update instead of incrementally diffing it:

  • return the cursor to the top-left of the previous frame (ESC[<rows>A followed by a carriage return, so the redraw starts at column 0 even if an external process left the cursor mid-line),
  • erase from there to the end of the display (ESC[0J),
  • reprint the frame — all in a single atomic write, so there is no flicker.

Because the whole region is erased on every frame, any characters an external process wrote in between are cleared. This matches pacquet's Output::Frame rendering (the column-reset hardening was applied to both stacks). The now-unused ansi-diff dependency has been removed.

Test plan

  • Run pnpm install in a project with a git+ssh dependency that triggers a passphrase prompt
  • Verify the progress line displays cleanly without leftover characters
  • pnpm --filter @pnpm/cli.default-reporter test passes

@Mayank-Maurya Mayank-Maurya requested a review from zkochan as a code owner June 12, 2026 11:03
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 12, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (6) 📘 Rule violations (0) 📎 Requirement gaps (0)

Context used

Grey Divider


Action required

1. Redraw starts mid-line 🐞 Bug ≡ Correctness
Description
logUpdate() moves the cursor up using ESC[nA (which preserves the current column) and then
erases/prints, so if any external output advanced the cursor horizontally, the next frame can be
printed starting mid-line and leave remnants/corrupt the display.
Code

cli/default-reporter/src/index.ts[R94-96]

+    const moveToFrameTop = prevRows > 0 ? `\x1b[${prevRows}A` : ''
+    write(`${moveToFrameTop}${ERASE_TO_END_OF_DISPLAY}${view}`)
+    prevRows = countRows(view)
Evidence
The code explicitly targets interleaved external output but moves the cursor with ESC[nA only; since
that preserves the current column, any intervening output that advances the cursor will cause the
erase/print to start from the wrong column.

cli/default-reporter/src/index.ts[85-96]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`logUpdate()` uses `\x1b[${prevRows}A` to move to the top of the prior frame, but that escape does **not** move the cursor to column 0. If an external process writes to the terminal between updates (the scenario this PR addresses), the cursor column can be non-zero and the redraw begins mid-line.
### Issue Context
The reporter appends a newline to allow prompts to appear, and explicitly aims to handle interleaved external output. Cursor-up without column reset breaks that guarantee.
### Fix Focus Areas
- Update cursor positioning to move to column 0 before erasing/printing:
- cli/default-reporter/src/index.ts[85-96]
### Suggested fix
Use an escape that moves up **and** to BOL, e.g. `ESC[nF`, or add an explicit column reset:
- `const moveToFrameTop = prevRows > 0 ? `\x1b[${prevRows}F` : '\r'`
- or `\x1b[${prevRows}A\x1b[0G` (plus `\r` when `prevRows===0`).
Then keep the existing `ERASE_TO_END_OF_DISPLAY` + `view` write.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Erase wipes interleaved prompts 🐞 Bug ☼ Reliability ⭐ New
Description
logUpdate() emits ESC[0J (erase to end of display) from the top of the previous frame before
reprinting, which can delete any interleaved output printed below the frame (including interactive
child-process prompts). If progress continues while a prompt is visible, the prompt text can
disappear/flicker, making the interaction confusing.
Code

cli/default-reporter/src/index.ts[R90-98]

+    // Redraw the whole frame in place: return the cursor to the top-left of the
+    // previous frame, erase everything below it, then reprint. The `\r` resets
+    // the column to 0 (cursor-up alone keeps the column) so the redraw starts
+    // cleanly even when an external process left the cursor mid-line. Doing it
+    // in a single write keeps the redraw atomic (no flicker) and clears any
+    // characters an external process wrote in between.
+    const moveToFrameTop = prevRows > 0 ? `\x1b[${prevRows}A\r` : '\r'
+    write(`${moveToFrameTop}${ERASE_TO_END_OF_DISPLAY}${view}`)
+    prevRows = countRows(view)
Evidence
The reporter now prepends each repaint with an erase-to-end-of-display escape and always appends a
newline specifically to allow prompts to appear; erasing the whole display below the frame top can
therefore remove those prompts/output if updates continue.

cli/default-reporter/src/index.ts[15-20]
cli/default-reporter/src/index.ts[85-98]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`logUpdate()` clears **to end of display** (`\x1b[0J`) before repainting, which can erase output that is *not* owned by the reporter but was printed below the previous frame (e.g. interactive prompts from child processes).

### Issue Context
The code explicitly appends a newline so prompts can appear on a fresh line; clearing the entire display region below the frame top undermines that by potentially wiping the prompt line on subsequent updates.

### Fix Focus Areas
- cli/default-reporter/src/index.ts[90-99]

Potential direction: clear only the reporter’s frame region (e.g., erase-to-end-of-line per printed line and/or clear exactly `prevRows` lines) rather than using an unconditional erase-to-end-of-display.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Row count misses wrapping 🐞 Bug ≡ Correctness ⭐ New
Description
countRows() only counts newline characters, so prevRows is wrong if any frame line soft-wraps (e.g.,
terminal resized narrower than the precomputed outputMaxWidth). The cursor-up + erase sequence can
then move to the wrong position and leave parts of the previous frame uncleared during redraws.
Code

cli/default-reporter/src/index.ts[R105-115]

+// Number of terminal rows a frame occupies. The frame always ends with a
+// newline, so this also equals how far below the frame's top the cursor rests
+// after printing it. Lines are assumed not to soft-wrap, matching how the
+// progress output is width-constrained before it reaches here.
+function countRows (frame: string): number {
+  let rows = 0
+  for (let i = 0; i < frame.length; i++) {
+    if (frame.charCodeAt(i) === 10 /* \n */) rows++
+  }
+  return rows
+}
Evidence
The TS reporter now relies on countRows() for cursor-up positioning but does not account for
soft-wrapping, while outputMaxWidth is derived once from terminal columns; pacquet’s equivalent
implementation explicitly counts wrapped rows, indicating wrapping is a real concern for
correctness.

cli/default-reporter/src/index.ts[49-56]
cli/default-reporter/src/index.ts[105-115]
pacquet/crates/default-reporter/src/lib.rs[163-176]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`countRows()` assumes no soft-wrapping and counts only literal `\n`. If wrapping happens, the reporter’s `prevRows` is underestimated and subsequent redraws can start from the wrong row.

### Issue Context
`outputMaxWidth` is computed once from `proc.stdout.columns` at init time; a later terminal resize (or any unexpected wide line) can cause wrapping even if frames were previously constrained.

### Fix Focus Areas
- cli/default-reporter/src/index.ts[49-56]
- cli/default-reporter/src/index.ts[90-99]
- cli/default-reporter/src/index.ts[105-115]

Potential direction: compute row count like pacquet does—split by `\n`, compute visible width per line (ignoring ANSI), and `div_ceil(columns)` to account for wrapping; also consider re-reading `proc.stdout.columns` on each update.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Full-frame redraw may be heavy 🐞 Bug ➹ Performance
Description
The interactive reporter now clears and rewrites the entire composed frame each update, which can
significantly increase terminal I/O and CPU for large multi-line frames (e.g., lifecycle output
blocks), especially at the default 200ms progress throttle.
Code

cli/default-reporter/src/index.ts[L68-71]

-  const diff = createDiffer({
-    height: proc.stdout.rows,
-    outputMaxWidth,
-  })
Evidence
The interactive mode now writes ${...}${view} directly on every emission. The frame is built by
joining potentially multi-line blocks, including lifecycle script output which is assembled with
join(EOL), so frame size can grow and make each redraw more expensive.

cli/default-reporter/src/index.ts[73-97]
cli/default-reporter/src/mergeOutputs.ts[49-60]
cli/default-reporter/src/reporterForClient/reportLifecycleScripts.ts[141-177]
pnpm/src/reporter/index.ts[19-34]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The reporter now always rewrites the full `view` string on each update. When the composed frame becomes large (many blocks and/or multi-line messages), this increases the amount of data written to the terminal and can reduce responsiveness.
### Issue Context
`mergeOutputs()` builds the full multi-line frame, and some reporters (e.g. lifecycle scripts) can emit multi-line output joined by `EOL`. Redrawing all of it each tick scales poorly.
### Fix Focus Areas
- Consider trimming/capping the rendered frame to a viewport-sized slice before writing and computing `prevRows` from that trimmed content:
- cli/default-reporter/src/index.ts[84-97]
- cli/default-reporter/src/mergeOutputs.ts[49-60]
- cli/default-reporter/src/reporterForClient/reportLifecycleScripts.ts[141-177]
### Suggested fix
Implement a lightweight “viewport” step in `logUpdate()`:
- Split `view` into lines, keep only the last `proc.stdout.rows` (or a small multiple) for interactive mode.
- Recompute `prevRows` from the trimmed frame.
This preserves the ‘clear remnants’ behavior while preventing very large frames from being reprinted each update.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
5. Erase not always emitted 🐞 Bug ≡ Correctness
Description
logUpdate() only injects \x1b[K by replacing literal \n characters in diff.update(view)
output, so any ansi-diff update frame that uses only cursor-movement escapes and overwrites
characters (no \n emitted) will not clear the rest of the line. In those frames, trailing remnants
from external terminal output can still remain visible after progress updates.
Code

cli/default-reporter/src/index.ts[93]

+    write(diff.update(view).toString().replaceAll('\n', `${ERASE_EOL}\n`))
Evidence
The new logic appends erases by rewriting newline characters in diff.update() output. However, the
repository explicitly notes that ansi-diff emits cursor-movement escape sequences for small changes,
which implies some frames can be cursor/character-only; those frames would contain no \n to
replace, so ERASE_EOL would not be emitted in them.

cli/default-reporter/src/index.ts[88-94]
cli/default-reporter/test/cli.ts[19-22]
cli/default-reporter/src/mergeOutputs.ts[69-75]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`logUpdate()` currently appends `ERASE_EOL` only when the *diff output* contains `\n` (`replaceAll('\n', ...)`). If `ansi-diff` produces a cursor-movement-only update for a frame (no newline in the output), then no erase escape is emitted for that frame, and any trailing characters from external output can still remain.
### Issue Context
The repo already documents that `ansi-diff` emits cursor-movement escapes for small changes, implying frames are not always full-line rewrites. The erase logic should therefore not depend solely on `\n` being present in the emitted diff.
### Fix Focus Areas
- cli/default-reporter/src/index.ts[88-94]
### Suggested fix approach
Update the implementation so an `erase-to-end-of-line` is guaranteed to be emitted on every frame for the rendered line(s), not only when `diff.update()` happens to include `\n`. One practical approach is to make the erase sequence itself part of what changes each frame (e.g., inject an equivalent erase code that alternates between `\x1b[K` and `\x1b[0K` per update) so `ansi-diff` is forced to output it even on minimal cursor updates.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

6. Hot-path string allocations 🐞 Bug ➹ Performance
Description
logUpdate() now does a split/join transformation on every rendered frame, which allocates an array
and additional strings per update and can add avoidable overhead when progress updates are frequent.
This is low severity, but it sits directly on the stdout/stderr update path and runs even when
throttling is disabled.
Code

cli/default-reporter/src/index.ts[R88-91]

+    // Erase to end of line on every line so that remnants from external
+    // processes (e.g. SSH passphrase prompts) don't bleed through.
+    const ERASE_EOL = '\x1b[K'
+    write(diff.update(view).split('\n').join(`${ERASE_EOL}\n`))
Evidence
logUpdate is the subscription handler for every emitted frame and writes directly to
stdout/stderr, so its per-call work is on the hot path. Throttling is optional and disabled when
throttleProgress is unset or <= 0, which can increase update frequency and make extra per-frame
allocations more relevant.

cli/default-reporter/src/index.ts[72-92]
cli/default-reporter/src/reporterForClient/index.ts[84-88]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`logUpdate()` runs on every frame and currently uses `split('\n').join(...)`, which creates an intermediate array and additional strings on each update.
### Issue Context
This code runs on the live progress rendering path; progress throttling can be disabled, increasing update frequency.
### Fix Focus Areas
- cli/default-reporter/src/index.ts[83-92]
- cli/default-reporter/src/reporterForClient/index.ts[84-88]
### Suggested fix
- Hoist `ERASE_EOL` out of `logUpdate()` (module-level or closure-level constant).
- Replace `split('\n').join(...)` with a non-array approach, e.g. `updated.replaceAll('\n', `${ERASE_EOL}\n`)` (or `updated.replace(/\n/g, ...)`), to reduce per-update allocations.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Previous review results

Review updated until commit 1401629

Results up to commit 38e1676


🐞 Bugs (4) 📘 Rule violations (0) 📎 Requirement gaps (0)

Context used

Action required
1. Redraw starts mid-line 🐞 Bug ≡ Correctness ⭐ New
Description
logUpdate() moves the cursor up using ESC[nA (which preserves the current column) and then
erases/prints, so if any external output advanced the cursor horizontally, the next frame can be
printed starting mid-line and leave remnants/corrupt the display.
Code

cli/default-reporter/src/index.ts[R94-96]

+    const moveToFrameTop = prevRows > 0 ? `\x1b[${prevRows}A` : ''
+    write(`${moveToFrameTop}${ERASE_TO_END_OF_DISPLAY}${view}`)
+    prevRows = countRows(view)
Evidence
The code explicitly targets interleaved external output but moves the cursor with ESC[nA only; since
that preserves the current column, any intervening output that advances the cursor will cause the
erase/print to start from the wrong column.

cli/default-reporter/src/index.ts[85-96]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`logUpdate()` uses `\x1b[${prevRows}A` to move to the top of the prior frame, but that escape does **not** move the cursor to column 0. If an external process writes to the terminal between updates (the scenario this PR addresses), the cursor column can be non-zero and the redraw begins mid-line.

### Issue Context
The reporter appends a newline to allow prompts to appear, and explicitly aims to handle interleaved external output. Cursor-up without column reset breaks that guarantee.

### Fix Focus Areas
- Update cursor positioning to move to column 0 before erasing/printing:
 - cli/default-reporter/src/index.ts[85-96]

### Suggested fix
Use an escape that moves up **and** to BOL, e.g. `ESC[nF`, or add an explicit column reset:
- `const moveToFrameTop = prevRows > 0 ? `\x1b[${prevRows}F` : '\r'`
- or `\x1b[${prevRows}A\x1b[0G` (plus `\r` when `prevRows===0`).
Then keep the existing `ERASE_TO_END_OF_DISPLAY` + `view` write.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
2. Full-frame redraw may be heavy 🐞 Bug ➹ Performance ⭐ New
Description
The interactive reporter now clears and rewrites the entire composed frame each update, which can
significantly increase terminal I/O and CPU for large multi-line frames (e.g., lifecycle output
blocks), especially at the default 200ms progress throttle.
Code

cli/default-reporter/src/index.ts[L68-71]

-  const diff = createDiffer({
-    height: proc.stdout.rows,
-    outputMaxWidth,
-  })
Evidence
The interactive mode now writes ${...}${view} directly on every emission. The frame is built by
joining potentially multi-line blocks, including lifecycle script output which is assembled with
join(EOL), so frame size can grow and make each redraw more expensive.

cli/default-reporter/src/index.ts[73-97]
cli/default-reporter/src/mergeOutputs.ts[49-60]
cli/default-reporter/src/reporterForClient/reportLifecycleScripts.ts[141-177]
pnpm/src/reporter/index.ts[19-34]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The reporter now always rewrites the full `view` string on each update. When the composed frame becomes large (many blocks and/or multi-line messages), this increases the amount of data written to the terminal and can reduce responsiveness.

### Issue Context
`mergeOutputs()` builds the full multi-line frame, and some reporters (e.g. lifecycle scripts) can emit multi-line output joined by `EOL`. Redrawing all of it each tick scales poorly.

### Fix Focus Areas
- Consider trimming/capping the rendered frame to a viewport-sized slice before writing and computing `prevRows` from that trimmed content:
 - cli/default-reporter/src/index.ts[84-97]
 - cli/default-reporter/src/mergeOutputs.ts[49-60]
 - cli/default-reporter/src/reporterForClient/reportLifecycleScripts.ts[141-177]

### Suggested fix
Implement a lightweight “viewport” step in `logUpdate()`:
- Split `view` into lines, keep only the last `proc.stdout.rows` (or a small multiple) for interactive mode.
- Recompute `prevRows` from the trimmed frame.
This preserves the ‘clear remnants’ behavior while preventing very large frames from being reprinted each update.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Erase not always emitted 🐞 Bug ≡ Correctness
Description
logUpdate() only injects \x1b[K by replacing literal \n characters in diff.update(view)
output, so any ansi-diff update frame that uses only cursor-movement escapes and overwrites
characters (no \n emitted) will not clear the rest of the line. In those frames, trailing remnants
from external terminal output can still remain visible after progress updates.
Code

cli/default-reporter/src/index.ts[93]

+    write(diff.update(view).toString().replaceAll('\n', `${ERASE_EOL}\n`))
Evidence
The new logic appends erases by rewriting newline characters in diff.update() output. However, the
repository explicitly notes that ansi-diff emits cursor-movement escape sequences for small changes,
which implies some frames can be cursor/character-only; those frames would contain no \n to
replace, so ERASE_EOL would not be emitted in them.

cli/default-reporter/src/index.ts[88-94]
cli/default-reporter/test/cli.ts[19-22]
cli/default-reporter/src/mergeOutputs.ts[69-75]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`logUpdate()` currently appends `ERASE_EOL` only when the *diff output* contains `\n` (`replaceAll('\n', ...)`). If `ansi-diff` produces a cursor-movement-only update for a frame (no newline in the output), then no erase escape is emitted for that frame, and any trailing characters from external output can still remain.
### Issue Context
The repo already documents that `ansi-diff` emits cursor-movement escapes for small changes, implying frames are not always full-line rewrites. The erase logic should therefore not depend solely on `\n` being present in the emitted diff.
### Fix Focus Areas
- cli/default-reporter/src/index.ts[88-94]
### Suggested fix approach
Update the implementation so an `erase-to-end-of-line` is guaranteed to be emitted on every frame for the rendered line(s), not only when `diff.update()` happens to include `\n`. One practical approach is to make the erase sequence itself part of what changes each frame (e.g., inject an equivalent erase code that alternates between `\x1b[K` and `\x1b[0K` per update) so `ansi-diff` is forced to output it even on minimal cursor updates.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational
4. Hot-path string allocations 🐞 Bug ➹ Performance
Description
logUpdate() now does a split/join transformation on every rendered frame, which allocates an array
and additional strings per update and can add avoidable overhead when progress updates are frequent.
This is low severity, but it sits directly on the stdout/stderr update path and runs even when
throttling is disabled.
Code

cli/default-reporter/src/index.ts[R88-91]

+    // Erase to end of line on every line so that remnants from external
+    // processes (e.g. SSH passphrase prompts) don't bleed through.
+    const ERASE_EOL = '\x1b[K'
+    write(diff.update(view).split('\n').join(`${ERASE_EOL}\n`))
Evidence
logUpdate is the subscription handler for every emitted frame and writes directly to
stdout/stderr, so its per-call work is on the hot path. Throttling is optional and disabled when
throttleProgress is unset or <= 0, which can increase update frequency and make extra per-frame
allocations more relevant.

cli/default-reporter/src/index.ts[72-92]
cli/default-reporter/src/reporterForClient/index.ts[84-88]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`logUpdate()` runs on every frame and currently uses `split('\n').join(...)`, which creates an intermediate array and additional strings on each update.
### Issue Context
This code runs on the live progress rendering path; progress throttling can be disabled, increasing update frequency.
### Fix Focus Areas
- cli/default-reporter/src/index.ts[83-92]
- cli/default-reporter/src/reporterForClient/index.ts[84-88]
### Suggested fix
- Hoist `ERASE_EOL` out of `logUpdate()` (module-level or closure-level constant).
- Replace `split('\n').join(...)` with a non-array approach, e.g. `updated.replaceAll('\n', `${ERASE_EOL}\n`)` (or `updated.replace(/\n/g, ...)`), to reduce per-update allocations.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit e3f1c00


🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0)

Context used

Remediation recommended
1. Erase not always emitted 🐞 Bug ≡ Correctness ⭐ New
Description
logUpdate() only injects \x1b[K by replacing literal \n characters in diff.update(view)
output, so any ansi-diff update frame that uses only cursor-movement escapes and overwrites
characters (no \n emitted) will not clear the rest of the line. In those frames, trailing remnants
from external terminal output can still remain visible after progress updates.
Code

cli/default-reporter/src/index.ts[93]

+    write(diff.update(view).toString().replaceAll('\n', `${ERASE_EOL}\n`))
Evidence
The new logic appends erases by rewriting newline characters in diff.update() output. However, the
repository explicitly notes that ansi-diff emits cursor-movement escape sequences for small changes,
which implies some frames can be cursor/character-only; those frames would contain no \n to
replace, so ERASE_EOL would not be emitted in them.

cli/default-reporter/src/index.ts[88-94]
cli/default-reporter/test/cli.ts[19-22]
cli/default-reporter/src/mergeOutputs.ts[69-75]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`logUpdate()` currently appends `ERASE_EOL` only when the *diff output* contains `\n` (`replaceAll('\n', ...)`). If `ansi-diff` produces a cursor-movement-only update for a frame (no newline in the output), then no erase escape is emitted for that frame, and any trailing characters from external output can still remain.

### Issue Context
The repo already documents that `ansi-diff` emits cursor-movement escapes for small changes, implying frames are not always full-line rewrites. The erase logic should therefore not depend solely on `\n` being present in the emitted diff.

### Fix Focus Areas
- cli/default-reporter/src/index.ts[88-94]

### Suggested fix approach
Update the implementation so an `erase-to-end-of-line` is guaranteed to be emitted on every frame for the rendered line(s), not only when `diff.update()` happens to include `\n`. One practical approach is to make the erase sequence itself part of what changes each frame (e.g., inject an equivalent erase code that alternates between `\x1b[K` and `\x1b[0K` per update) so `ansi-diff` is forced to output it even on minimal cursor updates.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational
2. Hot-path string allocations 🐞 Bug ➹ Performance
Description
logUpdate() now does a split/join transformation on every rendered frame, which allocates an array
and additional strings per update and can add avoidable overhead when progress updates are frequent.
This is low severity, but it sits directly on the stdout/stderr update path and runs even when
throttling is disabled.
Code

cli/default-reporter/src/index.ts[R88-91]

+    // Erase to end of line on every line so that remnants from external
+    // processes (e.g. SSH passphrase prompts) don't bleed through.
+    const ERASE_EOL = '\x1b[K'
+    write(diff.update(view).split('\n').join(`${ERASE_EOL}\n`))
Evidence
logUpdate is the subscription handler for every emitted frame and writes directly to
stdout/stderr, so its per-call work is on the hot path. Throttling is optional and disabled when
throttleProgress is unset or <= 0, which can increase update frequency and make extra per-frame
allocations more relevant.

cli/default-reporter/src/index.ts[72-92]
cli/default-reporter/src/reporterForClient/index.ts[84-88]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`logUpdate()` runs on every frame and currently uses `split('\n').join(...)`, which creates an intermediate array and additional strings on each update.
### Issue Context
This code runs on the live progress rendering path; progress throttling can be disabled, increasing update frequency.
### Fix Focus Areas
- cli/default-reporter/src/index.ts[83-92]
- cli/default-reporter/src/reporterForClient/index.ts[84-88]
### Suggested fix
- Hoist `ERASE_EOL` out of `logUpdate()` (module-level or closure-level constant).
- Replace `split('\n').join(...)` with a non-array approach, e.g. `updated.replaceAll('\n', `${ERASE_EOL}\n`)` (or `updated.replace(/\n/g, ...)`), to reduce per-update allocations.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit 6712792


🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0)

Context used

Informational
1. Hot-path string allocations 🐞 Bug ➹ Performance
Description
logUpdate() now does a split/join transformation on every rendered frame, which allocates an array
and additional strings per update and can add avoidable overhead when progress updates are frequent.
This is low severity, but it sits directly on the stdout/stderr update path and runs even when
throttling is disabled.
Code

cli/default-reporter/src/index.ts[R88-91]

+    // Erase to end of line on every line so that remnants from external
+    // processes (e.g. SSH passphrase prompts) don't bleed through.
+    const ERASE_EOL = '\x1b[K'
+    write(diff.update(view).split('\n').join(`${ERASE_EOL}\n`))
Evidence
logUpdate is the subscription handler for every emitted frame and writes directly to
stdout/stderr, so its per-call work is on the hot path. Throttling is optional and disabled when
throttleProgress is unset or <= 0, which can increase update frequency and make extra per-frame
allocations more relevant.

cli/default-reporter/src/index.ts[72-92]
cli/default-reporter/src/reporterForClient/index.ts[84-88]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`logUpdate()` runs on every frame and currently uses `split('\n').join(...)`, which creates an intermediate array and additional strings on each update.
### Issue Context
This code runs on the live progress rendering path; progress throttling can be disabled, increasing update frequency.
### Fix Focus Areas
- cli/default-reporter/src/index.ts[83-92]
- cli/default-reporter/src/reporterForClient/index.ts[84-88]
### Suggested fix
- Hoist `ERASE_EOL` out of `logUpdate()` (module-level or closure-level constant).
- Replace `split('\n').join(...)` with a non-array approach, e.g. `updated.replaceAll('\n', `${ERASE_EOL}\n`)` (or `updated.replace(/\n/g, ...)`), to reduce per-update allocations.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit 79944b1


🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0)

Context used

Informational
1. Hot-path string allocations 🐞 Bug ➹ Performance
Description
logUpdate() now does a split/join transformation on every rendered frame, which allocates an array
and additional strings per update and can add avoidable overhead when progress updates are frequent.
This is low severity, but it sits directly on the stdout/stderr update path and runs even when
throttling is disabled.
Code

cli/default-reporter/src/index.ts[R88-91]

+    // Erase to end of line on every line so that remnants from external
+    // processes (e.g. SSH passphrase prompts) don't bleed through.
+    const ERASE_EOL = '\x1b[K'
+    write(diff.update(view).split('\n').join(`${ERASE_EOL}\n`))
Evidence
logUpdate is the subscription handler for every emitted frame and writes directly to
stdout/stderr, so its per-call work is on the hot path. Throttling is optional and disabled when
throttleProgress is unset or <= 0, which can increase update frequency and make extra per-frame
allocations more relevant.

cli/default-reporter/src/index.ts[72-92]
cli/default-reporter/src/reporterForClient/index.ts[84-88]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`logUpdate()` runs on every frame and currently uses `split('\n').join(...)`, which creates an intermediate array and additional strings on each update.

### Issue Context
This code runs on the live progress rendering path; progress throttling can be disabled, increasing update frequency.

### Fix Focus Areas
- cli/default-reporter/src/index.ts[83-92]
- cli/default-reporter/src/reporterForClient/index.ts[84-88]

### Suggested fix
- Hoist `ERASE_EOL` out of `logUpdate()` (module-level or closure-level constant).
- Replace `split('\n').join(...)` with a non-array approach, e.g. `updated.replaceAll('\n', `${ERASE_EOL}\n`)` (or `updated.replace(/\n/g, ...)`), to reduce per-update allocations.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Qodo Logo

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The default reporter's progress renderer is refactored to eliminate ansi-diff and replace differential updates with full-frame terminal clearing. The new approach tracks the previous frame's row count, moves the cursor back to the top of that frame, sends an erase-to-end-of-display escape sequence, and reprints the entire updated view, ensuring external process output such as SSH passphrase prompts does not leave visible remnants. The Rust reporter receives a complementary carriage-return reset to ensure cursor column positioning is consistent before frame rewrites.

Changes

Progress Reporter Terminal Clearing Refactor

Layer / File(s) Summary
Full-frame terminal clearing in TypeScript reporter
cli/default-reporter/src/index.ts
The logUpdate function removes the ansi-diff import and createDiffer initialization. It adds an ERASE_TO_END_OF_DISPLAY escape constant (\x1b[0J) and reimplements progress updates to ensure a trailing newline, move the cursor back by the previous frame's row count, clear terminal output to end of display, reprint the entire frame, and track the new row count via a new countRows() helper.
Cursor column reset in Rust reporter
pacquet/crates/default-reporter/src/lib.rs
The frame output path now sends a carriage return (\r) before clearing and writing the updated frame, resetting cursor column position to 0 and ensuring clean redraws after external process output leaves the cursor mid-line.
Remove ansi-diff from dependencies
cli/default-reporter/package.json, pnpm-workspace.yaml
The ansi-diff entry is removed from the package dependencies and the workspace catalog version map.
Clarify test and documentation comments
cli/default-reporter/src/reporterForClient/reportLockfileVerification.ts, cli/default-reporter/test/cli.ts, cli/default-reporter/test/reportingLockfileVerification.ts
Comments are updated to reflect the interactive reporter's full-frame redraw behavior with cursor repositioning and terminal clearing instead of ansi-diff mode references.
Release changeset
.changeset/erase-progress-line-remnants.md
Documents patch version bumps for @pnpm/cli.default-reporter and pnpm and describes the fix for progress-line remnants caused by external terminal output.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

product: pacquet

Suggested reviewers

  • zkochan
  • KSXGitHub
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title follows Conventional Commits format with 'fix:' prefix and clearly describes the main change: erasing trailing characters on the progress line to fix remnants from external process output.
Linked Issues check ✅ Passed The PR addresses all coding requirements from #12350: removes ansi-diff dependency, implements cursor positioning fix to erase trailing characters on progress line, and applies the fix to both TypeScript and Rust reporters.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing progress line remnants: modifications to default-reporter, removal of ansi-diff dependency, cursor reset implementation, and related test/comment updates are all within the stated objectives.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Fix progress reporter by clearing trailing characters after ansi-diff updates
🐞 Bug fix 🕐 10-20 Minutes

Grey Divider

Walkthroughs

Description
• Clear residual characters after each progress-line render to avoid prompt remnants.
• Append ANSI erase-to-end-of-line (\x1b[K) on every line emitted by ansi-diff.
Diagram
graph TD
  A["Default reporter"] --> B["ansi-diff update"] --> C["Append \"erase EOL\""] --> D["Terminal write"]
  subgraph Legend
    direction LR
    _mod["Module"] ~~~ _io["Terminal I/O"]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use readline/TTY clearLine for the active line
  • ➕ Leverages Node’s terminal APIs instead of manual ANSI injection
  • ➕ Potentially clearer intent for single-line progress output
  • ➖ Harder when output can contain multiple lines
  • ➖ Requires TTY detection/behavior differences across environments
2. Configure/replace diffing strategy to always reprint full lines
  • ➕ Avoids per-line string post-processing
  • ➕ May reduce edge cases around newline splitting
  • ➖ Higher output volume and more flicker
  • ➖ May require deeper changes or swapping libraries (larger PR)

Recommendation: Current approach is the smallest, most targeted fix: appending \x1b[K per line directly addresses the observed artifact from interleaved external prompts while keeping ansi-diff behavior intact. Consider readline.clearLine only if the reporter is guaranteed to be single-line and always attached to a TTY.

Grey Divider

File Changes

Bug fix (1)
index.ts Erase to end-of-line after each ansi-diff progress line +4/-1

Erase to end-of-line after each ansi-diff progress line

• Injects an ANSI erase-to-end-of-line (\x1b[K) after each line of the ansi-diff output before writing to the terminal. This ensures any trailing characters left by interleaved external prompts (e.g., SSH passphrase requests) are cleared on subsequent progress renders.

cli/default-reporter/src/index.ts


Grey Divider

Qodo Logo

@zkochan zkochan force-pushed the fix/progress-line-remnants branch from 79944b1 to 6712792 Compare June 16, 2026 21:53
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 6712792

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/default-reporter/src/index.ts`:
- Around line 88-91: Create a new changeset entry in the `.changeset` directory
with a filename containing random words and the `.md` extension. The changeset
should include frontmatter listing both "`@pnpm/default-reporter`" and "pnpm" with
"patch" severity level, followed by a description explaining that the fix
addresses progress line remnants from external processes by erasing to end of
line on each progress update. This follows the AGENTS.md guidelines requiring
changeset entries for bug fixes affecting the release behavior of the
default-reporter package.
- Around line 88-91: The Rust DefaultReporter append-only mode (the code path
that writes lines without TTY framing) lacks ANSI erase-to-end-of-line
protection that was just added to the TypeScript implementation. In the
append-only write path of DefaultReporter, modify the line writing logic to
append the ANSI erase-to-end-of-line sequence (\x1b[K) before the newline
character when outputting each line, matching the TypeScript pattern of adding
this protection to every output line to prevent external process remnants (like
SSH prompts) from bleeding through.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 02c7f011-2972-41ad-bf28-fd923a1e6125

📥 Commits

Reviewing files that changed from the base of the PR and between 79944b1 and 6712792.

📒 Files selected for processing (1)
  • cli/default-reporter/src/index.ts

Comment thread cli/default-reporter/src/index.ts Outdated
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit e3f1c00

Comment thread cli/default-reporter/src/index.ts Outdated
// An example of such prompt may be seen by running: pnpm update --interactive
if (!view.endsWith(EOL)) view += EOL
write(diff.update(view))
write(diff.update(view).toString().replaceAll('\n', `${ERASE_EOL}\n`))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Erase not always emitted 🐞 Bug ≡ Correctness

logUpdate() only injects \x1b[K by replacing literal \n characters in diff.update(view)
output, so any ansi-diff update frame that uses only cursor-movement escapes and overwrites
characters (no \n emitted) will not clear the rest of the line. In those frames, trailing remnants
from external terminal output can still remain visible after progress updates.
Agent Prompt
### Issue description
`logUpdate()` currently appends `ERASE_EOL` only when the *diff output* contains `\n` (`replaceAll('\n', ...)`). If `ansi-diff` produces a cursor-movement-only update for a frame (no newline in the output), then no erase escape is emitted for that frame, and any trailing characters from external output can still remain.

### Issue Context
The repo already documents that `ansi-diff` emits cursor-movement escapes for small changes, implying frames are not always full-line rewrites. The erase logic should therefore not depend solely on `\n` being present in the emitted diff.

### Fix Focus Areas
- cli/default-reporter/src/index.ts[88-94]

### Suggested fix approach
Update the implementation so an `erase-to-end-of-line` is guaranteed to be emitted on every frame for the rendered line(s), not only when `diff.update()` happens to include `\n`. One practical approach is to make the erase sequence itself part of what changes each frame (e.g., inject an equivalent erase code that alternates between `\x1b[K` and `\x1b[0K` per update) so `ansi-diff` is forced to output it even on minimal cursor updates.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The reporter no longer post-processes an ansi-diff; it now redraws the full frame in place (return to frame top, ESC[0J, reprint) on every update, so the erase is unconditional and no longer depends on a newline appearing in the diff output. ansi-diff has been removed entirely.


Written by an agent (Claude Code, claude-opus-4-8).

Mayank-Maurya and others added 3 commits June 17, 2026 01:45
External processes like SSH passphrase prompts can write to the
terminal between progress updates. Because ansi-diff only overwrites
changed characters without clearing the rest of the line, remnants of
the longer prompt remain visible (e.g. "added 0sa':").

Append an ANSI "erase to end of line" escape after each line of the
diff output so leftover characters are always cleared.

Closes pnpm#12350

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…emnants

ansi-diff's update() returns a Buffer, which has no string methods, so the
erase-to-end-of-line transform threw at runtime. Convert to a string first,
hoist the escape constant, and add a changeset.
Replace the ansi-diff incremental renderer with a full-frame redraw that
moves the cursor to the top of the previous frame, erases to the end of the
display, and reprints. This guarantees characters written by external
processes (e.g. SSH passphrase prompts) between updates are cleared on every
frame, not only on frames where the diff happened to emit a newline. Matches
pacquet's Output::Frame rendering.
@zkochan zkochan force-pushed the fix/progress-line-remnants branch from e3f1c00 to 38e1676 Compare June 16, 2026 23:49
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 38e1676

Comment thread cli/default-reporter/src/index.ts Outdated
Comment on lines +94 to +96
const moveToFrameTop = prevRows > 0 ? `\x1b[${prevRows}A` : ''
write(`${moveToFrameTop}${ERASE_TO_END_OF_DISPLAY}${view}`)
prevRows = countRows(view)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Redraw starts mid-line 🐞 Bug ≡ Correctness

logUpdate() moves the cursor up using ESC[nA (which preserves the current column) and then
erases/prints, so if any external output advanced the cursor horizontally, the next frame can be
printed starting mid-line and leave remnants/corrupt the display.
Agent Prompt
### Issue description
`logUpdate()` uses `\x1b[${prevRows}A` to move to the top of the prior frame, but that escape does **not** move the cursor to column 0. If an external process writes to the terminal between updates (the scenario this PR addresses), the cursor column can be non-zero and the redraw begins mid-line.

### Issue Context
The reporter appends a newline to allow prompts to appear, and explicitly aims to handle interleaved external output. Cursor-up without column reset breaks that guarantee.

### Fix Focus Areas
- Update cursor positioning to move to column 0 before erasing/printing:
  - cli/default-reporter/src/index.ts[85-96]

### Suggested fix
Use an escape that moves up **and** to BOL, e.g. `ESC[nF`, or add an explicit column reset:
- `const moveToFrameTop = prevRows > 0 ? `\x1b[${prevRows}F` : '\r'`
- or `\x1b[${prevRows}A\x1b[0G` (plus `\r` when `prevRows===0`).
Then keep the existing `ERASE_TO_END_OF_DISPLAY` + `view` write.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — the cursor-up is now followed by a carriage return (ESC[${prevRows}A\r, or \r on the first frame) so the redraw always starts at column 0 even if an external process left the cursor mid-line. Mirrored in pacquet's Output::Frame path for parity.


Written by an agent (Claude Code, claude-opus-4-8).

subscription.unsubscribe()
}
}
const diff = createDiffer({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Full-frame redraw may be heavy 🐞 Bug ➹ Performance

The interactive reporter now clears and rewrites the entire composed frame each update, which can
significantly increase terminal I/O and CPU for large multi-line frames (e.g., lifecycle output
blocks), especially at the default 200ms progress throttle.
Agent Prompt
### Issue description
The reporter now always rewrites the full `view` string on each update. When the composed frame becomes large (many blocks and/or multi-line messages), this increases the amount of data written to the terminal and can reduce responsiveness.

### Issue Context
`mergeOutputs()` builds the full multi-line frame, and some reporters (e.g. lifecycle scripts) can emit multi-line output joined by `EOL`. Redrawing all of it each tick scales poorly.

### Fix Focus Areas
- Consider trimming/capping the rendered frame to a viewport-sized slice before writing and computing `prevRows` from that trimmed content:
  - cli/default-reporter/src/index.ts[84-97]
  - cli/default-reporter/src/mergeOutputs.ts[49-60]
  - cli/default-reporter/src/reporterForClient/reportLifecycleScripts.ts[141-177]

### Suggested fix
Implement a lightweight “viewport” step in `logUpdate()`:
- Split `view` into lines, keep only the last `proc.stdout.rows` (or a small multiple) for interactive mode.
- Recompute `prevRows` from the trimmed frame.
This preserves the ‘clear remnants’ behavior while preventing very large frames from being reprinted each update.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this. Lifecycle output is already capped (10 lines per script block, the rest collapsed) and the composed frame is bounded, so at the default 200ms progress throttle the full-frame redraw is negligible terminal I/O — it is exactly what pacquet's Output::Frame path does. Trimming to a viewport would hide content that the previous renderer displayed in full, which is a worse regression than the extra bytes.


Written by an agent (Claude Code, claude-opus-4-8).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/default-reporter/src/index.ts`:
- Around line 84-96: The `prevRows` variable in the `logUpdate` function only
tracks the previous reporter frame height but fails to account for newlines
printed by external processes between updates, causing the cursor rewind
calculation on line 94 (the `moveToFrameTop` variable) to move to the wrong row.
Adjust the cursor position tracking to account for any additional newlines that
may have been printed by external processes since the last frame update,
ensuring the `moveToFrameTop` calculation correctly identifies the actual frame
top before clearing and reprinting.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 08322302-4412-4092-8991-6d5b9e2be1fe

📥 Commits

Reviewing files that changed from the base of the PR and between e3f1c00 and 38e1676.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !pnpm-lock.yaml
📒 Files selected for processing (7)
  • .changeset/erase-progress-line-remnants.md
  • cli/default-reporter/package.json
  • cli/default-reporter/src/index.ts
  • cli/default-reporter/src/reporterForClient/reportLockfileVerification.ts
  • cli/default-reporter/test/cli.ts
  • cli/default-reporter/test/reportingLockfileVerification.ts
  • pnpm-workspace.yaml
💤 Files with no reviewable changes (1)
  • cli/default-reporter/package.json
✅ Files skipped from review due to trivial changes (3)
  • cli/default-reporter/test/cli.ts
  • cli/default-reporter/test/reportingLockfileVerification.ts
  • cli/default-reporter/src/reporterForClient/reportLockfileVerification.ts

Comment thread cli/default-reporter/src/index.ts
Cursor-up (ESC[nA) preserves the column, so if an external process left the
cursor mid-line the in-place redraw would start mid-line and leave remnants.
Emit a carriage return so the redraw always starts at column 0. Applied to
both the TypeScript reporter and pacquet's Output::Frame path.
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 17, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 1401629

Comment on lines +90 to +98
// Redraw the whole frame in place: return the cursor to the top-left of the
// previous frame, erase everything below it, then reprint. The `\r` resets
// the column to 0 (cursor-up alone keeps the column) so the redraw starts
// cleanly even when an external process left the cursor mid-line. Doing it
// in a single write keeps the redraw atomic (no flicker) and clears any
// characters an external process wrote in between.
const moveToFrameTop = prevRows > 0 ? `\x1b[${prevRows}A\r` : '\r'
write(`${moveToFrameTop}${ERASE_TO_END_OF_DISPLAY}${view}`)
prevRows = countRows(view)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Erase wipes interleaved prompts 🐞 Bug ☼ Reliability

logUpdate() emits ESC[0J (erase to end of display) from the top of the previous frame before
reprinting, which can delete any interleaved output printed below the frame (including interactive
child-process prompts). If progress continues while a prompt is visible, the prompt text can
disappear/flicker, making the interaction confusing.
Agent Prompt
### Issue description
`logUpdate()` clears **to end of display** (`\x1b[0J`) before repainting, which can erase output that is *not* owned by the reporter but was printed below the previous frame (e.g. interactive prompts from child processes).

### Issue Context
The code explicitly appends a newline so prompts can appear on a fresh line; clearing the entire display region below the frame top undermines that by potentially wiping the prompt line on subsequent updates.

### Fix Focus Areas
- cli/default-reporter/src/index.ts[90-99]

Potential direction: clear only the reporter’s frame region (e.g., erase-to-end-of-line per printed line and/or clear exactly `prevRows` lines) rather than using an unconditional erase-to-end-of-display.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged as a deliberate tradeoff. ESC[0J from the frame top clears the region below it, which is what makes remnant clearing reliable, but it also clears an interactive prompt printed below the frame on the next tick. This matches pacquet's Output::Frame behavior. The #12350 symptom is a remnant landing on the progress line itself, which the redraw clears. A more surgical per-line erase (clearing only the frame's own lines) would preserve a concurrently-visible prompt — flagging for the maintainer in case prompt preservation should take priority over the simpler full-region erase.


Written by an agent (Claude Code, claude-opus-4-8).

Comment on lines +105 to +115
// Number of terminal rows a frame occupies. The frame always ends with a
// newline, so this also equals how far below the frame's top the cursor rests
// after printing it. Lines are assumed not to soft-wrap, matching how the
// progress output is width-constrained before it reaches here.
function countRows (frame: string): number {
let rows = 0
for (let i = 0; i < frame.length; i++) {
if (frame.charCodeAt(i) === 10 /* \n */) rows++
}
return rows
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Row count misses wrapping 🐞 Bug ≡ Correctness

countRows() only counts newline characters, so prevRows is wrong if any frame line soft-wraps (e.g.,
terminal resized narrower than the precomputed outputMaxWidth). The cursor-up + erase sequence can
then move to the wrong position and leave parts of the previous frame uncleared during redraws.
Agent Prompt
### Issue description
`countRows()` assumes no soft-wrapping and counts only literal `\n`. If wrapping happens, the reporter’s `prevRows` is underestimated and subsequent redraws can start from the wrong row.

### Issue Context
`outputMaxWidth` is computed once from `proc.stdout.columns` at init time; a later terminal resize (or any unexpected wide line) can cause wrapping even if frames were previously constrained.

### Fix Focus Areas
- cli/default-reporter/src/index.ts[49-56]
- cli/default-reporter/src/index.ts[90-99]
- cli/default-reporter/src/index.ts[105-115]

Potential direction: compute row count like pacquet does—split by `\n`, compute visible width per line (ignoring ANSI), and `div_ceil(columns)` to account for wrapping; also consider re-reading `proc.stdout.columns` on each update.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This matches the prior behavior exactly: the ansi-diff renderer was constructed with width: Infinity (it ignores the outputMaxWidth key it was passed), so it also assumed one row per line and never accounted for soft-wrap. Output is constrained to outputMaxWidth before it reaches the reporter, and a mid-install narrower resize is a pre-existing edge shared with the old renderer — not a regression. pacquet's count_rows is marginally more correct (it does div_ceil on visible width); porting wrapping-aware counting to the TS side is a reasonable separate enhancement.


Written by an agent (Claude Code, claude-opus-4-8).

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 88.09%. Comparing base (531f2a3) to head (1401629).

Files with missing lines Patch % Lines
pacquet/crates/default-reporter/src/lib.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main   #12351   +/-   ##
=======================================
  Coverage   88.08%   88.09%           
=======================================
  Files         310      310           
  Lines       41863    41864    +1     
=======================================
+ Hits        36874    36878    +4     
+ Misses       4989     4986    -3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD.

Scenario: Isolated linker: fresh restore, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 3.890 ± 0.120 3.766 4.120 2.06 ± 0.15
pacquet@main 3.915 ± 0.120 3.807 4.133 2.07 ± 0.15
pnpr@HEAD 1.899 ± 0.115 1.730 2.076 1.00 ± 0.09
pnpr@main 1.890 ± 0.127 1.779 2.187 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 3.8904105469200005,
      "stddev": 0.12022044209322012,
      "median": 3.85501627062,
      "user": 4.293041239999999,
      "system": 2.1820932800000006,
      "min": 3.76578862662,
      "max": 4.11990437462,
      "times": [
        4.11990437462,
        3.83923882062,
        3.89354792562,
        3.77899836962,
        3.78918213662,
        3.9716407626200003,
        3.87079372062,
        4.04969112162,
        3.8253196106200003,
        3.76578862662
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.9148154376199997,
      "stddev": 0.11967551957984189,
      "median": 3.86396855162,
      "user": 4.26325544,
      "system": 2.2018021800000005,
      "min": 3.80712250262,
      "max": 4.13299926662,
      "times": [
        3.82911473762,
        3.8492987806200003,
        3.86827991362,
        4.046661783619999,
        4.06851988862,
        4.13299926662,
        3.81495357862,
        3.8715467346200003,
        3.85965718962,
        3.80712250262
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 1.89860215222,
      "stddev": 0.11536579971501949,
      "median": 1.9192735926200002,
      "user": 2.88915664,
      "system": 1.8256597799999998,
      "min": 1.7304530856200002,
      "max": 2.07584057362,
      "times": [
        1.78517213762,
        2.0351902706200002,
        1.9227268196200002,
        1.9263243406200001,
        1.7810537636200001,
        1.98727053062,
        2.07584057362,
        1.9158203656200001,
        1.8261696346200003,
        1.7304530856200002
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 1.8895485945200001,
      "stddev": 0.1269852080805147,
      "median": 1.84380697912,
      "user": 2.8676786400000003,
      "system": 1.8357497799999998,
      "min": 1.7787421086200002,
      "max": 2.18730347162,
      "times": [
        1.84877929062,
        2.04277445462,
        1.8147759796200003,
        1.80386913162,
        1.83496976762,
        1.83883466762,
        1.7787421086200002,
        2.18730347162,
        1.87180299962,
        1.8736340736200001
      ]
    }
  ]
}

Scenario: Isolated linker: fresh restore, hot cache + hot store

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 453.5 ± 8.2 444.7 474.4 1.00
pacquet@main 463.8 ± 7.1 455.9 475.7 1.02 ± 0.02
pnpr@HEAD 534.1 ± 84.3 468.4 705.5 1.18 ± 0.19
pnpr@main 482.7 ± 6.3 473.3 495.5 1.06 ± 0.02
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.45353497502,
      "stddev": 0.00819506233437159,
      "median": 0.45258697202000003,
      "user": 0.36077667999999996,
      "system": 0.7822776200000001,
      "min": 0.44465520252,
      "max": 0.47437548152000003,
      "times": [
        0.45618820352,
        0.44912138252,
        0.45253791052000003,
        0.45403450852,
        0.47437548152000003,
        0.45554813652000004,
        0.44837740352,
        0.44465520252,
        0.44787548752,
        0.45263603352000004
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.46384420312,
      "stddev": 0.007067563647856172,
      "median": 0.46200796802,
      "user": 0.35793227999999994,
      "system": 0.79743052,
      "min": 0.45592325452,
      "max": 0.47574154852,
      "times": [
        0.46069618252,
        0.47455483652,
        0.45984875652,
        0.45786483552,
        0.46917142452,
        0.46331975352,
        0.45592325452,
        0.47574154852,
        0.46382285852,
        0.45749858052000003
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.5340756347200001,
      "stddev": 0.08432639169755698,
      "median": 0.49017467252,
      "user": 0.37177198,
      "system": 0.8103482200000001,
      "min": 0.46843831052,
      "max": 0.7054554835200001,
      "times": [
        0.47806697052,
        0.47962029652,
        0.47807914952,
        0.46843831052,
        0.49876057052,
        0.5043596585200001,
        0.6356858925200001,
        0.7054554835200001,
        0.6107012405200001,
        0.48158877452000004
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.48271868642,
      "stddev": 0.006337159487295997,
      "median": 0.48243588552000005,
      "user": 0.36083148,
      "system": 0.8071610199999999,
      "min": 0.47334577752,
      "max": 0.49551549752,
      "times": [
        0.48259788852,
        0.47561257652,
        0.48338943452,
        0.48226729652,
        0.47334577752,
        0.48227388252000003,
        0.48606035852,
        0.47821125052,
        0.48791290152,
        0.49551549752
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.021 ± 0.045 3.946 4.106 2.00 ± 0.16
pacquet@main 4.003 ± 0.032 3.942 4.047 1.99 ± 0.16
pnpr@HEAD 2.007 ± 0.162 1.766 2.252 1.00
pnpr@main 2.062 ± 0.158 1.778 2.220 1.03 ± 0.11
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.021117842139999,
      "stddev": 0.04470521271150496,
      "median": 4.02197165674,
      "user": 4.05611686,
      "system": 2.11037724,
      "min": 3.94593485424,
      "max": 4.10557820624,
      "times": [
        4.0705481382399995,
        4.02337674424,
        4.0213013452399995,
        4.04054257524,
        3.94593485424,
        4.022641968239999,
        3.99025010424,
        4.00284052624,
        3.98816395924,
        4.10557820624
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.00314583964,
      "stddev": 0.03155370788414384,
      "median": 4.0008127047399995,
      "user": 4.049287560000001,
      "system": 2.0957646399999996,
      "min": 3.9418275002399996,
      "max": 4.04661813224,
      "times": [
        3.97855079624,
        4.045819725239999,
        3.99995237924,
        4.02843943624,
        3.9905595042399997,
        3.9418275002399996,
        4.04661813224,
        4.00167303024,
        3.9945258842399998,
        4.003492008239999
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.0071010439399997,
      "stddev": 0.1618147823615178,
      "median": 2.02655565524,
      "user": 2.7052799599999995,
      "system": 1.8025347399999998,
      "min": 1.7660415202400002,
      "max": 2.2518059342399996,
      "times": [
        2.09856547524,
        1.7660415202400002,
        1.9998531242400002,
        1.8675666812400002,
        2.2518059342399996,
        2.1496383452399996,
        2.15893642124,
        2.05325818624,
        1.8984305752400001,
        1.82691417624
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.0622067938399997,
      "stddev": 0.1577675059072014,
      "median": 2.08482357674,
      "user": 2.71786496,
      "system": 1.7854153400000001,
      "min": 1.7778125952400001,
      "max": 2.22012406124,
      "times": [
        2.0412489142399997,
        2.22012406124,
        2.10606461624,
        2.1581343132399997,
        1.7991020702400002,
        1.7778125952400001,
        2.20331495124,
        2.0582670352399997,
        2.06358253724,
        2.19441684424
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, hot cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.156 ± 0.012 1.132 1.170 2.41 ± 0.03
pacquet@main 1.172 ± 0.030 1.137 1.251 2.44 ± 0.07
pnpr@HEAD 0.480 ± 0.005 0.473 0.488 1.00
pnpr@main 0.493 ± 0.028 0.477 0.569 1.03 ± 0.06
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.15569629406,
      "stddev": 0.011872155060841137,
      "median": 1.15662679746,
      "user": 1.33456172,
      "system": 1.0223106000000002,
      "min": 1.13221192346,
      "max": 1.1700591394600002,
      "times": [
        1.15113854046,
        1.16074863446,
        1.14442862646,
        1.16356656546,
        1.1496830184600002,
        1.16492391446,
        1.1525049604600002,
        1.1700591394600002,
        1.13221192346,
        1.16769761746
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.1720484803600002,
      "stddev": 0.030344860636012678,
      "median": 1.16717509196,
      "user": 1.34428342,
      "system": 1.0286433000000001,
      "min": 1.1368463914600002,
      "max": 1.2511811964600001,
      "times": [
        1.16619275846,
        1.15367598846,
        1.1368463914600002,
        1.16815742546,
        1.1569936334600002,
        1.17240575046,
        1.17343476746,
        1.2511811964600001,
        1.1616518614600002,
        1.17994503046
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.48017146296,
      "stddev": 0.004722366742579173,
      "median": 0.48001667296000006,
      "user": 0.3204376199999999,
      "system": 0.7596347999999999,
      "min": 0.47284858146000003,
      "max": 0.48814857346,
      "times": [
        0.47635192746,
        0.47284858146000003,
        0.48048873146,
        0.47927108246,
        0.47994602746000004,
        0.48008731846,
        0.48814857346,
        0.48345670446,
        0.48588692946,
        0.47522875346
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.4929006993600001,
      "stddev": 0.027553193686237334,
      "median": 0.48314399496000004,
      "user": 0.31645221999999995,
      "system": 0.7632038999999999,
      "min": 0.47744444346000003,
      "max": 0.56902249746,
      "times": [
        0.48945578246,
        0.56902249746,
        0.49867634046000003,
        0.47898043046,
        0.48250456446,
        0.47744444346000003,
        0.48378342546,
        0.47802037846,
        0.48110029246,
        0.49001883846
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.850 ± 0.038 2.809 2.937 5.92 ± 0.10
pacquet@main 2.844 ± 0.038 2.797 2.904 5.91 ± 0.10
pnpr@HEAD 0.488 ± 0.004 0.482 0.496 1.02 ± 0.01
pnpr@main 0.481 ± 0.005 0.473 0.487 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.8496108541000003,
      "stddev": 0.03814258898650004,
      "median": 2.8405005677000004,
      "user": 1.81303184,
      "system": 1.2294034799999998,
      "min": 2.8091620627,
      "max": 2.9370565587,
      "times": [
        2.8091620627,
        2.8413001697,
        2.8390173417000004,
        2.8154079777,
        2.8495980467,
        2.8883154157,
        2.8397009657,
        2.8208612297,
        2.8556887727,
        2.9370565587
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.8444364258,
      "stddev": 0.037833352947205294,
      "median": 2.8384338922000003,
      "user": 1.8302572399999995,
      "system": 1.2195243799999997,
      "min": 2.7970538467000003,
      "max": 2.9044798527,
      "times": [
        2.8408775777,
        2.8359902067000005,
        2.8075593677,
        2.8102460067000004,
        2.7970538467000003,
        2.8412638587,
        2.8817340467,
        2.8969272167000004,
        2.8282322777,
        2.9044798527
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.48845631899999997,
      "stddev": 0.004150907481726728,
      "median": 0.4890870547,
      "user": 0.32745924,
      "system": 0.7697552799999999,
      "min": 0.4820316237,
      "max": 0.4958052987,
      "times": [
        0.4881988917,
        0.4820316237,
        0.4847132287,
        0.4845904057,
        0.4859927837,
        0.4921347857,
        0.4905316297,
        0.4905893247,
        0.4958052987,
        0.4899752177
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.4810474409,
      "stddev": 0.00457469017724651,
      "median": 0.48195155170000004,
      "user": 0.31748564,
      "system": 0.76078008,
      "min": 0.4734993227,
      "max": 0.4873461307,
      "times": [
        0.4852278217,
        0.4873461307,
        0.4821080097,
        0.4798020657,
        0.4840331047,
        0.4745414987,
        0.4842031617,
        0.4817950937,
        0.4734993227,
        0.4779181997
      ]
    }
  ]
}

@github-actions

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12351
Testbedpacquet
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
milliseconds (ms)
(Result Δ%)
Upper Boundary
milliseconds (ms)
(Limit %)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
🚷 view threshold
4,021.12 ms
(-3.77%)Baseline: 4,178.67 ms
5,014.41 ms
(80.19%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
2,849.61 ms
(-4.78%)Baseline: 2,992.57 ms
3,591.08 ms
(79.35%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,155.70 ms
(-11.86%)Baseline: 1,311.15 ms
1,573.38 ms
(73.45%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
3,890.41 ms
(-4.59%)Baseline: 4,077.75 ms
4,893.31 ms
(79.50%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
453.53 ms
(-26.08%)Baseline: 613.54 ms
736.25 ms
(61.60%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12351
Testbedpnpr

⚠️ WARNING: No Threshold found!

Without a Threshold, no Alerts will ever be generated.

Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the --ci-only-thresholds flag.

Click to view all benchmark results
BenchmarkLatencymilliseconds (ms)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,007.10 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
488.46 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
480.17 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
1,898.60 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
534.08 ms
🐰 View full continuous benchmarking report in Bencher

@zkochan zkochan merged commit 3f0fb21 into pnpm:main Jun 17, 2026
47 of 49 checks passed
@welcome

welcome Bot commented Jun 17, 2026

Copy link
Copy Markdown

Congrats on merging your first pull request! 🎉🎉🎉

aqeelat added a commit to aqeelat/pnpm that referenced this pull request Jun 27, 2026
PR pnpm#12351 replaced ansi-diff with manual full-frame reprint,
which re-writes unchanged sticky blocks on every progress tick. This
caused cached lockfile verdicts and deprecation warnings to appear
dozens of times in terminals that record each write (Warp, script,
CI logs). The manual countRows also produced unbounded cursor-up
values when the frame was taller than the terminal, causing an
infinite repaint loop on pnpm run --parallel.

Restore ansi-diff for differential rendering, wrapping its output with
\r (column reset) + \x1b[0J (erase below frame) to preserve the
external-process-output fix from pnpm#12351.

The Rust pacquet port (pacquet/crates/default-reporter/src/lib.rs) has
the same full-frame redraw shape and still needs an equivalent fix;
tracked as a follow-up.

Closes pnpm#12634.
aqeelat added a commit to aqeelat/pnpm that referenced this pull request Jun 27, 2026
PR pnpm#12351 replaced ansi-diff with manual full-frame reprint,
which re-writes unchanged sticky blocks on every progress tick. This
caused cached lockfile verdicts and deprecation warnings to appear
dozens of times in terminals that record each write (Warp, script,
CI logs). The manual countRows also produced unbounded cursor-up
values when the frame was taller than the terminal, causing an
infinite repaint loop on pnpm run --parallel.

Restore ansi-diff for differential rendering, wrapping its output with
\r (column reset) + \x1b[0J (erase below frame) to preserve the
external-process-output fix from pnpm#12351.

The Rust pacquet port (pacquet/crates/default-reporter/src/lib.rs) has
the same full-frame redraw shape and still needs an equivalent fix;
tracked as a follow-up.

Closes pnpm#12634.
aqeelat added a commit to aqeelat/pnpm that referenced this pull request Jul 1, 2026
PR pnpm#12351 replaced ansi-diff with manual full-frame reprint,
which re-writes unchanged sticky blocks on every progress tick. This
caused cached lockfile verdicts and deprecation warnings to appear
dozens of times in terminals that record each write (Warp, script,
CI logs). The manual countRows also produced unbounded cursor-up
values when the frame was taller than the terminal, causing an
infinite repaint loop on pnpm run --parallel.

Restore ansi-diff for differential rendering, wrapping its output with
\r (column reset) + \x1b[0J (erase below frame) to preserve the
external-process-output fix from pnpm#12351.

The Rust pacquet port (pacquet/crates/default-reporter/src/lib.rs) has
the same full-frame redraw shape and still needs an equivalent fix;
tracked as a follow-up.

Closes pnpm#12634.
zkochan pushed a commit to aqeelat/pnpm that referenced this pull request Jul 1, 2026
PR pnpm#12351 replaced ansi-diff with manual full-frame reprint,
which re-writes unchanged sticky blocks on every progress tick. This
caused cached lockfile verdicts and deprecation warnings to appear
dozens of times in terminals that record each write (Warp, script,
CI logs). The manual countRows also produced unbounded cursor-up
values when the frame was taller than the terminal, causing an
infinite repaint loop on pnpm run --parallel.

Restore ansi-diff for differential rendering, wrapping its output with
\r (column reset) + \x1b[0J (erase below frame) to preserve the
external-process-output fix from pnpm#12351.

The Rust pacquet port (pacquet/crates/default-reporter/src/lib.rs) has
the same full-frame redraw shape and still needs an equivalent fix;
tracked as a follow-up.

Closes pnpm#12634.
aqeelat added a commit to aqeelat/pnpm that referenced this pull request Jul 1, 2026
PR pnpm#12351 replaced ansi-diff with manual full-frame reprint,
which re-writes unchanged sticky blocks on every progress tick. This
caused cached lockfile verdicts and deprecation warnings to appear
dozens of times in terminals that record each write (Warp, script,
CI logs). The manual countRows also produced unbounded cursor-up
values when the frame was taller than the terminal, causing an
infinite repaint loop on pnpm run --parallel.

TypeScript CLI: restore the `ansi-diff` npm package for differential
rendering, wrapping its output with \r (column reset) + \x1b[0J
(erase below frame) to preserve the external-process-output fix
from pnpm#12351.

Rust pacquet port: implement a zero-dependency differential renderer
(`diff.rs`) with the same algorithm, replacing the full-frame reprint
in `Sink::write_output`.

Closes pnpm#12634.
aqeelat added a commit to aqeelat/pnpm that referenced this pull request Jul 1, 2026
PR pnpm#12351 replaced ansi-diff with manual full-frame reprint,
which re-writes unchanged sticky blocks on every progress tick. This
caused cached lockfile verdicts and deprecation warnings to appear
dozens of times in terminals that record each write (Warp, script,
CI logs). The manual countRows also produced unbounded cursor-up
values when the frame was taller than the terminal, causing an
infinite repaint loop on pnpm run --parallel.

TypeScript CLI: restore the `ansi-diff` npm package for differential
rendering, wrapping its output with \r (column reset) + \x1b[0J
(erase below frame) to preserve the external-process-output fix
from pnpm#12351.

Rust pacquet port: implement a zero-dependency differential renderer
(`diff.rs`) with the same algorithm, replacing the full-frame reprint
in `Sink::write_output`.

Closes pnpm#12634.
aqeelat added a commit to aqeelat/pnpm that referenced this pull request Jul 2, 2026
PR pnpm#12351 replaced ansi-diff with manual full-frame reprint,
which re-writes unchanged sticky blocks on every progress tick. This
caused cached lockfile verdicts and deprecation warnings to appear
dozens of times in terminals that record each write (Warp, script,
CI logs). The manual countRows also produced unbounded cursor-up
values when the frame was taller than the terminal, causing an
infinite repaint loop on pnpm run --parallel.

TypeScript CLI: restore the `ansi-diff` npm package for differential
rendering, wrapping its output with \r (column reset) + \x1b[0J
(erase below frame) to preserve the external-process-output fix
from pnpm#12351.

Rust pacquet port: implement a zero-dependency differential renderer
(`diff.rs`) with the same algorithm, replacing the full-frame reprint
in `Sink::write_output`.

Closes pnpm#12634.
zkochan pushed a commit to aqeelat/pnpm that referenced this pull request Jul 2, 2026
PR pnpm#12351 replaced ansi-diff with manual full-frame reprint,
which re-writes unchanged sticky blocks on every progress tick. This
caused cached lockfile verdicts and deprecation warnings to appear
dozens of times in terminals that record each write (Warp, script,
CI logs). The manual countRows also produced unbounded cursor-up
values when the frame was taller than the terminal, causing an
infinite repaint loop on pnpm run --parallel.

TypeScript CLI: restore the `ansi-diff` npm package for differential
rendering, wrapping its output with \r (column reset) + \x1b[0J
(erase below frame) to preserve the external-process-output fix
from pnpm#12351.

Rust pacquet port: implement a zero-dependency differential renderer
(`diff.rs`) with the same algorithm, replacing the full-frame reprint
in `Sink::write_output`.

Closes pnpm#12634.
zkochan added a commit that referenced this pull request Jul 2, 2026
…nes (#12698)

* fix(default-reporter): restore ansi-diff to stop duplicated output lines

PR #12351 replaced ansi-diff with manual full-frame reprint,
which re-writes unchanged sticky blocks on every progress tick. This
caused cached lockfile verdicts and deprecation warnings to appear
dozens of times in terminals that record each write (Warp, script,
CI logs). The manual countRows also produced unbounded cursor-up
values when the frame was taller than the terminal, causing an
infinite repaint loop on pnpm run --parallel.

TypeScript CLI: restore the `ansi-diff` npm package for differential
rendering, wrapping its output with \r (column reset) + \x1b[0J
(erase below frame) to preserve the external-process-output fix
from #12351.

Rust pacquet port: implement a zero-dependency differential renderer
(`diff.rs`) with the same algorithm, replacing the full-frame reprint
in `Sink::write_output`.

Closes #12634.

* fix(default-reporter): pass dylint, add trailing newline, harden reporter test

Builds on the restored ansi-diff differential renderer:

- pacquet: guarantee a trailing newline before diffing a frame, matching the
  TypeScript reporter. This keeps an interactive prompt on a fresh line below
  the frame instead of joined onto the last progress line, and leaves the
  differ's tracked cursor column at 0 so it stays in sync with the \r
  prepended on the next update (otherwise the inline diff computes relative
  moves from a stale column and corrupts in-place rendering).

- pacquet: move the differ's inline test module into src/diff/tests.rs with
  explicit imports and drop a stray trailing comma, so cargo dylint passes.

- TypeScript: locate the sticky lockfile verdict's first render with findIndex
  instead of assuming writes[0], and wrap each reporter test body in
  try/finally so the subscription is always stopped.

* fix(default-reporter): align rust diff with ansi-diff edge cases

- Add col !== width guard before inline diff, matching the JS
  ansi-diff edgecase check that prevents mispositioning at exact-width
  wrap boundaries.
- Fix clearDown to start from the cursor's current row (self.row)
  instead of new_last_row, matching the JS _clearDown pattern. The
  previous code could clear frame content when trailing lines were
  unchanged and the frame shrank.
- Document the visible_width vs wcwidth simplification.

* test(default-reporter): expand diff coverage to 14 tests

Add 8 new tests covering:
- inline diff skipped for ANSI-containing lines
- inline diff threshold edge case
- full line rewrite with clear
- clear_down starting from cursor row (not new_last_row)
- soft-wrapped line height tracking
- empty frame clears all previous content
- multiple progress ticks (simulated install sequence)
- frame without trailing newline

* fix(default-reporter): tighten test assertions and avoid hot-path allocation

- clear_down_from_cursor_row: replace weak || assertion with direct
  checks that Line A is absent, Line B changed is present, and clear
  sequence is emitted.
- TS reporterRenderer: capture writes.length before the 'fetched'
  event and wait for an increase, so the test can't pass early.
- pacquet Sink::write_output: write \r, diff output, and erase codes
  as separate write_all calls instead of format!() which allocated a
  new String on every frame.

* fix(default-reporter): make frame redraw a single write and fix diff test

Compose the \r + diff + erase sequence into a reusable per-sink buffer and
emit it with one write_all, keeping each frame update atomic against
interleaved writers without allocating on the hot progress path.

Rework inline_diff_skipped_for_small_change (which actually exercised the
inline-diff happy path already covered by inline_diff_writes_only_changed_chars)
into inline_diff_skipped_for_short_common_affix, covering the
left + right <= 4 full-rewrite branch its name promised.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: progress line shows remnants from external process output (e.g. SSH passphrase prompts)

3 participants