@xynogen/pix-pretty 1.16.0 → 1.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-pretty",
3
- "version": "1.16.0",
3
+ "version": "1.16.1",
4
4
  "description": "Enhanced tool output rendering with syntax highlighting, file icons, tree views, diff rendering, and FFF search",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/utils.test.ts CHANGED
@@ -31,6 +31,21 @@ class MockTextComponent {
31
31
  invalidate(): void {}
32
32
  }
33
33
 
34
+ // Counting variant: records how often the inner component is re-fitted, so a
35
+ // regression that recomputes every frame (the pre-memo CPU bug) fails loudly.
36
+ class CountingTextComponent {
37
+ static setCalls = 0;
38
+ private text = "";
39
+ setText(value: string): void {
40
+ CountingTextComponent.setCalls++;
41
+ this.text = value;
42
+ }
43
+ render(): string[] {
44
+ return this.text.split("\n");
45
+ }
46
+ invalidate(): void {}
47
+ }
48
+
34
49
  describe("viewportText", () => {
35
50
  it("trims a pre-filled row to Pi's narrower fullscreen viewport", () => {
36
51
  const text = viewportText(MockTextComponent);
@@ -39,6 +54,51 @@ describe("viewportText", () => {
39
54
  expect(plain(text.render(9)[0]!)).toBe("tool".padEnd(9));
40
55
  expect(plain(text.render(10)[0]!)).toBe("tool".padEnd(10));
41
56
  });
57
+
58
+ it("memoizes: repeat render at same width does not re-fit the inner component", () => {
59
+ CountingTextComponent.setCalls = 0;
60
+ const text = viewportText(CountingTextComponent);
61
+ text.setText("line one\nline two");
62
+
63
+ text.render(20);
64
+ text.render(20);
65
+ text.render(20);
66
+ // One fit for the width; repeats hit the cache. Pre-memo this was 3.
67
+ expect(CountingTextComponent.setCalls).toBe(1);
68
+ });
69
+
70
+ it("re-fits when width changes, and again when it returns", () => {
71
+ CountingTextComponent.setCalls = 0;
72
+ const text = viewportText(CountingTextComponent);
73
+ text.setText("abcdefghij");
74
+
75
+ text.render(20);
76
+ text.render(5); // width changed → re-fit
77
+ text.render(5); // same → cached
78
+ text.render(20); // changed back → re-fit
79
+ expect(CountingTextComponent.setCalls).toBe(3);
80
+ });
81
+
82
+ it("re-fits after setText changes the content", () => {
83
+ CountingTextComponent.setCalls = 0;
84
+ const text = viewportText(CountingTextComponent);
85
+ text.setText("first");
86
+ text.render(20);
87
+ text.render(20); // cached
88
+ text.setText("second"); // invalidates memo
89
+ text.render(20); // re-fit
90
+ expect(CountingTextComponent.setCalls).toBe(2);
91
+ });
92
+
93
+ it("setText with identical value is a no-op (no memo invalidation)", () => {
94
+ CountingTextComponent.setCalls = 0;
95
+ const text = viewportText(CountingTextComponent);
96
+ text.setText("same");
97
+ text.render(20);
98
+ text.setText("same"); // identical → must not invalidate
99
+ text.render(20); // still cached
100
+ expect(CountingTextComponent.setCalls).toBe(1);
101
+ });
42
102
  });
43
103
 
44
104
  // Strip ANSI escapes so assertions test content, not color codes.
package/src/utils.ts CHANGED
@@ -71,12 +71,18 @@ type ViewportComponent = {
71
71
 
72
72
  class ViewportText implements TextComponentLike, ViewportComponent {
73
73
  private text = "";
74
+ // Pi re-renders every frame (spinner/streaming). Without this cache each
75
+ // frame re-split + re-truncated the full text AND re-setText'd the inner
76
+ // component (re-dirtying it), burning CPU on unchanged content.
77
+ private fittedWidth = -1;
78
+ private fittedText = "";
74
79
 
75
80
  constructor(private readonly component: TextComponentLike) {}
76
81
 
77
82
  setText(value: string): void {
83
+ if (value === this.text) return;
78
84
  this.text = value;
79
- this.component.setText(value);
85
+ this.fittedWidth = -1; // invalidate memo
80
86
  }
81
87
 
82
88
  getText(): string {
@@ -84,12 +90,15 @@ class ViewportText implements TextComponentLike, ViewportComponent {
84
90
  }
85
91
 
86
92
  render(width: number): string[] {
87
- const fitted = this.text
88
- .split("\n")
89
- .map((line) => truncateToWidth(line, width, ""))
90
- .join("\n");
91
- this.component.setText(fitted);
92
- return this.component.render?.(width) ?? fitted.split("\n");
93
+ if (width !== this.fittedWidth) {
94
+ this.fittedText = this.text
95
+ .split("\n")
96
+ .map((line) => truncateToWidth(line, width, ""))
97
+ .join("\n");
98
+ this.fittedWidth = width;
99
+ this.component.setText(this.fittedText);
100
+ }
101
+ return this.component.render?.(width) ?? this.fittedText.split("\n");
93
102
  }
94
103
 
95
104
  invalidate(): void {