PLAYWRITER_HOST now accepts full URLs like `https://x-tunnel.traforo.dev`
in addition to plain hostnames. Previously it always constructed
`http://${host}:19988` which broke when using traforo tunnels (HTTPS on
port 443).
Added `parseRelayHost()` utility that detects URL protocols and returns
correct HTTP/WebSocket base URLs. Updated all consumers:
- cli.ts: getServerUrl()
- mcp.ts: getLogServerUrl(), checkRemoteServer()
- executor.ts: checkExtensionStatus()
- utils.ts: getCdpUrl() (ws:// → wss:// for HTTPS hosts)
Plain hostnames still work as before (appends :19988).
Added docs/remote-access.md covering:
- Architecture: playwriter serve + traforo tunnel through Cloudflare
- Host and remote machine setup with env vars
- Use cases: remote Mac mini, user support, multi-machine, VM/devcontainer
- Security model: non-guessable URLs, token auth, localhost-only extension
endpoint, no open ports, visible automation, instant revocation
Document the page-reuse pattern for the Framer project tab so repeated checks do not spawn duplicate tabs, and explicitly ban bringToFront in this guide to avoid interrupting the active browser window during local debugging.
Route Runtime.enable/disable through the incoming child sessionId instead of always using the tab root session so OOPIF iframe targets receive execution contexts reliably. Add a focused relay regression test for empty-src cross-origin iframe attach flow and update extension version/changelog for release tracking.
Retry the post-goto readyState check when Chromium tears down the execution context during ongoing navigation, so the relay navigation test validates page readiness without intermittent race failures.
getStableRefFromAttributes checked id before data-testid, opposite of
what getRefsForLocators does. Now both are consistent: test IDs first,
id second. Test IDs are developer-intentional and stable, id is often
auto-generated by frameworks.
Remove the custom WebSocket-based CDPSession class entirely. All CDP
communication now goes through PlaywrightCDPSessionAdapter which wraps
Playwright's existing CDP session, avoiding duplicate WebSocket connections
and Target.attachToTarget calls that the relay intercepts.
ICDPSession interface uses ProtocolMapping generics from devtools-protocol
so send/on/off methods are fully type-safe — return types are inferred
from the CDP command string (e.g. 'Page.getLayoutMetrics' returns
Protocol.Page.GetLayoutMetricsResponse).
- Remove CDPSession class and getCDPSessionForPage WebSocket factory
- Remove wsUrl parameter from all snapshot/label functions
- Remove CDPSession cache from executor (adapter is stateless)
- Remove 'as CDPSession' casts from debugger, editor, styles, react-source
- Rename getExistingCDPSessionForPage → getCDPSessionForPage
- Remove test for old getCDPSession through relay
Switch from getCDPSessionForPage (creates a new WS via Target.attachToTarget)
to getExistingCDPSessionForPage which reuses Playwright's internal CRSession.
This is critical for the relay server where Target.attachToTarget is intercepted
and cannot create real new sessions.
Also add FrameLocator support in accessibilitySnapshot — FrameLocator (from
locator.contentFrame()) is now auto-resolved to the real Frame object needed
for OOPIF CDP session attachment.
- New PlaywrightCDPSessionAdapter wrapping Playwright's CDPSession as ICDPSession
- New getExistingCDPSessionForPage() using context.getExistingCDPSession()
- resolveFrame() helper converts FrameLocator to Frame via elementHandle().contentFrame()
- getAriaSnapshot() frame param now accepts Frame | FrameLocator
- Executor CDP cache uses ICDPSession, borrowed sessions detach as no-op
- Test for getExistingCDPSession through the relay
- Updated framer iframe guide with alternative page.frames() example
- Bump playwright submodule to @xmorse/playwright-core@1.59.2
- Fix styles-api.md import path
When a user drags a tab into the playwriter group, connectTab() sets
its state to 'connecting'. syncTabGroup() previously only kept
'connected' tabs in the group, so it would immediately ungroup the
'connecting' tab. This ungroup fired chrome.tabs.onUpdated, which
called disconnectTab(). When the original connectTab() resolved, it
re-added the tab, triggering onUpdated again — creating an infinite
add/remove/add/remove loop.
Fix: include 'connecting' tabs in syncTabGroup's group membership
filter. This way syncTabGroup won't fight the user's intent by
removing tabs that are still being set up. Error-state tabs are
still correctly excluded from the group.
Chrome deprecated Debugger.setScriptSource in Chrome 142+ (removed in 145).
Falls back to Runtime.evaluate to re-execute modified scripts, which
works for scripts defining functions at global scope.
- Add "No changes since last call" hints for snapshots/html/markdown
- Adjust truncation message to suggest search
- Update skill.md snapshot behavior doc
Tabs created via Ghost Browser's openTab API now automatically
attach to Playwriter's debugger, making them immediately available
in context.pages() without requiring manual extension icon click.
- Resolve extension connections by stable key and map to current connection
- Reject Playwright clients without explicit extension binding when multiple browsers connected
- Reject duplicate clientId connections
- Allow default extension only when a single browser is connected
- Include stableKey in /extensions/status response