@sayknow-cli/tui 0.3.15 → 0.3.16

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 (2) hide show
  1. package/package.json +3 -3
  2. package/src/tui.ts +26 -8
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@sayknow-cli/tui",
4
- "version": "0.3.15",
4
+ "version": "0.3.16",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://sayknow-cli.com",
7
7
  "author": "jaybeyond",
@@ -38,8 +38,8 @@
38
38
  "fmt": "biome format --write ."
39
39
  },
40
40
  "dependencies": {
41
- "@sayknow-cli/natives": "0.3.15",
42
- "@sayknow-cli/utils": "0.3.15",
41
+ "@sayknow-cli/natives": "0.3.16",
42
+ "@sayknow-cli/utils": "0.3.16",
43
43
  "lru-cache": "11.3.6",
44
44
  "marked": "^18.0.3"
45
45
  },
package/src/tui.ts CHANGED
@@ -2182,16 +2182,25 @@ export class TUI extends Container {
2182
2182
  this.#previousHeight = height;
2183
2183
  };
2184
2184
 
2185
- const viewportRepaint = (reason: string): void => {
2185
+ const viewportRepaint = (reason: string, absoluteClear = false): void => {
2186
2186
  this.#fullRedrawCount += 1;
2187
2187
  if (renderMetrics.enabled) renderMetrics.recordFullRedraw(reason);
2188
2188
  const nextViewportTop = Math.max(0, newLines.length - height);
2189
- const currentScreenRow = Math.max(0, Math.min(height - 1, hardwareCursorRow - prevViewportTop));
2190
2189
  let buffer = "\x1b[?2026h";
2191
- if (currentScreenRow > 0) {
2192
- buffer += `\x1b[${currentScreenRow}A`;
2190
+ if (absoluteClear) {
2191
+ // A width reflow under a terminal multiplexer re-wraps the on-screen rows to
2192
+ // the new width before SIGWINCH fires, so cursor-relative row math (from the
2193
+ // old width) lands on the wrong physical row and corrupts the repaint. Clear
2194
+ // the visible screen and repaint from absolute home; skip 3J so scrollback
2195
+ // history is preserved.
2196
+ buffer += "\x1b[2J\x1b[H";
2197
+ } else {
2198
+ const currentScreenRow = Math.max(0, Math.min(height - 1, hardwareCursorRow - prevViewportTop));
2199
+ if (currentScreenRow > 0) {
2200
+ buffer += `\x1b[${currentScreenRow}A`;
2201
+ }
2202
+ buffer += "\r";
2193
2203
  }
2194
- buffer += "\r";
2195
2204
  for (let screenRow = 0; screenRow < height; screenRow++) {
2196
2205
  if (screenRow > 0) buffer += "\r\n";
2197
2206
  buffer += "\x1b[2K";
@@ -2253,13 +2262,22 @@ export class TUI extends Container {
2253
2262
 
2254
2263
  // Width changes always need a full re-render because wrapping changes.
2255
2264
  if (widthChanged) {
2265
+ // A forced render (requestRender(true)) resets #previousWidth to -1: that is a
2266
+ // *fake* width change (the terminal never reflowed), so it keeps the cheap
2267
+ // cursor-relative repaint. A *real* width change (a valid prior width that
2268
+ // differs) means the multiplexer already re-wrapped the on-screen rows,
2269
+ // invalidating cursor-relative row math, so it needs the absolute clear.
2270
+ const realWidthChange = this.#previousWidth > 0 && this.#previousWidth !== width;
2256
2271
  logRedraw(`terminal width changed (${this.#previousWidth} -> ${width})`);
2257
2272
  if (useViewportRepaintPath(this.terminal)) {
2258
2273
  // In viewport-repaint sessions a full replay can either pile the transcript
2259
2274
  // back onto scrollback (tmux/screen) or visibly jump to the transcript top
2260
- // (Windows Terminal). Repaint the viewport only, mirroring the height-change
2261
- // branch and neutralizing fake width changes from requestRender(true).
2262
- viewportRepaint(`terminal width changed (${this.#previousWidth} -> ${width})`);
2275
+ // (Windows Terminal). For a real width change the reflow invalidates
2276
+ // cursor-relative math, so clear the visible screen and repaint the viewport
2277
+ // from absolute home (absoluteClear) fixing the corruption without replaying
2278
+ // the whole transcript (no scrollback storm). A fake (force) width change
2279
+ // keeps the in-place relative repaint.
2280
+ viewportRepaint(`terminal width changed (${this.#previousWidth} -> ${width})`, realWidthChange);
2263
2281
  } else {
2264
2282
  fullRender(true, "terminal width changed");
2265
2283
  }