playwriter 0.0.80 → 0.0.89

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/a11y-client.js +18 -8
  2. package/dist/aria-snapshot.d.ts.map +1 -1
  3. package/dist/aria-snapshot.js +3 -1
  4. package/dist/aria-snapshot.js.map +1 -1
  5. package/dist/bippy.js +1 -1
  6. package/dist/cdp-relay.d.ts.map +1 -1
  7. package/dist/cdp-relay.js +84 -0
  8. package/dist/cdp-relay.js.map +1 -1
  9. package/dist/executor.d.ts.map +1 -1
  10. package/dist/executor.js +8 -6
  11. package/dist/executor.js.map +1 -1
  12. package/dist/ffmpeg.d.ts +6 -6
  13. package/dist/ffmpeg.d.ts.map +1 -1
  14. package/dist/ffmpeg.js +6 -6
  15. package/dist/ffmpeg.js.map +1 -1
  16. package/dist/ghost-cursor-client.js +15 -9
  17. package/dist/prompt.md +71 -337
  18. package/dist/readability.js +16 -2
  19. package/dist/recording-ghost-cursor.d.ts.map +1 -1
  20. package/dist/recording-ghost-cursor.js +1 -1
  21. package/dist/recording-ghost-cursor.js.map +1 -1
  22. package/dist/relay-client.js +1 -1
  23. package/dist/relay-client.js.map +1 -1
  24. package/dist/relay-core.test.d.ts.map +1 -1
  25. package/dist/relay-core.test.js +344 -16
  26. package/dist/relay-core.test.js.map +1 -1
  27. package/dist/relay-navigation.test.d.ts.map +1 -1
  28. package/dist/relay-navigation.test.js +115 -0
  29. package/dist/relay-navigation.test.js.map +1 -1
  30. package/dist/screen-recording.d.ts +24 -0
  31. package/dist/screen-recording.d.ts.map +1 -1
  32. package/dist/screen-recording.js +62 -0
  33. package/dist/screen-recording.js.map +1 -1
  34. package/dist/screen-recording.test.d.ts +2 -0
  35. package/dist/screen-recording.test.d.ts.map +1 -0
  36. package/dist/screen-recording.test.js +102 -0
  37. package/dist/screen-recording.test.js.map +1 -0
  38. package/dist/selector-generator.js +1 -1
  39. package/package.json +2 -2
  40. package/src/aria-snapshot.ts +3 -1
  41. package/src/aria-snapshots/github-interactive.txt +2 -0
  42. package/src/aria-snapshots/github-raw.txt +4 -0
  43. package/src/aria-snapshots/hackernews-interactive.txt +238 -241
  44. package/src/aria-snapshots/hackernews-raw.txt +267 -271
  45. package/src/assets/aria-labels-hacker-news.png +0 -0
  46. package/src/cdp-relay.ts +110 -0
  47. package/src/executor.ts +8 -6
  48. package/src/ffmpeg.ts +8 -8
  49. package/src/ghost-cursor-client.ts +3 -2
  50. package/src/recording-ghost-cursor.ts +7 -1
  51. package/src/relay-client.ts +1 -1
  52. package/src/relay-core.test.ts +378 -17
  53. package/src/relay-navigation.test.ts +132 -0
  54. package/src/screen-recording.test.ts +111 -0
  55. package/src/screen-recording.ts +81 -0
  56. package/src/skill.md +71 -339
  57. package/src/snapshots/shadcn-ui-accessibility-full.md +182 -180
  58. package/src/snapshots/shadcn-ui-accessibility-interactive.md +120 -118
package/dist/prompt.md CHANGED
@@ -61,97 +61,47 @@ You can collaborate with the user - they can help with captchas, difficult eleme
61
61
 
62
62
  ## interaction feedback loop
63
63
 
64
- Every browser interaction should follow a **observe → act → observe** loop. After every action, you must check its result before proceeding. Never chain multiple actions blindly — the page may not have responded as expected.
64
+ Every browser interaction must follow **observe → act → observe**. Never chain multiple actions blindly.
65
65
 
66
- **Core loop:**
67
-
68
- 1. **Open page** — get or create your page and navigate to the target URL
69
- 2. **Observe** — print `state.page.url()` and take an accessibility snapshot. Always print the URL so you know where you are — pages can redirect, and actions can trigger unexpected navigation.
70
- 3. **Check** — read the snapshot and URL. If the page isn't ready (still loading, expected content missing, wrong URL), **wait and observe again** — don't act on stale or incomplete state. Only proceed when you can identify the element to interact with.
66
+ 1. **Open page** — get or create your page, navigate to URL
67
+ 2. **Observe** — print `state.page.url()` + `snapshot()`. Always print URL — pages can redirect unexpectedly.
68
+ 3. **Check** — if page isn't ready (loading, wrong URL, content missing), wait and observe again
71
69
  4. **Act** — perform one action (click, type, submit)
