@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.
Files changed (3) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +3 -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.9",
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.9",
40
- "@gajae-code/utils": "0.18.9",
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
- const minimumLineCount = preserveLiveFrontier ? prevViewportTop + height : height;
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
- fullRender(false, "first render");
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
- if (this.#clearOnShrink && newLines.length < this.#previousLines.length && this.overlayStack.length === 0) {
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
- if (useViewportRepaintPath) {
5854
- viewportRepaint(`clearOnShrink (prev=${this.#previousLines.length}, new=${newLines.length})`);
5855
- } else {
5856
- fullRender(true, "clearOnShrink");
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