Replace the RecorderCapableContext cast with a module declaration on
@xmorse/playwright-core. The e2e test now calls _enableRecorder
directly. Playwright-core stores recorder state on typed context fields.
Session: ses_fcc0d3701ffeCWJ29O30JooBS8
A second Playwright connection to the same Chrome used to inject a
second document click listener. Recording 66 showed each physical click
three times.
The playwright-core fork now keeps one injected recorder per document
and attaches the server ActionAdded listener once. This commit adds the
regression test and changeset.
```
before: 1 click -> N action events (one per CDP client)
after: 1 click -> 1 action event
```
Session: ses_fcc0d3701ffeCWJ29O30JooBS8
The click did start a recording, but the button only flipped to Stop
when the relay went from zero recordings to one. If another recording
was already active, onActiveChanged never fired, extra clicks started
more recordings, and fetch errors were swallowed with no toast.
Show Starting with a spinner until /recorder/start returns. Then flip
to Stop, or toast and return to Record Skill. Ignore extra starts
while one is in flight. Always notify the toolbar on a successful
start, not only the first.
Extension 0.0.121.
Session: ses_fcc12cb15ffeTShy0O4UREzOXs
The copied-element toast lived inside the toolbar host. That host uses
transform and contain, so position:fixed was not viewport space. The
toast sat too far right of the click, and the recording "prompt copied"
toast sat on top of the bar.
Move toasts to their own overlay. Use the same dark surface as the
toolbar. Pin toasts sit on the click X. Recording toasts sit just
below the toolbar.
Bump the extension to 0.0.120.
Session: ses_fcc13cd0affe2QP4EiEj9GVZVz
Move the MCP log route below its authentication middleware so token mode rejects unauthenticated log writes. Require configured tokens on extension status and CDP discovery metadata routes while preserving normal authless localhost access.
Add regression coverage for authenticated and authless modes. Correct the security documentation to explain default automatic blank-tab creation, add a patch changeset, and bump the extension manifest to 0.0.119.
Session: ses_fd1b922c7ffe3ekUGs0pOBZulQ
The toolbar Record button already starts recording from the extension
service worker (chrome-extension:// origin). Allowing any website origin
on POST /recorder/start and /recorder/stop was leftover from when the
page fetched the relay directly.
A page can no longer pass the CORS preflight, so it cannot start or
stop a recording. The worker fetch still skips the Sec-Fetch-Site block
because chrome-extension:// to 127.0.0.1 is cross-site.
Also switch the idle-icon tutorial to npx playwriter examples, bump
the extension to 0.0.118, and tighten the changeset/extension rules
in AGENTS.md.
Session: ses_fd2023624ffeK812PCDMCsUNVJ
The toolbar used to pop in at full opacity. It now fades in with a 200ms
opacity transition via @starting-style, and skips the transition when
prefers-reduced-motion is set.
Pin-mode hover was the only hot path that mattered. Mouse moves called
elementsFromPoint and toggled display on every event, which forces layout
on heavy pages. Overlay updates now coalesce to one requestAnimationFrame,
hit-test with elementFromPoint, and hide with visibility instead of
display. contain:layout style paint isolates the toolbar and overlay from
page layout.
Did not rewrite drag to transform. Drag is rare and the risk is not worth
it.
Bumps the extension to 0.0.117.
Session: ses_fd213a8deffeVULbxEatPrFCEL
A start past the cap no longer returns 429. The manager drops the oldest
active recording from the map in the same turn it reserves the new one,
then stops the old recorder with reason replaced. Concurrent starts cannot
briefly exceed 10 because the map swap happens before the first await.
The HTTP /recorder/start route no longer pre-rejects at the cap. Same
session already recording still returns 409.
Session: ses_fd6311dd5ffekNfv19AGfh8K9m
POST /recorder/start and /recorder/stop stay open to any page origin so
the toolbar Record button works. A site can start or stop a recording
and read { recordingId, sessionId, file }. It cannot execute code or
read /recorder/events or /recorder/status.
Each start injects Playwright's API recorder and a CDP screencast. With
no cap, a page could open unbounded recordings and burn CPU and disk.
ActionRecordingManager.start() now refuses a new recording when 10 are
already active and throws RecordingError 429. The HTTP route checks the
same cap before creating a session, so spam cannot leave empty sessions
behind the manager guard. Stopping one recording frees a slot.
Session: ses_fd6311dd5ffekNfv19AGfh8K9m
The first tutorial commit shipped with no changeset. Add one so the
next playwriter release notes mention the new idle-icon page.
Also tighten the copy: title is now The Playwriter daemon is not
running, and the last line says to click the icon on a normal website
tab so agents can control an existing tab.
Bump the extension to 0.0.114.
Session: ses_fd6aecbb4ffe6Qzpvnjmc14rqQ
`npx skills add remorses/playwriter` showed the GitHub owner in every
install command. Akarso already avoids that by hosting the skills.sh
discovery document on its own domain. Do the same on playwriter.dev.
The website now answers:
GET /.well-known/agent-skills/index.json
GET /.well-known/agent-skills/playwriter/SKILL.md
The SKILL.md bytes are imported from skills/playwriter/SKILL.md so the
served file cannot drift from git. The index includes a sha256 digest
because skills.sh checks it.
All public install commands now use:
npx -y skills add https://playwriter.dev
This covers the README, website docs, welcome page, and tutorial page.
The extension version is 0.0.113 because those HTML pages changed.
The tutorial page is also shorter than the previous daemon walkthrough.
That rewrite was already in the working tree with the skill URL change.
Session: ses_fd657bfa3ffe8lJ3G5L134SgMB
First-time users click the Playwriter icon and see a gray or orange badge.
Nothing tells them the extension cannot attach a tab until an agent runs a
playwriter command and starts the local daemon.
When the relay is idle, an icon click now opens src/tutorial.html. The page
explains why it opened, that the agent must run playwriter first, and how
to install the CLI and skill. It polls /version so the banner turns green
when the daemon starts. A second click focuses the existing tutorial tab.
The click still tries to attach the original tab. An orange ... badge means
the tab is waiting and can turn green by itself after the daemon starts.
Skip the tutorial in tests and in bundled automation builds that already
disable welcome.html. Bump the extension to 0.0.110.
Session: ses_fd6aecbb4ffe6Qzpvnjmc14rqQ
While a skill records, the toolbar button used to show only
Recording skill. That text looks like status, not an action.
The button now shows a red stop square and Stop recording
with a unicode ellipsis. Click still stops the recording.
Also bump the extension to 0.0.109 and update the security
docs to match the new label.
Session: ses_fd70431a9ffejIwMLovDAQ4TLr
Recorded skills were getting /Users/... imports and a generic utils.js.
WebSockets never appear in the event log, so agents stopped at quota POSTs.
The recorder prompt and skill-recorder docs now:
- Import helpers relative to the playwriter cwd, or via homedir(). Never hardcode absolute paths.
- Name the script for the flow (submit.js, sdk.js). Do not default to utils.js.
- Prefer an in-page class SDK when the recording has a JSON API. fetch runs inside page.evaluate so cookies and captchas stay in the tab. Methods take one object. Failures throw method, path, status, and body.
- If submit has no matching POST, tap WebSocket with addInitScript, reload, replay the action, then dump construct/send/message. An empty tap after reload alone is expected.
Verified the tap on grok.com/imagine: wrap survives reload, Invia yields wss://grok.com/ws/imagine/listen frames.
Session: ses_fd72f6af5ffezwQjT8IVIfbZOH
The in-page Record button posted /recorder/start with no sessionId. The
relay treated many sessions as an error, so Record failed whenever more
than one playwriter session existed.
Session pick on start is not useful for this flow. Extension sessions
share the same Chrome tabs. The agent identifies the recording later,
at stop time.
Start without a sessionId now:
- uses an explicit sessionId when the CLI sent one
- otherwise reuses a free extension session
- otherwise creates an extension session
- never picks a headless or cloud session (those have no user tabs)
Stop without a recording id still stops the only active recording. If
more than one recording is active, stop throws and lists each id,
session, and current or last page URL:
Multiple active recordings. Pass a recording id.
3 session 1 https://app.example.com/settings
5 session 2 https://github.com/remorses/playwriter
Then: playwriter recorder stop 3
The toolbar Stop button now sends the recording id it started, so it is
never ambiguous. Failed start/stop calls toast the error.
Also document the recorder goal, mechanics, commands, multiple-recording
case, and troubleshooting in skill-recorder, CLI, and troubleshooting
docs. recorder status prints page URLs.
Extension 0.0.108.
Session: ses_fd76982e8ffejVfvd9Qqui5xcE
utils.js is still wanted. Stop output, skill.md, and the skill-recorder
docs now match recorder-prompt.md: write SKILL.md plus an importable
utils script, and replay with playwriter commands.
Also:
- jq upload filter is setInputFiles, not a fake file-upload event type
- fill examples define name/url so they run
- recorder stop prints events -r <id> so concurrent recordings do not mix
- e2e test again asserts click x is undefined
The toolbar events -r line stays unstaged with the other toolbar
session work.
Session: ses_fe411266affeR59e58UaOBxQm4
After a recording stops, agents were told to write SKILL.md plus a utils.js
script and to ask a long list of questions first. The toolbar clipboard
prompt also skipped how Playwriter works, so agents often drove raw
Playwright.
recorder stop and the toolbar copy now start with playwriter skill /
https://playwriter.dev/SKILL.md. The next step is recorder events, then a
SKILL.md of markdown steps with example playwriter -e commands built from
the events.
Docs and skill.md match that flow. Frames are described as
recordings/<id>/frames/<ms>.jpg: read the jpeg closest to an event ms.
CLI help no longer mentions snapshot-diff events that the recorder does
not emit.
Tests assert the structured fields already stored on actions (fill.text,
click.button, setInputFiles.files) and that thin page-error events keep
.message. Extension version is 0.0.106.
Session: ses_fe411266affeR59e58UaOBxQm4
The previous prompt told the agent to write a single SKILL.md under
OpenCode-only folders (~/.config/opencode/skills or repo skills/) and
explicitly banned a utils script. That made recorded skills client-specific
and expensive to replay, because every step had to live as inline
playwriter -e snippets.
The prompt now:
- writes personal skills to ~/.agents/skills/<name>/
- writes project skills to .agents/skills/<name>/ in the current repo
- still keeps markdown playwriter examples in SKILL.md
- also writes an importable utils.js (or utils.ts) so replay can import
one function instead of re-sending every command
- validates by replaying the utils script first
SKILL.md remains the human-readable source of the flow. The utils file is
the cheaper replay path: small ESM exports that take { page } plus
parameters. The playwriter sandbox can import local files.
Session: ses_fda163965ffeLTgGJ1AHb1l87g
Playwriter:
- Remove redundant `as` casts that already match the inferred type (CDP session.send results, JSON bodies, recording messages).
- Replace `as Protocol.X` on already-typed CDP params with `params!` or optional access.
- Annotate Hono `c.req.json()` results instead of asserting them.
- Mark fire-and-forget work with `void` (proxy page apply, MCP relay logs).
- Drop redundant callback parameter types in wait-for-page-load.
Extension:
- Switch tsconfig `moduleResolution` from `node` (node10, rejected by tsgolint) to `bundler` so lintcn can typecheck the package.
- Annotate offscreen `sendMessage` results instead of asserting them.
- Mark fire-and-forget chrome APIs and startup loops with `void`.
- Use `isTruthy` instead of ad hoc `filter` type guards for tab IDs.
- Bump manifest to 0.0.105.
Session: ses_fdb498c34ffe4ONDh2gAJKc45I
Preserve the parent CDP session when forwarding Target.detachedFromTarget events from the extension to Playwright.
Playwright now receives OOPIF detach events on the same frame session that received the attach event. It removes the detached FrameSession before recorder startup broadcasts Runtime.addBinding, which prevents stale iframe session errors on iframe-heavy pages.
Add a real cross-site iframe regression case that:
- starts from an empty iframe source
- navigates the iframe across two child documents
- removes the iframe and waits for Playwright to remove its frame
- exposes a binding after detach to prove no stale CDP session remains
Include a patch changeset for the playwriter package.
Session: ses_fe06e7114ffemoRV8JJ9cjFsd3
Refocus the recorder prompt on producing one SKILL.md with direct Playwriter commands.
- require agents to read the Playwriter skill before browser commands
- explain the event timeline and frame lookup with shorter examples
- remove the separate utility script workflow and phase ceremony
- keep locator validation, parameterization, authentication, and end-to-end replay rules
Session: ses_fe06e7114ffemoRV8JJ9cjFsd3
`injectRecorderCallbacks` used to install MAIN-world callbacks that
fetched the relay directly from the page. That fetch runs with the page
origin, so every cross-origin site failed the CORS preflight and the
Record Skill button silently did nothing.
The relay now allows CORS on the two toggle routes, but the extension no
longer depends on it: the request is made from the service worker, which
can reach localhost freely from the extension context.
MAIN world (toolbar)
│ window.postMessage({ __playwriter: 'recorder_start' })
▼
ISOLATED world bridge
│ chrome.runtime.sendMessage({ action: 'actionRecorderStart' })
▼
service worker
│ fetch http://127.0.0.1:19988/recorder/start
▼
relay
`window.postMessage` is used rather than `CustomEvent` because
CustomEvent does not reliably cross the MAIN/ISOLATED world boundary;
postMessage is the pattern Chrome documents for this. The bridge guards
on `__playwriterRecorderBridge` so repeated injections do not stack
listeners, and ignores messages whose `event.source` is not this window.
Feedback now comes back to the page through `executeScript`:
- start failure logs and shows the relay error in a toast, so a failed
start is visible instead of silent
- stop copies the analysis prompt, falls back to a hidden textarea plus
`document.execCommand('copy')` when the async clipboard API is denied,
flips the button back to idle, shows a toast, and plays the success
sound
Session: ses_fe43304dbffeT4MJIAE09Oo2s4
Session: ses_fe45ea8e3ffepbcZz6OQQ9sFSH
Toolbar polish, split across three parts.
**Tooltips.** Native `title` attributes take ~1s to appear, cannot be
styled, and looked out of place next to the dark toolbar. Buttons now use
a `data-tooltip` attribute rendered through `[data-tooltip]::after`, with
a 0.3s delay applied only on the fade-in (`transition: opacity 0.15s ease
0.3s`) so moving between buttons stays instant while a hover still needs
intent. `aria-label` is set alongside every `data-tooltip`, since the
pseudo-element text is invisible to screen readers.
Copy is action-first:
| control | tooltip |
| ------- | ------------------------------------ |
| pin | Select and copy element as prompt |
| record | Capture actions as a reusable skill |
| record (active) | Stop and copy analysis prompt |
| close | Hide toolbar |
| drag | Drag to move |
**Record Skill label.** `Record` alone did not say what gets recorded.
The idle state now reads `Record Skill`. The active state drops the stop
square icon and the red tint, leaving `Recording skill…` in the normal
zinc palette, so a recording in progress does not scream at the user from
every page.
**Sounds.** Buttons play short Web Audio tones synthesized at click time,
so no audio files ship with the extension. Recipes are adapted from
cuelume (https://github.com/Danilaa1/cuelume): `click` for any toolbar
button, `loading` for recording start, `success` when an element is
pinned or a prompt lands on the clipboard. The `AudioContext` is created
lazily and resumed on demand, because pages that never had a user gesture
start it suspended.
Two globals, `__playwriterToolbarShowToast` and
`__playwriterToolbarPlaySound`, let background.ts drive toast and sound
from the service worker. Both are removed in the destroy hook.
Clicking Record when the relay never injected the start callback now
shows `Relay not connected` instead of doing nothing.
Extension version 0.0.104.
Session: ses_fe43304dbffeT4MJIAE09Oo2s4
Session: ses_fe417770affe1ncN9WImr6JxOp
Clicking Record Skill in the toolbar did nothing on a fresh browser. The
toolbar POSTs `{}`, and the route required an existing session:
No sessions. Run 'playwriter session new' first.
`playwriter recorder start` never hit this because the CLI creates a
session before calling the route.
The route now resolves the session like this:
explicit sessionId -> use it
exactly one session -> use it
many sessions -> error, caller must pass a sessionId
no sessions -> create one from the connected extension
The created session reuses the extension connection metadata
(`stableKey`, browser, profile) so it shows up in `playwriter session
list` exactly like a CLI-created one. When no extension is connected the
error is now actionable: "Extension is not connected. Enable Playwriter
on a tab first."
Session: ses_fe45ea8e3ffepbcZz6OQQ9sFSH
The toolbar Record Skill button POSTed to the relay straight from the page
origin, so every site failed the preflight:
Access to fetch at 'http://127.0.0.1:19988/recorder/start' from origin
'https://antidetection.com' has been blocked by CORS policy
Two middlewares blocked it:
- the CORS `origin` callback only echoed `chrome-extension://<our ids>`
- `privilegedRouteMiddleware` rejected any request whose `Sec-Fetch-Site`
was not `same-origin` or `none`
Both now carve out exactly `/recorder/start` and `/recorder/stop`. The
CORS callback reads `c.req.path`, so the exception cannot leak to other
routes.
Why these two routes are safe to open:
- responses carry only `{ recordingId, sessionId, file }`, no page
content, credentials, or user data
- recorded events are served by `/recorder/events/:id`, which keeps the
extension-only origin check
- `Content-Type: application/json` is still required, so a plain form
POST cannot reach them and a preflight is still forced
- token auth in remote mode still applies on every request
- the toolbar shows `Recording skill…` while recording, so a silent
background recording is not possible
Tests assert `/recorder/start` and `/recorder/stop` are not 403 with
`Sec-Fetch-Site: cross-site`, that `/recorder/status` still is, and that
a `text/plain` body still returns 415.
The security docs get a "Recorder route CORS" section explaining the
tradeoff.
Session: ses_fe43304dbffeT4MJIAE09Oo2s4
A Hacker News recording was dominated by
`POST https://www.google-analytics.com/j/collect` entries. Those events
carry no information an agent can replay, and they push real site API
calls out of the context budget when reading `recorder events`.
`isTelemetryUrl()` filters mutating xhr/fetch requests by **hostname
only** against a suffix list: Google Analytics, Google Tag Manager,
DoubleClick, Google Syndication, Google Ad Services, Hotjar, Mixpanel,
Segment, FullStory, Amplitude, and Clarity.
Hostname matching keeps site APIs even when the path or query mentions
analytics, which a substring match over the whole URL would wrongly drop:
```js
isTelemetryUrl('https://uj5wyc0l7x-dsn.algolia.net/1/indexes/Item_dev/query') // false
isTelemetryUrl('https://example.com/api?next=https://analytics.example') // false
```
Unit tests cover both the collector hosts and the false-positive cases.
Session: ses_fe45ea8e3ffepbcZz6OQQ9sFSH
The ghost cursor painted its arrow with
`background-image: url("data:image/svg+xml;base64,…")`. Pages with a
strict `img-src` CSP refuse to load that URL, so on Hacker News every
navigation logged a CSP violation and the cursor rendered as an empty
box.
Changes:
- `setInnerSvg()` parses the markup with `DOMParser`, validates the root
is a real SVG element, imports it into the document, and appends it to
the cursor. No image URL is involved, so no `img-src` rule applies.
- The generated cursor module now exports raw SVG markup
(`pointer-macos-tahoe-svg.ts`) instead of a base64 data URL
(`pointer-macos-tahoe-data-url.ts`). `encode-screenstudio-cursor.ts`
writes the markup with `JSON.stringify`.
- `applyRuntimeVisualOptions()` clears previous children and resets
`background-image` on every style switch, so toggling between
`screenstudio`, `minimal`, and `dot` never stacks two arrows.
The always-on cursor test now asserts the injected element contains a
literal `<svg>` and no `data:image/svg` reference.
Session: ses_fe45ea8e3ffepbcZz6OQQ9sFSH
Bumps the playwright fork to include the `PollingRecorder` singleton fix
and adds the release note for `@xmorse/playwright-core`.
`extendInjectedScript` runs again on bfcache restore and on
`InternalFrameNavigatedToNewDocument` after `goBack()`. Each run created
a second in-page recorder with its own listener set, so one user click
produced two `click` events and one Enter produced two `press` events in
skill recordings.
The recorder is now one instance per `InjectedScript`.
Session: ses_fe45ea8e3ffepbcZz6OQQ9sFSH
Remove the gmail-oauth workspace importer that entered the lockfile while an ignored local helper directory existed at the repository root. The directory is not tracked and is already excluded by .gitignore.
Drop googleapis, googleapis-common, and url-template entries that had no remaining tracked workspace consumer. This keeps fresh installs aligned with the root workspace glob and committed package manifests.
Session: ses_fe59ba520ffeGCZE7vQUezrsp1
Append uncaught browser exceptions to CLI and MCP results for pages assigned directly to each session state. Generalize warning delivery into typed output events so errors raised between calls appear once on the next execution without leaking across agents that share the browser.
Add two-session integration coverage, document safe listener cleanup, add fresh-worktree test bootstrap instructions, regenerate AGENTS.md, and include the playwriter patch changeset.
Verification: focused page-error test, typecheck, and build pass. The full suite passes 255 tests and retains four documented pre-existing failures. lintcn reports only existing repository findings.
Session: ses_fe59ba520ffeGCZE7vQUezrsp1
- recorder-prompt.md: frame naming now uses <ms>.jpg with ms field on
events. Updated awk example for finding the frame before a click.
Note that recording auto-stops after 20 minutes.
- skill.md: document -s <id> flag for recording existing sessions and
mention auto-stop after 20 minutes.
- skill-recorder.mdx: add -s <id> usage, note that recording attaches
to current Playwriter tabs, mention auto-stop.
Session: ses_fe518993fffeLkk8nc00YYO4l2
Complete visual redesign of the extension toolbar, inspired by egaki:
**Dark theme**
- Background: #1c1c1c with rgba(255,255,255,0.1) border
- Pill shape (border-radius: 9999px)
- Deep shadow (multiple layers for depth)
- Zinc-toned icon colors with hover states
- System font stack
**Drag handle**
- 6-dot grip icon (2x3 grid) on the right edge
- Drag to reposition anywhere on the viewport
- Position persisted in localStorage as viewport percentages
- Survives page reloads and window resizes
- Clamped to viewport bounds during drag
**Recording controls**
- Record/Stop button always visible in toolbar
- Shows red circle icon + "Record" label (idle state)
- Shows red square icon + "Recording skill..." label (active state)
- Start recording: calls /recorder/start on the relay via fetch
- Stop recording: calls /recorder/stop, copies analysis prompt to
clipboard, resets UI state
- Toolbar state synced from relay via setRecorderState message
- Globals exposed: __playwriterToolbarSetRecording,
__playwriterToolbarStartRecording, __playwriterToolbarStopRecording
**Pin button**
- New SVG: evenodd fill-rule punches a cursor-shaped hole through the
icon (transparent cutout instead of white fill)
- Renamed from "clipboard" to "pin" for clarity
**Background.ts integration**
- Handle "setRecorderState" one-way messages from relay
- Inject start/stop recording callbacks after toolbar injection
- setRecorderStateInAllTabs() updates toolbar recording state in all
connected tabs
- Re-inject callbacks after DOMContentLoaded navigation
**Version bump: 0.0.99 -> 0.0.100**
Session: ses_fe518993fffeLkk8nc00YYO4l2
**Relay recording state notifications**
Add notifyExtensionRecorderState() that sends a fire-and-forget
{ method: "setRecorderState", params: { recording: bool } } message to
all connected extensions when action recording starts or stops. This
lets the extension toolbar show a live recording indicator without
polling.
The ActionRecordingManager onActiveChanged callback drives these
notifications: fires on first recording start and last recording stop.
**/recorder/start optional sessionId**
The sessionId field is no longer required in the POST body. When omitted
the relay auto-resolves:
- Single session: uses that session automatically
- No sessions: returns an error asking to create one
- Multiple sessions: returns an error listing available session ids
The request body itself is now lenient (empty body or missing
Content-Type is treated as {}).
**CLI timeout**
Add AbortSignal.timeout(30_000) to the recorder start fetch in the CLI.
Previously a hung relay would block the CLI forever; now it fails after
30 seconds.
Session: ses_fe518993fffeLkk8nc00YYO4l2
Major robustness overhaul for ActionRecorder start/stop lifecycle:
**Start timeout (enableTimeoutMs, default 15s)**
_enableRecorder can hang forever if a frame never gets a main world
(empty target, OOPIF). The recorder now races start against a timeout
and against stop(). If enable does not resolve within the timeout, start
throws a RecordingError and cleans up the zombie recording.
**Max duration auto-stop (maxDurationMs, default 20 minutes)**
Recordings auto-stop after 20 minutes to prevent unbounded resource
usage. The stop reason is recorded as "max-duration" in the events.
**State machine**
Add a "starting" state between construction and "recording". This lets
stop() abort a hung start cleanly. The stop reason is now tracked as a
RecordingStopReason union: "user" | "max-duration" | "context-closed" |
"start-failed".
**Disable timeout (disableTimeoutMs, default 2s)**
_disableRecorder can also hang on stuck frames. Race it against a 2s
timeout so stop() always returns promptly.
**Ms-based frame naming**
Screencast frames are now named <ms>.jpg (milliseconds since recording
start) instead of <t>-<seq>.jpg. Every recorded event gets a new `ms`
field on the same clock. This makes it trivial for an agent to find the
frame just before a click:
ls "$FRAMES" | awk -F"[.-]" "\$1+0 <= 8200 { print }" | tail -1
**Screencast fixes**
- Do not double-ack Page.screencastFrame (Playwright CDP session
already acks)
- Guard startPageScreencast against races with stop
- Clean up screencasts map on errors
- Use synchronous fs.writeFileSync for frame persistence (was async
fs.writeFile with no error handling)
**Click ripple fix**
Replace @keyframes animation with CSS transition for the click ripple
overlay. The keyframes approach required injecting a <style> element
that could be blocked by CSP. The transition approach works without any
injected styles. Also re-inject click ripple on hard navigation since
the MAIN world script dies.
**ActionRecordingManager changes**
- Add onActiveChanged callback (fires true when first recording starts,
false when last recording stops)
- Extract addRecording/removeRecording/markActive helpers
- Pass through new timeout options from manager.start to recorder
**Unit tests**
New test file with 5 tests covering:
- Start times out a hung _enableRecorder
- Stop during hung start removes the recording
- Max duration auto-stop
- Start still fails when disable also hangs
- Stop after enable but before start finishes
Session: ses_fe518993fffeLkk8nc00YYO4l2
Picks up 2d70e2fe7:
- frames.ts: race extendInjectedScript against a 1s timeout so it does
not hang forever on frames without a main world (empty targets, OOPIFs)
- recorder.ts: skip the built-in recorder overlay toolbar when
recorderMode is "api"; the extension toolbar handles recording UI
Session: ses_fe518993fffeLkk8nc00YYO4l2
The homepage relay diagram used nested boxes that were hard to
scan. Flatten it to Browser, localhost relay, and Client, with
the uncontrolled-tab note under the browser box.
In remote-access and security, replace filled Unicode arrows
(◄ ► ▲ ▼) with ASCII < > ^ v so the diagram renderer keeps
edges aligned. Also re-align the sandbox path table and the
host.docker.internal platform table.
index.mdx and pages/index.mdx stay in sync. They both render
the same homepage architecture diagram.
Session: ses_fe627d9dbffeHNaljibGg42LXw
Hero headings were 40/56/72 with py-18/24/28. That pushed the
first content too far down. Use 32/42/52 and py-8/10/12 instead.
VideoBackgroundShader writes inline opacity 1 on the canvas once
it is ready, so canvasClassName opacity utilities never apply.
Dim the shader with an outer wrapper at opacity-70.
Remove the Learn more arrow links. The unused ArrowDownIcon
goes with them.
Session: ses_fe627d9dbffeHNaljibGg42LXw
Bump @holocron.so/vite from 0.21.0 to 0.29.0 and
better-auth-drizzle-adapter from 1.0.5 to 1.1.0.
docs.json sets decorativeLines to none so the new Holocron
default does not draw extra page chrome.
The lockfile follows those two direct bumps and the Holocron
subtree (safe-mdx, mermaid, motion, and related peers).
Session: ses_fe627d9dbffeHNaljibGg42LXw
These notes belong to recorder work that already landed. They were
never committed, so the next playwriter changelog would skip them.
fix-recorder-spa-fill:
- history.pushState mid-fill no longer splits one field into two actions
- fill snapshot/storage capture waits until typing pauses
- cookie diffs are scoped by origin
- recording files are replaced atomically
recorder-allow-exec-during:
- recorder start no longer tells the agent to freeze the session
- the agent may exec if the user asks
Session: ses_fe627d9dbffeHNaljibGg42LXw
TabCapture needed an extension-icon click and produced a video
agents had to slice with ffmpeg. Page.startScreencast needs no
grant and already skips static frames.
Each compositor change is written to
~/.playwriter/recordings/<id>/frames/<t>-<seq>.jpg. A pink
ripple still marks user clicks. recording-stopped carries
framesDir and frameCount.
Session: ses_fe69b2cbbffeXWH0kfihFS54b1
Drop late click screenshots, custom coordinates, the 200ms stall,
scroll polling, and file-input scanning. Those either froze the
page, lied about the click frame, or duplicated Playwright.
Clicks record immediately. Typing reuses the focused-element
locator. File picks are setInputFiles actions. Network stays
POST/PUT/PATCH/DELETE xhr and fetch only.
Session: ses_fe69b2cbbffeXWH0kfihFS54b1
Drop post-action aria snapshots, cookies, storage, and focus.
Those CDP walks froze the page on every key. Persist Playwright
actions only, plus mutating xhr/fetch (POST/PUT/PATCH/DELETE).
Clicks store page x/y from the injected recorder and a tmp PNG
with a red rectangle on the locator box. Typing reuses the
cached selector. Double clicks stall 200ms so they are one
action. Persist is debounced 50ms.
The playwright submodule points at the JsonRecordActionTool
selector cache and click stall.
Session: ses_fe69b2cbbffeXWH0kfihFS54b1
The reload script previously only built playwriter and opened
chrome://extensions. Local code changes in the running daemon
were never picked up because ensureRelayServer skips restart
when versions already match.
Add a hidden `playwriter serve restart` command that force-kills
port 19988 and starts a new detached relay, then call it from
the root reload script after the playwriter build.
Session: ses_fe69b2cbbffeXWH0kfihFS54b1
Fill keystrokes from Playwright actionUpdated now mutate the last
action in place. The 800ms coalesce timer is gone.
Files are ~/.playwriter/recordings/<id>.json. recorder events still
prints one JSON object per line for jq. Old jsonl files still parse.
Session: ses_ff0d0feb1ffeF63vbdQE7Icb8V
API-mode recording no longer freezes the page on each click. The
injected recorder skipped unused ARIA walks (playwright submodule
26947bb2d).
Clicks and navigations now write immediately. Only fill coalesces
keystrokes. Before, the 800ms delay made clicks appear after the
navigation they caused.
Cookie diffs work in extension mode via Network.getCookies.
context.cookies() fails there because Storage.getCookies has no
browser session.
Icon-font glyphs are stripped from locator names so a login button
records as getByRole('button', { name: 'Login' }).
Session: ses_ff0d0feb1ffeF63vbdQE7Icb8V
`recorder events <recordingId> [...eventIds]` mixed two kinds of ids in
positionals: `recorder events 1 4 7` read as one recording id followed
by event ids, which is impossible to parse at a glance. Positionals are
now only event ids and the recording is selected with an optional
`-r, --recording <id>` flag defaulting to the latest recording:
playwriter recorder events # latest recording, thin view
playwriter recorder events 4 7 # full details of events 4, 7
playwriter recorder events -r 2 4 7 # same, for recording 2
Updated help examples, `recorder stop` next-steps output,
recorder-prompt.md and the website Skill Recorder page accordingly.
A matching design rule (never mix different id kinds in positionals) was
added to the goke CLI framework docs.
Session: ses_ff54ec81cffeYS7SzMYkGaS5tf