72
- 5. **Observe again** — print URL + snapshot to verify the action's effect. If the action didn't take effect (nothing changed, page still loading), wait and observe again before proceeding.
73
- 6. **Repeat** — continue from step 3 until the task is complete
74
-
75
- ```
76
- ┌─────────────────────────────────────────────┐
77
- │ open page + goto URL │
78
- └──────────────────┬──────────────────────────┘
79
-
80
- ┌────────────────┐
81
- ┌───►│ observe │◄─────────────────┐
82
- │ │ (url + snapshot) │ │
83
- │ └───────┬────────┘ │
84
- │ ▼ │
85
- │ ┌────────────────┐ │
86
- │ │ check │ │
87
- │ │ (read result) │ │
88
- │ └───┬────────┬───┘ │
89
- │ not │ │ ready │
90
- │ ready │ ▼ │
91
- └────────┘ ┌────────────────┐ │
92
- │ act │ │
93
- │ (click/type) │─────────────┘
94
- └────────────────┘
95
- ```
96
-
97
- **Example: opening a Framer plugin via the command palette**
98
-
99
- Each step is a separate execute call. Notice how every action is followed by a snapshot to verify what happened:
70
+ 5. **Observe again** — print URL + snapshot to verify the action's effect
71
+ 6. **Repeat** from step 3 until task is complete
100
72
 
101
73
  ```js
102
- // 1. Open page and observe always print URL first
74
+ // Each step should be a separate execute call:
75
+ // Step 1: navigate + observe
103
76
  state.page = context.pages().find((p) => p.url() === 'about:blank') ?? (await context.newPage())
104
- await state.page.goto('https://framer.com/projects/my-project', { waitUntil: 'domcontentloaded' })
77
+ await state.page.goto('https://example.com', { waitUntil: 'domcontentloaded' })
105
78
  console.log('URL:', state.page.url())
106
79
  await snapshot({ page: state.page }).then(console.log)
107
80
  ```
108
81
 
109
82
  ```js
110
- // 2. Act: open command palette → observe result
111
- await state.page.keyboard.press('Meta+k')
83
+ // Step 2: act + observe
84
+ await state.page.locator('button:has-text("Submit")').click()
112
85
  console.log('URL:', state.page.url())
113
- await snapshot({ page: state.page, search: /dialog|Search/ }).then(console.log)
114
- // If dialog didn't appear, observe again before retrying
86
+ await snapshot({ page: state.page }).then(console.log)
115
87
  ```
116
88
 
89
+ If nothing changed after an action, try `waitForPageLoad({ page: state.page, timeout: 3000 })` or you may have clicked the wrong element.
90
+
91
+ **Deeper observation** — when snapshots aren't enough to understand what happened, combine multiple channels:
92
+
117
93
  ```js
118
- // 3. Act: type search query observe result
119
- await state.page.keyboard.type('MCP')
120
- console.log('URL:', state.page.url())
121
- await snapshot({ page: state.page, search: /MCP/ }).then(console.log)
94
+ // Check console for errors after an action
95
+ const errors = await getLatestLogs({ page: state.page, search: /error|fail/i, count: 20 })
96
+
97
+ // Combine snapshot + logs for full picture
98
+ const snap = await snapshot({ page: state.page, search: /dialog|error|message/ })
99
+ const logs = await getLatestLogs({ page: state.page, search: /error/i, count: 10 })
100
+ console.log('UI:', snap)
101
+ console.log('Logs:', logs)
122
102
  ```
123
103
 
124
- ```js
125
- // 4. Act: press Enter → observe plugin loaded
126
- await state.page.keyboard.press('Enter')
127
- await state.page.waitForTimeout(1000)
128
- console.log('URL:', state.page.url())
129
- const frame = state.page.frames().find((f) => f.url().includes('plugins.framercdn.com'))
130
- await snapshot({ page: state.page, frame: frame || undefined }).then(console.log)
131
- // If frame not found, wait and observe again — plugin may still be loading
132
- ```
133
-
134
- **Other ways to observe action results:**
135
-
136
- Snapshots are the primary feedback mechanism, but some actions have side effects that are better observed through other channels:
137
-
138
- - **Console logs** — check for errors or app state after an action:
139
- ```js
140
- await getLatestLogs({ page: state.page, search: /error|fail/i, count: 20 })
141
- ```
142
- - **Network requests** — verify API calls were made after a form submit or button click:
143
- ```js
144
- state.page.on('response', async (res) => {
145
- if (res.url().includes('/api/')) {
146
- console.log(res.status(), res.url())
147
- }
148
- })
149
- ```
150
- - **URL changes** — confirm navigation happened:
151
- ```js
152
- console.log(state.page.url())
153
- ```
154
- - **Screenshots** — only for visual layout issues (see "choosing between snapshot methods" below).
104
+ Use `getLatestLogs()` for console errors, `state.page.url()` for navigation, screenshots only for visual layout issues.
155
105
 
156
106
  ## common mistakes to avoid
157
107
 
@@ -177,13 +127,9 @@ await state.page.keyboard.press('Meta+v') // always verify with screenshot!
177
127
  ```
178
128
 
179
129
  **3. Using stale locators from old snapshots**
180
- Locators (especially ones with `>> nth=`) can change when the page updates. Always get a fresh snapshot before clicking:
130
+ Locators (especially ones with `>> nth=`) can change when the page updates. Always get a fresh snapshot before clicking, then immediately use locators from that output:
181
131
 
182
132
  ```js
