fix(chat): keep the diff preview bound off a surrogate pair
The 256 KiB budget for an oversized tool diff preview counts UTF-16 units, so the cut can land between the two halves of an astral character - an emoji in a patched string, for instance. The preview then ended in a lone surrogate that renders as the replacement glyph. Step back one unit when the boundary splits a pair.
This commit is contained in:
@@ -22,8 +22,20 @@ const getPreviewEnd = (diff: string): number | null => {
|
||||
|
||||
export const isToolDiffPreviewOversized = (diff: string): boolean => getPreviewEnd(diff) !== null;
|
||||
|
||||
/**
|
||||
* The character budget counts UTF-16 units, so it can land between the two
|
||||
* halves of an astral character (an emoji in a patched string, say). Cutting
|
||||
* there leaves a lone surrogate that renders as the replacement glyph, so step
|
||||
* back one unit when the boundary splits a pair.
|
||||
*/
|
||||
const withoutSplitSurrogate = (diff: string, previewEnd: number): number => {
|
||||
const lastUnit = diff.charCodeAt(previewEnd - 1);
|
||||
const isHighSurrogate = lastUnit >= 0xd800 && lastUnit <= 0xdbff;
|
||||
return isHighSurrogate ? previewEnd - 1 : previewEnd;
|
||||
};
|
||||
|
||||
export const getToolDiffPreviewText = (diff: string): string => {
|
||||
const previewEnd = getPreviewEnd(diff);
|
||||
if (previewEnd === null) return diff;
|
||||
return `${diff.slice(0, previewEnd).trimEnd()}\n…`;
|
||||
return `${diff.slice(0, withoutSplitSurrogate(diff, previewEnd)).trimEnd()}\n…`;
|
||||
};
|
||||
|
||||
@@ -223,6 +223,18 @@ describe('toolDiffUtils', () => {
|
||||
expect(preview.length).toBe(TOOL_DIFF_PREVIEW_MAX_CHARS + 2);
|
||||
});
|
||||
|
||||
test('does not cut an astral character in half at the character limit', () => {
|
||||
// The boundary lands between the two halves of the emoji, which would
|
||||
// otherwise leave a lone surrogate rendering as the replacement glyph.
|
||||
const patch = `+${'x'.repeat(TOOL_DIFF_PREVIEW_MAX_CHARS - 2)}\u{1F600}${'y'.repeat(100)}`;
|
||||
const preview = getToolDiffPreviewText(patch);
|
||||
const body = preview.slice(0, -2);
|
||||
|
||||
expect(isToolDiffPreviewOversized(patch)).toBe(true);
|
||||
expect(body).toBe(`+${'x'.repeat(TOOL_DIFF_PREVIEW_MAX_CHARS - 2)}`);
|
||||
expect(/[\uD800-\uDFFF]/.test(body)).toBe(false);
|
||||
});
|
||||
|
||||
test('preserves diff previews at the character and line limits', () => {
|
||||
const characterLimit = 'x'.repeat(TOOL_DIFF_PREVIEW_MAX_CHARS);
|
||||
const lineLimit = Array.from({ length: TOOL_DIFF_PREVIEW_MAX_LINES }, () => '+line').join('\n');
|
||||
|
||||
Reference in New Issue
Block a user