@oh-my-pi/pi-tui 18.0.7 → 18.0.8
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 +11 -0
- package/dist/types/terminal.d.ts +11 -0
- package/dist/types/tui.d.ts +7 -0
- package/package.json +3 -3
- package/src/components/loader.ts +11 -5
- package/src/terminal.ts +22 -2
- package/src/tui.ts +20 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.0.8] - 2026-08-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `ProcessTerminal` accepts a `conpty` option to force ConPTY-hosted behavior on or off, keeping terminal tests hermetic on WSL where live env detection would otherwise flip kitty-keyboard flags and write chunking ([#9887](https://github.com/can1357/oh-my-pi/issues/9887)).
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed pending-work animations repeatedly composing expensive frames without applying their full render cost to CPU backpressure.
|
|
14
|
+
- Fixed unfinished live viewport rows entering tmux pane history and duplicating streamed output ([#9780](https://github.com/can1357/oh-my-pi/issues/9780)).
|
|
15
|
+
|
|
5
16
|
## [18.0.7] - 2026-08-26
|
|
6
17
|
|
|
7
18
|
### Breaking Changes
|
package/dist/types/terminal.d.ts
CHANGED
|
@@ -172,11 +172,22 @@ export interface Terminal {
|
|
|
172
172
|
* single predicate.
|
|
173
173
|
*/
|
|
174
174
|
export declare function isConPTYHosted(): boolean;
|
|
175
|
+
/** Construction-time overrides for {@link ProcessTerminal}. */
|
|
176
|
+
export interface ProcessTerminalOptions {
|
|
177
|
+
/**
|
|
178
|
+
* Force ConPTY-hosted behavior on or off. Defaults to live detection via
|
|
179
|
+
* {@link isConPTYHosted}. Tests set this so the kitty-flag and write-chunking
|
|
180
|
+
* paths stay hermetic regardless of the ambient WSL env (`WSL_DISTRO_NAME` /
|
|
181
|
+
* `WSL_INTEROP`) — the suite must behave identically on WSL and on CI.
|
|
182
|
+
*/
|
|
183
|
+
conpty?: boolean;
|
|
184
|
+
}
|
|
175
185
|
/**
|
|
176
186
|
* Real terminal using process.stdin/stdout
|
|
177
187
|
*/
|
|
178
188
|
export declare class ProcessTerminal implements Terminal {
|
|
179
189
|
#private;
|
|
190
|
+
constructor(options?: ProcessTerminalOptions);
|
|
180
191
|
get kittyProtocolActive(): boolean;
|
|
181
192
|
get kittyEnableSequence(): string | null;
|
|
182
193
|
get keyboardEnhancementEnterSequence(): string | null;
|
package/dist/types/tui.d.ts
CHANGED
|
@@ -292,6 +292,13 @@ export declare class TUI extends Container {
|
|
|
292
292
|
* positive report, disabled on a negative one.
|
|
293
293
|
*/
|
|
294
294
|
get synchronizedOutput(): boolean;
|
|
295
|
+
/**
|
|
296
|
+
* Cost in milliseconds of the most recently completed frame.
|
|
297
|
+
*
|
|
298
|
+
* Animation components use this to apply proportional backpressure after
|
|
299
|
+
* their render request is asynchronously composed and written.
|
|
300
|
+
*/
|
|
301
|
+
get lastFrameCostMs(): number;
|
|
295
302
|
setFocus(component: Component | null): void;
|
|
296
303
|
/** Component currently receiving keyboard input, if any. */
|
|
297
304
|
getFocused(): Component | null;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "18.0.
|
|
4
|
+
"version": "18.0.8",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Stencil Labs, Inc.",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-natives": "18.0.
|
|
41
|
-
"@oh-my-pi/pi-utils": "18.0.
|
|
40
|
+
"@oh-my-pi/pi-natives": "18.0.8",
|
|
41
|
+
"@oh-my-pi/pi-utils": "18.0.8"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"kitty-vt-wasm": "^0.2.0"
|
package/src/components/loader.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { Text } from "./text";
|
|
|
5
5
|
const RENDER_INTERVAL_MS = 1000 / 30;
|
|
6
6
|
const SPINNER_ADVANCE_MS = 80;
|
|
7
7
|
const RENDER_BACKPRESSURE_MULTIPLIER = 9;
|
|
8
|
+
const MAX_BACKPRESSURE_FRAME_COST_MS = 200;
|
|
8
9
|
|
|
9
10
|
type ColorFn = (str: string) => string;
|
|
10
11
|
|
|
@@ -139,12 +140,17 @@ export class Loader extends Text {
|
|
|
139
140
|
this.#requestPaint();
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
const
|
|
143
|
+
const completedFrameCostMs = this.#ui?.lastFrameCostMs ?? 0;
|
|
144
|
+
const requestCostMs = performance.now() - startedAt;
|
|
143
145
|
if (this.#intervalId !== timer) return;
|
|
144
|
-
const cadenceDelayMs = Math.max(0, intervalMs -
|
|
145
|
-
// Idle for nine times the
|
|
146
|
-
// 10% CPU
|
|
147
|
-
const
|
|
146
|
+
const cadenceDelayMs = Math.max(0, intervalMs - requestCostMs);
|
|
147
|
+
// Idle for nine times the full frame cost to keep animation at or
|
|
148
|
+
// below 10% CPU even though requestComponentRender() only enqueues.
|
|
149
|
+
const boundedFrameCostMs = Math.min(
|
|
150
|
+
MAX_BACKPRESSURE_FRAME_COST_MS,
|
|
151
|
+
Math.max(completedFrameCostMs, requestCostMs),
|
|
152
|
+
);
|
|
153
|
+
const backpressureDelayMs = boundedFrameCostMs * RENDER_BACKPRESSURE_MULTIPLIER;
|
|
148
154
|
this.#scheduleTick(intervalMs, Math.max(cadenceDelayMs, backpressureDelayMs));
|
|
149
155
|
}, delayMs);
|
|
150
156
|
this.#intervalId = timer;
|
package/src/terminal.ts
CHANGED
|
@@ -581,6 +581,17 @@ function isPrivateModeSupported(status: string): boolean {
|
|
|
581
581
|
return status !== "0" && status !== "4";
|
|
582
582
|
}
|
|
583
583
|
|
|
584
|
+
/** Construction-time overrides for {@link ProcessTerminal}. */
|
|
585
|
+
export interface ProcessTerminalOptions {
|
|
586
|
+
/**
|
|
587
|
+
* Force ConPTY-hosted behavior on or off. Defaults to live detection via
|
|
588
|
+
* {@link isConPTYHosted}. Tests set this so the kitty-flag and write-chunking
|
|
589
|
+
* paths stay hermetic regardless of the ambient WSL env (`WSL_DISTRO_NAME` /
|
|
590
|
+
* `WSL_INTEROP`) — the suite must behave identically on WSL and on CI.
|
|
591
|
+
*/
|
|
592
|
+
conpty?: boolean;
|
|
593
|
+
}
|
|
594
|
+
|
|
584
595
|
/**
|
|
585
596
|
* Real terminal using process.stdin/stdout
|
|
586
597
|
*/
|
|
@@ -619,6 +630,11 @@ export class ProcessTerminal implements Terminal {
|
|
|
619
630
|
// terminal side effect (writes, probes, raw mode, SIGWINCH, timers) is
|
|
620
631
|
// suppressed. Defaults on under `bun test` — see isTerminalHeadless().
|
|
621
632
|
#headless = isTerminalHeadless();
|
|
633
|
+
// Captured once at construction: whether stdout flows through a ConPTY
|
|
634
|
+
// pseudo-console. Gates the kitty-flag choice (#1216) and large-write
|
|
635
|
+
// chunking (#safeWrite). Live-detected by default; tests inject a fixed
|
|
636
|
+
// value so WSL env does not change behavior. See {@link ProcessTerminalOptions}.
|
|
637
|
+
readonly #conpty: boolean;
|
|
622
638
|
#writeLogPath = $env.PI_TUI_WRITE_LOG || "";
|
|
623
639
|
#stdoutErrorCleanup?: () => void;
|
|
624
640
|
#stdoutErrorHandler = (err: Error) => {
|
|
@@ -674,6 +690,10 @@ export class ProcessTerminal implements Terminal {
|
|
|
674
690
|
#windowsTerminalAppearancePollTimer?: Timer;
|
|
675
691
|
#progressTimer?: Timer;
|
|
676
692
|
|
|
693
|
+
constructor(options?: ProcessTerminalOptions) {
|
|
694
|
+
this.#conpty = options?.conpty ?? isConPTYHosted();
|
|
695
|
+
}
|
|
696
|
+
|
|
677
697
|
get kittyProtocolActive(): boolean {
|
|
678
698
|
return this.#kittyProtocolActive;
|
|
679
699
|
}
|
|
@@ -1213,7 +1233,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
1213
1233
|
const reportedFlags = parseInt(match[1]!, 10);
|
|
1214
1234
|
this.#kittyProtocolActive = true;
|
|
1215
1235
|
setKittyProtocolActive(true);
|
|
1216
|
-
if (
|
|
1236
|
+
if (this.#conpty) {
|
|
1217
1237
|
// ConPTY (native Windows and WSL) drops Shift+letter keypresses
|
|
1218
1238
|
// entirely when flag 4 (report alternate keys) is set. Use flag 1
|
|
1219
1239
|
// (disambiguate only), preserving flag 2 if already active.
|
|
@@ -1863,7 +1883,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
1863
1883
|
// threshold. See #2034 and #2095.
|
|
1864
1884
|
const bytes = Buffer.byteLength(data, "utf8");
|
|
1865
1885
|
let accepted: boolean;
|
|
1866
|
-
if (
|
|
1886
|
+
if (this.#conpty && bytes > MAX_CONPTY_WRITE_CHUNK_BYTES) {
|
|
1867
1887
|
accepted = true;
|
|
1868
1888
|
for (const chunk of chunkForConPTY(data, MAX_CONPTY_WRITE_CHUNK_BYTES)) {
|
|
1869
1889
|
if (this.#dead) break;
|
package/src/tui.ts
CHANGED
|
@@ -901,6 +901,16 @@ export class TUI extends Container {
|
|
|
901
901
|
return this.#synchronizedOutputEnabled;
|
|
902
902
|
}
|
|
903
903
|
|
|
904
|
+
/**
|
|
905
|
+
* Cost in milliseconds of the most recently completed frame.
|
|
906
|
+
*
|
|
907
|
+
* Animation components use this to apply proportional backpressure after
|
|
908
|
+
* their render request is asynchronously composed and written.
|
|
909
|
+
*/
|
|
910
|
+
get lastFrameCostMs(): number {
|
|
911
|
+
return this.#lastFrameCostMs;
|
|
912
|
+
}
|
|
913
|
+
|
|
904
914
|
setFocus(component: Component | null): void {
|
|
905
915
|
const topVisibleOverlay = this.#getTopmostVisibleOverlay();
|
|
906
916
|
if (topVisibleOverlay && !isOverlayFocusTarget(topVisibleOverlay.component, component)) {
|
|
@@ -2361,7 +2371,16 @@ export class TUI extends Container {
|
|
|
2361
2371
|
// committed rows and blanks, never an unfinished frame.
|
|
2362
2372
|
const pushed = Math.max(0, startTop + preparedHistory.length + rows - height);
|
|
2363
2373
|
if (pushed > this.#providerViewportTop && this.#providerWindow.length > 0) {
|
|
2364
|
-
|
|
2374
|
+
const eraseTop = this.#providerViewportTop;
|
|
2375
|
+
if (eraseTop > 0) {
|
|
2376
|
+
// Below existing history: ED0 here never spans the whole screen.
|
|
2377
|
+
buffer += `\x1b[${eraseTop + 1};1H\x1b[J`;
|
|
2378
|
+
} else {
|
|
2379
|
+
// Full-screen erase makes tmux preserve the live rows (#9780);
|
|
2380
|
+
// EL2 the first row + ED0 the rest clears the same cells, no full clear.
|
|
2381
|
+
buffer += "\x1b[1;1H\x1b[2K";
|
|
2382
|
+
if (height > 1) buffer += "\x1b[2;1H\x1b[J";
|
|
2383
|
+
}
|
|
2365
2384
|
}
|
|
2366
2385
|
buffer += `\x1b[${startTop + 1};1H`;
|
|
2367
2386
|
let screenRow = startTop;
|