@oh-my-pi/pi-tui 16.0.10 → 16.0.11

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,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.0.11] - 2026-06-19
6
+
7
+ ### Breaking Changes
8
+
9
+ - Removed `getIndentation` and `getIndentationNoescape` exported utilities
10
+ - Tab-related operations no longer respect per-file or globally configured indentation settings
11
+
12
+ ### Changed
13
+
14
+ - Standardized tab expansion to use a fixed display width instead of configurable settings
15
+ - Removed support for custom tab width configuration in text rendering and input handling
16
+
17
+ ### Fixed
18
+
19
+ - Corrected logic in string truncation to prevent improper truncation of short strings
20
+ - Fixed a one-frame transcript flash during a non-multiplexer resize drag: while the drag borrowed the alternate screen and painted only the viewport, any ordinary (non-forced) render from a still-animating block — a tool spinner tick, a streamed token, a cursor blink — fell through to the deferred geometry-rebuild full paint, which left the alternate screen to repaint the whole transcript on the normal screen for a single frame before the next SIGWINCH re-entered the viewport fast path, so a live tool block flashed in and vanished. Ordinary renders mid-drag now stay on the viewport fast path; only forced renders (tool finalization, reset, image reconciliation) still preempt it.
21
+
5
22
  ## [16.0.10] - 2026-06-18
6
23
 
7
24
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  import { Ellipsis, type ExtractSegmentsResult, type SliceResult } from "@oh-my-pi/pi-natives";
2
2
  export { Ellipsis } from "@oh-my-pi/pi-natives";
3
- export { getDefaultTabWidth, getIndentation } from "@oh-my-pi/pi-utils";
3
+ export { DEFAULT_TAB_WIDTH } from "@oh-my-pi/pi-utils";
4
4
  export type TextSizingScale = 1 | 2 | 3;
5
5
  export type TextSizingVerticalAlign = "top" | "bottom" | "center";
6
6
  export type TextSizingHorizontalAlign = "left" | "right" | "center";
@@ -17,14 +17,10 @@ export interface TextSizingOptions {
17
17
  */
18
18
  export declare function encodeTextSized(text: string, options?: TextSizingOptions): string;
19
19
  export declare function sliceWithWidth(line: string, startCol: number, length: number, strict?: boolean | null): SliceResult;
20
- export declare function truncateToWidth(text: string, maxWidth: number, ellipsisKind?: Ellipsis | null, pad?: boolean | null): string;
20
+ export declare function truncateToWidth(text: string, maxWidth: number, ellipsisKind?: Ellipsis | null | "", pad?: boolean | null): string;
21
21
  export declare function wrapTextWithAnsi(text: string, width: number): string[];
22
22
  export declare function extractSegments(line: string, beforeEnd: number, afterStart: number, afterLen: number, strictAfter: boolean): ExtractSegmentsResult;
23
- /**
24
- * Tab width in columns for `file`, using `process.cwd()` as the project root for relative paths.
25
- */
26
- export declare function getIndentationNoescape(file?: string): number;
27
- export declare function replaceTabs(text: string, file?: string): string;
23
+ export declare function replaceTabs(text: string): string;
28
24
  /**
29
25
  * Returns a string of n spaces. Uses a pre-allocated buffer for efficiency.
30
26
  */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "16.0.10",
4
+ "version": "16.0.11",
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": "16.0.10",
41
- "@oh-my-pi/pi-utils": "16.0.10",
40
+ "@oh-my-pi/pi-natives": "16.0.11",
41
+ "@oh-my-pi/pi-utils": "16.0.11",
42
42
  "lru-cache": "11.5.1",
43
43
  "marked": "^18.0.5"
44
44
  },
