fix: sharp fallback with viewport clipping when unavailable
This commit is contained in:
@@ -47,7 +47,14 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Restart your MCP client and you're ready to go! Your AI assistant can now control the browser through the extension.
|
Or use the following command to automatically add the MCP setting for your client:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npx -y install-mcp playwriter # pass `--client claude` or `--client opencode` for the client you want to use
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart your MCP client and you're ready to go! Your AI assistant can now control the browser through the extension.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -161,11 +168,11 @@ When set, the MCP will automatically create an initial tab when a Playwright cli
|
|||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"playwriter": {
|
"playwriter": {
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": ["-y", "playwriter@latest"],
|
"args": ["-y", "playwriter@latest"],
|
||||||
"env": {
|
"env": {
|
||||||
"PLAYWRITER_AUTO_ENABLE": "1"
|
"PLAYWRITER_AUTO_ENABLE": "1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -359,8 +366,8 @@ Configure your MCP client with the host and token. You can pass them as CLI argu
|
|||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"playwriter": {
|
"playwriter": {
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": ["-y", "playwriter@latest", "--host", "host.docker.internal", "--token", "<secret>"]
|
"args": ["-y", "playwriter@latest", "--host", "host.docker.internal", "--token", "<secret>"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -372,12 +379,12 @@ Or use environment variables (useful if you want to set them globally in your pr
|
|||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"playwriter": {
|
"playwriter": {
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": ["-y", "playwriter@latest"],
|
"args": ["-y", "playwriter@latest"],
|
||||||
"env": {
|
"env": {
|
||||||
"PLAYWRITER_HOST": "host.docker.internal",
|
"PLAYWRITER_HOST": "host.docker.internal",
|
||||||
"PLAYWRITER_TOKEN": "<secret>"
|
"PLAYWRITER_TOKEN": "<secret>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.0.50
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **Sharp fallback with viewport clipping**: When sharp is unavailable (optional dependency), screenshots now clip to max 1568px instead of relying on Claude's auto-resize
|
||||||
|
- **Error logging for sharp failures**: Added logging when sharp import or resize fails, making it easier to debug screenshot optimization issues
|
||||||
|
|
||||||
## 0.0.49
|
## 0.0.49
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "playwriter",
|
"name": "playwriter",
|
||||||
"description": "",
|
"description": "",
|
||||||
"version": "0.0.49",
|
"version": "0.0.50",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import type { Page, Locator, ElementHandle } from 'playwright-core'
|
|||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
|
// Import sharp at module level - resolves to null if not available
|
||||||
|
const sharpPromise = import('sharp')
|
||||||
|
.then((m) => { return m.default })
|
||||||
|
.catch(() => { return null })
|
||||||
|
|
||||||
export interface AriaRef {
|
export interface AriaRef {
|
||||||
role: string
|
role: string
|
||||||
name: string
|
name: string
|
||||||
@@ -558,10 +563,11 @@ export async function hideAriaRefLabels({ page }: { page: Page }): Promise<void>
|
|||||||
* await page.locator('aria-ref=e5').click()
|
* await page.locator('aria-ref=e5').click()
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export async function screenshotWithAccessibilityLabels({ page, interactiveOnly = true, collector }: {
|
export async function screenshotWithAccessibilityLabels({ page, interactiveOnly = true, collector, logger }: {
|
||||||
page: Page
|
page: Page
|
||||||
interactiveOnly?: boolean
|
interactiveOnly?: boolean
|
||||||
collector: ScreenshotResult[]
|
collector: ScreenshotResult[]
|
||||||
|
logger?: { error: (...args: unknown[]) => void }
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
// Show labels and get snapshot
|
// Show labels and get snapshot
|
||||||
const { snapshot, labelCount } = await showAriaRefLabels({ page, interactiveOnly })
|
const { snapshot, labelCount } = await showAriaRefLabels({ page, interactiveOnly })
|
||||||
@@ -581,23 +587,33 @@ export async function screenshotWithAccessibilityLabels({ page, interactiveOnly
|
|||||||
// Get viewport size to clip screenshot to visible area
|
// Get viewport size to clip screenshot to visible area
|
||||||
const viewport = await page.evaluate('({ width: window.innerWidth, height: window.innerHeight })') as { width: number; height: number }
|
const viewport = await page.evaluate('({ width: window.innerWidth, height: window.innerHeight })') as { width: number; height: number }
|
||||||
|
|
||||||
|
// Max 1568px on any edge (larger gets auto-resized by Claude, adding latency)
|
||||||
|
// Token formula: tokens = (width * height) / 750
|
||||||
|
const MAX_DIMENSION = 1568
|
||||||
|
|
||||||
|
// Check if sharp is available for resizing
|
||||||
|
const sharp = await sharpPromise
|
||||||
|
|
||||||
|
// Clip dimensions: if sharp unavailable, limit capture area to MAX_DIMENSION
|
||||||
|
const clipWidth = sharp ? viewport.width : Math.min(viewport.width, MAX_DIMENSION)
|
||||||
|
const clipHeight = sharp ? viewport.height : Math.min(viewport.height, MAX_DIMENSION)
|
||||||
|
|
||||||
// Take viewport screenshot with scale: 'css' to ignore device pixel ratio
|
// Take viewport screenshot with scale: 'css' to ignore device pixel ratio
|
||||||
const rawBuffer = await page.screenshot({
|
const rawBuffer = await page.screenshot({
|
||||||
type: 'jpeg',
|
type: 'jpeg',
|
||||||
quality: 80,
|
quality: 80,
|
||||||
scale: 'css',
|
scale: 'css',
|
||||||
clip: { x: 0, y: 0, width: viewport.width, height: viewport.height },
|
clip: { x: 0, y: 0, width: clipWidth, height: clipHeight },
|
||||||
})
|
})
|
||||||
|
|
||||||
// Resize to fit within Claude's optimal limits:
|
// Resize with sharp if available, otherwise use clipped raw buffer
|
||||||
// - Max 1568px on any edge (larger gets auto-resized by Claude, adding latency)
|
const buffer = await (async () => {
|
||||||
// - Target ~1.15 megapixels for optimal token usage (~1,533 tokens)
|
if (!sharp) {
|
||||||
// Token formula: tokens = (width * height) / 750
|
logger?.error('[playwriter] sharp not available, using clipped screenshot (max', MAX_DIMENSION, 'px)')
|
||||||
const buffer = await Promise.resolve()
|
return rawBuffer
|
||||||
.then(() => import('sharp'))
|
}
|
||||||
.then(async ({ default: sharp }) => {
|
try {
|
||||||
const MAX_DIMENSION = 1568
|
return await sharp(rawBuffer)
|
||||||
return sharp(rawBuffer)
|
|
||||||
.resize({
|
.resize({
|
||||||
width: MAX_DIMENSION,
|
width: MAX_DIMENSION,
|
||||||
height: MAX_DIMENSION,
|
height: MAX_DIMENSION,
|
||||||
@@ -606,8 +622,11 @@ export async function screenshotWithAccessibilityLabels({ page, interactiveOnly
|
|||||||
})
|
})
|
||||||
.jpeg({ quality: 80 })
|
.jpeg({ quality: 80 })
|
||||||
.toBuffer()
|
.toBuffer()
|
||||||
})
|
} catch (err) {
|
||||||
.catch(() => rawBuffer) // sharp not available, Claude will auto-resize
|
logger?.error('[playwriter] sharp resize failed, using raw buffer:', err)
|
||||||
|
return rawBuffer
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
// Save to file
|
// Save to file
|
||||||
fs.writeFileSync(screenshotPath, buffer)
|
fs.writeFileSync(screenshotPath, buffer)
|
||||||
|
|||||||
@@ -918,7 +918,11 @@ server.tool(
|
|||||||
const screenshotCollector: ScreenshotResult[] = []
|
const screenshotCollector: ScreenshotResult[] = []
|
||||||
|
|
||||||
const screenshotWithAccessibilityLabelsFn = async (options: { page: Page; interactiveOnly?: boolean }) => {
|
const screenshotWithAccessibilityLabelsFn = async (options: { page: Page; interactiveOnly?: boolean }) => {
|
||||||
return screenshotWithAccessibilityLabels({ ...options, collector: screenshotCollector })
|
return screenshotWithAccessibilityLabels({
|
||||||
|
...options,
|
||||||
|
collector: screenshotCollector,
|
||||||
|
logger: { error: (...args) => { console.error('[playwriter]', ...args) } },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let vmContextObj: VMContextWithGlobals = {
|
let vmContextObj: VMContextWithGlobals = {
|
||||||
@@ -1134,6 +1138,7 @@ export async function startMcp(options: { host?: string; token?: string } = {})
|
|||||||
mcpLog(`Using remote CDP relay server: ${remote.host}:${remote.port}`)
|
mcpLog(`Using remote CDP relay server: ${remote.host}:${remote.port}`)
|
||||||
await checkRemoteServer(remote)
|
await checkRemoteServer(remote)
|
||||||
}
|
}
|
||||||
|
|
||||||
const transport = new StdioServerTransport()
|
const transport = new StdioServerTransport()
|
||||||
await server.connect(transport)
|
await server.connect(transport)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user