fix: emoji text wrapping crash in pi-tui

Fixed crash when rendering text containing emojis followed by long content.
The breakLongWord function was iterating over UTF-16 code units instead of
grapheme clusters, causing emojis (surrogate pairs) to be miscounted during
line wrapping. Now uses Intl.Segmenter to properly handle multi-codepoint
characters.
This commit is contained in:
Mario Zechner 2025-11-26 23:55:01 +01:00
parent 5b940c2686
commit e7c48e33a2
3 changed files with 48 additions and 8 deletions

12
fuck.txt Normal file
View file

@ -0,0 +1,12 @@
↑21 ↓442 R12k W10k $0.048 3.7% claude-sonnet-4-5file:///opt/homebrew/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@mariozechner/pi-tui/dist/tui.js:174
throw new Error(`Rendered line ${i} exceeds terminal width\n\n${newLines[i]}`);
^
Error: Rendered line 160 exceeds terminal width
😂\n\n[2025-11-21T21-45-04-863Z_b85e1b94-c1f2-4592-8521-b36cb24f93e7.html](https:
at TUI.doRender (file:///opt/homebrew/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@mariozechner/pi-tui/dist/tui.js:174:23)
at file:///opt/homebrew/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@mariozechner/pi-tui/dist/tui.js:68:18
at process.processTicksAndRejections (node:internal/process/task_queues:85:11)
Node.js v23.4.0

View file

@ -4,6 +4,7 @@
### Fixed
- **Emoji Text Wrapping Crash**: Fixed crash when rendering text containing emojis (e.g., 😂) followed by long content like URLs. The `breakLongWord` function in `pi-tui` was iterating over UTF-16 code units instead of grapheme clusters, causing emojis (which are surrogate pairs) to be miscounted during line wrapping. Now uses `Intl.Segmenter` to properly handle multi-codepoint characters.
- **Footer Cost Display**: Added `$` prefix to cost display in footer. Now shows `$0.078` instead of `0.078`. ([#53](https://github.com/badlogic/pi-mono/issues/53))
## [0.9.3] - 2025-11-24

View file

@ -203,33 +203,60 @@ function wrapSingleLine(line: string, width: number): string[] {
return wrapped.length > 0 ? wrapped : [""];
}
// Grapheme segmenter for proper Unicode iteration (handles emojis, etc.)
const segmenter = new Intl.Segmenter();
function breakLongWord(word: string, width: number, tracker: AnsiCodeTracker): string[] {
const lines: string[] = [];
let currentLine = tracker.getActiveCodes();
let currentWidth = 0;
// First, separate ANSI codes from visible content
// We need to handle ANSI codes specially since they're not graphemes
let i = 0;
const segments: Array<{ type: "ansi" | "grapheme"; value: string }> = [];
while (i < word.length) {
const ansiResult = extractAnsiCode(word, i);
if (ansiResult) {
currentLine += ansiResult.code;
tracker.process(ansiResult.code);
segments.push({ type: "ansi", value: ansiResult.code });
i += ansiResult.length;
} else {
// Find the next ANSI code or end of string
let end = i;
while (end < word.length) {
const nextAnsi = extractAnsiCode(word, end);
if (nextAnsi) break;
end++;
}
// Segment this non-ANSI portion into graphemes
const textPortion = word.slice(i, end);
for (const seg of segmenter.segment(textPortion)) {
segments.push({ type: "grapheme", value: seg.segment });
}
i = end;
}
}
// Now process segments
for (const seg of segments) {
if (seg.type === "ansi") {
currentLine += seg.value;
tracker.process(seg.value);
continue;
}
const char = word[i];
const charWidth = visibleWidth(char);
const grapheme = seg.value;
const graphemeWidth = visibleWidth(grapheme);
if (currentWidth + charWidth > width) {
if (currentWidth + graphemeWidth > width) {
lines.push(currentLine);
currentLine = tracker.getActiveCodes();
currentWidth = 0;
}
currentLine += char;
currentWidth += charWidth;
i++;
currentLine += grapheme;
currentWidth += graphemeWidth;
}
if (currentLine) {