release: playwriter@0.0.39, extension@0.0.66

- fix icon not updating on WS disconnect
- increase aria-labels auto-hide timeout to 30s
This commit is contained in:
Tommy D. Rossi
2026-01-03 17:38:28 +01:00
parent f9a2312a58
commit 1ad94f3930
8 changed files with 70 additions and 35 deletions
+1
View File
@@ -1,4 +1,5 @@
node_modules node_modules
.vercel
dist dist
esm esm
.DS_Store .DS_Store
+3 -3
View File
@@ -108,18 +108,18 @@ await page.screenshot({ path: '/tmp/labeled-page.png' })
// Use the snapshot to find elements, then interact using aria-ref selector // Use the snapshot to find elements, then interact using aria-ref selector
await page.locator('aria-ref=e5').click() await page.locator('aria-ref=e5').click()
// Labels auto-hide after 5 seconds, or remove manually: // Labels auto-hide after 30 seconds, or remove manually:
await hideAriaRefLabels({ page }) await hideAriaRefLabels({ page })
``` ```
**Important:** Labels auto-hide after 5 seconds to prevent stale labels. If the page HTML changes (navigation, dynamic content), call `showAriaRefLabels()` again to get fresh labels matching the current DOM. **Important:** Labels auto-hide after 30 seconds to prevent stale labels. If the page HTML changes (navigation, dynamic content), call `showAriaRefLabels()` again to get fresh labels matching the current DOM.
**Features:** **Features:**
- **Role filtering** - Only shows labels for interactive elements (buttons, links, inputs, etc.) - **Role filtering** - Only shows labels for interactive elements (buttons, links, inputs, etc.)
- **Visibility detection** - Skips elements covered by modals or overlays - **Visibility detection** - Skips elements covered by modals or overlays
- **Overlap prevention** - Skips labels that would overlap with already-placed ones - **Overlap prevention** - Skips labels that would overlap with already-placed ones
- **Color-coded by type** - Warm color scheme helps distinguish element types - **Color-coded by type** - Warm color scheme helps distinguish element types
- **Auto-hide** - Labels disappear after 5 seconds to prevent stale overlays - **Auto-hide** - Labels disappear after 30 seconds to prevent stale overlays
**Color legend:** **Color legend:**
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Playwriter MCP", "name": "Playwriter MCP",
"version": "0.0.65", "version": "0.0.66",
"description": "Automate your Browser using Cursor, Claude, VS Code. More capable and context efficient than Playwright MCP.", "description": "Automate your Browser using Cursor, Claude, VS Code. More capable and context efficient than Playwright MCP.",
"permissions": ["debugger", "tabGroups", "contextMenus", "tabs"], "permissions": ["debugger", "tabGroups", "contextMenus", "tabs"],
"host_permissions": ["<all_urls>"], "host_permissions": ["<all_urls>"],
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "mcp-extension", "name": "mcp-extension",
"version": "0.0.65", "version": "0.0.66",
"description": "Playwright MCP Browser Extension", "description": "Playwright MCP Browser Extension",
"private": true, "private": true,
"repository": { "repository": {
+9 -2
View File
@@ -1,5 +1,12 @@
# Changelog # Changelog
## 0.0.39
### Patch Changes
- **Fix icon not updating on WS disconnect**: `maintainLoop` now ensures tabs transition to 'connecting' state when WebSocket is not connected, fixing edge cases where `handleClose` wasn't called
- **Increased aria-labels auto-hide timeout**: Labels now auto-hide after 30 seconds instead of 5 seconds
## 0.0.38 ## 0.0.38
### Patch Changes ### Patch Changes
@@ -16,7 +23,7 @@
- Only shows truly interactive roles (button, link, textbox, combobox, checkbox, etc.) - Only shows truly interactive roles (button, link, textbox, combobox, checkbox, etc.)
- Skips elements covered by opaque overlays using `elementsFromPoint()` - Skips elements covered by opaque overlays using `elementsFromPoint()`
- Greedy overlap prevention skips labels that would overlap with already-placed ones - Greedy overlap prevention skips labels that would overlap with already-placed ones
- Auto-hides after 5 seconds to prevent stale labels (timer cancelled if called again) - Auto-hides after 30 seconds to prevent stale labels (timer cancelled if called again)
- Available in MCP execute context - Available in MCP execute context
### Usage ### Usage
@@ -25,7 +32,7 @@
const { snapshot, labelCount } = await showAriaRefLabels({ page }); const { snapshot, labelCount } = await showAriaRefLabels({ page });
await page.screenshot({ path: '/tmp/labeled-page.png' }); await page.screenshot({ path: '/tmp/labeled-page.png' });
await page.locator('aria-ref=e5').click(); await page.locator('aria-ref=e5').click();
// Labels auto-hide after 5 seconds, or call hideAriaRefLabels({ page }) manually // Labels auto-hide after 30 seconds, or call hideAriaRefLabels({ page }) manually
``` ```
## 0.0.35 ## 0.0.35
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "playwriter", "name": "playwriter",
"description": "", "description": "",
"version": "0.0.38", "version": "0.0.39",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
+52 -25
View File
@@ -197,7 +197,7 @@ export async function getAriaSnapshot({ page }: { page: Page }): Promise<AriaSna
* Labels are yellow badges positioned above each element showing the aria ref (e.g., "e1", "e2"). * Labels are yellow badges positioned above each element showing the aria ref (e.g., "e1", "e2").
* Use with screenshots so agents can see which elements are interactive. * Use with screenshots so agents can see which elements are interactive.
* *
* Labels auto-hide after 5 seconds to prevent stale labels remaining on the page. * Labels auto-hide after 30 seconds to prevent stale labels remaining on the page.
* Call this function again if the page HTML changes to get fresh labels. * Call this function again if the page HTML changes to get fresh labels.
* *
* By default, only shows labels for truly interactive roles (button, link, textbox, etc.) * By default, only shows labels for truly interactive roles (button, link, textbox, etc.)
@@ -209,7 +209,7 @@ export async function getAriaSnapshot({ page }: { page: Page }): Promise<AriaSna
* await page.screenshot({ path: '/tmp/screenshot.png' }) * await page.screenshot({ path: '/tmp/screenshot.png' })
* // Agent sees [e5] label on "Submit" button * // Agent sees [e5] label on "Submit" button
* await page.locator('aria-ref=e5').click() * await page.locator('aria-ref=e5').click()
* // Labels auto-hide after 5 seconds, or call hideAriaRefLabels() manually * // Labels auto-hide after 30 seconds, or call hideAriaRefLabels() manually
* ``` * ```
*/ */
export async function showAriaRefLabels({ page, interactiveOnly = true }: { export async function showAriaRefLabels({ page, interactiveOnly = true }: {
@@ -238,10 +238,9 @@ export async function showAriaRefLabels({ page, interactiveOnly = true }: {
// Single evaluate call: create container, styles, and all labels // Single evaluate call: create container, styles, and all labels
// ElementHandles get unwrapped to DOM elements in browser context // ElementHandles get unwrapped to DOM elements in browser context
// Using 'any' types here since this code runs in browser context
const labelCount = await page.evaluate( const labelCount = await page.evaluate(
// Using 'any' for browser types since this runs in browser context // Using 'any' for browser types since this runs in browser context
({ refs, containerId, containerStyles, labelStyles, roleColors, defaultColors }: { function ({ refs, containerId, containerStyles, labelStyles, roleColors, defaultColors }: {
refs: Array<{ refs: Array<{
ref: string ref: string
role: string role: string
@@ -252,7 +251,9 @@ export async function showAriaRefLabels({ page, interactiveOnly = true }: {
labelStyles: string labelStyles: string
roleColors: Record<string, [string, string, string]> roleColors: Record<string, [string, string, string]>
defaultColors: [string, string, string] defaultColors: [string, string, string]
}) => { }) {
// Polyfill esbuild's __name helper which gets injected by vite-node but doesn't exist in browser
;(globalThis as any).__name ||= (fn: any) => fn
const doc = (globalThis as any).document const doc = (globalThis as any).document
const win = globalThis as any const win = globalThis as any
@@ -285,8 +286,10 @@ export async function showAriaRefLabels({ page, interactiveOnly = true }: {
const LABEL_CHAR_WIDTH = 7 // approximate width per character const LABEL_CHAR_WIDTH = 7 // approximate width per character
// Parse alpha from rgb/rgba color string (getComputedStyle always returns these formats) // Parse alpha from rgb/rgba color string (getComputedStyle always returns these formats)
const getColorAlpha = (color: string): number => { function getColorAlpha(color: string): number {
if (color === 'transparent') return 0 if (color === 'transparent') {
return 0
}
// Match rgba(r, g, b, a) or rgb(r, g, b) // Match rgba(r, g, b, a) or rgb(r, g, b)
const match = color.match(/rgba?\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+\s*(?:,\s*([\d.]+)\s*)?\)/) const match = color.match(/rgba?\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+\s*(?:,\s*([\d.]+)\s*)?\)/)
if (match) { if (match) {
@@ -296,25 +299,31 @@ export async function showAriaRefLabels({ page, interactiveOnly = true }: {
} }
// Check if an element has an opaque background that would block elements behind it // Check if an element has an opaque background that would block elements behind it
const isOpaqueElement = (el: any): boolean => { function isOpaqueElement(el: any): boolean {
const style = win.getComputedStyle(el) const style = win.getComputedStyle(el)
// Check element opacity // Check element opacity
const opacity = parseFloat(style.opacity) const opacity = parseFloat(style.opacity)
if (opacity < 0.1) return false if (opacity < 0.1) {
return false
}
// Check background-color alpha // Check background-color alpha
const bgAlpha = getColorAlpha(style.backgroundColor) const bgAlpha = getColorAlpha(style.backgroundColor)
if (bgAlpha > 0.1) return true if (bgAlpha > 0.1) {
return true
}
// Check if has background-image (usually opaque) // Check if has background-image (usually opaque)
if (style.backgroundImage !== 'none') return true if (style.backgroundImage !== 'none') {
return true
}
return false return false
} }
// Check if element is visible (not covered by opaque overlay) // Check if element is visible (not covered by opaque overlay)
const isElementVisible = (element: any, rect: any): boolean => { function isElementVisible(element: any, rect: any): boolean {
const centerX = rect.left + rect.width / 2 const centerX = rect.left + rect.width / 2
const centerY = rect.top + rect.height / 2 const centerY = rect.top + rect.height / 2
@@ -322,32 +331,44 @@ export async function showAriaRefLabels({ page, interactiveOnly = true }: {
const stack = doc.elementsFromPoint(centerX, centerY) as any[] const stack = doc.elementsFromPoint(centerX, centerY) as any[]
// Find our target element in the stack // Find our target element in the stack
const targetIndex = stack.findIndex((el: any) => let targetIndex = -1
element.contains(el) || el.contains(element) for (let i = 0; i < stack.length; i++) {
) if (element.contains(stack[i]) || stack[i].contains(element)) {
targetIndex = i
break
}
}
// Element not in stack at all - not visible // Element not in stack at all - not visible
if (targetIndex === -1) return false if (targetIndex === -1) {
return false
}
// Check if any opaque element is above our target // Check if any opaque element is above our target
for (let i = 0; i < targetIndex; i++) { for (let i = 0; i < targetIndex; i++) {
const el = stack[i] const el = stack[i]
// Skip our own overlay container // Skip our own overlay container
if (el.id === containerId) continue if (el.id === containerId) {
continue
}
// Skip pointer-events: none elements (decorative overlays) // Skip pointer-events: none elements (decorative overlays)
if (win.getComputedStyle(el).pointerEvents === 'none') continue if (win.getComputedStyle(el).pointerEvents === 'none') {
continue
}
// If this element is opaque, our target is blocked // If this element is opaque, our target is blocked
if (isOpaqueElement(el)) return false if (isOpaqueElement(el)) {
return false
}
} }
return true return true
} }
// Check if two rectangles overlap // Check if two rectangles overlap
const rectsOverlap = ( function rectsOverlap(
a: { left: number; top: number; right: number; bottom: number }, a: { left: number; top: number; right: number; bottom: number },
b: { left: number; top: number; right: number; bottom: number } b: { left: number; top: number; right: number; bottom: number }
) => { ) {
return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top
} }
@@ -378,7 +399,13 @@ export async function showAriaRefLabels({ page, interactiveOnly = true }: {
} }
// Skip if this label would overlap with any already-placed label // Skip if this label would overlap with any already-placed label
const overlaps = placedLabels.some((placed) => rectsOverlap(labelRect, placed)) let overlaps = false
for (const placed of placedLabels) {
if (rectsOverlap(labelRect, placed)) {
overlaps = true
break
}
}
if (overlaps) { if (overlaps) {
continue continue
} }
@@ -404,12 +431,12 @@ export async function showAriaRefLabels({ page, interactiveOnly = true }: {
doc.documentElement.appendChild(container) doc.documentElement.appendChild(container)
// Auto-hide labels after 5 seconds to prevent stale labels // Auto-hide labels after 30 seconds to prevent stale labels
// Store timer ID so it can be cancelled if showAriaRefLabels is called again // Store timer ID so it can be cancelled if showAriaRefLabels is called again
win[timerKey] = win.setTimeout(() => { win[timerKey] = win.setTimeout(function() {
doc.getElementById(containerId)?.remove() doc.getElementById(containerId)?.remove()
win[timerKey] = null win[timerKey] = null
}, 5000) }, 30000)
return count return count
}, },
+2 -2
View File
@@ -206,7 +206,7 @@ const matches = await editor.grep({ regex: /console\.log/ });
await editor.edit({ url: matches[0].url, oldString: 'DEBUG = false', newString: 'DEBUG = true' }); await editor.edit({ url: matches[0].url, oldString: 'DEBUG = false', newString: 'DEBUG = true' });
``` ```
**showAriaRefLabels** - overlay Vimium-style visual labels on interactive elements. Useful for taking screenshots where you can see element references. Labels auto-hide after 5 seconds. Call again if page HTML changes to get fresh labels. **showAriaRefLabels** - overlay Vimium-style visual labels on interactive elements. Useful for taking screenshots where you can see element references. Labels auto-hide after 30 seconds. Call again if page HTML changes or scrolls to get fresh labels. Use a timeout of 10 seconds at least.
```js ```js
const { snapshot, labelCount } = await showAriaRefLabels({ page }); const { snapshot, labelCount } = await showAriaRefLabels({ page });
@@ -218,7 +218,7 @@ await page.locator('aria-ref=e5').click();
Labels are color-coded: yellow=links, orange=buttons, coral=inputs, pink=checkboxes, peach=sliders, salmon=menus, amber=tabs. Labels are color-coded: yellow=links, orange=buttons, coral=inputs, pink=checkboxes, peach=sliders, salmon=menus, amber=tabs.
**hideAriaRefLabels** - manually remove labels before the 5-second auto-hide: **hideAriaRefLabels** - manually remove labels before the 30-second auto-hide:
```js ```js
await hideAriaRefLabels({ page }); await hideAriaRefLabels({ page });