From 9bf212765f665cd3d741e6f66ede5abc420385d2 Mon Sep 17 00:00:00 2001 From: maweibin Date: Thu, 9 Jul 2026 17:42:34 +0800 Subject: [PATCH] fix(ui): keep workboard capture text truncation UTF-16 safe (#102544) * fix(ui): keep workboard capture text truncation UTF-16 safe Replace raw .slice(0, N - 3) with truncateUtf16Safe in clampSessionCaptureText and clampSessionCaptureTitle to prevent lone surrogates when emoji cross the truncation boundary. Completes the UTF-16 hardening started in #101685, which added the import but only covered buildCardSessionLabel. * test(ui): cover workboard UTF-16 boundaries * docs(changelog): note Workboard UTF-16 fix * docs(changelog): leave release notes to release flow --------- Co-authored-by: Peter Steinberger --- ui/src/lib/workboard/index.ts | 4 ++-- ui/src/pages/workboard/data.test.ts | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/ui/src/lib/workboard/index.ts b/ui/src/lib/workboard/index.ts index 40db3b4087d5..62eff7992841 100644 --- a/ui/src/lib/workboard/index.ts +++ b/ui/src/lib/workboard/index.ts @@ -2909,7 +2909,7 @@ function clampSessionCaptureText(value: string): string { if (compact.length <= SESSION_CAPTURE_TEXT_MAX_CHARS) { return compact; } - return `${compact.slice(0, SESSION_CAPTURE_TEXT_MAX_CHARS - 3).trimEnd()}...`; + return `${truncateUtf16Safe(compact, SESSION_CAPTURE_TEXT_MAX_CHARS - 3).trimEnd()}...`; } function clampSessionCaptureTitle(value: string): string { @@ -2917,7 +2917,7 @@ function clampSessionCaptureTitle(value: string): string { if (compact.length <= WORKBOARD_CAPTURE_TITLE_MAX_CHARS) { return compact; } - return `${compact.slice(0, WORKBOARD_CAPTURE_TITLE_MAX_CHARS - 3).trimEnd()}...`; + return `${truncateUtf16Safe(compact, WORKBOARD_CAPTURE_TITLE_MAX_CHARS - 3).trimEnd()}...`; } function sessionTitle(session: GatewaySessionRow, recentUserText: string | null): string { diff --git a/ui/src/pages/workboard/data.test.ts b/ui/src/pages/workboard/data.test.ts index 779a0ff76212..bc3a3ef24fb3 100644 --- a/ui/src/pages/workboard/data.test.ts +++ b/ui/src/pages/workboard/data.test.ts @@ -3440,18 +3440,21 @@ describe("workboard controller", () => { ); }); - it("clamps long session labels before creating captured cards", async () => { + it("clamps captured session fields without splitting surrogate pairs", async () => { const host = {}; - const longLabel = "x".repeat(220); + const titlePrefix = "x".repeat(176); + const textPrefix = "y".repeat(696); const client = createClient((method) => { if (method === "workboard.cards.list") { return { cards: [], statuses: ["todo"] }; } if (method === "chat.history") { - return { messages: [] }; + return { + messages: [{ role: "user", content: [{ type: "text", text: `${textPrefix}😀tail` }] }], + }; } if (method === "workboard.cards.create") { - return { card: { ...sampleCard, title: `${"x".repeat(177)}...` } }; + return { card: { ...sampleCard, title: `${titlePrefix}...` } }; } return {}; }); @@ -3459,14 +3462,17 @@ describe("workboard controller", () => { await captureSessionToWorkboard({ host, client: client as never, - session: { ...sampleSession, label: longLabel }, + session: { ...sampleSession, label: `${titlePrefix}😀tail` }, }); expect(client.request).toHaveBeenNthCalledWith( 3, "workboard.cards.create", expect.objectContaining({ - title: `${"x".repeat(177)}...`, + title: `${titlePrefix}...`, + notes: [`Session: ${sampleSession.key}`, "", `Recent user prompt: ${textPrefix}...`].join( + "\n", + ), }), ); });