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. - - - - - Polymarket-style prediction line. "Will Bitcoin stay above - $67,500?" - - -
- -
- - - 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. + + + + + Native tab capture. 30–60fps. Survives navigation. + + +
    + +
    + + + 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. +
  • +