1/**
2 * Longest edge, in pixels, a PNG or JPEG attachment keeps. Anthropic rejects an image over
3 * 2000px on a side once a request holds more than 20 images, and Claude Code forwards images
4 * sent into a running turn unchanged, so one large paste breaks every later request in that
5 * thread. Web and mobile scale attachments down to this edge when they are attached.
6 */
7export const IMAGE_ATTACHMENT_MAX_EDGE = 2000;
8
9/** Formats a client re-encodes in place. GIF, WebP, and SVG are left as attached. */
10export type DownscalableImageMimeType = "image/png" | "image/jpeg";
11
12export function downscalableImageMimeType(mimeType: string): DownscalableImageMimeType | null {
13 const normalized = mimeType.toLowerCase();
14 if (normalized === "image/png") return "image/png";
15 if (normalized === "image/jpeg" || normalized === "image/jpg") return "image/jpeg";
16 return null;
17}
18
19/**
20 * The size an image should be scaled to so its long edge fits `IMAGE_ATTACHMENT_MAX_EDGE`,
21 * keeping its aspect ratio. Returns null when it already fits.
22 */
23export function fitImageAttachmentSize(size: {
24 readonly width: number;
25 readonly height: number;
26}): { width: number; height: number } | null {
27 const longestEdge = Math.max(size.width, size.height);
28 if (longestEdge <= IMAGE_ATTACHMENT_MAX_EDGE) return null;
29 const scale = IMAGE_ATTACHMENT_MAX_EDGE / longestEdge;
30 return {
31 width: Math.max(1, Math.min(IMAGE_ATTACHMENT_MAX_EDGE, Math.round(size.width * scale))),
32 height: Math.max(1, Math.min(IMAGE_ATTACHMENT_MAX_EDGE, Math.round(size.height * scale))),
33 };
34}