@oh-my-pi/pi-tui 17.1.3 → 17.1.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,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.1.4] - 2026-07-26
6
+
7
+ ### Fixed
8
+
9
+ - Prevented inline Kitty graphics from covering full-width overlays such as `/switch`.
10
+ - Fixed Ctrl+O (expand tools) truncating the session on ConPTY hosts (native Windows and WSL): the full-view replay routed through the ConPTY frame-truncation intended only for bulk transcript-replacement paints (issue #2115), dropping every row above the retained tail behind an "older lines hidden" marker. The bound now keys on paint intent — bulk replacements (initial resume, `/resume`, handoff, resize geometry rebuilds) stay bounded, while a user-driven `resetDisplay()` (Ctrl+O expand, thinking/setting toggles, display reset) replays the whole transcript ([#4863](https://github.com/can1357/oh-my-pi/issues/4863)).
11
+
5
12
  ## [17.1.2] - 2026-07-24
6
13
 
7
14
  ### Fixed
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "17.1.3",
4
+ "version": "17.1.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": "17.1.3",
41
- "@oh-my-pi/pi-utils": "17.1.3",
40
+ "@oh-my-pi/pi-natives": "17.1.5",
41
+ "@oh-my-pi/pi-utils": "17.1.5",
42
42
  "lru-cache": "11.5.2",
43
43
  "marked": "^18.0.6"
44
44
  },
