diff --git a/fuck.txt b/fuck.txt new file mode 100644 index 00000000..5d4dac92 --- /dev/null +++ b/fuck.txt @@ -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 \ No newline at end of file diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 360c8e53..c7067035 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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 diff --git a/packages/tui/src/utils.ts b/packages/tui/src/utils.ts index b80c620d..251abc19 100644 --- a/packages/tui/src/utils.ts +++ b/packages/tui/src/utils.ts @@ -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) {