mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(ui): decode artifact previews as UTF-8 (#117657)
Co-authored-by: Peter Steinberger <steipete@macos.shared>
This commit is contained in:
committed by
GitHub
parent
1becbdcb60
commit
12a8d80873
@@ -62,6 +62,95 @@ describe("custodian panel toggle", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("session workspace artifacts", () => {
|
||||
function createArtifactHost(params: { data: string; mimeType: string; title?: string }) {
|
||||
const handleOpenSidebar = vi.fn();
|
||||
const request = vi.fn().mockResolvedValue({
|
||||
artifact: {
|
||||
id: "artifact-1",
|
||||
mimeType: params.mimeType,
|
||||
title: params.title ?? "Unicode artifact",
|
||||
},
|
||||
data: params.data,
|
||||
encoding: "base64",
|
||||
});
|
||||
const state = {
|
||||
client: { request },
|
||||
connected: true,
|
||||
handleOpenSidebar,
|
||||
hello: gatewayHello([]),
|
||||
sessionKey: "agent:main:current",
|
||||
sessions: {},
|
||||
} as unknown as SessionWorkspaceHost;
|
||||
return { handleOpenSidebar, request, state };
|
||||
}
|
||||
|
||||
it.each([
|
||||
{
|
||||
content: "Résumé 東京 🦀",
|
||||
fence: "```",
|
||||
mimeType: "text/plain",
|
||||
},
|
||||
{
|
||||
content: JSON.stringify({ message: "Résumé 東京 🦀" }),
|
||||
fence: "```json",
|
||||
mimeType: "application/json",
|
||||
},
|
||||
])(
|
||||
"decodes UTF-8 $mimeType artifacts without corrupting visible or raw text",
|
||||
async (testCase) => {
|
||||
const data = btoa(String.fromCharCode(...new TextEncoder().encode(testCase.content)));
|
||||
const { handleOpenSidebar, state } = createArtifactHost({
|
||||
data,
|
||||
mimeType: testCase.mimeType,
|
||||
});
|
||||
|
||||
createSessionWorkspaceProps(state).onOpenArtifact("artifact-1");
|
||||
|
||||
await vi.waitFor(() => expect(handleOpenSidebar).toHaveBeenCalledOnce());
|
||||
expect(handleOpenSidebar.mock.calls[0]?.[0]).toEqual({
|
||||
kind: "markdown",
|
||||
content: `# Unicode artifact\n\n${testCase.fence}\n${testCase.content}\n\`\`\``,
|
||||
rawText: testCase.content,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("preserves inline image artifacts as their original base64 data URLs", async () => {
|
||||
const data = "iVBORw0KGgo=";
|
||||
const { handleOpenSidebar, state } = createArtifactHost({
|
||||
data,
|
||||
mimeType: "image/png",
|
||||
title: "preview.png",
|
||||
});
|
||||
|
||||
createSessionWorkspaceProps(state).onOpenArtifact("artifact-1");
|
||||
|
||||
await vi.waitFor(() => expect(handleOpenSidebar).toHaveBeenCalledOnce());
|
||||
expect(handleOpenSidebar.mock.calls[0]?.[0]).toEqual({
|
||||
kind: "image",
|
||||
mimeType: "image/png",
|
||||
rawText: null,
|
||||
src: `data:image/png;base64,${data}`,
|
||||
title: "preview.png",
|
||||
});
|
||||
});
|
||||
|
||||
it("reports malformed base64 artifact data as a visible workspace error", async () => {
|
||||
const { handleOpenSidebar, state } = createArtifactHost({
|
||||
data: "not-base64!",
|
||||
mimeType: "text/plain",
|
||||
});
|
||||
|
||||
createSessionWorkspaceProps(state).onOpenArtifact("artifact-1");
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(createSessionWorkspaceProps(state).error).toMatch(/InvalidCharacterError|invalid/i),
|
||||
);
|
||||
expect(handleOpenSidebar).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("openSessionWorkspaceFile", () => {
|
||||
it("opens Markdown with a canonical Gateway- and pane-scoped draft identity", async () => {
|
||||
const handleOpenSidebar = vi.fn();
|
||||
|
||||
@@ -281,19 +281,17 @@ function artifactSidebarContent(params: {
|
||||
rawText: url ?? null,
|
||||
};
|
||||
}
|
||||
if (encoding === "base64" && data && mimeType === "application/json") {
|
||||
const decoded = globalThis.atob(data);
|
||||
if (
|
||||
encoding === "base64" &&
|
||||
data &&
|
||||
(mimeType === "application/json" || mimeType.startsWith("text/"))
|
||||
) {
|
||||
const bytes = Uint8Array.from(globalThis.atob(data), (char) => char.charCodeAt(0));
|
||||
const decoded = new TextDecoder().decode(bytes);
|
||||
const language = mimeType === "application/json" ? "json" : "";
|
||||
return {
|
||||
kind: "markdown",
|
||||
content: `# ${title}\n\n\`\`\`json\n${decoded}\n\`\`\``,
|
||||
rawText: decoded,
|
||||
};
|
||||
}
|
||||
if (encoding === "base64" && data && mimeType.startsWith("text/")) {
|
||||
const decoded = globalThis.atob(data);
|
||||
return {
|
||||
kind: "markdown",
|
||||
content: `# ${title}\n\n\`\`\`\n${decoded}\n\`\`\``,
|
||||
content: `# ${title}\n\n\`\`\`${language}\n${decoded}\n\`\`\``,
|
||||
rawText: decoded,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user