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
`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
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 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
- 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
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
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
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
`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
Review fixes (from a post-merge agent review of the recorder commits):
- recording id collision: files are now created with flag 'wx' so a
concurrent relay process sharing ~/.playwriter/recordings can no longer
truncate an active recording; the manager retries with the next id on
EEXIST (up to 20 attempts)
- test isolation: recordings dir is resolved at call time via
getRecordingsDir() honoring PLAYWRITER_RECORDINGS_DIR; recorder.test.ts
now records into a temp dir and cleans it up, instead of polluting the
user's real recordings dir and bumping their ids
- capture chain hangs: each queued capture races a 15s timeout so one
hung page.evaluate can't stall later captures or block stop() forever;
page polling skips ticks while captures are backed up
- cached rejected import: the recording manager import promise resets on
failure instead of caching the rejection forever
- `recorder events` rejects non-numeric event ids with a clear message
instead of silently printing nothing
- changeset cleanup: correct command name (`recorder`, not `record`),
bullet list no longer interrupted by stray paragraphs
- stale `playwriter record` naming fixed in comments; documented the
known coalescing tradeoff (long typing pause splits one fill into two
action events)
- website CLI reference now lists the recorder commands
Also includes: `recorder events` (CLI + /recorder/events/latest route)
defaults to the most recent recording via latestRecordingId() when no
recording id is passed.
Session: ses_ff500aa18ffeiSe873v4YFxyfA
New "Skill Recorder" navigation tab with a landing page explaining the
record-then-generate-skill workflow: value proposition, quick start with
thin timeline + per-id drill-down examples, event reference table, and
ten use-case sections (typed API client reverse engineering, CRM data
entry, message-driven bookings, invoice harvesting, internal admin ops,
repeat submissions, legacy apps, QA-pass E2E tests, bug repro reports,
fixing broken skills) sourced from a research pass on real browser
automation pain points.
The Changelog tab is removed from the top navigation and replaced by a
"Changelog" link in the footer platform links pointing to
/changelog/intro.
Session: ses_ff500aa18ffeiSe873v4YFxyfA
Send the configured bearer token during the MCP remote relay health check so token-protected Docker and devcontainer connections can start successfully.
Add an end-to-end MCP startup regression test against a token-protected relay. Update CLI help and remote access guides to require the host and token configuration with a 0.0.0.0 relay binding.
Add a playwriter patch changeset that references issue #108.
Session: ses_00e1a04d8ffew22iE5MbYEiZXb
Explains why JavaScript dialogs (alert, confirm, prompt) disappear
instantly when using Playwriter. This is core Playwright behavior:
when no page.on("dialog") handler is registered, Playwright auto-
dismisses the dialog to prevent the page from freezing (an open JS
dialog blocks all CDP/JS execution).
Documents the default behavior for each dialog type:
- alert: dismissed (OK)
- confirm: dismissed (Cancel)
- prompt: dismissed (Cancel)
- beforeunload: accepted (navigation proceeds)
Shows how agents can handle dialogs by registering a handler before
the triggering action.
Session: ses_019076847ffeYQGXqk77ZLixZ4
- Add IvarText serif font file and @font-face declaration in globals.css
- Remove playwriter logo from all three hero sections (landing, changelog, cloud)
- Increase hero heading font sizes from 28/36/44px to 40/56/72px
- Add leading-tight for more line height
- Add font-ready fade-in to prevent FOUT (fallback serif flash)
- Match akarso hero layout: more vertical padding, consistent button styling
- Simplify changelog hero by removing unnecessary wrapper divs
Session: ses_07a1be50affeNIY6WWST7gW5bD
- Chrome activeTab requires a manual extension icon click per tab
(managed browser via `playwriter browser start` auto-accepts)
- one capture per tab: stream or record, never both
- concurrent multi-tab streaming is architecturally supported but
untested; recommend the tee muxer (repeat --rtmp) for streaming the
same tab to several platforms with a single capture and encode
- note the ~2-4s latency expectation
Session: ses_0a8205a5cffeId4FJXly9O4aOr
New `playwriter stream start|stop|status` CLI commands and `stream.*`
executor API for streaming a browser tab live to RTMP endpoints
(X Live, Twitch, YouTube). Built for 24/7 auto-generated content:
render a page, stream it forever.
Architecture:
extension MediaRecorder (fMP4 chunks, tabCapture)
│ same WS recordingData messages as recording (no protocol
│ or extension changes, old extensions keep working)
▼
relay: StreamRelay pipes chunks to ffmpeg stdin
│ re-encode H.264 CBR + AAC, forced GOP (MediaRecorder
│ output is VFR with uncontrolled keyframes)
▼
-f flv rtmp://... (single) or tee muxer with per-output FIFO
threads + onfail=ignore (multi-destination, one encode)
ffmpeg is a child of the relay process, so streams survive CLI exit
and run indefinitely (no 15-min recording cap).
Details:
- defaults match X Live recommended encoder settings: 1920x1080,
9000 kbps, 30 fps, 3s keyframes, AAC 128k 44100Hz stereo; use
--video-bitrate 6000 --keyframe-interval 2 for Twitch
- binary/recordingData WS routing offers chunks to StreamRelay first,
falls back to RecordingRelay (tabId sets are disjoint because the
extension refuses a second capture of the same tab)
- /stream/start|stop|status endpoints under privileged auth
- stream keys never leak: destinations redacted in logs/status and
raw URLs scrubbed from ffmpeg stderr before storage
- backpressure: 32MB stdin cap kills a stalled stream instead of
buffering unbounded
- stop flow hardened after oracle review: shared stop promise for
concurrent stops, extension stop failures and forced/nonzero
ffmpeg exits report failure instead of false success
- --no-audio injects silent anullsrc track (X Live requires audio)
- tests: ffmpeg arg snapshots + real fMP4 pipe integration tests
(generate with ffmpeg testsrc, stream to local flv, ffprobe verify)
- docs: website/docs/streaming.mdx, short skill.md reference, README
Session: ses_0a8205a5cffeId4FJXly9O4aOr
New docs page covering four profiling workflows through Playwriter:
1. CPU profiling via CDP Profiler.* commands
Start/stop V8 profiler, save .cpuprofile, analyze with profano CLI.
Covers sampling interval, session cwd gotchas, extension overhead.
2. Web Vitals collection (TTFB, FCP, LCP, CLS)
PerformanceObserver-based collection with page reload for fresh metrics.
3. Long tasks and interaction latency
Detect >50ms tasks and slow event handlers blocking interactivity.
4. React component render profiling (React 19.2+)
Capture React Performance Track measures, convert to .cpuprofile format,
analyze with profano --sort self. Shows component names, mount times,
scheduler events (Cascading Update, Update Blocked).
The conversion creates a flat profile where each component/event is a
node under root with samples proportional to measure duration.
Session: ses_10139b5c5ffeU14dA4oc9TeCUi
Add dedicated pages for each major Playwriter feature, breaking out
content that was previously only covered on the homepage. Each page
has SEO-optimized titles and descriptions with searchable keywords.
New pages in Features group:
- Accessibility Snapshots, Search, and Page Diffing
snapshot(), getCleanHTML(), getPageMarkdown(), search, showDiffSinceLastCall
- Screenshot with Element Labels for AI Agents
screenshotWithAccessibilityLabels, color coding, refToLocator
- Multi-Agent Browser Sessions with Isolated State
state persistence, shared pages, popup handling, context variables
- JavaScript Debugger, Live Code Editor, and CSS Inspector
createDebugger, createEditor, breakpoints, getStylesForLocator, React inspection
- Network Interception, API Capture, and Cookie Management
response capture, API replay, authenticated fetches, cookie management via CDP
- Record Browser Automation as MP4 Video
recording.start/stop, ghost cursor, createDemoVideo, tabCapture
- Headless Chrome, Direct CDP, and CI Setup
browser install/start, --browser headless, --direct, CI/Docker
- Security Model and Sandboxed Execution
local-only architecture, origin validation, sandboxed fs, cookie safety
New pages in Getting Started:
- Quick Start: Automate Chrome in 2 Minutes
core workflow, 4-step setup, MCP config, links to feature pages
New pages in Reference:
- Configuration, Environment Variables, and MCP Setup Options
all env vars, CLI flags, MCP config examples for Claude/Cursor/OpenCode
Navigation restructure:
- Added Features group between Getting Started and Guides
- Moved Use Cases from its own group into Guides
- Added Configuration to Reference group
- Replaced Changelog anchor with proper Holocron changelog tab
- Removed Use Cases group (merged into Guides)
Session: ses_10139b5c5ffeU14dA4oc9TeCUi
Add a ChangelogHeroSection component to hero-section.tsx and a
changelog/intro.mdx page that links to GitHub releases.
This replaces the old Changelog anchor link in the global nav
with a proper Holocron changelog tab that pulls releases from
GitHub automatically.
Session: ses_10139b5c5ffeU14dA4oc9TeCUi
"Control Your Real Chrome Browser with Playwright"
→ "Let Agents Control Your Real Chrome Browser"
Shifts the framing from Playwright (implementation detail)
to the core value prop: AI agents controlling your browser.
Session: ses_1015d49fcffebpkTVLdZmlqk9s
The Cloud Browsers docs page was hidden behind `"hidden": true`.
Removing the flag makes it visible in the navigation sidebar
so users can discover cloud browser features.
Session: ses_1015d49fcffebpkTVLdZmlqk9s
- title: "Control Your Real Chrome Browser with Playwright" (shows as
"Control Your Real Chrome Browser with Playwright — Playwriter" in
browser tab and Google results)
- sidebarTitle: "Home" (clean sidebar label instead of repeating the title)
Session: ses_1019526e6ffenre3Zso9o0s2dB
Removes the separate D1 write for the rate limit timestamp by
piggybacking it onto the placeholder→real-ID update that already
happens. Both statements run in one db.batch() call, so connect
has the same number of D1 round trips as before the rate limit
was added.
Session: ses_107161964ffefCyMno6mi5LtMr
The lastCloudCreateAt update and bu.createBrowser() were running in
Promise.all. If createBrowser succeeded but the D1 update failed,
Promise.all rejected and vmResult was lost, orphaning a paid VM.
Now the D1 update runs first (sequentially), then createBrowser.
If the DB write fails, no VM is created. If VM creation fails,
the catch block has no vm.id to worry about and just cleans up
the placeholder row.
Session: ses_107161964ffefCyMno6mi5LtMr
- use row.slotIndex from DB instead of sequential counter in /api/cloud/status
so slots keep their real index after gaps (e.g. stopping slot 2 of 3)
- dashboard renders max(totalSlots, maxSessionIndex) rows so sessions
above subscription quantity are still visible after downgrades
Session: ses_10bc2a749ffeInm3HXNY1r5v6D
Empty title skips the h1 render; browser tab falls back to "Playwriter".
No new props needed: AuthPage conditionally renders h1 only when title is truthy.
Session: ses_105c733a9ffekXw867fixfqgKX
**CLI: `playwriter cloud live [key]`**
- prints and opens playwriter.dev/live?wss= URL for an active cloud session
- accepts cloud-N key, cloudSessionId, or browserUseSessionId
- auto-selects when only one session is active
**Dashboard: browser slots table**
- new CloudSessionsPanel shows all subscription slots (not just active)
- active sessions: green dot, duration, expiry countdown, "Watch live" link
- empty slots: gray "Available" status with dashes
- header shows active/total counter (e.g. "1/4 active")
- auto-refreshes every 10s, clears transient errors on success
**Replace Browser Use live URLs**
- all liveUrl fields now point to our /live?wss= route instead of live.browser-use.com
- uses exact CDP endpoint URL (not reconstructed from UUID) to handle BU sharding
- cloud-api.ts, cli.ts, cloud-client.ts all updated
- new buildLiveUrl() helper in cloud-client.ts (CLI) and cloud-api.ts (website)
Session: ses_10bc2a749ffeInm3HXNY1r5v6D
The /login route passed visualTitle={<PlaywriterLogo>} to AuthPage,
but AuthPage already renders the logo at the top. This caused the logo
to appear twice on the login screen.
- Remove the visualTitle prop from the /login route, use "Sign in" text title
- Remove unused visualTitle and headTitle props from AuthPage component
- No other pages had this issue (device flow and dashboard use text titles)
Session: ses_105c733a9ffekXw867fixfqgKX
Instead of combining browserCost + proxyCost into a single cloudSpend
counter, track them separately with independent budgets. This lets
operators set different limits for proxy bandwidth vs VM runtime.
Schema (org table):
- Restore proxySpendCents / proxyBudgetCents (proxy bandwidth)
- Add browserSpendCents / browserBudgetCents (VM runtime)
- Rename proxySpendPeriodStart → spendPeriodStart (shared period marker)
- Keep lastCloudCreateAt for rate limiting
Schema (cloudSession table):
- Restore lastProxyCostCents (proxy cost baseline)
- Add lastBrowserCostCents (browser cost baseline)
cloud-api.ts connect handler:
- Check both budgets independently before allowing new sessions
- Reset both counters on billing period rollover
scheduled.ts cron:
- Compute proxy and browser deltas separately per session
- Atomic increment both spend counters independently
- Kill sessions if either budget is exceeded
- Conditional baseline update uses WHERE on both previous values
Migration adds new columns alongside existing ones (no renames needed
since proxy columns were never dropped from D1).
Session: ses_107161964ffefCyMno6mi5LtMr
**Problem:** Several cost vulnerabilities in cloud browser management:
1. Budget only tracked proxyCost, not browserCost ($0.02/hr VM runtime).
Users with proxyRegion=null bypassed the budget entirely.
2. No rate limiting on /connect: rapid connect/disconnect loops waste
VM costs (Browser Use charges minimum 1 minute per VM).
3. Timeout allowed up to 240 minutes (4 hours) with no server cap.
4. customProxy accepted from users without validation.
5. Sessions with cancelled subscriptions kept running until timeout.
6. Concurrent flood: VM created before slot claim meant N concurrent
requests could create N paid VMs when only 1 slot was available.
**Fixes:**
Schema changes (db/src/schema.ts + migration):
- Rename proxy-only columns to cloud-wide:
proxySpendCents → cloudSpendCents
proxyBudgetCents → cloudBudgetCents
proxySpendPeriodStart → cloudSpendPeriodStart
lastProxyCostCents → lastTotalCostCents
- Add org.lastCloudCreateAt for per-org creation rate limiting
- Migration copies existing data to new columns
cloud-api.ts:
- Restore slot-claim-before-VM-creation pattern to prevent
concurrent flood (slot claim is cheap D1 write; VM creation
is expensive BU API call that costs money)
- Track browserCost + proxyCost as total cost in budget checks
- Add 10-second per-org creation cooldown (MIN_CREATE_INTERVAL_MS)
to prevent rapid connect/disconnect loops
- Cap timeout at 60 minutes (MAX_TIMEOUT_MINUTES) instead of 240
- Remove customProxy from connect schema (silently ignored if sent)
- Parallelize rate limit timestamp update with VM creation
scheduled.ts:
- Track total cost (browserCost + proxyCost) instead of proxy only
- Kill sessions for orgs without active subscription (cancelled/expired)
- Rename all proxy* references to cloud* for consistency
Session: ses_107161964ffefCyMno6mi5LtMr
**Before:** connect did 2 D1 writes per session: insert a placeholder row
with a pending-xxx ID, create the BU VM, then update the placeholder
with the real session ID. Required stale placeholder cleanup, pending
row filtering in status/cron, and complex rollback paths.
**After:** connect creates the BU VM first, then does 1 D1 insert with
the real session ID. If the insert fails (unique constraint = slot taken,
or any DB error), stops the VM. Simpler code, fewer D1 writes.
Changes:
- Remove PENDING_PREFIX, PENDING_STALE_MS, isPendingRow(),
checkSlotOccupied(), claimCloudSessionSlot()
- Add insertCloudSession() that inserts with real BU session ID
- Wrap post-createBrowser code in try/catch to stop VM on any failure
(prevents VM leaks on DB errors)
- Status endpoint no longer filters out pending rows
- Cron no longer has WHERE NOT LIKE pending-% filter
- resolveActiveSession and cron error handler now treat HTTP 400
(malformed ID) as dead alongside 404, cleaning up any legacy
pending-* rows on first check after deploy
Trade-off: in a rare race (two concurrent connects for the last slot),
the loser creates a VM then immediately stops it (fractions of a cent).
Session: ses_107161964ffefCyMno6mi5LtMr
better-auth-drizzle-adapter@1.0.5 now implements consumeOne and
incrementOne, so the device authorization plugin endpoints work
natively. Removed:
- Custom handleDeviceToken function and middleware intercept
- Direct DB queries for device verify, approve, deny
- orm/schema imports that were only used for device flow workarounds
The device page now uses auth.api.deviceVerify with request.headers,
and approve/deny actions use auth.api.deviceApprove/deviceDeny.
The built-in /api/auth/device/token endpoint handles token exchange.
Session: ses_10aba81e2ffeUWR93aNz4avLK2
**CLI (`playwriter/src/cli.ts`):**
- Use `createAuthClient` with `deviceAuthorizationClient` plugin instead of
hardcoded endpoint URLs. Hardcoded URLs broke when better-auth changed paths
between versions. Now calls `client.device.code()` and `client.device.token()`.
- Added `better-auth` as a dependency of the playwriter package.
**Website server (`website/src/server.tsx`):**
- Added custom `/api/auth/device/token` handler in the auth middleware, intercepted
before better-auth processes it. The drizzle adapter does not implement `consumeOne`
which the device authorization plugin requires for the token exchange. Our handler
does: find approved device code → delete it → create session → return access_token.
- Device verify page queries the deviceCode table directly via Drizzle instead of
`auth.api.deviceVerify` (which depends on `incrementOne`, also missing from adapter).
- Session check moved before device code lookup so we redirect to login first.
**Website actions (`website/src/actions.tsx`):**
- Approve/deny actions update the DB directly via Drizzle instead of
`auth.api.deviceApprove`/`deviceDeny` (which depend on adapter methods not
available in better-auth-drizzle-adapter: `consumeOne`, `incrementOne`).
**Other:**
- Hidden Cloud Browsers tab in docs.json.
- Cloud browsers docs copy improvements (unrelated).
- Changeset for the playwriter package.
Session: ses_10aba81e2ffeUWR93aNz4avLK2
The PlaywriterLogo component is now rendered above the title in the
shared AuthPage layout, wrapped in a spiceflow Link to /dashboard.
This affects all message screens: CLI Approved, CLI Denied, Invalid
Device Code, Device Code Claimed, and login pages.
Session: ses_10ab972f7ffehKgUpczAXRiPCm
- Rename "concurrent sessions" to "browsers" in billing panel subtitle
- Show subscription card before Quick Start code snippets
- Remove "Full documentation" link from Quick Start panel
- Remove descriptive paragraphs from both CLI and MCP tabs
- Remove subtitle paragraph under Quick Start heading
- Remove "Organization: ..." line from dashboard header
Session: ses_10ac8dddaffe3Oth3P4r7dJukF
Use the same orange brand color across Holocron docs and custom website routes.
- Set Holocron light and dark primary colors to #FF9500 so docs do not derive a lighter dark-mode accent.
- Override the dashboard CSS primary token and ring token to the same orange.
- Leave unrelated docs navigation changes unstaged.
Session: ses_10af1cdd3ffeTJr7Qg6c3hHUA7
Add a /live page in the website for viewing and controlling a remote CDP browser session from the browser.
This includes:
- a reusable CDP screencast component that streams Page.screencastFrame frames to a canvas
- mouse, wheel, keyboard, navigation, reload, and viewport forwarding over CDP
- a small /live entry page that accepts either a Browser Use session id or a full WebSocket URL
- stable scrollbar gutter styling to avoid layout shifts around the full-screen viewer
Session: ses_10bc2a749ffeInm3HXNY1r5v6D
New QuickStartPanel component at top of dashboard showing how to use
cloud browsers. Two tabs: CLI and MCP.
CLI tab shows two usage cases in a single code block split by a comment:
- Interactive login flow: cloud login, session new, execute
- API key flow: export PLAYWRITER_API_KEY, session new --proxy, execute
MCP tab shows the MCP client JSON config with API key env var.
Uses holocron CodeBlock for Prism syntax highlighting with copy buttons.
Matches existing dashboard card styling (rounded-xl, border-border).
Session: ses_10bb4920bffeAdYhx4RlRNuzEp
Adds placement.mode = "smart" so the worker runs closer to the D1 database
rather than always at the nearest edge, reducing latency for DB-heavy requests.
- outline button variant: replace generic border+shadow with border-foreground/15 and hover border-foreground/25, explicit bg-white with dark:bg-background
- ghost variant: remove bg-accent hover (just text color change now)
- hero GitHub link: match the new outline border style
- sign out button: same border treatment, switch from double to single quotes for consistency