@code-yeongyu/senpi-tui 2026.9.12 → 2026.9.13-2

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 (107) hide show
  1. package/README.md +61 -4
  2. package/dist/alt-screen-search.d.ts +21 -1
  3. package/dist/alt-screen-search.d.ts.map +1 -1
  4. package/dist/alt-screen-search.js +199 -42
  5. package/dist/alt-screen-search.js.map +1 -1
  6. package/dist/components/box.d.ts +3 -1
  7. package/dist/components/box.d.ts.map +1 -1
  8. package/dist/components/box.js +28 -0
  9. package/dist/components/box.js.map +1 -1
  10. package/dist/components/editor.d.ts +6 -1
  11. package/dist/components/editor.d.ts.map +1 -1
  12. package/dist/components/editor.js +98 -18
  13. package/dist/components/editor.js.map +1 -1
  14. package/dist/components/input.d.ts +12 -1
  15. package/dist/components/input.d.ts.map +1 -1
  16. package/dist/components/input.js +40 -6
  17. package/dist/components/input.js.map +1 -1
  18. package/dist/components/latex.d.ts.map +1 -1
  19. package/dist/components/latex.js +7 -0
  20. package/dist/components/latex.js.map +1 -1
  21. package/dist/components/loader.d.ts +3 -0
  22. package/dist/components/loader.d.ts.map +1 -1
  23. package/dist/components/loader.js +20 -8
  24. package/dist/components/loader.js.map +1 -1
  25. package/dist/components/mouse-region.d.ts +12 -0
  26. package/dist/components/mouse-region.d.ts.map +1 -0
  27. package/dist/components/mouse-region.js +19 -0
  28. package/dist/components/mouse-region.js.map +1 -0
  29. package/dist/components/scroll-view.d.ts +6 -3
  30. package/dist/components/scroll-view.d.ts.map +1 -1
  31. package/dist/components/scroll-view.js +6 -1
  32. package/dist/components/scroll-view.js.map +1 -1
  33. package/dist/components/select-list.d.ts +4 -1
  34. package/dist/components/select-list.d.ts.map +1 -1
  35. package/dist/components/select-list.js +48 -2
  36. package/dist/components/select-list.js.map +1 -1
  37. package/dist/components/settings-list.d.ts +5 -1
  38. package/dist/components/settings-list.d.ts.map +1 -1
  39. package/dist/components/settings-list.js +54 -5
  40. package/dist/components/settings-list.js.map +1 -1
  41. package/dist/index.d.ts +5 -2
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +4 -0
  44. package/dist/index.js.map +1 -1
  45. package/dist/layout.d.ts +3 -1
  46. package/dist/layout.d.ts.map +1 -1
  47. package/dist/layout.js +32 -9
  48. package/dist/layout.js.map +1 -1
  49. package/dist/mouse-input.d.ts +37 -0
  50. package/dist/mouse-input.d.ts.map +1 -0
  51. package/dist/mouse-input.js +97 -0
  52. package/dist/mouse-input.js.map +1 -0
  53. package/dist/native-modifiers.d.ts +2 -1
  54. package/dist/native-modifiers.d.ts.map +1 -1
  55. package/dist/native-modifiers.js +3 -44
  56. package/dist/native-modifiers.js.map +1 -1
  57. package/dist/native-platform.d.ts +18 -0
  58. package/dist/native-platform.d.ts.map +1 -0
  59. package/dist/native-platform.js +42 -0
  60. package/dist/native-platform.js.map +1 -0
  61. package/dist/stdin-buffer.d.ts +2 -0
  62. package/dist/stdin-buffer.d.ts.map +1 -1
  63. package/dist/stdin-buffer.js +38 -0
  64. package/dist/stdin-buffer.js.map +1 -1
  65. package/dist/terminal.d.ts +25 -1
  66. package/dist/terminal.d.ts.map +1 -1
  67. package/dist/terminal.js +112 -24
  68. package/dist/terminal.js.map +1 -1
  69. package/dist/tui-alt-screen.d.ts +31 -1
  70. package/dist/tui-alt-screen.d.ts.map +1 -1
  71. package/dist/tui-alt-screen.js +327 -78
  72. package/dist/tui-alt-screen.js.map +1 -1
  73. package/dist/tui-main-screen.d.ts +12 -0
  74. package/dist/tui-main-screen.d.ts.map +1 -1
  75. package/dist/tui-main-screen.js +129 -3
  76. package/dist/tui-main-screen.js.map +1 -1
  77. package/dist/tui.d.ts +111 -5
  78. package/dist/tui.d.ts.map +1 -1
  79. package/dist/tui.js +278 -4
  80. package/dist/tui.js.map +1 -1
  81. package/dist/utils.d.ts +2 -0
  82. package/dist/utils.d.ts.map +1 -1
  83. package/dist/utils.js +9 -0
  84. package/dist/utils.js.map +1 -1
  85. package/native/clipboard.h +170 -0
  86. package/native/darwin/README.md +8 -6
  87. package/native/darwin/build.sh +12 -5
  88. package/native/darwin/prebuilds/darwin-arm64/{darwin-modifiers.node → darwin-platform.node} +0 -0
  89. package/native/darwin/prebuilds/darwin-x64/{darwin-modifiers.node → darwin-platform.node} +0 -0
  90. package/native/darwin/src/darwin-platform.m +93 -0
  91. package/native/linux/README.md +23 -0
  92. package/native/linux/build.sh +65 -0
  93. package/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  94. package/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  95. package/native/linux/src/clipboard-worker.h +70 -0
  96. package/native/linux/src/linux-platform-x11.c +274 -0
  97. package/native/napi.h +89 -0
  98. package/native/win32/README.md +18 -7
  99. package/native/win32/build.mjs +7 -4
  100. package/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  101. package/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  102. package/native/win32/src/win32-platform.c +241 -0
  103. package/package.json +10 -3
  104. package/native/darwin/src/darwin-modifiers.c +0 -70
  105. package/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  106. package/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  107. package/native/win32/src/win32-console-mode.c +0 -119
