From e9df2d61b4052d5b5911281b8d14827e8d634028 Mon Sep 17 00:00:00 2001 From: Antisophy <293439221+Antisophy@users.noreply.github.com> Date: Fri, 31 Jul 2026 18:08:19 -0700 Subject: [PATCH] fix(web): show attachments in session-start framed messages The first message of a task renders through the SystemUserMessage framing (session-start label, body variable, "Full message" expander), which extracted only the text blocks from the message content. Image blocks were dropped, so an attachment on the first message appeared while the optimistic plain user message was showing, then vanished when the framed canonical message replaced it on replay. The image itself was fine: the backend keeps attachment blocks beside the rendered prompt and the model receives them; only the framed rendering lost them. Render the image blocks in both framed branches, above the prompt text in each (below the label header), with the same markup and ordering the plain user message uses: attachments first, then text. --- web/src/components/MessageList.test.tsx | 39 +++++++++++++++++++++++++ web/src/components/MessageList.tsx | 24 +++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/web/src/components/MessageList.test.tsx b/web/src/components/MessageList.test.tsx index fa06a5f..d2ac203 100644 --- a/web/src/components/MessageList.test.tsx +++ b/web/src/components/MessageList.test.tsx @@ -455,6 +455,45 @@ describe("MessageList task diagnostics", () => { expect(html).toContain("The latest turn could not be loaded."); }); + it("renders attachments inside a session-start framed message", () => { + const framed: DisplayMessage = { + id: "cydo-start-1", + uuid: "start-uuid-1", + type: "user", + isMeta: true, + content: [ + { type: "text", text: "SYSTEM rendered prompt body" }, + { type: "image", data: "aGVsbG8=", media_type: "image/png" }, + ], + cydoMeta: { + label: "Session start: blank", + vars: { task_description: "what is in this picture?" }, + bodyVar: "task_description", + }, + }; + + const html = renderToString( + , + ); + + // the framed view must keep the attachment visible, not just the text: + // losing it on replay made a briefly-shown image vanish from the task + expect(html).toContain("user-image"); + expect(html).toContain("data:image/png;base64,aGVsbG8="); + expect(html).toContain("what is in this picture?"); + // attachment above the prompt, matching plain user messages + expect(html.indexOf("data:image/png")).toBeLessThan( + html.indexOf("what is in this picture?"), + ); + }); + it("keeps a metadata-bearing CyDo nudge editable", () => { const nudge: DisplayMessage = { id: "cydo-nudge-1", diff --git a/web/src/components/MessageList.tsx b/web/src/components/MessageList.tsx index fd805fe..9421e33 100644 --- a/web/src/components/MessageList.tsx +++ b/web/src/components/MessageList.tsx @@ -218,6 +218,28 @@ function SystemUserMessage({ message }: { message: DisplayMessage }) { .map((b) => b.text) .join("\n"); + // Attachments survive the session-start framing (the backend keeps image + // blocks beside the rendered prompt), so they must survive its rendering + // too; the optimistic plain user message showed them, and losing them on + // replay looked like the attachment vanished. + const imageBlocks = message.content.filter( + (b): b is { type: "image"; data: string; media_type: string } => + b.type === "image" && + typeof (b as Record).data === "string", + ); + const images = imageBlocks.length > 0 && ( +
+ {imageBlocks.map((img, i) => ( + User attached image + ))} +
+ ); + const hasVars = meta.vars && Object.keys(meta.vars).length > 0; if (!hasVars) { @@ -262,6 +284,7 @@ function SystemUserMessage({ message }: { message: DisplayMessage }) { }} >
{meta.label}
+ {images}
{text}
); @@ -297,6 +320,7 @@ function SystemUserMessage({ message }: { message: DisplayMessage }) { {meta.label} + {images} {bodyValue !== undefined && (
{meta.bodyMarkdown ? (