diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index 8db93a1..4fea0c4 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -703,7 +703,7 @@ function buildPromptItems(prompt: acp.ContentBlock[]): UserInput[] { case "text": return {type: "text", text: block.text, text_elements: []}; case "image": { - const url = block.uri ?? `data:${block.mimeType};base64,${block.data}`; + const url = isSupportedImageUrl(block.uri) ? block.uri : imageDataUrl(block); return {type: "image", url}; } case "resource_link": @@ -729,10 +729,26 @@ function buildPromptItems(prompt: acp.ContentBlock[]): UserInput[] { }).filter((block): block is UserInput => block !== null); } +function imageDataUrl(block: acp.ContentBlock & { type: "image" }): string { + return `data:${block.mimeType};base64,${block.data}`; +} + function isImageMimeType(mimeType: string | null | undefined): mimeType is string { return mimeType?.startsWith("image/") ?? false; } +function isSupportedImageUrl(uri: string | null | undefined): uri is string { + if (!uri) { + return false; + } + try { + const protocol = new URL(uri).protocol; + return protocol === "http:" || protocol === "https:" || protocol === "data:"; + } catch { + return false; + } +} + function formatUriAsLink(name: string | null | undefined, uri: string): string { if (name && name.length > 0) { return `[@${name}](${uri})`; diff --git a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts index 4e0aaa2..2edecec 100644 --- a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts +++ b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts @@ -1412,6 +1412,62 @@ describe('ACP server test', { timeout: 40_000 }, () => { })); }); + it ('should use inline image data for internal image URIs', async () => { + const { mockFixture, turnStartSpy } = setupPromptFixture({ + supportedInputModalities: ["text", "image"], + }); + + const prompt: acp.ContentBlock[] = [ + { type: "text", text: "Hello" }, + { type: "image", mimeType: "image/png", data: "abc123", uri: "zed:///agent/pasted-image?name=Image" }, + ]; + + await mockFixture.getCodexAcpAgent().prompt({ sessionId: "id", prompt }); + + expect(turnStartSpy).toHaveBeenCalledWith(expect.objectContaining({ + input: [ + { type: "text", text: "Hello", text_elements: [] }, + { type: "image", url: "data:image/png;base64,abc123" }, + ] + })); + }); + + it ('should use inline image data for local file image URIs', async () => { + const { mockFixture, turnStartSpy } = setupPromptFixture({ + supportedInputModalities: ["text", "image"], + }); + + const prompt: acp.ContentBlock[] = [ + { type: "image", mimeType: "image/png", data: "abc123", uri: "file:///Users/test/Desktop/Screenshot%201.png" }, + ]; + + await mockFixture.getCodexAcpAgent().prompt({ sessionId: "id", prompt }); + + expect(turnStartSpy).toHaveBeenCalledWith(expect.objectContaining({ + input: [ + { type: "image", url: "data:image/png;base64,abc123" }, + ] + })); + }); + + it ('should preserve data image URLs', async () => { + const { mockFixture, turnStartSpy } = setupPromptFixture({ + supportedInputModalities: ["text", "image"], + }); + + const prompt: acp.ContentBlock[] = [ + { type: "image", mimeType: "image/png", data: "fallback", uri: "data:image/png;base64,abc123" }, + ]; + + await mockFixture.getCodexAcpAgent().prompt({ sessionId: "id", prompt }); + + expect(turnStartSpy).toHaveBeenCalledWith(expect.objectContaining({ + input: [ + { type: "image", url: "data:image/png;base64,abc123" }, + ] + })); + }); + it ('should show rate limits from multiple sources in status', async () => { const rateLimits: RateLimitsMap = new Map(); rateLimits.set("limit-1", {