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<Ellipsis>` / `Option<bool>` 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
This commit is contained in:
can1357
2026-04-30 04:36:33 +02:00
parent d9376c5866
commit d731e57513
2 changed files with 56 additions and 2 deletions
+9 -2
View File
@@ -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<u8>` (Ellipsis) / `Option<bool>` (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[] {
+47
View File
@@ -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<T>` 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<T>`. 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");
});
});