package/src/tui.ts CHANGED
@@ -1055,6 +1055,13 @@ export class TUI extends Container {
1055
1055
  #ghosttyInitialImageDelayTimer: RenderTimer | undefined;
1056
1056
  #ghosttyImageReadyAtMs = 0;
1057
1057
  #clearScrollbackOnNextRender = false;
1058
+ // Set by `resetDisplay()` and consumed by the next authoritative normal-screen
1059
+ // render. If that render is a full paint, it is a user-driven replay of the
1060
+ // current transcript (Ctrl+O expand, thinking/setting toggles, display reset)
1061
+ // that must show every row, so it opts out of #truncateLargeConptyFrame.
1062
+ // Multiplexer resets render as in-place updates; consuming the flag there
1063
+ // prevents a later /resume or handoff bulk replacement from inheriting it.
1064
+ #unboundedConptyPaintRequested = false;
1058
1065
  #forceViewportRepaintOnNextRender = false;
1059
1066
  #hasEverRendered = false;
1060
1067
  #scrollbackRebuildEnabled =
@@ -1889,6 +1896,10 @@ export class TUI extends Container {
1889
1896
  */
1890
1897
  resetDisplay(): void {
1891
1898
  if (this.#stopped) return;
1899
+ // This is a user-driven redraw of the current transcript; it must replay
1900
+ // every row, so opt the next full paint out of the ConPTY resume bound.
1901
+ // Set before the multiplexer early-return so it survives a deferred paint.
1902
+ this.#unboundedConptyPaintRequested = true;
1892
1903
  this.invalidate();
1893
1904
  // A reset that lands inside a tmux/screen/zellij resize burst would
1894
1905
  // paint mid-reflow and re-introduce the flash race (issue #2088).
@@ -2655,7 +2666,14 @@ export class TUI extends Container {
2655
2666
  overlayWidth: number,
2656
2667
  totalWidth: number,
2657
2668
  ): string {
2658
- if (TERMINAL.isImageLine(baseLine)) return baseLine;
2669
+ if (TERMINAL.isImageLine(baseLine)) {
2670
+ // Full-width overlays such as /switch are opaque: replace the
2671
+ // Unicode placeholder cells so the image cannot cover the modal.
2672
+ // Partial overlays cannot safely splice placement control sequences.
2673
+ if (startCol !== 0 || overlayWidth < totalWidth) return baseLine;
2674
+ const overlay = sliceWithWidth(overlayLine, 0, totalWidth, true);
2675
+ return SEGMENT_RESET + overlay.text + " ".repeat(Math.max(0, totalWidth - overlay.width));
2676
+ }
2659
2677
 
2660
2678
  // Single pass through baseLine extracts both before and after segments
2661
2679
  const afterStart = startCol + overlayWidth;
@@ -3154,6 +3172,12 @@ export class TUI extends Container {
3154
3172
  }
3155
3173
  const cursorTrackingLineCount = hasVisibleOverlay ? Math.max(frame.length, windowTop + height) : frame.length;
3156
3174
 
3175
+ // `resetDisplay()` requests an unbounded replay of the current
3176
+ // transcript. Consume that one-shot intent on this authoritative
3177
+ // normal-screen render even when a multiplexer makes it an in-place
3178
+ // update; otherwise a later /resume or handoff full paint inherits it.
3179
+ const unboundedConptyPaint = this.#unboundedConptyPaintRequested;
3180
+ this.#unboundedConptyPaintRequested = false;
3157
3181
  const intent: RenderIntent = fullPaint
3158
3182
  ? {
3159
3183
  kind: "fullPaint",
@@ -3187,6 +3211,7 @@ export class TUI extends Container {
3187
3211
  chunkTo,
3188
3212
  windowTop,
3189
3213
  cursorTrackingLineCount,
3214
+ boundConptyPaint: !unboundedConptyPaint,
3190
3215
  leadingSequence: deferredAltExit,
3191
3216
  });
3192
3217
  this.#pendingAltExit = "";
@@ -3588,6 +3613,16 @@ export class TUI extends Container {
3588
3613
  chunkTo: number;
3589
3614
  windowTop: number;
3590
3615
  cursorTrackingLineCount: number;
3616
+ /**
3617
+ * Whether this paint may be bounded by {@link #truncateLargeConptyFrame}
3618
+ * on ConPTY hosts. True for bulk transcript-replacement paints — first
3619
+ * paint, /resume, handoff, and resize geometry rebuilds — where a
3620
+ * multi-megabyte synchronized frame stalls conhost (issue #2115). False
3621
+ * for a user-driven `resetDisplay()` (Ctrl+O expand, thinking/setting
3622
+ * toggles, display reset), which must replay the whole transcript so
3623
+ * nothing is silently dropped from scrollback (issue #4863).
3624
+ */
3625
+ boundConptyPaint: boolean;
3591
3626
  leadingSequence: string;
3592
3627
  },
3593
3628
  ): void {
@@ -3604,16 +3639,19 @@ export class TUI extends Container {
3604
3639
  paintCursorPos = { row: chunkTo + cursorPos.row - windowTop, col: cursorPos.col };
3605
3640
  }
3606
3641
  }
3607
- // ConPTY hosts bound the replay: merge prefix + window into one array
3608
- // so #truncateLargeConptyFrame can measure the payload and retain only
3609
- // the tail. Gated on the host check everywhere else the merge would
3610
- // copy a pointer per committed row (a 50k-row session = 50k-entry
3611
- // array per resize step / theme change / session replace) just to be
3612
- // returned unchanged. `paintLines` stays null unless truncation
3613
- // actually rewrote the replay.
3642
+ // ConPTY hosts bound bulk transcript-replacement replays (resume, handoff,
3643
+ // first paint, resize): merge prefix + window into one array so
3644
+ // #truncateLargeConptyFrame can measure the payload and retain only the
3645
+ // tail (#2115). Gated on `boundConptyPaint` a user-driven `resetDisplay()`
3646
+ // (Ctrl+O expand, toggles) sets it false and replays the whole transcript
3647
+ // untruncated so nothing is dropped from scrollback (#4863). Gated on the
3648
+ // host check too — everywhere else the merge would copy a pointer per
3649
+ // committed row (a 50k-row session = 50k-entry array per resize step /
3650
+ // theme change / session replace) just to be returned unchanged.
3651
+ // `paintLines` stays null unless truncation actually rewrote the replay.
3614
3652
  let paintLines: string[] | null = null;
3615
3653
  let paintLineCount = chunkTo + height;
3616
- if (isConPTYHosted()) {
3654
+ if (options.boundConptyPaint && isConPTYHosted()) {
3617
3655
  const merged = new Array<string>(chunkTo + height);
3618
3656
  for (let i = 0; i < chunkTo; i++) merged[i] = frame[i] ?? "";
3619
3657
  for (let screenRow = 0; screenRow < height; screenRow++) {