diff --git a/CHANGELOG.md b/CHANGELOG.md index 228d580b7..aadfe9650 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,7 @@ - macOS: log health refresh failures and recovery to make gateway issues easier to diagnose. - macOS codesign: skip hardened runtime for ad-hoc signing and avoid empty options args (#70) — thanks @petter-b - macOS codesign: include camera entitlement so permission prompts work in the menu bar app. +- Agent tools: map `camera.snap` JPEG payloads to `image/jpeg` to avoid MIME mismatch errors. - macOS packaging: move rpath config into swift build for reliability (#69) — thanks @petter-b - macOS: prioritize main bundle for device resources to prevent crash (#73) — thanks @petter-b - macOS remote: route settings through gateway config and avoid local config reads in remote mode. diff --git a/src/agents/clawdis-tools.ts b/src/agents/clawdis-tools.ts index 2b3a37647..65b776b0e 100644 --- a/src/agents/clawdis-tools.ts +++ b/src/agents/clawdis-tools.ts @@ -43,7 +43,7 @@ import { parseDurationMs } from "../cli/parse-duration.js"; import { loadConfig } from "../config/config.js"; import { reactMessageDiscord } from "../discord/send.js"; import { callGateway } from "../gateway/call.js"; -import { detectMime } from "../media/mime.js"; +import { detectMime, imageMimeFromFormat } from "../media/mime.js"; import { sanitizeToolResultImages } from "./tool-images.js"; // biome-ignore lint/suspicious/noExplicitAny: TypeBox schema type from pi-ai uses a different module instance. @@ -875,7 +875,7 @@ function createCanvasTool(): AnyAgentTool { }); await writeBase64ToFile(filePath, payload.base64); const mimeType = - payload.format === "jpeg" ? "image/jpeg" : "image/png"; + imageMimeFromFormat(payload.format) ?? "image/png"; return await imageResult({ label: "canvas:snapshot", path: filePath, @@ -1141,7 +1141,8 @@ function createNodesTool(): AnyAgentTool { content.push({ type: "image", data: payload.base64, - mimeType: payload.format === "jpeg" ? "image/jpeg" : "image/png", + mimeType: + imageMimeFromFormat(payload.format) ?? "image/png", }); details.push({ facing, diff --git a/src/media/mime.test.ts b/src/media/mime.test.ts index 3eeb72a68..425a36a6b 100644 --- a/src/media/mime.test.ts +++ b/src/media/mime.test.ts @@ -1,7 +1,7 @@ import JSZip from "jszip"; import { describe, expect, it } from "vitest"; -import { detectMime } from "./mime.js"; +import { detectMime, imageMimeFromFormat } from "./mime.js"; async function makeOoxmlZip(opts: { mainMime: string; @@ -17,6 +17,15 @@ async function makeOoxmlZip(opts: { } describe("mime detection", () => { + it("maps common image formats to mime types", () => { + expect(imageMimeFromFormat("jpg")).toBe("image/jpeg"); + expect(imageMimeFromFormat("jpeg")).toBe("image/jpeg"); + expect(imageMimeFromFormat("png")).toBe("image/png"); + expect(imageMimeFromFormat("webp")).toBe("image/webp"); + expect(imageMimeFromFormat("gif")).toBe("image/gif"); + expect(imageMimeFromFormat("unknown")).toBeUndefined(); + }); + it("detects docx from buffer", async () => { const buf = await makeOoxmlZip({ mainMime: diff --git a/src/media/mime.ts b/src/media/mime.ts index 0f6bf057c..d26cfb969 100644 --- a/src/media/mime.ts +++ b/src/media/mime.ts @@ -107,6 +107,25 @@ export function extensionForMime(mime?: string | null): string | undefined { return EXT_BY_MIME[mime.toLowerCase()]; } +export function imageMimeFromFormat( + format?: string | null, +): string | undefined { + if (!format) return undefined; + switch (format.toLowerCase()) { + case "jpg": + case "jpeg": + return "image/jpeg"; + case "png": + return "image/png"; + case "webp": + return "image/webp"; + case "gif": + return "image/gif"; + default: + return undefined; + } +} + export function kindFromMime(mime?: string | null): MediaKind { return mediaKindFromMime(mime); }