liveline: add Link component, OL, remove BackButton, dedent code blocks, spacing tweaks

- Add A (link) component with --ll-accent color, bright #58a6ff in dark mode
- Add OL (ordered list) component for numbered steps
- Remove BackButton from EditorialPage
- Use string-dedent for all CodeBlock strings, properly indented in source
- Getting started steps as OL with clickable Chrome Web Store link
- Star on GitHub link in intro paragraph
- Inline heading rule (flex + 1px line) replaces separate Divider
- Heading padding 24px top/bottom, article gap 32px
- Sidebar TOC item spacing increased to 5px
- Centered image captions
This commit is contained in:
Tommy D. Rossi
2026-02-20 12:53:21 +01:00
parent abd28162aa
commit acd63a9235
3 changed files with 353 additions and 277 deletions
+43 -2
View File
@@ -114,7 +114,7 @@ export function TableOfContents({
fontWeight: 475, fontWeight: 475,
lineHeight: "15.6px", lineHeight: "15.6px",
letterSpacing: "-0.04px", letterSpacing: "-0.04px",
padding: "3px 0", padding: "5px 0",
color: defaultColor, color: defaultColor,
fontFamily: "var(--ll-font-primary)", fontFamily: "var(--ll-font-primary)",
transition: "color 0.15s ease", transition: "color 0.15s ease",
@@ -249,6 +249,29 @@ export function Caption({ children }: { children: React.ReactNode }) {
); );
} }
export function A({ href, children }: { href: string; children: React.ReactNode }) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
style={{
color: "var(--ll-accent, #0969da)",
fontWeight: 600,
textDecoration: "none",
}}
onMouseEnter={(e) => {
e.currentTarget.style.textDecoration = "underline";
}}
onMouseLeave={(e) => {
e.currentTarget.style.textDecoration = "none";
}}
>
{children}
</a>
);
}
export function Code({ children }: { children: React.ReactNode }) { export function Code({ children }: { children: React.ReactNode }) {
return ( return (
<code className="ll-inline-code"> <code className="ll-inline-code">
@@ -278,6 +301,25 @@ export function Section({ id, title, children }: { id: string; title: string; ch
); );
} }
export function OL({ children }: { children: React.ReactNode }) {
return (
<ol
className="m-0 pl-5"
style={{
fontFamily: "var(--ll-font-primary)",
fontSize: "14px",
fontWeight: 475,
lineHeight: "20px",
letterSpacing: "-0.09px",
color: "var(--ll-text-primary)",
listStyleType: "decimal",
}}
>
{children}
</ol>
);
}
export function List({ children }: { children: React.ReactNode }) { export function List({ children }: { children: React.ReactNode }) {
return ( return (
<ul <ul
@@ -564,7 +606,6 @@ export function EditorialPage({
textRendering: "optimizeLegibility", textRendering: "optimizeLegibility",
}} }}
> >
<BackButton />
<TableOfContents items={toc} logo={logo} /> <TableOfContents items={toc} logo={logo} />
<div <div
+306 -275
View File
@@ -4,11 +4,13 @@
* Styles from liveline.css and liveline-prism.css. * Styles from liveline.css and liveline-prism.css.
*/ */
import dedent from "string-dedent";
import "website/src/styles/liveline.css"; import "website/src/styles/liveline.css";
import "website/src/styles/liveline-prism.css"; import "website/src/styles/liveline-prism.css";
import { import {
EditorialPage, EditorialPage,
P, P,
A,
Code, Code,
Caption, Caption,
CodeBlock, CodeBlock,
@@ -16,6 +18,7 @@ import {
Section, Section,
PropsTable, PropsTable,
List, List,
OL,
Li, Li,
} from "website/src/components/markdown"; } from "website/src/components/markdown";
@@ -40,7 +43,8 @@ export default function LivelinePage() {
<P> <P>
Playwriter lets you control your Chrome browser with the full Playwriter lets you control your Chrome browser with the full
Playwright API. A Chrome extension, a local relay, and a CLI. No new Playwright API. A Chrome extension, a local relay, and a CLI. No new
browser windows, no Chrome flags, no context bloat. browser windows, no Chrome flags, no context bloat.{" "}
<A href="https://github.com/remorses/playwriter">Star on GitHub</A>.
</P> </P>
<ChartPlaceholder height={300} label="demo" /> <ChartPlaceholder height={300} label="demo" />
@@ -59,375 +63,402 @@ export default function LivelinePage() {
<Section id="getting-started" title="Getting started"> <Section id="getting-started" title="Getting started">
<P> <P>
Three steps. Extension, icon click, then you&apos;re automating. Three steps. Extension, icon click, then you&apos;re automating.
</P> </P>
<CodeBlock lang="bash">{`# 1. Install the Chrome extension <OL>
# https://chromewebstore.google.com/detail/playwriter-mcp/jfeammnjpkecdekppnclgkkffahnhfhe <Li>
Install the{" "}
<A href="https://chromewebstore.google.com/detail/playwriter-mcp/jfeammnjpkecdekppnclgkkffahnhfhe">Chrome extension</A>
</Li>
<Li>Click the extension icon on a tab it turns green</Li>
<Li>Install CLI and run your first command:</Li>
</OL>
# 2. Click the extension icon on a tab — it turns green <CodeBlock lang="bash">{dedent`
npm i -g playwriter
playwriter -s 1 -e "await page.goto('https://example.com')"
`}</CodeBlock>
# 3. Install CLI and run your first command <P>
npm i -g playwriter The extension connects your browser to a local WebSocket relay on{" "}
playwriter -s 1 -e "await page.goto('https://example.com')"`}</CodeBlock> <Code>localhost:19988</Code>. The CLI sends Playwright
code through the relay. No remote servers, no accounts, nothing
leaves your machine.
</P>
<P> <CodeBlock lang="bash">{dedent`
The extension connects your browser to a local WebSocket relay on{" "} playwriter session new # new sandbox, outputs id (e.g. 1)
<Code>localhost:19988</Code>. The CLI sends Playwright playwriter -s 1 -e "await page.goto('https://example.com')"
code through the relay. No remote servers, no accounts, nothing playwriter -s 1 -e "console.log(await snapshot({ page }))"
leaves your machine. playwriter -s 1 -e "await page.locator('aria-ref=e5').click()"
</P> `}</CodeBlock>
<CodeBlock lang="bash">{`playwriter session new # new sandbox, outputs id (e.g. 1) <ChartPlaceholder height={200} label="getting started" />
playwriter -s 1 -e "await page.goto('https://example.com')" <Caption>
playwriter -s 1 -e "console.log(await snapshot({ page }))" Extension icon green = connected. Gray = not attached to this tab.
playwriter -s 1 -e "await page.locator('aria-ref=e5').click()"`}</CodeBlock> </Caption>
<ChartPlaceholder height={200} label="getting started" />
<Caption>
Extension icon green = connected. Gray = not attached to this tab.
</Caption>
</Section> </Section>
<Section id="how-it-works" title="How it works"> <Section id="how-it-works" title="How it works">
<P> <P>
The extension uses <Code>chrome.debugger</Code> to The extension uses <Code>chrome.debugger</Code> to
attach to tabs where you clicked the icon. It opens a WebSocket 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 connection to a local relay server. The CLI (or MCP, or your own
Playwright script) connects to the same relay. CDP commands flow Playwright script) connects to the same relay. CDP commands flow
through; the extension forwards them to Chrome and sends responses through; the extension forwards them to Chrome and sends responses
back. back.
</P> </P>
<CodeBlock lang="bash">{`┌─────────────────────┐ ┌──────────────────────┐ ┌─────────────────┐ <CodeBlock lang="bash">{dedent`
│ BROWSER │ │ LOCALHOST │ │ CLIENT │ ┌─────────────────────┐ ┌──────────────────────┐ ┌─────────────────┐
│ │ │ │ BROWSER │ │ LOCALHOST │ │ CLIENT
│ ┌───────────────┐ │ │ WebSocket Server │ │ ┌───────────┐ │ │ │ │ │
Extension │<───────┬───> :19988 │ │ │ CLI / MCP │ │ ┌───────────────┐ │ │ WebSocket Server │ │ ┌───────────┐
│ └───────┬───────┘ │ WS │ │ └───────────┘ │ │ Extension │<───────┬───> :19988 CLI / MCP │
/extension └───────┬───────┘ │ WS │ │ │ └───────────┘
chrome.debugger │ │ │ v │ /extension │ │
v v ┌────────────┐ chrome.debugger │ v
│ ┌───────────────┐ │ │ /cdp/:id <───────────────>│ │ execute │ │ v │ │ v │ │ ┌────────────┐
│ Tab 1 (green) │ └────────────────────── WS │ └────────────┘ ─────────────── │ │ /cdp/:id <───────────────>│ │ execute │
│ │ Tab 2 (green) │ │ │ │ │ │ Tab 1 (green) │ │ └──────────────────────┘ WS │ └────────────┘
│ │ Tab 3 (gray) Tab 3 not controlled │ Playwright API │ │ Tab 2 (green) │ │ │
└─────────────────────┘ (extension not clicked) └─────────────────┘`}</CodeBlock> │ │ Tab 3 (gray) │ │ Tab 3 not controlled │ Playwright API │
└─────────────────────┘ (extension not clicked) └─────────────────┘
`}</CodeBlock>
<P> <P>
No Chrome restart required. No <Code>--remote-debugging-port</Code>{" "} No Chrome restart required. No <Code>--remote-debugging-port</Code>{" "}
flags. The extension handles the CDP attachment transparently, and flags. The extension handles the CDP attachment transparently, and
the relay multiplexes sessions so multiple agents or CLI instances the relay multiplexes sessions so multiple agents or CLI instances
can work with the same browser simultaneously. can work with the same browser simultaneously.
</P> </P>
</Section> </Section>
<Section id="snapshots" title="Accessibility snapshots"> <Section id="snapshots" title="Accessibility snapshots">
<P> <P>
The core feedback loop is <strong>observe &rarr; act &rarr; observe</strong>. The core feedback loop is <strong>observe &rarr; act &rarr; observe</strong>.
Accessibility snapshots are the primary way to read page state. They return Accessibility snapshots are the primary way to read page state. They return
the full interactive element tree as text, with Playwright locators attached the full interactive element tree as text, with Playwright locators attached
to every element. to every element.
</P> </P>
<CodeBlock lang="bash">{`playwriter -s 1 -e "await snapshot({ page })" <CodeBlock lang="bash">{dedent`
playwriter -s 1 -e "await snapshot({ page })"
# Output: # Output:
# - banner: # - banner:
# - link "Home" [id="nav-home"] # - link "Home" [id="nav-home"]
# - navigation: # - navigation:
# - link "Docs" [data-testid="docs-link"] # - link "Docs" [data-testid="docs-link"]
# - link "Blog" role=link[name="Blog"]`}</CodeBlock> # - link "Blog" role=link[name="Blog"]
`}</CodeBlock>
<P> <P>
Each line ends with a locator you can pass directly to{" "} Each line ends with a locator you can pass directly to{" "}
<Code>page.locator()</Code>. Subsequent calls return a <Code>page.locator()</Code>. Subsequent calls return a
diff, so you only see what changed. Use{" "} diff, so you only see what changed. Use{" "}
<Code>search</Code> to filter large pages. <Code>search</Code> to filter large pages.
</P> </P>
<CodeBlock lang="bash">{`# Search for specific elements <CodeBlock lang="bash">{dedent`
playwriter -s 1 -e "await snapshot({ page, search: /button|submit/i })" # Search for specific elements
playwriter -s 1 -e "await snapshot({ page, search: /button|submit/i })"
# Always print URL first, then snapshot — pages can redirect # Always print URL first, then snapshot — pages can redirect
playwriter -s 1 -e "console.log('URL:', page.url()); await snapshot({ page }).then(console.log)"`}</CodeBlock> playwriter -s 1 -e "console.log('URL:', page.url()); await snapshot({ page }).then(console.log)"
`}</CodeBlock>
<P> <P>
Snapshots are text. They cost a fraction of what screenshots cost in Snapshots are text. They cost a fraction of what screenshots cost in
tokens. Use them as your primary debugging tool. Only reach for tokens. Use them as your primary debugging tool. Only reach for
screenshots when spatial layout matters &mdash; grids, dashboards, maps. screenshots when spatial layout matters &mdash; grids, dashboards, maps.
</P> </P>
<ChartPlaceholder height={200} label="snapshot" /> <ChartPlaceholder height={200} label="snapshot" />
<Caption> <Caption>
Accessibility tree as text. 5&ndash;20KB vs 100KB+ for screenshots. Accessibility tree as text. 5&ndash;20KB vs 100KB+ for screenshots.
</Caption> </Caption>
</Section> </Section>
<Section id="visual-labels" title="Visual labels"> <Section id="visual-labels" title="Visual labels">
<P> <P>
For pages where spatial layout matters,{" "} For pages where spatial layout matters,{" "}
<Code>screenshotWithAccessibilityLabels</Code> overlays <Code>screenshotWithAccessibilityLabels</Code> overlays
Vimium-style labels on every interactive element. Take a screenshot, Vimium-style labels on every interactive element. Take a screenshot,
read the labels, click by reference. read the labels, click by reference.
</P> </P>
<CodeBlock lang="bash">{`playwriter -s 1 -e "await screenshotWithAccessibilityLabels({ page })" <CodeBlock lang="bash">{dedent`
# Returns screenshot + accessibility snapshot with aria-ref selectors 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()"`}</CodeBlock> playwriter -s 1 -e "await page.locator('aria-ref=e5').click()"
`}</CodeBlock>
<P> <P>
Labels are color-coded by element type: yellow for links, orange for Labels are color-coded by element type: yellow for links, orange for
buttons, coral for inputs, pink for checkboxes, peach for sliders, buttons, coral for inputs, pink for checkboxes, peach for sliders,
salmon for menus, amber for tabs. The ref system is shared with{" "} salmon for menus, amber for tabs. The ref system is shared with{" "}
<Code>snapshot()</Code>, so you can switch between text <Code>snapshot()</Code>, so you can switch between text
and visual modes freely. and visual modes freely.
</P> </P>
<ChartPlaceholder height={260} label="visual labels" /> <ChartPlaceholder height={260} label="visual labels" />
<Caption> <Caption>
Vimium-style labels. Screenshot + snapshot in one call. Vimium-style labels. Screenshot + snapshot in one call.
</Caption> </Caption>
</Section> </Section>
<Section id="sessions" title="Sessions"> <Section id="sessions" title="Sessions">
<P> <P>
Each session runs in an isolated sandbox with its own{" "} Each session runs in an isolated sandbox with its own{" "}
<Code>state</Code> object. Variables, pages, listeners <Code>state</Code> object. Variables, pages, listeners
persist between calls within a session. Different sessions get persist between calls within a session. Different sessions get
different state. Browser tabs are shared. different state. Browser tabs are shared.
</P> </P>
<CodeBlock lang="bash">{`playwriter session new # => 1 <CodeBlock lang="bash">{dedent`
playwriter session new # => 2 playwriter session new # => 1
playwriter session list # shows sessions + state keys playwriter session new # => 2
playwriter session list # shows sessions + state keys
# Session 1 stores data # Session 1 stores data
playwriter -s 1 -e "state.users = await page.$$eval('.user', els => els.map(e => e.textContent))" playwriter -s 1 -e "state.users = await page.$$eval('.user', els => els.map(e => e.textContent))"
# Session 2 can't see it # Session 2 can't see it
playwriter -s 2 -e "console.log(state.users)" # undefined`}</CodeBlock> playwriter -s 2 -e "console.log(state.users)" # undefined
`}</CodeBlock>
<P> <P>
Create your own page to avoid interference from other agents. Reuse Create your own page to avoid interference from other agents. Reuse
an existing <Code>about:blank</Code> tab or create a an existing <Code>about:blank</Code> tab or create a
fresh one, and store it in <Code>state</Code>. fresh one, and store it in <Code>state</Code>.
</P> </P>
<CodeBlock lang="bash">{`playwriter -s 1 -e "state.myPage = context.pages().find(p => p.url() === 'about:blank') ?? await context.newPage(); await state.myPage.goto('https://example.com')" <CodeBlock lang="bash">{dedent`
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 # All subsequent calls use state.myPage
playwriter -s 1 -e "console.log(await state.myPage.title())"`}</CodeBlock> playwriter -s 1 -e "console.log(await state.myPage.title())"
`}</CodeBlock>
</Section> </Section>
<Section id="debugger-and-editor" title="Debugger & editor"> <Section id="debugger-and-editor" title="Debugger & editor">
<P> <P>
Full Chrome DevTools Protocol access. Set breakpoints, step through Full Chrome DevTools Protocol access. Set breakpoints, step through
code, inspect variables at runtime. Live-edit page scripts and CSS code, inspect variables at runtime. Live-edit page scripts and CSS
without reloading. without reloading.
</P> </P>
<CodeBlock lang="bash">{`# Set breakpoints and debug <CodeBlock lang="bash">{dedent`
playwriter -s 1 -e "state.cdp = await getCDPSession({ page }); state.dbg = createDebugger({ cdp: state.cdp }); await state.dbg.enable()" # Set breakpoints and debug
playwriter -s 1 -e "state.scripts = await state.dbg.listScripts({ search: 'app' }); console.log(state.scripts.map(s => s.url))" playwriter -s 1 -e "state.cdp = await getCDPSession({ page }); state.dbg = createDebugger({ cdp: state.cdp }); await state.dbg.enable()"
playwriter -s 1 -e "await state.dbg.setBreakpoint({ file: state.scripts[0].url, line: 42 })" 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 # Live edit page code
playwriter -s 1 -e "state.editor = createEditor({ cdp: state.cdp }); await state.editor.enable()" 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' })"`}</CodeBlock> playwriter -s 1 -e "await state.editor.edit({ url: 'https://example.com/app.js', oldString: 'const DEBUG = false', newString: 'const DEBUG = true' })"
`}</CodeBlock>
<P> <P>
Edits are in-memory and persist until the page reloads. Useful for Edits are in-memory and persist until the page reloads. Useful for
toggling debug flags, patching broken code, or testing quick fixes toggling debug flags, patching broken code, or testing quick fixes
without touching source files. The editor also supports{" "} without touching source files. The editor also supports{" "}
<Code>grep</Code> across all loaded scripts. <Code>grep</Code> across all loaded scripts.
</P> </P>
<ChartPlaceholder height={200} label="debugger" /> <ChartPlaceholder height={200} label="debugger" />
<Caption> <Caption>
Breakpoints, stepping, variable inspection &mdash; from the CLI. Breakpoints, stepping, variable inspection &mdash; from the CLI.
</Caption> </Caption>
</Section> </Section>
<Section id="network-interception" title="Network interception"> <Section id="network-interception" title="Network interception">
<P> <P>
Intercept requests and responses to reverse-engineer APIs, scrape Intercept requests and responses to reverse-engineer APIs, scrape
data, or debug network issues. Store captured data in{" "} data, or debug network issues. Store captured data in{" "}
<Code>state</Code> and analyze across calls. <Code>state</Code> and analyze across calls.
</P> </P>
<CodeBlock lang="bash">{`# Start intercepting <CodeBlock lang="bash">{dedent`
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 {} } })" # 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 # Trigger actions, then analyze
playwriter -s 1 -e "await page.click('button.load-more')" 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)))" 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 # 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)"`}</CodeBlock> 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)"
`}</CodeBlock>
<P> <P>
This is faster than scrolling through DOM. Capture the real API This is faster than scrolling through DOM. Capture the real API
calls, inspect their schemas, and replay them with different calls, inspect their schemas, and replay them with different
parameters. Works for pagination, authenticated endpoints, and parameters. Works for pagination, authenticated endpoints, and
anything behind JavaScript rendering. anything behind JavaScript rendering.
</P> </P>
</Section> </Section>
<Section id="screen-recording" title="Screen recording"> <Section id="screen-recording" title="Screen recording">
<P> <P>
Record the active tab as video using{" "} Record the active tab as video using{" "}
<Code>chrome.tabCapture</Code>. The recording runs in <Code>chrome.tabCapture</Code>. The recording runs in
the extension context, so it survives page navigation. Video is saved the extension context, so it survives page navigation. Video is saved
as MP4. as MP4.
</P> </P>
<CodeBlock lang="bash">{`# Start recording <CodeBlock lang="bash">{dedent`
playwriter -s 1 -e "await startRecording({ page, outputPath: './recording.mp4', frameRate: 30 })" # Start recording
playwriter -s 1 -e "await startRecording({ page, outputPath: './recording.mp4', frameRate: 30 })"
# Navigate, interact — recording continues # Navigate, interact — recording continues
playwriter -s 1 -e "await page.click('a'); await page.waitForLoadState('domcontentloaded')" playwriter -s 1 -e "await page.click('a'); await page.waitForLoadState('domcontentloaded')"
playwriter -s 1 -e "await page.goBack()" playwriter -s 1 -e "await page.goBack()"
# Stop and save # Stop and save
playwriter -s 1 -e "const { path, duration, size } = await stopRecording({ page }); console.log(path, duration + 'ms', size + ' bytes')"`}</CodeBlock> playwriter -s 1 -e "const { path, duration, size } = await stopRecording({ page }); console.log(path, duration + 'ms', size + ' bytes')"
`}</CodeBlock>
<P> <P>
Unlike <Code>getDisplayMedia</Code>, this approach Unlike <Code>getDisplayMedia</Code>, this approach
persists across navigations because the extension holds the{" "} persists across navigations because the extension holds the{" "}
<Code>MediaRecorder</Code>, not the page. You can also <Code>MediaRecorder</Code>, not the page. You can also
check recording status with <Code>isRecording</Code> or check recording status with <Code>isRecording</Code> or
cancel without saving with <Code>cancelRecording</Code>. cancel without saving with <Code>cancelRecording</Code>.
</P> </P>
<ChartPlaceholder height={200} label="recording" /> <ChartPlaceholder height={200} label="recording" />
<Caption> <Caption>
Native tab capture. 30&ndash;60fps. Survives navigation. Native tab capture. 30&ndash;60fps. Survives navigation.
</Caption> </Caption>
</Section> </Section>
<Section id="comparison" title="Comparison"> <Section id="comparison" title="Comparison">
<P> <P>
How Playwriter compares to other browser automation approaches. How Playwriter compares to other browser automation approaches.
</P> </P>
<PropsTable <PropsTable
title="vs Playwright MCP" title="vs Playwright MCP"
rows={[ rows={[
["Browser", "Spawns new Chrome", "Uses your Chrome"], ["Browser", "Spawns new Chrome", "Uses your Chrome"],
["Extensions", "None", "Your existing ones"], ["Extensions", "None", "Your existing ones"],
["Login state", "Fresh", "Already logged in"], ["Login state", "Fresh", "Already logged in"],
["Bot detection", "Always detected", "Can bypass"], ["Bot detection", "Always detected", "Can bypass"],
["Collaboration", "Separate window", "Same browser as user"], ["Collaboration", "Separate window", "Same browser as user"],
]} ]}
/> />
<PropsTable <PropsTable
title="vs BrowserMCP" title="vs BrowserMCP"
rows={[ rows={[
["Tools", "12+ dedicated tools", "1 execute tool"], ["Tools", "12+ dedicated tools", "1 execute tool"],
["API", "Limited actions", "Full Playwright"], ["API", "Limited actions", "Full Playwright"],
["Context usage", "High (tool schemas)", "Low"], ["Context usage", "High (tool schemas)", "Low"],
["LLM knowledge", "Must learn tools", "Already knows Playwright"], ["LLM knowledge", "Must learn tools", "Already knows Playwright"],
]} ]}
/> />
<PropsTable <PropsTable
title="vs Antigravity (Jetski)" title="vs Antigravity (Jetski)"
rows={[ rows={[
["Tools", "17+ tools", "1 tool"], ["Tools", "17+ tools", "1 tool"],
["Subagent", "Spawns for each task", "Direct execution"], ["Subagent", "Spawns for each task", "Direct execution"],
["Latency", "High (agent overhead)", "Low"], ["Latency", "High (agent overhead)", "Low"],
]} ]}
/> />
<PropsTable <PropsTable
title="vs Claude Browser Extension" title="vs Claude Browser Extension"
rows={[ rows={[
["Agent support", "Claude only", "Any MCP client"], ["Agent support", "Claude only", "Any MCP client"],
["Windows WSL", "No", "Yes"], ["Windows WSL", "No", "Yes"],
["Context method", "Screenshots (100KB+)", "A11y snapshots (5\u201320KB)"], ["Context method", "Screenshots (100KB+)", "A11y snapshots (5\u201320KB)"],
["Playwright API", "No", "Full"], ["Playwright API", "No", "Full"],
["Debugger", "No", "Yes"], ["Debugger", "No", "Yes"],
["Live code editing", "No", "Yes"], ["Live code editing", "No", "Yes"],
["Network interception", "Limited", "Full"], ["Network interception", "Limited", "Full"],
["Raw CDP access", "No", "Yes"], ["Raw CDP access", "No", "Yes"],
]} ]}
/> />
</Section> </Section>
<Section id="remote-access" title="Remote access"> <Section id="remote-access" title="Remote access">
<P> <P>
Control Chrome on a remote machine over the internet using tunnels. Control Chrome on a remote machine over the internet using tunnels.
Run the relay on the host, expose it through a tunnel, and connect Run the relay on the host, expose it through a tunnel, and connect
from anywhere. from anywhere.
</P> </P>
<CodeBlock lang="bash">{`# On the host machine <CodeBlock lang="bash">{dedent`
npx -y traforo -p 19988 -t my-machine -- npx -y playwriter serve --token <secret> # On the host machine
npx -y traforo -p 19988 -t my-machine -- npx -y playwriter serve --token <secret>
# From anywhere # From anywhere
export PLAYWRITER_HOST=https://my-machine-tunnel.traforo.dev export PLAYWRITER_HOST=https://my-machine-tunnel.traforo.dev
export PLAYWRITER_TOKEN=<secret> export PLAYWRITER_TOKEN=<secret>
playwriter -s 1 -e "await page.goto('https://example.com')"`}</CodeBlock> playwriter -s 1 -e "await page.goto('https://example.com')"
`}</CodeBlock>
<P> <P>
Also works on a LAN without tunnels &mdash; just set{" "} Also works on a LAN without tunnels &mdash; just set{" "}
<Code>PLAYWRITER_HOST=192.168.1.10</Code>. Use cases <Code>PLAYWRITER_HOST=192.168.1.10</Code>. Use cases
include controlling a headless Mac mini, providing remote user include controlling a headless Mac mini, providing remote user
support, and multi-machine automation. support, and multi-machine automation.
</P> </P>
</Section> </Section>
<Section id="security" title="Security"> <Section id="security" title="Security">
<P> <P>
Playwriter is local by default. The relay runs on{" "} Playwriter is local by default. The relay runs on{" "}
<Code>localhost:19988</Code> and only accepts connections <Code>localhost:19988</Code> and only accepts connections
from the extension. There&apos;s no remote server, no account, no from the extension. There&apos;s no remote server, no account, no
telemetry. telemetry.
</P> </P>
<List> <List>
<Li> <Li>
<strong>Local only</strong> &mdash; WebSocket server binds to <strong>Local only</strong> &mdash; WebSocket server binds to
localhost. Nothing leaves your machine. localhost. Nothing leaves your machine.
</Li> </Li>
<Li> <Li>
<strong>Origin validation</strong> &mdash; only the Playwriter <strong>Origin validation</strong> &mdash; only the Playwriter
extension origin is accepted. Browsers cannot spoof the Origin extension origin is accepted. Browsers cannot spoof the Origin
header, so malicious websites cannot connect. header, so malicious websites cannot connect.
</Li> </Li>
<Li> <Li>
<strong>Explicit consent</strong> &mdash; only tabs where you <strong>Explicit consent</strong> &mdash; only tabs where you
clicked the extension icon are controlled. No background access. clicked the extension icon are controlled. No background access.
</Li> </Li>
<Li> <Li>
<strong>Visible automation</strong> &mdash; Chrome shows an <strong>Visible automation</strong> &mdash; Chrome shows an
automation banner on controlled tabs. automation banner on controlled tabs.
</Li> </Li>
</List> </List>
</Section> </Section>
+4
View File
@@ -79,6 +79,9 @@
rgba(0, 0, 0, 0.04) 0px 4px 16px, rgba(0, 0, 0, 0.04) 0px 4px 16px,
rgba(0, 0, 0, 0.06) 0px 0px 0px 1px inset; rgba(0, 0, 0, 0.06) 0px 0px 0px 1px inset;
/* Link accent color */
--ll-accent: #0969da;
/* P3 accent colors (wider gamut) */ /* P3 accent colors (wider gamut) */
--ll-primary: #3e9fff; --ll-primary: #3e9fff;
--ll-secondary: #f09637; --ll-secondary: #f09637;
@@ -130,6 +133,7 @@
rgba(255, 255, 255, 0.02) 0px 4px 16px, rgba(255, 255, 255, 0.02) 0px 4px 16px,
rgba(255, 255, 255, 0.06) 0px 0px 0px 1px inset; rgba(255, 255, 255, 0.06) 0px 0px 0px 1px inset;
--ll-accent: #58a6ff;
--ll-primary: #60a5fa; --ll-primary: #60a5fa;
--ll-secondary: #f5a623; --ll-secondary: #f5a623;