@oh-my-pi/pi-tui 15.7.3 → 15.7.5

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.5] - 2026-06-01
6
+
7
+ ### Fixed
8
+
9
+ - Fixed native Windows + Windows Terminal scrollback being yanked to the top when a streaming response triggered a TUI full redraw. Under ConPTY the `kernel32` `GetConsoleScreenBufferInfo` probe answers about the pseudo-console (always at the buffer tail) and not about WT's host scrollback, so `isNativeViewportAtBottom()` falsely returned `true` while the user was scrolled up and the shrink-across-viewport branch issued a destructive `historyRebuild` (`\x1b[2J\x1b[H\x1b[3J`). The probe now short-circuits to `undefined` whenever `WT_SESSION` is set, letting the existing deferred-rebuild path keep streaming-time mutations non-destructive and reconcile native history at the next prompt-submit checkpoint. ([#1635](https://github.com/can1357/oh-my-pi/issues/1635))
10
+
5
11
  ## [15.7.3] - 2026-05-31
6
12
 
7
13
  ### Added
@@ -41,6 +41,21 @@ export interface Terminal {
41
41
  /** The last detected terminal appearance, or undefined if not yet known. */
42
42
  get appearance(): TerminalAppearance | undefined;
43
43
  }
44
+ /**
45
+ * Whether the native console viewport-position probe should be consulted.
46
+ *
47
+ * Returns `true` only on native Windows that is *not* fronted by Windows
48
+ * Terminal. The kernel32 `GetConsoleScreenBufferInfo` API answers about the
49
+ * ConPTY pseudo-console — which is always pinned to its tail — and not about
50
+ * the user-visible scrollback in modern hosts. Treat any such host as
51
+ * unreportable so the renderer falls back to the deferred-rebuild path.
52
+ *
53
+ * Pure helper for unit testing; the runtime call site reads `$env` /
54
+ * `process.platform`. See #1635.
55
+ */
56
+ export declare function shouldTrustNativeViewportProbe(env?: {
57
+ WT_SESSION?: string | undefined;
58
+ }, platform?: NodeJS.Platform): boolean;
44
59
  /**
45
60
  * Real terminal using process.stdin/stdout
46
61
  */
@@ -53,6 +68,15 @@ export declare class ProcessTerminal implements Terminal {
53
68
  /**
54
69
  * Returns true when Windows' active console viewport is at the scrollback tail.
55
70
  * POSIX terminals do not expose native scrollback position through a standard API.
71
+ *
72
+ * On native Windows running under Windows Terminal (the default modern
73
+ * host), the `kernel32` probe answers about the ConPTY pseudo-console — not
74
+ * the user-visible WT viewport — so it would always read "at bottom" while
75
+ * the user is scrolled up. Return `undefined` there so the renderer falls
76
+ * back to the POSIX-style deferred-rebuild path: streaming mutations stay
77
+ * non-destructive (no `\x1b[3J`), and the rebuild fires at the next prompt
78
+ * checkpoint via {@link TUI.refreshNativeScrollbackIfDirty} where the user
79
+ * is already pinned to the bottom by the editor keystroke. See #1635.
56
80
  */
57
81
  isNativeViewportAtBottom(): boolean | undefined;
58
82
  drainInput(maxMs?: number, idleMs?: number): Promise<void>;
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.7.3",
4
+ "version": "15.7.5",
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.7.3",
41
- "@oh-my-pi/pi-utils": "15.7.3",
40
+ "@oh-my-pi/pi-natives": "15.7.5",
41
+ "@oh-my-pi/pi-utils": "15.7.5",
42
42
  "lru-cache": "11.5.1",
43
43
  "marked": "^18.0.4"
44
44
  },
package/src/terminal.ts CHANGED
@@ -113,6 +113,27 @@ function isWindowsSubsystemForLinux(): boolean {
113
113
  return process.platform === "linux" && (!!$env.WSL_DISTRO_NAME || !!$env.WSL_INTEROP);
114
114
  }
115
115
 
116
+ /**
117
+ * Whether the native console viewport-position probe should be consulted.
118
+ *
119
+ * Returns `true` only on native Windows that is *not* fronted by Windows
120
+ * Terminal. The kernel32 `GetConsoleScreenBufferInfo` API answers about the
121
+ * ConPTY pseudo-console — which is always pinned to its tail — and not about
122
+ * the user-visible scrollback in modern hosts. Treat any such host as
123
+ * unreportable so the renderer falls back to the deferred-rebuild path.
124
+ *
125
+ * Pure helper for unit testing; the runtime call site reads `$env` /
126
+ * `process.platform`. See #1635.
127
+ */
128
+ export function shouldTrustNativeViewportProbe(
129
+ env: { WT_SESSION?: string | undefined } = $env,
130
+ platform: NodeJS.Platform = process.platform,
131
+ ): boolean {
132
+ if (platform !== "win32") return false;
133
+ if (env.WT_SESSION) return false;
134
+ return true;
135
+ }
136
+
116
137
  /**
117
138
  * Real terminal using process.stdin/stdout
118
139
  */