183
- // BAD: using ref from minutes ago
184
- await state.page.locator('[id="old-id"]').click() // element may have changed
185
-
186
- // GOOD: get fresh snapshot, then immediately use locators from it
187
133
  await snapshot({ page: state.page, showDiffSinceLastCall: true })
188
134
  // Now use the NEW locators from this output
189
135
  ```
@@ -198,29 +144,22 @@ await screenshotWithAccessibilityLabels({ page: state.page })
198
144
  ```
199
145
 
200
146
  **5. Text concatenation without line breaks**
201
- `keyboard.type()` doesn't insert newlines from `\n` in strings. Use `keyboard.press('Enter')`:
147
+ `keyboard.type()` doesn't insert newlines from `\n` in strings. Use `keyboard.press('Enter')` between lines:
202
148
 
203
149
  ```js
204
- // BAD: newlines in string don't create line breaks
205
- await state.page.keyboard.type('Line 1\nLine 2') // becomes "Line 1Line 2"
206
-
207
- // GOOD: use Enter key for line breaks
208
150
  await state.page.keyboard.type('Line 1')
209
151
  await state.page.keyboard.press('Enter')
210
152
  await state.page.keyboard.type('Line 2')
211
153
  ```
212
154
 
213
155
  **6. Quote escaping in bash**
214
- Bash parses `$`, backticks, and `\` inside double-quoted strings. This silently corrupts JS code containing dollar signs (regex like `/\$[\d.]+/`), template literals, or backslash patterns.
156
+ Bash parses `$`, backticks, and `\` inside double-quoted strings. This silently corrupts JS code. Always use single quotes or heredoc:
215
157
 
216
158
  ```bash
217
- # BAD: double quotes — bash interprets $ and backticks in your JS
218
- playwriter -s 1 -e "const price = text.match(/\$[\d.]+/)"
219
-
220
- # GOOD: single quotes — bash passes everything through literally
159
+ # single quotes — bash passes everything through literally
221
160
  playwriter -s 1 -e 'await state.page.locator(`[id="_r_a_"]`).click()'
222
161
 
223
- # GOOD: heredoc for complex code with mixed quotes
162
+ # heredoc for complex code with mixed quotes
224
163
  playwriter -s 1 -e "$(cat <<'EOF'
225
164
  await state.page.locator('[id="_r_a_"]').click()
226
165
  const match = html.match(/\$[\d.]+/g)
@@ -229,17 +168,10 @@ EOF
229
168
  ```
230
169
 
231
170
  **7. Using screenshots when snapshots suffice**
232
- Screenshots + image analysis is expensive and slow. Only use screenshots for visual/CSS issues:
171
+ Screenshots + image analysis is expensive and slow. Only use screenshots for visual/CSS issues. Use snapshot for text checks:
233
172
 
234
173
  ```js
235
- // BAD: screenshot to check if text appeared (wastes tokens on image analysis)
236
- await state.page.screenshot({ path: 'check.png', scale: 'css' })
237
-
238
- // GOOD: snapshot is text — fast, cheap, searchable
239
174
  await snapshot({ page: state.page, search: /expected text/i })
240
-
241
- // GOOD: evaluate DOM directly for content checks
242
- const text = await state.page.evaluate(() => document.querySelector('.message')?.textContent)
243
175
  ```
244
176
 
245
177
  **8. Assuming page content loaded**
@@ -254,28 +186,19 @@ await waitForPageLoad({ page: state.page, timeout: 5000 })
254
186
  ```
255
187
 
256
188
  **9. Not using playwriter for JS-rendered sites**
257
- Do NOT waste context trying webfetch, curl, or Playwright CLI screenshots on SPAs (Instagram, Twitter, etc.). These sites return empty HTML shells — the real content is rendered by JavaScript. Use playwriter with a real browser session instead:
189
+ Do NOT waste context trying webfetch, curl, or Playwright CLI screenshots on SPAs (Instagram, Twitter, etc.). These return empty HTML shells. Use playwriter directly:
258
190
 
259
191
  ```js
260
- // BAD: webfetch/curl on Instagram returns empty HTML, grep finds nothing, huge context wasted
261
- // BAD: Playwright CLI screenshot needs browser install, produces blank/modal-blocked images
262
-
263
- // GOOD: use playwriter — real browser, full JS rendering, interactive
264
192
  state.page = context.pages().find((p) => p.url() === 'about:blank') ?? (await context.newPage())
265
193
  await state.page.goto('https://www.instagram.com/p/ABC123/', { waitUntil: 'domcontentloaded' })
266
194
  await waitForPageLoad({ page: state.page, timeout: 8000 })
267
195
  await snapshot({ page: state.page, search: /cookie|consent|accept/i }).then(console.log)
268
- // Now you can see modals, dismiss them, navigate carousels, extract content
269
196
  ```
270
197
 
271
198
  **10. Login buttons that open popups**
272
- Playwriter extension cannot control popup windows. If a login button opens a popup (common with OAuth/SSO), use cmd+click to open in a new tab instead:
199
+ Playwriter cannot control popup windows. Use cmd+click to open in a new tab instead:
273
200
 