package/dist/tui.js CHANGED
@@ -60,6 +60,41 @@ function extractKittyImageRows(line) {
60
60
  function isTermuxSession() {
61
61
  return Boolean(process.env.TERMUX_VERSION);
62
62
  }
63
+ /**
64
+ * Dispatch an event to a component and retain the exact target and coordinate
65
+ * transform. Containers use this when forwarding events to nested children.
66
+ */
67
+ export function dispatchMouseEvent(component, event) {
68
+ const result = component.handleMouse?.(event);
69
+ if (!result)
70
+ return undefined;
71
+ if ("target" in result)
72
+ return result;
73
+ if (!result.handled && !result.capture && !result.focus)
74
+ return undefined;
75
+ return {
76
+ ...result,
77
+ handled: true,
78
+ ...(result.focus ? { focusTarget: component } : {}),
79
+ target: {
80
+ component,
81
+ originX: event.screenX - event.x,
82
+ originY: event.screenY - event.y,
83
+ width: event.width,
84
+ height: event.height,
85
+ },
86
+ };
87
+ }
88
+ /** Recreate local coordinates for a previously dispatched mouse target. */
89
+ export function retargetMouseEvent(event, target) {
90
+ return {
91
+ ...event,
92
+ x: event.screenX - target.originX,
93
+ y: event.screenY - target.originY,
94
+ width: target.width,
95
+ height: target.height,
96
+ };
97
+ }
63
98
  /** Type guard to check if a component implements Focusable */
64
99
  export function isFocusable(component) {
65
100
  return component !== null && "focused" in component;
@@ -250,8 +285,31 @@ export class Container {
250
285
  child.invalidate?.();
251
286
  }
252
287
  }
288
+ handleMouse(event) {
289
+ if (event.y < 0 || event.y >= event.height)
290
+ return undefined;
291
+ const mouseChildren = this.mouseLayout?.width === event.width
292
+ ? this.mouseLayout.children
293
+ : this.children.map((component) => ({ component, height: component.render(event.width).length }));
294
+ let childY = 0;
295
+ for (const { component: child, height: childHeight } of mouseChildren) {
296
+ if (event.y >= childY && event.y < childY + childHeight) {
297
+ const result = dispatchMouseEvent(child, {
298
+ ...event,
299
+ y: event.y - childY,
300
+ height: childHeight,
301
+ });
302
+ if (result?.focus && this.handleInput)
303
+ return { ...result, focusTarget: this };
304
+ return result;
305
+ }
306
+ childY += childHeight;
307
+ }
308
+ return undefined;
309
+ }
253
310
  render(width) {
254
311
  const lines = [];
312
+ const mouseChildren = [];
255
313
  for (const child of this.children) {
256
314
  let childLines;
257
315
  try {
@@ -263,10 +321,12 @@ export class Container {
263
321
  // Focus ownership stays unchanged; render containment must not steal or clear focus implicitly.
264
322
  childLines = [`[render error: ${componentName}]`];
265
323
  }
324
+ mouseChildren.push({ component: child, height: childLines.length });
266
325
  for (const line of childLines) {
267
326
  lines.push(line);
268
327
  }
269
328
  }
329
+ this.mouseLayout = { width, children: mouseChildren };
270
330
  return lines;
271
331
  }
272
332
  }
@@ -352,7 +412,15 @@ export class TuiBase extends Container {
352
412
  // Overlay stack for modal components rendered on top of base content
353
413
  this.focusOrderCounter = 0;
354
414
  this.overlayStack = [];
415
+ this.renderedOverlayLayouts = [];
355
416
  this.overlayFocusRestore = { status: "inactive" };
417
+ this.mouseLeases = new Map();
418
+ this.mouseBlockers = new Set();
419
+ this.placementEpoch = 0;
420
+ this.anchor = { kind: "unknown", epoch: 0, rows: 0, columns: 0 };
421
+ this.mouseCommittedLineCount = 0;
422
+ this.mouseAnchorPending = false;
423
+ this.mouseExternalWritePending = false;
356
424
  this.terminal = terminal;
357
425
  // Preserve existing positional boolean callers while allowing explicit render-policy overrides.
358
426
  const normalizedOptions = typeof options === "boolean" ? { showHardwareCursor: options } : (options ?? {});
@@ -362,6 +430,152 @@ export class TuiBase extends Container {
362
430
  this.showHardwareCursor = normalizedOptions.showHardwareCursor;
363
431
  }
364
432
  }
433
+ /** Host-owned intent. A replacement renderer starts with no leases. */
434
+ acquireMouseCapture(reason) {
435
+ const token = Symbol(reason);
436
+ const first = this.mouseLeases.size === 0;
437
+ this.mouseLeases.set(token, reason);
438
+ if (first) {
439
+ this.mouseWriteUnsubscribe ??= this.terminal.observeExternalWrites?.(() => {
440
+ this.placementEpoch++;
441
+ this.mouseExternalWritePending = true;
442
+ });
443
+ this.applyMouseTracking(this.mouseCaptureEnabled);
444
+ this.calibrateMouseAnchor();
445
+ }
446
+ return () => {
447
+ if (!this.mouseLeases.delete(token))
448
+ return;
449
+ if (this.mouseLeases.size === 0)
450
+ this.applyMouseTracking(false);
451
+ };
452
+ }
453
+ resetMouseCaptureState() {
454
+ this.mouseLeases.clear();
455
+ this.mouseBlockers.clear();
456
+ this.mouseWriteUnsubscribe?.();
457
+ this.mouseWriteUnsubscribe = undefined;
458
+ this.placementEpoch++;
459
+ }
460
+ get mouseCaptureEnabled() {
461
+ return this.mouseLeases.size > 0 && this.mouseBlockers.size === 0;
462
+ }
463
+ applyMouseTracking(_enabled) { }
464
+ getMouseLayoutRoots() {
465
+ return [...this.getMountedRoots(), ...this.renderedOverlayLayouts.map((layout) => layout.entry.component)];
466
+ }
467
+ setMouseBlocker(name, on) {
468
+ if (this.mouseBlockers.has(name) === on)
469
+ return;
470
+ if (on)
471
+ this.mouseBlockers.add(name);
472
+ else
473
+ this.mouseBlockers.delete(name);
474
+ this.placementEpoch++;
475
+ this.applyMouseTracking(this.mouseCaptureEnabled);
476
+ }
477
+ noteFullRender(clear) {
478
+ this.placementEpoch++;
479
+ this.anchor = {
480
+ kind: clear ? "cleared" : "unknown",
481
+ frameTopScreenRow: clear ? 0 : undefined,
482
+ epoch: this.placementEpoch,
483
+ rows: this.terminal.rows,
484
+ columns: this.terminal.columns,
485
+ };
486
+ this.mouseCommittedLineCount = this.previousLines.length;
487
+ this.noteCommittedMouseFrame();
488
+ this.calibrateMouseAnchor();
489
+ }
490
+ /** Called only after the renderer has published its geometry and bytes. */
491
+ noteCommittedMouseFrame() {
492
+ if (this.mouseCommittedLineCount !== this.previousLines.length)
493
+ this.placementEpoch++;
494
+ this.mouseCommittedLineCount = this.previousLines.length;
495
+ if (this.previousLines.some(isImageLine)) {
496
+ this.placementEpoch++;
497
+ this.anchor.kind = "unknown";
498
+ return;
499
+ }
500
+ if (this.previousLines.length >= this.terminal.rows) {
501
+ this.anchor = {
502
+ kind: "viewport",
503
+ epoch: this.placementEpoch,
504
+ rows: this.terminal.rows,
505
+ columns: this.terminal.columns,
506
+ };
507
+ }
508
+ else if (this.anchor.epoch !== this.placementEpoch ||
509
+ this.anchor.rows !== this.terminal.rows ||
510
+ this.anchor.columns !== this.terminal.columns) {
511
+ this.anchor = {
512
+ kind: "unknown",
513
+ epoch: this.placementEpoch,
514
+ rows: this.terminal.rows,
515
+ columns: this.terminal.columns,
516
+ };
517
+ }
518
+ }
519
+ /** Never block a frame on terminal protocol negotiation or a missing reply. */
520
+ calibrateMouseAnchor() {
521
+ if (!this.mouseCaptureEnabled ||
522
+ this.stopped ||
523
+ this.mouseAnchorPending ||
524
+ this.mouseExternalWritePending ||
525
+ !this.terminal.queryCursorPosition ||
526
+ this.previousLines.length === 0 ||
527
+ this.previousLines.some(isImageLine))
528
+ return;
529
+ if (this.anchor.kind !== "unknown" &&
530
+ this.anchor.epoch === this.placementEpoch &&
531
+ this.anchor.rows === this.terminal.rows &&
532
+ this.anchor.columns === this.terminal.columns)
533
+ return;
534
+ const epoch = this.placementEpoch;
535
+ const rows = this.terminal.rows;
536
+ const columns = this.terminal.columns;
537
+ const hardwareCursorRow = this.hardwareCursorRow;
538
+ const lineCount = this.previousLines.length;
539
+ this.mouseAnchorPending = true;
540
+ void this.terminal.queryCursorPosition().then((position) => {
541
+ this.mouseAnchorPending = false;
542
+ if (!position ||
543
+ this.stopped ||
544
+ !this.mouseCaptureEnabled ||
545
+ epoch !== this.placementEpoch ||
546
+ rows !== this.terminal.rows ||
547
+ columns !== this.terminal.columns ||
548
+ hardwareCursorRow !== this.hardwareCursorRow ||
549
+ lineCount !== this.previousLines.length)
550
+ return;
551
+ const top = position.row - 1 - hardwareCursorRow;
552
+ if (!Number.isSafeInteger(top) ||
553
+ top < 0 ||
554
+ top + lineCount > rows ||
555
+ !Number.isSafeInteger(position.column) ||
556
+ position.column < 1 ||
557
+ // Some emulators report the pending-wrap cell just past the right edge.
558
+ position.column > columns + 1 ||
559
+ (position.page !== undefined && position.page !== 1))
560
+ return;
561
+ this.anchor = { kind: "cpr", frameTopScreenRow: top, epoch, rows, columns };
562
+ });
563
+ }
564
+ /** Input rows are one-based; the returned committed frame line is zero-based. */
565
+ resolveFrameLine(screenRow) {
566
+ const anchor = this.anchor;
567
+ if (anchor.kind === "unknown" ||
568
+ anchor.epoch !== this.placementEpoch ||
569
+ anchor.rows !== this.terminal.rows ||
570
+ anchor.columns !== this.terminal.columns ||
571
+ screenRow < 1 ||
572
+ screenRow > anchor.rows)
573
+ return undefined;
574
+ const line = anchor.kind === "viewport"
575
+ ? this.previousViewportTop + screenRow - 1
576
+ : screenRow - 1 - (anchor.frameTopScreenRow ?? 0);
577
+ return line >= 0 && line < this.previousLines.length ? line : undefined;
578
+ }
365
579
  resetRenderState() { }
366
580
  beforeTerminalStart() { }
367
581
  afterTerminalStart() { }
@@ -387,8 +601,8 @@ export class TuiBase extends Container {
387
601
  }
388
602
  /**
389
603
  * Set whether to trigger full re-render when content shrinks.
390
- * When true (default), empty rows are cleared when content shrinks.
391
- * When false, empty rows remain (reduces redraws on slower terminals).
604
+ * When true, empty rows are cleared when content shrinks.
605
+ * When false (default), empty rows remain (reduces redraws on slower terminals).
392
606
  */
393
607
  setClearOnShrink(enabled) {
394
608
  this.clearOnShrink = enabled;
@@ -608,6 +822,11 @@ export class TuiBase extends Container {
608
822
  this.requestRender();
609
823
  },
610
824
  isFocused: () => this.focusedComponent === component,
825
+ getBounds: () => {
826
+ if (!this.overlayStack.includes(entry) || !this.isOverlayVisible(entry) || !entry.bounds)
827
+ return undefined;
828
+ return { ...entry.bounds };
829
+ },
611
830
  };
612
831
  }
