@prometheus-ai/tui 0.5.4 → 0.5.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.
Files changed (55) hide show
  1. package/dist/types/autocomplete.d.ts +3 -1
  2. package/dist/types/components/box.d.ts +1 -1
  3. package/dist/types/components/editor.d.ts +35 -2
  4. package/dist/types/components/image.d.ts +22 -3
  5. package/dist/types/components/input.d.ts +6 -1
  6. package/dist/types/components/loader.d.ts +9 -2
  7. package/dist/types/components/markdown.d.ts +3 -1
  8. package/dist/types/components/scroll-view.d.ts +23 -1
  9. package/dist/types/components/select-list.d.ts +19 -1
  10. package/dist/types/components/settings-list.d.ts +87 -7
  11. package/dist/types/components/spacer.d.ts +1 -1
  12. package/dist/types/components/tab-bar.d.ts +37 -4
  13. package/dist/types/components/text.d.ts +2 -2
  14. package/dist/types/components/truncated-text.d.ts +1 -1
  15. package/dist/types/fuzzy.d.ts +10 -1
  16. package/dist/types/index.d.ts +1 -0
  17. package/dist/types/keybindings.d.ts +5 -3
  18. package/dist/types/keys.d.ts +1 -1
  19. package/dist/types/kill-ring.d.ts +0 -7
  20. package/dist/types/kitty-graphics.d.ts +16 -31
  21. package/dist/types/loop-watchdog.d.ts +39 -0
  22. package/dist/types/mouse.d.ts +41 -0
  23. package/dist/types/stdin-buffer.d.ts +17 -0
  24. package/dist/types/terminal-capabilities.d.ts +74 -18
  25. package/dist/types/terminal.d.ts +34 -36
  26. package/dist/types/tui.d.ts +191 -79
  27. package/dist/types/utils.d.ts +5 -2
  28. package/package.json +4 -4
  29. package/src/autocomplete.ts +79 -65
  30. package/src/components/box.ts +43 -63
  31. package/src/components/editor.ts +471 -136
  32. package/src/components/image.ts +85 -9
  33. package/src/components/input.ts +12 -3
  34. package/src/components/loader.ts +35 -21
  35. package/src/components/markdown.ts +174 -53
  36. package/src/components/scroll-view.ts +63 -2
  37. package/src/components/select-list.ts +233 -38
  38. package/src/components/settings-list.ts +626 -64
  39. package/src/components/spacer.ts +9 -5
  40. package/src/components/tab-bar.ts +153 -28
  41. package/src/components/text.ts +6 -2
  42. package/src/components/truncated-text.ts +10 -2
  43. package/src/fuzzy.ts +214 -59
  44. package/src/index.ts +3 -1
  45. package/src/keybindings.ts +72 -14
  46. package/src/keys.ts +1 -1
  47. package/src/kill-ring.ts +5 -0
  48. package/src/kitty-graphics.ts +2 -101
  49. package/src/loop-watchdog.ts +106 -0
  50. package/src/mouse.ts +55 -0
  51. package/src/stdin-buffer.ts +291 -81
  52. package/src/terminal-capabilities.ts +206 -168
  53. package/src/terminal.ts +367 -110
  54. package/src/tui.ts +2102 -1729
  55. package/src/utils.ts +92 -60
@@ -17,11 +17,32 @@
17
17
  * MIT License - Copyright (c) 2025 opentui
18
18
  */
19
19
  import { EventEmitter } from "events";
20
+ import { isKittyProtocolActive } from "./keys";
20
21
 
21
22
  const ESC = "\x1b";
22
23
  const BRACKETED_PASTE_START = "\x1b[200~";
23
24
  const BRACKETED_PASTE_END = "\x1b[201~";
