Commit Graph

821 Commits

Author SHA1 Message Date
Tommy D. Rossi 2d7e293d1f hardening: namespace snapshot cache keys to avoid sentinel collisions
Use 'page' and 'locator:' prefixed keys instead of raw '__page__' sentinel,
preventing theoretical collision if a CSS selector equals '__page__'.
2026-02-28 14:30:16 +01:00
Tommy D. Rossi ece02cd4ad fix: isolate snapshot diff cache by locator scope + use Locator.selector()
The snapshot diff cache in executor.ts used only the Page object as key, so
calling snapshot({ page }) then snapshot({ page, locator }) would diff a
locator-scoped subtree against the previous full-page snapshot — producing
misleading diffs full of 'removed' lines that are simply outside the scope.

Fix: change lastSnapshots from WeakMap<Page, string> to a two-level
WeakMap<Page, Map<string, string>> keyed by locator.selector() (or '__page__'
for full-page). This matches the pattern already used by getCleanHTML in
clean-html.ts.

Also replaced (locator as any)._selector with the new typed
locator.selector() API from @xmorse/playwright-core 1.59.5 in both
executor.ts and clean-html.ts — zero `as any` casts.

Added locator-selector.test.ts with 28 inline snapshots covering CSS
selectors, getByRole, getByText, getByPlaceholder, getByLabel, getByAltText,
getByTestId, chained/filtered/described locators, and and/or combinators.
2026-02-28 14:27:52 +01:00
Tommy D. Rossi bf319088ab chore: gitignore generated skill docs (source of truth is playwriter/src/skill.md) 2026-02-28 14:21:30 +01:00
Tommy D. Rossi 2a2f21dd7b docs: use single quotes in all bash examples to prevent shell expansion
All `-e` arguments in bash examples now use single quotes instead of double
quotes. This prevents bash from interpreting `$`, backticks, and backslashes
inside JS code — a major pain point for agents that construct playwriter
commands programmatically.

Changes across skill docs, README, and website copies:

- **Single quotes for one-liners**: `playwriter -s 1 -e 'await page.goto("...")'`
- **Heredoc preferred for multiline**: `<<'EOF'` disables all bash expansion
- **Quoting rules summary**: explains when to use single quotes vs heredoc vs `$'...'`
- **Rewrote mistake #6**: now explains *why* double quotes break (bash `$` expansion
  silently corrupts regex like `/\$[\d.]+/` and template literals)
- **Added locator-scoped snapshot section**: shows `snapshot({ locator: page.locator("main") })`
  which dramatically reduces output size from ~150 lines to ~20 lines by snapshotting
  only a subtree instead of the full page
- **Added single-quote tip** to README quick start and SKILL.md stub

The locator-scoped snapshot feature already existed but wasn't documented.
Discovered during real-world Cloudflare dashboard automation where full page
snapshots were dominated by 60+ sidebar navigation links.
2026-02-28 14:03:34 +01:00
Tommy D. Rossi e856809657 fix: use AST node offsets to strip trailing semicolons in execute wrapping
Single-expression code ending with `;` (e.g. `await foo();`) was wrapped as
`return await (await foo();)` — the semicolon inside parens is a SyntaxError.

Refactored `shouldAutoReturn` into `getAutoReturnExpression` which returns the
exact expression source via acorn's `expr.start`/`expr.end` offsets, naturally
excluding the trailing semicolon. More robust than regex stripping since it uses
the parser we already have.

Closes #58
2026-02-28 10:45:30 +01:00
Tommy D. Rossi 6b71fc5351 add Playwright CLI comparison table to website and README
Closes #57

Highlights key differences: Playwriter uses your existing Chrome
(logins, extensions, cookies), can bypass captchas by disconnecting
the extension, lets you collaborate in the same browser window,
and gives full Playwright API + raw CDP access instead of a limited
shell command set.
2026-02-27 22:41:17 +01:00
Tommy D. Rossi 4d9a410799 playwriter skill self improvement: snapshot() is the primary tool, not screenshot + image agents
The biggest context waste pattern is: screenshot → spawn image-understanding agent →
read description → repeat. snapshot() gives the same information (element names, roles,
locators, text) instantly as text.

