From 07822aaefdb249c2678bea03cca6c8db4863a650 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 2 Aug 2026 03:19:48 +0800 Subject: [PATCH] fix(ui): preserve Talk transcript surrogate bounds --- .../chat/realtime-talk-conversation.test.ts | 23 +++++++++++++++++++ .../pages/chat/realtime-talk-conversation.ts | 7 +++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/ui/src/pages/chat/realtime-talk-conversation.test.ts b/ui/src/pages/chat/realtime-talk-conversation.test.ts index 08a44cae000c..49cb5e1ab315 100644 --- a/ui/src/pages/chat/realtime-talk-conversation.test.ts +++ b/ui/src/pages/chat/realtime-talk-conversation.test.ts @@ -217,6 +217,29 @@ describe("realtime Talk conversation", () => { expect(state.entries[0]?.text.endsWith("NEWEST")).toBe(true); }); + it.each([255, 256])( + "does not retain a lone high surrogate before a natural marker at offset %i", + (markerOffset) => { + let state = createRealtimeTalkConversationState(); + const retainedText = "a".repeat(markerOffset - 1); + + state = updateRealtimeTalkConversation(state, { + role: "assistant", + text: `${retainedText}\uD800\n…\n${"b".repeat(8_000)}NEWEST`, + final: true, + nowMs: 1, + }); + + const text = state.entries[0]?.text ?? ""; + expect(text.length).toBeLessThanOrEqual(8_000); + expect(text.startsWith(`${retainedText}\n…\n`)).toBe(true); + expect(text.endsWith("NEWEST")).toBe(true); + expect(text).not.toMatch( + /(?:[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(? { diff --git a/ui/src/pages/chat/realtime-talk-conversation.ts b/ui/src/pages/chat/realtime-talk-conversation.ts index 04fbd493b041..74c275db500c 100644 --- a/ui/src/pages/chat/realtime-talk-conversation.ts +++ b/ui/src/pages/chat/realtime-talk-conversation.ts @@ -285,9 +285,10 @@ function boundRealtimeConversationText(text: string): string { const hasBoundedPrefix = markerIndex >= CONVERSATION_ENTRY_PREFIX_CHARS - 1 && markerIndex <= CONVERSATION_ENTRY_PREFIX_CHARS; - const prefix = hasBoundedPrefix - ? text.slice(0, markerIndex) - : sliceUtf16Safe(text, 0, CONVERSATION_ENTRY_PREFIX_CHARS); + const prefixEnd = hasBoundedPrefix ? markerIndex : CONVERSATION_ENTRY_PREFIX_CHARS; + // A natural marker can follow malformed provider text ending in a lone high + // surrogate. Keep that code unit out of the retained truncation boundary. + const prefix = sliceUtf16Safe(text, 0, prefixEnd).replace(/[\uD800-\uDBFF]$/, ""); const tailChars = MAX_CONVERSATION_ENTRY_CHARS - prefix.length - CONVERSATION_ENTRY_TRUNCATION_MARKER.length; const tail = sliceUtf16Safe(text, -tailChars);