When Playwright sends Target.detachFromTarget via the root browser session
(no top-level sessionId), the extension couldn't find the target tab because
it only checked msg.params.sessionId for routing. This caused 'No tab found'
errors that cascaded into disconnects and instability.
- Add getTabForCommand() helper with params.sessionId fallback so any command
referencing a session in its params can be routed when the top-level
sessionId is absent
- No-op Target.detachFromTarget for stale/unknown sessions instead of throwing
- Always re-apply tab group color on every sync to prevent Chrome resetting
it to white
- Replace silent .catch() with error log in aria-snapshot OOPIF detach
- Add regression test using raw WebSocket to verify routing without sessionId
Extension bumped to 0.0.74.
This commit batches the pending workspace updates that were coupled in practice: extension reliability fixes, ghost cursor persistence across MPA navigations, timeout tuning, and aligned docs/versioning updates so behavior and guidance stay in sync.
Key updates:\n- extension: add MV3 keepalive via chrome.alarms, add alarms permission, and bump extension changelog/version to 0.0.73 to reduce silent relay disconnects while idle\n- playwriter runtime: raise default action timeout to 60s while keeping navigation timeout separate; increase relay /version fetch timeout to 2000ms\n- recording + cursor: add persistent cursor init scripts with Page.addScriptToEvaluateOnNewDocument, remove init script on teardown, and ensure cursor enablement before applying mouse actions\n- demo video defaults: use 0.5s side buffer (1s total) and default idle speed 6x for createDemoVideo/computeIdleSections\n- tests/docs: keep click-error tests deterministic with explicit 5000ms per-click timeout, remove outdated click-timeout section from skill mistakes, and update package/changelog entries
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.
The function is now exposed as `snapshot()` in the executor sandbox scope.
`accessibilitySnapshot` remains as a backward-compatible alias pointing to
the same function, so existing agents and saved commands continue to work.
Updated all code examples across skill.md, README, PLAYWRITER_AGENTS,
docs, tests, and the extension welcome page to use the shorter name.
The extension now reports which playwriter version it was built with via a
`?v=` query param on its WebSocket connection to the relay. The relay stores
this and exposes it in `/extension/status` and `/extensions/status` endpoints.
CLI and MCP compare the extension's bundled version against their own VERSION.
If the extension was built with a newer playwriter, a warning is shown:
Playwriter 0.0.60 is outdated (extension requires 0.0.62).
Run `npm install -g playwriter@latest` or update the playwriter package.
This is fully backward compatible:
- Old extensions without `?v=` get `playwriterVersion: null`, no warning
- No WS protocol changes (version is in URL query params)
- Warning fires once per executor/session lifetime
Flow:
vite build reads playwriter/package.json → injects __PLAYWRITER_VERSION__
→ extension sends ?v=X.Y.Z on WS connect → relay stores in ExtensionInfo
→ /extension/status returns playwriterVersion → CLI/executor checks & warns
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.
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.
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.
- Extension connections are now tracked per-connection with isolated state
- Each connection has its own connectedTargets, pendingRequests, messageId
- Sessions are bound to specific extensionId via SessionMetadata
- New /extensions/status endpoint for listing all connected browsers
- CLI session new shows browser selection when multiple connected
- Browser detection via navigator.userAgentData (Ghost, Brave, Edge, etc)
- Added identity permission for Chrome profile email detection
- Constants, types, proxy factory, and handler in one file
- Works in both Node.js (executor) and Chrome (extension) environments
- executor.ts imports createGhostBrowserChrome factory
- background.ts imports handleGhostBrowserCommand handler
- Export store, connectionManager, logger, sendMessage, getTabBySessionId from background.ts
- recording.ts now imports directly instead of using initRecording()
- Simplify RecordingRelay constructor to take params directly instead of interface
- Change offscreen.ts from single recording variable to Map<tabId, RecordingState>
- Update all recording message handlers to route by tabId
- Track sessionId in relay server for proper recording lookup when stopping
- All recording operations now consistently require tabId
- Fix misleading comment about user gesture requirement in background.ts
- Remove unused 'target' variable in /recording/stop endpoint
- Fix /recording/cancel to not delete all recordings (handled by event)
- Remove redundant security middlewares (CORS handles it)
- Add getChromeRestartCommand() with --allowlisted-extension-id and --auto-accept-this-tab-capture flags
- Move EXTENSION_IDS to utils.ts for cleaner imports
- Add sessionId support for multi-tab recording
- Add helpful error message when activeTab permission is missing
- Add null check for empty extension result
- Document offscreen recording flow with ASCII diagram
- Fix vite output path (was broken lib/background.mjs, now background.js)
- Import recording types from protocol.ts instead of duplicating in background.ts
- Remove dead getSessionId() and unused sessionId params from screen-recording.ts
- Replace 4 executor wrapper functions with single withRecordingDefaults helper
- Fix binary chunk routing with lastRecordingMetadataTabId tracking
- Add ExtensionStopRecordingResult type for proper extension/relay separation
Saves ~56 lines while improving correctness of multi-tab recording.
- Move welcome.html to src/
- Use vite HTML inputs instead of static copy for offscreen.html and welcome.html
- Source HTML references .ts, vite transforms to .js
- Update paths in background.ts to src/
- Use chrome.tabCapture.getMediaStreamId() from service worker
- Pass stream ID to offscreen document for MediaRecorder
- Recording survives page navigation (unlike getDisplayMedia)
- Requires user to click extension icon first (activeTab permission)
- Add esbuild to build offscreen.ts separately
- Update prompt docs with recording instructions
Work in progress for navigation-safe screen recording.
Implemented:
- tabCapture permission in manifest
- Recording handlers in extension background.ts
- Recording endpoints in relay server
- Binary WebSocket streaming for video chunks
- Protocol types for recording messages
- Executor integration
Needs:
- Offscreen document implementation (tabCapture not available in service workers)
- Connect background.ts to offscreen document via messages
- Update manifest for offscreen document
The offscreen.ts/html files are scaffolding for this next step.
Inject manifest key via vite plugin for dev/test builds (not production).
This ensures all developers get the same extension ID: pebbngnfojnignonigcnkdilknapkgid
- Update vite.config.mts to inject key when !PRODUCTION
- Update release script to use PRODUCTION=true
- Replace old hardcoded dev extension ID in all files
- Server rejects new extension (code 4002) if existing has active tabs
- Idle extensions (no tabs) can still be replaced
- Extension properly enters polling mode when rejected during handshake
- /extension/status now returns activeTargets count for smarter reconnection
- Use 127.0.0.1 instead of localhost to avoid DNS/IPv6 resolution issues
- Add 15s global timeout wrapper around connect() to prevent hanging forever
- Reduce HTTP retry attempts from 30 to 5 (maintainLoop retries anyway)
- Fix WebSocket cleanup: close socket on timeout to prevent orphaned connections
- Improve settled state tracking in WebSocket connection promise
- Remove global playwriterGroupId cache that could become stale
- Query chrome.tabGroups by title on each sync instead
- Use batch chrome.tabs.ungroup() with array of IDs instead of loops
- Fixes issue where group wasn't created after debugger detach/reattach