@oh-my-pi/pi-tui 15.6.0 → 15.7.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.7.0] - 2026-05-31
6
+
7
+ ### Fixed
8
+
9
+ - Fixed slash-command autocomplete repainting when a Windows Terminal session cannot report native scrollback position; live input renders can now bypass the unknown-viewport deferral without weakening background scrollback protection. ([#1550](https://github.com/can1357/oh-my-pi/issues/1550))
10
+
5
11
  ## [15.6.0] - 2026-05-30
6
12
  ### Added
7
13
 
@@ -44,6 +44,18 @@ export interface Focusable {
44
44
  export interface RenderRequestOptions {
45
45
  /** Clear terminal scrollback for intentional transcript replacement. */
46
46
  clearScrollback?: boolean;
47
+ /**
48
+ * Bypass the unknown-Windows-viewport deferral for this render so the
49
+ * caller's intentional live UI mutation reaches the terminal even when
50
+ * `Terminal#isNativeViewportAtBottom()` cannot answer.
51
+ *
52
+ * Use only for renders driven by direct user interaction (autocomplete
53
+ * updates, IME, etc.). Any background/offscreen transcript change that
54
+ * coalesces into the same frame WILL also bypass the deferral and reach
55
+ * native scrollback — that is the trade-off, and the reason ordinary
56
+ * `requestRender()` calls must continue to omit this flag.
57
+ */
58
+ allowUnknownViewportMutation?: boolean;
47
59
  }
48
60
  /** Options for deferred native scrollback rebuild checkpoints. */
49
61
  export interface NativeScrollbackRefreshOptions {
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.6.0",
4
+ "version": "15.7.0",
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.6.0",
41
- "@oh-my-pi/pi-utils": "15.6.0",
40
+ "@oh-my-pi/pi-natives": "15.7.0",
41
+ "@oh-my-pi/pi-utils": "15.7.0",
42
42
  "lru-cache": "11.5.1",
43
43
  "marked": "^18.0.4"
44
44
  },
package/src/tui.ts CHANGED
@@ -84,6 +84,18 @@ export interface Focusable {
84
84
  export interface RenderRequestOptions {
85
85
  /** Clear terminal scrollback for intentional transcript replacement. */
86
86
  clearScrollback?: boolean;
87
+ /**
88
+ * Bypass the unknown-Windows-viewport deferral for this render so the
89
+ * caller's intentional live UI mutation reaches the terminal even when
90
+ * `Terminal#isNativeViewportAtBottom()` cannot answer.
91
+ *
92
+ * Use only for renders driven by direct user interaction (autocomplete
93
+ * updates, IME, etc.). Any background/offscreen transcript change that
94
+ * coalesces into the same frame WILL also bypass the deferral and reach
95
+ * native scrollback — that is the trade-off, and the reason ordinary
96
+ * `requestRender()` calls must continue to omit this flag.
97
+ */
98
+ allowUnknownViewportMutation?: boolean;
87
99
  }
88
100
 
89
101
  /** Options for deferred native scrollback rebuild checkpoints. */
@@ -316,6 +328,7 @@ export class TUI extends Container {
316
328
  #nativeScrollbackDirty = false;
317
329
  #fullRedrawCount = 0;
318
330
  #clearScrollbackOnNextRender = false;
331
+ #allowUnknownViewportMutationOnNextRender = false;
319
332
  #hasEverRendered = false;
320
333
  #stopped = false;
321
334
 
@@ -670,6 +683,7 @@ export class TUI extends Container {
670
683
  }
671
684
 
672
685
  requestRender(force = false, options?: RenderRequestOptions): void {
686
+ this.#allowUnknownViewportMutationOnNextRender ||= options?.allowUnknownViewportMutation === true;
673
687
  if (force) {
674
688
  this.#prepareForcedRender(options?.clearScrollback === true);
675
689
  this.#renderRequested = true;
@@ -1138,11 +1152,19 @@ export class TUI extends Container {
1138
1152
  const prevHardwareCursorRow = this.#hardwareCursorRow;
1139
1153
  const widthChanged = this.#previousWidth > 0 && this.#previousWidth !== width;
1140
1154
  const heightChanged = this.#previousHeight > 0 && this.#previousHeight !== height;
1155
+ const allowUnknownViewportMutation = this.#allowUnknownViewportMutationOnNextRender;
1156
+ this.#allowUnknownViewportMutationOnNextRender = false;
1141
1157
 
1142
1158
  // 3. Classify intent.
1143
- const intent = this.#planRender(lines, widthChanged, heightChanged, prevViewportTop, height);
1159
+ const intent = this.#planRender(
1160
+ lines,
1161
+ widthChanged,
1162
+ heightChanged,
1163
+ prevViewportTop,
1164
+ height,
1165
+ allowUnknownViewportMutation,
1166
+ );
1144
1167
  this.#logRedraw(intent, lines.length, height);
1145
-
1146
1168
  // 4. Execute.
1147
1169
  switch (intent.kind) {
1148
1170
  case "noop":
@@ -1219,6 +1241,7 @@ export class TUI extends Container {
1219
1241
  heightChanged: boolean,
1220
1242
  prevViewportTop: number,
1221
1243
  height: number,
1244
+ allowUnknownViewportMutation: boolean,
1222
1245
  ): RenderIntent {
1223
1246
  // Initial paint after start(): scrollback must keep its prior shell
1224
1247
  // content, but the viewport must be cleared so stale rows do not bleed
@@ -1253,14 +1276,14 @@ export class TUI extends Container {
1253
1276
  !isMultiplexerSession()
1254
1277
  ) {
1255
1278
  if (widthChanged || heightChanged) {
1256
- if (this.#nativeViewportIsScrolled(this.#readNativeViewportAtBottom())) {
1279
+ if (this.#nativeViewportIsScrolled(this.#readNativeViewportAtBottom(), allowUnknownViewportMutation)) {
1257
1280
  this.#markNativeScrollbackDirty();
1258
1281
  return { kind: "deferredShrink", paddedLength: this.#previousLines.length };
1259
1282
  }
1260
1283
  return { kind: "historyRebuild" };
1261
1284
  }
1262
1285
  this.#markNativeScrollbackDirty();
1263
- if (this.#nativeViewportIsScrolled(this.#readNativeViewportAtBottom())) {
1286
+ if (this.#nativeViewportIsScrolled(this.#readNativeViewportAtBottom(), allowUnknownViewportMutation)) {
1264
1287
  return { kind: "deferredShrink", paddedLength: this.#previousLines.length };
1265
1288
  }
1266
1289
  return { kind: "viewportRepaint" };
@@ -1292,7 +1315,7 @@ export class TUI extends Container {
1292
1315
  // through to the diff path so the append handler scrolls them into history.
1293
1316
  if (widthChanged) {
1294
1317
  if (diff.firstChanged < prevViewportTop) {
1295
- if (this.#nativeViewportIsScrolled(this.#readNativeViewportAtBottom())) {
1318
+ if (this.#nativeViewportIsScrolled(this.#readNativeViewportAtBottom(), allowUnknownViewportMutation)) {
1296
1319
  this.#markNativeScrollbackDirty();
1297
1320
  return { kind: "viewportRepaint" };
1298
1321
  }
@@ -1307,7 +1330,7 @@ export class TUI extends Container {
1307
1330
  const structuralMutation = newLines.length !== this.#previousLines.length || diff.firstChanged < prevViewportTop;
1308
1331
  if (!pureAppend && structuralMutation && !isMultiplexerSession()) {
1309
1332
  const nativeViewportAtBottom = this.#readNativeViewportAtBottom();
1310
- if (this.#nativeViewportIsScrolled(nativeViewportAtBottom)) {
1333
+ if (this.#nativeViewportIsScrolled(nativeViewportAtBottom, allowUnknownViewportMutation)) {
1311
1334
  this.#markNativeScrollbackDirty();
1312
1335
  return { kind: "deferredMutation" };
1313
1336
  }
@@ -1430,8 +1453,14 @@ export class TUI extends Container {
1430
1453
  return this.terminal.isNativeViewportAtBottom?.();
1431
1454
  }
1432
1455
 
1433
- #nativeViewportIsScrolled(nativeViewportAtBottom: boolean | undefined): boolean {
1434
- return nativeViewportAtBottom === false || (nativeViewportAtBottom === undefined && process.platform === "win32");
1456
+ #nativeViewportIsScrolled(
1457
+ nativeViewportAtBottom: boolean | undefined,
1458
+ allowUnknownViewportMutation = false,
1459
+ ): boolean {
1460
+ return (
1461
+ nativeViewportAtBottom === false ||
1462
+ (nativeViewportAtBottom === undefined && process.platform === "win32" && !allowUnknownViewportMutation)
1463
+ );
1435
1464
  }
1436
1465
 
1437
1466
  #nativeViewportIsAtBottom(nativeViewportAtBottom: boolean | undefined): boolean {