@code-yeongyu/senpi-tui 2026.11.5-1 → 2026.11.6-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.
@@ -62,8 +62,10 @@ export declare class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
62
62
  private readonly clock;
63
63
  private lastInputAt;
64
64
  private heldNewline;
65
- private heldNewlineEndsPaste;
65
+ private heldNewlineKind;
66
66
  private lastBurstPasteAt;
67
+ /** True when the last burst paste's own line breaks were bare CR (an unbracketed paste from xterm/VTE/iTerm2). */
68
+ private lastBurstPasteUsesCrLineBreaks;
67
69
  private burstTimer;
68
70
  constructor(options?: StdinBufferOptions);
69
71
  process(data: string | Buffer): void;
@@ -246,7 +246,9 @@ export class StdinBuffer extends EventEmitter {
246
246
  this.pasteBuffer = "";
247
247
  this.decoder = new StringDecoder("utf8");
248
248
  this.heldNewline = "";
249
- this.heldNewlineEndsPaste = false;
249
+ this.heldNewlineKind = "keystroke";
250
+ /** True when the last burst paste's own line breaks were bare CR (an unbracketed paste from xterm/VTE/iTerm2). */
251
+ this.lastBurstPasteUsesCrLineBreaks = false;
250
252
  this.burstTimer = null;
251
253
  this.timeoutMs = options.timeout ?? DEFAULT_SEQUENCE_TIMEOUT_MS;
252
254
  this.escapeTimeoutMs = options.escapeTimeout ?? DEFAULT_ESCAPE_TIMEOUT_MS;
@@ -316,10 +318,15 @@ export class StdinBuffer extends EventEmitter {
316
318
  if (burstGap !== undefined && burstGap >= this.burstWindowMs) {
317
319
  this.releaseHeldNewline();
318
320
  }
321
+ else if (this.heldNewlineKind === "enter" && !this.buffer.startsWith("\n")) {
322
+ // A held Enter only continues into the LF half of a CRLF pair; any other read means the submit
323
+ // stands, so the Enter lands before that read is framed.
324
+ this.releaseHeldNewline();
325
+ }
319
326
  else {
320
327
  this.buffer = this.heldNewline + this.buffer;
321
328
  this.heldNewline = "";
322
- this.heldNewlineEndsPaste = false;
329
+ this.heldNewlineKind = "keystroke";
323
330
  }
324
331
  }
325
332
  if (this.pasteMode) {
@@ -404,15 +411,28 @@ export class StdinBuffer extends EventEmitter {
404
411
  if (breaks === 0) {
405
412
  return false;
406
413
  }
414
+ const trailing = text.match(TRAILING_LINE_BREAK_RUN)?.[0];
415
+ const trailingEndsWithCr = trailing !== undefined && trailing.endsWith("\r");
416
+ // An unbracketed paste from a terminal that turns pasted newlines into bare CR (xterm, VTE, iTerm2) carries
417
+ // no LF at all, so its own trailing CR closes that paste. Only a burst whose breaks include LF or CRLF can
418
+ // end in the Enter key that submits it; anything else would submit a human's CR-only paste.
419
+ const crOnlyPasteBurst = trailingEndsWithCr && !text.includes("\n") && /[^\r\n]/.test(text);
407
420
  // Typing delivers one key per read, so a single read that holds two line breaks, or text after a
408
421
  // line break, is pasted. Both carry text: a run of bare Enters stays keystrokes.
409
422
  if ((breaks >= 2 || LINE_BREAK_BEFORE_TEXT.test(text)) && /[^\r\n]/.test(text)) {
423
+ const endsWithEnter = trailingEndsWithCr && !crOnlyPasteBurst;
424
+ const body = endsWithEnter ? text.slice(0, -1) : text;
410
425
  this.buffer = "";
411
426
  this.lastBurstPasteAt = this.lastInputAt;
412
- this.emit("paste", text);
427
+ this.lastBurstPasteUsesCrLineBreaks = !body.includes("\n");
428
+ this.emit("paste", body);
429
+ if (endsWithEnter) {
430
+ // The paste is committed first, so the Enter that trails it submits the block after it. It waits out
431
+ // the window only because a CRLF split across reads still belongs to its paste.
432
+ this.holdTrailingNewline("\r", "enter");
433
+ }
413
434
  return true;
414
435
  }
415
- const trailing = text.match(TRAILING_LINE_BREAK_RUN)?.[0];
416
436
  if (trailing === undefined || burstGap === undefined || burstGap >= this.burstWindowMs) {
417
437
  return false;
418
438
  }
@@ -422,20 +442,23 @@ export class StdinBuffer extends EventEmitter {
422
442
  const endsPaste = this.lastBurstPasteAt !== undefined &&
423
443
  this.lastInputAt !== undefined &&
424
444
  this.lastInputAt - this.lastBurstPasteAt < this.burstWindowMs;
425
- // A read of bare line breaks is a keystroke unless it closes a paste, so it is forwarded at once.
426
- if (head.length === 0 && !endsPaste) {
445
+ // A CR closing a burst whose own breaks were bare CR continues that paste, exactly as the same text in one
446
+ // read keeps it; after an LF/CRLF paste it is the Enter key the sender pressed.
447
+ const crContinuesPaste = endsPaste && this.lastBurstPasteUsesCrLineBreaks;
448
+ // A read of bare line breaks is a keystroke unless it closes a paste, and a submit is never held back.
449
+ if ((trailingEndsWithCr && !crContinuesPaste) || (head.length === 0 && !endsPaste)) {
427
450
  return false;
428
451
  }
429
452
  for (const chunk of head) {
430
453
  this.emitDataSequence(chunk);
431
454
  }
432
455
  this.buffer = "";
433
- this.holdTrailingNewline(trailing, endsPaste);
456
+ this.holdTrailingNewline(trailing, endsPaste ? "paste" : "keystroke");
434
457
  return true;
435
458
  }
436
- holdTrailingNewline(run, endsPaste) {
459
+ holdTrailingNewline(run, kind) {
437
460
  this.heldNewline = run;
438
- this.heldNewlineEndsPaste = endsPaste;
461
+ this.heldNewlineKind = kind;
439
462
  if (this.burstTimer) {
440
463
  clearTimeout(this.burstTimer);
441
464
  }
@@ -449,10 +472,10 @@ export class StdinBuffer extends EventEmitter {
449
472
  return;
450
473
  }
451
474
  const held = this.heldNewline;
452
- const endsPaste = this.heldNewlineEndsPaste;
475
+ const kind = this.heldNewlineKind;
453
476
  this.heldNewline = "";
454
- this.heldNewlineEndsPaste = false;
455
- if (endsPaste) {
477
+ this.heldNewlineKind = "keystroke";
478
+ if (kind === "paste") {
456
479
  this.emit("paste", held);
457
480
  return;
458
481
  }
@@ -509,8 +532,9 @@ export class StdinBuffer extends EventEmitter {
509
532
  this.burstTimer = null;
510
533
  }
511
534
  this.heldNewline = "";
512
- this.heldNewlineEndsPaste = false;
535
+ this.heldNewlineKind = "keystroke";
513
536
  this.lastBurstPasteAt = undefined;
537
+ this.lastBurstPasteUsesCrLineBreaks = false;
514
538
  this.buffer = "";
515
539
  this.pasteMode = false;
516
540
  this.pasteBuffer = "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-tui",
3
- "version": "2026.11.5-1",
3
+ "version": "2026.11.6-1",
4
4
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",