@linxiraos/pi-tui 1.1.7 → 1.1.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 +3 -7
- package/dist/types/components/editor.d.ts +9 -3
- package/dist/types/components/image.d.ts +5 -6
- package/dist/types/keys.d.ts +1 -1
- package/dist/types/kitty-graphics.d.ts +5 -5
- package/dist/types/stdin-buffer.d.ts +6 -1
- package/dist/types/terminal-capabilities.d.ts +12 -7
- package/dist/types/terminal-multiplexer.d.ts +4 -0
- package/dist/types/terminal.d.ts +46 -21
- package/dist/types/tui.d.ts +18 -0
- package/package.json +3 -3
- package/src/autocomplete.ts +1 -1
- package/src/components/editor.ts +13 -6
- package/src/components/image.ts +31 -24
- package/src/components/markdown.ts +21 -10
- package/src/components/select-list.ts +1 -0
- package/src/components/spacer.ts +1 -1
- package/src/components/text.ts +1 -1
- package/src/deccara.ts +2 -0
- package/src/keys.ts +1 -1
- package/src/kitty-graphics.ts +9 -6
- package/src/latex-block.ts +1 -0
- package/src/stdin-buffer.ts +28 -5
- package/src/terminal-capabilities.ts +22 -21
- package/src/terminal-multiplexer.ts +24 -0
- package/src/terminal.ts +173 -85
- package/src/tui.ts +158 -23
package/src/terminal.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
$env,
|
|
6
6
|
isBunTestRuntime,
|
|
7
7
|
isTerminalHeadless,
|
|
8
|
+
isWsl,
|
|
8
9
|
logger,
|
|
9
10
|
postmortem,
|
|
10
11
|
restoreTerminalStderr,
|
|
@@ -142,62 +143,109 @@ export function chunkForConPTY(data: string, maxChunkBytes: number = MAX_CONPTY_
|
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
146
|
+
* Backlog ceiling that arms the stall watchdog. A live terminal keeps this
|
|
147
|
+
* near zero; crossing it means either a genuinely wedged PTY reader (#6854) or
|
|
148
|
+
* a single legitimately-huge frame — a `--resume` transcript repaint of many
|
|
149
|
+
* inline images is one multi-tens-of-MiB write (#10430). The two are told
|
|
150
|
+
* apart by {@link StdoutStallWatchdog} (drain progress), not by this number
|
|
151
|
+
* alone: inter-frame production is already gated at 256 KiB
|
|
152
|
+
* (`TUI.#deferRenderForOutputBacklog`), so the only way to reach this cap is a
|
|
153
|
+
* lone oversized frame or a reader that has stopped consuming entirely.
|
|
152
154
|
*/
|
|
153
155
|
const MAX_STDOUT_BACKLOG_BYTES = 64 * 1024 * 1024;
|
|
154
156
|
|
|
155
157
|
/**
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
+
* Backlog at or below which stdout is healthy again: the pump has kept up, the
|
|
159
|
+
* TUI resumes composing frames, and a {@link StdoutStallWatchdog} episode ends.
|
|
160
|
+
* The TUI render gate (`TUI.#MAX_PENDING_OUTPUT_BYTES`) is this same value, so
|
|
161
|
+
* the watchdog stays armed across the entire range where frames are deferred —
|
|
162
|
+
* otherwise a consumer that wedges between this level and the arm cap is never
|
|
163
|
+
* re-sampled and the session freezes instead of disconnecting (#10434 review).
|
|
164
|
+
*/
|
|
165
|
+
export const STDOUT_BACKLOG_CLEAR_BYTES = 256 * 1024;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* How long an armed backlog may go without any drain progress before the
|
|
169
|
+
* consumer is declared gone. A slow-but-alive terminal keeps reaching new
|
|
170
|
+
* low-water marks (so it never trips); a wedged one that flushes nothing is
|
|
171
|
+
* torn down within this window.
|
|
172
|
+
*/
|
|
173
|
+
const STDOUT_STALL_TIMEOUT_MS = 2_000;
|
|
174
|
+
|
|
175
|
+
/** Cadence at which {@link ProcessTerminal} re-samples the backlog while an episode is armed. */
|
|
176
|
+
const STDOUT_STALL_POLL_MS = 250;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Bounds a never-draining stdout backlog without killing a single large but
|
|
180
|
+
* actively-draining frame.
|
|
158
181
|
*
|
|
159
182
|
* `process.stdout.write()` returns `false` once its buffer exceeds the stream
|
|
160
|
-
* high-water mark
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
183
|
+
* high-water mark, and the off-thread pump's `pending()` climbs the same way;
|
|
184
|
+
* a stalled-but-alive PTY reader never throws, so the byte count is the only
|
|
185
|
+
* signal that output is going nowhere. Tripping on the instantaneous count
|
|
186
|
+
* alone is wrong: a legitimate oversized frame (a resume repaint of dozens of
|
|
187
|
+
* inline screenshots, #10430) briefly exceeds the cap and then drains.
|
|
188
|
+
*
|
|
189
|
+
* An episode starts when the backlog first exceeds `armBytes` and lasts until
|
|
190
|
+
* it drains back to `clearBytes` (healthy). The backlog can fall below
|
|
191
|
+
* `armBytes` while still unhealthy, so the episode must outlive that dip
|
|
192
|
+
* (#10434): during it the watchdog declares the terminal disconnected only when
|
|
193
|
+
* the backlog makes no drain progress (no new low-water mark) for `stallMs` —
|
|
194
|
+
* a draining terminal keeps lowering the mark and never trips, while a wedged
|
|
195
|
+
* one (#6854) still tears down within the window.
|
|
167
196
|
*
|
|
168
197
|
* Exported for unit testing; `ProcessTerminal` is the sole production user.
|
|
169
198
|
*/
|
|
170
|
-
export class
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
199
|
+
export class StdoutStallWatchdog {
|
|
200
|
+
#lowWater = Number.POSITIVE_INFINITY;
|
|
201
|
+
#stalledSinceMs = 0;
|
|
202
|
+
#armed = false;
|
|
203
|
+
|
|
204
|
+
constructor(
|
|
205
|
+
private readonly armBytes: number = MAX_STDOUT_BACKLOG_BYTES,
|
|
206
|
+
private readonly clearBytes: number = STDOUT_BACKLOG_CLEAR_BYTES,
|
|
207
|
+
private readonly stallMs: number = STDOUT_STALL_TIMEOUT_MS,
|
|
208
|
+
) {}
|
|
209
|
+
|
|
210
|
+
/** True while an episode is active and the backlog must be polled to completion. */
|
|
211
|
+
get armed(): boolean {
|
|
212
|
+
return this.#armed;
|
|
179
213
|
}
|
|
180
214
|
|
|
181
215
|
/**
|
|
182
|
-
*
|
|
183
|
-
* `
|
|
184
|
-
*
|
|
216
|
+
* Feed the current pending-byte count and clock reading. Returns true once an
|
|
217
|
+
* armed episode has gone `stallMs` with no drain progress, at which point the
|
|
218
|
+
* caller treats the terminal as disconnected.
|
|
185
219
|
*/
|
|
186
|
-
|
|
187
|
-
if (!this.#
|
|
188
|
-
//
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
this.#
|
|
220
|
+
sample(pending: number, nowMs: number): boolean {
|
|
221
|
+
if (!this.#armed) {
|
|
222
|
+
// Idle: only an oversized backlog starts an episode.
|
|
223
|
+
if (pending <= this.armBytes) return false;
|
|
224
|
+
this.#armed = true;
|
|
225
|
+
this.#lowWater = pending;
|
|
226
|
+
this.#stalledSinceMs = nowMs;
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
if (pending <= this.clearBytes) {
|
|
230
|
+
// Drained back to a healthy level: the episode is over.
|
|
231
|
+
this.reset();
|
|
232
|
+
return false;
|
|
192
233
|
}
|
|
193
|
-
this.#
|
|
194
|
-
|
|
234
|
+
if (pending < this.#lowWater) {
|
|
235
|
+
// Drain progress: a new low-water mark restarts the stall clock.
|
|
236
|
+
this.#lowWater = pending;
|
|
237
|
+
this.#stalledSinceMs = nowMs;
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
// Still unhealthy with no new low-water mark since the clock started.
|
|
241
|
+
return nowMs - this.#stalledSinceMs >= this.stallMs;
|
|
195
242
|
}
|
|
196
243
|
|
|
197
|
-
/**
|
|
244
|
+
/** Episode ended (drained) or terminal torn down: stop watching. */
|
|
198
245
|
reset(): void {
|
|
199
|
-
this.#
|
|
200
|
-
this.#
|
|
246
|
+
this.#armed = false;
|
|
247
|
+
this.#lowWater = Number.POSITIVE_INFINITY;
|
|
248
|
+
this.#stalledSinceMs = 0;
|
|
201
249
|
}
|
|
202
250
|
}
|
|
203
251
|
|
|
@@ -406,6 +454,13 @@ export interface TerminalStartOptions {
|
|
|
406
454
|
}
|
|
407
455
|
/** Identity of an accepted explicit terminal appearance refresh request. */
|
|
408
456
|
export type TerminalAppearanceRequestToken = number;
|
|
457
|
+
/**
|
|
458
|
+
* Fired once per DEC private mode when DECRQM support resolves.
|
|
459
|
+
* `confirmed` is false when only the DA1 sentinel arrived.
|
|
460
|
+
* `status` is the DECRPM value (0 unrecognized, 1/2 set/reset, 3 permanently
|
|
461
|
+
* set, 4 permanently reset) when the terminal answered DECRQM.
|
|
462
|
+
*/
|
|
463
|
+
export type PrivateModeReportHandler = (mode: number, supported: boolean, confirmed?: boolean, status?: number) => void;
|
|
409
464
|
export interface Terminal {
|
|
410
465
|
// Start the terminal with input, resize, and host-disconnect handlers.
|
|
411
466
|
start(
|
|
@@ -529,8 +584,9 @@ export interface Terminal {
|
|
|
529
584
|
* status resolves. `confirmed` is false when the terminal answered the DA1
|
|
530
585
|
* sentinel without answering DECRQM, which proves only that querying support
|
|
531
586
|
* is unavailable — not that the private mode itself is unsupported.
|
|
587
|
+
* `status` is the DECRPM value when the terminal answered DECRQM.
|
|
532
588
|
*/
|
|
533
|
-
onPrivateModeReport?(callback:
|
|
589
|
+
onPrivateModeReport?(callback: PrivateModeReportHandler): void;
|
|
534
590
|
}
|
|
535
591
|
|
|
536
592
|
/**
|
|
@@ -542,9 +598,9 @@ export interface Terminal {
|
|
|
542
598
|
* single predicate.
|
|
543
599
|
*/
|
|
544
600
|
export function isConPTYHosted(): boolean {
|
|
545
|
-
|
|
546
|
-
//
|
|
547
|
-
return process.platform === "
|
|
601
|
+
// win32 always hosts through ConPTY; under WSL stdout still crosses into
|
|
602
|
+
// ConPTY at the `wslhost` boundary.
|
|
603
|
+
return process.platform === "win32" || isWsl(process.platform, $env);
|
|
548
604
|
}
|
|
549
605
|
|
|
550
606
|
/** Discriminated owner of an outstanding DA1 sentinel in the unified probe FIFO. */
|
|
@@ -640,22 +696,19 @@ export class ProcessTerminal implements Terminal {
|
|
|
640
696
|
#stdoutErrorHandler = (err: Error) => {
|
|
641
697
|
this.#markTerminalDisconnected("stdout failed", err);
|
|
642
698
|
};
|
|
643
|
-
// Bounds the stdout
|
|
644
|
-
// stalled-but-alive reader never throws,
|
|
645
|
-
//
|
|
646
|
-
//
|
|
647
|
-
#
|
|
699
|
+
// Bounds the stdout backlog against a stalled PTY consumer without killing a
|
|
700
|
+
// single large-but-draining frame: a stalled-but-alive reader never throws,
|
|
701
|
+
// so the pending byte count is the only stall signal, and a legitimate
|
|
702
|
+
// oversized frame (a resume repaint of many inline images) must be allowed
|
|
703
|
+
// to drain. See StdoutStallWatchdog, #6854, and #10430.
|
|
704
|
+
#stdoutStall = new StdoutStallWatchdog();
|
|
705
|
+
#stdoutStallTimer?: Timer;
|
|
648
706
|
// Off-thread output pump (unix TTYs): Bun's `process.stdout.write` blocks
|
|
649
707
|
// the event loop until the terminal drains, so a slow/occluded emulator
|
|
650
708
|
// froze the whole TUI for the duration of a multi-MB repaint. The pump
|
|
651
709
|
// enqueues frames and performs the blocking write(2) on its own thread;
|
|
652
710
|
// `pendingOutputBytes` exposes the backlog for render-side frame skipping.
|
|
653
711
|
#outputPump?: TtyWriter;
|
|
654
|
-
#stdoutDrainArmed = false;
|
|
655
|
-
#stdoutDrainHandler = () => {
|
|
656
|
-
this.#stdoutDrainArmed = false;
|
|
657
|
-
this.#stdoutBacklog.reset();
|
|
658
|
-
};
|
|
659
712
|
|
|
660
713
|
#windowsVTInputRestore?: () => void;
|
|
661
714
|
#xtermScrollToBottomRestoreModes = new Set<number>();
|
|
@@ -679,7 +732,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
679
732
|
#da1SentinelOwners: Da1SentinelOwner[] = [];
|
|
680
733
|
/** Resolved DECRQM support per private mode (mode → supported). */
|
|
681
734
|
#privateModeSupport = new Map<number, boolean>();
|
|
682
|
-
#privateModeCallbacks:
|
|
735
|
+
#privateModeCallbacks: PrivateModeReportHandler[] = [];
|
|
683
736
|
/** Whether DEC 2048 in-band resize notifications are currently enabled. */
|
|
684
737
|
#inBandResizeActive = false;
|
|
685
738
|
/** Reassembly buffer for a DEC 2048 in-band resize report split across stdin reads. */
|
|
@@ -770,7 +823,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
770
823
|
return token;
|
|
771
824
|
}
|
|
772
825
|
|
|
773
|
-
onPrivateModeReport(callback:
|
|
826
|
+
onPrivateModeReport(callback: PrivateModeReportHandler): void {
|
|
774
827
|
this.#privateModeCallbacks.push(callback);
|
|
775
828
|
}
|
|
776
829
|
|
|
@@ -1306,10 +1359,13 @@ export class ProcessTerminal implements Terminal {
|
|
|
1306
1359
|
}
|
|
1307
1360
|
});
|
|
1308
1361
|
|
|
1309
|
-
// Re-wrap paste content with bracketed paste markers for existing editor
|
|
1310
|
-
|
|
1362
|
+
// Re-wrap paste content with bracketed paste markers for existing editor
|
|
1363
|
+
// handling. An Enter that shared the paste's stdin read rides along so
|
|
1364
|
+
// paste and submit reach the component focused right now, not one the
|
|
1365
|
+
// paste itself is about to open.
|
|
1366
|
+
this.#stdinBuffer.on("paste", (content: string, enter?: string) => {
|
|
1311
1367
|
if (this.#inputHandler) {
|
|
1312
|
-
this.#inputHandler(`\x1b[200~${content}\x1b[201
|
|
1368
|
+
this.#inputHandler(`\x1b[200~${content}\x1b[201~${enter ?? ""}`);
|
|
1313
1369
|
}
|
|
1314
1370
|
});
|
|
1315
1371
|
|
|
@@ -1430,6 +1486,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
1430
1486
|
const mode: TerminalAppearance = luminance < 0.5 ? "dark" : "light";
|
|
1431
1487
|
const changed = mode !== this.#appearance;
|
|
1432
1488
|
this.#appearance = mode;
|
|
1489
|
+
//DISABLED(biome-unknown-rule) lint/complexity/noUselessSpread: callbacks may unsubscribe while reporting
|
|
1433
1490
|
for (const cb of [...this.#appearanceReportCallbacks]) {
|
|
1434
1491
|
try {
|
|
1435
1492
|
cb(mode, requestToken);
|
|
@@ -1491,7 +1548,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
1491
1548
|
}
|
|
1492
1549
|
|
|
1493
1550
|
#handlePrivateModeReport(mode: number, status: string): void {
|
|
1494
|
-
this.#resolvePrivateMode(mode, isPrivateModeSupported(status), true);
|
|
1551
|
+
this.#resolvePrivateMode(mode, isPrivateModeSupported(status), true, Number.parseInt(status, 10));
|
|
1495
1552
|
if (isXtermScrollToBottomMode(mode) && isPrivateModeSet(status)) {
|
|
1496
1553
|
this.#disableXtermScrollToBottomMode(mode);
|
|
1497
1554
|
}
|
|
@@ -1503,12 +1560,12 @@ export class ProcessTerminal implements Terminal {
|
|
|
1503
1560
|
* unsupported response from an absent response followed by the DA1 sentinel.
|
|
1504
1561
|
* Enables DEC 2048 in-band resize only after positive confirmation.
|
|
1505
1562
|
*/
|
|
1506
|
-
#resolvePrivateMode(mode: number, supported: boolean, confirmed: boolean): void {
|
|
1563
|
+
#resolvePrivateMode(mode: number, supported: boolean, confirmed: boolean, status?: number): void {
|
|
1507
1564
|
if (this.#privateModeSupport.has(mode)) return;
|
|
1508
1565
|
this.#privateModeSupport.set(mode, supported);
|
|
1509
1566
|
for (const cb of this.#privateModeCallbacks) {
|
|
1510
1567
|
try {
|
|
1511
|
-
cb(mode, supported, confirmed);
|
|
1568
|
+
cb(mode, supported, confirmed, status);
|
|
1512
1569
|
} catch {
|
|
1513
1570
|
// Ignore subscriber errors — capability reporting must not crash input.
|
|
1514
1571
|
}
|
|
@@ -1759,11 +1816,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
1759
1816
|
process.stdout.removeListener("resize", this.#stdoutResizeListener);
|
|
1760
1817
|
this.#stdoutResizeListener = undefined;
|
|
1761
1818
|
}
|
|
1762
|
-
|
|
1763
|
-
process.stdout.removeListener("drain", this.#stdoutDrainHandler);
|
|
1764
|
-
this.#stdoutDrainArmed = false;
|
|
1765
|
-
}
|
|
1766
|
-
this.#stdoutBacklog.reset();
|
|
1819
|
+
this.#disarmStdoutStallWatchdog();
|
|
1767
1820
|
this.#resizeHandler = undefined;
|
|
1768
1821
|
// Flush the restore sequences enqueued above (bounded — a stalled PTY
|
|
1769
1822
|
// must not wedge exit), then retire the pump. Later writes (emergency
|
|
@@ -1802,6 +1855,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
1802
1855
|
#markTerminalDisconnected(reason: string, err?: unknown): void {
|
|
1803
1856
|
if (this.#dead) return;
|
|
1804
1857
|
this.#dead = true;
|
|
1858
|
+
this.#disarmStdoutStallWatchdog();
|
|
1805
1859
|
logger.warn("terminal disconnected; stopping interactive rendering", { reason, err });
|
|
1806
1860
|
|
|
1807
1861
|
const disconnectHandler = this.#disconnectHandler;
|
|
@@ -1854,11 +1908,11 @@ export class ProcessTerminal implements Terminal {
|
|
|
1854
1908
|
return;
|
|
1855
1909
|
}
|
|
1856
1910
|
try {
|
|
1857
|
-
//
|
|
1858
|
-
//
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1911
|
+
// Feed the live backlog to the stall watchdog rather than tripping on
|
|
1912
|
+
// the instantaneous byte count: a single large-but-draining frame (a
|
|
1913
|
+
// resume repaint of many inline images) must open normally, while a
|
|
1914
|
+
// never-draining reader is still torn down (#6854, #10430).
|
|
1915
|
+
this.#trackStdoutBacklog(pump.write(data));
|
|
1862
1916
|
} catch (err) {
|
|
1863
1917
|
this.#markTerminalDisconnected("stdout failed", err);
|
|
1864
1918
|
}
|
|
@@ -1882,26 +1936,19 @@ export class ProcessTerminal implements Terminal {
|
|
|
1882
1936
|
// and a code-unit cap would let CJK transcript rows expand past the
|
|
1883
1937
|
// threshold. See #2034 and #2095.
|
|
1884
1938
|
const bytes = Buffer.byteLength(data, "utf8");
|
|
1885
|
-
let accepted: boolean;
|
|
1886
1939
|
if (this.#conpty && bytes > MAX_CONPTY_WRITE_CHUNK_BYTES) {
|
|
1887
|
-
accepted = true;
|
|
1888
1940
|
for (const chunk of chunkForConPTY(data, MAX_CONPTY_WRITE_CHUNK_BYTES)) {
|
|
1889
1941
|
if (this.#dead) break;
|
|
1890
|
-
|
|
1942
|
+
process.stdout.write(chunk);
|
|
1891
1943
|
}
|
|
1892
1944
|
} else {
|
|
1893
|
-
|
|
1894
|
-
}
|
|
1895
|
-
// A stalled-but-alive PTY consumer never throws: write() just returns
|
|
1896
|
-
// false and queues the bytes. Bound that never-draining backlog by
|
|
1897
|
-
// declaring the terminal disconnected once it crosses the cap — the
|
|
1898
|
-
// same clean-exit path a dead terminal takes (#6854).
|
|
1899
|
-
if (this.#stdoutBacklog.record(accepted, bytes)) {
|
|
1900
|
-
this.#markTerminalDisconnected("stdout backlog exceeded cap; PTY consumer stalled");
|
|
1901
|
-
} else if (this.#stdoutBacklog.tracking && !this.#stdoutDrainArmed) {
|
|
1902
|
-
this.#stdoutDrainArmed = true;
|
|
1903
|
-
process.stdout.once("drain", this.#stdoutDrainHandler);
|
|
1945
|
+
process.stdout.write(data);
|
|
1904
1946
|
}
|
|
1947
|
+
// A stalled-but-alive PTY consumer never throws: write() just queues the
|
|
1948
|
+
// bytes and writableLength grows. Feed that backlog to the stall watchdog
|
|
1949
|
+
// so a genuinely wedged reader is bounded (#6854) without killing a lone
|
|
1950
|
+
// oversized frame that is still draining (#10430).
|
|
1951
|
+
this.#trackStdoutBacklog(process.stdout.writableLength ?? 0);
|
|
1905
1952
|
} catch (err) {
|
|
1906
1953
|
this.#markTerminalDisconnected("stdout failed", err);
|
|
1907
1954
|
}
|
|
@@ -1917,6 +1964,47 @@ export class ProcessTerminal implements Terminal {
|
|
|
1917
1964
|
return process.stdout.writableLength ?? 0;
|
|
1918
1965
|
}
|
|
1919
1966
|
|
|
1967
|
+
/**
|
|
1968
|
+
* Reconcile the stdout backlog after a write or a poll. The watchdog runs an
|
|
1969
|
+
* episode from the moment the backlog crosses the arm cap until it drains to
|
|
1970
|
+
* a healthy level; while an episode is armed we keep a poll running because,
|
|
1971
|
+
* once the render gate (256 KiB) defers frames, no write is guaranteed to
|
|
1972
|
+
* re-sample the backlog — so a consumer that wedges anywhere above the
|
|
1973
|
+
* healthy threshold, even after the backlog dips below the arm cap, is still
|
|
1974
|
+
* caught. See {@link StdoutStallWatchdog}, #6854, #10430, and #10434.
|
|
1975
|
+
*/
|
|
1976
|
+
#trackStdoutBacklog(pending: number): void {
|
|
1977
|
+
if (this.#stdoutStall.sample(pending, Date.now())) {
|
|
1978
|
+
this.#disarmStdoutStallWatchdog();
|
|
1979
|
+
this.#markTerminalDisconnected("stdout backlog stalled without draining; PTY consumer stalled");
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
if (!this.#stdoutStall.armed) {
|
|
1983
|
+
this.#disarmStdoutStallWatchdog();
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
if (!this.#stdoutStallTimer) {
|
|
1987
|
+
this.#stdoutStallTimer = setInterval(() => this.#pollStdoutStall(), STDOUT_STALL_POLL_MS);
|
|
1988
|
+
this.#stdoutStallTimer.unref?.();
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
#pollStdoutStall(): void {
|
|
1993
|
+
if (this.#dead) {
|
|
1994
|
+
this.#disarmStdoutStallWatchdog();
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
this.#trackStdoutBacklog(this.pendingOutputBytes);
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
#disarmStdoutStallWatchdog(): void {
|
|
2001
|
+
this.#stdoutStall.reset();
|
|
2002
|
+
if (this.#stdoutStallTimer) {
|
|
2003
|
+
clearInterval(this.#stdoutStallTimer);
|
|
2004
|
+
this.#stdoutStallTimer = undefined;
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
|
|
1920
2008
|
get rows(): number {
|
|
1921
2009
|
if (this.#inBandResizeActive && this.#reportedRows) return this.#reportedRows;
|
|
1922
2010
|
return process.stdout.rows || Number(Bun.env.LINES) || 24;
|