Skip to content

Commit 259f022

Browse files
ly-wang19claude
andcommitted
fix(chunk): keep emoji whole when length-splitting a long line
chunkByNewline length-splits a long line with a raw `lineValue.slice(0, firstLimit)`, which can cut through a surrogate pair when firstLimit lands on an odd UTF-16 offset. The first chunk then ends on a lone high surrogate and the next chunk begins with the orphaned low surrogate, rendering a broken/replacement glyph. Guard the boundary with avoidTrailingHighSurrogateBreak (already used by chunkText/chunkMarkdownText in this file and already imported), so the split lands on a code-point boundary. Only the rare mid-surrogate boundary changes; all other inputs are byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
1 parent e4763b0 commit 259f022

2 files changed

Lines changed: 19 additions & 2 deletions

File tree

src/auto-reply/chunk.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,22 @@ describe("chunkByNewline", () => {
513513
it.each(["", " \n\n "] as const)("returns empty array for input %j", (text) => {
514514
expect(chunkByNewline(text, 100)).toStrictEqual([]);
515515
});
516+
517+
it("does not split surrogate pairs when length-splitting a long line", () => {
518+
// A single line of 8 emoji (16 UTF-16 units) with an odd maxLineLength that
519+
// would otherwise land mid-surrogate-pair at index 3.
520+
const text = "😀".repeat(8);
521+
const chunks = chunkByNewline(text, 3);
522+
523+
expect(chunks.join("")).toBe(text);
524+
expect(chunks.length).toBeGreaterThan(1);
525+
// No chunk ends on a lone high surrogate.
526+
expect(chunks.every((chunk) => !/[\uD800-\uDBFF]$/u.test(chunk))).toBe(true);
527+
// No chunk starts on a lone low surrogate.
528+
expect(chunks.every((chunk) => !/^[\uDC00-\uDFFF]/u.test(chunk))).toBe(true);
529+
// First chunk carries a single whole emoji rather than 😀 + lone surrogate.
530+
expect(chunks[0]).toBe("😀");
531+
});
516532
});
517533

518534
describe("chunkTextWithMode", () => {

src/auto-reply/chunk.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,10 @@ export function chunkByNewline(
164164
}
165165

166166
const firstLimit = Math.max(1, maxLineLength - prefix.length);
167-
const first = lineValue.slice(0, firstLimit);
167+
const safeLimit = avoidTrailingHighSurrogateBreak(lineValue, 0, firstLimit);
168+
const first = lineValue.slice(0, safeLimit);
168169
chunks.push(prefix + first);
169-
const remaining = lineValue.slice(firstLimit);
170+
const remaining = lineValue.slice(safeLimit);
170171
if (remaining) {
171172
chunks.push(...chunkText(remaining, maxLineLength));
172173
}

0 commit comments

Comments
 (0)