Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions web/src/components/MessageList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<MessageList
taskTid={1}
messages={[framed]}
blocks={new Map()}
replacementEvents={new Map()}
isProcessing={false}
bandStatus=""
/>,
);

// 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",
Expand Down
24 changes: 24 additions & 0 deletions web/src/components/MessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>).data === "string",
);
const images = imageBlocks.length > 0 && (
<div class="user-images">
{imageBlocks.map((img, i) => (
<img
key={i}
src={`data:${img.media_type};base64,${img.data}`}
alt="User attached image"
class="user-image"
/>
))}
</div>
);

const hasVars = meta.vars && Object.keys(meta.vars).length > 0;

if (!hasVars) {
Expand Down Expand Up @@ -262,6 +284,7 @@ function SystemUserMessage({ message }: { message: DisplayMessage }) {
}}
>
<div class="system-user-header">{meta.label}</div>
{images}
<pre class="system-user-pre">{text}</pre>
</div>
);
Expand Down Expand Up @@ -297,6 +320,7 @@ function SystemUserMessage({ message }: { message: DisplayMessage }) {
</svg>
{meta.label}
</div>
{images}
{bodyValue !== undefined && (
<div class="system-user-body">
{meta.bodyMarkdown ? (
Expand Down