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 && (
+