@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.
- package/dist/tui.js +57 -8
- 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
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
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
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
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
|
|
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