package/src/tui.ts CHANGED
@@ -1005,11 +1005,6 @@ export class TUI extends Container {
1005
1005
  // fast path (`#renderResizeViewport`) instead of an authoritative full
1006
1006
  // paint, and no commit/window/diff state is advanced.
1007
1007
  #resizeViewportActive = false;
1008
- // Set only by the resize callback's cheap-paint request. A concurrent
1009
- // caller-forced render (tool finalization, reset, image reconciliation) must
1010
- // not be downgraded to the throwaway viewport path just because a resize
1011
- // settle window is active.
1012
- #resizeViewportPaintPending = false;
1013
1008
  // Quiet-window timer that ends the drag: its callback clears the flag and
1014
1009
  // drives the one authoritative full paint. Reset on every resize event so it
1015
1010
  // only fires once the drag stops. Cancelled on stop().
@@ -1792,7 +1787,6 @@ export class TUI extends Container {
1792
1787
  // Any non-component-scoped request makes the pending frame a full one.
1793
1788
  this.#pendingRenderComponentsOnly = false;
1794
1789
  if (force) {
1795
- this.#resizeViewportPaintPending = false;
1796
1790
  // Forced repaints landing inside the multiplexer resize debounce
1797
1791
  // (e.g. `#finishSixelProbe`, image-budget eviction, a programmatic
1798
1792
  // `requestRender(true)`) would paint into a still-reflowing pane
@@ -2471,16 +2465,30 @@ export class TUI extends Container {
2471
2465
  // Strictly state-isolated: it never consumes #resizeEventPending nor
2472
2466
  // advances any commit/window/diff field, so the authoritative full paint
2473
2467
  // the settle timer queues reconciles as if these throwaway frames never
2474
- // ran. A visible overlay composites over the transcript and needs the
2475
- // whole window, so fall through to the normal forced paint when one is up
2476
- // (overlay resizes are not on the drag-cost hot path).
2468
+ // ran. Two render sources reach here mid-drag and BOTH must stay on this
2469
+ // path:
2470
+ // - the resize callback's own cheap paint after each SIGWINCH;
2471
+ // - an ordinary (non-forced) render from a live block that keeps
2472
+ // animating through the drag — a spinner tick, a streamed token, a
2473
+ // cursor blink — firing requestRender(false)/requestComponentRender.
2474
+ // #resizeEventPending is still set (the fast path never consumed it),
2475
+ // so without this branch the ordinary render falls through to the
2476
+ // geometry-rebuild full paint below, which LEAVES the borrowed
2477
+ // alternate screen to repaint the whole transcript on the normal
2478
+ // screen — then the next SIGWINCH re-enters the alt screen and paints
2479
+ // only the tail, so the block flashes in for one frame and vanishes.
2480
+ // A forced render (tool finalization, reset, image reconciliation) must
2481
+ // still preempt: it set #forceViewportRepaintOnNextRender via
2482
+ // #prepareForcedRender and owns the next authoritative paint, so it falls
2483
+ // through. A visible overlay composites over the transcript and needs the
2484
+ // whole window, so it also falls through (overlay resizes are not on the
2485
+ // drag-cost hot path).
2477
2486
  if (
2478
- this.#resizeViewportPaintPending &&
2479
2487
  this.#resizeViewportActive &&
2488
+ !this.#forceViewportRepaintOnNextRender &&
2480
2489
  this.#hasEverRendered &&
2481
2490
  this.#getTopmostVisibleOverlay() === undefined
2482
2491
  ) {
2483
- this.#resizeViewportPaintPending = false;
2484
2492
  this.#componentRenderTargets.clear();
2485
2493
  this.#renderResizeViewport(width, height);
2486
2494
  return;
@@ -3145,7 +3153,6 @@ export class TUI extends Container {
3145
3153
 
3146
3154
  #requestResizeViewportPaint(): void {
3147
3155
  if (this.#stopped) return;
3148
- this.#resizeViewportPaintPending = true;
3149
3156
  this.#renderRequested = false;
3150
3157
  this.#lastRenderAt = this.#renderScheduler.now();
3151
3158
  this.#doRender();
package/src/utils.ts CHANGED
@@ -7,11 +7,11 @@ import {
7
7
  wrapTextWithAnsi as nativeWrapTextWithAnsi,
8
8
  type SliceResult,
9
9
  } from "@oh-my-pi/pi-natives";
10
- import { getDefaultTabWidth, getIndentation } from "@oh-my-pi/pi-utils";
10
+ import { DEFAULT_TAB_WIDTH } from "@oh-my-pi/pi-utils";
11
11
 
12
12
  export { Ellipsis } from "@oh-my-pi/pi-natives";
13
13
 
14
- export { getDefaultTabWidth, getIndentation } from "@oh-my-pi/pi-utils";
14
+ export { DEFAULT_TAB_WIDTH } from "@oh-my-pi/pi-utils";
15
15
 
16
16
  export type TextSizingScale = 1 | 2 | 3;
17
17
  export type TextSizingVerticalAlign = "top" | "bottom" | "center";
@@ -74,35 +74,32 @@ export function encodeTextSized(text: string, options: TextSizingOptions = {}):
74
74
  }
75
75
 
76
76
  export function sliceWithWidth(line: string, startCol: number, length: number, strict?: boolean | null): SliceResult {
77
- return nativeSliceWithWidth(line, startCol, length, strict ?? null, getDefaultTabWidth());
77
+ return nativeSliceWithWidth(line, startCol, length, strict ?? null, DEFAULT_TAB_WIDTH);
78
78
  }
79
79
 
80
80
  export function truncateToWidth(
81
81
  text: string,
82
82
  maxWidth: number,
83
- ellipsisKind?: Ellipsis | null,
83
+ ellipsisKind?: Ellipsis | null | "",
84
84
  pad?: boolean | null,
85
85
  ): string {
86
- // Guard nullish napi inputs: napi-rs 3 on the Windows prebuilt rejects
87
- // `null` for `Option<u8>` (Ellipsis) / `Option<bool>` (pad) (issue #848),
88
- // and `maxWidth` is a required `u32` that throws on `null`/`undefined`
89
- // everywhere. Pass concrete defaults that mirror the Rust `unwrap_or`s.
90
- const safeWidth = Number.isFinite(maxWidth) ? Math.max(0, Math.trunc(maxWidth)) : 0;
91
- let resolvedEllipsis: Ellipsis | null | undefined | string = ellipsisKind;
92
- if (typeof resolvedEllipsis === "string") {
93
- resolvedEllipsis = resolvedEllipsis === "" ? Ellipsis.Omit : Ellipsis.Unicode;
86
+ maxWidth = Math.max(0, maxWidth | 0);
87
+ // Fast path: every UTF-16 unit is at most 3 cells wide, so a string whose
88
+ // `length * 3` already fits within `safeWidth` cannot need truncation.
89
+ if (!pad && text.length * 3 <= maxWidth) {
90
+ return text;
94
91
  }
95
92
  return nativeTruncateToWidth(
96
93
  text,
97
- safeWidth,
98
- resolvedEllipsis ?? Ellipsis.Unicode,
94
+ maxWidth,
95
+ (ellipsisKind === "" ? Ellipsis.Omit : ellipsisKind) ?? Ellipsis.Unicode,
99
96
  pad ?? false,
100
- getDefaultTabWidth(),
97
+ DEFAULT_TAB_WIDTH,
101
98
  );
102
99
  }
103
100
 
104
101
  export function wrapTextWithAnsi(text: string, width: number): string[] {
105
- return nativeWrapTextWithAnsi(text, width, getDefaultTabWidth());
102
+ return nativeWrapTextWithAnsi(text, width, DEFAULT_TAB_WIDTH);
106
103
  }
107
104
 
108
105
  export function extractSegments(
@@ -112,24 +109,17 @@ export function extractSegments(
112
109
  afterLen: number,
113
110
  strictAfter: boolean,
114
111
  ): ExtractSegmentsResult {
115
- return nativeExtractSegments(line, beforeEnd, afterStart, afterLen, strictAfter, getDefaultTabWidth());
112
+ return nativeExtractSegments(line, beforeEnd, afterStart, afterLen, strictAfter, DEFAULT_TAB_WIDTH);
116
113
  }
117
114
 
118
115
  // Pre-allocated space buffer for padding
119
116
  const SPACE_BUFFER = " ".repeat(512);
120
117
 
121
- /**
122
- * Tab width in columns for `file`, using `process.cwd()` as the project root for relative paths.
123
- */
124
- export function getIndentationNoescape(file?: string): number {
125
- return getIndentation(file, process.cwd());
126
- }
127
-
128
118
  /*
129
- * Replace tabs with configured spacing for consistent rendering.
119
+ * Replace tabs with the fixed display tab width for consistent rendering.
130
120
  */
131
- export function replaceTabs(text: string, file?: string): string {
132
- return text.replaceAll("\t", " ".repeat(getIndentation(file)));
121
+ export function replaceTabs(text: string): string {
122
+ return text.replaceAll("\t", " ".repeat(DEFAULT_TAB_WIDTH));
133
123
  }
134
124
 
135
125
  /**
@@ -186,7 +176,7 @@ export function visibleWidth(str: string): number {
186
176
  for (let tabIndex = str.indexOf(TAB); tabIndex !== -1; tabIndex = str.indexOf(TAB, tabIndex + 1)) {
187
177
  tabCount++;
188
178
  }
189
- if (tabCount > 0) width += tabCount * getDefaultTabWidth();
179
+ if (tabCount > 0) width += tabCount * DEFAULT_TAB_WIDTH;
190
180
  return width;
191
181
  }
192
182
 
@@ -203,7 +193,7 @@ export function visibleWidth(str: string): number {
203
193
  }
204
194
  }
205
195
  if (i === str.length) {
206
- return tabCount === 0 ? str.length : str.length + tabCount * (getDefaultTabWidth() - 1);
196
+ return tabCount === 0 ? str.length : str.length + tabCount * (DEFAULT_TAB_WIDTH - 1);
207
197
  }
208
198
 
209
199
  if (tabCount === 0) {
@@ -224,7 +214,7 @@ export function visibleWidth(str: string): number {
224
214
  // the native scanner that traps under Bun 1.3.x GC/N-API load). It strips
225
215
  // CSI/OSC to zero cells and shares the native engine's UAX#11 width tables.
226
216
  let width = Bun.stringWidth(str, STRING_WIDTH_OPTS);
227
- if (tabCount > 0) width += tabCount * getDefaultTabWidth();
217
+ if (tabCount > 0) width += tabCount * DEFAULT_TAB_WIDTH;
228
218
 
229
219
  // OSC 66: add back each stripped span as `scale * (explicit w ?? payload
230
220
  // width)`. Matched rather than replaced to avoid reallocating the string.