Changes:
- Split 'snapshot before screenshot' into two rules: one for snapshot vs screenshot,
  one explicitly saying snapshot replaces page.evaluate() DOM inspection
- Remove wrong claim that screenshotWithAccessibilityLabels returns images inline
  in CLI context (it doesn't — that's MCP-only)
- Rewrite mistake #13 as numbered steps: snapshot first, try correct interaction
  pattern second, investigate internals only as last resort
- Clarify when page.evaluate() IS still appropriate (state mutations, non-DOM data)
2026-02-27 21:19:40 +01:00
Tommy D. Rossi 356165da90 playwriter skill self improvement: clarify screenshotWithAccessibilityLabels is inline (no Task agent needed), add freehand drawing pattern for mouse.down/move/up, warn about createDemoVideo timeout, expand mistake #12 to cover evaluate().click(), add mistake #13 about over-investigating before trying correct interaction pattern, clarify snapshot() replaces page.evaluate() DOM inspection 2026-02-27 19:31:53 +01:00
Tommy D. Rossi c9809967e5 fix: hide tab group when relay dies instead of keeping it green
syncTabGroup was including 'connecting' tabs in the group unconditionally,
so when the relay WebSocket closed and all tabs transitioned to 'connecting',
the group stayed green. Now 'connecting' tabs are only included when the
relay is actually connected (connectionState === 'connected'). When the
relay is dead, 'connecting' tabs are excluded and the group gets cleaned up.

The onUpdated handler at line ~1601 already guards against the
ungroup→disconnect loop for 'connecting' tabs, so this is safe.
2026-02-27 09:04:43 +01:00
Tommy D. Rossi 6807209e85 fix: route CDP clients through live extension bindings
When an extension reconnects, clients can be rebound to a successor connection without reopening the Playwright socket. Command routing was still resolving via the extension id captured at client socket open, so rebound clients could hit stale routes and return Extension not connected.

Resolve the bound extension id from relay state on every incoming Playwright message, and prefer the newest same-stableKey successor when rebinding on extension close. This keeps reconnect handoff consistent between state transitions and runtime routing.
2026-02-27 09:01:53 +01:00
Tommy D. Rossi 44212ceda8 refactor: inline/remove single-use relay-state functions
- Inline incrementExtensionMessageId: trivially messageId + 1, no value
  as a separate function with one call site and one test
- Remove clearExtensionPendingRequests: redundant before removeExtension
  which deletes the entire extension entry (including pendingRequests)
- Fold removeClientsForExtension into removeExtension: always called
  together, callers can no longer forget to clean up bound clients

Net: -3 exported functions, -3 tests replaced by 1 new test verifying
removeExtension also removes bound playwright clients.
2026-02-26 21:27:41 +01:00
Tommy D. Rossi e584008eaf Merge branch 'main' of https://github.com/remorses/playwriter 2026-02-26 21:18:47 +01:00
Tommy D. Rossi 86e7f7d861 Update remote-access.md 2026-02-26 21:18:43 +01:00
Tommy D. Rossi e814dc3788 deeper serve description 2026-02-26 21:18:23 +01:00
Tommy D. Rossi f2728d791a Update relay-session.test.ts 2026-02-26 21:18:08 +01:00
Tommy D. Rossi 38aa756d8a refactor: centralize relay state into zustand store, deduplicate playwright client maps
Introduce relay-state.ts with a single Zustand vanilla store holding all relay
server state (extensions, playwright clients, targets). All state transitions are
pure functions: data in, data out, no I/O inside setState().

Key changes:
- Move scattered mutable Maps (extensionConnections, extensionKeyIndex,
  playwrightClients) into one RelayState atom managed by zustand/vanilla
- Co-locate I/O handles (ws, pingInterval, pendingRequests) on ExtensionEntry
  instead of separate Maps, following the same pattern for PlaywrightClient