274
201
  ```js
275
- // BAD: popup window is not controllable by playwriter
276
- await state.page.click('button:has-text("Login with Google")')
277
-
278
- // GOOD: cmd+click opens in new tab that playwriter can control
279
202
  await state.page.locator('button:has-text("Login with Google")').click({ modifiers: ['Meta'] })
280
203
  await state.page.waitForTimeout(2000)
281
204
 
@@ -303,14 +226,9 @@ await state.page.getByRole('radio', { name: 'Nope, Vanilla' }).click()
303
226
  ```
304
227
 
305
228
  **12. Never use `dispatchEvent` or `{ force: true }` to bypass blockers**
306
- `dispatchEvent(new MouseEvent(...))` and `{ force: true }` bypass Playwright checks but **do not trigger React/Vue/Svelte handlers** — state won't update. The same applies to `element.click()` inside `page.evaluate()`. If a click "succeeds" but nothing changes, you're either clicking the wrong node or using the wrong interaction pattern:
229
+ `dispatchEvent(new MouseEvent(...))`, `{ force: true }`, and `element.click()` inside `page.evaluate()` bypass Playwright checks but **do not trigger React/Vue/Svelte handlers** — state won't update. Use snapshot to find the real interactive element:
307
230
 
308
231
  ```js
309
- // BAD: heading click bypasses overlay but React ignores it
310
- await state.page.locator('h3:has-text("Node.js")').click({ force: true })
311
- // BAD: evaluate click bypasses all Playwright input simulation
312
- await state.page.evaluate(() => document.querySelector('button').click())
313
- // GOOD: snapshot shows the real interactive element is a radio, not the heading
314
232
  await state.page.getByRole('radio', { name: 'Node.js' }).click()
315
233
  ```
316
234
 
@@ -326,30 +244,12 @@ When something doesn't respond to a click, do NOT start inspecting CDP event lis
326
244
  3. Take another `snapshot()` to see what changed
327
245
  4. Only investigate DOM internals if correct interaction patterns produce zero response after 2–3 attempts
328
246
 
329
- ## checking page state
330
-
331
- After any action (click, submit, navigate), verify what happened. Always print URL first, then snapshot:
332
-
333
- ```js
334
- // Always print URL first, then snapshot
335
- console.log('URL:', state.page.url())
336
- await snapshot({ page: state.page }).then(console.log)
337
-
338
- // Filter for specific content when snapshot is large
339
- console.log('URL:', state.page.url())
340
- await snapshot({ page: state.page, search: /dialog|button|error/i }).then(console.log)
341
- ```
342
-
343
- If nothing changed, try `await waitForPageLoad({ page: state.page, timeout: 3000 })` or you may have clicked the wrong element.
344
-
345
247
  ## accessibility snapshots
346
248
 
347
249
  ```js
348
250
  await snapshot({ page: state.page, search?, showDiffSinceLastCall? })
349
251
  ```
350
252
 
351
- `accessibilitySnapshot` is still available as an alias for backward compatibility.
352
-
353
253
  - `search` - string/regex to filter results (returns first 10 matching lines)
354
254
  - `showDiffSinceLastCall` - returns diff since last snapshot (default: `true`, but `false` when `search` is provided). Pass `false` to get full snapshot.
355
255
 
@@ -388,12 +288,6 @@ const locator = refToLocator({ ref: 'e3' })
388
288
  await state.page.locator(locator!).click()
389
289
  ```
390
290
 
391
- ```js
392
- await state.page.locator('[id="nav-home"]').click()
393
- await state.page.locator('[data-testid="docs-link"]').click()
394
- await state.page.locator('role=link[name="Blog"]').click()
395
- ```
396
-
397
291
  Search for specific elements:
398
292
 
399
293
  ```js
@@ -416,38 +310,11 @@ await snapshot({ locator: state.page.locator('form#checkout') })
416
310
 
417
311
  Use this whenever the full page snapshot is dominated by navigation or layout elements you don't need. It saves significant tokens and makes the output much easier to parse.
418
312
 
419
- **Filtering large snapshots in JS** — when the built-in `search` isn't enough (e.g., you need multiple patterns or custom logic), filter the snapshot string directly:
420
-
421
- ```js
422
- const snap = await snapshot({ page: state.page, showDiffSinceLastCall: false })
423
- const relevant = snap
424
- .split('\n')
425
- .filter((l) => l.includes('dialog') || l.includes('error') || l.includes('button'))
426
- .join('\n')
427
- console.log(relevant)
428
- ```
429
-
430
- This is much cheaper than taking a screenshot — use it as your primary debugging tool for verifying text content, checking if elements exist, or confirming state changes.
313
+ **Filtering large snapshots in JS** — when `search` isn't enough, filter the string directly: `snap.split('\n').filter(l => l.includes('dialog') || l.includes('error')).join('\n')`
431
314
 
432
315
  ## choosing between snapshot methods
433
316
 
434
- Both `snapshot` and `screenshotWithAccessibilityLabels` use the same ref system, so you can combine them effectively.
435
-
436
- **Use `snapshot` when:**
437
-
438
- - Page has simple, semantic structure (articles, forms, lists)
439
- - You need to search for specific text or patterns
440
- - Token usage matters (text is smaller than images)
441
- - You need to process the output programmatically
442
-
443
- **Use `screenshotWithAccessibilityLabels` when:**
444
-
445
- - Page has complex visual layout (grids, galleries, dashboards, maps)
446
- - Spatial position matters (e.g., "first image", "top-left button")
447
- - DOM order doesn't match visual order
448
- - You need to understand the visual hierarchy
449
-
450
- **Combining both:** Use screenshot first to understand layout and identify target elements visually, then use `snapshot({ search: /pattern/ })` for efficient searching in subsequent calls.
317
+ Use `snapshot` for text-heavy pages (forms, articles) — fast, cheap, searchable. Use `screenshotWithAccessibilityLabels` for complex visual layouts (grids, galleries, dashboards) where spatial position matters. Both share the same ref system and can be combined.
451
318
 
