diff --git a/docs/tools/diffs.md b/docs/tools/diffs.md index 3f19fae586a..90da207e234 100644 --- a/docs/tools/diffs.md +++ b/docs/tools/diffs.md @@ -145,6 +145,7 @@ Compatibility note: `defaults.format` is accepted as an alias for `defaults.file - `mode: "file"` uses a faster file-only render path and does not create a viewer URL. - File quality presets include hard pixel caps to prevent runaway renders on very large diffs. - PNG or PDF rendering requires a Chromium-compatible browser. If auto-detection is not enough, set `browser.executablePath`. +- If your channel compresses images aggressively (for example Telegram or WhatsApp), prefer `fileFormat: "pdf"` to preserve diff readability. - Diff rendering is powered by [Diffs](https://diffs.com). ## Related docs diff --git a/extensions/diffs/README.md b/extensions/diffs/README.md index 62bec10017e..3a385ee5c2c 100644 --- a/extensions/diffs/README.md +++ b/extensions/diffs/README.md @@ -117,7 +117,7 @@ After: This is version two. ``` -Render a PNG: +Render a file (PNG or PDF): ```text Use the `diffs` tool in `file` mode for this before and after input. After it returns `details.filePath`, use the `message` tool with `path` or `filePath` to send me the rendered diff image. @@ -165,4 +165,5 @@ diff --git a/src/example.ts b/src/example.ts - The viewer is hosted locally through the gateway under `/plugins/diffs/...`. - Artifacts are ephemeral and stored in the local temp directory. - PNG/PDF rendering requires a Chromium-compatible browser. Set `browser.executablePath` if auto-detection is not enough. +- If your delivery channel compresses images heavily (for example Telegram or WhatsApp), prefer `fileFormat: "pdf"` to preserve readability. - Diff rendering is powered by [Diffs](https://diffs.com). diff --git a/extensions/diffs/src/browser.ts b/extensions/diffs/src/browser.ts index 96402fdf2ef..a4956de99fe 100644 --- a/extensions/diffs/src/browser.ts +++ b/extensions/diffs/src/browser.ts @@ -8,6 +8,7 @@ import { VIEWER_ASSET_PREFIX, getServedViewerAsset } from "./viewer-assets.js"; const DEFAULT_BROWSER_IDLE_MS = 30_000; const SHARED_BROWSER_KEY = "__default__"; +const IMAGE_SIZE_LIMIT_ERROR = "Diff frame did not render within image size limits."; export type DiffScreenshotter = { screenshotHtml(params: { @@ -237,8 +238,11 @@ export class PlaywrightDiffScreenshotter implements DiffScreenshotter { }); return params.outputPath; } - throw new Error("Diff frame did not render within image size limits."); + throw new Error(IMAGE_SIZE_LIMIT_ERROR); } catch (error) { + if (error instanceof Error && error.message === IMAGE_SIZE_LIMIT_ERROR) { + throw error; + } const reason = error instanceof Error ? error.message : String(error); throw new Error( `Diff PNG/PDF rendering requires a Chromium-compatible browser. Set browser.executablePath or install Chrome/Chromium. ${reason}`, diff --git a/extensions/diffs/src/tool.test.ts b/extensions/diffs/src/tool.test.ts index 2e55ba38493..7e0de7d5318 100644 --- a/extensions/diffs/src/tool.test.ts +++ b/extensions/diffs/src/tool.test.ts @@ -206,6 +206,42 @@ describe("diffs tool", () => { expect((result?.details as Record).fileMaxWidth).toBe(1100); }); + it("accepts deprecated format alias for fileFormat", async () => { + const screenshotter = { + screenshotHtml: vi.fn( + async ({ + outputPath, + image, + }: { + outputPath: string; + image: { format: string; qualityPreset: string; scale: number; maxWidth: number }; + }) => { + expect(image.format).toBe("pdf"); + await fs.mkdir(path.dirname(outputPath), { recursive: true }); + await fs.writeFile(outputPath, Buffer.from("%PDF-1.7")); + return outputPath; + }, + ), + }; + + const tool = createDiffsTool({ + api: createApi(), + store, + defaults: DEFAULT_DIFFS_TOOL_DEFAULTS, + screenshotter, + }); + + const result = await tool.execute?.("tool-2format", { + before: "one\n", + after: "two\n", + mode: "file", + format: "pdf", + }); + + expect((result?.details as Record).fileFormat).toBe("pdf"); + expect((result?.details as Record).filePath).toMatch(/preview\.pdf$/); + }); + it("honors defaults.mode=file when mode is omitted", async () => { const screenshotter = { screenshotHtml: vi.fn(async ({ outputPath }: { outputPath: string }) => {