- Merge PlaywrightClientState + PlaywrightClientIO into single PlaywrightClient
  type with { id, extensionId, ws } — eliminates the duplicate playwrightClientIO
  map that had to stay in sync (and was already buggy: removeClientsForExtension
  only cleaned playwrightClients but leaked playwrightClientIO entries)
- Derive extensionKeyIndex on demand via findExtensionByStableKey() linear scan
  instead of maintaining a separate index Map
- Add relay-state.test.ts with 38 pure data-in/data-out tests covering all
  state transitions and derivation helpers
2026-02-26 21:17:23 +01:00
Tommy D. Rossi c9492fb4b0 feat: optimize ffmpeg encoding for screen recordings on social media
Add hardware encoder detection with automatic fallback:
- h264_videotoolbox (macOS) → h264_nvenc/qsv/amf (Windows) → h264_nvenc/qsv (Linux) → libx264
- Probe caches result per process, 5s timeout per encoder test
- h264_vaapi excluded (needs -vaapi_device + hwupload in filter graph)

Add social-media-optimized encoding parameters:
- libx264: CRF 18 (near visually lossless for sharp text), preset fast,
  deblock=-1,-1 (preserves text edges), no -tune flag (animation blurs
  text, zerolatency is for real-time capture only)
- h264_videotoolbox: -q:v 80 (≈CRF 18 for screen content)
- Common: yuv420p, maxrate 25M (X.com cap), bufsize 50M, movflags +faststart

Skip redundant fps/scale filters on normal-speed passthrough segments
when output dimensions and frame rate match the probed input values.

Previously no -c:v or quality params were specified at all — ffmpeg
defaulted to libx264 -preset medium -crf 23, which is tuned for natural
video, not screen recordings, and purely CPU-based.

Tested on 48s 3440x1964 screen recording: 17s encode time with
VideoToolbox hardware acceleration, 5.5 Mbps output bitrate.
2026-02-26 20:31:19 +01:00
Tommy D. Rossi 85b7fb8a57 Create plan-centralize-relay-state.md 2026-02-26 16:47:06 +01:00
Tommy D. Rossi f3485b9d70 fix: default showDiffSinceLastCall to false when search is provided
Instead of hard-blocking diff when search is present, change the
default: showDiffSinceLastCall defaults to !search. This way agents
don't get implicit diffs that swallow their search results, but can
still explicitly pass showDiffSinceLastCall: true to combine both.

Applied to snapshot(), getPageMarkdown(), and getCleanHTML().
Updated skill.md docs to document the new default behavior.
2026-02-26 13:35:39 +01:00
Tommy D. Rossi de74b32c34 fix: skip diff mode when search string is provided in snapshot/markdown/html
When both showDiffSinceLastCall and search were set, the diff block
returned early and the search filter was silently ignored. Now all
three functions (snapshot, getPageMarkdown, getCleanHTML) skip diffing
when a search string is present, ensuring the agent always gets
filtered results.
2026-02-26 13:33:42 +01:00
Tommy D. Rossi 5c58cac7da docs: improve recording guidance and tune cursor defaults
Clarify recording guidance to prefer interaction-driven flows (click, hover, typing) so ghost cursor motion is visible during demos.

Tune ghost cursor movement defaults for faster, human-like motion and align cursor encoder script path handling with __dirname plus path.resolve conventions.
2026-02-26 13:16:12 +01:00
Tommy D. Rossi 69428e7240 docs: improve recording guidance and tune cursor defaults
Clarify recording guidance to prefer interaction-driven flows (click, hover, typing) so ghost cursor motion is visible during demos.

