diff --git a/playwriter/src/clean-html.ts b/playwriter/src/clean-html.ts
index a24a00b..b2c7313 100644
--- a/playwriter/src/clean-html.ts
+++ b/playwriter/src/clean-html.ts
@@ -36,7 +36,7 @@ export async function getCleanHTML(options: GetCleanHTMLOptions): Promise {
expect(result.content).toBe('No changes detected since last snapshot')
})
- it('returns diff for small changes', () => {
+ it('returns diff when diff is shorter than full content', () => {
+ // Small change on 5-line content - diff may be shorter or longer depending on overhead
const result = createSmartDiff({
oldContent: 'line1\nline2\nline3\nline4\nline5',
newContent: 'line1\nline2-modified\nline3\nline4\nline5',
})
+ // The behavior depends on whether diff is shorter than full content
+ if (result.type === 'diff') {
+ expect(result.content).toContain('-line2')
+ expect(result.content).toContain('+line2-modified')
+ } else {
+ expect(result.content).toContain('Full new content')
+ }
+ })
+
+ it('returns diff for large content with small changes', () => {
+ // Generate large content where diff will be shorter
+ const lines = Array.from({ length: 100 }, (_, i) => `line ${i + 1}: some content here`)
+ const oldContent = lines.join('\n')
+ const newLines = [...lines]
+ newLines[50] = 'line 51: MODIFIED content here'
+ const newContent = newLines.join('\n')
+
+ const result = createSmartDiff({ oldContent, newContent })
expect(result.type).toBe('diff')
- expect(result.content).toContain('-line2')
- expect(result.content).toContain('+line2-modified')
+ expect(result.content).toContain('-line 51: some content here')
+ expect(result.content).toContain('+line 51: MODIFIED content here')
})
it('returns full content when changes exceed 50% threshold', () => {
@@ -31,10 +50,16 @@ describe('createSmartDiff', () => {
expect(result.content).toContain('x\ny\nz\nw')
})
- it('respects custom threshold', () => {
- // 2 out of 4 lines changed = 50%
- const oldContent = 'a\nb\nc\nd'
- const newContent = 'a\nX\nY\nd'
+ it('respects custom threshold with large content', () => {
+ // Generate content where diff is shorter than full
+ const lines = Array.from({ length: 50 }, (_, i) => `line ${i + 1}`)
+ const oldContent = lines.join('\n')
+ const newLines = [...lines]
+ // Change 25 lines = 50%
+ for (let i = 0; i < 25; i++) {
+ newLines[i * 2] = `CHANGED ${i}`
+ }
+ const newContent = newLines.join('\n')
// With 40% threshold, should return full (50% >= 40%)
const resultLow = createSmartDiff({
@@ -44,21 +69,29 @@ describe('createSmartDiff', () => {
})
expect(resultLow.type).toBe('full')
- // With 60% threshold, should return diff (50% < 60%)
+ // With 60% threshold, should return diff (50% < 60%) - but only if diff is shorter
const resultHigh = createSmartDiff({
oldContent,
newContent,
threshold: 0.6,
})
- expect(resultHigh.type).toBe('diff')
+ // For this case, even with high threshold, the diff may be longer than full content
+ // so it could return 'full' - the key is threshold + length check both apply
+ expect(['diff', 'full']).toContain(resultHigh.type)
})
it('uses custom label in diff output', () => {
+ // Generate large content where diff will be shorter than full
+ const lines = Array.from({ length: 100 }, (_, i) => `line ${i + 1}: content`)
+ const oldContent = lines.join('\n')
+ const newLines = [...lines]
+ newLines[50] = 'line 51: MODIFIED'
+ const newContent = newLines.join('\n')
+
const result = createSmartDiff({
- oldContent: 'line1\nline2\nline3\nline4\nline5',
- newContent: 'line1\nmodified\nline3\nline4\nline5',
+ oldContent,
+ newContent,
label: 'snapshot',
- threshold: 0.5, // 20% change, will return diff
})
expect(result.type).toBe('diff')
expect(result.content).toContain('--- snapshot')
@@ -104,16 +137,22 @@ describe('createSmartDiff', () => {
})
it('includes context lines in diff output', () => {
+ // Generate large content where diff is shorter than full
+ const lines = Array.from({ length: 100 }, (_, i) => `line ${i + 1}: some longer content`)
+ const oldContent = lines.join('\n')
+ const newLines = [...lines]
+ newLines[50] = 'line 51: MODIFIED content'
+ const newContent = newLines.join('\n')
+
const result = createSmartDiff({
- oldContent: 'line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10',
- newContent: 'line1\nline2\nline3\nline4\nMODIFIED\nline6\nline7\nline8\nline9\nline10',
- threshold: 0.9, // Force diff
+ oldContent,
+ newContent,
})
expect(result.type).toBe('diff')
// Should have context around the change
- expect(result.content).toContain('line4')
- expect(result.content).toContain('-line5')
- expect(result.content).toContain('+MODIFIED')
- expect(result.content).toContain('line6')
+ expect(result.content).toContain('line 50')
+ expect(result.content).toContain('-line 51: some longer content')
+ expect(result.content).toContain('+line 51: MODIFIED content')
+ expect(result.content).toContain('line 52')
})
})
diff --git a/playwriter/src/executor.ts b/playwriter/src/executor.ts
index a1930a5..0245b17 100644
--- a/playwriter/src/executor.ts
+++ b/playwriter/src/executor.ts
@@ -450,7 +450,7 @@ export class PlaywrightExecutor {
/** Only include interactive elements (default: true) */
interactiveOnly?: boolean
}) => {
- const { page: targetPage, locator, search, showDiffSinceLastCall = false, interactiveOnly = true } = options
+ const { page: targetPage, locator, search, showDiffSinceLastCall = true, interactiveOnly = true } = options
// Use new in-page implementation via getAriaSnapshot
const { snapshot: rawSnapshot, refs, getSelectorForRef } = await getAriaSnapshot({
@@ -745,7 +745,7 @@ export class PlaywrightExecutor {
responseText += `Accessibility snapshot:\n${screenshot.snapshot}\n`
}
- const MAX_LENGTH = 6000
+ const MAX_LENGTH = 10000
let finalText = responseText.trim()
if (finalText.length > MAX_LENGTH) {
finalText =
diff --git a/playwriter/src/page-markdown.ts b/playwriter/src/page-markdown.ts
index 68e6d7c..523e209 100644
--- a/playwriter/src/page-markdown.ts
+++ b/playwriter/src/page-markdown.ts
@@ -9,7 +9,7 @@ import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import type { Page } from 'playwright-core'
-import { createPatch } from 'diff'
+import { createSmartDiff } from './diff-utils.js'
export interface PageMarkdownResult {
/** Extracted content as plain text (HTML tags stripped) */
@@ -70,7 +70,7 @@ function isRegExp(value: unknown): value is RegExp {
* the main content. Returns plain text content (no HTML).
*/
export async function getPageMarkdown(options: GetPageMarkdownOptions): Promise {
- const { page, search, showDiffSinceLastCall = false } = options
+ const { page, search, showDiffSinceLastCall = true } = options
// Check if readability is already injected
const hasReadability = await page.evaluate(() => !!(globalThis as any).__readability)
@@ -177,16 +177,15 @@ export async function getPageMarkdown(options: GetPageMarkdownOptions): Promise<
return 'No previous snapshot available. This is the first call. Full snapshot stored for next diff.'
}
- const patch = createPatch('content', previousSnapshot, markdown, 'previous', 'current', {
- context: 3,
+ const diffResult = createSmartDiff({
+ oldContent: previousSnapshot,
+ newContent: markdown,
+ label: 'content',
})
lastMarkdownSnapshots.set(page, markdown)
- if (patch.split('\n').length <= 4) {
- return 'No changes detected since last snapshot'
- }
- return patch
+ return diffResult.content
}
// Store snapshot for future diffs
diff --git a/playwriter/src/prompt.md b/playwriter/src/prompt.md
deleted file mode 100644
index 4ba49f7..0000000
--- a/playwriter/src/prompt.md
+++ /dev/null
@@ -1,410 +0,0 @@
-# playwriter best practices
-
-Control user's Chrome browser via playwright code snippets. Prefer single-line code with semicolons between statements. If you get "extension is not connected" or "no browser tabs have Playwriter enabled" error, tell user to click the playwriter extension icon on the tab they want to control.
-
-You can collaborate with the user - they can help with captchas, difficult elements, or reproducing bugs.
-
-## context variables
-
-- `state` - object persisted between calls **within your session**. Each session has its own isolated state. Use to store pages, data, listeners (e.g., `state.myPage = await context.newPage()`)
-- `page` - default page the user activated, use this unless working with multiple pages
-- `context` - browser context, access all pages via `context.pages()`
-- `require` - load Node.js modules like fs
-- Node.js globals: `setTimeout`, `setInterval`, `fetch`, `URL`, `Buffer`, `crypto`, etc.
-
-**Important:** `state` is **session-isolated** but `context.pages()` is **shared** across all sessions. All agents see the same browser tabs. If another agent navigates or closes a page, you'll see it. To avoid interference, create your own page and store it in `state` (see "working with pages").
-
-## rules
-
-- **Use your own session**: always pass `-s ` to commands. Get a session ID with `playwriter session new`. This isolates your state from other agents.
-- **Store pages in state**: when working on a task, create a page with `context.newPage()` and store it in `state.myPage`. This prevents other agents from interfering with your page.
-- **Multiple calls**: use multiple execute calls for complex logic - helps understand intermediate state and isolate which action failed
-- **Never close**: never call `browser.close()` or `context.close()`. Only close pages you created or if user asks
-- **No bringToFront**: never call unless user asks - it's disruptive and unnecessary, you can interact with background pages
-- **Check state after actions**: always verify page state after clicking/submitting (see next section)
-- **Clean up listeners**: call `page.removeAllListeners()` at end of message to prevent leaks
-- **CDP sessions**: use `getCDPSession({ page })` not `page.context().newCDPSession()` - NEVER use `newCDPSession()` method, it doesn't work through playwriter relay
-- **Wait for load**: use `page.waitForLoadState('domcontentloaded')` not `page.waitForEvent('load')` - waitForEvent times out if already loaded
-- **Avoid timeouts**: prefer proper waits over `page.waitForTimeout()` - there are better ways to wait for elements
-
-## checking page state
-
-After any action (click, submit, navigate), verify what happened:
-
-```js
-console.log('url:', page.url()); console.log(await accessibilitySnapshot({ page }).then(x => x.split('\n').slice(0, 30).join('\n')));
-```
-
-For visually complex pages (grids, galleries, dashboards), use `screenshotWithAccessibilityLabels({ page })` instead to understand spatial layout.
-
-If nothing changed, try `await waitForPageLoad({ page, timeout: 3000 })` or you may have clicked the wrong element.
-
-## accessibility snapshots
-
-```js
-await accessibilitySnapshot({ page, search?, showDiffSinceLastCall? })
-```
-
-- `search` - string/regex to filter results (returns first 10 matching lines)
-- `showDiffSinceLastCall` - returns diff since last snapshot (useful after actions)
-
-For pagination, use `.split('\n').slice(offset, offset + limit).join('\n')`:
-```js
-console.log((await accessibilitySnapshot({ page })).split('\n').slice(0, 50).join('\n')); // first 50 lines
-console.log((await accessibilitySnapshot({ page })).split('\n').slice(50, 100).join('\n')); // next 50 lines
-```
-
-Example output:
-
-```md
-- banner [ref=e3]:
- - link "Home" [ref=e5] [cursor=pointer]:
- - /url: /
- - navigation [ref=e12]:
- - link "Docs" [ref=e13] [cursor=pointer]:
- - /url: /docs
-```
-
-Use `aria-ref` to interact - **no quotes around the ref value**:
-
-```js
-await page.locator('aria-ref=e13').click()
-```
-
-Search for specific elements:
-
-```js
-const snapshot = await accessibilitySnapshot({ page, search: /button|submit/i })
-```
-
-## choosing between snapshot methods
-
-Both `accessibilitySnapshot` and `screenshotWithAccessibilityLabels` use the same `aria-ref` system, so you can combine them effectively.
-
-**Use `accessibilitySnapshot` when:**
-- Page has simple, semantic structure (articles, forms, lists)
-- You need to search for specific text or patterns
-- Token usage matters (text is smaller than images)
-- You need to process the output programmatically
-
-**Use `screenshotWithAccessibilityLabels` when:**
-- Page has complex visual layout (grids, galleries, dashboards, maps)
-- Spatial position matters (e.g., "first image", "top-left button")
-- DOM order doesn't match visual order
-- You need to understand the visual hierarchy
-
-**Combining both:** Use screenshot first to understand layout and identify target elements visually, then use `accessibilitySnapshot({ search: /pattern/ })` for efficient searching in subsequent calls.
-
-## selector best practices
-
-**For unknown websites**: use `accessibilitySnapshot()` with `aria-ref` - it shows what's actually interactive.
-
-**For development** (when you have source code access), prefer stable selectors in this order:
-
-1. **Best**: `[data-testid="submit"]` - explicit test attributes, never change accidentally
-2. **Good**: `getByRole('button', { name: 'Save' })` - accessible, semantic
-3. **Good**: `getByText('Sign in')`, `getByLabel('Email')` - readable, user-facing
-4. **OK**: `input[name="email"]`, `button[type="submit"]` - semantic HTML
-5. **Avoid**: `.btn-primary`, `#submit` - classes/IDs change frequently
-6. **Last resort**: `div.container > form > button` - fragile, breaks easily
-
-Combine locators for precision:
-
-```js
-page.locator('tr').filter({ hasText: 'John' }).locator('button').click()
-page.locator('button').nth(2).click()
-```
-
-If a locator matches multiple elements, Playwright throws "strict mode violation". Use `.first()`, `.last()`, or `.nth(n)`:
-
-```js
-await page.locator('button').first().click() // first match
-await page.locator('.item').last().click() // last match
-await page.locator('li').nth(3).click() // 4th item (0-indexed)
-```
-
-## working with pages
-
-**Understanding page sharing:** `context.pages()` returns all browser tabs with playwriter enabled. These are **shared across all sessions** - if multiple agents are running, they all see the same tabs. However, each session's `state` is isolated, so storing a page reference in `state.myPage` keeps it safe from other sessions overwriting your reference.
-
-**Create your own page (recommended for automation):**
-
-When automating tasks, create a dedicated page and store it in `state`. This prevents other agents from interfering with your work:
-
-```js
-state.myPage = await context.newPage();
-await state.myPage.goto('https://example.com');
-// Use state.myPage for all subsequent operations in this session
-```
-
-**Find a page the user opened:**
-
-Sometimes the user enables playwriter extension on a specific tab they want you to control (e.g., they're logged into an app). Find it by URL pattern:
-
-```js
-const pages = context.pages().filter(x => x.url().includes('myapp.com'));
-if (pages.length === 0) throw new Error('No myapp.com page found. Ask user to enable playwriter on it.');
-if (pages.length > 1) throw new Error(`Found ${pages.length} matching pages, expected 1`);
-state.targetPage = pages[0];
-```
-
-**Find a specific page by partial URL:**
-
-```js
-const pages = context.pages().filter(x => x.url().includes('localhost'));
-if (pages.length !== 1) throw new Error(`Expected 1 page, found ${pages.length}`);
-state.targetPage = pages[0];
-```
-
-**List all available pages:**
-
-```js
-console.log(context.pages().map(p => p.url()));
-```
-
-## navigation
-
-**Use `domcontentloaded`** for `page.goto()`:
-
-```js
-await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
-await waitForPageLoad({ page, timeout: 5000 });
-```
-
-## common patterns
-
-**Popups** - capture before triggering:
-
-```js
-const [popup] = await Promise.all([page.waitForEvent('popup'), page.click('a[target=_blank]')]);
-await popup.waitForLoadState(); console.log('Popup URL:', popup.url());
-```
-
-**Downloads** - capture and save:
-
-```js
-const [download] = await Promise.all([page.waitForEvent('download'), page.click('button.download')]);
-await download.saveAs(`/tmp/${download.suggestedFilename()}`);
-```
-
-**iFrames** - use frameLocator:
-
-```js
-const frame = page.frameLocator('#my-iframe');
-await frame.locator('button').click();
-```
-
-**Dialogs** - handle alerts/confirms/prompts:
-
-```js
-page.on('dialog', async dialog => { console.log(dialog.message()); await dialog.accept(); });
-await page.click('button.trigger-alert');
-```
-
-## utility functions
-
-**getLatestLogs** - retrieve captured browser console logs (up to 5000 per page, cleared on navigation):
-
-```js
-await getLatestLogs({ page?, count?, search? })
-// Examples:
-const errors = await getLatestLogs({ search: /error/i, count: 50 })
-const pageLogs = await getLatestLogs({ page })
-```
-
-For custom log collection across runs, store in state: `state.logs = []; page.on('console', m => state.logs.push(m.text()))`
-
-**getCleanHTML** - get cleaned HTML from a locator or page, with search and diffing:
-
-```js
-await getCleanHTML({ locator, search?, showDiffSinceLastCall?, includeStyles? })
-// Examples:
-const html = await getCleanHTML({ locator: page.locator('body') })
-const html = await getCleanHTML({ locator: page, search: /button/i })
-const diff = await getCleanHTML({ locator: page, showDiffSinceLastCall: true })
-```
-
-- `locator` - Playwright Locator or Page to get HTML from
-- `search` - string/regex to filter results (returns first 10 matching lines)
-- `showDiffSinceLastCall` - returns diff since last snapshot
-- `includeStyles` - keep style and class attributes (default: false)
-
-Returns cleaned HTML with only essential attributes (aria-*, data-*, href, role, title, alt, etc.). Removes script, style, svg, head tags.
-
-For pagination, use `.split('\n').slice(offset, offset + limit).join('\n')`:
-```js
-console.log((await getCleanHTML({ locator: page })).split('\n').slice(0, 50).join('\n')); // first 50 lines
-console.log((await getCleanHTML({ locator: page })).split('\n').slice(50, 100).join('\n')); // next 50 lines
-```
-
-**waitForPageLoad** - smart load detection that ignores analytics/ads:
-
-```js
-await waitForPageLoad({ page, timeout?, pollInterval?, minWait? })
-// Returns: { success, readyState, pendingRequests, waitTimeMs, timedOut }
-```
-
-**getCDPSession** - send raw CDP commands:
-
-```js
-const cdp = await getCDPSession({ page });
-const metrics = await cdp.send('Page.getLayoutMetrics');
-```
-
-**getLocatorStringForElement** - get stable selector from ephemeral aria-ref:
-
-```js
-const selector = await getLocatorStringForElement(page.locator('aria-ref=e14'));
-// => "getByRole('button', { name: 'Save' })"
-```
-
-**getReactSource** - get React component source location (dev mode only):
-
-```js
-const source = await getReactSource({ locator: page.locator('aria-ref=e5') });
-// => { fileName, lineNumber, columnNumber, componentName }
-```
-
-**getStylesForLocator** - inspect CSS styles applied to an element, like browser DevTools "Styles" panel. Useful for debugging styling issues, finding where a CSS property is defined (file:line), and checking inherited styles. Returns selector, source location, and declarations for each matching rule. ALWAYS fetch `https://playwriter.dev/resources/styles-api.md` first with curl or webfetch tool.
-
-```js
-const styles = await getStylesForLocator({ locator: page.locator('.btn'), cdp: await getCDPSession({ page }) });
-console.log(formatStylesAsText(styles));
-```
-
-**createDebugger** - set breakpoints, step through code, inspect variables at runtime. Useful for debugging issues that only reproduce in browser, understanding code flow, and inspecting state at specific points. Can pause on exceptions, evaluate expressions in scope, and blackbox framework code. ALWAYS fetch `https://playwriter.dev/resources/debugger-api.md` first.
-
-```js
-const cdp = await getCDPSession({ page }); const dbg = createDebugger({ cdp }); await dbg.enable();
-const scripts = await dbg.listScripts({ search: 'app' });
-await dbg.setBreakpoint({ file: scripts[0].url, line: 42 });
-// when paused: dbg.inspectLocalVariables(), dbg.stepOver(), dbg.resume()
-```
-
-**createEditor** - view and live-edit page scripts and CSS at runtime. Edits are in-memory (persist until reload). Useful for testing quick fixes, searching page scripts with grep, and toggling debug flags. ALWAYS read `https://playwriter.dev/resources/editor-api.md` first.
-
-```js
-const cdp = await getCDPSession({ page }); const editor = createEditor({ cdp }); await editor.enable();
-const matches = await editor.grep({ regex: /console\.log/ });
-await editor.edit({ url: matches[0].url, oldString: 'DEBUG = false', newString: 'DEBUG = true' });
-```
-
-**screenshotWithAccessibilityLabels** - take a screenshot with Vimium-style visual labels overlaid on interactive elements. Shows labels, captures screenshot, then removes labels. The image and accessibility snapshot are automatically included in the response. Can be called multiple times to capture multiple screenshots. Use a timeout of **20 seconds** for complex pages.
-
-Prefer this for pages with grids, image galleries, maps, or complex visual layouts where spatial position matters. For simple text-heavy pages, `accessibilitySnapshot` with search is faster and uses fewer tokens.
-
-```js
-await screenshotWithAccessibilityLabels({ page });
-// Image and accessibility snapshot are automatically included in response
-// Use aria-ref from snapshot to interact with elements
-await page.locator('aria-ref=e5').click();
-
-// Can take multiple screenshots in one execution
-await screenshotWithAccessibilityLabels({ page });
-await page.click('button');
-await screenshotWithAccessibilityLabels({ page });
-// Both images are included in the response
-```
-
-Labels are color-coded: yellow=links, orange=buttons, coral=inputs, pink=checkboxes, peach=sliders, salmon=menus, amber=tabs.
-
-## pinned elements
-
-Users can right-click → "Copy Playwriter Element Reference" to store elements in `globalThis.playwriterPinnedElem1` (increments for each pin). The reference is copied to clipboard:
-
-```js
-const el = await page.evaluateHandle(() => globalThis.playwriterPinnedElem1);
-await el.click();
-```
-
-## taking screenshots
-
-Always use `scale: 'css'` to avoid 2-4x larger images on high-DPI displays:
-
-```js
-await page.screenshot({ path: 'shot.png', scale: 'css' });
-```
-
-If you want to read back the image file into context make sure to resize it first, scaling down the image to make sure max size is 1500px. for example with `sips --resampleHeightWidthMax 1500 input.png --out output.png` on macOS.
-
-## page.evaluate
-
-Code inside `page.evaluate()` runs in the browser - use plain JavaScript only, no TypeScript syntax. Return values and log outside (console.log inside evaluate runs in browser, not visible):
-
-```js
-const title = await page.evaluate(() => document.title);
-console.log('Title:', title);
-
-const info = await page.evaluate(() => ({
- url: location.href,
- buttons: document.querySelectorAll('button').length,
-}));
-console.log(info);
-```
-
-## loading files
-
-Fill inputs with file content:
-
-```js
-const fs = require('node:fs'); const content = fs.readFileSync('./data.txt', 'utf-8'); await page.locator('textarea').fill(content);
-```
-
-## network interception
-
-For scraping or reverse-engineering APIs, intercept network requests instead of scrolling DOM. Store in `state` to analyze across calls:
-
-```js
-state.requests = []; state.responses = [];
-page.on('request', req => { if (req.url().includes('/api/')) state.requests.push({ url: req.url(), method: req.method(), headers: req.headers() }); });
-page.on('response', async res => { if (res.url().includes('/api/')) { try { state.responses.push({ url: res.url(), status: res.status(), body: await res.json() }); } catch {} } });
-```
-
-Then trigger actions (scroll, click, navigate) and analyze captured data:
-
-```js
-console.log('Captured', state.responses.length, 'API calls');
-state.responses.forEach(r => console.log(r.status, r.url.slice(0, 80)));
-```
-
-Inspect a specific response to understand schema:
-
-```js
-const resp = state.responses.find(r => r.url.includes('users'));
-console.log(JSON.stringify(resp.body, null, 2).slice(0, 2000));
-```
-
-Replay API directly (useful for pagination):
-
-```js
-const { url, headers } = state.requests.find(r => r.url.includes('feed'));
-const data = await page.evaluate(async ({ url, headers }) => { const res = await fetch(url, { headers }); return res.json(); }, { url, headers });
-console.log(data);
-```
-
-Clean up listeners when done: `page.removeAllListeners('request'); page.removeAllListeners('response');`
-
-## capabilities
-
-Examples of what playwriter can do:
-- Monitor console logs while user reproduces a bug
-- Intercept network requests to reverse-engineer APIs and build SDKs
-- Scrape data by replaying paginated API calls instead of scrolling DOM
-- Get accessibility snapshot to find elements, then automate interactions
-- Use visual screenshots to understand complex layouts like image grids, dashboards, or maps
-- Debug issues by collecting logs and controlling the page simultaneously
-- Handle popups, downloads, iframes, and dialog boxes
-
-
-## debugging playwriter issues
-
-if some internal critical error happens you can read the relay server logs to understand the issue. the log file is located in the system temp directory:
-
-```bash
-playwriter logfile # prints the log file path
-# typically: /tmp/playwriter/relay-server.log (Linux/macOS) or %TEMP%\playwriter\relay-server.log (Windows)
-```
-
-the log file contains logs from the extension, MCP and WS server together with all CDP events. the file is recreated every time the server starts. for debugging internal playwriter errors, read this file with grep/rg to find relevant lines.
-
-if you find a bug, you can create a gh issue using `gh issue create -R remorses/playwriter --title title --body body`. ask for user confirmation before doing this.
diff --git a/playwriter/src/skill.md b/playwriter/src/skill.md
index 46a897d..5b505e9 100644
--- a/playwriter/src/skill.md
+++ b/playwriter/src/skill.md
@@ -229,7 +229,7 @@ await loginPage.waitForURL('**/callback**');
After any action (click, submit, navigate), verify what happened:
```js
-console.log('url:', page.url()); console.log(await accessibilitySnapshot({ page }).then(x => x.split('\n').slice(0, 30).join('\n')));
+console.log('url:', page.url()); console.log(await accessibilitySnapshot({ page }));
```
For visually complex pages (grids, galleries, dashboards), use `screenshotWithAccessibilityLabels({ page })` instead to understand spatial layout. Label refs are short `eN` strings (e.g. `e3`).
@@ -243,13 +243,9 @@ await accessibilitySnapshot({ page, search?, showDiffSinceLastCall? })
```
- `search` - string/regex to filter results (returns first 10 matching lines)
-- `showDiffSinceLastCall` - returns diff since last snapshot (useful after actions)
+- `showDiffSinceLastCall` - returns diff since last snapshot (default: `true`). Pass `false` to get full snapshot.
-For pagination, use `.split('\n').slice(offset, offset + limit).join('\n')`:
-```js
-console.log((await accessibilitySnapshot({ page })).split('\n').slice(0, 50).join('\n')); // first 50 lines
-console.log((await accessibilitySnapshot({ page })).split('\n').slice(50, 100).join('\n')); // next 50 lines
-```
+Snapshots automatically return diffs after the first call. The diff is returned only when it's shorter than the full content.
Example output:
@@ -477,13 +473,13 @@ await getCleanHTML({ locator, search?, showDiffSinceLastCall?, includeStyles? })
// Examples:
const html = await getCleanHTML({ locator: page.locator('body') })
const html = await getCleanHTML({ locator: page, search: /button/i })
-const diff = await getCleanHTML({ locator: page, showDiffSinceLastCall: true })
+const fullHtml = await getCleanHTML({ locator: page, showDiffSinceLastCall: false }) // disable diff
```
**Parameters:**
- `locator` - Playwright Locator or Page to get HTML from
- `search` - string/regex to filter results (returns first 10 matching lines with 5 lines context)
-- `showDiffSinceLastCall` - returns unified diff since last call for same locator/page. First call stores snapshot and returns message; subsequent calls return the diff. Useful for tracking DOM changes after actions.
+- `showDiffSinceLastCall` - returns diff since last call (default: `true`). Pass `false` to get full HTML.
- `includeStyles` - keep style and class attributes (default: false)
**HTML processing:**
@@ -498,20 +494,15 @@ The function cleans HTML for compact, readable output:
- All `data-*` test attributes
- Frequently used test IDs and special attributes (e.g., `testid`, `qa`, `e2e`, `vimium-label`)
-For pagination, use `.split('\n').slice(offset, offset + limit).join('\n')`:
-```js
-console.log((await getCleanHTML({ locator: page })).split('\n').slice(0, 50).join('\n')); // first 50 lines
-console.log((await getCleanHTML({ locator: page })).split('\n').slice(50, 100).join('\n')); // next 50 lines
-```
+Snapshots automatically return diffs after the first call. The diff is returned only when it's shorter than the full content.
**getPageMarkdown** - extract main page content as plain text using Mozilla Readability (same algorithm as Firefox Reader View). Strips navigation, ads, sidebars, and other clutter. Returns formatted text with title, author, and content:
```js
await getPageMarkdown({ page, search?, showDiffSinceLastCall? })
// Examples:
-const content = await getPageMarkdown({ page }) // full article as plain text
+const content = await getPageMarkdown({ page, showDiffSinceLastCall: false }) // full article
const matches = await getPageMarkdown({ page, search: /API/i }) // search within content
-const diff = await getPageMarkdown({ page, showDiffSinceLastCall: true }) // track content changes
```
**Output format:**
@@ -528,7 +519,9 @@ The main article content as plain text, with paragraphs preserved...
**Parameters:**
- `page` - Playwright Page to extract content from
- `search` - string/regex to filter content (returns first 10 matching lines with 5 lines context)
-- `showDiffSinceLastCall` - returns unified diff since last call. Useful for tracking content changes.
+- `showDiffSinceLastCall` - returns diff since last call (default: `true`). Pass `false` to get full content.
+
+Snapshots automatically return diffs after the first call. The diff is returned only when it's shorter than the full content.
**Use cases:**
- Extract article text for LLM processing without HTML noise