@oh-my-pi/pi-tui 15.5.11 → 15.5.12
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/CHANGELOG.md +6 -0
- package/dist/types/tui.d.ts +6 -0
- package/package.json +3 -3
- package/src/tui.ts +53 -27
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.5.12] - 2026-05-29
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed terminal resizes corrupting native scrollback with duplicated rows. The 15.4.0 change that defers a destructive scrollback clear+replay (so a user scrolled into history is not yanked while a streaming tail cell mutates) also caught genuine width/height resizes: a resize reflows the terminal's own committed scrollback at the new geometry, but repainting only the viewport left the stale old-size rows in history, so every overflowed row showed up twice (old-size wrap + new-size copy) when scrolling back, until the next prompt submit cleaned it up. `#planRender` now rebuilds history synchronously when the frame's geometry actually changed (`widthChanged || heightChanged`) via the restored `historyRebuild` intent, and defers the rebuild only for pure content mutations where the user may be reading scrollback mid-stream.
|
|
10
|
+
|
|
5
11
|
## [15.5.0] - 2026-05-26
|
|
6
12
|
|
|
7
13
|
### Fixed
|
package/dist/types/tui.d.ts
CHANGED
|
@@ -162,5 +162,11 @@ export declare class TUI extends Container {
|
|
|
162
162
|
addInputListener(listener: InputListener): () => void;
|
|
163
163
|
removeInputListener(listener: InputListener): void;
|
|
164
164
|
stop(): void;
|
|
165
|
+
/**
|
|
166
|
+
* Rebuild native terminal scrollback if live rendering deferred a history rewrite.
|
|
167
|
+
* Callers should only invoke this at checkpoints where the user is expected to be
|
|
168
|
+
* at the terminal bottom, such as after submitting a new prompt.
|
|
169
|
+
*/
|
|
170
|
+
refreshNativeScrollbackIfDirty(): boolean;
|
|
165
171
|
requestRender(force?: boolean, options?: RenderRequestOptions): void;
|
|
166
172
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "15.5.
|
|
4
|
+
"version": "15.5.12",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-natives": "15.5.
|
|
41
|
-
"@oh-my-pi/pi-utils": "15.5.
|
|
40
|
+
"@oh-my-pi/pi-natives": "15.5.12",
|
|
41
|
+
"@oh-my-pi/pi-utils": "15.5.12",
|
|
42
42
|
"lru-cache": "11.3.6",
|
|
43
43
|
"marked": "^18.0.3"
|
|
44
44
|
},
|
package/src/tui.ts
CHANGED
|
@@ -236,8 +236,9 @@ export class Container implements Component {
|
|
|
236
236
|
* - `initial`: first paint after `start()` — clear viewport, emit transcript.
|
|
237
237
|
* - `sessionReplace`: caller asked for `{ clearScrollback: true }` on a forced
|
|
238
238
|
* render — clear viewport, clear scrollback (outside multiplexers).
|
|
239
|
-
* - `historyRebuild`:
|
|
240
|
-
* and scrollback so
|
|
239
|
+
* - `historyRebuild`: a geometry change (terminal resize) left native history
|
|
240
|
+
* wrapped at the old size — clear viewport and scrollback so it rewraps at the
|
|
241
|
+
* new geometry. Also flushes deferred content-only rewrites.
|
|
241
242
|
* - `viewportRepaint`: rewrite the visible viewport in place. If `appendFrom`
|
|
242
243
|
* is set, emit those tail rows as scrollback growth first so streaming
|
|
243
244
|
* output reaches terminal history before the corrected viewport is drawn.
|
|
@@ -290,6 +291,7 @@ export class TUI extends Container {
|
|
|
290
291
|
// Set after a clear+full replay so the next insert-above-suffix frame does
|
|
291
292
|
// not scroll replayed live chrome (status/editor) into fresh history.
|
|
292
293
|
#suppressNextSuffixScroll = false;
|
|
294
|
+
#nativeScrollbackDirty = false;
|
|
293
295
|
#fullRedrawCount = 0;
|
|
294
296
|
#clearScrollbackOnNextRender = false;
|
|
295
297
|
#hasEverRendered = false;
|
|
@@ -627,20 +629,23 @@ export class TUI extends Container {
|
|
|
627
629
|
this.terminal.stop();
|
|
628
630
|
}
|
|
629
631
|
|
|
632
|
+
/**
|
|
633
|
+
* Rebuild native terminal scrollback if live rendering deferred a history rewrite.
|
|
634
|
+
* Callers should only invoke this at checkpoints where the user is expected to be
|
|
635
|
+
* at the terminal bottom, such as after submitting a new prompt.
|
|
636
|
+
*/
|
|
637
|
+
refreshNativeScrollbackIfDirty(): boolean {
|
|
638
|
+
if (!this.#nativeScrollbackDirty || this.#stopped) return false;
|
|
639
|
+
this.#prepareForcedRender(true);
|
|
640
|
+
this.#renderRequested = false;
|
|
641
|
+
this.#lastRenderAt = performance.now();
|
|
642
|
+
this.#doRender();
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
|
|
630
646
|
requestRender(force = false, options?: RenderRequestOptions): void {
|
|
631
647
|
if (force) {
|
|
632
|
-
this.#
|
|
633
|
-
this.#previousLines = [];
|
|
634
|
-
this.#previousWidth = -1; // -1 triggers widthChanged, forcing a full clear
|
|
635
|
-
this.#previousHeight = -1; // -1 triggers heightChanged, forcing a full clear
|
|
636
|
-
this.#cursorRow = 0;
|
|
637
|
-
this.#hardwareCursorRow = 0;
|
|
638
|
-
this.#viewportTopRow = 0;
|
|
639
|
-
this.#maxLinesRendered = 0;
|
|
640
|
-
if (this.#renderTimer) {
|
|
641
|
-
clearTimeout(this.#renderTimer);
|
|
642
|
-
this.#renderTimer = undefined;
|
|
643
|
-
}
|
|
648
|
+
this.#prepareForcedRender(options?.clearScrollback === true);
|
|
644
649
|
this.#renderRequested = true;
|
|
645
650
|
process.nextTick(() => {
|
|
646
651
|
if (this.#stopped || !this.#renderRequested) {
|
|
@@ -657,6 +662,21 @@ export class TUI extends Container {
|
|
|
657
662
|
process.nextTick(() => this.#scheduleRender());
|
|
658
663
|
}
|
|
659
664
|
|
|
665
|
+
#prepareForcedRender(clearScrollback: boolean): void {
|
|
666
|
+
this.#clearScrollbackOnNextRender ||= clearScrollback;
|
|
667
|
+
this.#previousLines = [];
|
|
668
|
+
this.#previousWidth = -1; // -1 triggers widthChanged, forcing a full clear
|
|
669
|
+
this.#previousHeight = -1; // -1 triggers heightChanged, forcing a full clear
|
|
670
|
+
this.#cursorRow = 0;
|
|
671
|
+
this.#hardwareCursorRow = 0;
|
|
672
|
+
this.#viewportTopRow = 0;
|
|
673
|
+
this.#maxLinesRendered = 0;
|
|
674
|
+
if (this.#renderTimer) {
|
|
675
|
+
clearTimeout(this.#renderTimer);
|
|
676
|
+
this.#renderTimer = undefined;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
660
680
|
#scheduleRender(): void {
|
|
661
681
|
if (this.#stopped || this.#renderTimer || !this.#renderRequested) {
|
|
662
682
|
return;
|
|
@@ -1111,12 +1131,14 @@ export class TUI extends Container {
|
|
|
1111
1131
|
return;
|
|
1112
1132
|
case "sessionReplace":
|
|
1113
1133
|
this.#clearScrollbackOnNextRender = false;
|
|
1134
|
+
this.#nativeScrollbackDirty = false;
|
|
1114
1135
|
this.#emitFullPaint(lines, width, height, cursorPos, {
|
|
1115
1136
|
clearViewport: true,
|
|
1116
1137
|
clearScrollback: !isMultiplexerSession(),
|
|
1117
1138
|
});
|
|
1118
1139
|
return;
|
|
1119
1140
|
case "historyRebuild":
|
|
1141
|
+
this.#nativeScrollbackDirty = false;
|
|
1120
1142
|
this.#emitFullPaint(lines, width, height, cursorPos, {
|
|
1121
1143
|
clearViewport: true,
|
|
1122
1144
|
clearScrollback: !isMultiplexerSession(),
|
|
@@ -1149,9 +1171,11 @@ export class TUI extends Container {
|
|
|
1149
1171
|
|
|
1150
1172
|
/**
|
|
1151
1173
|
* Map the current frame onto a single render intent. Order matters: forced
|
|
1152
|
-
* resets and session replacement short-circuit before any diff work
|
|
1153
|
-
*
|
|
1154
|
-
*
|
|
1174
|
+
* resets and session replacement short-circuit before any diff work. A real
|
|
1175
|
+
* resize (geometry change) that invalidates native scrollback rebuilds it now;
|
|
1176
|
+
* a pure content mutation that does the same marks scrollback dirty and
|
|
1177
|
+
* repaints only the viewport, deferring the destructive clear+replay to an
|
|
1178
|
+
* explicit checkpoint so users scrolled into history are not yanked.
|
|
1155
1179
|
*/
|
|
1156
1180
|
#planRender(
|
|
1157
1181
|
newLines: string[],
|
|
@@ -1175,11 +1199,11 @@ export class TUI extends Container {
|
|
|
1175
1199
|
|
|
1176
1200
|
const diff = this.#diffLines(newLines);
|
|
1177
1201
|
|
|
1178
|
-
// Shrink
|
|
1179
|
-
//
|
|
1180
|
-
//
|
|
1181
|
-
//
|
|
1182
|
-
//
|
|
1202
|
+
// Shrink across the viewport boundary: the new transcript would re-expose
|
|
1203
|
+
// rows already committed to native scrollback. A real resize already
|
|
1204
|
+
// reflowed history, so rebuild it now; a pure content shrink (e.g. a
|
|
1205
|
+
// streaming tail cell collapsing) defers the clear+replay so a user
|
|
1206
|
+
// scrolled into history is not yanked to the bottom mid-stream.
|
|
1183
1207
|
const naturalViewportTop = Math.max(0, newLines.length - height);
|
|
1184
1208
|
if (
|
|
1185
1209
|
diff.firstChanged !== -1 &&
|
|
@@ -1187,7 +1211,9 @@ export class TUI extends Container {
|
|
|
1187
1211
|
naturalViewportTop < this.#scrollbackHighWater &&
|
|
1188
1212
|
!isMultiplexerSession()
|
|
1189
1213
|
) {
|
|
1190
|
-
return { kind: "historyRebuild" };
|
|
1214
|
+
if (widthChanged || heightChanged) return { kind: "historyRebuild" };
|
|
1215
|
+
this.#nativeScrollbackDirty = true;
|
|
1216
|
+
return { kind: "viewportRepaint" };
|
|
1191
1217
|
}
|
|
1192
1218
|
|
|
1193
1219
|
const suppressSuffixScroll = this.#suppressNextSuffixScroll;
|
|
@@ -1210,10 +1236,10 @@ export class TUI extends Container {
|
|
|
1210
1236
|
return { kind: "noop" };
|
|
1211
1237
|
}
|
|
1212
1238
|
|
|
1213
|
-
// Width changes
|
|
1214
|
-
//
|
|
1215
|
-
//
|
|
1216
|
-
// append handler scrolls them into
|
|
1239
|
+
// Width changes rewrap the whole transcript. An offscreen edit leaves
|
|
1240
|
+
// native history at the old width, so rebuild it now — the terminal already
|
|
1241
|
+
// reflowed and the user is at the terminal to resize. Pure appends fall
|
|
1242
|
+
// through to the diff path so the append handler scrolls them into history.
|
|
1217
1243
|
if (widthChanged) {
|
|
1218
1244
|
if (diff.firstChanged < prevViewportTop) return { kind: "historyRebuild" };
|
|
1219
1245
|
const pureAppend = diff.appendedLines && diff.firstChanged === this.#previousLines.length;
|