Tune ghost cursor movement defaults for faster, human-like motion and align cursor encoder script path handling with __dirname plus path.resolve conventions.
2026-02-26 13:11:12 +01:00
Tommy D. Rossi e4c7ea46be docs: shorten resizeImage section in skill.md 2026-02-26 13:10:37 +01:00
Tommy D. Rossi bf5ae2686d fix: suppress stack traces for timeout and abort errors in CLI/MCP output
Timeout and abort error stack traces are internal noise (Promise.race,
setTimeout, AbortController internals) that waste context window space
for LLM agents and clutter CLI output. Now only error.message is shown
to the user for these error types. Full stacks are still logged to the
relay server log file for debugging.
2026-02-26 13:04:18 +01:00
Tommy D. Rossi 2c7e859a7b fix: stabilize recording cursor and demo speedup behavior
Improve recording lifecycle safety by keeping stop and cancel cleanup aligned with successful relay operations, serializing per-page ghost cursor action application, and ensuring cursor transforms refresh when style or hotspot options change.

Harden demo video generation by preferring sane probed frame rates, capping speedup output FPS to source rate, and avoiding full-video speedup when execution timestamps are unavailable.
2026-02-26 12:59:54 +01:00
Tommy D. Rossi c13c835c5f feat: add resizeImage sandbox utility for shrinking screenshots before LLM ingestion
Extracts the sharp resize logic from screenshotWithAccessibilityLabels into a
standalone resizeImage() function exposed in the executor sandbox. Agents can
now shrink screenshots (or any image) to consume fewer context tokens.

Default behavior: fit within 1568×1568px (Claude-optimal), overwrite input file.
Also supports explicit width/height/maxDimension/fit/quality for custom resizing.

Replaces the old macOS-specific sips advice in skill.md.
2026-02-26 12:57:39 +01:00
Tommy D. Rossi de9c3e961b playwriter skill self improvement: prevent context waste from modal-blocking loops, React dispatchEvent anti-pattern, and unclear snapshot locator direct-use 2026-02-26 12:55:03 +01:00
Tommy D. Rossi 2b5d2fa559 feat: add ghost cursor APIs and harden reconnect flow
Introduce a first-class ghost cursor system for recordings and screenshots with namespace APIs (, ) while preserving backward-compatible top-level recording helpers.

Improve reliability after relay restarts by consolidating extension wait logic around , reducing false disconnected states during  and execute flows.

Refactor recording glue into dedicated modules (,  in ) to keep executor orchestration thin and make cursor lifecycle state explicit and testable.
2026-02-25 19:33:55 +01:00
Tommy D. Rossi 43ad91e439 nn 2026-02-25 19:01:13 +01:00
Tommy D. Rossi 5173526475 docs: clarify Playwright video recording overhead
Add a README note that Playwright video recording sends base64 images for every frame while documenting Playwriter's claimed 100x efficiency.
2026-02-25 18:00:20 +01:00
Tommy D. Rossi 1e9fecaf60 add --profile-directory=Default to Chrome restart commands
Prevents Chrome from hanging on the profile picker when agents restart it
programmatically. Applied to:

- getChromeRestartCommand() in screen-recording.ts (used for recording setup)
- All Chrome startup examples in skill.md (basic + recording flags)
- Both macOS, Linux, and Windows variants
2026-02-25 17:26:21 +01:00
Tommy D. Rossi eb420dabff docs: verify relay port cleanup in local CLI instructions
Replace the macOS kill one-liner with a zsh-safe array pattern and add explicit post-kill verification commands for macOS and Windows. This prevents false positives where port 19988 remains occupied after an attempted shutdown.
2026-02-25 17:14:29 +01:00
Tommy D. Rossi 95ff2a797d fix: scope CDP tab sessions and simplify recording sessionId routing 2026-02-25 16:52:41 +01:00
Tommy D. Rossi 40f9c54f74 fix: properly resolve extension for recording routes with CDP session IDs
The recording HTTP routes receive sessionId which can be either a CDP
session ID (pw-tab-*) from page.sessionId() or an executor session ID
(numeric). Previously, all sessionIds were looked up as executor sessions,
causing 'Session not found' for CDP session IDs.

New approach with resolveRecordingExtensionId():
- CDP session IDs (pw-tab-*): search all extension connections'
  connectedTargets to find which extension owns that tab. Works correctly
  with multiple Chrome profiles connected.
- Executor session IDs (numeric): look up executor metadata for extensionId
  as before.
- null: fall back to default extension.

Applied consistently to all 4 recording routes (start/stop/status/cancel).
2026-02-25 13:27:12 +01:00
Tommy D. Rossi 8744de94a1 revert: remove cdp-relay recording route changes for review
Reverts the isCdpSessionId changes from 1cad56f to working directory
for oracle analysis. The fix may not be correct.
2026-02-25 13:21:25 +01:00
Tommy D. Rossi 5637a13003 fix: guard timestamp recording against cross-session contamination
If recording is stopped and restarted inside the same execute() call,
the finally block could append an interval from the old recording into
the new recording's executionTimestamps. Fix by comparing against the
snapshotted recordingStartedAt instead of just checking non-null.
2026-02-25 13:16:51 +01:00
Tommy D. Rossi a0b7af1adb fix: update playwright submodule with oracle review fixes 2026-02-25 13:12:58 +01:00
Tommy D. Rossi 1cad56f1a2 fix: recording session ID routing, IIFE for timestamps, default speed 5x
cdp-relay.ts:
- Fix pre-existing bug: recording HTTP routes (/recording/start, stop,
  status, cancel) were treating CDP session IDs (pw-tab-*) as executor
  session IDs, causing 'Session not found' errors. Now skip executor
  lookup when sessionId is a CDP session ID and pass it through directly
  to the extension for tab routing.

executor.ts:
- Use async IIFE instead of let+try for VM execution timestamp tracking.
  Cleaner pattern: const result = await (async () => { try/finally })()

ffmpeg.ts + skill.md:
- Change default idle speed from 4x to 5x
2026-02-25 13:12:41 +01:00
Tommy D. Rossi d84cdba528 feat: add page.onMouseAction callback + tests
Adds onMouseAction to playwright fork — an async callback on Page
that fires before every mouse action (move/down/up/wheel). Covers
both page.mouse.* and locator-initiated actions.

Test validates:
- Callback fires for page.mouse.click() with correct coordinates
- Callback fires for locator.click() with resolved element center
- Callback stops firing when set to null
2026-02-25 12:57:41 +01:00
Tommy D. Rossi 2d0f4ac456 fix: address oracle review — shell injection, range validation, timestamp robustness
ffmpeg.ts:
- Replace exec() with spawn() for ffmpeg/ffprobe — no shell interpreter,
  no injection risk from filenames with special characters
- Filter invalid ranges in computeIdleSections() after clamping to video
  bounds (handles timestamps exceeding video duration)
- Use path.basename() for timer IDs instead of split('/') for Windows compat

executor.ts:
- Use try/finally around VM execution so execution timestamps are recorded
  even when code throws — the execution still occupied real time that should
  not be sped up in the demo video
- Snapshot recordingStartedAt before try to avoid race if stopRecording()
  is called inside the same execute() clearing the field
2026-02-25 12:51:48 +01:00
Tommy D. Rossi 95bda7acc7 feat: demo video creation — auto speed up idle sections during recordings
Add ffmpeg.ts with utilities for video manipulation:
- concatenateVideos: merge multiple video files with trim/scale/fps normalization
- speedUpSections: speed up arbitrary timestamp ranges in a single ffmpeg pass
- computeIdleSections: compute idle gaps from execution timestamps
- createDemoVideo: high-level wrapper that takes stopRecording() result and
  produces a sped-up video where idle time (between execute() calls) is fast-forwarded

Executor changes:
- Track execution timestamps (start/end in seconds) while recording is active
- startRecording wrapper resets tracking state
- stopRecording wrapper returns executionTimestamps alongside path/duration/size
- cancelRecording wrapper clears tracking state
- Expose createDemoVideo in the sandbox VM context

The recording timeline looks like:
  0s      5s   8s   12s  15s  20s  25s   30s
  ├───────┼────┼────┼────┼────┼────┼─────┤
          ▓▓▓▓▓     ▓▓▓▓▓     ▓▓▓▓▓▓▓▓▓
          exec1     exec2     exec3
  ░░░░░░░       ░░░░░       ░░░░░
  IDLE(4x)      IDLE(4x)    IDLE(4x)

A 1-second buffer (INTERACTION_BUFFER_SECONDS) is kept around each
execution at normal speed so viewers see context before/after actions.

Requires ffmpeg + ffprobe on the system.
2026-02-25 12:45:04 +01:00
Tommy D. Rossi 0d3ec43da3 fix: stop forcing light mode on connectOverCDP pages
Removed the preserveSystemColorScheme() workaround from executor.ts that was
mutating private Playwright internals via (context as any)._options to patch
around the forced light mode.

The root cause is now fixed in @xmorse/playwright-core@1.59.4: the default
colorScheme fallback in page.emulatedMedia() changed from 'light' to
'no-override', so Chrome respects the user's system color scheme.

Activated the previously-todo test that validates dark mode is preserved
after MCP connection (matchesDark: true, matchesLight: false).
2026-02-24 15:31:14 +01:00
Tommy D. Rossi 9c376b7637 website: brighter dark mode links, lighter diagram font weight
- Dark mode --link-accent: #79bbff → #aadcff for better visibility
- Diagram font-weight: 500 → 400 for thinner box-drawing and text
2026-02-24 15:22:30 +01:00
Tommy D. Rossi 6dca255bd7 website: tune diagram colors — darker structure, teal-green labels, heavier weight
- Structure chars darkened: #c0c0c0 → #999 (light), #555#666 (dark)
- Labels switched to teal-green: #0d7a5f (light), #6ee7b7 (dark)
- Removed bold from labels, bumped diagram font-weight to 500 so
  box-drawing characters aren't paper-thin at 400
2026-02-24 14:49:35 +01:00
Tommy D. Rossi 786c6bf395 new test for logs from iframes 2026-02-24 14:44:21 +01:00
Tommy D. Rossi 1a9c6ddcd0 website: add diagram Prism language, CodeBlock showLineNumbers prop, brighter dark mode links
- Custom 'diagram' Prism language: box-drawing chars get neutral color,
  text labels get a saturated blue so they pop against the structure
- CodeBlock gains showLineNumbers prop (default true) and skips Prism
  highlighting when lang is empty
- Diagram block in index page uses lang='diagram' with line numbers off
- Dark mode --link-accent bumped from #58a6ff to #79bbff for better
  visibility against dark backgrounds
2026-02-24 14:44:08 +01:00
Tommy D. Rossi c23eced495 Update .gitignore 2026-02-23 16:19:52 +01:00
Tommy D. Rossi cab71368e6 nn 2026-02-23 16:19:38 +01:00
Tommy D. Rossi d32a8c901e fix: auto-select extension with active targets when multiple Chrome profiles are connected
When multiple Chrome profiles have the playwriter extension installed, the
MCP fails with 'Multiple extensions connected. Specify extensionId.' even
if only one profile has playwriter-enabled tabs.

The relay's getExtensionConnection fallback now checks if exactly one
extension has active targets (connectedTargets > 0) and auto-selects it.
Falls through to the existing error when zero or multiple extensions are
active, so no regressions for single-extension setups.

The fix lives in the relay so all clients (MCP, Stagehand, raw CDP)
benefit. CLI session creation is unaffected — it still prompts the user
to pick a browser with --browser when multiple profiles are connected.

Fixes #52
2026-02-23 15:19:06 +01:00
Tommy D. Rossi 8f2dcc822e website: simplify code examples, tighten code letter-spacing
- Remove -s 1 from all CLI examples (kept in sessions section for isolation demo)
- Remove unnecessary await from -e expressions (CLI auto-awaits)
- Remove console.log wrappers where CLI auto-prints results
- Reduce code letter-spacing from 0.02em to 0.01em
2026-02-22 22:46:39 +01:00