@oh-my-pi/pi-tui 17.2.2 → 17.2.4

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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.2.4] - 2026-08-01
6
+
7
+ ### Fixed
8
+
9
+ - Fixed animated Loader paints saturating a CPU core on slow WSL/ConPTY terminals by applying cost-aware cadence backpressure while preserving 30fps on cheap frames ([#7290](https://github.com/can1357/oh-my-pi/issues/7290)).
10
+ - Fixed interactive terminals suppressing all output and input when the host project sets `NODE_ENV=test` or `BUN_ENV=test` ([#7261](https://github.com/can1357/oh-my-pi/issues/7261)).
11
+
5
12
  ## [17.2.2] - 2026-07-31
6
13
 
7
14
  ### Added
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "17.2.2",
4
+ "version": "17.2.4",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -37,8 +37,8 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@oh-my-pi/pi-natives": "17.2.2",
41
- "@oh-my-pi/pi-utils": "17.2.2",
40
+ "@oh-my-pi/pi-natives": "17.2.4",
41
+ "@oh-my-pi/pi-utils": "17.2.4",
42
42
  "lru-cache": "11.5.2",
43
43
  "marked": "^18.0.6"
44
44
  },
@@ -4,6 +4,8 @@ import { Text } from "./text";
4
4
 
5
5
  const RENDER_INTERVAL_MS = 1000 / 30;
6
6
  const SPINNER_ADVANCE_MS = 80;
7
+ const MAX_RENDER_BACKPRESSURE_MS = 200;
8
+ const RENDER_BACKPRESSURE_MULTIPLIER = 9;
7
9
 
8
10
  type ColorFn = (str: string) => string;
9
11
 
@@ -98,25 +100,12 @@ export class Loader extends Text {
98
100
  this.#syncText();
99
101
  this.#requestPaint();
100
102
  const intervalMs = this.messageColorFn.animated === true ? RENDER_INTERVAL_MS : SPINNER_ADVANCE_MS;
101
- this.#intervalId = setInterval(() => {
102
- const now = performance.now();
103
- const elapsed = now - this.#lastSpinnerTick;
104
- const shouldAdvanceSpinner = elapsed >= SPINNER_ADVANCE_MS;
105
- if (shouldAdvanceSpinner) {
106
- const steps = Math.floor(elapsed / SPINNER_ADVANCE_MS);
107
- this.#currentFrame = (this.#currentFrame + steps) % this.#frames.length;
108
- this.#lastSpinnerTick += steps * SPINNER_ADVANCE_MS;
109
- this.#syncText();
110
- }
111
- if (shouldAdvanceSpinner || this.#ui?.synchronizedOutput === true) {
112
- this.#requestPaint();
113
- }
114
- }, intervalMs);
103
+ this.#scheduleTick(intervalMs, intervalMs);
115
104
  }
116
105
 
117
106
  stop() {
118
107
  if (this.#intervalId) {
119
- clearInterval(this.#intervalId);
108
+ clearTimeout(this.#intervalId);
120
109
  this.#intervalId = undefined;
121
110
  }
122
111
  }
@@ -135,6 +124,32 @@ export class Loader extends Text {
135
124
  this.#requestPaint();
136
125
  }
137
126
 
127
+ #scheduleTick(intervalMs: number, delayMs: number): void {
128
+ const timer = setTimeout(() => {
129
+ if (this.#intervalId !== timer) return;
130
+ const startedAt = performance.now();
131
+ const elapsed = startedAt - this.#lastSpinnerTick;
132
+ const shouldAdvanceSpinner = elapsed >= SPINNER_ADVANCE_MS;
133
+ if (shouldAdvanceSpinner) {
134
+ const steps = Math.floor(elapsed / SPINNER_ADVANCE_MS);
135
+ this.#currentFrame = (this.#currentFrame + steps) % this.#frames.length;
136
+ this.#lastSpinnerTick += steps * SPINNER_ADVANCE_MS;
137
+ this.#syncText();
138
+ }
139
+ if (shouldAdvanceSpinner || this.#ui?.synchronizedOutput === true) {
140
+ this.#requestPaint();
141
+ }
142
+
143
+ const frameCostMs = performance.now() - startedAt;
144
+ if (this.#intervalId !== timer) return;
145
+ const cadenceDelayMs = Math.max(0, intervalMs - frameCostMs);
146
+ // Idle for nine times the paint cost to keep animation at or below
147
+ // 10% CPU, while cheap frames retain their original cadence.
148
+ const backpressureDelayMs = Math.min(MAX_RENDER_BACKPRESSURE_MS, frameCostMs * RENDER_BACKPRESSURE_MULTIPLIER);
149
+ this.#scheduleTick(intervalMs, Math.max(cadenceDelayMs, backpressureDelayMs));
150
+ }, delayMs);
151
+ this.#intervalId = timer;
152
+ }
138
153
  /** Re-wrap the underlying Text only when its message or frame width changes. */
139
154
  #syncText(): boolean {
140
155
  const layoutFrame = this.#layoutFrames[this.#currentFrame];