@code-yeongyu/senpi-tui 2026.10.10-5 → 2026.10.10-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 (2) hide show
  1. package/dist/tui.js +57 -8
  2. package/package.json +1 -1
package/dist/tui.js CHANGED
@@ -624,6 +624,8 @@ export function isViewportTUI(tui) {
624
624
  return tui[VIEWPORT_TUI] === true;
625
625
  }
626
626
  export class TuiBase extends Container {
627
+ /** A multiplexer pane regained focus: the next frame repaints its viewport once (senpi#1704). */
628
+ #muxViewportRepaintPending;
627
629
  /** Minimum interval between scheduled renders. Default preserves the historic ~60fps cap. */
628
630
  #minRenderIntervalMs;
629
631
  #lastCursorVisibility;
@@ -641,6 +643,8 @@ export class TuiBase extends Container {
641
643
  this.previousRawLines = [];
642
644
  this.normalizeMemo = new Map();
643
645
  this.previousKittyImageIds = new Set();
646
+ /** A multiplexer pane regained focus: the next frame repaints its viewport once (senpi#1704). */
647
+ this.#muxViewportRepaintPending = false;
644
648
  this.previousWidth = 0;
645
649
  this.previousHeight = 0;
646
650
  this.focusedComponent = null;
@@ -1272,6 +1276,7 @@ export class TuiBase extends Container {
1272
1276
  // for an external editor or a suspend in the middle of a turn must not drop it.
1273
1277
  this.#scrollbackStale = false;
1274
1278
  this.#scrollbackCatchUpPending = false;
1279
+ this.#muxViewportRepaintPending = false;
1275
1280
  this.renderRequested = false;
1276
1281
  this.inputRenderPending = false;
1277
1282
  this.cancelRenderTimer();
@@ -1454,9 +1459,22 @@ export class TuiBase extends Container {
1454
1459
  if (this.mode !== "fullscreen") {
1455
1460
  const focus = consumeTmuxFocusEvent(data);
1456
1461
  if (focus.event !== null) {
1457
- resetCapabilitiesCache();
1458
- this.invalidate();
1459
- this.requestRender(true);
1462
+ if (this.shouldPreserveMuxScrollback()) {
1463
+ // senpi#1704: a forced render re-emitted the whole transcript into the pane's history on every
1464
+ // focus event. A pane losing focus is not visible, so it repaints nothing; a pane gaining focus
1465
+ // refreshes the terminal capabilities and repaints its viewport only.
1466
+ if (focus.event === "in") {
1467
+ resetCapabilitiesCache();
1468
+ this.invalidate();
1469
+ this.#muxViewportRepaintPending = true;
1470
+ this.requestRender();
1471
+ }
1472
+ }
1473
+ else {
1474
+ resetCapabilitiesCache();
1475
+ this.invalidate();
1476
+ this.requestRender(true);
1477
+ }
1460
1478
  if (focus.data.length === 0)
1461
1479
  return;
1462
1480
  data = focus.data;
@@ -2217,16 +2235,21 @@ export class TuiBase extends Container {
2217
2235
  this.placementEpoch++;
2218
2236
  return true;
2219
2237
  }
2220
- renderMuxViewportRepaint(newLines, rawLines, cursorPos, width, height, viewportTop = Math.max(0, newLines.length - height)) {
2238
+ renderMuxViewportRepaint(newLines, rawLines, cursorPos, width, height, viewportTop = Math.max(0, newLines.length - height), home = "relative") {
2221
2239
  const previousVisible = this.getViewportRows(this.previousLines, this.previousViewportTop, height);
2222
2240
  const nextVisible = this.getViewportRows(newLines, viewportTop, height);
2223
2241
  if (previousVisible.some(isImageLine) || nextVisible.some(isImageLine)) {
2224
2242
  return false;
2225
2243
  }
2226
2244
  let buffer = TUI.FRAME_BEGIN;
2227
- const currentScreenRow = Math.max(0, Math.min(height - 1, this.hardwareCursorRow - this.previousViewportTop));
2228
- if (currentScreenRow > 0) {
2229
- buffer += `\x1b[${currentScreenRow}A`;
2245
+ if (home === "absolute") {
2246
+ buffer += "\x1b[H";
2247
+ }
2248
+ else {
2249
+ const currentScreenRow = Math.max(0, Math.min(height - 1, this.hardwareCursorRow - this.previousViewportTop));
2250
+ if (currentScreenRow > 0) {
2251
+ buffer += `\x1b[${currentScreenRow}A`;
2252
+ }
2230
2253
  }
2231
2254
  for (let row = 0; row < height; row++) {
2232
2255
  if (row > 0)
@@ -2455,11 +2478,37 @@ export class TuiBase extends Container {
2455
2478
  }
2456
2479
  // Width changes always need a full re-render because wrapping changes.
2457
2480
  if (widthChanged) {
2481
+ // A width change (or a forced render) repaints everything below; a focus repaint queued in the
2482
+ // same tick is satisfied by it.
2483
+ this.#muxViewportRepaintPending = false;
2458
2484
  logRedraw(`terminal width changed (${this.previousWidth} -> ${width})`);
2459
- // In multiplexers, re-emit the viewport without 3J so pane history survives; an older copy may remain above.
2485
+ // In a multiplexer only the re-wrapped viewport is repainted: re-emitting every line of the buffer scrolled a
2486
+ // copy of the whole transcript into the pane's history on each width change (senpi#1704). Rows already in
2487
+ // that history keep their old wrapping. The pane re-wrapped the screen itself, so the repaint homes there.
2488
+ if (preserveMuxScrollback && this.previousWidth > 0) {
2489
+ if (this.renderMuxViewportRepaint(newLines, rawLines, cursorPos, width, height, undefined, "absolute"))
2490
+ return;
2491
+ }
2460
2492
  fullRender(true, !preserveMuxScrollback);
2461
2493
  return;
2462
2494
  }
2495
+ if (this.#muxViewportRepaintPending) {
2496
+ this.#muxViewportRepaintPending = false;
2497
+ if (preserveMuxScrollback && !heightChanged) {
2498
+ logRedraw("multiplexer pane focus regained");
2499
+ // A frame queued before the focus event may have grown the content: follow it exactly as an
2500
+ // ordinary frame does, so rows added below (the editor, the status line) stay on screen.
2501
+ // After a shrink the viewport keeps its top (as the deleted-lines path does), so the repaint does
2502
+ // not scroll rows already in the pane's history into view a second time.
2503
+ const focusViewportTop = newLines.length > prevViewportTop
2504
+ ? Math.max(prevViewportTop, newLines.length - height)
2505
+ : Math.max(0, newLines.length - height);
2506
+ if (!this.renderMuxViewportRepaint(newLines, rawLines, cursorPos, width, height, focusViewportTop)) {
2507
+ fullRender(true, false);
2508
+ }
2509
+ return;
2510
+ }
2511
+ }
2463
2512
  // Height changes normally need a full re-render to keep the visible viewport aligned,
2464
2513
  // but Termux changes height when the software keyboard shows or hides.
2465
2514
  // In that environment, a full redraw causes the entire history to replay on every toggle.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-tui",
3
- "version": "2026.10.10-5",
3
+ "version": "2026.10.10-8",
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",