mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 06:04:40 +00:00
fix(tui): stabilize regional indicator width to prevent streaming render drift (#1783)
This commit is contained in:
parent
d4084a7ad6
commit
85d06052fb
4 changed files with 68 additions and 0 deletions
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed TUI width calculation for regional indicator symbols (e.g. partial flag sequences like `🇨` during streaming) to prevent wrap drift and stale character artifacts in differential rendering.
|
||||
|
||||
## [0.55.4] - 2026-03-02
|
||||
|
||||
## [0.55.3] - 2026-02-27
|
||||
|
|
|
|||
|
|
@ -60,6 +60,13 @@ function graphemeWidth(segment: string): number {
|
|||
return 0;
|
||||
}
|
||||
|
||||
// Regional indicator symbols (U+1F1E6..U+1F1FF) are often rendered as
|
||||
// full-width emoji in terminals, even when isolated during streaming.
|
||||
// Keep width conservative (2) to avoid terminal auto-wrap drift artifacts.
|
||||
if (cp >= 0x1f1e6 && cp <= 0x1f1ff) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
let width = eastAsianWidth(cp);
|
||||
|
||||
// Trailing halfwidth/fullwidth forms
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
import assert from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
import { visibleWidth, wrapTextWithAnsi } from "../src/utils.js";
|
||||
|
||||
describe("regional indicator width regression", () => {
|
||||
it("treats partial flag grapheme as full-width to avoid streaming render drift", () => {
|
||||
// Repro context:
|
||||
// During streaming, "🇨🇳" often appears as an intermediate "🇨" first.
|
||||
// If "🇨" is measured as width 1 while terminal renders it as width 2,
|
||||
// differential rendering can drift and leave stale characters on screen.
|
||||
const partialFlag = "🇨";
|
||||
const listLine = " - 🇨";
|
||||
|
||||
assert.strictEqual(visibleWidth(partialFlag), 2);
|
||||
assert.strictEqual(visibleWidth(listLine), 10);
|
||||
});
|
||||
|
||||
it("wraps intermediate partial-flag list line before overflow", () => {
|
||||
// Width 9 cannot fit " - 🇨" if 🇨 is width 2 (8 + 2 = 10).
|
||||
// This must wrap to avoid terminal auto-wrap mismatch.
|
||||
const wrapped = wrapTextWithAnsi(" - 🇨", 9);
|
||||
|
||||
assert.strictEqual(wrapped.length, 2);
|
||||
assert.strictEqual(visibleWidth(wrapped[0] || ""), 7);
|
||||
assert.strictEqual(visibleWidth(wrapped[1] || ""), 2);
|
||||
});
|
||||
|
||||
it("treats all regional-indicator singleton graphemes as width 2", () => {
|
||||
for (let cp = 0x1f1e6; cp <= 0x1f1ff; cp++) {
|
||||
const regionalIndicator = String.fromCodePoint(cp);
|
||||
assert.strictEqual(
|
||||
visibleWidth(regionalIndicator),
|
||||
2,
|
||||
`Expected ${regionalIndicator} (U+${cp.toString(16).toUpperCase()}) to be width 2`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps full flag pairs at width 2", () => {
|
||||
const samples = ["🇯🇵", "🇺🇸", "🇬🇧", "🇨🇳", "🇩🇪", "🇫🇷"];
|
||||
for (const flag of samples) {
|
||||
assert.strictEqual(visibleWidth(flag), 2, `Expected ${flag} to be width 2`);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps common streaming emoji intermediates at stable width", () => {
|
||||
const samples = ["👍", "👍🏻", "✅", "⚡", "⚡️", "👨", "👨💻", "🏳️🌈"];
|
||||
for (const sample of samples) {
|
||||
assert.strictEqual(visibleWidth(sample), 2, `Expected ${sample} to be width 2`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -111,6 +111,11 @@ describe("wrapTextWithAnsi", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("should treat isolated regional indicators as width 2", () => {
|
||||
assert.strictEqual(visibleWidth("🇨"), 2);
|
||||
assert.strictEqual(visibleWidth("🇨🇳"), 2);
|
||||
});
|
||||
|
||||
it("should truncate trailing whitespace that exceeds width", () => {
|
||||
const twoSpacesWrappedToWidth1 = wrapTextWithAnsi(" ", 1);
|
||||
assert.ok(visibleWidth(twoSpacesWrappedToWidth1[0]) <= 1);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue