Skip to content

Commit 67e1d43

Browse files
authored
fix(ui): preserve UTF-16 workshop previews (#104047)
1 parent 7ac8e2e commit 67e1d43

2 files changed

Lines changed: 39 additions & 1 deletion

File tree

ui/src/pages/skill-workshop/skill-workshop-page.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,43 @@ afterEach(() => {
7575
});
7676

7777
describe("SkillWorkshopPage lifecycle", () => {
78+
it("renders truncated Today previews without dangling surrogates", async () => {
79+
const previewText = `${"a".repeat(118)}😀trailing`;
80+
const proposal = {
81+
key: "proposal-utf16-preview",
82+
slug: "proposal-utf16-preview",
83+
name: "UTF-16 preview",
84+
oneLine: "Preview boundary coverage",
85+
body: `## Workflow\n- ${previewText}`,
86+
status: "pending",
87+
version: 1,
88+
createdAt: 0,
89+
updatedAt: 0,
90+
recencyGroup: "today",
91+
ageLabel: "now",
92+
supportFiles: [],
93+
isNew: false,
94+
origin: {
95+
agentId: "research",
96+
sessionKey: "agent:research:proposal-utf16-preview",
97+
},
98+
} satisfies SkillWorkshopProposal;
99+
const loadedState = createSkillWorkshopState();
100+
loadedState.skillWorkshopAgentId = "research";
101+
loadedState.skillWorkshopLoaded = true;
102+
loadedState.skillWorkshopProposals = [proposal];
103+
loadedState.skillWorkshopSelectedKey = proposal.key;
104+
const page = document.createElement(
105+
"openclaw-skill-workshop-page",
106+
) as SkillWorkshopPageTestElement;
107+
page.data = skillWorkshopRouteData(loadedState);
108+
page.context = createContext(vi.fn(async () => ({})));
109+
document.body.append(page);
110+
await page.updateComplete;
111+
112+
expect(page.querySelector(".sw-today__does li")?.textContent).toBe(`${"a".repeat(118)}…`);
113+
});
114+
78115
it("forces a fresh proposal load when the gateway source changes", async () => {
79116
const firstRequest = vi.fn(async () => ({}));
80117
const secondRequest = vi.fn(async () => ({

ui/src/pages/skill-workshop/view.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Control UI view renders skill workshop screen content.
2+
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
23
import { html, nothing } from "lit";
34
import { keyed } from "lit/directives/keyed.js";
45
import { styleMap } from "lit/directives/style-map.js";
@@ -916,7 +917,7 @@ function truncateAtWord(value: string, maxChars: number): string {
916917
if (value.length <= maxChars) {
917918
return value;
918919
}
919-
const clipped = value.slice(0, maxChars - 1);
920+
const clipped = truncateUtf16Safe(value, maxChars - 1);
920921
const boundary = clipped.lastIndexOf(" ");
921922
const base = boundary > 48 ? clipped.slice(0, boundary) : clipped;
922923
return `${base.trimEnd()}…`;

0 commit comments

Comments
 (0)