mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
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 <steipete@gmail.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user