Commit Graph

200 Commits

Author SHA1 Message Date
Tommy D. Rossi 3758ecac7e release: playwriter@0.5.0
Skill Recorder, live RTMP streaming, native ESM imports in execute,
daemon tutorial on idle icon click, PLAYWRITER_EXEC_TIMEOUT, screenshot
UI hiding, tracked page errors, quieter CDP logs, and recorder/sandbox
fixes.

Also ships @xmorse/playwright-core@1.59.11 (faster API-mode recorder).

Fixes #114
Fixes #108
Fixes #105
Fixes #107
Fixes #102
Fixes #99
Fixes #96
Fixes #95
Fixes #74

Session: ses_fd2023624ffeK812PCDMCsUNVJ
2026-08-23 11:45:13 +02:00
Tommy D. Rossi 9722518c5d Close wildcard CORS on recorder start/stop.
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
2026-08-23 11:43:25 +02:00
Tommy D. Rossi ea7cfb542a Fade the in-page toolbar in and cheapen pin-mode overlay updates.
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
2026-08-23 11:39:49 +02:00
Tommy D. Rossi 24bec4ef83 Clarify the idle-icon tutorial and add a playwriter changeset.
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
2026-08-22 15:41:38 +02:00
Tommy D. Rossi c38866dea9 Serve the Playwriter skill from playwriter.dev instead of GitHub.
`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
2026-08-22 15:39:20 +02:00
Tommy D. Rossi 9d0ff4ac6a Open a daemon tutorial when the extension icon is clicked with no relay.
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
2026-08-22 15:23:11 +02:00
Tommy D. Rossi a6cb87a74e Show Stop recording with a red square on the toolbar
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
2026-08-22 12:24:34 +02:00
Tommy D. Rossi c06dd1e7e3 Stop failing toolbar Record when many sessions are open.
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
2026-08-22 10:48:08 +02:00
Tommy D. Rossi abd6709e9d Point recorder skill generation at playwriter commands and persist action fields in tests.
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
2026-08-22 10:27:58 +02:00
Tommy D. Rossi 1bbd3a2dde Fix lintcn errors in playwriter and the Chrome extension.
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
2026-08-21 17:30:45 +02:00
Tommy D. Rossi 588b96d527 fix(extension): route toolbar recorder toggles through extension messaging
`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
2026-08-20 16:25:19 +02:00
Tommy D. Rossi b7a973bcd6 feat(extension): toolbar tooltips, Record Skill label, and click sounds
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
2026-08-20 16:25:08 +02:00
Tommy D. Rossi bdf7ba644a feat: dark toolbar redesign with drag handle and recording controls
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
2026-08-19 18:46:28 +02:00
Tommy D. Rossi 750bf3951d extension: prevent shift+click text selection in pin mode
Add a capture-phase mousedown handler that calls preventDefault()
while pin mode is active. The browser starts text selection on
mousedown, not click, so the existing click handler preventDefault
was too late to suppress shift+click selection behavior.

Session: ses_03126316bffeg7zoAmgWgz8P6W
2026-08-06 15:01:34 +02:00
Tommy D. Rossi 68f345f27f extension: shift-click multi-select in toolbar pin mode
Shift+clicking elements in pin mode now accumulates them instead of
exiting. Each shift-click adds the element to the selection, keeps
the green outline persistent, and updates the clipboard with all
accumulated locator strings joined by comma + newline.

Normal click (no shift) finalizes: includes any shift-accumulated
elements in the clipboard, then exits pin mode. Single click without
any prior shift-clicks behaves exactly as before.

Edge cases handled:
- Shift-clicking the same element twice is a no-op (prevents saving
  the already-green outline as the "original" style)
- On final normal click, persistent outlines are cleared before the
  temporary flash so cleanup never restores green
- Escape and toolbar close clear all persistent outlines
- Re-entering pin mode resets accumulated state

Manifest version bumped to 0.0.99.