452
319
  ## selector best practices
453
320
 
@@ -533,19 +400,25 @@ await waitForPageLoad({ page: state.page, timeout: 5000 })
533
400
 
534
401
  ## common patterns
535
402
 
536
- **Authenticated fetches** - to access protected resources, fetch from within page context (includes session cookies automatically):
403
+ **Authenticated fetches** - fetch from within page context to include session cookies automatically:
537
404
 
538
405
  ```js
539
- // BAD: curl/external requests don't have session cookies
540
- // curl -H "Cookie: ..." often fails due to missing cookies or CSRF
541
-
542
- // GOOD: fetch inside state.page.evaluate uses browser's full session
543
406
  const data = await state.page.evaluate(async (url) => {
544
407
  const resp = await fetch(url)
545
408
  return await resp.text()
546
409
  }, 'https://example.com/protected/resource')
547
410
  ```
548
411
 
412
+ **Read page cookies via CDP** - use `Network.getCookies` on the page CDP session:
413
+
414
+ ```js
415
+ const cdp = await getCDPSession({ page: state.page })
416
+ const { cookies } = await cdp.send('Network.getCookies', { urls: [state.page.url()] })
417
+ console.log(cookies)
418
+ ```
419
+
420
+ MUST use this for page-scoped cookies in extension mode. `Storage.getCookies` is a root-session command and will fail in playwriter.
421
+
549
422
  **Downloading large data** - console output truncates large strings. Trigger a browser download instead:
550
423
 
551
424
  ```js
@@ -571,16 +444,6 @@ await state.page.evaluate(async (url) => {
571
444
 
572
445
  Instead, use simpler alternatives (single download via `a.click()`, store data in `state`, etc).
573
446
 
574
- **Links that open new tabs** - playwriter cannot control popup windows opened via `window.open`. Use cmd+click to open in a controllable new tab instead (see mistake #9 above for a full example):
575
-
576
- ```js
577
- await state.page.locator('a[target=_blank]').click({ modifiers: ['Meta'] })
578
- await state.page.waitForTimeout(1000)
579
- const pages = context.pages()
580
- const newTab = pages[pages.length - 1]
581
- console.log('New tab URL:', newTab.url())
582
- ```
583
-
584
447
  **Downloads** - capture and save:
585
448
 
586
449
  ```js
