From bd05303fed20c04b8cc97090f6d87205e452fc3a Mon Sep 17 00:00:00 2001 From: roboomp Date: Sun, 28 Jun 2026 04:56:18 +0000 Subject: [PATCH] fix(coding-agent): routed custom images as user content Split image-bearing custom messages into developer text plus user image content before provider conversion so queued skill images stay out of developer slots. --- packages/coding-agent/src/session/messages.ts | 31 ++++++++++++++++++- .../test/session-messages.test.ts | 28 +++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/src/session/messages.ts b/packages/coding-agent/src/session/messages.ts index ce4e23a0f..bf1731e35 100644 --- a/packages/coding-agent/src/session/messages.ts +++ b/packages/coding-agent/src/session/messages.ts @@ -481,6 +481,30 @@ export function sanitizeRehydratedOpenAIResponsesAssistantMessage(message: Assis }; } +function convertImageBearingCustomMessage(message: CustomMessage | HookMessage): Message[] | undefined { + if (typeof message.content === "string") return undefined; + const textBlocks = message.content.filter((content): content is TextContent => content.type === "text"); + const imageBlocks = message.content.filter((content): content is ImageContent => content.type === "image"); + if (imageBlocks.length === 0) return undefined; + + const converted: Message[] = []; + if (textBlocks.length > 0) { + converted.push({ + role: "developer", + content: textBlocks, + attribution: message.attribution, + timestamp: message.timestamp, + }); + } + converted.push({ + role: "user", + content: [{ type: "text", text: `Images attached to ${message.customType}.` }, ...imageBlocks], + attribution: message.attribution, + timestamp: message.timestamp, + }); + return converted; +} + /** * Transform AgentMessages (including custom types) to LLM-compatible Messages. * @@ -556,7 +580,12 @@ export function convertToLlm(messages: AgentMessage[]): Message[] { return out; } case "custom": - case "hookMessage": + case "hookMessage": { + const split = convertImageBearingCustomMessage(m); + if (split) return split; + const converted = convertMessageToLlm(m); + return converted ? [converted] : []; + } case "branchSummary": case "compactionSummary": case "user": diff --git a/packages/coding-agent/test/session-messages.test.ts b/packages/coding-agent/test/session-messages.test.ts index 27b0fcfde..d0e17c6f0 100644 --- a/packages/coding-agent/test/session-messages.test.ts +++ b/packages/coding-agent/test/session-messages.test.ts @@ -259,6 +259,34 @@ describe("convertToLlm custom message mapping", () => { expectAttribution(converted[0], "user"); expect(inferCopilotInitiator(converted)).toBe("user"); }); + + it("routes custom-message images through a user message", () => { + const image: ImageContent = { type: "image", data: "c2tpbGw=", mimeType: "image/png" }; + const messages: AgentMessage[] = [ + { + role: "custom", + customType: "skill-prompt", + content: [{ type: "text", text: "Skill body" }, image], + display: true, + attribution: "user", + timestamp: Date.now(), + }, + ]; + + const converted = convertToLlm(messages); + + expect(converted.map(message => message.role)).toEqual(["developer", "user"]); + expectAttribution(converted[0], "user"); + expectAttribution(converted[1], "user"); + if (converted[0]?.role !== "developer" || !Array.isArray(converted[0].content)) { + throw new Error("Expected developer skill text"); + } + expect(converted[0].content).toEqual([{ type: "text", text: "Skill body" }]); + if (converted[1]?.role !== "user" || !Array.isArray(converted[1].content)) { + throw new Error("Expected user skill images"); + } + expect(converted[1].content.filter(content => content.type === "image")).toEqual([image]); + }); }); function getUserText(message: AgentMessage | undefined): string {