diff --git a/src/resources/extensions/browser-tools/capture.ts b/src/resources/extensions/browser-tools/capture.ts index 24dd890df..0c980b871 100644 --- a/src/resources/extensions/browser-tools/capture.ts +++ b/src/resources/extensions/browser-tools/capture.ts @@ -13,8 +13,40 @@ import { formatCompactStateSummary } from "./utils.js"; // Anthropic vision: 1568px is the recommended optimal width. Height is capped // generously at 8000px so tall full-page screenshots remain readable rather // than being squished into a square constraint. -const MAX_SCREENSHOT_WIDTH = 1568; -const MAX_SCREENSHOT_HEIGHT = 8000; +// +// Override via environment variables: +// SCREENSHOT_MAX_WIDTH=0 → uncap width (use raw resolution) +// SCREENSHOT_MAX_HEIGHT=0 → uncap height +// SCREENSHOT_FORMAT=png → lossless PNG for all viewport/fullpage screenshots +// SCREENSHOT_QUALITY=100 → max JPEG quality (1-100, default 80) +const MAX_SCREENSHOT_WIDTH = parseScreenshotDimension(process.env.SCREENSHOT_MAX_WIDTH, 1568); +const MAX_SCREENSHOT_HEIGHT = parseScreenshotDimension(process.env.SCREENSHOT_MAX_HEIGHT, 8000); + +/** Parse a dimension env var: positive int = that value, 0 = Infinity (uncapped), absent/invalid = default. */ +function parseScreenshotDimension(value: string | undefined, fallback: number): number { + if (value === undefined || value === "") return fallback; + const n = parseInt(value, 10); + if (isNaN(n) || n < 0) return fallback; + if (n === 0) return Infinity; + return n; +} + +/** Return the user-configured screenshot format override, or null for default behavior. */ +export function getScreenshotFormatOverride(): "png" | "jpeg" | null { + const fmt = process.env.SCREENSHOT_FORMAT?.toLowerCase(); + if (fmt === "png") return "png"; + if (fmt === "jpeg" || fmt === "jpg") return "jpeg"; + return null; +} + +/** Return the user-configured default JPEG quality, or the provided fallback. */ +export function getScreenshotQualityDefault(fallback: number): number { + const q = process.env.SCREENSHOT_QUALITY; + if (q === undefined || q === "") return fallback; + const n = parseInt(q, 10); + if (isNaN(n) || n < 1 || n > 100) return fallback; + return n; +} // --------------------------------------------------------------------------- // Compact page state capture diff --git a/src/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs b/src/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs index 8f6d5a323..0f2432672 100644 --- a/src/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs +++ b/src/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs @@ -92,7 +92,7 @@ let page; before(async () => { browser = await chromium.launch({ headless: true }); - const context = await browser.newContext({ viewport: { width: 1280, height: 720 } }); + const context = await browser.newContext({ viewport: { width: 1280, height: 800 }, deviceScaleFactor: 2 }); page = await context.newPage(); }); diff --git a/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs b/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs index f63aa3066..de138002d 100644 --- a/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +++ b/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs @@ -602,14 +602,14 @@ describe("constrainScreenshot", () => { }); it("handles an image where only height exceeds the limit", async () => { - const buf = await createTestJpeg(1000, 2000); + const buf = await createTestJpeg(1000, 9000); const result = await constrainScreenshot(null, buf, "image/jpeg", 80); const sharp = require("sharp"); const meta = await sharp(result).metadata(); assert.ok(meta.width <= 1568); - assert.ok(meta.height <= 1568); + assert.ok(meta.height <= 8000); // Height was the constraining dimension - assert.equal(meta.height, 1568); + assert.equal(meta.height, 8000); }); }); diff --git a/src/resources/extensions/browser-tools/tools/screenshot.ts b/src/resources/extensions/browser-tools/tools/screenshot.ts index 7515d91b7..344911883 100644 --- a/src/resources/extensions/browser-tools/tools/screenshot.ts +++ b/src/resources/extensions/browser-tools/tools/screenshot.ts @@ -1,6 +1,7 @@ import type { ExtensionAPI } from "@gsd/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import type { ToolDeps } from "../state.js"; +import { getScreenshotFormatOverride, getScreenshotQualityDefault } from "../capture.js"; export function registerScreenshotTools(pi: ExtensionAPI, deps: ToolDeps): void { pi.registerTool({ @@ -32,20 +33,37 @@ export function registerScreenshotTools(pi: ExtensionAPI, deps: ToolDeps): void let screenshotBuffer: Buffer; let mimeType: string; - const quality = params.quality ?? 80; + const formatOverride = getScreenshotFormatOverride(); + const quality = params.quality ?? getScreenshotQualityDefault(80); if (params.selector) { + const fmt = formatOverride ?? "png"; const locator = p.locator(params.selector).first(); - screenshotBuffer = await locator.screenshot({ type: "png", scale: "css" }); - mimeType = "image/png"; + if (fmt === "jpeg") { + screenshotBuffer = await locator.screenshot({ type: "jpeg", quality, scale: "css" }); + mimeType = "image/jpeg"; + } else { + screenshotBuffer = await locator.screenshot({ type: "png", scale: "css" }); + mimeType = "image/png"; + } } else { - screenshotBuffer = await p.screenshot({ - fullPage: params.fullPage ?? false, - type: "jpeg", - quality, - scale: "css", - }); - mimeType = "image/jpeg"; + const fmt = formatOverride ?? "jpeg"; + if (fmt === "png") { + screenshotBuffer = await p.screenshot({ + fullPage: params.fullPage ?? false, + type: "png", + scale: "css", + }); + mimeType = "image/png"; + } else { + screenshotBuffer = await p.screenshot({ + fullPage: params.fullPage ?? false, + type: "jpeg", + quality, + scale: "css", + }); + mimeType = "image/jpeg"; + } } screenshotBuffer = await deps.constrainScreenshot(p, screenshotBuffer, mimeType, quality);