@gajae-code/tui 0.18.9 → 0.18.11
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 +14 -0
- package/package.json +3 -3
- package/src/tui.ts +50 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.18.11] - 2026-10-12
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed the viewport going blank above the status line and input box when a tall live block (task output, streaming tool output) collapsed with `clearOnShrink` off: the TUI now replays the frame when padding to the committed frontier would leave no transcript row on screen.
|
|
10
|
+
|
|
11
|
+
## [0.18.10] - 2026-10-12
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- TUI no longer goes blank after stop/restart cycles: `#hardwareCursorRow` is now properly reset during `stop()` to prevent stale cursor position state from corrupting viewport calculations on the next `start()`.
|
|
16
|
+
|
|
17
|
+
- With `clearOnShrink` enabled, a live block that shrinks after the transcript has scrolled into history no longer leaves blank spacer rows above the pinned composer until the next resize. The contraction now clears the screen and history and replays the frame, so the viewport is filled again and no row appears twice in history.
|
|
18
|
+
|
|
5
19
|
## [0.18.9] - 2026-10-11
|
|
6
20
|
|
|
7
21
|
### Added
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/tui",
|
|
4
|
-
"version": "0.18.
|
|
4
|
+
"version": "0.18.11",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://gajae-code.com",
|
|
7
7
|
"author": "Yeachan-Heo and Gajae Code Contributors",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"fmt": "biome format --write ."
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@gajae-code/natives": "0.18.
|
|
40
|
-
"@gajae-code/utils": "0.18.
|
|
39
|
+
"@gajae-code/natives": "0.18.11",
|
|
40
|
+
"@gajae-code/utils": "0.18.11",
|
|
41
41
|
"lru-cache": "11.3.6",
|
|
42
42
|
"marked": "18.0.6"
|
|
43
43
|
},
|
package/src/tui.ts
CHANGED
|
@@ -1152,6 +1152,7 @@ export class TUI extends Container {
|
|
|
1152
1152
|
#fullRedrawCount = 0;
|
|
1153
1153
|
#stopped = false;
|
|
1154
1154
|
#terminalUnavailable = false;
|
|
1155
|
+
#restartNeedsClear = false; // Set when stop() resets viewport state
|
|
1155
1156
|
#bottomPinnedComponent: Component | null = null;
|
|
1156
1157
|
#pendingTerminalCleanup: Array<{ payload: string; onDelivered?: () => void }> = [];
|
|
1157
1158
|
#mouseSelectionStart: MouseSelectionPoint | null = null;
|
|
@@ -1432,6 +1433,11 @@ export class TUI extends Container {
|
|
|
1432
1433
|
this.#clearOnShrink = enabled;
|
|
1433
1434
|
}
|
|
1434
1435
|
|
|
1436
|
+
/** Overlays rely on padded rows, so contraction replays only without them. */
|
|
1437
|
+
#clearOnShrinkReplays(): boolean {
|
|
1438
|
+
return this.#clearOnShrink && this.overlayStack.length === 0;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1435
1441
|
setFocus(component: Component | null): void {
|
|
1436
1442
|
// Clear focused flag on old component
|
|
1437
1443
|
if (isFocusable(this.#focusedComponent)) {
|
|
@@ -2981,6 +2987,12 @@ export class TUI extends Container {
|
|
|
2981
2987
|
this.#lineEmitWidthCache.clear();
|
|
2982
2988
|
this.#previousWidth = 0;
|
|
2983
2989
|
this.#previousHeight = 0;
|
|
2990
|
+
this.#cursorRow = 0;
|
|
2991
|
+
this.#hardwareCursorRow = 0;
|
|
2992
|
+
this.#viewportTopRow = 0;
|
|
2993
|
+
this.#maxLinesRendered = 0;
|
|
2994
|
+
// Mark that the next initial render needs to clear the screen after terminal reset
|
|
2995
|
+
this.#restartNeedsClear = true;
|
|
2984
2996
|
this.#forcedRenderPreviousWidth = 0;
|
|
2985
2997
|
this.#forcedRenderPreviousHeight = 0;
|
|
2986
2998
|
this.#transcriptRebuildPending = false;
|
|
@@ -5228,6 +5240,7 @@ export class TUI extends Container {
|
|
|
5228
5240
|
let usedWindowNormalize = false;
|
|
5229
5241
|
let stitched = false;
|
|
5230
5242
|
let frontierSpacerLineCount = 0;
|
|
5243
|
+
let strandedContractionReplay = false;
|
|
5231
5244
|
if (reusedAnchor !== null && layoutPrefixEligible && !layoutPrefixBlocked) {
|
|
5232
5245
|
const reused = this.#reuseCachedLayoutPrefix(
|
|
5233
5246
|
width,
|
|
@@ -5268,12 +5281,32 @@ export class TUI extends Container {
|
|
|
5268
5281
|
if (hasStickySuffix && height > 0 && this.#manualViewportTop === undefined) {
|
|
5269
5282
|
// Rebuilds start at their new transcript frontier. Forced redraws use the
|
|
5270
5283
|
// captured rendered dimensions rather than their -1 invalidation sentinels.
|
|
5284
|
+
// clearOnShrink replays the whole frame on contraction instead; spacer rows
|
|
5285
|
+
// would otherwise stay on screen as a blank gap above the pinned component.
|
|
5271
5286
|
const preserveLiveFrontier =
|
|
5287
|
+
!this.#clearOnShrinkReplays() &&
|
|
5272
5288
|
!transcriptIdentityReplaced &&
|
|
5273
5289
|
!transcriptRebuildPending &&
|
|
5274
5290
|
frontierPreviousWidth === width &&
|
|
5275
5291
|
(frontierPreviousHeight === height || this.#viewportRepaintHost());
|
|
5276
|
-
|
|
5292
|
+
// Padding to the committed frontier keeps scrollback rows from being
|
|
5293
|
+
// repainted, but when the contraction ends the transcript at or above that
|
|
5294
|
+
// frontier the padded viewport holds nothing but spacer rows. Replay the
|
|
5295
|
+
// frame instead of leaving a blank screen above the pinned component. A
|
|
5296
|
+
// pinned suffix that already fills the viewport hides the spacers, so it
|
|
5297
|
+
// keeps the frontier and its scrollback.
|
|
5298
|
+
strandedContractionReplay =
|
|
5299
|
+
preserveLiveFrontier &&
|
|
5300
|
+
!heightChanged &&
|
|
5301
|
+
frontierPreviousHeight === height &&
|
|
5302
|
+
this.overlayStack.length === 0 &&
|
|
5303
|
+
prevViewportTop > 0 &&
|
|
5304
|
+
sourceTranscriptLineCount > 0 &&
|
|
5305
|
+
sourceTranscriptLineCount <= prevViewportTop &&
|
|
5306
|
+
newLines.length < prevViewportTop + height &&
|
|
5307
|
+
newLines.length - sourceTranscriptLineCount < height;
|
|
5308
|
+
const minimumLineCount =
|
|
5309
|
+
preserveLiveFrontier && !strandedContractionReplay ? prevViewportTop + height : height;
|
|
5277
5310
|
const padded = this.#padBeforeBottomPinnedComponent(
|
|
5278
5311
|
newLines,
|
|
5279
5312
|
minimumLineCount,
|
|
@@ -5788,9 +5821,12 @@ export class TUI extends Container {
|
|
|
5788
5821
|
return;
|
|
5789
5822
|
}
|
|
5790
5823
|
// First render - just output everything without clearing (assumes clean screen)
|
|
5824
|
+
// unless we're restarting after a stop() which leaves stale content
|
|
5791
5825
|
if (initialRender) {
|
|
5792
5826
|
logRedraw("first render");
|
|
5793
|
-
|
|
5827
|
+
const shouldClear = this.#restartNeedsClear;
|
|
5828
|
+
this.#restartNeedsClear = false;
|
|
5829
|
+
fullRender(shouldClear, "first render");
|
|
5794
5830
|
return;
|
|
5795
5831
|
}
|
|
5796
5832
|
|
|
@@ -5848,13 +5884,19 @@ export class TUI extends Container {
|
|
|
5848
5884
|
// Content shrunk below the previous render and no overlays - re-render to clear empty rows
|
|
5849
5885
|
// (overlays need the padding, so only do this when no overlays are active)
|
|
5850
5886
|
// Configurable via setClearOnShrink() or GJC_CLEAR_ON_SHRINK=0 env var
|
|
5851
|
-
|
|
5887
|
+
// Erase native scrollback too: scrolled-off rows cannot be pulled back into the
|
|
5888
|
+
// viewport, and repainting them on screen would duplicate them in history.
|
|
5889
|
+
if (this.#clearOnShrinkReplays() && newLines.length < this.#previousLines.length) {
|
|
5852
5890
|
logRedraw(`clearOnShrink (prev=${this.#previousLines.length}, new=${newLines.length})`);
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5891
|
+
fullRender(true, "clearOnShrink", true);
|
|
5892
|
+
return;
|
|
5893
|
+
}
|
|
5894
|
+
// Default (clearOnShrink off) still replays when frontier padding would leave the
|
|
5895
|
+
// viewport with no transcript row, e.g. a tall live block collapsing back into
|
|
5896
|
+
// history. Same scrollback erase as above, for the same duplication reason.
|
|
5897
|
+
if (strandedContractionReplay) {
|
|
5898
|
+
logRedraw(`stranded contraction (prev=${this.#previousLines.length}, new=${newLines.length})`);
|
|
5899
|
+
fullRender(true, "stranded contraction", true);
|
|
5858
5900
|
return;
|
|
5859
5901
|
}
|
|
5860
5902
|
|