613
832
  /** Hide the topmost overlay and restore previous focus. */
@@ -635,6 +854,42 @@ export class TuiBase extends Container {
635
854
  isOverlayFocused() {
636
855
  return this.overlayStack.some((entry) => entry.component === this.focusedComponent && this.isOverlayVisible(entry));
637
856
  }
857
+ /** Keep overlay containers as keyboard focus owners when a nested control is clicked. */
858
+ resolveMouseFocusTarget(component) {
859
+ for (let index = this.overlayStack.length - 1; index >= 0; index--) {
860
+ const overlay = this.overlayStack[index];
861
+ if (this.isOverlayVisible(overlay) && this.containsComponent(overlay.component, component)) {
862
+ return overlay.component;
863
+ }
864
+ }
865
+ return component;
866
+ }
867
+ /** Dispatch to the visually topmost overlay under the pointer. */
868
+ dispatchMouseToOverlay(event) {
869
+ for (let index = this.renderedOverlayLayouts.length - 1; index >= 0; index--) {
870
+ const layout = this.renderedOverlayLayouts[index];
871
+ if (event.screenX < layout.col ||
872
+ event.screenX >= layout.col + layout.width ||
873
+ event.screenY < layout.row ||
874
+ event.screenY >= layout.row + layout.height) {
875
+ continue;
876
+ }
877
+ const result = dispatchMouseEvent(layout.entry.component, {
878
+ ...event,
879
+ x: event.screenX - layout.col,
880
+ y: event.screenY - layout.row,
881
+ width: layout.width,
882
+ height: layout.height,
883
+ });
884
+ return result
885
+ ? {
886
+ hit: true,
887
+ result: result.focus ? { ...result, focusTarget: layout.entry.component } : result,
888
+ }
889
+ : { hit: true };
890
+ }
891
+ return { hit: false };
892
+ }
638
893
  /** Check if an overlay entry is currently visible */
639
894
  isOverlayVisible(entry) {
640
895
  if (entry.hidden)
@@ -721,6 +976,7 @@ export class TuiBase extends Container {
721
976
  this.terminal.write("\x1b[?2031l");
722
977
  }
723
978
  this.beforeTerminalStop(options);
979
+ this.resetMouseCaptureState();
724
980
  // Move cursor to the end of the content to prevent overwriting/artifacts on exit.
725
981
  // Skipped when the screen is preserved for another renderer taking over this terminal.
726
982
  if (!options.preserveScreen && this.previousLines.length > 0) {
@@ -1129,9 +1385,13 @@ export class TuiBase extends Container {
1129
1385
  }
1130
1386
  /** Composite all overlays into content lines (sorted by focusOrder, higher = on top). */
1131
1387
  compositeOverlays(lines, termWidth, termHeight) {
1132
- if (this.overlayStack.length === 0)
1388
+ if (this.overlayStack.length === 0) {
1389
+ this.renderedOverlayLayouts = [];
1133
1390
  return lines;
1391
+ }
1134
1392
  const result = [...lines];
1393
+ for (const entry of this.overlayStack)
1394
+ entry.bounds = undefined;
1135
1395
  // Pre-render all visible overlays and calculate positions
1136
1396
  const rendered = [];
1137
1397
  let minLinesNeeded = result.length;
@@ -1150,9 +1410,17 @@ export class TuiBase extends Container {
1150
1410
  }
1151
1411
  // Get final row/col with actual overlay height
1152
1412
  const { row, col } = this.resolveOverlayLayout(options, overlayLines.length, termWidth, termHeight);
1153
- rendered.push({ overlayLines, row, col, w: width });
1413
+ entry.bounds = { row, col, width, height: overlayLines.length };
1414
+ rendered.push({ entry, overlayLines, row, col, w: width });
1154
1415
  minLinesNeeded = Math.max(minLinesNeeded, row + overlayLines.length);
1155
1416
  }
1417
+ this.renderedOverlayLayouts = rendered.map(({ entry, row, col, w, overlayLines }) => ({
1418
+ entry,
1419
+ row,
1420
+ col,
1421
+ width: w,
1422
+ height: overlayLines.length,
1423
+ }));
1156
1424
  // Pad to at least terminal height so overlays have screen-relative positions.
1157
1425
  // Excludes maxLinesRendered: the historical high-water mark caused self-reinforcing
1158
1426
  // inflation that pushed content into scrollback on terminal widen.
@@ -1435,6 +1703,7 @@ export class TuiBase extends Container {
1435
1703
  this.previousKittyImageIds = this.collectKittyImageIds(newLines);
1436
1704
  this.previousWidth = width;
1437
1705
  this.previousHeight = height;
1706
+ this.placementEpoch++;
1438
1707
  }
1439
1708
  renderScrollbackReplay(newLines, rawLines, cursorPos, width, height, prevViewportTop, hardwareCursorRow) {
1440
1709
  let buffer = TUI.FRAME_BEGIN;
@@ -1462,6 +1731,7 @@ export class TuiBase extends Container {
1462
1731
  this.previousKittyImageIds = this.collectKittyImageIds(newLines);
1463
1732
  this.previousWidth = width;
1464
1733
  this.previousHeight = height;
1734
+ this.placementEpoch++;
1465
1735
  }
1466
1736
  renderMuxViewportRepaint(newLines, rawLines, cursorPos, width, height, viewportTop = Math.max(0, newLines.length - height)) {
1467
1737
  const previousVisible = this.getViewportRows(this.previousLines, this.previousViewportTop, height);
@@ -1491,6 +1761,7 @@ export class TuiBase extends Container {
1491
1761
  this.previousKittyImageIds = this.collectKittyImageIds(newLines);
1492
1762
  this.previousWidth = width;
1493
1763
  this.previousHeight = height;
1764
+ this.placementEpoch++;
1494
1765
  return true;
1495
1766
  }
1496
1767
  /** Splice overlay content into a base line at a specific column. Single-pass optimized. */
@@ -1581,6 +1852,8 @@ export class TuiBase extends Container {
1581
1852
  const height = this.terminal.rows;
1582
1853
  const widthChanged = this.previousWidth !== 0 && this.previousWidth !== width;
1583
1854
  const heightChanged = this.previousHeight !== 0 && this.previousHeight !== height;
1855
+ if (widthChanged || heightChanged)
1856
+ this.placementEpoch++;
1584
1857
  const previousBufferLength = this.previousHeight > 0 ? this.previousViewportTop + this.previousHeight : height;
1585
1858
  let prevViewportTop = heightChanged ? Math.max(0, previousBufferLength - height) : this.previousViewportTop;
1586
1859
  let viewportTop = prevViewportTop;
@@ -1651,6 +1924,7 @@ export class TuiBase extends Container {
1651
1924
  this.previousKittyImageIds = this.collectKittyImageIds(newLines);
1652
1925
  this.previousWidth = width;
1653
1926
  this.previousHeight = height;
1927
+ this.noteFullRender(clear);
1654
1928
  };
1655
1929
  const debugRedraw = process.env.PI_DEBUG_REDRAW === "1";
1656
1930
  const logRedraw = (reason) => {