@@ -214,9 +235,18 @@ export class ProcessTerminal implements Terminal {
214
235
  /**
215
236
  * Returns true when Windows' active console viewport is at the scrollback tail.
216
237
  * POSIX terminals do not expose native scrollback position through a standard API.
238
+ *
239
+ * On native Windows running under Windows Terminal (the default modern
240
+ * host), the `kernel32` probe answers about the ConPTY pseudo-console — not
241
+ * the user-visible WT viewport — so it would always read "at bottom" while
242
+ * the user is scrolled up. Return `undefined` there so the renderer falls
243
+ * back to the POSIX-style deferred-rebuild path: streaming mutations stay
244
+ * non-destructive (no `\x1b[3J`), and the rebuild fires at the next prompt
245
+ * checkpoint via {@link TUI.refreshNativeScrollbackIfDirty} where the user
246
+ * is already pinned to the bottom by the editor keystroke. See #1635.
217
247
  */
218
248
  isNativeViewportAtBottom(): boolean | undefined {
219
- if (process.platform !== "win32") return undefined;
249
+ if (!shouldTrustNativeViewportProbe()) return undefined;
220
250
  try {
221
251
  const kernel32 = dlopen("kernel32.dll", {
222
252
  GetStdHandle: { args: [FFIType.i32], returns: FFIType.ptr },
package/src/tui.ts CHANGED
@@ -1335,8 +1335,42 @@ export class TUI extends Container {
1335
1335
  ) {
1336
1336
  return { kind: "historyRebuild" };
1337
1337
  }
1338
+ // POSIX terminals — and Windows Terminal/ConPTY — that cannot report the
1339
+ // viewport position fall through here (`canRebuildNativeScrollbackLive` is
1340
+ // false). A destructive rebuild emits `\x1b[3J`, which on modern terminals
1341
+ // resets the viewport to the top of scrollback and yanks a scrolled-up
1342
+ // reader (issue #1635), so it is unsafe while the probe is unavailable.
1343
+ //
1344
+ // When the shrunk transcript now fits entirely in the viewport there is no
1345
+ // new native history to preserve during the live frame: repaint the screen
1346
+ // in place (no `\x1b[3J`) and defer stale-scrollback cleanup to the next
1347
+ // checkpoint rebuild (e.g. prompt submit -> `refreshNativeScrollbackIfDirty`).
1348
+ if (nativeViewportAtBottom === undefined && newLines.length <= height) {
1349
+ this.#markNativeScrollbackDirty();
1350
+ return { kind: "viewportRepaint" };
1351
+ }
1352
+ // The shrunk transcript still overflows the viewport. A plain viewport
1353
+ // repaint would re-emit the rows between the new and old viewport tops on top
1354
+ // of the copies the terminal already kept in native scrollback; `deferredShrink`
1355
+ // pads to the previous row count so no committed row is re-emitted, and the
1356
+ // next checkpoint rebuild cleans up.
1357
+ //
1358
+ // That deferral only carries real content when `newLines.length` reaches the
1359
+ // padded viewport top (`previousLines.length - height`) — otherwise every row
1360
+ // the padded repaint draws is past the end of `newLines` and renders blank,
1361
+ // hiding the prompt until the next checkpoint. This can happen even when
1362
+ // `scrollbackHighWater` is far below `previousLines.length - height`, because
1363
+ // prior unknown-POSIX viewport repaints commit longer logical frames without
1364
+ // moving the native scrollback boundary. For a shrink that large a blank,
1365
+ // uninteractable viewport is the greater evil, so yank with `historyRebuild`.
1366
+ // Real win32 unknown probes defer as scrolled above and never reach this; the
1367
+ // yank only lands on non-win32 hosts whose probe is genuinely unavailable.
1368
+ const paddedViewportTop = Math.max(0, this.#previousLines.length - height);
1369
+ if (newLines.length <= paddedViewportTop) {
1370
+ return { kind: "historyRebuild" };
1371
+ }
1338
1372
  this.#markNativeScrollbackDirty();
1339
- return { kind: "viewportRepaint" };
1373
+ return { kind: "deferredShrink", paddedLength: this.#previousLines.length };
1340
1374
  }
1341
1375
 
1342
1376
  const suppressSuffixScroll = this.#suppressNextSuffixScroll;