24
-
25
+ // Paste-mode recovery bounds: a lost/corrupted end marker (ssh/tmux
26
+ // truncation) must not hang input forever or grow memory unboundedly.
27
+ const PASTE_INACTIVITY_TIMEOUT_MS = 1000;
28
+ const PASTE_MAX_BYTES = 64 * 1024 * 1024;
29
+ // A buggy double-report (CSI-u event plus the bare printable for the same
30
+ // keypress) arrives in the same terminal write; a bare char that shows up
31
+ // later than this window is a real keystroke and must not be swallowed.
32
+ const KITTY_PRINTABLE_DEDUP_WINDOW_MS = 25;
33
+ // An SGR mouse report prefix is unambiguous: no keyboard sequence starts with
34
+ // `\x1b[<`, so a buffer still matching this is always the head of a split
35
+ // mouse report. Flushing it on timeout would deliver the tail as literal
36
+ // typed text to whatever component is focused (fullscreen overlays enable
37
+ // any-motion tracking, so report floods plus render stalls make the split
38
+ // routine — see the settings search leaking `[<35;8;16M`).
39
+ const SGR_MOUSE_PARTIAL = /^\x1b\[<[\d;]*$/;
40
+ // Upper bound on how long an unambiguous partial is held past the flush
41
+ // timeout before being delivered raw anyway (terminal died mid-sequence).
42
+ // This is also the worst-case added latency for a partial that never
43
+ // completes (e.g. a bare ESC delivered while the kitty-active flag is
44
+ // stale); keep it small.
45
+ const PARTIAL_HOLD_MAX_MS = 150;
25
46
  /**
26
47
  * Check if a string is a complete escape sequence or needs more data
27
48
  */
@@ -202,41 +223,67 @@ function parseUnmodifiedKittyPrintableCodepoint(sequence: string): number | unde
202
223
 
203
224
  function extractCompleteSequences(buffer: string): { sequences: string[]; remainder: string } {
204
225
  const sequences: string[] = [];
226
+ const length = buffer.length;
205
227
  let pos = 0;
206
228
 
207
- while (pos < buffer.length) {
208
- const remaining = buffer.slice(pos);
209
-
210
- // Try to extract a sequence starting at this position
211
- if (remaining.startsWith(ESC)) {
212
- // Find the end of this escape sequence
213
- let seqEnd = 1;
214
- while (seqEnd <= remaining.length) {
215
- const candidate = remaining.slice(0, seqEnd);
229
+ // Index-based scanning: this is the input hot path. Slicing the remaining
230
+ // buffer (or Array.from-ing it) per iteration would make plain-text bursts
231
+ // O(n²) — a 100KB non-bracketed paste must stay O(n).
232
+ while (pos < length) {
233
+ if (buffer.charCodeAt(pos) === 0x1b) {
234
+ // Find the end of this escape sequence by growing the candidate.
235
+ let end = pos + 1;
236
+ let consumed = false;
237
+ while (end <= length) {
238
+ const candidate = buffer.slice(pos, end);
216
239
  const status = isCompleteSequence(candidate);
217
-
218
- if (status === "complete") {
219
- sequences.push(candidate);
220
- pos += seqEnd;
221
- break;
222
- } else if (status === "incomplete") {
223
- seqEnd++;
224
- } else {
225
- // Should not happen when starting with ESC
226
- sequences.push(candidate);
227
- pos += seqEnd;
240
+ if (status === "incomplete") {
241
+ end++;
242
+ continue;
243
+ }
244
+ // "\x1b\x1b" alone parses as "complete" (legacy alt+esc), but when the
245
+ // next byte opens a CSI/SS3 ("[" or "O") this is really ESC prefixing
246
+ // another sequence (meta-CSI, or a held Esc keypress joined by a
247
+ // follower). Consuming two bytes here would tear the follower and leak
248
+ // its tail as typed text (settings search filling with "[B" or
249
+ // "[<35;22;17M"). Keep growing; when the buffer ends here, hold the
250
+ // partial for the flush window so the disambiguating byte can arrive.
251
+ if (candidate === `${ESC}${ESC}`) {
252
+ if (end >= length) {
253
+ return { sequences, remainder: buffer.slice(pos) };
254
+ }
255
+ const next = buffer.charCodeAt(end);
256
+ if (next === 0x5b || next === 0x4f) {
257
+ end++;
258
+ continue;
259
+ }
260
+ }
261
+ // ESC + SGR mouse report is never a meta chord: alt-modified mouse
262
+ // reports carry the modifier in the button bits, not an ESC prefix.
263
+ // Deliver the bare ESC (a real Esc keypress) and the report separately.
264
+ if (candidate.startsWith(`${ESC}${ESC}[<`)) {
265
+ sequences.push(ESC, candidate.slice(1));
266
+ pos = end;
267
+ consumed = true;
228
268
  break;
229
269
  }
270
+ // "complete" — or "not-escape", which should not happen when
271
+ // starting with ESC; both consume the candidate.
272
+ sequences.push(candidate);
273
+ pos = end;
274
+ consumed = true;
275
+ break;
230
276
  }
231
277
 
232
- if (seqEnd > remaining.length) {
233
- return { sequences, remainder: remaining };
278
+ if (!consumed) {
279
+ return { sequences, remainder: buffer.slice(pos) };
234
280
  }
235
281
  } else {
236
282
  // Not an escape sequence - take one Unicode scalar, not a UTF-16 code unit.
237
- const char = Array.from(remaining)[0] ?? "";
238
- sequences.push(char);
239
- pos += char.length;
283
+ const codePoint = buffer.codePointAt(pos)!;
284
+ const charLength = codePoint > 0xffff ? 2 : 1;
285
+ sequences.push(buffer.slice(pos, pos + charLength));
286
+ pos += charLength;
240
287
  }
241
288
  }
242
289
 
@@ -249,6 +296,23 @@ export type StdinBufferOptions = {
249
296
  * After this time, a genuinely incomplete escape is flushed.
250
297
  */
251
298
  timeout?: number;
299
+ /**
300
+ * Maximum extra time (default: 150ms) an unambiguous escape partial — an
301
+ * SGR mouse prefix, or any dangling escape while the kitty keyboard
302
+ * protocol is active — is held past `timeout` waiting for its tail.
303
+ */
304
+ partialHoldTimeout?: number;
305
+ /**
306
+ * Paste-mode inactivity watchdog (default: 1000ms). If no input arrives for
307
+ * this long while waiting for the bracketed-paste end marker, the paste is
308
+ * assumed truncated: accumulated bytes are delivered and input recovers.
309
+ */
310
+ pasteTimeout?: number;
311
+ /**
312
+ * Paste-mode byte cap (default: 64 MiB). Exceeding it aborts paste mode the
313
+ * same way, bounding memory when the end marker never arrives.
314
+ */
315
+ pasteByteLimit?: number;
252
316
  };
253
317
 
254
318
  export type StdinBufferEventMap = {
@@ -263,23 +327,29 @@ export type StdinBufferEventMap = {
263
327
  export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
264
328
  #buffer: string = "";
265
329
  #timeout?: NodeJS.Timeout;
330
+ #flushDeferral?: NodeJS.Timeout;
331
+ #partialHoldStartMs = 0;
266
332
  readonly #timeoutMs: number;
333
+ readonly #partialHoldMaxMs: number;
334
+ readonly #pasteTimeoutMs: number;
335
+ readonly #pasteByteLimit: number;
267
336
  #pasteMode: boolean = false;
268
- #pasteBuffer: string = "";
337
+ #pasteChunks: string[] = [];
338
+ #pasteOverlap: string = "";
339
+ #pasteBytes = 0;
340
+ #pasteWatchdog?: NodeJS.Timeout;
269
341
  #pendingKittyPrintableCodepoint: number | undefined;
342
+ #pendingKittyPrintableAtMs = 0;
270
343
 
271
344
  constructor(options: StdinBufferOptions = {}) {
272
345
  super();
273
346
  this.#timeoutMs = options.timeout ?? 75;
347
+ this.#partialHoldMaxMs = options.partialHoldTimeout ?? PARTIAL_HOLD_MAX_MS;
348
+ this.#pasteTimeoutMs = options.pasteTimeout ?? PASTE_INACTIVITY_TIMEOUT_MS;
349
+ this.#pasteByteLimit = options.pasteByteLimit ?? PASTE_MAX_BYTES;
274
350
  }
275
351
 
276
352
  process(data: string | Buffer): void {
277
- // Clear any pending timeout
278
- if (this.#timeout) {
279
- clearTimeout(this.#timeout);
280
- this.#timeout = undefined;
281
- }
282
-
283
353
  // Handle high-byte conversion (for compatibility with parseKeypress)
284
354
  // If buffer has single byte > 127, convert to ESC + (byte - 128)
285
355
  let str: string;
@@ -294,6 +364,16 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
294
364
  str = data;
295
365
  }
296
366
 
367
+ if (this.#flushDeferral && this.#isFreshEscapeAfterDeferredFlush(str)) {
368
+ // The buffered partial already hit its flush timeout. A new escape is
369
+ // a fresh sequence, not a tail; flush the stale partial first so the
370
+ // new sequence can be parsed from a clean buffer.
371
+ this.#flushExpired();
372
+ } else {
373
+ // Cancel any pending flush — new data may complete the buffered partial.
374
+ this.#clearFlushTimer();
375
+ }
376
+
297
377
  if (str.length === 0 && this.#buffer.length === 0) {
298
378
  this.#emitDataSequence("");
299
379
  return;
@@ -302,24 +382,9 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
302
382
  this.#buffer += str;
303
383
 
304
384
  if (this.#pasteMode) {
305
- this.#pasteBuffer += this.#buffer;
385
+ const chunk = this.#buffer;
306
386
  this.#buffer = "";
307
-
308
- const endIndex = this.#pasteBuffer.indexOf(BRACKETED_PASTE_END);
309
- if (endIndex !== -1) {
310
- const pastedContent = this.#pasteBuffer.slice(0, endIndex);
311
- const remaining = this.#pasteBuffer.slice(endIndex + BRACKETED_PASTE_END.length);
312
-
313
- this.#pasteMode = false;
314
- this.#pasteBuffer = "";
315
- this.#pendingKittyPrintableCodepoint = undefined;
316
-
317
- this.emit("paste", pastedContent);
318
-
319
- if (remaining.length > 0) {
320
- this.process(remaining);
321
- }
322
- }
387
+ this.#consumePasteChunk(chunk);
323
388
  return;
324
389
  }
325
390
 
@@ -335,25 +400,13 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
335
400
 
336
401
  this.#pendingKittyPrintableCodepoint = undefined;
337
402
  this.#buffer = this.#buffer.slice(startIndex + BRACKETED_PASTE_START.length);
338
- this.#pasteMode = true;
339
- this.#pasteBuffer = this.#buffer;
403
+ const firstChunk = this.#buffer;
340
404
  this.#buffer = "";
341
-
342
- const endIndex = this.#pasteBuffer.indexOf(BRACKETED_PASTE_END);
343
- if (endIndex !== -1) {
344
- const pastedContent = this.#pasteBuffer.slice(0, endIndex);
345
- const remaining = this.#pasteBuffer.slice(endIndex + BRACKETED_PASTE_END.length);
346
-
347
- this.#pasteMode = false;
348
- this.#pasteBuffer = "";
349
- this.#pendingKittyPrintableCodepoint = undefined;
350
-
351
- this.emit("paste", pastedContent);
352
-
353
- if (remaining.length > 0) {
354
- this.process(remaining);
355
- }
356
- }
405
+ this.#pasteMode = true;
406
+ this.#pasteChunks = [];
407
+ this.#pasteOverlap = "";
408
+ this.#pasteBytes = 0;
409
+ this.#consumePasteChunk(firstChunk);
357
410
  return;
358
411
  }
359
412
 
@@ -365,32 +418,188 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
365
418
  }
366
419
 
367
420
  if (this.#buffer.length > 0) {
368
- this.#timeout = setTimeout(() => {
369
- const flushed = this.flush();
421
+ this.#armFlushTimer();
422
+ } else {
423
+ this.#partialHoldStartMs = 0;
424
+ }
425
+ }
370
426
 
371
- for (const sequence of flushed) {
372
- this.#emitDataSequence(sequence);
373
- }
374
- }, this.#timeoutMs);
427
+ /**
428
+ * Consume one chunk of paste-mode input. Chunks are accumulated in an array
429
+ * and only joined once the end marker arrives, so a large paste delivered in
430
+ * many small terminal reads stays O(total) instead of the O(total^2) cost of
431
+ * re-concatenating and rescanning the whole buffer on every chunk. A short
432
+ * overlap tail (end-marker length - 1) is carried across chunk boundaries so
433
+ * a marker split between two reads is still detected without rescanning.
434
+ */
435
+ #consumePasteChunk(chunk: string): void {
436
+ const probe = this.#pasteOverlap + chunk;
437
+ if (probe.indexOf(BRACKETED_PASTE_END) === -1) {
438
+ this.#pasteChunks.push(chunk);
439
+ this.#pasteBytes += chunk.length;
440
+ const keep = BRACKETED_PASTE_END.length - 1;
441
+ this.#pasteOverlap = probe.length > keep ? probe.slice(probe.length - keep) : probe;
442
+ if (this.#pasteBytes > this.#pasteByteLimit) {
443
+ this.#abortPaste();
444
+ return;
445
+ }
446
+ this.#armPasteWatchdog();
447
+ return;
448
+ }
449
+
450
+ // End marker arrived: join once and split at its first occurrence,
451
+ // matching the prior indexOf-from-start semantics exactly.
452
+ const flat = this.#pasteChunks.length > 0 ? `${this.#pasteChunks.join("")}${chunk}` : chunk;
453
+ const endIndex = flat.indexOf(BRACKETED_PASTE_END);
454
+ const pastedContent = flat.slice(0, endIndex);
455
+ const remaining = flat.slice(endIndex + BRACKETED_PASTE_END.length);
456
+
457
+ this.#clearPasteWatchdog();
458
+ this.#pasteMode = false;
459
+ this.#pasteChunks = [];
460
+ this.#pasteOverlap = "";
461
+ this.#pasteBytes = 0;
462
+ this.#pendingKittyPrintableCodepoint = undefined;
463
+
464
+ this.emit("paste", pastedContent);
465
+
466
+ if (remaining.length > 0) {
467
+ this.process(remaining);
468
+ }
469
+ }
470
+
471
+ /** Re-arm the paste-mode inactivity watchdog after each chunk. */
472
+ #armPasteWatchdog(): void {
473
+ if (this.#pasteWatchdog) clearTimeout(this.#pasteWatchdog);
474
+ this.#pasteWatchdog = setTimeout(() => {
475
+ this.#pasteWatchdog = undefined;
476
+ this.#abortPaste();
477
+ }, this.#pasteTimeoutMs);
478
+ }
479
+
480
+ #clearPasteWatchdog(): void {
481
+ if (this.#pasteWatchdog) {
482
+ clearTimeout(this.#pasteWatchdog);
483
+ this.#pasteWatchdog = undefined;
375
484
  }
376
485
  }
377
486
 
487
+ /**
488
+ * Recover from a paste whose end marker never arrived (dropped or corrupted
489
+ * in transit, or past the byte cap): exit paste mode and deliver the
490
+ * accumulated bytes as a paste, so they are neither lost, replayed as
491
+ * keystrokes, nor accumulated forever while input appears dead.
492
+ */
493
+ #abortPaste(): void {
494
+ this.#clearPasteWatchdog();
495
+ const content = this.#pasteChunks.join("");
496
+ this.#pasteMode = false;
497
+ this.#pasteChunks = [];
498
+ this.#pasteOverlap = "";
499
+ this.#pasteBytes = 0;
500
+ this.emit("paste", content);
501
+ }
502
+
378
503
  #emitDataSequence(sequence: string): void {
379
504
  const rawCodepoint = sequence.length === 1 ? sequence.codePointAt(0) : undefined;
380
- if (rawCodepoint !== undefined && rawCodepoint === this.#pendingKittyPrintableCodepoint) {
505
+ if (
506
+ rawCodepoint !== undefined &&
507
+ rawCodepoint === this.#pendingKittyPrintableCodepoint &&
508
+ Date.now() - this.#pendingKittyPrintableAtMs <= KITTY_PRINTABLE_DEDUP_WINDOW_MS
509
+ ) {
381
510
  this.#pendingKittyPrintableCodepoint = undefined;
382
511
  return;
383
512
  }
384
513
 
385
514
  this.#pendingKittyPrintableCodepoint = parseUnmodifiedKittyPrintableCodepoint(sequence);
515
+ if (this.#pendingKittyPrintableCodepoint !== undefined) {
516
+ this.#pendingKittyPrintableAtMs = Date.now();
517
+ }
386
518
  this.emit("data", sequence);
387
519
  }
388
520
 
389
- flush(): string[] {
521
+ /**
522
+ * setTimeout(0): when the event loop stalls past the timeout (heavy render)
523
+ * while the tail of a split escape is already queued on stdin, expired
524
+ * timers run before the poll phase that delivers the tail — flushing
525
+ * straight from the timer would tear the sequence apart and leak the tail
526
+ * as typed text. The zero-delay deferral runs on the next timers pass,
527
+ * after poll has had a chance to deliver the pending chunk to process()
528
+ * and cancel the deferral.
529
+ */
530
+ #armFlushTimer(): void {
531
+ this.#timeout = setTimeout(() => {
532
+ this.#timeout = undefined;
533
+ this.#flushDeferral = setTimeout(() => {
534
+ this.#flushDeferral = undefined;
535
+ this.#flushExpired();
536
+ });
537
+ }, this.#timeoutMs);
538
+ }
539
+
540
+ #clearFlushTimer(): void {
390
541
  if (this.#timeout) {
391
542
  clearTimeout(this.#timeout);
392
543
  this.#timeout = undefined;
393
544
  }
545
+ if (this.#flushDeferral) {
546
+ clearTimeout(this.#flushDeferral);
547
+ this.#flushDeferral = undefined;
548
+ }
549
+ }
550
+
551
+ /**
552
+ * A deferred flush means the current buffer already waited for the
553
+ * incomplete-sequence timeout. If the next chunk starts a fresh escape, do
554
+ * not merge it into the stale partial. Keep ESC-backslash as a continuation
555
+ * for OSC/DCS/APC string terminators (`ST`).
556
+ */
557
+ #isFreshEscapeAfterDeferredFlush(str: string): boolean {
558
+ if (!str.startsWith(ESC) || this.#buffer.length === 0) return false;
559
+ if (
560
+ str.startsWith(`${ESC}\\`) &&
561
+ (this.#buffer.startsWith(`${ESC}]`) ||
562
+ this.#buffer.startsWith(`${ESC}P`) ||
563
+ this.#buffer.startsWith(`${ESC}_`))
564
+ ) {
565
+ return false;
566
+ }
567
+ return true;
568
+ }
569
+
570
+ /**
571
+ * Whether the dangling partial cannot be a finished keypress and is worth
572
+ * holding for its tail instead of flushing:
573
+ * - SGR mouse prefixes (`\x1b[<…`) — no keyboard sequence uses them.
574
+ * - Any partial while the kitty keyboard protocol is active — the ESC key
575
+ * arrives as `\x1b[27u` and alt-chords as CSI-u, so a bare `\x1b` (or
576
+ * any unterminated escape) is always a split sequence, never a key.
577
+ */
578
+ #shouldHoldPartial(): boolean {
579
+ return SGR_MOUSE_PARTIAL.test(this.#buffer) || isKittyProtocolActive();
580
+ }
581
+
582
+ /** Timeout-driven flush: hold unambiguous partials (bounded), else deliver. */
583
+ #flushExpired(): void {
584
+ if (this.#buffer.length === 0) {
585
+ this.#partialHoldStartMs = 0;
586
+ return;
587
+ }
588
+ if (this.#shouldHoldPartial()) {
589
+ if (this.#partialHoldStartMs === 0) this.#partialHoldStartMs = Date.now();
590
+ if (Date.now() - this.#partialHoldStartMs < this.#partialHoldMaxMs) {
591
+ this.#armFlushTimer();
592
+ return;
593
+ }
594
+ }
595
+ this.#partialHoldStartMs = 0;
596
+ for (const sequence of this.flush()) {
597
+ this.#emitDataSequence(sequence);
598
+ }
599
+ }
600
+
601
+ flush(): string[] {
602
+ this.#clearFlushTimer();
394
603
 
395
604
  if (this.#buffer.length === 0) {
396
605
  return [];
@@ -403,14 +612,15 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
403
612
  }
404
613
 
405
614
  clear(): void {
406
- if (this.#timeout) {
407
- clearTimeout(this.#timeout);
408
- this.#timeout = undefined;
409
- }
615
+ this.#clearFlushTimer();
616
+ this.#clearPasteWatchdog();
410
617
  this.#buffer = "";
411
618
  this.#pasteMode = false;
412
- this.#pasteBuffer = "";
619
+ this.#pasteChunks = [];
620
+ this.#pasteOverlap = "";
621
+ this.#pasteBytes = 0;
413
622
  this.#pendingKittyPrintableCodepoint = undefined;
623
+ this.#partialHoldStartMs = 0;
414
624
  }
415
625
 
416
626
  getBuffer(): string {