fix(default-reporter): erase trailing characters on progress line#12351
Conversation
Code Review by Qodo
Context used 1. Redraw starts mid-line
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe 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. ChangesProgress Reporter Terminal Clearing Refactor
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
PR Summary by QodoFix progress reporter by clearing trailing characters after ansi-diff updates WalkthroughsDescription• 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. Diagramgraph 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
High-Level AssessmentThe following are alternative approaches to this PR: 1. Use readline/TTY clearLine for the active line
2. Configure/replace diffing strategy to always reprint full lines
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. File ChangesBug fix (1)
|
79944b1 to
6712792
Compare
|
Code review by qodo was updated up to the latest commit 6712792 |
There was a problem hiding this comment.
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
📒 Files selected for processing (1)
cli/default-reporter/src/index.ts
|
Code review by qodo was updated up to the latest commit e3f1c00 |
| // 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`)) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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).
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.
e3f1c00 to
38e1676
Compare
|
Code review by qodo was updated up to the latest commit 38e1676 |
| const moveToFrameTop = prevRows > 0 ? `\x1b[${prevRows}A` : '' | ||
| write(`${moveToFrameTop}${ERASE_TO_END_OF_DISPLAY}${view}`) | ||
| prevRows = countRows(view) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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({ |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml,!pnpm-lock.yaml
📒 Files selected for processing (7)
.changeset/erase-progress-line-remnants.mdcli/default-reporter/package.jsoncli/default-reporter/src/index.tscli/default-reporter/src/reporterForClient/reportLockfileVerification.tscli/default-reporter/test/cli.tscli/default-reporter/test/reportingLockfileVerification.tspnpm-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
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.
|
Code review by qodo was updated up to the latest commit 1401629 |
| // 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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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).
| // 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 | ||
| } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
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
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
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
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
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
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
]
}
]
} |
|
| Branch | pr/12351 |
| Testbed | pacquet |
Click to view all benchmark results
| Benchmark | Latency | Benchmark 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%) |
|
| Branch | pr/12351 |
| Testbed | pnpr |
⚠️ 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-thresholdsflag.
Click to view all benchmark results
| Benchmark | Latency | milliseconds (ms) |
|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot | 2,007.10 ms |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot | 488.46 ms |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot | 480.17 ms |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot | 1,898.60 ms |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot | 534.08 ms |
|
Congrats on merging your first pull request! 🎉🎉🎉 |
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.
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.
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.
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.
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.
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.
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.
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.
…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>
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':, wheresa':is a fragment ofEnter 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:
ESC[<rows>Afollowed by a carriage return, so the redraw starts at column 0 even if an external process left the cursor mid-line),ESC[0J),Because the whole region is erased on every frame, any characters an external process wrote in between are cleared. This matches pacquet's
Output::Framerendering (the column-reset hardening was applied to both stacks). The now-unusedansi-diffdependency has been removed.Test plan
pnpm installin a project with a git+ssh dependency that triggers a passphrase promptpnpm --filter @pnpm/cli.default-reporter testpasses