@oh-my-pi/pi-tui 14.1.2 → 14.2.0

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 +4 -0
  2. package/package.json +3 -3
  3. package/src/tui.ts +29 -13
package/CHANGELOG.md CHANGED
@@ -12,6 +12,10 @@
12
12
 
13
13
  - Changed truncation debug logging to run only when `debugRedraw` is enabled
14
14
 
15
+ ### Fixed
16
+
17
+ - Fixed viewport jumping during streaming and session swap by tracking actual content height instead of high-water mark
18
+
15
19
  ## [14.0.5] - 2026-04-11
16
20
 
17
21
  ### Changed
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "14.1.2",
4
+ "version": "14.2.0",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
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": "14.1.2",
41
- "@oh-my-pi/pi-utils": "14.1.2",
40
+ "@oh-my-pi/pi-natives": "14.2.0",
41
+ "@oh-my-pi/pi-utils": "14.2.0",
42
42
  "marked": "^17.0"
43
43
  },
44
44
  "devDependencies": {
package/src/tui.ts CHANGED
@@ -230,7 +230,7 @@ export class TUI extends Container {
230
230
  #sixelProbeUnsubscribe?: () => void;
231
231
  #showHardwareCursor = $flag("PI_HARDWARE_CURSOR");
232
232
  #clearOnShrink = $flag("PI_CLEAR_ON_SHRINK"); // Clear empty rows when content shrinks (default: off)
233
- #maxLinesRendered = 0; // High-water line count used for clear-on-shrink policy
233
+ #maxLinesRendered = 0; // Line count from last render, used for viewport calculation
234
234
  #fullRedrawCount = 0;
235
235
  #stopped = false;
236
236
 
@@ -1066,11 +1066,11 @@ export class TUI extends Container {
1066
1066
  return;
1067
1067
  }
1068
1068
 
1069
- // Content shrunk below the working area and no overlays - re-render to clear empty rows
1069
+ // Content shrunk below the previous render and no overlays - re-render to clear empty rows
1070
1070
  // (overlays need the padding, so only do this when no overlays are active)
1071
1071
  // Configurable via setClearOnShrink() or PI_CLEAR_ON_SHRINK=0 env var
1072
- if (this.#clearOnShrink && newLines.length < this.#maxLinesRendered && this.overlayStack.length === 0) {
1073
- logRedraw(`clearOnShrink (maxLinesRendered=${this.#maxLinesRendered})`);
1072
+ if (this.#clearOnShrink && newLines.length < this.#previousLines.length && this.overlayStack.length === 0) {
1073
+ logRedraw(`clearOnShrink (prev=${this.#previousLines.length}, new=${newLines.length})`);
1074
1074
  fullRender(true);
1075
1075
  return;
1076
1076
  }
@@ -1144,18 +1144,34 @@ export class TUI extends Container {
1144
1144
  this.#previousLines = newLines;
1145
1145
  this.#previousWidth = width;
1146
1146
  this.#previousHeight = height;
1147
- this.#viewportTopRow = Math.max(0, this.#maxLinesRendered - height);
1147
+ this.#maxLinesRendered = newLines.length;
1148
+ this.#viewportTopRow = Math.max(0, newLines.length - height);
1148
1149
  return;
1149
1150
  }
1150
1151
 
1151
1152
  // Check if firstChanged is above what was previously visible
1152
- // Use previousLines.length (not maxLinesRendered) to avoid false positives after content shrinks
1153
1153
  const previousContentViewportTop = Math.max(0, this.#previousLines.length - height);
1154
1154
  if (firstChanged < previousContentViewportTop) {
1155
- // First change is above previous viewport - need full re-render
1156
- logRedraw(`firstChanged < viewportTop (${firstChanged} < ${previousContentViewportTop})`);
1157
- fullRender(true);
1158
- return;
1155
+ const newViewportTop = Math.max(0, newLines.length - height);
1156
+ if (newViewportTop < previousContentViewportTop) {
1157
+ // Viewport needs to shift up — can only be done with a full redraw
1158
+ logRedraw(`viewport shift up (new=${newViewportTop} < prev=${previousContentViewportTop})`);
1159
+ fullRender(true);
1160
+ return;
1161
+ }
1162
+ // Viewport is stable or shifting down — skip invisible above-viewport changes
1163
+ firstChanged = previousContentViewportTop;
1164
+ if (lastChanged < firstChanged) {
1165
+ // All changes are above the viewport — nothing visible to update
1166
+ this.#cursorRow = Math.max(0, newLines.length - 1);
1167
+ this.#maxLinesRendered = newLines.length;
1168
+ this.#viewportTopRow = Math.max(0, newLines.length - height);
1169
+ this.#positionHardwareCursor(cursorPos, newLines.length);
1170
+ this.#previousLines = newLines;
1171
+ this.#previousWidth = width;
1172
+ this.#previousHeight = height;
1173
+ return;
1174
+ }
1159
1175
  }
1160
1176
 
1161
1177
  // Render from first changed line to end
@@ -1272,9 +1288,9 @@ export class TUI extends Container {
1272
1288
  // hardwareCursorRow tracks actual terminal cursor position (for movement)
1273
1289
  this.#cursorRow = Math.max(0, newLines.length - 1);
1274
1290
  this.#hardwareCursorRow = finalCursorRow;
1275
- // Track terminal's working area (grows but doesn't shrink unless cleared)
1276
- this.#maxLinesRendered = Math.max(this.#maxLinesRendered, newLines.length);
1277
- this.#viewportTopRow = Math.max(0, this.#maxLinesRendered - height);
1291
+ // Track content height for viewport calculation
1292
+ this.#maxLinesRendered = newLines.length;
1293
+ this.#viewportTopRow = Math.max(0, newLines.length - height);
1278
1294
 
1279
1295
  // Position hardware cursor for IME
1280
1296
  this.#positionHardwareCursor(cursorPos, newLines.length);