diff --git a/playwriter/src/skill.md b/playwriter/src/skill.md
index 042c652..dad01cf 100644
--- a/playwriter/src/skill.md
+++ b/playwriter/src/skill.md
@@ -162,7 +162,7 @@ You can collaborate with the user - they can help with captchas, difficult eleme
## rules
-- **Create your own page**: see "working with pages" — always create and store your own page in `state`, never use the default `page` for automation. Examples below use bare `page` for brevity, but in real automation always use `state.myPage`
+- **Create your own page**: see "working with pages" — create and store your own page in `state`, don't use the default `page` for automation unless the user asks you to use a specific tab. Reusing an existing `about:blank` tab is fine. Examples below use bare `page` for brevity, but in real automation always use `state.myPage`
- **Multiple calls**: use multiple execute calls for complex logic - helps understand intermediate state and isolate which action failed
- **Never close**: never call `browser.close()` or `context.close()`. Only close pages you created or if user asks
- **No bringToFront**: never call unless user asks - it's disruptive and unnecessary, you can interact with background pages
@@ -599,7 +599,7 @@ console.log('New tab URL:', newTab.url());
```js
const [download] = await Promise.all([page.waitForEvent('download'), page.click('button.download')]);
-await download.saveAs(`./${download.suggestedFilename()}`);
+await download.saveAs(`/tmp/${download.suggestedFilename()}`);
```
**iFrames** - two approaches depending on what you need:
diff --git a/public/og-errore-vs-effect.png b/public/og-errore-vs-effect.png
new file mode 100644
index 0000000..8daafad
Binary files /dev/null and b/public/og-errore-vs-effect.png differ
diff --git a/website/src/routes/liveline.tsx b/website/src/routes/liveline.tsx
index 5f00790..d4b167c 100644
--- a/website/src/routes/liveline.tsx
+++ b/website/src/routes/liveline.tsx
@@ -1,7 +1,7 @@
/*
- * Recreation of benji.org/liveline page design.
+ * Playwriter editorial page — benji.org/liveline-inspired design.
* Uses the same editorial layout, typography, and styling system.
- * Chart canvas components are replaced with dark placeholder boxes.
+ * ChartPlaceholder boxes reserved for future showcase videos.
*
* Prism.js is used for syntax highlighting with a custom light theme
* that matches the original benji.org subtle code block style.
@@ -79,18 +79,16 @@ function useActiveTocId() {
const tocItems = [
{ label: "Getting started", href: "#getting-started" },
- { label: "Momentum", href: "#momentum" },
- { label: "Value overlay", href: "#value-overlay" },
- { label: "Time windows", href: "#time-windows" },
- { label: "Reference line", href: "#reference-line" },
- { label: "Orderbook", href: "#orderbook" },
- { label: "Theming", href: "#theming" },
- { label: "More features", href: "#more-features" },
{ label: "How it works", href: "#how-it-works" },
- { label: "Props", href: "#props" },
- { label: "Stress testing", href: "#stress-testing" },
- { label: "Just a line", href: "#just-a-line" },
- { label: "Acknowledgements", href: "#acknowledgements" },
+ { label: "Snapshots", href: "#snapshots" },
+ { label: "Visual labels", href: "#visual-labels" },
+ { label: "Sessions", href: "#sessions" },
+ { label: "Debugger & editor", href: "#debugger-and-editor" },
+ { label: "Network interception", href: "#network-interception" },
+ { label: "Screen recording", href: "#screen-recording" },
+ { label: "Comparison", href: "#comparison" },
+ { label: "Remote access", href: "#remote-access" },
+ { label: "Security", href: "#security" },
];
function TableOfContents() {
@@ -615,7 +613,7 @@ export default function LivelinePage() {
padding: "0 0 16px",
}}
>
- Liveline
+ Playwriter
{/* Intro */}
- Liveline is a real-time animated line chart component for React. One{" "}
- {", no dependencies beyond React
- 18, smooth interpolation at 60fps.
+ Playwriter lets you control your Chrome browser with the full
+ Playwright API. A Chrome extension, a local relay, and a CLI. No new
+ browser windows, no Chrome flags, no context bloat.
-
+
- Degen mode (chart shake and particles) with momentum arrows.
+ Your existing Chrome session. Extensions, logins, cookies — all there.
- I built this because every charting library I tried was either too
- heavy for a simple live feed, or too rigid to feel alive. Liveline
- does one thing: draw a line that moves smoothly as new data arrives.
- Everything else is opt-in.
+ Every browser automation MCP I tried either spawns a new Chrome
+ instance or forces you into a limited set of predefined tools. Playwriter
+ does neither. It connects to the browser you already have open,
+ exposes the full Playwright API through a single{" "}
+ execute tool, and gets out of the way.
+ One tool. Any Playwright code. No wrappers.
- npm install liveline
-
- The component fills its parent container. Set a height on the
- wrapper.
+ Three steps. Extension, icon click, then you're automating.
-
- {`import { Liveline } from 'liveline'
-
-function Chart({ data, value }) {
- return (
-
-
-
- )
-}`}
-
+ {`# 1. Install the Chrome extension
+# https://chromewebstore.google.com/detail/playwriter-mcp/jfeammnjpkecdekppnclgkkffahnhfhe
+
+# 2. Click the extension icon on a tab — it turns green
+
+# 3. Install CLI and run your first command
+npm i -g playwriter
+playwriter -s 1 -e "await page.goto('https://example.com')"`}
- data is an array of{" "}
- {"{ time, value }"} points.{" "}
- value is the latest number.
+ The extension connects your browser to a local WebSocket relay on{" "}
+ localhost:19988. The CLI sends Playwright
+ code through the relay. No remote servers, no accounts, nothing
+ leaves your machine.
-
-
Two props. That's it.
+ {`playwriter session new # new sandbox, outputs id (e.g. 1)
+playwriter -s 1 -e "await page.goto('https://example.com')"
+playwriter -s 1 -e "console.log(await snapshot({ page }))"
+playwriter -s 1 -e "await page.locator('aria-ref=e5').click()"`}
-
- Feed it data however you like. WebSocket, polling, random walk.
- Liveline interpolates between updates so even infrequent data looks
- smooth. It works for anything with a value that changes over time.
-
-
-
+
- Resting heart rate. Custom formatter, exaggerated Y-axis.
+ Extension icon green = connected. Gray = not attached to this tab.
-
-
-
- The momentum prop adds directional arrows
- and a glow to the live dot. Green for up, red for down, grey for
- flat. Pass true to auto-detect direction,
- or force it with{" "}
- "up",{" "}
- "down", or{" "}
- "flat".
-
-
-
-
- Arrows fade out fully before the new direction fades in.
-
-
-
-
-
-
-
- showValue renders the current value as a
- large number over the chart. It updates at 60fps through direct DOM
- manipulation, not React re-renders. Pair it with{" "}
- valueMomentumColor to tint the number based
- on direction.
-
-
-
-
60fps value overlay with momentum colouring.
-
-
-
-
-
-
- Pass a windows array to render time horizon
- buttons. Each entry has a label and{" "}
- secs value. Three styles are available via{" "}
- windowStyle:{" "}
- "default",{" "}
- "rounded", and{" "}
- "text".
-
-
-
- {``}
-
-
-
-
- CPU usage with occasional spikes. Rounded time windows.
-
-
-
-
-
-
-
- referenceLine draws a horizontal line at a
- fixed value. Pass an object with value and
- an optional label.
-
-
-
-
-
-
-
-
-
-
- Pass an orderbook prop with{" "}
- bids and asks{" "}
- arrays to render streaming order labels behind the line. Each entry
- is a [price, size] tuple. Labels spawn at
- the bottom, drift upward, and fade out. Green for bids, red for
- asks. Bigger orders appear brighter.
-
-
-
- The stream speed reacts to price momentum and orderbook churn (how
- much the bid/ask totals are changing). Calm markets drift slowly,
- volatile ones rush.
-
-
-
-
- Kalshi-style orderbook stream. Bid and ask sizes float upward behind
- the price line.
-
-
-
-
-
-
-
- Pass any CSS colour string to color and
- Liveline derives the full palette. Line, fill gradient, glow, badge,
- grid labels. It converts the input to HSL and generates every variant
- from there.
-
-
-
-
Dark theme. Same component, different colour.
-
-
-
-
-
-
- Everything is off by default or has sensible defaults. A few more
- things you can turn on:
-
-
-
-
- exaggerate tightens the Y-axis range so
- small movements fill the full chart height. Useful for values that
- move in tiny increments, like the heart rate demo above.
-
-
- scrub shows a crosshair with time and
- value tooltips on hover. On by default.
-
-
- degen enables burst particles and chart
- shake on momentum swings. For when subtlety is not the goal.
-
-
- badgeVariant="minimal" renders a
- quieter white pill instead of the accent-colored default. Or{" "}
- {"badge={false}"} to remove it entirely.
-
-
-
-
-
- One {", one{" "}
- requestAnimationFrame loop. When a new
- value arrives, nothing jumps. The chart lerps toward the new state at
- 8% per frame (lerpSpeed). The Y-axis range,
- the badge, the grid labels all use the same lerp. The range snaps
- outward instantly when data exceeds it, so the line is never clipped.
- That's why it feels like one thing breathing rather than a bunch
- of parts updating independently.
+ The extension uses chrome.debugger to
+ attach to tabs where you clicked the icon. It opens a WebSocket
+ connection to a local relay server. The CLI (or MCP, or your own
+ Playwright script) connects to the same relay. CDP commands flow
+ through; the extension forwards them to Chrome and sends responses
+ back.
+
+
+ {`+---------------------+ +-------------------+ +-----------------+
+| BROWSER | | LOCALHOST | | CLIENT |
+| | | | | |
+| +---------------+ | | WebSocket Server | | +-----------+ |
+| | Extension |<---------> :19988 | | | CLI / MCP | |
+| +-------+-------+ | WS | | | +-----------+ |
+| | | | /extension | | | |
+| chrome.debugger | | | | | v |
+| v | | v | | +-----------+ |
+| +---------------+ | | /cdp/:id <------------> | | execute | |
+| | Tab 1 (green) | | +--------------------+ WS | +-----------+ |
+| | Tab 2 (green) | | | | |
+| | Tab 3 (gray) | | Tab 3 not controlled | Playwright API |
++---------------------+ (extension not clicked) +-----------------+`}
+
+
+ No Chrome restart required. No --remote-debugging-port{" "}
+ flags. The extension handles the CDP attachment transparently, and
+ the relay multiplexes sessions so multiple agents or CLI instances
+ can work with the same browser simultaneously.
-
+
+
+
+ The core feedback loop is observe → act → observe.
+ Accessibility snapshots are the primary way to read page state. They return
+ the full interactive element tree as text, with Playwright locators attached
+ to every element.
+
+
+ {`playwriter -s 1 -e "await snapshot({ page })"
+
+# Output:
+# - banner:
+# - link "Home" [id="nav-home"]
+# - navigation:
+# - link "Docs" [data-testid="docs-link"]
+# - link "Blog" role=link[name="Blog"]`}
+
+
+ Each line ends with a locator you can pass directly to{" "}
+ page.locator(). Subsequent calls return a
+ diff, so you only see what changed. Use{" "}
+ search to filter large pages.
+
+
+ {`# Search for specific elements
+playwriter -s 1 -e "await snapshot({ page, search: /button|submit/i })"
+
+# Always print URL first, then snapshot — pages can redirect
+playwriter -s 1 -e "console.log('URL:', page.url()); await snapshot({ page }).then(console.log)"`}
+
+
+ Snapshots are text. They cost a fraction of what screenshots cost in
+ tokens. Use them as your primary debugging tool. Only reach for
+ screenshots when spatial layout matters — grids, dashboards, maps.
+
+
+
+
+ Accessibility tree as text. 5–20KB vs 100KB+ for screenshots.
+
+
+
+
+
+
+
+ For pages where spatial layout matters,{" "}
+ screenshotWithAccessibilityLabels overlays
+ Vimium-style labels on every interactive element. Take a screenshot,
+ read the labels, click by reference.
+
+
+ {`playwriter -s 1 -e "await screenshotWithAccessibilityLabels({ page })"
+# Returns screenshot + accessibility snapshot with aria-ref selectors
+
+playwriter -s 1 -e "await page.locator('aria-ref=e5').click()"`}
+
+
+ Labels are color-coded by element type: yellow for links, orange for
+ buttons, coral for inputs, pink for checkboxes, peach for sliders,
+ salmon for menus, amber for tabs. The ref system is shared with{" "}
+ snapshot(), so you can switch between text
+ and visual modes freely.
+
+
+
+
+ Vimium-style labels. Screenshot + snapshot in one call.
+
+
+
+
+
+
+
+ Each session runs in an isolated sandbox with its own{" "}
+ state object. Variables, pages, listeners
+ persist between calls within a session. Different sessions get
+ different state. Browser tabs are shared.
+
+
+ {`playwriter session new # => 1
+playwriter session new # => 2
+playwriter session list # shows sessions + state keys
+
+# Session 1 stores data
+playwriter -s 1 -e "state.users = await page.$$eval('.user', els => els.map(e => e.textContent))"
+
+# Session 2 can't see it
+playwriter -s 2 -e "console.log(state.users)" # undefined`}
+
+
+ Create your own page to avoid interference from other agents. Reuse
+ an existing about:blank tab or create a
+ fresh one, and store it in state.
+
+
+ {`playwriter -s 1 -e "state.myPage = context.pages().find(p => p.url() === 'about:blank') ?? await context.newPage(); await state.myPage.goto('https://example.com')"
+
+# All subsequent calls use state.myPage
+playwriter -s 1 -e "console.log(await state.myPage.title())"`}
+
+
+
+
+
+
+ Full Chrome DevTools Protocol access. Set breakpoints, step through
+ code, inspect variables at runtime. Live-edit page scripts and CSS
+ without reloading.
+
+
+ {`# Set breakpoints and debug
+playwriter -s 1 -e "state.cdp = await getCDPSession({ page }); state.dbg = createDebugger({ cdp: state.cdp }); await state.dbg.enable()"
+playwriter -s 1 -e "state.scripts = await state.dbg.listScripts({ search: 'app' }); console.log(state.scripts.map(s => s.url))"
+playwriter -s 1 -e "await state.dbg.setBreakpoint({ file: state.scripts[0].url, line: 42 })"
+
+# Live edit page code
+playwriter -s 1 -e "state.editor = createEditor({ cdp: state.cdp }); await state.editor.enable()"
+playwriter -s 1 -e "await state.editor.edit({ url: 'https://example.com/app.js', oldString: 'const DEBUG = false', newString: 'const DEBUG = true' })"`}
+
+
+ Edits are in-memory and persist until the page reloads. Useful for
+ toggling debug flags, patching broken code, or testing quick fixes
+ without touching source files. The editor also supports{" "}
+ grep across all loaded scripts.
+
+
+
+
+ Breakpoints, stepping, variable inspection — from the CLI.
+
+
+
+
+
+
+
+ Intercept requests and responses to reverse-engineer APIs, scrape
+ data, or debug network issues. Store captured data in{" "}
+ state and analyze across calls.
+
+
+ {`# Start intercepting
+playwriter -s 1 -e "state.responses = []; page.on('response', async res => { if (res.url().includes('/api/')) { try { state.responses.push({ url: res.url(), status: res.status(), body: await res.json() }); } catch {} } })"
+
+# Trigger actions, then analyze
+playwriter -s 1 -e "await page.click('button.load-more')"
+playwriter -s 1 -e "console.log('Captured', state.responses.length, 'API calls'); state.responses.forEach(r => console.log(r.status, r.url.slice(0, 80)))"
+
+# Replay an API call directly
+playwriter -s 1 -e "const data = await page.evaluate(async (url) => { const res = await fetch(url); return res.json(); }, state.responses[0].url); console.log(data)"`}
+
+
+ This is faster than scrolling through DOM. Capture the real API
+ calls, inspect their schemas, and replay them with different
+ parameters. Works for pagination, authenticated endpoints, and
+ anything behind JavaScript rendering.
+
+
+
+
+
+
+
+ Record the active tab as video using{" "}
+ chrome.tabCapture. The recording runs in
+ the extension context, so it survives page navigation. Video is saved
+ as MP4.
+
+
+ {`# Start recording
+playwriter -s 1 -e "await startRecording({ page, outputPath: './recording.mp4', frameRate: 30 })"
+
+# Navigate, interact — recording continues
+playwriter -s 1 -e "await page.click('a'); await page.waitForLoadState('domcontentloaded')"
+playwriter -s 1 -e "await page.goBack()"
+
+# Stop and save
+playwriter -s 1 -e "const { path, duration, size } = await stopRecording({ page }); console.log(path, duration + 'ms', size + ' bytes')"`}
+
+
+ Unlike getDisplayMedia, this approach
+ persists across navigations because the extension holds the{" "}
+ MediaRecorder, not the page. You can also
+ check recording status with isRecording or
+ cancel without saving with cancelRecording.
+
+
+
+
+
+
+
+
+
+
+ How Playwriter compares to other browser automation approaches.
+ void", "\u2014"],
- ["windowStyle", "'default' | 'rounded' | 'text'", "\u2014"],
- ]}
- />
-
-
-
-
-
- string", "v.toFixed(2)"],
- ["formatTime", "(t: number) => string", "HH:MM:SS"],
- ["lerpSpeed", "number", "0.08"],
- [
- "padding",
- "Padding",
- "{ top: 12, right: 80, bottom: 28, left: 12 }",
- ],
- ["onHover", "(point: HoverPoint | null) => void", "\u2014"],
- ["cursor", "string", "'crosshair'"],
- ["className", "string", "\u2014"],
- ["style", "CSSProperties", "\u2014"],
+ ["Agent support", "Claude only", "Any MCP client"],
+ ["Windows WSL", "No", "Yes"],
+ ["Context method", "Screenshots (100KB+)", "A11y snapshots (5\u201320KB)"],
+ ["Playwright API", "No", "Full"],
+ ["Debugger", "No", "Yes"],
+ ["Live code editing", "No", "Yes"],
+ ["Network interception", "Limited", "Full"],
+ ["Raw CDP access", "No", "Yes"],
]}
/>
-
+
- A chart that only looks good on calm data isn't much use. These
- demos throw the worst stuff I could think of at it: wild volatility,
- sharp direction changes, isolated spikes on flat lines, and irregular
- data arrival with random gaps.
+ Control Chrome on a remote machine over the internet using tunnels.
+ Run the relay on the host, expose it through a tunnel, and connect
+ from anywhere.
-
+ {`# On the host machine
+npx -y traforo -p 19988 -t my-machine -- npx -y playwriter serve --token
+
+# From anywhere
+export PLAYWRITER_HOST=https://my-machine-tunnel.traforo.dev
+export PLAYWRITER_TOKEN=
+playwriter -s 1 -e "await page.goto('https://example.com')"`}
- Sharp reversals are the classic breaking point. The first chart
- hammers the line with frequent direction changes at 60ms. The second
- holds nearly flat, then fires massive isolated spikes. The third is
- just chaos.
+ Also works on a LAN without tunnels — just set{" "}
+ PLAYWRITER_HOST=192.168.1.10. Use cases
+ include controlling a headless Mac mini, providing remote user
+ support, and multi-machine automation.
-
-
-
- Real-world data doesn't arrive at regular intervals. WebSocket
- connections drop, batch updates land all at once, mobile networks
- stall. This one simulates that: long quiet stretches of 1–3
- seconds between points, then sudden bursts at 40–80ms. The
- tick interval itself is random.
-
-
-
-
-
+
- Liveline can do a lot. Momentum arrows, particles, orderbooks,
- scrubbing, time windows. But at the end of the day, if you just want
- a line that moves when a number changes, it does that just fine too.
+ Playwriter is local by default. The relay runs on{" "}
+ localhost:19988 and only accepts connections
+ from the extension. There's no remote server, no account, no
+ telemetry.
-
-
-
-
-
-
-
- Built with React and HTML Canvas. Inspired by TradingView, Robinhood,
- and Polymarket chart aesthetics. The interpolation approach is
- borrowed from game development — lerp everything, snap nothing.
-
+
+
+ Local only — WebSocket server binds to
+ localhost. Nothing leaves your machine.
+
+
+ Origin validation — only the Playwriter
+ extension origin is accepted. Browsers cannot spoof the Origin
+ header, so malicious websites cannot connect.
+
+
+ Explicit consent — only tabs where you
+ clicked the extension icon are controlled. No background access.
+
+
+ Visible automation — Chrome shows an
+ automation banner on controlled tabs.
+