feat(browser-tools): configurable screenshot resolution, format, and quality (#1152)

Add environment variable overrides for screenshot capture settings so
users can opt into full-resolution output for human review while keeping
the Anthropic vision-optimized defaults:

- SCREENSHOT_MAX_WIDTH (default 1568, set 0 to uncap)
- SCREENSHOT_MAX_HEIGHT (default 8000, set 0 to uncap)
- SCREENSHOT_FORMAT (default jpeg for viewport / png for crops)
- SCREENSHOT_QUALITY (default 80, range 1-100)

Also fixes:
- Integration test viewport/scale mismatch: was 1280x720 scale 1,
  now 1280x800 scale 2 to match production browser context
- Unit test height-limit assertion: test expected <= 1568 but
  MAX_SCREENSHOT_HEIGHT is 8000 — corrected test image and assertions
This commit is contained in:
Jeremy McSpadden 2026-03-18 09:33:40 -05:00 committed by GitHub
parent d24095971c
commit 45af9f7f9d
4 changed files with 66 additions and 16 deletions

View file

@ -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

View file

@ -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();
});

View file

@ -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);
});
});

View file

@ -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);