From d731e575139801e45d96b4f488b44ae42e0c1e2b Mon Sep 17 00:00:00 2001 From: can1357 Date: Thu, 30 Apr 2026 04:36:33 +0200 Subject: [PATCH] fix(tui): guard nullish args in truncateToWidth wrapper renderContentPreview crashed on the Windows prebuilt with "Failed to convert napi value Null into rust type `u8`" (and the related `bool` variant) because the wrapper forwarded `null` to napi-rs 3's `Option` / `Option` parameters; the Windows binding rejects null where Linux/macOS tolerates it. `maxWidth` was also passed through unchecked, so a stray null/undefined upstream blew up the required `u32` everywhere. Pass concrete defaults that mirror the Rust `unwrap_or`s and clamp `maxWidth` to a sane non-negative integer. Fixes #848 --- packages/tui/src/utils.ts | 11 +++++- packages/tui/test/issue-848-repro.test.ts | 47 +++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 packages/tui/test/issue-848-repro.test.ts diff --git a/packages/tui/src/utils.ts b/packages/tui/src/utils.ts index d408caf56..a2e11db15 100644 --- a/packages/tui/src/utils.ts +++ b/packages/tui/src/utils.ts @@ -1,9 +1,11 @@ -import type { Ellipsis, ExtractSegmentsResult, SliceResult } from "@oh-my-pi/pi-natives"; import { + Ellipsis, + type ExtractSegmentsResult, extractSegments as nativeExtractSegments, sliceWithWidth as nativeSliceWithWidth, truncateToWidth as nativeTruncateToWidth, wrapTextWithAnsi as nativeWrapTextWithAnsi, + type SliceResult, } from "@oh-my-pi/pi-natives"; import { getDefaultTabWidth, getIndentation } from "@oh-my-pi/pi-utils"; @@ -21,7 +23,12 @@ export function truncateToWidth( ellipsisKind?: Ellipsis | null, pad?: boolean | null, ): string { - return nativeTruncateToWidth(text, maxWidth, ellipsisKind ?? null, pad ?? null, getDefaultTabWidth()); + // Guard nullish napi inputs: napi-rs 3 on the Windows prebuilt rejects + // `null` for `Option` (Ellipsis) / `Option` (pad) (issue #848), + // and `maxWidth` is a required `u32` that throws on `null`/`undefined` + // everywhere. Pass concrete defaults that mirror the Rust `unwrap_or`s. + const safeWidth = Number.isFinite(maxWidth) ? Math.max(0, Math.trunc(maxWidth)) : 0; + return nativeTruncateToWidth(text, safeWidth, ellipsisKind ?? Ellipsis.Unicode, pad ?? false, getDefaultTabWidth()); } export function wrapTextWithAnsi(text: string, width: number): string[] { diff --git a/packages/tui/test/issue-848-repro.test.ts b/packages/tui/test/issue-848-repro.test.ts new file mode 100644 index 000000000..655f7cfd7 --- /dev/null +++ b/packages/tui/test/issue-848-repro.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from "bun:test"; +import { Ellipsis, truncateToWidth } from "../src/utils"; + +// Regression test for https://github.com/can1357/oh-my-pi/issues/848 +// +// On Windows, rendering a saved write tool record crashed with: +// Error: Failed to convert napi value Null into rust type `u8` +// at truncateToWidth (unknown) +// at renderContentPreview (...) +// and the related variant "Failed to convert napi value into rust type `bool`" +// at the same call site. +// +// Root cause: the `truncateToWidth` JS wrapper coerced optional ellipsis/pad +// arguments to literal `null` (`?? null`) before forwarding to the napi +// binding. napi-rs 3 accepts that on Linux/macOS but rejects it on the +// Windows prebuilt: the null hits the underlying `u8` (Ellipsis enum) / +// `bool` (pad) conversions instead of the `Option` short-circuit. The +// wrapper must pass concrete defaults (matching the Rust `unwrap_or`s) so it +// never depends on the platform's null-handling for `Option`. Likewise, +// `maxWidth` must not silently forward `null`/`undefined` into the required +// `u32` parameter — the wrapper must clamp it to a sane non-negative integer. +describe("issue #848: truncateToWidth wrapper rejects nullish napi inputs", () => { + it("returns a string when ellipsisKind / pad are null", () => { + const result = truncateToWidth("hello world", 80, null, null); + expect(typeof result).toBe("string"); + expect(result).toBe("hello world"); + }); + + it("returns a string when ellipsisKind / pad are undefined", () => { + const result = truncateToWidth("hello world", 80, undefined, undefined); + expect(typeof result).toBe("string"); + expect(result).toBe("hello world"); + }); + + it("does not throw a napi conversion error when maxWidth is null", () => { + // Matches the resumed-render code path on Windows where a width derived + // from terminal state can briefly be null/undefined. The wrapper must + // degrade to an empty truncation rather than blowing up the renderer. + const result = truncateToWidth("hello world", null as unknown as number, Ellipsis.Omit, null); + expect(typeof result).toBe("string"); + }); + + it("does not throw a napi conversion error when maxWidth is undefined", () => { + const result = truncateToWidth("hello world", undefined as unknown as number, Ellipsis.Omit, null); + expect(typeof result).toBe("string"); + }); +});