From 501a68a69bb70a2cfdaa0d5699e691b765051d3d Mon Sep 17 00:00:00 2001 From: Alec Hrdina Date: Sat, 18 Apr 2026 00:29:31 -0500 Subject: [PATCH] fix(ui): ignore non-image transcript media paths --- ui/src/ui/chat/grouped-render.ts | 38 +++++++++++++++++++++++++++++++- ui/src/ui/views/chat.test.ts | 25 +++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/ui/src/ui/chat/grouped-render.ts b/ui/src/ui/chat/grouped-render.ts index a5d4682f23e..a03467ec5f1 100644 --- a/ui/src/ui/chat/grouped-render.ts +++ b/ui/src/ui/chat/grouped-render.ts @@ -64,6 +64,34 @@ function buildBase64ImageUrl(params: { data: string; mediaType?: string }): stri : `data:${params.mediaType ?? "image/png"};base64,${params.data}`; } +function getFileExtension(url: string): string | undefined { + const source = (() => { + try { + const trimmed = url.trim(); + if (/^https?:\/\//i.test(trimmed)) { + return new URL(trimmed).pathname; + } + } catch { + // Fall back to the raw path when URL parsing fails. + } + return url; + })(); + const fileName = source.split(/[\\/]/).pop() ?? source; + const match = /\.([a-zA-Z0-9]+)$/.exec(fileName); + return match?.[1]?.toLowerCase(); +} + +function isImageTranscriptMediaPath(path: string, mediaType: unknown): boolean { + if (typeof mediaType === "string" && mediaType.trim()) { + return mediaType.trim().toLowerCase().startsWith("image/"); + } + const ext = getFileExtension(path); + return ( + ext !== undefined && + ["png", "jpg", "jpeg", "gif", "webp", "bmp", "svg", "heic", "heif", "avif"].includes(ext) + ); +} + function extractImages(message: unknown): ImageBlock[] { const m = message as Record; const content = m.content; @@ -116,7 +144,15 @@ function extractImages(message: unknown): ImageBlock[] { : typeof m.MediaPath === "string" ? [m.MediaPath] : []; - for (const mediaPath of transcriptMediaPaths) { + const transcriptMediaTypes = Array.isArray(m.MediaTypes) + ? m.MediaTypes + : typeof m.MediaType === "string" + ? [m.MediaType] + : []; + for (const [index, mediaPath] of transcriptMediaPaths.entries()) { + if (!isImageTranscriptMediaPath(mediaPath, transcriptMediaTypes[index])) { + continue; + } appendImageBlock(images, { url: mediaPath }); } diff --git a/ui/src/ui/views/chat.test.ts b/ui/src/ui/views/chat.test.ts index 78d69bc4896..0fa2b25c505 100644 --- a/ui/src/ui/views/chat.test.ts +++ b/ui/src/ui/views/chat.test.ts @@ -946,6 +946,31 @@ describe("chat view", () => { ); }); + it("skips non-image transcript media paths after history reload", () => { + const container = document.createElement("div"); + + renderGroupedMessage( + container, + { + id: "user-history-document", + role: "user", + content: "", + MediaPath: "/tmp/openclaw/user-upload.pdf", + MediaType: "application/pdf", + timestamp: Date.now(), + }, + "user", + { + showToolCalls: false, + basePath: "/openclaw", + assistantAttachmentAuthToken: "session-token", + localMediaPreviewRoots: ["/tmp/openclaw"], + }, + ); + + expect(container.querySelector(".chat-message-image")).toBeNull(); + }); + it("opens only safe assistant image URLs in a hardened new tab", () => { const container = document.createElement("div"); const openSpy = vi.spyOn(window, "open").mockReturnValue(null);