diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index ae628067d..c8512cd79 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed tab width accounting in column slicing and overlay compositing so tab-containing output cannot exceed the terminal width ([#5218](https://github.com/earendil-works/pi/issues/5218)). + ## [0.78.0] - 2026-05-29 ### Fixed diff --git a/packages/tui/src/utils.ts b/packages/tui/src/utils.ts index d613a76d5..02c40c798 100644 --- a/packages/tui/src/utils.ts +++ b/packages/tui/src/utils.ts @@ -162,6 +162,10 @@ function finalizeTruncatedResult( * check to avoid running the RGI_Emoji regex unnecessarily. */ function graphemeWidth(segment: string): number { + if (segment === "\t") { + return 3; + } + // Zero-width clusters if (zeroWidthRegex.test(segment)) { return 0; diff --git a/packages/tui/test/tab-width.test.ts b/packages/tui/test/tab-width.test.ts new file mode 100644 index 000000000..2797f89fe --- /dev/null +++ b/packages/tui/test/tab-width.test.ts @@ -0,0 +1,23 @@ +import assert from "node:assert"; +import { describe, it } from "node:test"; +import { extractSegments, sliceWithWidth, visibleWidth } from "../src/utils.ts"; + +describe("tab width accounting", () => { + it("keeps slice helper widths consistent with visible width", () => { + const text = "out 192M\t.pi/skill-tests/results-ha"; + const slice = sliceWithWidth(text, 0, 10, true); + + assert.strictEqual(slice.text, "out 192M"); + assert.strictEqual(slice.width, 8); + assert.strictEqual(visibleWidth(slice.text), slice.width); + }); + + it("keeps overlay segment widths consistent with visible width", () => { + const text = "out 192M\t.pi/skill-tests/results-ha"; + const segments = extractSegments(text, 10, 13, 10, true); + + assert.strictEqual(segments.before, "out 192M\t"); + assert.strictEqual(segments.beforeWidth, 11); + assert.strictEqual(visibleWidth(segments.before), segments.beforeWidth); + }); +});