diff --git a/extensions/diffs/src/browser.test.ts b/extensions/diffs/src/browser.test.ts index 48ff91b40..e23dec9e7 100644 --- a/extensions/diffs/src/browser.test.ts +++ b/extensions/diffs/src/browser.test.ts @@ -58,6 +58,12 @@ describe("PlaywrightDiffScreenshotter", () => { expect(launchMock).toHaveBeenCalledTimes(1); expect(browser.newPage).toHaveBeenCalledTimes(2); + expect(browser.newPage).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + deviceScaleFactor: 2, + }), + ); expect(pages).toHaveLength(2); expect(pages[0]?.close).toHaveBeenCalledTimes(1); expect(pages[1]?.close).toHaveBeenCalledTimes(1); diff --git a/extensions/diffs/src/browser.ts b/extensions/diffs/src/browser.ts index 0ca661d7e..c5a8b38c1 100644 --- a/extensions/diffs/src/browser.ts +++ b/extensions/diffs/src/browser.ts @@ -122,15 +122,29 @@ export class PlaywrightDiffScreenshotter implements DiffScreenshotter { throw new Error("Diff frame was lost after resizing."); } + const dpr = await page.evaluate(() => window.devicePixelRatio || 1); + + // Raw clip in CSS px + const rawX = Math.max(box.x - padding, 0); + const rawY = Math.max(box.y - padding, 0); + const rawRight = rawX + clipWidth; + const rawBottom = rawY + clipHeight; + + // Snap to device-pixel grid to avoid soft text from sub-pixel crop + const x = Math.floor(rawX * dpr) / dpr; + const y = Math.floor(rawY * dpr) / dpr; + const right = Math.ceil(rawRight * dpr) / dpr; + const bottom = Math.ceil(rawBottom * dpr) / dpr; + await page.screenshot({ path: params.outputPath, type: "png", scale: "device", clip: { - x: Math.max(box.x - padding, 0), - y: Math.max(box.y - padding, 0), - width: clipWidth, - height: clipHeight, + x, + y, + width: right - x, + height: bottom - y, }, }); return params.outputPath; @@ -233,7 +247,7 @@ async function acquireSharedBrowser(params: { .launch({ headless: true, ...(executablePath ? { executablePath } : {}), - args: ["--disable-dev-shm-usage", "--disable-gpu"], + args: ["--disable-dev-shm-usage"], }) .then((browser) => { if (sharedBrowserState?.browserPromise === browserPromise) { diff --git a/extensions/diffs/src/render.test.ts b/extensions/diffs/src/render.test.ts index e9b7f764a..b0ce38cc8 100644 --- a/extensions/diffs/src/render.test.ts +++ b/extensions/diffs/src/render.test.ts @@ -24,6 +24,9 @@ describe("renderDiffDocument", () => { expect(rendered.html).toContain("/plugins/diffs/assets/viewer.js"); expect(rendered.imageHtml).not.toContain("/plugins/diffs/assets/viewer.js"); expect(rendered.imageHtml).toContain('data-openclaw-diffs-ready="true"'); + expect(rendered.imageHtml).toContain("max-width: 960px;"); + expect(rendered.imageHtml).toContain("--diffs-font-size: 16px;"); + expect(rendered.html).toContain("--diffs-font-size: 15px;"); expect(rendered.html).not.toContain("fonts.googleapis.com"); }); diff --git a/extensions/diffs/src/render.ts b/extensions/diffs/src/render.ts index a6ebd29d9..53de0c0a8 100644 --- a/extensions/diffs/src/render.ts +++ b/extensions/diffs/src/render.ts @@ -136,6 +136,16 @@ function buildDiffOptions(options: DiffRenderOptions): DiffViewerOptions { }; } +function buildImageRenderOptions(options: DiffRenderOptions): DiffRenderOptions { + return { + ...options, + presentation: { + ...options.presentation, + fontSize: Math.max(16, options.presentation.fontSize), + }, + }; +} + function normalizeSupportedLanguage(value?: string): SupportedLanguages | undefined { const normalized = value?.trim(); return normalized ? (normalized as SupportedLanguages) : undefined; @@ -225,7 +235,7 @@ function buildHtmlDocument(params: { } .oc-frame[data-render-mode="image"] { - max-width: 1120px; + max-width: 960px; } [data-openclaw-diff-root] { @@ -293,22 +303,33 @@ async function renderBeforeAfterDiff( contents: input.after, ...(lang ? { lang } : {}), }; - const payloadOptions = buildDiffOptions(options); - const result = await preloadMultiFileDiff({ - oldFile, - newFile, - options: payloadOptions, - }); + const viewerPayloadOptions = buildDiffOptions(options); + const imagePayloadOptions = buildDiffOptions(buildImageRenderOptions(options)); + const [viewerResult, imageResult] = await Promise.all([ + preloadMultiFileDiff({ + oldFile, + newFile, + options: viewerPayloadOptions, + }), + preloadMultiFileDiff({ + oldFile, + newFile, + options: imagePayloadOptions, + }), + ]); return { viewerBodyHtml: renderDiffCard({ - prerenderedHTML: result.prerenderedHTML, - oldFile: result.oldFile, - newFile: result.newFile, - options: payloadOptions, - langs: buildPayloadLanguages({ oldFile: result.oldFile, newFile: result.newFile }), + prerenderedHTML: viewerResult.prerenderedHTML, + oldFile: viewerResult.oldFile, + newFile: viewerResult.newFile, + options: viewerPayloadOptions, + langs: buildPayloadLanguages({ + oldFile: viewerResult.oldFile, + newFile: viewerResult.newFile, + }), }), - imageBodyHtml: renderStaticDiffCard(result.prerenderedHTML), + imageBodyHtml: renderStaticDiffCard(imageResult.prerenderedHTML), fileCount: 1, }; } @@ -322,22 +343,29 @@ async function renderPatchDiff( throw new Error("Patch input did not contain any file diffs."); } - const payloadOptions = buildDiffOptions(options); + const viewerPayloadOptions = buildDiffOptions(options); + const imagePayloadOptions = buildDiffOptions(buildImageRenderOptions(options)); const sections = await Promise.all( files.map(async (fileDiff) => { - const result = await preloadFileDiff({ - fileDiff, - options: payloadOptions, - }); + const [viewerResult, imageResult] = await Promise.all([ + preloadFileDiff({ + fileDiff, + options: viewerPayloadOptions, + }), + preloadFileDiff({ + fileDiff, + options: imagePayloadOptions, + }), + ]); return { viewer: renderDiffCard({ - prerenderedHTML: result.prerenderedHTML, - fileDiff: result.fileDiff, - options: payloadOptions, - langs: buildPayloadLanguages({ fileDiff: result.fileDiff }), + prerenderedHTML: viewerResult.prerenderedHTML, + fileDiff: viewerResult.fileDiff, + options: viewerPayloadOptions, + langs: buildPayloadLanguages({ fileDiff: viewerResult.fileDiff }), }), - image: renderStaticDiffCard(result.prerenderedHTML), + image: renderStaticDiffCard(imageResult.prerenderedHTML), }; }), );