Session: ses_03126316bffeg7zoAmgWgz8P6W
2026-08-06 08:07:20 +02:00
Tommy D. Rossi 4147d06b8e drop noisy CDP events + batch log writes to reduce relay flooding
Filter high-frequency CDP events that cause relay saturation on
realtime/large pages (issue #96). Two layers:

**Dropped entirely** (not forwarded, not logged):
- Network.dataReceived
- Network.resourceChangedPriority

**Filtered from logs only** (still forwarded to Playwright):
- Network.*ExtraInfo (needed by ResponseExtraInfoTracker for allHeaders())
- Network.webSocketFrame* (needed by page.on websocket)
- Network.requestServedFromCache
- Network.requestWillBeSent, responseReceived, loadingFinished

Extension-side filtering prevents events from hitting the WebSocket.
Relay-side filtering provides backwards compat with old extensions.

Both relay-server.log and cdp.jsonl now batch writes with a 500ms
flush interval instead of one appendFile per event. Added flush()
to Logger type for crash/exit paths.

Fixes #96

Session: ses_0ed5496d9ffeeoNKF0jxBnUQkH
2026-06-29 11:46:24 +02:00
Tommy D. Rossi a17af39616 release: playwriter@0.4.0
Cloud browser sessions, headless browser mode, API key auth,
multi-profile relay routing fix, cloud billing race fix, cloud login fix.

Fixes #80
2026-06-25 14:04:03 +02:00
Tommy D. Rossi 7631d5c4e1 Fix oracle review: add Network.enable + clearTimeout + remove addScriptToEvaluateOnNewDocument
- Added Network.enable and Browser.getWindowForTarget to timeout map (both
  are part of Chromium page init and hang on frozen tabs)
- Removed Page.addScriptToEvaluateOnNewDocument from timeout map because
  user-provided scripts with runImmediately:true can legitimately take >10s
- Added clearTimeout in finally block to prevent dangling timers when
  commands resolve before the timeout fires

Session: ses_1111c0a0cffe6Wt66mAFRmke9e
2026-06-22 12:57:34 +02:00
Tommy D. Rossi 7dfabe239c Add 10s timeout for CDP init commands to prevent frozen tabs from blocking connection
Ghost Browser (and potentially other Chromium forks) can have frozen/hibernated
tabs where chrome.debugger.sendCommand hangs forever. This blocked the entire
Playwright connectOverCDP setup because initialization commands like Page.enable,
Runtime.enable, Log.enable etc. would each wait 30s before timing out.

Added a FAST_CDP_COMMAND_TIMEOUT_MS map with 11 CDP commands that should return
near-instantly on a healthy tab. These now use Promise.race with a 10s timeout
via sendCommandWithTimeout(). Commands not in the map (Runtime.evaluate,
Network.enable, Input.*, DOM.*, screenshots, navigation) pass through without
any timeout since they can legitimately take longer.

Applied to three code paths:
- Default command handler (generic CDP forwarding)
- Runtime.enable/disable special case (OOPIF iframe handling)
- Target.setAutoAttach broadcast loop (all connected tabs)

Frozen tabs now fail in 10s instead of 30s, giving connectOverCDP enough time
to finish initializing the responsive tabs.

Session: ses_1111c0a0cffe6Wt66mAFRmke9e
2026-06-22 12:52:44 +02:00
Tommy D. Rossi bd6c7eb363 fix playwriter multi-profile browser routing
Route extension relay connections by their per-profile install id before falling back to Google account identity. This keeps two Chrome profiles signed into the same account from replacing each other in the relay.

Keep Playwright-created tabs in the intended Chrome window by remembering the window where the extension was activated and using that window for Target.createTarget and initial tabs.

Handle Chrome debugger cancellation as a global detach event. When Chrome reports canceled_by_user, clear all tracked debugger tabs and emit detach events so Playwright stops sending commands to tabs Chrome already detached from. Also avoid resurrecting stale error tab state if the detach happens while attachTab is still unwinding.

Validation:
- pnpm test --run src/relay-state.test.ts
- pnpm typecheck
- pnpm build in extension/
- Manual two-profile Hacker News navigation and comment extraction
- Mythos review found no blocking issues

Session: ses_120923e0fffe5l4GT77WwyXEGP
2026-06-19 13:31:31 +02:00
Tommy D. Rossi 2bd7c3725b Improve extension browser name detection
Use full user-agent client hints when the browser exposes them so the extension can report more specific Chromium-family names before falling back to low-entropy brands and the user agent string.

This keeps detection inside the browser instead of guessing from local filesystem installs. If a browser only exposes the generic Google Chrome brand, Playwriter keeps showing Chrome.

Bump the extension manifest to 0.0.95 and add a playwriter patch changeset for the browser list improvement.

Session: ses_143f600f9ffe0ftfzwvYK56gQV
2026-06-12 16:24:27 +02:00
Tommy D. Rossi bc0e044095 updates 2026-05-19 16:07:16 +02:00
Tommy D. Rossi aa1183c23d feat(extension): update pin copy toast to guide users toward agent prompt usage
The old toast just said "Copied pin #N" which gave no hint about what to do next. Now it says "Copied playwriter element reference, use it in your agent prompt" so users know to paste it into their AI agent.

Bump extension to 0.0.94.

Session: ses_1d03be05affeS58tMdQDs6GMbj
2026-05-16 09:52:49 +02:00
Tommy D. Rossi a77f5ad861 chore(deps): bump vite ^8.0.3 to ^8.0.12 and update lockfile
Update vite in root and extension packages from ^8.0.3 to ^8.0.12.

Lockfile reflects all transitive dependency updates from this session:
goke 6.12.1, @holocron.so/vite 0.8.0, dompurify 3.4.3, katex 0.16.46,
and various @csstools patches.
2026-05-16 09:45:37 +02:00
Tommy D. Rossi 7195359b22 fix: strip Next.js webpack layer prefixes from React source paths
bippy.normalizeFileName strips "/app-pages-browser/" but not the
parenthesized form "/(app-pages-browser)/" that Next.js webpack
actually uses in source maps. Added cleanFileName/cleanName that
runs after normalizeFileName and strips any /(layer-name)/ prefix
using regex, plus trailing "./".

Covers all Next.js webpack layer names: (app-pages-browser), (ssr),
(rsc), (action-browser), (pages-dir-browser), (pages-dir-edge),
(pages-dir-node).

Applied in both:
- Extension context menu handler (background.ts)
- MCP react-source.ts (getReactSource + getReactComponentInfo)

Also applied oracle review fixes:
- userGesture: true on clipboard Runtime.evaluate calls to prevent
  NotAllowedError when page loses focus after context menu
- Defensive fileName null check before building clipboard text

Session: ses_208943d84ffenqdbjxK0cO0iak
2026-05-05 11:38:36 +02:00
Tommy D. Rossi d0c93e0f3a feat(extension): add "Copy React Component Source Path" right-click menu item
New context menu item that copies the React component source file path
to clipboard when right-clicking an element on a React page.

Uses bippy (React fiber introspection library) to walk the fiber tree
from the right-clicked DOM element up to the nearest composite component
with source location info. Tries getSource() first (direct __source prop
from JSX transform), then falls back to getOwnerStack() for builds with
source maps.

Copies "path/to/file.tsx:42" format to clipboard on success (green flash),
shows red flash if the element is not part of a React app or no source
maps are available.

Also refactored all inline JS strings in the context menu handlers to use
string-dedent with descriptive variable names for syntax highlighting.

extension@0.0.93

Session: ses_208943d84ffenqdbjxK0cO0iak
2026-05-05 11:24:28 +02:00
Tommy D. Rossi ae136e91ec fix: copy pinned element command directly
Remove the natural-language wrapper around pinned element clipboard text so toolbar and context-menu pins copy a shell-ready playwriter command. This keeps the copied text compact while still delegating DOM and React inspection to inspectPinnedElement.

Session: ses_221fdd7c5ffexkyCmOtfmgwjoI
2026-04-30 17:26:21 +02:00
Tommy D. Rossi fabd25e8f6 feat: add React component info for pinned elements
Expose getReactComponentInfo so agents can inspect the React component name, parent hierarchy, sanitized props, and source metadata for a locator or element handle. Non-React elements return null so the helper is safe to call from generic DOM inspection flows.

Update toolbar and context-menu pinned element copy to use a short inspectPinnedElement(url, expression) call. The wrapper resolves the pinned element, logs outerHTML plus React info internally, and keeps copied prompts small.

Session: ses_221fdd7c5ffexkyCmOtfmgwjoI
2026-04-30 15:51:45 +02:00
Tommy D. Rossi a145e5c5d5 simplify pinned element clipboard snippet to just outerHTML
Removed the verbose describeElement() summary (tag, id, classes, role, aria,
href, type, text, rect, visible) — all redundant since the agent runs the code
and sees outerHTML directly. Also fixed the evaluate call to use a static
property access (globalThis.playwriterPinnedElem1) instead of passing the
pin number as a runtime argument.

Bumps extension to 0.0.90.

Session: ses_25fb67764ffeSaDgE3xne8F5sB
2026-04-18 13:20:43 +02:00
Tommy D. Rossi f7499bd1d9 fix: two-element cursor split so move and press transitions don't interfere
The cursor DOM is now outer (translate, move timing) + inner (scale+opacity,
press timing). Previously setPressed() overrode transitionDuration to 140ms
which retargeted in-flight 1500ms move transitions, causing clicks to
'sprint' the cursor to the target.

Also:
- Fix toast animation jump when positioned above anchor (toolbar.ts):
  keyframes now use --toast-transform CSS variable so translateY(-100%)
  base transform is included during animation, not just after.
- Remove stale navigation-rehydration entry from CHANGELOG (was cut in
  the simplification pass but the changelog entry remained).
2026-04-13 09:21:25 +02:00
Tommy D. Rossi 7455abbf5a feat: always-on ghost cursor injected by extension on tab attach
The Chrome extension now injects the ghost-cursor-client.js bundle into
every attached tab via CDP Page.addScriptToEvaluateOnNewDocument +
Runtime.evaluate. The cursor auto-enables on load (top frame only, skips
iframes) and stays on the last spot Playwright clicked or moved to.

Key changes:

- Extension injects cursor bundle via ?raw vite import, inlined into
  background.js at build time. Torn down on detachTab.
- RecordingGhostCursorController renamed to GhostCursorController with
  attachToPage/detachFromPage instead of enableForRecording/disableForRecording.
  Single instance on PlaywrightExecutor, wired in setupPageListeners.
- Recording no longer toggles the cursor on/off.
- 5s idle debounce: cursor fades out (600ms) after no activity, wakes on
  next action by teleporting to the new target and fading back in (140ms).
- Animation polish (Emil Kowalski guidelines): easeInOutCubic for moves,
  dedicated 140ms press duration with strong ease-out, subtler 0.95 press
  scale, transform-origin anchored to cursor hotspot.
- Slower cinematic defaults: 220ms min, 1500ms max, 1.2 px/ms.
- Test: verify cursor element exists on attached tab without explicit enable.
2026-04-13 09:10:34 +02:00
Tommy D. Rossi b9c96dba25 feat(toolbar): wrap clipboard in natural-language prompt + bash-safe \u0027
Clipboard now copies a prompt-style message instead of a bare JS snippet:

    see the element I pinned in the playwriter tab `playwriter -e '<code>'`

The user pastes this into a chat with their agent. The agent reads it as
"user pinned an element, here is the code to run" and executes it with
their own session (via `-s <id>` or `PLAYWRITER_SESSION`). Makes the
intent obvious when the clipboard lands in a chat window — the agent
sees the goal plus the exact code without needing instructions.

Also fixes a latent bash-safety bug in `buildInspectionCode`. The code
is wrapped in bash `'...'` so single quotes are the only character we
cannot have inside it. `JSON.stringify` does NOT escape literal `'`, so
element text containing an apostrophe (e.g. "Don't save", aria labels
like "I'm loading", URLs with `?q=what's+new`) leaked a literal `'` into
the code and broke the outer bash wrapper.

Fix: replace every `'` in the JSON-stringified URL and summary with
`\u0027`. This is a valid JSON escape that the JS engine parses back to
`'`, so the string value is unchanged at runtime, but the generated
code is now guaranteed to contain zero literal single quotes regardless
of page content.

Single vs double bash quotes comparison:

    '...'   literal everything except '          ← 1 problem char
    "..."   $foo expands, `cmd` runs,            ← 4 problem chars
            \ escapes, " terminates

Our code is full of " (JSON strings) and can contain $ from element
text, so double quotes are a non-starter. Single quotes with the
\u0027 substitution is the simplest robust solution.

Verified with hostile input:
- url with "what's+new" in query → 0 literal ' in code
- summary with "Don't save 'now'" → 0 literal ' in code
- JS eval of the escaped code reproduces the original strings

Bumps extension to 0.0.85. Bundle 72.79 kB → 72.90 kB (+110 bytes for
the new prose prefix and the two .replace calls).
2026-04-11 16:47:45 +02:00
Tommy D. Rossi 9e33e21484 refactor(toolbar): drop session plumbing, copy raw eval code on pin-click
The previous implementation fetched the CLI session list from the relay via
a new GET /extension/sessions endpoint, plumbed it into the MAIN-world
toolbar via chrome.scripting.executeScript args, cached it in a closure,
and baked `playwriter -s <id> -e '<code>'` into the clipboard. The
picked session id was fragile: sessions[0] is the oldest by insertion
order, which is almost never the session the agent is actually using.
The agent also has its own active session already — guessing one is
the wrong job for a browser extension.

Drop all of it. The toolbar now copies only the JS eval code. The agent
wraps it in their own `playwriter -s <session> -e '<paste>'` using
whichever session they already have (or they create a new one). The
code is JSON.stringify'd so it contains no single quotes and slots
into any shell quoting strategy the agent prefers.

Removed:
- `GET /extension/sessions` on the relay (playwriter/src/cdp-relay.ts)
- `fetchSessionSummary` helper in background.ts
- `ToolbarConfig` interface + import + args plumbing
- Async wrapping around `chrome.scripting.executeScript` in both
  injection sites (attachTab + webNavigation.onDOMContentLoaded)
- `currentSessions` / `nextSuggested` closure vars in toolbar.ts
- `window.__playwriterUpdateSessions` setter
- `pickSessionId()` helper
- `shellSingleQuote()` helper (agent does their own shell quoting)
- Re-init update routing through `window.__playwriterUpdateSessions`

`onClick` now just:

    copyText(buildInspectionCode(n, url, summary))
    showToast(`Copied inspection code (pin #${n})`)

The eval code shape is unchanged:

    state.page=context.pages().find(x=>x.url()===URL)||context.pages()[0];
    console.log(SUMMARY + "\n\nouterHTML:\n" +
      await state.page.evaluate(n => globalThis["playwriterPinnedElem"+n]?.outerHTML, N))

Bundle drops from 74.35 kB to 72.79 kB (-1.56 kB). Bumps extension to
0.0.84 and playwriter to 0.0.108 with CHANGELOG entries in both packages.
2026-04-11 14:45:42 +02:00
Tommy D. Rossi 2cbb943b00 feat(toolbar): copy full playwriter -s X -e inspection command on pin-click
The in-page toolbar's pin button used to copy a plain
`see playwriter reference: globalThis.playwriterPinnedElem1` string that
still required the agent to wrap it in their own `-e` call. It now copies
a complete, ready-to-run shell command:

    playwriter -s 1 -e 'state.page=context.pages().find(x=>x.url()===URL)||context.pages()[0]; console.log(SUMMARY+"\n\nouterHTML:\n"+await state.page.evaluate(n=>globalThis["playwriterPinnedElem"+n]?.outerHTML,N))'

Two `;`-separated statements: pick the right page by URL with first-page
fallback, assign state.page for follow-up calls, log a pre-baked element
summary plus the current outerHTML of `globalThis.playwriterPinnedElemN`
fetched via one `page.evaluate`. No `getCleanHTML`, no
`getLocatorStringForElement`, no ElementHandle dance, no locator round-
trips — the generated JS is runnable against any playwriter version.

Everything the eval code doesn't strictly need a Playwright sandbox for is
pre-computed at pin time in MAIN world (synchronous DOM access) and baked
into the command via `JSON.stringify`:

- page URL from `location.href`
- element summary: tag, id, classes, role, aria-label, name, href, type,
  truncated text, bounding rect, visibility
- pin index

Session-id plumbing:

- New public `GET /extension/sessions` endpoint on the relay returns
  `{ sessions: [{ id, stateKeys }], nextSuggested }`. It sits outside the
  `/cli/*` privileged middleware, which rejects the cross-site
  `Sec-Fetch-Site` that chrome-extension fetches to 127.0.0.1 send. It
  exposes the same data as `playwriter session list`, so no new attack
  surface.
- `background.ts` `fetchSessionSummary()` calls the endpoint with a
  1500ms timeout and falls back to `{ sessions: [], nextSuggested: '1' }`
  on any failure (relay down, old relay without the endpoint, timeout,
  malformed JSON). The extension stays backward-compatible with older
  relays.
- Both toolbar injection sites (`attachTab` + `webNavigation.onDOMContentLoaded`
  re-inject) now fetch the summary and pass it as
  `chrome.scripting.executeScript({ args: [summary] })`.
- The guarded re-init path in `initPlaywriterToolbar` now calls
  `window.__playwriterUpdateSessions?.(config)` so re-injects refresh the
  closure cache instead of becoming no-ops.
- `pickSessionId()` prefers the first existing session id, falling back
  to `nextSuggested` (typically "1") so the agent stays in their current
  state when one exists.

Toolbar runtime behavior does not depend on the relay: sessions are
cached in closure at inject time, clipboard writes stay offline, and
all failure modes surface as visible `undefined` in the command's
output rather than exceptions or timeouts.

Bumps extension to 0.0.83 and playwriter to 0.0.107 with CHANGELOG
entries in both packages.
2026-04-11 14:29:20 +02:00
Tommy D. Rossi 365e5ba3ca fix: restore toolbar size to previous (26px buttons, 3px padding) 2026-04-11 13:23:24 +02:00
Tommy D. Rossi f5c513df1c fix: logo icon visibility in active state, toast font/padding 2026-04-11 13:02:01 +02:00
Tommy D. Rossi 794ff593e0 fix: further reduce toolbar size 2026-04-11 13:00:00 +02:00
Tommy D. Rossi 1c44c7ae99 fix: reduce toolbar padding and button size 2026-04-11 12:54:33 +02:00
Tommy D. Rossi 0dfb73879d feat: polish toolbar pin-element UX
- Deactivate pin mode after each click (toggle off after pin)
- Green flash outline: 3px → 1px to match overlay edge thickness
- Clipboard text prefixed: 'see playwriter reference: globalThis.playwriterPinnedElemN'
  so both user and AI agent understand the string's purpose at a glance
- Replace clipboard icon with Playwriter logo-square SVG (inlined)
2026-04-11 12:53:59 +02:00
Tommy D. Rossi be1778f8b3 fix: resolve remaining pw reference and strict cast errors in toolbar
- Replace (window as Record<string, Element>)[name] with a typed template literal
  index signature on the Window interface: [key: playwriterPinnedElem${number}]: Element
- Return type of allocatePinName narrowed to playwriterPinnedElem${number} so
  the assignment window[name] = target type-checks without any casts
- Fix stray pw reference missed during declare global refactor
2026-04-10 21:25:24 +02:00
Tommy D. Rossi f96004d1d7 refactor: use declare global to augment Window instead of interface cast 2026-04-10 21:22:47 +02:00
Tommy D. Rossi 240e211c13 refactor: replace as any with PlaywriterWindow interface in toolbar
Declare PlaywriterWindow inside the function body (interfaces are TypeScript-only
and stripped at compile time, so safe inside the serialized func injection).
Cast window once to pw = window as PlaywriterWindow and use typed property access
throughout. Dynamic element assignment uses Record<string, Element>.
2026-04-10 21:21:22 +02:00
Tommy D. Rossi 023ed8da04 feat: add in-page floating toolbar with pin-element mode (extension v0.0.82)
Injects a floating toolbar into the top-right corner of every Playwriter-connected
tab via chrome.scripting.executeScript({ world: 'MAIN', func: initPlaywriterToolbar }).

- Toolbar uses a closed Shadow DOM for full style isolation from page CSS
- Matches mesurer.ibelick.com visual language: white pill background, mesurer's
  exact shadow, 32x32 buttons with rounded-[8px], #0d99ff active state
- Selection overlay uses four 1px edge divs + color-mix fill (same technique as
  mesurer's measurement-box) instead of a CSS outline — thinner and cleaner

Pin element mode (clipboard icon button):
- Toggle activates crosshair cursor + hover overlay tracking the element under cursor
- Click assigns the element to globalThis.playwriterPinnedElemN and copies
  'globalThis.playwriterPinnedElemN' to clipboard — identical format to right-click menu
- Esc exits pin mode; × button hides toolbar for the session
- Uses window.__playwriterPinCount as a shared MAIN-world counter so toolbar and
  right-click context menu never produce conflicting element indices

background.ts changes:
- Inject toolbar in attachTab() after debugger attaches
- Destroy toolbar in detachTab() (fire-and-forget scripting call)
- Re-inject via chrome.webNavigation.onDOMContentLoaded after hard navigations
- Right-click handler now reads window.__playwriterPinCount via Runtime.evaluate
  instead of the Zustand store's pinnedCount field

Removed pinnedCount from TabInfo — counter now lives entirely in MAIN world.
No new manifest permissions needed (scripting + webNavigation already present).
2026-04-10 21:17:12 +02:00
Tommy D. Rossi 7e633d5ac0 fix: prevent cross-browser extension takeover loops
Persist a per-install fallback identity for the extension so Chromium-family browsers without chrome.identity profile data no longer collapse to browser-only relay keys and replace each other.

Also stop replaced workers from reclaiming the relay slot while the replacement connection is merely idle, add a regression test that launches a second Chromium context against the same relay, and bump the shipped package versions/changelogs for the extension and playwriter packages.
2026-04-07 21:43:46 +02:00
Tommy D. Rossi 129fff896c feat: auto-relocate popup windows into source tab's window
Chrome popup windows (window.open with features, OAuth login popups)
could not be controlled by Playwriter because chrome.debugger does not
attach to separate popup windows. Agents had to work around this with
cmd+click (`{ modifiers: ['Meta'] }`) to coerce new tabs instead.

The extension now listens for chrome.windows.onCreated with
type === 'popup' and relocates the tab(s) into the source tab's window,
then auto-attaches Playwriter so the new tab appears in context.pages().

Gating: the popup is only relocated when the source tab is already
Playwriter-connected. Otherwise the popup is left alone so unrelated
sites keep their normal Chrome popup behavior. Source tab is discovered
via chrome.webNavigation.onCreatedNavigationTarget because
chrome.tabs.Tab.openerTabId is unreliable for window.open popups on
Chromium 145 (leaves it null).

Focus is not stolen — the relocated tab is appended at the end of the
source tab's strip (index: -1) but the user's active tab stays active.

Executor changes
- Replaced setupPopupDetection with setupNewPageLogging. Warning
  message changed from "Popup window detected ... cannot be controlled"
  to "New page opened from current page (index N, initial url: ...)"
  pointing the agent at the controllable tab.
- Added MIN_EXTENSION_PLAYWRITER_VERSION check so agents running with
  old extensions see a targeted warning to update via chrome://extensions.
- enqueueWarning is now called from warnIfExtensionOutdated so the LLM
  sees version-skew messages in its warning stream, not just stdout.

Extension changes
- New `webNavigation` permission for the source-tab correlation map.
- connectTab now verifies `chrome.tabs.get(tabId)` before writing an
  `error` state, preventing dead tabs from leaking into store.tabs /
  badge counts / group sync when a tab closes mid-attach.
- popupSourceTabMap entries are cleaned up both on a 10s timeout and
  when chrome.tabs.onRemoved fires, capping memory for plain-new-tab
  cases that never trigger windows.onCreated.

Tests
- popup-relocation.test.ts verifies both paths: when a Playwriter tab
  is connected the popup is relocated into the main window and appears
  in context.pages(); when no tab is connected the popup-type window
  remains (extension does not interfere).

Skill docs
- Removed section #10's cmd+click workaround; added a note about
  auto-relocation under "working with pages".

Versions
- extension 0.0.79 -> 0.0.80
- playwriter 0.0.103 -> 0.0.104
2026-04-05 14:22:21 +02:00
Tommy D. Rossi 7d54c4b6a0 fix: handle chrome-extension:// iframes from other extensions (#18)
Chrome's chrome.debugger.attach API refuses to attach to tabs containing
chrome-extension:// iframes from other extensions (LastPass, SurfingKeys,
password managers, etc). This causes the extension to immediately disconnect
after clicking the icon.

Two-layer fix:

1. Pre-attachment (removeRestrictedIframes): when chrome.debugger.attach fails
   with the chrome-extension:// error, use chrome.scripting.executeScript to
   remove restricted iframes from the DOM, then retry attachment. Requires
   new 'scripting' permission.

2. Runtime filtering (onDebuggerEvent): when Target.attachedToTarget arrives
   for a restricted child target (injected after debugger attached), filter it
   out immediately — don't track, don't forward to relay, detach the child
   target. Defense-in-depth against runtime re-injection by other extensions'
   content scripts.

Test: loads a fixture extension alongside playwriter, creates a page with a
chrome-extension:// iframe, enables the debugger, and verifies it stays
connected and can execute JS.

Closes #18
2026-03-29 23:26:26 +02:00
Tommy D. Rossi 65922f1bbf chore: upgrade vite to v8.0.3 across all packages
Bumps vite from various versions (5.x, 6.x, 7.x) to v8.0.3 uniformly:

- root package.json: ^7.2.2 → ^8.0.3
- extension/package.json: ^5.4.21 → ^8.0.3
- website/package.json: ^7.3.0 → ^8.0.3
- playwright submodule package.json: ^6.4.1 → ^8.0.3
2026-03-28 16:39:04 +01:00
Tommy D. Rossi 3dda03ea7e feat(playwriter): launch a managed browser with bundled extension
Bundle the extension into the CLI so fresh AVPS and local source checkouts can start a Playwriter-ready Chrome for Testing or Chromium without separate installation steps. Also disable the packaged welcome tab, reuse Playwright\s managed Chromium fallback, surface searched browser paths on discovery failures, and document that managed browser launches already enable the tab-capture flags needed for recording on normal web pages.
2026-03-25 21:31:47 +01:00
Tommy D. Rossi 3c19066532 release: playwriter@0.0.89 2026-03-12 22:18:02 +01:00
Tommy D. Rossi df7476af06 fix(extension): write prism assets to selected output dir
Respect PLAYWRITER_EXTENSION_DIST in the Prism download step so release builds include welcome page Prism scripts in dist-release/src instead of polluting dist/src.
2026-03-12 15:36:37 +01:00