fix: revert ghost cursor init-script persistence path

This reverts the addGhostCursorInitScript approach after it proved unreliable in real usage.

What changed:\n- remove addGhostCursorInitScript/removeGhostCursorInitScript and related CDP init-script wiring\n- restore direct per-page ghost cursor injection fallback in applyGhostCursorMouseAction\n- remove recording controller state/cleanup tied to init-script identifiers\n- remove skill.md claim that ghost cursor persists automatically across MPA navigations\n- bump playwriter version to 0.0.85 and document the revert in changelog
This commit is contained in:
Tommy D. Rossi
2026-02-28 22:41:53 +01:00
parent f793dd8ac4
commit 9ad8b22f93
5 changed files with 23 additions and 106 deletions
+6
View File
@@ -1,5 +1,11 @@
# Changelog
## 0.0.85
### Bug Fixes
- **Revert ghost cursor init-script persistence**: Removed the `Page.addScriptToEvaluateOnNewDocument` cursor injection path (`addGhostCursorInitScript`) and restored direct per-page injection behavior used before, since the init-script approach did not work reliably in practice.
## 0.0.84
### Docs
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "playwriter",
"description": "",
"version": "0.0.84",
"version": "0.0.85",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
+16 -74
View File
@@ -1,19 +1,12 @@
/**
* Node-side ghost cursor helpers.
* Injects the browser bundle and forwards mouse action events to the page overlay.
*
* Two injection strategies:
* 1. One-shot: page.evaluate() — used by enableGhostCursor() for single pages
* 2. Persistent: Page.addScriptToEvaluateOnNewDocument — used by
* addGhostCursorInitScript() so the cursor survives MPA navigations.
* Chrome re-runs the script on every new document automatically.
*/
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import type { Page, MouseActionEvent } from '@xmorse/playwright-core'
import { getCDPSessionForPage, type ICDPSession } from './cdp-session.js'
export interface GhostCursorClientOptions {
style?: 'minimal' | 'dot' | 'screenstudio'
@@ -30,7 +23,6 @@ interface GhostCursorBrowserApi {
enable: (options?: GhostCursorClientOptions) => void
disable: () => void
applyMouseAction: (event: MouseActionEvent) => void
isEnabled: () => boolean
}
let ghostCursorCode: string | null = null
@@ -84,84 +76,34 @@ export async function disableGhostCursor(options: { page: Page }): Promise<void>
})
}
/**
* Register the ghost cursor bundle as a persistent init script via CDP
* Page.addScriptToEvaluateOnNewDocument. Chrome re-runs this on every new
* document (navigation), so the cursor survives MPA page loads.
*
* The script auto-enables when the DOM is ready (DOMContentLoaded or
* requestAnimationFrame fallback) so the cursor is visible immediately
* on each new page.
*
* Returns the CDP identifier needed to remove it later.
*/
export async function addGhostCursorInitScript(options: {
page: Page
cursorOptions?: GhostCursorClientOptions
}): Promise<{ cdp: ICDPSession; identifier: string }> {
const { page, cursorOptions } = options
const cdp = await getCDPSessionForPage({ page })
const code = getGhostCursorCode()
// Wrap the bundle: inject, then auto-enable once DOM is ready.
// DOMContentLoaded may have already fired if the script runs late,
// so check document.readyState first.
const optionsJson = JSON.stringify(cursorOptions ?? {})
const wrappedCode = `
${code}
;(function() {
function autoEnable() {
var api = globalThis.__playwriterGhostCursor;
if (api) {
var opts = ${optionsJson};
api.enable(Object.keys(opts).length ? opts : undefined);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', autoEnable, { once: true });
} else {
requestAnimationFrame(autoEnable);
}
})();
`
const result = await cdp.send('Page.addScriptToEvaluateOnNewDocument', {
source: wrappedCode,
})
return { cdp, identifier: result.identifier }
}
/**
* Remove a previously registered ghost cursor init script.
*/
export async function removeGhostCursorInitScript(options: {
cdp: ICDPSession
identifier: string
}): Promise<void> {
const { cdp, identifier } = options
await cdp.send('Page.removeScriptToEvaluateOnNewDocument', { identifier })
}
export async function applyGhostCursorMouseAction(options: {
page: Page
event: MouseActionEvent
}): Promise<void> {
const { page, event } = options
await page.evaluate(
const applied = await page.evaluate(
({ serializedEvent }) => {
const api = (globalThis as { __playwriterGhostCursor?: GhostCursorBrowserApi }).__playwriterGhostCursor
if (!api) {
return
return false
}
// Ensure cursor is enabled (may be a freshly injected init script
// where DOMContentLoaded hasn't fired yet)
if (!api.isEnabled()) {
api.enable()
}
api.applyMouseAction(serializedEvent)
return true
},
{ serializedEvent: event },
)
if (applied) {
return
}
await ensureGhostCursorInjected({ page })
await page.evaluate(
({ serializedEvent }) => {
const api = (globalThis as { __playwriterGhostCursor?: GhostCursorBrowserApi }).__playwriterGhostCursor
api?.applyMouseAction(serializedEvent)
},
{ serializedEvent: event },
)
-29
View File
@@ -1,19 +1,13 @@
/**
* Encapsulates ghost cursor lifecycle for recording sessions.
* Keeps onMouseAction chaining/restoration isolated from executor logic.
*
* Uses Page.addScriptToEvaluateOnNewDocument so the cursor persists across
* MPA navigations — Chrome re-runs the init script on every new document.
*/
import type { BrowserContext, Page } from '@xmorse/playwright-core'
import type { ICDPSession } from './cdp-session.js'
import {
addGhostCursorInitScript,
applyGhostCursorMouseAction,
disableGhostCursor,
enableGhostCursor,
removeGhostCursorInitScript,
type GhostCursorClientOptions,
} from './ghost-cursor.js'
@@ -26,15 +20,9 @@ interface RecordingTargetOptions {
sessionId?: string
}
interface InitScriptHandle {
cdp: ICDPSession
identifier: string
}
export class RecordingGhostCursorController {
private readonly previousMouseActionByPage = new WeakMap<Page, Page['onMouseAction']>()
private readonly cursorApplyQueueByPage = new WeakMap<Page, Promise<void>>()
private readonly initScriptByPage = new WeakMap<Page, InitScriptHandle>()
private readonly logger: RecordingGhostCursorLogger
constructor(options: { logger: RecordingGhostCursorLogger }) {
@@ -69,12 +57,6 @@ export class RecordingGhostCursorController {
const { page } = options
try {
// Register persistent init script so cursor survives MPA navigations
const handle = await addGhostCursorInitScript({ page })
this.initScriptByPage.set(page, handle)
// Also enable on the current page immediately (init script only runs
// on future navigations, not the current document)
await enableGhostCursor({ page })
if (!this.previousMouseActionByPage.has(page)) {
@@ -112,17 +94,6 @@ export class RecordingGhostCursorController {
this.previousMouseActionByPage.delete(page)
this.cursorApplyQueueByPage.delete(page)
// Remove the persistent init script so future navigations don't inject cursor
const handle = this.initScriptByPage.get(page)
if (handle) {
try {
await removeGhostCursorInitScript(handle)
} catch (error) {
this.logger.error('[playwriter] Failed to remove ghost cursor init script', error)
}
this.initScriptByPage.delete(page)
}
try {
await disableGhostCursor({ page })
} catch (error) {
-2
View File
@@ -938,8 +938,6 @@ Labels are color-coded: yellow=links, orange=buttons, coral=inputs, pink=checkbo
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.
**Ghost cursor survives MPA navigation**: when recording starts, the cursor bundle is registered via `Page.addScriptToEvaluateOnNewDocument`, so Chrome re-injects and re-enables it on every new document. This means the cursor is visible across full-page navigations (GitHub, Hacker News, etc.) without any extra work.
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.
**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.