@@ -681,19 +544,7 @@ const fullHtml = await getCleanHTML({ locator: state.page, showDiffSinceLastCall
681
544
  - `showDiffSinceLastCall` - returns diff since last call (default: `true`, but `false` when `search` is provided). Pass `false` to get full HTML.
682
545
  - `includeStyles` - keep style and class attributes (default: false)
683
546
 
684
- **HTML processing:**
685
- The function cleans HTML for compact, readable output:
686
-
687
- - **Removes tags**: script, style, link, meta, noscript, svg, head
688
- - **Unwraps nested wrappers**: Empty divs/spans with no attributes that only wrap a single child are collapsed (e.g., `<div><div><div><p>text</p></div></div></div>` → `<div><p>text</p></div>`)
689
- - **Removes empty elements**: Elements with no attributes and no content are removed
690
- - **Truncates long values**: Attribute values >200 chars and text content >500 chars are truncated
691
-
692
- **Attributes kept (summary):**
693
-
694
- - Common semantic and ARIA attributes (e.g., `href`, `name`, `type`, `aria-*`)
695
- - All `data-*` test attributes
696
- - Frequently used test IDs and special attributes (e.g., `testid`, `qa`, `e2e`, `vimium-label`)
547
+ Cleans HTML automatically: removes script/style/svg/head tags, unwraps empty wrappers, removes empty elements, truncates long values. Keeps semantic attributes (`href`, `name`, `type`, `aria-*`, `data-*`).
697
548
 
698
549
  **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:
699
550
 
@@ -722,12 +573,6 @@ The main article content as plain text, with paragraphs preserved...
722
573
  - `search` - string/regex to filter content (returns first 10 matching lines with 5 lines context)
723
574
  - `showDiffSinceLastCall` - returns diff since last call (default: `true`, but `false` when `search` is provided). Pass `false` to get full content.
724
575
 
725
- **Use cases:**
726
-
727
- - Extract article text for LLM processing without HTML noise
728
- - Get readable content from news sites, blogs, documentation
729
- - Compare content changes after interactions
730
-
731
576
  **waitForPageLoad** - smart load detection that ignores analytics/ads:
732
577
 
733
578
  ```js
@@ -808,94 +653,51 @@ Labels are color-coded: yellow=links, orange=buttons, coral=inputs, pink=checkbo
808
653
 
809
654
  **resizeImage** - shrink an image in-place so it consumes fewer tokens when read back into context. `await resizeImage({ input: './screenshot.png' })`. Also accepts `width`, `height`, `maxDimension`, `quality`, `output`.
810
655
 
811
- **recording.start / recording.stop** - record the page as a video at native FPS (30-60fps). Uses `chrome.tabCapture` in the extension context, so **recording survives page navigation**. Video is saved as mp4.
812
-
813
- While recording is active, Playwriter automatically overlays a smooth ghost cursor that follows automated mouse actions (`page.mouse.*`, `locator.click()`, hover flows) using `page.onMouseAction` from the Playwright fork.
814
-
815
- For demos where cursor movement should be visible and human-like, drive the page with interaction methods (`locator.click()`, `page.click()`, `page.mouse.move()`, `press`, typing). Avoid skipping interactions with direct state jumps (for example, `goto(itemUrl)` instead of clicking the link) when your goal is to show realistic pointer motion in the recording.
656
+ **recording.start / recording.stop** - record the page as a video at native FPS (30-60fps). Uses `chrome.tabCapture` so **recording survives page navigation**. Auto-overlays a ghost cursor that follows mouse actions. Requires user to have clicked the Playwriter extension icon on the tab. Auto-resizes viewport to 16:9 (override with `aspectRatio: null`). Auto-stops after 15 min (override with `maxDurationMs`).
816
657
 
817
- **Note**: Recording requires the user to have clicked the Playwriter extension icon on the tab. This grants `activeTab` permission needed for `chrome.tabCapture`. Recording works on tabs where the icon was clicked - if you need to record a new tab, ask the user to click the icon on it first.
658
+ For demos, use interaction methods (`locator.click()`, `page.mouse.move()`) instead of `goto()` to show realistic cursor motion.
818
659
 
819
660
  ```js
820
- // Start recording - outputPath must be specified upfront
821
661
  await recording.start({
822
662
  page: state.page,
823
663
  outputPath: './recording.mp4',
824
- frameRate: 30, // default: 30
825
- audio: false, // default: false (tab audio)
826
- videoBitsPerSecond: 2500000, // 2.5 Mbps
664
+ frameRate: 30, // default
665
+ audio: false, // default (tab audio)
666
+ videoBitsPerSecond: 2500000,
667
+ aspectRatio: { width: 16, height: 9 }, // default, set null to skip
668
+ maxDurationMs: 15 * 60 * 1000, // default, set 0 to disable
827
669
  })
828
670
 
829
- // Navigate around - recording continues!
671
+ // Recording survives navigation
830
672
  await state.page.click('a')
831
673
  await state.page.waitForLoadState('domcontentloaded')
832
- await state.page.goBack()
833
674
 
834
- // Stop and get result
835
- const { path, duration, size } = await recording.stop({ page: state.page })
836
- console.log(`Saved ${size} bytes, duration: ${duration}ms`)
837
- ```
838
-
839
- Additional recording utilities:
840
-
841
- ```js
842
- // Check if recording is active
843
- const { isRecording, startedAt } = await recording.isRecording({ page: state.page })
675
+ // Stop save full result including executionTimestamps for createDemoVideo
676
+ state.recordingResult = await recording.stop({ page: state.page })
844
677
 
845
- // Cancel recording without saving
846
- await recording.cancel({ page: state.page })
678
+ // Other: recording.isRecording({ page }), recording.cancel({ page })
847
679
  ```
848
680
 
849
- **ghostCursor.show / ghostCursor.hide** - manually show or hide the in-page cursor overlay. Useful for screenshots and demos even when recording is not running.
681
+ **ghostCursor.show / ghostCursor.hide** - show/hide cursor overlay for screenshots and demos:
850
682
 
851
683
  ```js
852
- // Show cursor in the center (or keep current position if already visible)
853
- await ghostCursor.show({ page: state.page })
854
-
855
- // Optional styles: 'minimal' (default triangular pointer), 'dot', 'screenstudio'
856
- await ghostCursor.show({ page: state.page, style: 'minimal' })
857
-
858
- // Hide cursor overlay
684
+ await ghostCursor.show({ page: state.page, style: 'minimal' }) // 'minimal', 'dot', 'screenstudio'
859
685
  await ghostCursor.hide({ page: state.page })
860
686
  ```
861
687
 
862
- `startRecording`, `stopRecording`, `isRecording`, and `cancelRecording` remain available as backward-compatible aliases.
863
-
864
- **Key difference from getDisplayMedia**: This approach uses `chrome.tabCapture` which runs in the extension context, not the page. The recording persists across navigations because the extension holds the `MediaRecorder`, not the page's JavaScript context.
865
-
866
- **createDemoVideo** - create a polished demo video from a recording by automatically speeding up idle sections (time between execute() calls) while keeping interactions at normal speed. Useful for creating demo videos of agent workflows without long pauses.
867
-
868
- While recording is active, playwriter tracks when each `execute()` call starts and ends. `recording.stop()` returns these timestamps alongside the video file. `createDemoVideo` uses this data to identify idle gaps and speed them up with ffmpeg in a single pass.
869
-
870
- A 1-second buffer is preserved around each interaction so viewers see context before and after each action.
871
-
872
- Requires `ffmpeg` and `ffprobe` installed on the system.
873
-
874
- **Timeout**: `createDemoVideo` runs ffmpeg on the full recording and can take 60–120+ seconds. Always pass `--timeout 120000` (or higher) to the playwriter execute call that contains it, otherwise it will silently time out before the file is written.
875
-
876
- ```js
877
- // Start recording
878
- await recording.start({ page: state.page, outputPath: './recording.mp4' })
879
- ```
880
-
881
- ```js
882
- // ... multiple execute() calls with browser interactions ...
883
- // Each call's timing is tracked automatically while recording is active
884
- ```
688
+ **createDemoVideo** - speeds up idle sections (time between execute() calls) while keeping interactions at normal speed. Requires `ffmpeg`/`ffprobe`. Timestamps are tracked automatically during recording and returned by `recording.stop()`. **Timeout**: can take 60–120+ seconds, always pass `--timeout 120000` or higher.
885
689
 
886
690
  ```js
887
- // Stop recording executionTimestamps is included in the result
888
- const recordingResult = await recording.stop({ page: state.page })
691
+ // After recording.stop(), save full result to state (executionTimestamps powers idle detection)
692
+ state.recordingResult = await recording.stop({ page: state.page })
889
693
 
890
- // Create demo video idle gaps are sped up 4x (default)
694
+ // In a SEPARATE execute call with --timeout 120000:
891
695
  const demoPath = await createDemoVideo({
892
- recordingPath: recordingResult.path,
893
- durationMs: recordingResult.duration,
894
- executionTimestamps: recordingResult.executionTimestamps,
895
- speed: 5, // optional, default 5x for idle sections
896
- // outputFile: './demo.mp4', // optional, defaults to recording-demo.mp4
696
+ recordingPath: state.recordingResult.path,
697
+ durationMs: state.recordingResult.duration,
698
+ executionTimestamps: state.recordingResult.executionTimestamps,
699
+ speed: 6, // default 6x for idle sections
897
700
  })
898
- console.log('Demo video:', demoPath)
899
701
  ```
900
702
 
901
703
  ## pinned elements
@@ -995,60 +797,7 @@ console.log(data)
995
797
 
996
798
  Clean up listeners when done: `state.page.removeAllListeners('request'); state.page.removeAllListeners('response');`
997
799
 
998
- ## debugging web apps
999
-
1000
- When debugging why a web app isn't working (e.g., content not rendering, API errors, state issues), use these techniques **before** resorting to screenshots:
1001
-
1002
- **1. Console logs** — use `getLatestLogs` to check for errors:
1003
-
1004
- ```js
1005
- const errors = await getLatestLogs({ page: state.page, search: /error|fail/i, count: 20 })
1006
- const appLogs = await getLatestLogs({ page: state.page, search: /myComponent|state/i })
1007
- ```
1008
-
1009
- **2. DOM inspection via evaluate** — check content directly without screenshots:
1010
-
1011
- ```js
1012
- const info = await state.page.evaluate(() => {
1013
- const msgs = document.querySelectorAll('.message')
1014
- return Array.from(msgs).map((m) => ({
1015
- text: m.textContent?.slice(0, 200),
1016
- visible: m.offsetHeight > 0,
1017
- }))
1018
- })
1019
- console.log(JSON.stringify(info, null, 2))
1020
- ```
1021
-
1022
- **3. Combine snapshot + logs for full picture:**
1023
-
1024
- ```js
1025
- await state.page.keyboard.press('Enter')
1026
- await state.page.waitForTimeout(2000)
1027
-
1028
- const snap = await snapshot({ page: state.page, search: /dialog|error|message/ })
1029
- const logs = await getLatestLogs({ page: state.page, search: /error/i, count: 10 })
1030
- console.log('UI:', snap)
1031
- console.log('Logs:', logs)
1032
- ```
1033
-
1034
- ## capabilities
1035
-
1036
- Examples of what playwriter can do:
1037
-
1038
- - Monitor console logs while user reproduces a bug
1039
- - Intercept network requests to reverse-engineer APIs and build SDKs
1040
- - Scrape data by replaying paginated API calls instead of scrolling DOM
1041
- - Get accessibility snapshot to find elements, then automate interactions
1042
- - Use visual screenshots to understand complex layouts like image grids, dashboards, or maps
1043
- - Debug issues by collecting logs and controlling the page simultaneously
1044
- - Handle popups, downloads, iframes, and dialog boxes
1045
- - Record videos of browser sessions that survive page navigation
1046
-
1047
- ## computer use
1048
-
1049
- Playwriter provides the same browser control as Anthropic's `computer_20250124` tool and the Claude Chrome extension, using Playwright APIs instead of screenshot-based coordinate clicking. No computer use beta needed.
1050
-
1051
- This section covers low-level mouse/keyboard APIs not documented elsewhere. For locator-based clicking, screenshots, navigation, forms, evaluate, snapshots, and network interception see their dedicated sections above.
800
+ ## computer use (low-level mouse/keyboard)
1052
801
 
1053
802
  ### clicking
1054
803
 
@@ -1151,19 +900,4 @@ Prefer locator-based actions over coordinates — locators are stable across scr
1151
900
 
1152
901
  ## Ghost Browser integration
1153
902
 
1154
- Playwriter supports [Ghost Browser](https://ghostbrowser.com/) for multi-identity automation. When running in Ghost Browser, the `chrome` object exposes APIs to control identities, proxies, and sessions - useful for managing multiple accounts, rotating proxies, or isolated cookie sessions.
1155
-
1156
- ```js
1157
- // List identities and open tabs in different ones
1158
- const identities = await chrome.projects.getIdentitiesList()
1159
- await chrome.ghostPublicAPI.openTab({ url: 'https://reddit.com', identity: identities[0].id })
1160
-
1161
- // Assign proxies per tab or identity
1162
- const proxies = await chrome.ghostProxies.getList()
1163
- await chrome.ghostProxies.setTabProxy(tabId, proxies[0].id)
1164
- ```
1165
-
1166
- For complete API reference with all methods, types, and examples, read:
1167
- `extension/src/ghost-browser-api.d.ts`
1168
-
1169
- Note: Only works in Ghost Browser. In regular Chrome, calls fail with "not available".
903
+ When running in [Ghost Browser](https://ghostbrowser.com/), the `chrome` object exposes APIs for multi-identity automation (identities, proxies, sessions). See `extension/src/ghost-browser-api.d.ts` for full API reference. Only works in Ghost Browser — calls fail in regular Chrome.
@@ -4,15 +4,29 @@
4
4
  var __defProp = Object.defineProperty;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ function __accessProp(key) {
8
+ return this[key];
9
+ }
10
+ var __toESMCache_node;
11
+ var __toESMCache_esm;
7
12
  var __toESM = (mod, isNodeMode, target) => {
13
+ var canCache = mod != null && typeof mod === "object";
14
+ if (canCache) {
15
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
16
+ var cached = cache.get(mod);
17
+ if (cached)
18
+ return cached;
19
+ }
8
20
  target = mod != null ? __create(__getProtoOf(mod)) : {};
9
21
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
22
  for (let key of __getOwnPropNames(mod))
11
23
  if (!__hasOwnProp.call(to, key))
12
24
  __defProp(to, key, {
13
- get: () => mod[key],
25
+ get: __accessProp.bind(mod, key),
14
26
  enumerable: true
15
27
  });
28
+ if (canCache)
29
+ cache.set(mod, to);
16
30
  return to;
17
31
  };
18
32
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
@@ -1654,7 +1668,7 @@
1654
1668
  };
1655
1669
  });
1656
1670
 
1657
- // dist/_readability-entry-46963-1772299414460.js
1671
+ // dist/_readability-entry-64982-1773397884858.js
1658
1672
  var import_readability = __toESM(require_readability(), 1);
1659
1673
  globalThis.__readability = { Readability: import_readability.Readability, isProbablyReaderable: import_readability.isProbablyReaderable };
1660
1674
  })();
@@ -1 +1 @@
1
- {"version":3,"file":"recording-ghost-cursor.d.ts","sourceRoot":"","sources":["../src/recording-ghost-cursor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAA;AACnE,OAAO,EAAsE,KAAK,wBAAwB,EAAE,MAAM,mBAAmB,CAAA;AAErI,UAAU,0BAA0B;IAClC,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAA;CACpC;AAED,UAAU,sBAAsB;IAC9B,IAAI,CAAC,EAAE,IAAI,CAAA;IACX,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,qBAAa,8BAA8B;IACzC,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAA6C;IACvF,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAqC;IAC5E,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;gBAEvC,OAAO,EAAE;QAAE,MAAM,EAAE,0BAA0B,CAAA;KAAE;IAI3D,0BAA0B,CAAC,OAAO,EAAE;QAClC,OAAO,EAAE,cAAc,CAAA;QACvB,WAAW,EAAE,IAAI,CAAA;QACjB,MAAM,CAAC,EAAE,sBAAsB,CAAA;KAChC,GAAG,IAAI;IAoBF,kBAAkB,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAkC1D,mBAAmB,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAa3D,IAAI,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,IAAI,CAAC;QAAC,aAAa,CAAC,EAAE,wBAAwB,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAKtF,IAAI,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CAInD"}
1
+ {"version":3,"file":"recording-ghost-cursor.d.ts","sourceRoot":"","sources":["../src/recording-ghost-cursor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAA;AACnE,OAAO,EAIL,KAAK,wBAAwB,EAC9B,MAAM,mBAAmB,CAAA;AAE1B,UAAU,0BAA0B;IAClC,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAA;CACpC;AAED,UAAU,sBAAsB;IAC9B,IAAI,CAAC,EAAE,IAAI,CAAA;IACX,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,qBAAa,8BAA8B;IACzC,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAA6C;IACvF,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAqC;IAC5E,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;gBAEvC,OAAO,EAAE;QAAE,MAAM,EAAE,0BAA0B,CAAA;KAAE;IAI3D,0BAA0B,CAAC,OAAO,EAAE;QAClC,OAAO,EAAE,cAAc,CAAA;QACvB,WAAW,EAAE,IAAI,CAAA;QACjB,MAAM,CAAC,EAAE,sBAAsB,CAAA;KAChC,GAAG,IAAI;IAoBF,kBAAkB,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAmC1D,mBAAmB,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAa3D,IAAI,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,IAAI,CAAC;QAAC,aAAa,CAAC,EAAE,wBAAwB,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAKtF,IAAI,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CAInD"}