Skip to content

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

Merged
zkochan merged 6 commits into
pnpm:mainfrom
aqeelat:fix/restore-ansi-diff-renderer
Jul 2, 2026
Merged

fix(default-reporter): restore ansi-diff to stop duplicated output lines#12698
zkochan merged 6 commits into
pnpm:mainfrom
aqeelat:fix/restore-ansi-diff-renderer

Conversation

@aqeelat

@aqeelat aqeelat commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Summary

PR #12351 replaced ansi-diff (differential rendering) with manual full-frame reprint to fix external-process output bleeding into the progress line (#12350). The full-frame reprint caused two regressions:

  1. Duplicated one-shot messages — sticky blocks (cached lockfile verdicts, deprecation warnings) are re-written on every progress tick, appearing dozens of times in terminals that record each write (Warp, script, CI logs, scrollback).
  2. Unbounded cursor-up / infinite repaint — the manual countRows/prevRows grows without bound when the frame is taller than the terminal, causing an infinite scroll/repaint loop on pnpm run --parallel.

This PR restores ansi-diff for differential rendering, wrapping its output with \r (column reset) + \x1b[0J (erase below frame) to preserve the #12350 fix. Instrumented traces confirmed the root cause and verified the duplication fix.

The Rust pacquet/ port is fixed in this same PR: pacquet/crates/default-reporter/ had the identical manual full-frame reprint (prev_rows/count_rows + cursor-up), so a FrameDiffer that ports the ansi-diff line-diff algorithm now replaces it, driven through the sink with the same \r + \x1b[0J wrapper. Both stacks stay behaviorally aligned.

Closes #12634.

Squash Commit Body

PR pnpm/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/pnpm#12351.

The Rust pacquet port (pacquet/crates/default-reporter/) had the same
manual full-frame reprint and is fixed in this PR too: a FrameDiffer
porting the ansi-diff line-diff algorithm replaces it, wrapped
identically with \r + \x1b[0J.

Closes pnpm/pnpm#12634.

Checklist

  • The change is implemented in both the TypeScript CLI and the Rust
    pacquet/ port, or the description notes what still needs porting.
    — Implemented in both: TypeScript @pnpm/cli.default-reporter (restored ansi-diff) and pacquet pacquet/crates/default-reporter/ (a FrameDiffer porting the ansi-diff algorithm).
  • Added a changeset (pnpm changeset) if this PR changes any published
    package. Keep it short and written for pnpm users — it becomes a release note.
  • Added or updated tests.
  • Updated the documentation if needed. — Not needed for this internal rendering fix.

Written by an agent (opencode, glm-5.2).
Updated by an agent (Claude Code, claude-opus-4-8).

Summary by CodeRabbit

  • New Features
    • Improved the CLI default reporter’s terminal progress rendering to use differential ANSI updates for smoother, less flickery output.
  • Bug Fixes
    • Restored correct incremental rendering to prevent duplicated/overlapping progress lines and to improve cleanup when the output shrinks.
  • Tests
    • Added/expanded unit and Jest regression coverage to verify incremental output emission and that renders end with the terminal “erase to end of display” escape sequence.
  • Chores
    • Updated release configuration to restore consistent ansi-diff patch releases.

@aqeelat aqeelat requested a review from zkochan as a code owner June 27, 2026 14:21
@coderabbitai

coderabbitai Bot commented Jun 27, 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

Restores ansi-diff for default reporter output, replaces full-frame redraws with incremental ANSI updates, and adds tests plus release metadata.

Changes

Restore ansi-diff in default reporter

Layer / File(s) Summary
ansi-diff dependency and changeset
pnpm-workspace.yaml, pnpm11/cli/default-reporter/package.json, .changeset/restore-ansi-diff.md
Pins ansi-diff in the workspace catalog, adds it to the default-reporter manifest, and records the patch changeset.
Differential frame renderer
pacquet/crates/default-reporter/src/diff.rs, pacquet/crates/default-reporter/src/diff/tests.rs
Adds the Rust differential renderer with wrapped-line tracking, inline diffing, and clearing behavior, plus unit tests for incremental updates and removals.
Reporter diff integration
pacquet/crates/default-reporter/src/lib.rs, pnpm11/cli/default-reporter/src/index.ts, pnpm11/cli/default-reporter/test/reporterRenderer.ts
Wires the diff renderer into both reporters, rewrites frame output to use incremental ANSI writes, and adds regression tests for deduplicated output and terminal clearing.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • pnpm/pnpm#12351: Modifies the same default reporter render path around ansi-diff and frame clearing, but in the opposite direction.

Suggested labels: product: pnpm@11, product: pacquet

Suggested reviewers: zkochan

✨ 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.

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

Copy link
Copy Markdown

PR Summary by Qodo

Restore ansi-diff rendering to prevent duplicated default-reporter output
🐞 Bug fix 🧪 Tests ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

Description

• Reintroduce ansi-diff differential frame rendering to avoid reprinting unchanged sticky output.
• Preserve the external-process-output fix by clearing below the rendered frame after each update.
• Add regression tests to prevent duplicated lockfile verdicts and ensure frame clearing stays
 intact.
Diagram

graph TD
  SP["StreamParser"] --> OUT["toOutput$ (Rx stream)"] --> REP["initDefaultReporter"] --> DIFF["ansi-diff update"] --> WR["proc stdout/stderr write"] --> TERM["Terminal display"]
  EXT["External process output"] --> TERM
  DIFF -- "append \\r + \\x1b[0J" --> TERM
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep full-frame redraw but cache/skip sticky blocks
  • ➕ Avoids reintroducing ansi-diff as a dependency
  • ➕ Can be tailored to pnpm’s specific sticky sections
  • ➖ More complex and fragile (must reliably identify and segment sticky regions)
  • ➖ Still needs correct cursor positioning and wrapped-line handling
  • ➖ Higher risk of future regressions across terminals/recorders
2. Adopt a higher-level renderer (e.g., log-update/ansi-escapes)
  • ➕ Well-trodden terminal update patterns with fewer bespoke ANSI details
  • ➕ Potentially better handling of resize/wrapping edge cases
  • ➖ Larger behavioral change vs. a targeted regression fix
  • ➖ May require refactoring existing renderer assumptions and tests

Recommendation: Restoring ansi-diff is the most targeted fix: it addresses the duplication regression by reverting to differential updates while retaining the external-process-output mitigation via \r and \x1b[0J. The added regression tests make this approach sustainable and lower-risk than maintaining a bespoke full-frame redraw implementation.

Files changed (6) +155 / -26

Bug fix (1) +16 / -26
index.tsRestore ansi-diff-based differential terminal updates +16/-26

Restore ansi-diff-based differential terminal updates

• Replaces the manual full-frame redraw logic with 'ansi-diff' differential updates. Ensures each update resets the column ('\r') and clears below the frame ('\x1b[0J') to prevent external-process output from bleeding into progress rendering.

pnpm11/cli/default-reporter/src/index.ts

Tests (1) +125 / -0
reporterRenderer.tsAdd regression tests for duplicate output and frame clearing +125/-0

Add regression tests for duplicate output and frame clearing

• Introduces tests that simulate reporter writes and assert sticky blocks (e.g., lockfile verdict) are not reprinted across progress ticks. Also verifies every write ends with the erase-to-end-of-display sequence to preserve the external-output clearing behavior.

pnpm11/cli/default-reporter/test/reporterRenderer.ts

Documentation (1) +6 / -0
restore-ansi-diff.mdAdd changeset for restoring differential rendering +6/-0

Add changeset for restoring differential rendering

• Adds a patch changeset for 'pnpm' and '@pnpm/cli.default-reporter' describing the restoration of 'ansi-diff' to fix duplicated output lines.

.changeset/restore-ansi-diff.md

Other (3) +8 / -0
pnpm-lock.yamlLockfile updates for reintroducing ansi-diff +6/-0

Lockfile updates for reintroducing ansi-diff

• Updates the lockfile to include 'ansi-diff@1.2.0' in the workspace catalog and importer dependency graph.

pnpm-lock.yaml

pnpm-workspace.yamlAdd ansi-diff to the workspace catalog +1/-0

Add ansi-diff to the workspace catalog

• Adds 'ansi-diff' to the shared catalog to standardize the version used across workspace packages.

pnpm-workspace.yaml

package.jsonDepend on ansi-diff in default reporter package +1/-0

Depend on ansi-diff in default reporter package

• Adds 'ansi-diff' as a dependency of '@pnpm/cli.default-reporter' via the workspace catalog.

pnpm11/cli/default-reporter/package.json

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

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

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (11) 📘 Rule violations (0) 📎 Requirement gaps (5) 📜 Skill insights (0)

Context used

Grey Divider


Action required

1. Diff not reset on logs 📎 Requirement gap ☼ Reliability
Description
In pacquet, non-frame output (Output::Lines) can be written between interactive frames without
resetting/synchronizing the new diff::Diff cursor tracking. This can desync cursor movement for
subsequent frame updates and cause redraw instability/flicker in pnpm -r --parallel-style
interleaved output scenarios.
Code

pacquet/crates/default-reporter/src/lib.rs[R139-150]

+            Output::Frame(mut frame) => {
+                // A trailing newline keeps an interactive prompt on a fresh line
+                // below the frame rather than joined onto its last line, and it
+                // leaves the differ's tracked cursor at column 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).
+                if !frame.ends_with('\n') {
+                    frame.push('\n');
      }
-                // Reset 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.
-                buf.push('\r');
-                buf.push_str("\x1b[0J");
-                buf.push_str(&frame);
-                buf.push('\n');
-                let _ = out.write_all(buf.as_bytes());
-                self.prev_rows = count_rows(&frame, self.columns);
+                let diff_output = self.diff.update(&frame);
+                let _ = out.write_all(format!("\r{diff_output}\x1b[K\x1b[0J").as_bytes());
  }
Evidence
Compliance ID 1 requires stable interactive redraw under --parallel where plain output lines can
interleave with frame updates. The sink writes Output::Lines directly but does not
reset/synchronize diff::Diff, while Diff::update() relies on persisted self.row/self.col to
compute relative cursor moves, making it vulnerable to desync when intervening output moves the real
terminal cursor.

Interactive reporter must not flicker or continuously repaint when frame exceeds terminal height (run --parallel regression in v11.8.0)
pacquet/crates/default-reporter/src/lib.rs[130-151]
pacquet/crates/default-reporter/src/diff.rs[20-133]

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

## Issue description
pacquet's new differential renderer (`diff::Diff`) tracks `row`/`col` internally across frames, but the sink may write `Output::Lines` (non-frame lines) between frames without telling the differ. This can leave the differ's cursor state stale, so the next `Diff::update()` emits incorrect `ESC[A/B/C/D` moves and can produce unstable repainting in interactive/parallel output.
## Issue Context
`pnpm -r --parallel ...` produces interleaved script output (plain lines) and periodic frame updates. The compliance requirement is that interactive redraw remains stable even when the frame is large and output is noisy.
## Fix Focus Areas
- pacquet/crates/default-reporter/src/lib.rs[130-151]
- pacquet/crates/default-reporter/src/diff.rs[20-145]

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


2. Pacquet frame write lacks ESC[K 📎 Requirement gap ≡ Correctness
Description
The pacquet reporter writes \r{diff_output}\x1b[0J but never emits an erase-to-end-of-line
sequence on the current progress line. If an external process prints a longer line and the reporter
only overwrites part of it, trailing characters can remain visible, violating the clean
progress-line requirement.
Code

pacquet/crates/default-reporter/src/lib.rs[R139-142]

Output::Frame(frame) => {
-                let mut buf = String::new();
-                if self.prev_rows > 0 {
-                    let _ = write!(buf, "\x1b[{}A", self.prev_rows);
-                }
-                // Reset 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.
-                buf.push('\r');
-                buf.push_str("\x1b[0J");
-                buf.push_str(&frame);
-                buf.push('\n');
-                let _ = out.write_all(buf.as_bytes());
-                self.prev_rows = count_rows(&frame, self.columns);
+                let diff_output = self.diff.update(&frame);
+                let _ = out.write_all(format!("\r{diff_output}\x1b[0J").as_bytes());
}
Evidence
PR Compliance ID 1 requires the progress line to clear any trailing characters after external
process output. The pacquet reporter’s Output::Frame write appends only \r and \x1b[0J and
does not emit an erase-to-end-of-line control sequence, so remnants on the current line may persist.

Progress line clears trailing characters after external process output
pacquet/crates/default-reporter/src/lib.rs[139-142]

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 pacquet default reporter’s interactive frame updates do not guarantee clearing trailing characters on the current line after external process output.
## Issue Context
Compliance requires that after any external terminal output, progress redraws must erase any leftover characters beyond the end of the new progress content. The current write path only uses `\r` and `\x1b[0J`.
## Fix Focus Areas
- pacquet/crates/default-reporter/src/lib.rs[139-142]

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


3. Cursor reset desync 🐞 Bug ≡ Correctness
Description
pacquet’s sink prepends a raw carriage return (\r) before writing Diff::update() output, but
Diff maintains its own cursor column state and is not reset, so try_inline_diff() can emit
incorrect relative cursor moves and corrupt in-place rendering.
Code

pacquet/crates/default-reporter/src/lib.rs[R140-141]

+                let diff_output = self.diff.update(&frame);
+                let _ = out.write_all(format!("\r{diff_output}\x1b[0J").as_bytes());
Evidence
The sink always writes a literal \r before the diff output, but Diff::update() may perform an
inline diff before any move_to(0, ...) call; inline diff moves to a non-zero column based on the
passed x/y (which come from Diff’s internal state), so a caller-side column reset makes those
relative moves incorrect. ReporterState can render multiple fixed blocks (progress + downloading),
making the progress/downloading lines eligible for inline-diff (newline == true) in real runs.

pacquet/crates/default-reporter/src/lib.rs[129-142]
pacquet/crates/default-reporter/src/diff.rs[40-57]
pacquet/crates/default-reporter/src/diff.rs[161-195]
pacquet/crates/default-reporter/src/state.rs[379-455]

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

## Issue description
`Sink::write_output()` emits `\r` before the diff output, but `Diff` continues to assume the cursor is still at its previously-tracked `x` coordinate. Because `Diff::update()` may enter the `try_inline_diff()` path before any `move_to(0, ...)` happens, the first cursor move can be computed from stale `x` and produce wrong placement.
### Issue Context
This is most likely to show up when multiple fixed blocks are present (e.g. progress + big-tarball downloading lines), because then progress lines are not necessarily last and can qualify for `newline == true`, enabling `try_inline_diff()`.
### Fix Focus Areas
- pacquet/crates/default-reporter/src/lib.rs[129-146]
- pacquet/crates/default-reporter/src/diff.rs[31-67]
- pacquet/crates/default-reporter/src/diff.rs[161-195]
### Suggested fix
Add an explicit API to `Diff` to reflect the caller’s carriage-return, e.g. `diff.reset_column()` (sets internal `x = 0` without changing `y`). Call it immediately before (or immediately after) emitting `\r`, so internal cursor tracking matches the terminal’s cursor column when `Diff::update()` generates relative moves.
Also consider moving the `\r` emission inside the `Diff` module (so the module owns both the escape output and the cursor state).

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


View more (1)
4. Pacquet Diff lacks height clamp 📎 Requirement gap ☼ Reliability
Description
The new pacquet differential renderer is not terminal-height-aware: it does not track rows and
emits potentially unbounded vertical cursor moves (ESC[A/ESC[B) based solely on frame
coordinates. When the frame is taller than the terminal viewport, terminals may not reliably apply
these moves (e.g., can’t move into scrollback), causing mispositioned updates, unstable redraw, and
flicker/non-converging repaint in small terminals.
Code

pacquet/crates/default-reporter/src/diff.rs[R105-118]

+    fn move_to(&mut self, out: &mut String, x: usize, y: usize) {
+        if x > self.x {
+            let _ = write!(out, "\x1b[{}C", x - self.x);
+        } else if x < self.x {
+            let _ = write!(out, "\x1b[{}D", self.x - x);
+        }
+        if y > self.y {
+            let _ = write!(out, "\x1b[{}B", y - self.y);
+        } else if y < self.y {
+            let _ = write!(out, "\x1b[{}A", self.y - y);
+        }
+        self.x = x;
+        self.y = y;
+    }
Evidence
PR Compliance ID 2 requires redraw/cursor movement to be bounded relative to terminal height, but
the current implementation only queries terminal columns and constructs Diff with
Diff::new(columns). Diff tracks only width, and its move_to emits raw ESC[A/ESC[B
vertical cursor movements without any clamp or reference to terminal row height, so it cannot
satisfy height-aware behavior when rendered frames exceed the viewport.

Interactive reporter redraw must be height-aware and must not flicker when frame exceeds terminal height
pacquet/crates/default-reporter/src/lib.rs[104-112]
pacquet/crates/default-reporter/src/diff.rs[22-30]
pacquet/crates/default-reporter/src/diff.rs[105-118]
pacquet/crates/default-reporter/src/lib.rs[100-113]
pacquet/crates/default-reporter/src/diff.rs[14-20]

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 pacquet default reporter’s differential renderer (`diff::Diff`) is not terminal-height-aware: it is constructed using only terminal columns/`width`, never queries/passes terminal `rows`, and can emit large `ESC[<N>A`/`ESC[<N>B` cursor-up/down sequences without clamping to the visible viewport. This breaks the requirement that interactive redraw/cursor movement be bounded relative to terminal height and can lead to mispositioned updates and flicker/non-converging repaint when frames exceed the terminal viewport.
## Issue Context
Compliance requires interactive redraw logic to be height-aware and to prevent unstable redraw behavior when the rendered frame is taller than the terminal. The current Rust implementation computes cursor `y` from the full frame and moves the cursor with raw ANSI vertical movements without any reference to terminal height; in contrast, the TypeScript version restores `ansi-diff` with `height: stream.rows` to avoid unbounded cursor-up behavior. To align behavior, the Rust port needs an equivalent terminal-rows query and a height parameter in `Diff`, plus bounded movement and/or a safe fallback mode when the frame’s effective height exceeds the viewport.
## Fix Focus Areas
- pacquet/crates/default-reporter/src/lib.rs[100-157]
- pacquet/crates/default-reporter/src/diff.rs[14-20]
- pacquet/crates/default-reporter/src/diff.rs[22-30]
- pacquet/crates/default-reporter/src/diff.rs[105-118]

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



Remediation recommended

5. ansi-diff height not updated 📎 Requirement gap ☼ Reliability
Description
ansi-diff is instantiated once with height: stream.rows/width: stream.columns, but there is no
resize handling to keep these values in sync with the current terminal viewport. After a terminal
resize (especially shrinking), the reporter may emit cursor movements based on stale dimensions,
risking unstable redraw/flicker in tall-frame scenarios.
Code

pnpm11/cli/default-reporter/src/index.ts[R73-78]

+  const stream = opts.useStderr ? proc.stderr : proc.stdout
+  const write = stream.write.bind(stream)
+  const diff = createDiffer({
+    height: stream.rows,
+    width: stream.columns ?? outputMaxWidth,
+  })
Evidence
PR Compliance ID 2 requires cursor movement to be constrained to the current terminal height. The
new code constructs an ansi-diff differ with a fixed height: stream.rows and `width:
stream.columns ?? outputMaxWidth`, but does not update/recreate the differ when terminal dimensions
change, so the differ can operate with stale viewport bounds.

Interactive reporter cursor positioning must be height-aware and clamped to terminal viewport
pnpm11/cli/default-reporter/src/index.ts[73-78]

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 interactive reporter initializes `ansi-diff` with `height`/`width` only once, so terminal resizes can leave the differ using stale viewport dimensions.
## Issue Context
Compliance requires cursor positioning to be height-aware and clamped to the *current* terminal viewport. Without resize handling, a shrink can cause cursor-up logic to be computed against the old `stream.rows`, risking mispositioned updates and non-settling redraw when the frame is taller than the new viewport.
## Fix Focus Areas
- pnpm11/cli/default-reporter/src/index.ts[73-78]

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


6. Duplicate frame retention 🐞 Bug ➹ Performance
Description
In pacquet interactive mode, the previous frame is retained both as a full last_frame String
(cloned) and again inside the new differ as per-line owned Strings, increasing peak memory and
allocation work proportional to frame size (especially when lifecycle output is rendered uncollapsed
on failures). This is a bounded overhead (not unbounded growth), but it is on the hot redraw path
and can become noticeable for large multi-project frames.
Code

pacquet/crates/default-reporter/src/diff.rs[R136-178]

+#[derive(Clone)]
+struct Line {
+    raw: String,
+    row: usize,
+    length: usize,
+    height: usize,
+    remainder: usize,
+    newline: bool,
+}
+
+impl Line {
+    fn new(text: &str, row: usize, newline: bool, width: usize) -> Self {
+        let length = visible_width(text);
+        let (height, remainder) = match width {
+            0 => (0, length),
+            term_width => {
+                let line_height = length / term_width;
+                let line_remainder = length % term_width;
+                if line_height > 0 && line_remainder == 0 {
+                    (line_height - 1, term_width)
+                } else {
+                    (line_height, line_remainder)
+                }
+            }
+        };
+        Line { raw: text.to_string(), row, length, height, remainder, newline }
+    }
+
+    fn split(input: &str, width: usize) -> Vec<Self> {
+        let parts: Vec<&str> = input.split('\n').collect();
+        let count = parts.len();
+        let mut row_offset = 0;
+        parts
+            .into_iter()
+            .enumerate()
+            .map(|(idx, text)| {
+                let newline = idx < count - 1;
+                let line = Line::new(text, row_offset, newline, width);
+                row_offset += line.height + u8::from(newline) as usize;
+                line
+            })
+            .collect()
+    }
Evidence
The new differ stores each line as an owned String (Line.raw from text.to_string()), while the
reporter state simultaneously clones and retains the full frame in last_frame for deduplication;
on lifecycle failures, render_script can include the entire accumulated output, making the
retained frame (and thus the duplication) large.

pacquet/crates/default-reporter/src/diff.rs[136-178]
pacquet/crates/default-reporter/src/state.rs[327-338]
pacquet/crates/default-reporter/src/state.rs[736-757]

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

## Issue description
pacquet’s interactive reporter now retains the previous rendered frame in two owned representations:
1) `ReporterState.last_frame` stores a cloned full-frame `String` for deduplication.
2) `diff::Diff` stores the previous frame again as `Vec<Line>` where each `Line.raw` is an owned `String`.
This duplicates memory and increases allocation/copy work proportional to frame size.
### Issue Context
- The new differ needs access to the previous frame to compute a minimal update.
- `ReporterState` already keeps `last_frame` for “frame unchanged → Output::None” filtering.
- Some frames can become large (e.g., lifecycle failure paths render full accumulated output).
### Fix Focus Areas
- pacquet/crates/default-reporter/src/diff.rs[136-178]
- pacquet/crates/default-reporter/src/state.rs[327-338]
- pacquet/crates/default-reporter/src/state.rs[736-757]
### Suggested fix direction
Pick one primary source of truth for the previous frame:
- **Option A (preferred):** Remove/avoid `ReporterState.last_frame` in interactive mode and instead always emit `Output::Frame(frame)`; in `Sink::write_output`, call `diff.update_into(...)` and if it produces no terminal updates, skip the write. This eliminates the full-frame clone.
- **Option B:** Refactor `Diff` to store the previous frame as a single `String` plus line offset metadata (ranges) rather than per-line `String` copies, so `Diff` can diff against slices without duplicating the text.
Either approach reduces peak memory and avoids repeated per-line string allocations on every frame.

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


7. ANSI parsing desyncs cursor 🐞 Bug ☼ Reliability
Description
The new pacquet Diff computes wrapping/cursor moves from visible_width(), but visible_width()
only skips escape content until an ASCII letter and cut_line() may truncate in the middle of an
escape sequence, so lifecycle output containing non-trivial ANSI/OSC sequences can make Diff’s
internal (row,col) diverge from the real terminal cursor. Once desynced, subsequent Diff::update()
calls can emit incorrect ESC[A/B/C/D moves and clears, causing unstable or corrupted interactive
rendering.
Code

pacquet/crates/default-reporter/src/diff.rs[R146-161]

+impl Line {
+    fn new(text: &str, row: usize, newline: bool, width: usize) -> Self {
+        let length = visible_width(text);
+        let (height, remainder) = match width {
+            0 => (0, length),
+            term_width => {
+                let line_height = length / term_width;
+                let line_remainder = length % term_width;
+                if line_height > 0 && line_remainder == 0 {
+                    (line_height - 1, term_width)
+                } else {
+                    (line_height, line_remainder)
+                }
+            }
+        };
+        Line { raw: text.to_string(), row, length, height, remainder, newline }
Evidence
Diff::Line::new() uses visible_width() to compute length/height/remainder, which drives the
cursor-move deltas emitted by move_to(). Lifecycle output lines are incorporated into the frame
with only cut_line() truncation and no ANSI stripping, so ANSI/OSC sequences from scripts can
reach Diff; with the current simplistic escape skipping/truncation, the computed cursor position
can diverge from the terminal’s actual cursor state and break differential updates.

pacquet/crates/default-reporter/src/diff.rs[146-175]
pacquet/crates/default-reporter/src/format.rs[67-88]
pacquet/crates/default-reporter/src/format.rs[90-106]
pacquet/crates/default-reporter/src/state.rs[736-742]
pacquet/crates/default-reporter/src/state.rs[835-842]

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

## Issue description
`pacquet/crates/default-reporter`’s new differential renderer (`Diff`) relies on `format::visible_width()` and the frame’s (possibly truncated) raw text to compute cursor movements. The current `visible_width()` implementation only skips escape sequences until the next ASCII alphabetic character, and lifecycle output is truncated with `cut_line()` which is not ANSI-aware and can cut an escape sequence in half.
When lifecycle output contains OSC sequences (e.g. title-setting) or CSI sequences that don’t terminate with an ASCII letter, or when truncation produces an incomplete escape, `Diff`’s cursor math can diverge from the actual terminal cursor position and corrupt subsequent in-place updates.
### Issue Context
Lifecycle output (`LifecycleMessage::Stdio`) is inserted into the interactive frame without stripping escape sequences, and is only truncated by character count.
### Fix Focus Areas
- pacquet/crates/default-reporter/src/format.rs[67-106]
- pacquet/crates/default-reporter/src/state.rs[835-842]
- pacquet/crates/default-reporter/src/diff.rs[146-178]
### Suggested fix approach
1. Make `visible_width()` properly skip ANSI sequences:
- For CSI: after `ESC [` consume parameter/intermediate bytes until a final byte in `0x40..=0x7E` (not just alphabetic).
- For OSC: after `ESC ]` consume until BEL (`\x07`) or ST (`ESC \\`).
- Consider treating other common escape forms (DCS, etc.) conservatively (skip until terminator) to avoid desync.
2. Make truncation ANSI-safe for lifecycle output:
- Either strip escape sequences from lifecycle `line` before truncation, or implement an ANSI-aware truncate that counts only visible cells and never splits an escape sequence.
3. Add a regression test covering a lifecycle stdio line containing an OSC or `CSI ... ~` sequence (and/or a truncated ANSI sequence) to ensure `Diff::update()` remains stable and doesn’t misposition subsequent updates.

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


View more (8)
8. Carriage return desyncs differ 🐞 Bug ☼ Reliability
Description
pacquet’s new Diff computes cursor movement from visible_width() and only treats \n as a
cursor-moving control, but lifecycle output lines are ingested as raw text and can contain \r
(carriage return) which resets the terminal column. When such a line enters a rendered frame,
Diff’s internal (row,col) model can diverge from the terminal’s real cursor position and later
updates may emit incorrect cursor moves, corrupting interactive output.
Code

pacquet/crates/default-reporter/src/diff.rs[R147-199]

+    fn new(text: &str, row: usize, newline: bool, width: usize) -> Self {
+        let length = visible_width(text);
+        let (height, remainder) = match width {
+            0 => (0, length),
+            term_width => {
+                let line_height = length / term_width;
+                let line_remainder = length % term_width;
+                if line_height > 0 && line_remainder == 0 {
+                    (line_height - 1, term_width)
+                } else {
+                    (line_height, line_remainder)
+                }
+            }
+        };
+        Line { raw: text.to_string(), row, length, height, remainder, newline }
+    }
+
+    fn split(input: &str, width: usize) -> Vec<Self> {
+        let parts: Vec<&str> = input.split('\n').collect();
+        let count = parts.len();
+        let mut row_offset = 0;
+        parts
+            .into_iter()
+            .enumerate()
+            .map(|(idx, text)| {
+                let newline = idx < count - 1;
+                let line = Line::new(text, row_offset, newline, width);
+                row_offset += line.height + u8::from(newline) as usize;
+                line
+            })
+            .collect()
+    }
+
+    /// Inline diff: if only a few characters changed, write just those
+    /// instead of the whole line. Only attempted on lines without ANSI
+    /// escape codes (plain text like progress lines).
+    fn try_inline_diff(
+        &self,
+        other: &Self,
+        out: &mut String,
+        col: &mut usize,
+        row: &mut usize,
+        width: usize,
+    ) -> bool {
+        if self.length != other.length
+            || self.row != other.row
+            || !self.newline
+            || !other.newline
+            || self.raw.contains('\u{1b}')
+            || other.raw.contains('\u{1b}')
+        {
+            return false;
+        }
Evidence
Lifecycle script output is emitted as raw per-line text and passed through formatting without
stripping control chars, while Diff uses visible_width() and only gates inline diff on presence
of ESC. This makes \r (and similar controls) capable of altering the real cursor without being
reflected in Diff’s internal cursor math.

pacquet/crates/executor/src/lifecycle.rs[412-444]
pacquet/crates/reporter/src/lib.rs[497-506]
pacquet/crates/default-reporter/src/state.rs[835-842]
pacquet/crates/default-reporter/src/format.rs[67-87]
pacquet/crates/default-reporter/src/diff.rs[147-199]

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

## Issue description
`pacquet/crates/default-reporter`’s new `Diff` assumes frame content advances the cursor only via printable characters (counted by `visible_width`) and `\n`. Raw lifecycle output lines can contain control characters like `\r` that change the terminal cursor (column reset) without contributing to visible width, which can desync `Diff`’s internal cursor tracking and cause incorrect relative cursor moves on later updates.
### Issue Context
Lifecycle script stdout/stderr is emitted as raw text and then formatted into the interactive frame without stripping control characters. `visible_width()` only skips ANSI CSI sequences starting with ESC, so `\r` is treated as a visible character (width +1) even though terminals interpret it as a cursor movement.
### Fix Focus Areas
- pacquet/crates/default-reporter/src/state.rs[835-842]
- pacquet/crates/default-reporter/src/diff.rs[147-217]
- pacquet/crates/default-reporter/src/format.rs[67-87]
- pacquet/crates/executor/src/lifecycle.rs[412-444]
- pacquet/crates/reporter/src/lib.rs[497-506]
### Suggested fix approach
- Prefer sanitizing at ingestion/formatting time (so both rendering and width math agree): in `format_indented_output` (or immediately when receiving `LifecycleMessage::Stdio`), strip or replace ASCII control characters other than `\n` (e.g. remove `\r`, `\x08`, etc., or render them as printable placeholders like `^M`).
- Additionally (defense-in-depth), consider making `Diff::try_inline_diff()` bail out when either line contains non-printable control characters (not just `ESC`).
- Add a targeted unit test to demonstrate the problem and the fix, e.g. a lifecycle line containing `"foo\rbar"` should not desync subsequent updates (or should be sanitized to a stable printable form).

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


9. Non-atomic frame writes 🐞 Bug ☼ Reliability
Description
pacquet’s TTY frame update is emitted via three separate stdout writes ("\r", then the diff, then
"\x1b[K\x1b[0J"), allowing external-process output on the same TTY to interleave between them and
corrupt cursor/erase control sequences. This can partially reintroduce the “external output
remnants”/screen corruption class of issues this PR is explicitly trying to harden against.
Code

pacquet/crates/default-reporter/src/lib.rs[R139-152]

+            Output::Frame(mut frame) => {
+                // A trailing newline keeps an interactive prompt on a fresh line
+                // below the frame rather than joined onto its last line, and it
+                // leaves the differ's tracked cursor at column 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).
+                if !frame.ends_with('\n') {
+                    frame.push('\n');
           }
-                // Reset 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.
-                buf.push('\r');
-                buf.push_str("\x1b[0J");
-                buf.push_str(&frame);
-                buf.push('\n');
-                let _ = out.write_all(buf.as_bytes());
-                self.prev_rows = count_rows(&frame, self.columns);
+                let diff_output = self.diff.update(&frame);
+                let _ = out.write_all(b"\r");
+                let _ = out.write_all(diff_output.as_bytes());
+                let _ = out.write_all(b"\x1b[K\x1b[0J");
       }
Evidence
The pacquet sink currently performs three separate writes for a single frame update, which increases
the chance that unrelated output (from other processes sharing the TTY) lands between cursor reset,
diff output, and erase sequences. The TypeScript reporter demonstrates the intended single-write
pattern for the same update semantics.

pacquet/crates/default-reporter/src/lib.rs[139-152]
pnpm11/cli/default-reporter/src/index.ts[73-101]

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

## Issue description
`Sink::write_output()` emits the interactive frame update using three `write_all()` calls. Because external processes can write to the same TTY concurrently, output can interleave between these calls, producing torn ANSI sequences and leaving artifacts.
### Issue Context
The TS implementation writes the entire update as a single string, reducing the interleaving window. pacquet should match that robustness when trying to clear external output below/after the frame.
### Fix Focus Areas
- pacquet/crates/default-reporter/src/lib.rs[139-152]
### Suggested fix
- Build a single buffer containing: `"\r" + diff_output + "\x1b[K\x1b[0J"` and `write_all()` once.
- Prefer reusing an allocation (e.g., store a `String`/`Vec<u8>` scratch buffer in `Sink`) if you want to avoid per-frame heap churn, but keep the terminal write atomic.

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


10. Non-TTY differ initialization 🐞 Bug ☼ Reliability
Description
initDefaultReporter() now always constructs an ansi-diff differ in the non-append-only path
using stream.rows/stream.columns, but the package’s runCli() wrapper calls it without
switching to append-only when stdout is not a TTY (a common case when reading NDJSON from stdin).
This can lead to interactive ANSI control output being written into redirected output and to the
differ being configured with missing terminal dimensions.
Code

pnpm11/cli/default-reporter/src/index.ts[R73-78]

+  const stream = opts.useStderr ? proc.stderr : proc.stdout
+  const write = stream.write.bind(stream)
+  const diff = createDiffer({
+    height: stream.rows,
+    width: stream.columns ?? outputMaxWidth,
+  })
Evidence
The default-reporter CLI wrapper does not follow pnpm core’s non-TTY append-only selection, yet
initDefaultReporter()’s interactive path now depends on terminal geometry (rows/columns) for
ansi-diff initialization.

pnpm11/cli/default-reporter/src/index.ts[48-78]
pnpm11/cli/default-reporter/src/cli.ts[22-28]
pnpm11/pnpm/src/main.ts[193-198]

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

## Issue description
`initDefaultReporter()` initializes `ansi-diff` with `height: stream.rows` and `width: stream.columns ?? outputMaxWidth` whenever `appendOnly` is not explicitly enabled. The `@pnpm/cli.default-reporter` package also ships a CLI (`src/cli.ts`) that feeds it log lines from stdin, but it does not force append-only when stdout is redirected/non-TTY, unlike pnpm’s main CLI.
### Issue Context
- pnpm core explicitly selects `append-only` when `!process.stdout.isTTY`, so the default reporter’s interactive mode is normally TTY-only.
- The default-reporter package’s own `runCli()` helper is a realistic path where stdin is piped, and stdout may also be piped/redirected.
### Fix Focus Areas
- pnpm11/cli/default-reporter/src/index.ts[73-78]
- pnpm11/cli/default-reporter/src/cli.ts[22-28]
### Suggested fix
Implement a non-TTY safety fallback:
1) In `src/cli.ts`, set `reportingOptions.appendOnly` based on `process.stdout.isTTY` (and/or `process.stderr.isTTY` if using stderr).
2) Additionally (defensive), in `initDefaultReporter()` treat `stream.rows`/`stream.columns` being null/undefined (or `stream.isTTY !== true`) as a signal to use append-only mode (or to supply safe defaults) so the differ is not initialized with missing terminal geometry.

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


11. visible_width miscounts wide glyphs 📎 Requirement gap ≡ Correctness
Description
The new pacquet frame differ computes wrapped-row height using visible_width, which counts 1
column per Unicode scalar instead of terminal display width (wcwidth). This can undercount
soft-wrapped physical rows for CJK/emoji text and misposition cursor moves during redraws.
Code

pacquet/crates/default-reporter/src/diff.rs[R148-160]

+        let length = visible_width(text);
+        let (height, remainder) = match width {
+            0 => (0, length),
+            term_width => {
+                let line_height = length / term_width;
+                let line_remainder = length % term_width;
+                if line_height > 0 && line_remainder == 0 {
+                    (line_height - 1, term_width)
+                } else {
+                    (line_height, line_remainder)
+                }
+            }
+        };
Evidence
Rule 4 requires redraw positioning to account for soft-wrapped physical rows. The new Line::new()
uses visible_width(text) to compute height and remainder, and the file comment states
visible_width counts 1 column per char (not wcwidth), which makes row calculations incorrect for
wide glyphs that wrap.

Row counting for redraw positioning must account for soft-wrapped lines
pacquet/crates/default-reporter/src/diff.rs[14-18]
pacquet/crates/default-reporter/src/diff.rs[148-160]

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

## Issue description
`Diff` computes line `height`/`remainder` using `visible_width`, which counts 1 column per char. This miscounts soft-wrapped physical rows for wide glyphs (CJK/emoji), risking incorrect cursor positioning during interactive redraw.
## Issue Context
Compliance requires row counting to account for soft-wrapped lines accurately. The current implementation explicitly documents that wide characters are undercounted.
## Fix Focus Areas
- pacquet/crates/default-reporter/src/diff.rs[148-160]
- pacquet/crates/default-reporter/src/diff.rs[14-18]

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


12. Pacquet Diff width vs frame width off-by-two 🐞 Bug ≡ Correctness
Description
In pacquet's Sink::new(), frame content is formatted at width = columns.saturating_sub(2) but
Diff::new() receives the full columns value. The differ's soft-wrap height/remainder
calculations therefore use a 2-column-wider value than the actual frame content, causing incorrect
cursor positioning for lines near the terminal width.
Code

pacquet/crates/default-reporter/src/lib.rs[R106-109]

let width = if is_tty { columns.saturating_sub(2) } else { 80 };
let colors = Colors { enabled: is_tty && std::env::var_os("NO_COLOR").is_none() };
let state = ReporterState::new(cwd(), width, colors, append_only);
-        // pnpm's `throttleProgress`: 200ms in place, 1000ms append-only.
+        let diff = diff::Diff::new(columns);
Evidence
lib.rs line 106 sets width = columns.saturating_sub(2) and passes it to ReporterState::new() for
frame formatting. Line 109 passes the full columns to Diff::new(). In diff.rs, Line::new()
uses this width for height = length / term_width and remainder = length % term_width. A frame
line of exactly columns-1 visible chars has height=0 in the frame (fits within columns-2 is false,
but fits within columns-1 is true), but the differ computes height=0 with the full columns value
too—the mismatch becomes observable when a line is exactly columns-1 or columns chars wide,
causing the differ to emit wrong cursor-up/down moves.

pacquet/crates/default-reporter/src/lib.rs[106-109]
pacquet/crates/default-reporter/src/diff.rs[147-162]

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

## Issue description
`Diff::new()` is initialized with `columns` but frame content is formatted at `columns - 2`. The differ's wrap math uses a wider value than the actual content, causing incorrect height/remainder for lines near the terminal edge.
## Issue Context
`Line::new()` in diff.rs computes `height = length / term_width` and `remainder = length % term_width`. These must match the width used by `ReporterState` to format the frame.
## Fix Focus Areas
- pacquet/crates/default-reporter/src/lib.rs[106-109]: Change `diff::Diff::new(columns)` to `diff::Diff::new(width)` so the differ uses the same `columns - 2` value as the frame formatter.

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


13. Redundant row-clearing loop 🐞 Bug ➹ Performance
Description
In pacquet, Diff::update() clears removed trailing rows by emitting per-row "\x1b[0K\n", but the
sink already appends "\x1b[K\x1b[0J" after every update, which already clears the rest of the frame
area. This duplicates terminal output and work proportional to the number of removed rows when
frames shrink (worst-case large in tall-frame scenarios).
Code

pacquet/crates/default-reporter/src/diff.rs[R96-105]

+        if let Some(old_last) = self.lines.last() {
+            let new_last_row = next.last().map_or(0, |line| line.row + line.height);
+            let old_last_row = old_last.row + old_last.height;
+            if new_last_row < old_last_row {
+                self.move_to(&mut out, 0, new_last_row);
+                for _ in new_last_row..old_last_row {
+                    out.push_str("\x1b[0K\n");
+                    self.row += 1;
+                }
+            }
Evidence
Diff::update() emits per-row clearing when shrinking (\x1b[0K\n loop). The sink always appends
\x1b[K\x1b[0J after applying the diff, and \x1b[0J already clears everything below the cursor
(i.e., the removed trailing rows), making the loop redundant overhead.

pacquet/crates/default-reporter/src/diff.rs[96-105]
pacquet/crates/default-reporter/src/lib.rs[139-150]

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

## Issue description
`Diff::update()` emits a loop of `\x1b[0K\n` sequences to clear rows when the new frame is shorter than the old frame. In this PR, the sink already appends `\x1b[K\x1b[0J` after every update (`\x1b[0J` erases from cursor to end of display), so the per-row clearing is largely redundant and increases output size and CPU/terminal work when frames shrink.
### Issue Context
The module docstring explicitly states the caller wraps output with `\r` + `\x1b[0J`, and the sink does that (and also adds `\x1b[K`). With the cursor positioned at the end of the rendered frame, `\x1b[0J` clears all removed lines below the frame.
### Fix Focus Areas
- pacquet/crates/default-reporter/src/diff.rs[96-105]
- pacquet/crates/default-reporter/src/lib.rs[139-150]

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


14. Unicode inline-diff mispositions cursor 🐞 Bug ≡ Correctness
Description
pacquet’s try_inline_diff() computes the shared-prefix offset as a char count and uses it as a
terminal column in ESC[C/ESC[D moves, which is wrong for double-width glyphs (CJK/emoji) and can
corrupt in-place rendering. This can be triggered by non-ANSI frames (e.g. NO_COLOR) that include
user filesystem paths or other Unicode text, making the differential update place and overwrite text
at the wrong screen column.
Code

pacquet/crates/default-reporter/src/diff.rs[R181-193]

+        let a_chars: Vec<char> = self.raw.chars().collect();
+        let b_chars: Vec<char> = other.raw.chars().collect();
+        let left = a_chars.iter().zip(&b_chars).take_while(|(a, b)| a == b).count();
+        let right =
+            a_chars.iter().rev().zip(b_chars.iter().rev()).take_while(|(a, b)| a == b).count();
+        let changed_len = a_chars.len().saturating_sub(left + right);
+        if left + right <= 4 || left + changed_len >= width.saturating_sub(1) {
+            return false;
+        }
+        move_to(out, x, y, left, self.y);
+        let changed: String = a_chars[left..left + changed_len].iter().collect();
+        out.push_str(&changed);
+        *x = left + changed_len;
Evidence
try_inline_diff() derives left via .count() over zipped char iterators and passes that
directly to move_to() as the target x-coordinate (terminal column). The renderer can include
user-controlled filesystem paths (which may contain arbitrary Unicode) in frame text, and when
NO_COLOR disables ANSI, the inline-diff path is eligible and can be taken.

pacquet/crates/default-reporter/src/diff.rs[161-194]
pacquet/crates/default-reporter/src/lib.rs[61-65]
pacquet/crates/default-reporter/src/state.rs[923-931]

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

## Issue description
`try_inline_diff()` uses `left` (a character-count prefix length) as the terminal column when emitting cursor movement. Terminals move by *cells*, and for wide glyphs (e.g. CJK/emoji) character count != cell width, so the inline diff can write at the wrong column and corrupt the frame.
### Issue Context
This is new behavior introduced by the pacquet port’s inline-diff optimization. It’s most likely to show up when frames are plain-text (no ANSI), e.g. when `NO_COLOR` disables coloring, and the output contains Unicode from paths.
### Fix Focus Areas
- pacquet/crates/default-reporter/src/diff.rs[161-195]
Suggested implementation direction (pick one):
1) **Safe & simple**: only attempt inline diffs for ASCII-only lines:
- `if !self.raw.is_ascii() || !other.raw.is_ascii() { return false; }`
2) **Correct & complete**: make inline-diff cursor positioning display-width-aware:
- compute prefix/suffix boundaries in *cell widths* and slice accordingly (may require a width function that matches terminal cell behavior).

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


15. Per-frame format allocation 🐞 Bug ➹ Performance
Description
pacquet’s TTY frame rendering now allocates an additional string each refresh by calling
format!("\r{diff_output}\x1b[0J") before writing. This adds avoidable allocation/copy overhead to
the high-frequency redraw path and can increase CPU/memory churn during large installs or frequent
progress ticks.
Code

pacquet/crates/default-reporter/src/lib.rs[R139-142]

Output::Frame(frame) => {
-                let mut buf = String::new();
-                if self.prev_rows > 0 {
-                    let _ = write!(buf, "\x1b[{}A", self.prev_rows);
-                }
-                // Reset 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.
-                buf.push('\r');
-                buf.push_str("\x1b[0J");
-                buf.push_str(&frame);
-                buf.push('\n');
-                let _ = out.write_all(buf.as_bytes());
-                self.prev_rows = count_rows(&frame, self.columns);
+                let diff_output = self.diff.update(&frame);
+                let _ = out.write_all(format!("\r{diff_output}\x1b[0J").as_bytes());
}
Evidence
The Output::Frame arm constructs diff_output (already a String) and then immediately allocates
another String via format! to prepend \r and append \x1b[0J. This code is executed on every
frame refresh, which is the hot-path for interactive progress rendering.

pacquet/crates/default-reporter/src/lib.rs[115-126]
pacquet/crates/default-reporter/src/lib.rs[139-142]

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

## Issue description
`Output::Frame` currently does `format!("\r{diff_output}\x1b[0J")`, creating a new owned string per refresh in addition to the already-allocated `diff_output`. This adds extra allocations and copies on the frequent redraw path.
### Issue Context
This sits directly in the stdout redraw loop for interactive TTY mode.
### Fix Focus Areas
- pacquet/crates/default-reporter/src/lib.rs[129-145]
Suggested implementation direction:
- Keep the write atomic (single `write_all`) but avoid `format!` by building into a pre-sized buffer:
- `let mut buf = String::with_capacity(1 + diff_output.len() + 4);`
- `buf.push('\r'); buf.push_str(&diff_output); buf.push_str("\x1b[0J");`
- `out.write_all(buf.as_bytes())`

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



Informational

16. Inline diff allocates chars 🐞 Bug ➹ Performance ⭐ New
Description
pacquet’s Diff::try_inline_diff() allocates Vec<char> copies of both the new and old line, plus
a new String for the changed segment, whenever the inline-diff heuristic is taken. This is an
avoidable CPU/allocation cost in the interactive rendering loop for frames/lines that are eligible
for inline diff (plain-text, same-length, small edits).
Code

pacquet/crates/default-reporter/src/diff.rs[R191-217]

+        if self.length != other.length
+            || self.row != other.row
+            || !self.newline
+            || !other.newline
+            || self.raw.contains('\u{1b}')
+            || other.raw.contains('\u{1b}')
+        {
+            return false;
+        }
+        let self_chars: Vec<char> = self...

@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: 3

🤖 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 `@pnpm11/cli/default-reporter/src/index.ts`:
- Around line 73-76: The differ in createDiffer is sized from proc.stdout even
though DefaultReporter may render to proc.stderr when useStderr is enabled, so
update the geometry source to match the actual output stream being written to.
Adjust the logic around the DefaultReporter/createDiffer setup to choose rows
and columns from the active render stream, using proc.stderr when useStderr is
true and proc.stdout otherwise, so ansi-diff gets the correct dimensions.

In `@pnpm11/cli/default-reporter/test/reporterRenderer.ts`:
- Around line 20-22: The reporter tests are using the settle() helper as a fixed
30 ms sleep, which makes them flaky on slower CI. Replace the time-based wait
with a deterministic wait on an observable reporter signal, such as a change in
writes, a specific sticky-line entry, or the next expected progress write after
invoking the reporter logic. Update the affected assertions to await the exact
state they need, and reuse settle() only if it waits for that concrete condition
rather than wall-clock time.
- Around line 73-77: The reporter test only checks the first and last writes, so
it can miss a duplicated sticky verdict in the middle; update the assertion in
reporterRenderer’s sticky-verdict test to verify every write after the first
render does not contain the sticky block. Use the existing writes array in the
test and the stripAnsi checks around “Lockfile passes supply-chain policies” to
assert no subsequent entry reprints that text after the initial appearance.
🪄 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: 47fa8528-75b6-48e5-ac4b-7779236b54c7

📥 Commits

Reviewing files that changed from the base of the PR and between f8e5a0d and 935c1fe.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !pnpm-lock.yaml
📒 Files selected for processing (5)
  • .changeset/restore-ansi-diff.md
  • pnpm-workspace.yaml
  • pnpm11/cli/default-reporter/package.json
  • pnpm11/cli/default-reporter/src/index.ts
  • pnpm11/cli/default-reporter/test/reporterRenderer.ts

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 935c1fe

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 8359f95

1 similar comment
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 8359f95

@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Commit: 6fb996adbfed

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 4.040 ± 0.163 3.858 4.386 1.92 ± 0.12
pacquet@main 3.956 ± 0.116 3.852 4.171 1.88 ± 0.11
pnpr@HEAD 2.101 ± 0.107 1.976 2.241 1.00
pnpr@main 2.161 ± 0.136 1.960 2.357 1.03 ± 0.08
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.04023242602,
      "stddev": 0.16297494369737053,
      "median": 3.9942584072200003,
      "user": 3.902401799999999,
      "system": 3.37636738,
      "min": 3.85822772022,
      "max": 4.386433241220001,
      "times": [
        4.14305278822,
        4.17253396122,
        3.8988361452199998,
        4.10917139922,
        4.386433241220001,
        3.96997172822,
        3.85822772022,
        4.0185450862200005,
        3.93388181622,
        3.91167037422
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.9562374204200004,
      "stddev": 0.11640007381476733,
      "median": 3.9062300417199998,
      "user": 3.9197683999999997,
      "system": 3.3459535799999998,
      "min": 3.85170351222,
      "max": 4.17055854322,
      "times": [
        4.12941787222,
        3.85874949322,
        3.85170351222,
        4.17055854322,
        4.04164126522,
        3.9096435122199997,
        3.9397079382199998,
        3.90281657122,
        3.9023315792199997,
        3.85580391722
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.1009889123199996,
      "stddev": 0.10680160968424873,
      "median": 2.0673269937199996,
      "user": 2.7198554999999995,
      "system": 2.87009518,
      "min": 1.97612854322,
      "max": 2.2413004962199996,
      "times": [
        2.0197827892199998,
        1.97612854322,
        2.23391190722,
        2.0463781532199996,
        1.97669412722,
        2.2413004962199996,
        2.03451025522,
        2.0882758342199996,
        2.2271072522199997,
        2.1657997652199996
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.16106185512,
      "stddev": 0.13562892317747247,
      "median": 2.20318067372,
      "user": 2.6988987,
      "system": 2.88268368,
      "min": 1.95966859622,
      "max": 2.35652720622,
      "times": [
        2.21576502722,
        2.1228377362199997,
        1.97140313222,
        2.19572525122,
        2.2286178792199998,
        2.35652720622,
        2.3121432392199996,
        1.95966859622,
        2.21063609622,
        2.03729438722
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 644.3 ± 9.3 630.3 657.7 1.01 ± 0.02
pacquet@main 635.3 ± 9.1 620.2 653.2 1.00
pnpr@HEAD 713.9 ± 90.3 666.0 968.6 1.12 ± 0.14
pnpr@main 690.3 ± 20.6 665.4 736.9 1.09 ± 0.04
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6443029957599999,
      "stddev": 0.009318673484328318,
      "median": 0.6432083981600001,
      "user": 0.3901028,
      "system": 1.32245758,
      "min": 0.63030807666,
      "max": 0.65770906966,
      "times": [
        0.63666744766,
        0.6552924396600001,
        0.63829415666,
        0.65770906966,
        0.64649867766,
        0.63030807666,
        0.65500128266,
        0.63684201066,
        0.64061552966,
        0.64580126666
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.63530658336,
      "stddev": 0.009075043929393532,
      "median": 0.63483265166,
      "user": 0.3713532,
      "system": 1.30797428,
      "min": 0.6201502306600001,
      "max": 0.6531826886600001,
      "times": [
        0.63242417066,
        0.6268708766600001,
        0.64411450666,
        0.63158554066,
        0.63523316166,
        0.6398393546600001,
        0.6531826886600001,
        0.6201502306600001,
        0.63468794166,
        0.6349773616600001
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.71387516566,
      "stddev": 0.09026405292753106,
      "median": 0.6883881836600001,
      "user": 0.4090644,
      "system": 1.35477268,
      "min": 0.66599244266,
      "max": 0.9686076556600001,
      "times": [
        0.69093931366,
        0.6834826096600001,
        0.66599244266,
        0.68583705366,
        0.67564913766,
        0.69855362666,
        0.69708853066,
        0.7006641676600001,
        0.67193711866,
        0.9686076556600001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.69028119276,
      "stddev": 0.02058241036034452,
      "median": 0.6927431161600001,
      "user": 0.38197509999999996,
      "system": 1.3548669800000002,
      "min": 0.6653587696600001,
      "max": 0.7368641326600001,
      "times": [
        0.6988794016600001,
        0.68150317666,
        0.69861737366,
        0.6653587696600001,
        0.69256023466,
        0.6703137866600001,
        0.67103838066,
        0.69475067366,
        0.7368641326600001,
        0.6929259976600001
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.220 ± 0.053 4.132 4.296 1.92 ± 0.10
pacquet@main 4.219 ± 0.066 4.098 4.357 1.92 ± 0.10
pnpr@HEAD 2.195 ± 0.113 2.055 2.381 1.00
pnpr@main 2.232 ± 0.171 1.978 2.456 1.02 ± 0.09
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.22013982302,
      "stddev": 0.05337813253976519,
      "median": 4.22364630542,
      "user": 3.8210879999999996,
      "system": 3.2773365000000005,
      "min": 4.13187914692,
      "max": 4.29633080392,
      "times": [
        4.28679111692,
        4.20981567292,
        4.15015677292,
        4.13187914692,
        4.18962068292,
        4.21729347492,
        4.23147427992,
        4.22999913592,
        4.29633080392,
        4.25803714292
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.21856663312,
      "stddev": 0.06627048737547761,
      "median": 4.20820094692,
      "user": 3.8118811999999993,
      "system": 3.2779556999999997,
      "min": 4.09797089292,
      "max": 4.35652365492,
      "times": [
        4.24412168592,
        4.19831035392,
        4.20207195592,
        4.21052154092,
        4.17210546892,
        4.2058803529199995,
        4.23550429492,
        4.26265612992,
        4.09797089292,
        4.35652365492
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.19500767012,
      "stddev": 0.11276061482110641,
      "median": 2.14992208642,
      "user": 2.5560032,
      "system": 2.7880747,
      "min": 2.05472537692,
      "max": 2.38058135292,
      "times": [
        2.12604874192,
        2.13544092992,
        2.05472537692,
        2.31807175592,
        2.08904473792,
        2.22109953992,
        2.1644032429199997,
        2.33408172192,
        2.38058135292,
        2.12657930092
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.2318869251199995,
      "stddev": 0.17124758028635934,
      "median": 2.2723088044199997,
      "user": 2.5362066,
      "system": 2.7873469,
      "min": 1.97765763092,
      "max": 2.45622668292,
      "times": [
        2.45622668292,
        2.33666207492,
        1.98722575692,
        2.36739932592,
        2.18207216192,
        2.31057726592,
        1.97765763092,
        2.39332682092,
        2.07368118792,
        2.2340403429199998
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.381 ± 0.012 1.366 1.401 2.13 ± 0.03
pacquet@main 1.387 ± 0.075 1.348 1.599 2.14 ± 0.12
pnpr@HEAD 0.671 ± 0.033 0.646 0.762 1.03 ± 0.05
pnpr@main 0.648 ± 0.008 0.635 0.659 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.3807709204800003,
      "stddev": 0.012380852075941022,
      "median": 1.37923366188,
      "user": 1.3413869199999997,
      "system": 1.74325728,
      "min": 1.36643363588,
      "max": 1.4011470028800002,
      "times": [
        1.38068341588,
        1.3672068188800002,
        1.4011470028800002,
        1.3895595848800002,
        1.38477683688,
        1.3974634028800001,
        1.36643363588,
        1.37778390788,
        1.3741220578800002,
        1.36853254088
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.38655501118,
      "stddev": 0.07536432676660415,
      "median": 1.36208052738,
      "user": 1.3125663199999997,
      "system": 1.70842588,
      "min": 1.3475363428800002,
      "max": 1.5988863128800002,
      "times": [
        1.37590062688,
        1.5988863128800002,
        1.3560200958800002,
        1.36545363888,
        1.37626668988,
        1.3475363428800002,
        1.35870741588,
        1.3562459178800002,
        1.3527474488800002,
        1.3777856218800002
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.67117803338,
      "stddev": 0.03347406318517879,
      "median": 0.6628736098800001,
      "user": 0.35202432000000006,
      "system": 1.30030008,
      "min": 0.64625809488,
      "max": 0.76226442888,
      "times": [
        0.66237791288,
        0.64625809488,
        0.65392173288,
        0.64739019888,
        0.76226442888,
        0.6633693068800001,
        0.67622080188,
        0.67374660588,
        0.66429675688,
        0.66193449388
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6484898949800001,
      "stddev": 0.008222503246224302,
      "median": 0.64863895438,
      "user": 0.31801872,
      "system": 1.28419408,
      "min": 0.63504486988,
      "max": 0.65915258188,
      "times": [
        0.64271068088,
        0.64942009888,
        0.65658973888,
        0.64266044288,
        0.63504486988,
        0.65262734788,
        0.64044745488,
        0.65915258188,
        0.65838792388,
        0.64785780988
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 3.055 ± 0.048 2.997 3.162 4.71 ± 0.10
pacquet@main 3.016 ± 0.017 2.988 3.052 4.65 ± 0.06
pnpr@HEAD 0.670 ± 0.012 0.651 0.684 1.03 ± 0.02
pnpr@main 0.649 ± 0.008 0.637 0.666 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 3.0546317284199995,
      "stddev": 0.048033795995897674,
      "median": 3.04471167182,
      "user": 1.8037268800000004,
      "system": 2.01147858,
      "min": 2.99748853782,
      "max": 3.16152963482,
      "times": [
        2.99748853782,
        3.07123209282,
        3.0092442128199997,
        3.0452275418199997,
        3.0881493988199997,
        3.07500201682,
        3.01150309382,
        3.16152963482,
        3.04419580182,
        3.0427449528199997
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.0158926195199998,
      "stddev": 0.016724959251692068,
      "median": 3.01339305632,
      "user": 1.75189938,
      "system": 1.9993786799999995,
      "min": 2.98818732082,
      "max": 3.05188848382,
      "times": [
        3.00300975382,
        3.01232567382,
        3.02620593082,
        3.02617039682,
        3.01395679582,
        3.0155519268199997,
        3.01282931682,
        2.98818732082,
        3.05188848382,
        3.00880059582
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6695108290199998,
      "stddev": 0.011740548468366831,
      "median": 0.6698445363200001,
      "user": 0.34496977999999995,
      "system": 1.31073588,
      "min": 0.65062622182,
      "max": 0.68380402982,
      "times": [
        0.65724556382,
        0.65837784382,
        0.68177287882,
        0.66711803682,
        0.67932636682,
        0.66407976982,
        0.67257103582,
        0.65062622182,
        0.68380402982,
        0.68018654282
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.64909312562,
      "stddev": 0.008259728924758953,
      "median": 0.64813122682,
      "user": 0.3350743799999999,
      "system": 1.2641922799999998,
      "min": 0.63679997882,
      "max": 0.66629852982,
      "times": [
        0.64743957882,
        0.64839381482,
        0.64417944582,
        0.65647025482,
        0.63679997882,
        0.64994010682,
        0.64786863882,
        0.66629852982,
        0.64076775382,
        0.65277315382
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 6.758 ± 0.163 6.574 7.032 1.40 ± 0.05
pacquet@main 6.809 ± 0.184 6.592 7.103 1.41 ± 0.05
pnpr@HEAD 4.842 ± 0.112 4.752 5.119 1.00
pnpr@main 4.871 ± 0.054 4.798 4.991 1.01 ± 0.03
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 6.758182756679998,
      "stddev": 0.16251191726906428,
      "median": 6.6841924847800005,
      "user": 4.075884820000001,
      "system": 3.6476667399999996,
      "min": 6.57428153728,
      "max": 7.03176838028,
      "times": [
        6.73828741528,
        6.67528608828,
        7.02973257928,
        6.68810319828,
        6.86617460028,
        6.67645557228,
        7.03176838028,
        6.680281771280001,
        6.621456424280001,
        6.57428153728
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 6.8089628182799995,
      "stddev": 0.18444629337322235,
      "median": 6.734120967780001,
      "user": 4.04005412,
      "system": 3.66157444,
      "min": 6.59239039028,
      "max": 7.10332520928,
      "times": [
        6.77265857328,
        6.975362625280001,
        6.629149416280001,
        6.68812777028,
        6.99751126828,
        7.10332520928,
        6.695583362280001,
        6.66263076028,
        6.59239039028,
        6.97288880728
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 4.84177453788,
      "stddev": 0.11164007810040211,
      "median": 4.82685081378,
      "user": 2.83000792,
      "system": 3.08916594,
      "min": 4.75197609928,
      "max": 5.1185402282800005,
      "times": [
        4.75485135028,
        4.8650274832800005,
        4.89053003128,
        5.1185402282800005,
        4.753538580280001,
        4.8688990082800006,
        4.79953602528,
        4.76068097028,
        4.75197609928,
        4.85416560228
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 4.870695733380001,
      "stddev": 0.05428566292421363,
      "median": 4.867455796780001,
      "user": 2.8150612199999996,
      "system": 3.1030313400000002,
      "min": 4.7984344322800006,
      "max": 4.991076312280001,
      "times": [
        4.88594138928,
        4.79896298328,
        4.858696972280001,
        4.894029217280001,
        4.853549511280001,
        4.891354922280001,
        4.868159637280001,
        4.991076312280001,
        4.866751956280001,
        4.7984344322800006
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12698
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,220.14 ms
(-7.36%)Baseline: 4,555.61 ms
5,466.73 ms
(77.20%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
3,054.63 ms
(-0.11%)Baseline: 3,057.92 ms
3,669.51 ms
(83.24%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,380.77 ms
(+1.21%)Baseline: 1,364.22 ms
1,637.06 ms
(84.34%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
4,040.23 ms
(-9.15%)Baseline: 4,447.32 ms
5,336.79 ms
(75.71%)
isolated-linker.fresh-restore.cold-cache.cold-store.cold-pnpr📈 view plot
🚷 view threshold
6,758.18 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
644.30 ms
(+2.52%)Baseline: 628.47 ms
754.16 ms
(85.43%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12698
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,195.01 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
669.51 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
671.18 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,100.99 ms
isolated-linker.fresh-restore.cold-cache.cold-store.cold-pnpr📈 view plot
⚠️ NO THRESHOLD
4,841.77 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
713.88 ms
🐰 View full continuous benchmarking report in Bencher

@aqeelat aqeelat force-pushed the fix/restore-ansi-diff-renderer branch from 8359f95 to f3fa416 Compare June 27, 2026 19:32
@github-actions github-actions Bot added the reviewed: coderabbit CodeRabbit submitted an approving review label Jun 27, 2026
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

1 similar comment
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

@zkochan

zkochan commented Jun 29, 2026

Copy link
Copy Markdown
Member

The same fix should be implemented in pacquet if you want this to be merged.

@aqeelat

aqeelat commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

The same fix should be implemented in pacquet if you want this to be merged.

@zkochan pacquet doesn't have the same issue 👀

@aqeelat

aqeelat commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

@zkochan on second thought, ok I'll add the same fix to pacquet. Three ways to go about it:

  1. Use ansi-diff from crates.io — but it pulls in two extra crates (regex + lazy_static), and the source repo (substack/ansi-diff.rs) returns 404 so I can't audit or contribute fixes.

  2. Port the algorithm into the pnpm monorepo as a module inside pacquet-default-reporter. no external deps, but not reusable outside the workspace.

  3. Publish ansi-diff-rs as a standalone crate (zero deps, same algorithm, char-loop ANSI parsing) and depend on it from pacquet. I'd publish it under my name since the original repo is gone and the algorithm is well-understood. Would that be fine?

I lean toward 3 unless you'd prefer to keep it internal (2) or you're OK with the existing crate's deps (1).

Copilot AI review requested due to automatic review settings July 1, 2026 11:16
@aqeelat aqeelat force-pushed the fix/restore-ansi-diff-renderer branch from f3fa416 to 6886fff Compare July 1, 2026 11:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Restores ansi-diff-based differential rendering in the pnpm v11 default reporter to eliminate repeated “sticky” output blocks and avoid cursor-up growth issues from the prior full-frame redraw approach, while preserving the “clear external output below the frame” behavior.

Changes:

  • Reintroduce ansi-diff in the default reporter render loop and append \x1b[0J after each update to clear below-frame output.
  • Add regression tests to ensure unchanged sticky blocks aren’t reprinted and that each write ends with erase-to-end-of-display.
  • Add ansi-diff to the workspace catalog, lockfile, and the @pnpm/cli.default-reporter package dependencies, plus a changeset entry.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pnpm11/cli/default-reporter/test/reporterRenderer.ts Adds regression tests for differential rendering (no duplicated sticky blocks) and “erase below frame” behavior.
pnpm11/cli/default-reporter/src/index.ts Switches rendering back to ansi-diff updates and appends erase-to-end-of-display per write.
pnpm11/cli/default-reporter/package.json Adds ansi-diff dependency via the workspace catalog.
pnpm-workspace.yaml Adds ansi-diff to the workspace catalog.
pnpm-lock.yaml Locks ansi-diff@1.2.0 in catalogs and importer deps.
.changeset/restore-ansi-diff.md Adds a patch changeset for @pnpm/cli.default-reporter and pnpm.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 6886fff

1 similar comment
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 6886fff

@zkochan zkochan force-pushed the fix/restore-ansi-diff-renderer branch from 6886fff to c6e936b Compare July 1, 2026 16:22
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

@aqeelat

aqeelat commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

@zkochan PR is ready

@zkochan zkochan added this pull request to the merge queue Jul 2, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 2, 2026
Copilot AI review requested due to automatic review settings July 2, 2026 13:38
@aqeelat aqeelat force-pushed the fix/restore-ansi-diff-renderer branch from 0887ec2 to 5c4e051 Compare July 2, 2026 13:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 9 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment thread pacquet/crates/default-reporter/src/lib.rs Outdated
Comment thread pacquet/crates/default-reporter/src/diff/tests.rs Outdated
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 5c4e051

1 similar comment
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 5c4e051

aqeelat and others added 6 commits July 2, 2026 17:09
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.
…rter 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.
- 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.
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
…ocation

- 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.
…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.
@aqeelat

aqeelat commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

@zkochan the same tests are failing on all of my PRs. They're infra stuff.

@zkochan zkochan force-pushed the fix/restore-ansi-diff-renderer branch from 5c4e051 to 6fb996a Compare July 2, 2026 15:15

@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 `@pacquet/crates/default-reporter/src/diff.rs`:
- Line 64: `Diff::update_into` is writing `new_line.raw` directly into the
frame, so terminal control sequences from lifecycle stdout/stderr can reach the
renderer unchanged. Update the frame-building path in `diff.rs` to sanitize or
strip VT/ANSI controls before `out.push_str`, and make the same sanitization
change in the matching default-reporter implementation in
`@pnpm/cli.default-reporter` so both renderers handle the same safe text.
🪄 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: 6826d1dc-13de-4e56-a937-578e3006d259

📥 Commits

Reviewing files that changed from the base of the PR and between 5c4e051 and 6fb996a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !pnpm-lock.yaml
📒 Files selected for processing (8)
  • .changeset/restore-ansi-diff.md
  • pacquet/crates/default-reporter/src/diff.rs
  • pacquet/crates/default-reporter/src/diff/tests.rs
  • pacquet/crates/default-reporter/src/lib.rs
  • pnpm-workspace.yaml
  • pnpm11/cli/default-reporter/package.json
  • pnpm11/cli/default-reporter/src/index.ts
  • pnpm11/cli/default-reporter/test/reporterRenderer.ts
✅ Files skipped from review due to trivial changes (1)
  • .changeset/restore-ansi-diff.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • pnpm11/cli/default-reporter/package.json
  • pnpm11/cli/default-reporter/test/reporterRenderer.ts
  • pnpm11/cli/default-reporter/src/index.ts
  • pnpm-workspace.yaml
  • pacquet/crates/default-reporter/src/lib.rs
  • pacquet/crates/default-reporter/src/diff/tests.rs

Comment thread pacquet/crates/default-reporter/src/diff.rs
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 6fb996a

1 similar comment
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 6fb996a

@zkochan

zkochan commented Jul 2, 2026

Copy link
Copy Markdown
Member

Status update on CI:

  • Rust CI / Test / macos: failed on runner-infrastructure errors (Illegal byte sequence (os error 92) writing cargo fingerprint files, in crates this PR does not touch). Re-run passed — Rust CI is green.
  • TS CI / Test / ubuntu / Node 24: the two failures (installing/commands test/update/recursive.ts and test/update/interactive.ts) are a pre-existing regression on main from the registry-mounts merge (pnpm/pnpm#12747), unrelated to this PR. TS CI was path-filter-skipped on that merge commit, so this PR's rebase is the first time the full TS suite ran on top of it.

Root cause, in two parts:

  1. The bundled registry-mock router routes @zkochan/* to the local hosted mount, but only @zkochan/test-pnpm-issue219 is a fixture — with no cross-origin fall-through, @zkochan/async-regex-replace (proxied from npm before the mounts change) now 404s.
  2. Dist-tag writes on upstream-routed packages are rejected (cannot publish … to the path-less base: it routes to an upstream registry; name a hosted mount). The pre-mounts server materialized the upstream packument into hosted storage on first dist-tag write; the full-purity model rejects the write instead. TS tests dist-tag real upstream packages (is-negative, is-positive, foo, micromatch, lodash, es6-iterator, es5-ext) in at least 6 files across installing/commands, installing/deps-installer, and the pnpm e2e suite, and registry-access/commands publishes unscoped names — pnpm -r test bails at the first failing project, so only the first two surfaced.

The fix belongs in a separate PR against main (registry-mock semantics / mount model), not here. Every PR that runs the TS suite will hit the same failures until it lands.


Written by an agent (Claude Code, claude-fable-5).

@aqeelat

aqeelat commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

@zkochan incoming PR soon.

@zkochan

zkochan commented Jul 2, 2026

Copy link
Copy Markdown
Member

The TS CI regression diagnosed above is fixed by pnpm/pnpm#12769 — once that lands on main, a rebase of this PR should turn TS CI green.


Written by an agent (Claude Code, claude-fable-5).

@zkochan zkochan merged commit 991405e into pnpm:main Jul 2, 2026
45 of 52 checks passed
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.

Regression in 11.8.0: interactive reporter flickers and never settles on run --parallel when the frame is taller than the terminal

4 participants