feat(rebase): fork base on upstream v0.24.0 native architecture

- Rebased onto upstream/main (v0.24.0, full Rust native)
- Renamed package to agent-browser-stealth, version 0.24.0-fork.1
- Preserved fork-specific: abs alias, extensions/tab-group-cdp, .husky hooks
- Removed upstream-only: docs/, packages/dashboard, examples/, benchmarks/
- Simplified pnpm workspace to root-only
- Added [[bin]] section to keep binary name as "agent-browser"

Track 1 of native-stealth migration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
leeguooooo
2026-05-08 23:46:53 +09:00
co-authored by Claude Opus 4.6
parent 82eadcee41
commit 6addc80aa1
189 changed files with 4422 additions and 30395 deletions
-41
View File
@@ -1,41 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
-22
View File
@@ -1,22 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}
-18
View File
@@ -1,18 +0,0 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;
-102
View File
@@ -1,102 +0,0 @@
import type { MDXComponents } from "mdx/types";
import Link from "next/link";
import { CodeBlock } from "@/components/code-block";
function slugify(text: string): string {
return text
.toLowerCase()
.replace(/[^\w\s-]/g, "")
.replace(/\s+/g, "-")
.trim();
}
function extractText(children: React.ReactNode): string {
if (typeof children === "string") return children;
if (typeof children === "number") return String(children);
if (Array.isArray(children)) return children.map(extractText).join("");
if (children && typeof children === "object") {
const obj = children as unknown as Record<string, unknown>;
if ("props" in obj) {
const props = obj.props as { children?: React.ReactNode } | undefined;
return extractText(props?.children);
}
}
return "";
}
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
...components,
h1: ({ children }: { children?: React.ReactNode }) => {
const id = slugify(extractText(children));
return (
<h1 id={id} className="heading-anchor">
{children}
<a href={`#${id}`} aria-label="Link to this section">#</a>
</h1>
);
},
h2: ({ children }: { children?: React.ReactNode }) => {
const id = slugify(extractText(children));
return (
<h2 id={id} className="heading-anchor">
{children}
<a href={`#${id}`} aria-label="Link to this section">#</a>
</h2>
);
},
h3: ({ children }: { children?: React.ReactNode }) => {
const id = slugify(extractText(children));
return (
<h3 id={id} className="heading-anchor">
{children}
<a href={`#${id}`} aria-label="Link to this section">#</a>
</h3>
);
},
a: ({
href,
children,
}: {
href?: string;
children?: React.ReactNode;
}) => {
if (href?.startsWith("/")) {
return <Link href={href}>{children}</Link>;
}
return (
<a href={href} target="_blank" rel="noopener noreferrer">
{children}
</a>
);
},
code: ({
children,
className,
}: {
children?: React.ReactNode;
className?: string;
}) => {
if (className) {
return <code className={className}>{children}</code>;
}
return <code>{children}</code>;
},
pre: async ({ children }: { children?: React.ReactNode }) => {
const codeElement = children as React.ReactElement<{
className?: string;
children?: string;
}>;
const className = codeElement?.props?.className || "";
const lang = className.replace("language-", "") || "bash";
const code = codeElement?.props?.children || "";
return (
<CodeBlock
code={typeof code === "string" ? code : String(code)}
lang={lang}
/>
);
},
};
}
-11
View File
@@ -1,11 +0,0 @@
import createMDX from "@next/mdx";
/** @type {import('next').NextConfig} */
const nextConfig = {
pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"],
serverExternalPackages: ["just-bash", "bash-tool"],
};
const withMDX = createMDX({});
export default withMDX(nextConfig);
-48
View File
@@ -1,48 +0,0 @@
{
"name": "docs",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "portless agent-browser next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@ai-sdk/react": "^3.0.80",
"@mdx-js/loader": "^3.1.1",
"@mdx-js/mdx": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@next/mdx": "^16.1.6",
"@streamdown/code": "^1.0.2",
"@upstash/ratelimit": "^2.0.8",
"@upstash/redis": "^1.36.2",
"@vercel/analytics": "^1.6.1",
"@vercel/speed-insights": "^1.3.1",
"ai": "^6.0.78",
"bash-tool": "^1.3.14",
"clsx": "^2.1.1",
"geist": "^1.7.0",
"just-bash": "^2.9.6",
"next": "16.1.1",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
"react": "19.2.3",
"react-dom": "19.2.3",
"shiki": "^3.21.0",
"streamdown": "^2.1.0",
"tailwind-merge": "^3.4.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/mdx": "^2.0.13",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.1",
"tailwindcss": "^4",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5"
}
}
-8139
View File
File diff suppressed because it is too large Load Diff
-7
View File
@@ -1,7 +0,0 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
Binary file not shown.
Binary file not shown.
-123
View File
@@ -1,123 +0,0 @@
import { readFile } from "fs/promises";
import { join } from "path";
import { convertToModelMessages, stepCountIs, streamText } from "ai";
import type { ModelMessage, UIMessage } from "ai";
import { createBashTool } from "bash-tool";
import { headers } from "next/headers";
import { allDocsPages } from "@/lib/docs-navigation";
import { mdxToCleanMarkdown } from "@/lib/mdx-to-markdown";
import { minuteRateLimit, dailyRateLimit } from "@/lib/rate-limit";
export const maxDuration = 60;
const DEFAULT_MODEL = "anthropic/claude-sonnet-4.6";
const SYSTEM_PROMPT = `You are a helpful documentation assistant for agent-browser, a browser automation CLI designed for AI agents.
GitHub repository: https://github.com/vercel-labs/agent-browser
Documentation: https://agent-browser.dev
npm package: agent-browser
You have access to the full agent-browser documentation via the bash and readFile tools. The docs are available as markdown files in the /workspace/ directory.
When answering questions:
- Use the bash tool to list files (ls /workspace/) or search for content (grep -r "keyword" /workspace/)
- Use the readFile tool to read specific documentation pages (e.g. readFile with path "/workspace/index.md")
- Do NOT use bash to write, create, modify, or delete files (no tee, cat >, sed -i, echo >, cp, mv, rm, mkdir, touch, etc.) — you are read-only
- Always base your answers on the actual documentation content
- Be concise and accurate
- If the docs don't cover a topic, say so honestly
- Do NOT include source references or file paths in your response
- Do NOT use emojis in your responses`;
async function loadDocsFiles(): Promise<Record<string, string>> {
const files: Record<string, string> = {};
const results = await Promise.allSettled(
allDocsPages.map(async (page) => {
const slug = page.href === "/" ? "" : page.href.replace(/^\//, "");
const filePath = slug
? join(process.cwd(), "src", "app", slug, "page.mdx")
: join(process.cwd(), "src", "app", "page.mdx");
const raw = await readFile(filePath, "utf-8");
const md = mdxToCleanMarkdown(raw);
const fileName = slug ? `/${slug}.md` : "/index.md";
return { fileName, md };
}),
);
for (const result of results) {
if (result.status === "fulfilled") {
files[result.value.fileName] = result.value.md;
}
}
return files;
}
function addCacheControl(messages: ModelMessage[]): ModelMessage[] {
if (messages.length === 0) return messages;
return messages.map((message, index) => {
if (index === messages.length - 1) {
return {
...message,
providerOptions: {
...message.providerOptions,
anthropic: { cacheControl: { type: "ephemeral" } },
},
};
}
return message;
});
}
export async function POST(req: Request) {
const headersList = await headers();
const ip = headersList.get("x-forwarded-for")?.split(",")[0] ?? "anonymous";
const [minuteResult, dailyResult] = await Promise.all([
minuteRateLimit.limit(ip),
dailyRateLimit.limit(ip),
]);
if (!minuteResult.success || !dailyResult.success) {
const isMinuteLimit = !minuteResult.success;
return new Response(
JSON.stringify({
error: "Rate limit exceeded",
message: isMinuteLimit
? "Too many requests. Please wait a moment before trying again."
: "Daily limit reached. Please try again tomorrow.",
}),
{
status: 429,
headers: { "Content-Type": "application/json" },
},
);
}
const { messages }: { messages: UIMessage[] } = await req.json();
const docsFiles = await loadDocsFiles();
const {
tools: { bash, readFile },
} = await createBashTool({ files: docsFiles });
const result = streamText({
headers: {
"http-referer": "https://agent-browser.dev",
"x-title": "agent-browser",
},
model: DEFAULT_MODEL,
system: SYSTEM_PROMPT,
messages: await convertToModelMessages(messages),
stopWhen: stepCountIs(5),
tools: { bash, readFile },
prepareStep: ({ messages: stepMessages }) => ({
messages: addCacheControl(stepMessages),
}),
});
return result.toUIMessageStreamResponse();
}
-40
View File
@@ -1,40 +0,0 @@
import { readFile } from "fs/promises";
import { join } from "path";
import { NextRequest, NextResponse } from "next/server";
import { mdxToCleanMarkdown } from "@/lib/mdx-to-markdown";
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const docPath = searchParams.get("path");
if (!docPath) {
return NextResponse.json(
{ error: "Missing ?path= parameter" },
{ status: 400 },
);
}
const normalized = docPath
.replace(/^\//, "")
.replace(/\.\./g, "")
.replace(/[^a-zA-Z0-9/_-]/g, "");
const slug = normalized;
const filePath = slug
? join(process.cwd(), "src", "app", ...slug.split("/"), "page.mdx")
: join(process.cwd(), "src", "app", "page.mdx");
try {
const raw = await readFile(filePath, "utf-8");
const markdown = mdxToCleanMarkdown(raw);
return new NextResponse(markdown, {
headers: {
"Content-Type": "text/markdown; charset=utf-8",
"Cache-Control": "public, max-age=3600",
},
});
} catch {
return NextResponse.json({ error: "Page not found" }, { status: 404 });
}
}
-69
View File
@@ -1,69 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { getSearchIndex } from "@/lib/search-index";
export async function GET(req: NextRequest) {
const q = req.nextUrl.searchParams.get("q")?.trim().toLowerCase();
if (!q) {
return NextResponse.json({ results: [] });
}
const index = await getSearchIndex();
const terms = q.split(/\s+/).filter(Boolean);
const results = index
.map((entry) => {
const titleLower = entry.title.toLowerCase();
const contentLower = entry.content.toLowerCase();
const titleMatch = terms.every((t) => titleLower.includes(t));
const contentMatch = terms.every((t) => contentLower.includes(t));
if (!titleMatch && !contentMatch) return null;
let snippet = "";
if (contentMatch) {
const firstTermIdx = Math.min(
...terms.map((t) => {
const idx = contentLower.indexOf(t);
return idx === -1 ? Infinity : idx;
}),
);
if (firstTermIdx !== Infinity) {
const start = Math.max(0, firstTermIdx - 40);
const end = Math.min(entry.content.length, firstTermIdx + 120);
snippet =
(start > 0 ? "..." : "") +
entry.content.slice(start, end).replace(/\n/g, " ") +
(end < entry.content.length ? "..." : "");
}
}
return {
title: entry.title,
href: entry.href,
section: entry.section,
snippet,
score: titleMatch ? 2 : 1,
};
})
.filter(
(
r,
): r is {
title: string;
href: string;
section: string;
snippet: string;
score: number;
} => r !== null,
)
.sort((a, b) => b.score - a.score)
.slice(0, 20)
.map(({ score: _, ...rest }) => rest);
return NextResponse.json(
{ results },
{ headers: { "Cache-Control": "public, max-age=60" } },
);
}
-7
View File
@@ -1,7 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("cdp-mode");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
-120
View File
@@ -1,120 +0,0 @@
# CDP Mode
Connect to an existing browser via Chrome DevTools Protocol:
```bash
# Start Chrome with: google-chrome --remote-debugging-port=9222
# Connect once, then run commands without --cdp
agent-browser connect 9222
agent-browser snapshot
agent-browser tab
agent-browser close
# Or pass --cdp on each command
agent-browser --cdp 9222 snapshot
```
## Remote WebSocket URLs
Connect to remote browser services via WebSocket URL:
```bash
# Connect to remote browser service
agent-browser --cdp "wss://browser-service.com/cdp?token=..." snapshot
# Works with any CDP-compatible service
agent-browser --cdp "ws://localhost:9222/devtools/browser/abc123" open example.com
```
The `--cdp` flag accepts either:
- A port number (e.g., `9222`) for local connections via `http://localhost:{port}`
- A full WebSocket URL (e.g., `wss://...` or `ws://...`) for remote browser services
## Auto-Connect
Use `--auto-connect` to automatically discover and connect to a running Chrome instance without specifying a port:
```bash
# Auto-discover running Chrome with remote debugging
agent-browser --auto-connect open example.com
agent-browser --auto-connect snapshot
# Or via environment variable
AGENT_BROWSER_AUTO_CONNECT=1 agent-browser snapshot
```
Auto-connect discovers Chrome by:
1. Reading Chrome's `DevToolsActivePort` file from the default user data directory
2. Falling back to probing common debugging ports (9222, 9229)
3. If HTTP-based discovery (`/json/version`, `/json/list`) fails, falling back to a direct WebSocket connection
This is useful when:
- Chrome 144+ has remote debugging enabled via `chrome://inspect/#remote-debugging` (which uses a dynamic port)
- You want a zero-configuration connection to your existing browser
- You don't want to track which port Chrome is using
## Color scheme
Use `--color-scheme` to set a persistent preference when connecting via CDP:
```bash
agent-browser --cdp 9222 --color-scheme dark open https://example.com
agent-browser --cdp 9222 snapshot # stays in dark mode
```
Or set it globally via config or environment variable:
```bash
AGENT_BROWSER_COLOR_SCHEME=dark agent-browser --cdp 9222 open https://example.com
```
## Use cases
This enables control of:
- Electron apps
- Chrome/Chromium with remote debugging
- WebView2 applications
- Remote browser services (via WebSocket URL)
- Any browser exposing a CDP endpoint
## Global options
<table>
<thead>
<tr><th>Option</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td><code>--session &lt;name&gt;</code></td><td>Use isolated session</td></tr>
<tr><td><code>--profile &lt;path&gt;</code></td><td>Persistent browser profile directory</td></tr>
<tr><td><code>-p &lt;provider&gt;</code></td><td>Cloud browser provider (<code>browserbase</code>, <code>browseruse</code>, <code>kernel</code>, <code>browserless</code>)</td></tr>
<tr><td><code>--headers &lt;json&gt;</code></td><td>HTTP headers scoped to origin</td></tr>
<tr><td><code>--executable-path</code></td><td>Custom browser executable</td></tr>
<tr><td><code>--args &lt;args&gt;</code></td><td>Browser launch args (comma-separated)</td></tr>
<tr><td><code>--user-agent &lt;ua&gt;</code></td><td>Custom User-Agent string</td></tr>
<tr><td><code>--proxy &lt;url&gt;</code></td><td>Proxy server URL</td></tr>
<tr><td><code>--proxy-bypass &lt;hosts&gt;</code></td><td>Hosts to bypass proxy</td></tr>
<tr><td><code>--json</code></td><td>JSON output for scripts</td></tr>
<tr><td><code>--name, -n</code></td><td>Locator name filter</td></tr>
<tr><td><code>--exact</code></td><td>Exact text match</td></tr>
<tr><td><code>--headed</code></td><td>Show browser window</td></tr>
<tr><td><code>{"--cdp <port|url>"}</code></td><td>CDP connection (port or WebSocket URL)</td></tr>
<tr><td><code>--auto-connect</code></td><td>Auto-discover and connect to running Chrome</td></tr>
<tr><td><code>--color-scheme &lt;scheme&gt;</code></td><td>Persistent color scheme (<code>dark</code>, <code>light</code>, <code>no-preference</code>)</td></tr>
<tr><td><code>--debug</code></td><td>Debug output</td></tr>
</tbody>
</table>
## Cloud providers
Use the `-p` flag to connect to a cloud browser provider instead of launching a local browser:
```bash
agent-browser -p browserbase open https://example.com
```
See the [Providers](/providers/browser-use) section for setup and configuration of each supported provider: [Browser Use](/providers/browser-use), [Browserbase](/providers/browserbase), [Browserless](/providers/browserless), and [Kernel](/providers/kernel).
-7
View File
@@ -1,7 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("changelog");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
File diff suppressed because it is too large Load Diff
-7
View File
@@ -1,7 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("commands");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
-569
View File
@@ -1,569 +0,0 @@
# Commands
## Core
```bash
agent-browser open # Launch browser (no nav); stays on about:blank
agent-browser open <url> # Launch + navigate (aliases: goto, navigate)
agent-browser click <sel> # Click element (--new-tab to open in new tab)
agent-browser dblclick <sel> # Double-click
agent-browser fill <sel> <text> # Clear and fill
agent-browser type <sel> <text> # Type into element
agent-browser press <key> # Press key (Enter, Tab, Control+a) (alias: key)
agent-browser keyboard type <text> # Type at current focus (no selector needed)
agent-browser keyboard inserttext <text> # Insert text without key events
agent-browser keydown <key> # Hold key down
agent-browser keyup <key> # Release key
agent-browser hover <sel> # Hover element
agent-browser focus <sel> # Focus element
agent-browser select <sel> <val> # Select dropdown option
agent-browser check <sel> # Check checkbox
agent-browser uncheck <sel> # Uncheck checkbox
agent-browser scroll <dir> [px] # Scroll (up/down/left/right, --selector <sel>)
agent-browser scrollintoview <sel> # Scroll element into view
agent-browser drag <src> <dst> # Drag and drop
agent-browser upload <sel> <files> # Upload files
agent-browser screenshot [path] # Screenshot (--full for full page)
agent-browser screenshot --annotate # Annotated screenshot with numbered element labels
agent-browser screenshot --screenshot-dir ./shots # Save to custom directory
agent-browser screenshot --screenshot-format jpeg --screenshot-quality 80
agent-browser pdf <path> # Save page as PDF
agent-browser snapshot # Accessibility tree with refs
agent-browser eval <js> # Run JavaScript
agent-browser connect <port|url> # Connect to browser via CDP
agent-browser stream enable [--port <port>] # Start runtime WebSocket streaming
agent-browser stream status # Show runtime streaming state and bound port
agent-browser stream disable # Stop runtime WebSocket streaming
agent-browser close # Close browser (aliases: quit, exit)
agent-browser close --all # Close all active sessions
```
## Get info
```bash
agent-browser get text <sel> # Get text content
agent-browser get html <sel> # Get innerHTML
agent-browser get value <sel> # Get input value
agent-browser get attr <sel> <attr> # Get attribute
agent-browser get title # Get page title
agent-browser get url # Get current URL
agent-browser get cdp-url # Get CDP WebSocket URL
agent-browser get count <sel> # Count matching elements
agent-browser get box <sel> # Get bounding box
agent-browser get styles <sel> # Get computed styles
```
## Check state
```bash
agent-browser is visible <sel> # Check if visible
agent-browser is enabled <sel> # Check if enabled
agent-browser is checked <sel> # Check if checked
```
## Find elements
Semantic locators with actions (`click`, `fill`, `type`, `hover`, `focus`, `check`, `uncheck`, `text`):
```bash
agent-browser find role <role> <action> [value]
agent-browser find text <text> <action>
agent-browser find label <label> <action> [value]
agent-browser find placeholder <ph> <action> [value]
agent-browser find alt <text> <action>
agent-browser find title <text> <action>
agent-browser find testid <id> <action> [value]
agent-browser find first <sel> <action> [value]
agent-browser find last <sel> <action> [value]
agent-browser find nth <n> <sel> <action> [value]
```
Options:
- `--name <name>` -- filter role by accessible name
- `--exact` -- require exact text match
Examples:
```bash
agent-browser find role button click --name "Submit"
agent-browser find label "Email" fill "test@test.com"
agent-browser find alt "Logo" click
agent-browser find first ".item" click
agent-browser find last ".item" text
agent-browser find nth 2 ".card" hover
```
## Wait
```bash
agent-browser wait <selector> # Wait for element
agent-browser wait <ms> # Wait for time
agent-browser wait --text "Welcome" # Wait for text (substring match)
agent-browser wait --url "**/dash" # Wait for URL pattern
agent-browser wait --load networkidle # Wait for load state
agent-browser wait --fn "condition" # Wait for JS condition
agent-browser wait --download [path] # Wait for download
agent-browser wait --fn "!document.body.innerText.includes('Loading...')" # Wait for text to disappear
agent-browser wait "#spinner" --state hidden # Wait for element to disappear
```
## Downloads
```bash
agent-browser download <sel> <path> # Click element to trigger download
agent-browser wait --download [path] # Wait for any download to complete
```
Use `--download-path <dir>` (or `AGENT_BROWSER_DOWNLOAD_PATH` env) to set a default download directory. Without it, downloads go to a temporary directory that is deleted when the browser closes.
## Mouse
```bash
agent-browser mouse move <x> <y> # Move mouse
agent-browser mouse down [button] # Press button
agent-browser mouse up [button] # Release button
agent-browser mouse wheel <dy> [dx] # Scroll wheel
```
## Clipboard
```bash
agent-browser clipboard read # Read text from clipboard
agent-browser clipboard write "Hello, World!" # Write text to clipboard
agent-browser clipboard copy # Copy current selection (Ctrl+C)
agent-browser clipboard paste # Paste from clipboard (Ctrl+V)
```
## Settings
```bash
agent-browser set viewport <w> <h> [scale] # Set viewport size (scale for retina, e.g. 2)
agent-browser set device <name> # Emulate device ("iPhone 14")
agent-browser set geo <lat> <lng> # Set geolocation
agent-browser set offline [on|off] # Toggle offline mode
agent-browser set headers <json> # Extra HTTP headers
agent-browser set credentials <u> <p> # HTTP basic auth
agent-browser set media [dark|light] # Emulate color scheme (persists for session)
```
Use `--color-scheme` for persistent dark/light mode across all commands:
```bash
agent-browser --color-scheme dark open https://example.com
```
## Cookies & storage
```bash
agent-browser cookies # Get all cookies
agent-browser cookies set <name> <val> # Set cookie
agent-browser cookies clear # Clear cookies
agent-browser storage local # Get all localStorage
agent-browser storage local <key> # Get specific key
agent-browser storage local set <k> <v> # Set value
agent-browser storage local clear # Clear all
agent-browser storage session # Same for sessionStorage
```
## Network
```bash
agent-browser network route <url> # Intercept requests
agent-browser network route <url> --abort # Block requests
agent-browser network route <url> --body <json> # Mock response
agent-browser network route '*' --abort --resource-type script # Block scripts only
agent-browser network unroute [url] # Remove routes
agent-browser network requests # View tracked requests
agent-browser network requests --clear # Clear request log
agent-browser network requests --filter <pat> # Filter by URL pattern
agent-browser network requests --type xhr,fetch # Filter by resource type
agent-browser network requests --method POST # Filter by HTTP method
agent-browser network requests --status 2xx # Filter by status (200, 2xx, 400-499)
agent-browser network request <requestId> # View full request/response detail
agent-browser network har start # Start HAR recording
agent-browser network har stop [output.har] # Stop and save HAR (temp path if omitted)
```
## Tabs & frames
```bash
agent-browser tab # List tabs (each row shows tabId and label)
agent-browser tab new [url] # New tab
agent-browser tab new --label docs [url] # New tab with a user-assigned label
agent-browser tab <t<N>|label> # Switch to a tab by id or label
agent-browser tab close [t<N>|label] # Close a tab (defaults to active)
agent-browser window new # Open new browser window
agent-browser frame <sel> # Switch to iframe by CSS selector
agent-browser frame @e3 # Switch to iframe by element ref
agent-browser frame main # Back to main frame
```
### Stable tab ids and labels
Tab ids are stable strings of the form `t1`, `t2`, `t3`. They're never reused
within a session, so `t2` keeps pointing at the same tab even as other tabs
are opened or closed. The `t` prefix mirrors the `@e1` element-ref convention
and is not interchangeable with positional integers — `agent-browser tab 2`
errors with a teaching message; use `t2`.
You can also assign a memorable label (`docs`, `app`, `admin`) at tab-creation
time and use it anywhere an id is accepted:
```bash
agent-browser tab new --label docs https://docs.example.com
agent-browser tab docs # switch to the docs tab
agent-browser snapshot # populate refs for docs
agent-browser click @e3 # click uses docs's refs
agent-browser tab close docs # close by label
```
Labels are never auto-generated and never rewritten on navigation — an agent
that names a tab `docs` keeps that name until the tab is closed. Labels are
unique within a session; creating a second tab with an existing label
errors.
Refs (`@e1`, etc.) are scoped to the tab that was active when the snapshot
ran, so switch tabs first, then snapshot and interact:
```bash
agent-browser tab docs # switch first
agent-browser snapshot # refs for docs
agent-browser click @e3 # uses docs's refs
```
### Iframe support
Iframes are detected automatically during snapshots. `Iframe` nodes are resolved and their content is
inlined beneath the iframe element in the snapshot output. Refs assigned to elements inside iframes carry
frame context, so `click`, `fill`, and other interactions work without manually switching frames.
```bash
agent-browser snapshot -i
# @e3 [Iframe] "payment-frame"
# @e4 [input] "Card number"
# @e5 [button] "Pay"
# Interact directly using refs — no frame switch needed
agent-browser fill @e4 "4111111111111111"
agent-browser click @e5
# Or switch frame context for scoped snapshots
agent-browser frame @e3
agent-browser snapshot -i # Only elements inside that iframe
agent-browser frame main # Return to main frame
```
The `frame` command accepts element refs (`@e3`), CSS selectors (`"#my-iframe"`), or frame name/URL.
## Dialogs
```bash
agent-browser dialog accept [text] # Accept dialog (with optional prompt text)
agent-browser dialog dismiss # Dismiss dialog
agent-browser dialog status # Check if a dialog is currently open
```
By default, `alert` and `beforeunload` dialogs are automatically accepted so they never block the agent. `confirm` and `prompt` dialogs still require explicit handling. Use `--no-auto-dialog` (or `AGENT_BROWSER_NO_AUTO_DIALOG=1`) to disable automatic handling.
When a JavaScript dialog (`alert`, `confirm`, `prompt`) is pending, all command responses include a `warning` field with the dialog type and message.
## Streaming
```bash
agent-browser stream enable # Start runtime WebSocket streaming on an auto-selected port
agent-browser stream enable --port 9223 # Bind a specific localhost port
agent-browser stream status # Show enabled state, port, browser connection, screencasting
agent-browser stream disable # Stop runtime streaming and remove the .stream metadata file
```
Streaming is enabled automatically for all sessions. Use these commands to check status, re-enable on a specific port, or disable streaming.
## Debug
```bash
agent-browser trace start [path] # Start trace
agent-browser trace stop [path] # Stop and save trace
agent-browser profiler start # Start Chrome DevTools profiling
agent-browser profiler stop [path] # Stop and save profile (.json)
agent-browser record start <path> # Start video recording (WebM)
agent-browser record stop # Stop and save video
agent-browser record restart <path> # Stop current and start new recording
agent-browser console # View console messages
agent-browser console --json # JSON output with raw CDP args
agent-browser console --clear # Clear console log
agent-browser errors # View page errors
agent-browser errors --clear # Clear error log
agent-browser highlight <sel> # Highlight element
agent-browser inspect # Open Chrome DevTools for the active page
```
## Auth vault
```bash
agent-browser auth save <name> [opts] # Save auth profile
agent-browser auth login <name> # Login using saved credentials
agent-browser auth list # List saved profiles (names and URLs only)
agent-browser auth show <name> # Show profile metadata (no passwords)
agent-browser auth delete <name> # Delete a saved profile
```
Save options:
- `--url <url>` -- login page URL (required)
- `--username <user>` -- username (required)
- `--password <pass>` -- password (required unless `--password-stdin`)
- `--password-stdin` -- read password from stdin (recommended to avoid shell history exposure)
- `--username-selector <sel>` -- custom CSS selector for username field
- `--password-selector <sel>` -- custom CSS selector for password field
- `--submit-selector <sel>` -- custom CSS selector for submit button
`auth login` navigates with `load` and then waits for the username/password/submit selectors to appear before interacting. This improves reliability on SPA login pages where fields render after initial page load.
```bash
echo "pass" | agent-browser auth save github --url https://github.com/login --username user --password-stdin
agent-browser auth login github
agent-browser auth list
```
## Confirmation
When `--confirm-actions` is set, certain action categories return a `confirmation_required` response instead of executing immediately. Use `confirm` or `deny` to approve or reject the action.
```bash
agent-browser confirm <confirmation-id> # Approve a pending action
agent-browser deny <confirmation-id> # Deny a pending action
```
Pending confirmations auto-deny after 60 seconds.
```bash
agent-browser --confirm-actions eval,download eval "document.title"
# Returns confirmation_required with ID
agent-browser confirm c_8f3a1234
```
## State management
```bash
agent-browser state save <path> # Save auth state to file
agent-browser state load <path> # Load auth state from file
agent-browser state list # List saved state files
agent-browser state show <file> # Show state summary
agent-browser state rename <old> <new> # Rename state file
agent-browser state clear [name] # Clear states for session name
agent-browser state clear --all # Clear all saved states
agent-browser state clean --older-than <days> # Delete old states
```
## Sessions
```bash
agent-browser session # Show current session name
agent-browser session list # List active sessions
```
## Chrome profiles
```bash
agent-browser profiles # List available Chrome profiles
agent-browser profiles --json # List profiles as JSON
agent-browser --profile Default open https://gmail.com # Reuse a profile's login state
```
## Dashboard
```bash
agent-browser dashboard [start] # Start the dashboard server (default port: 4848)
agent-browser dashboard start --port <n> # Start on a specific port
agent-browser dashboard stop # Stop the dashboard server
```
Open the dashboard through `http://localhost:4848` or a proxied/forwarded dashboard URL such as `https://dashboard.agent-browser.localhost`. The browser stays on the dashboard origin; per-session tabs, status, and stream traffic are proxied internally, so session ports do not need to be exposed.
## Doctor
Diagnose your install, auto-clean stale daemon files, and optionally repair common problems.
```bash
agent-browser doctor # Full diagnosis (env, Chrome, daemons, config, providers, network, launch test)
agent-browser doctor --offline --quick # Local-only, fastest
agent-browser doctor --fix # Also run destructive repairs (reinstall Chrome, purge old state, ...)
agent-browser doctor --json # Structured JSON output for agents
```
Exit code is `0` if all checks pass (warnings are fine), `1` if any fail. See the [Installation page](/installation#doctor) for the full check catalog.
## Chat
Use natural language to control the browser via AI. The `chat` command translates instructions into agent-browser commands, executes them, and streams the AI response. Requires `AI_GATEWAY_API_KEY` to be set.
```bash
agent-browser chat "open google.com and search for cats" # Single-shot instruction
agent-browser chat # Interactive REPL (type quit to exit)
echo "summarize this page" | agent-browser chat # Piped input
agent-browser -q chat "summarize this page" # Quiet: text only, no tool calls shown
agent-browser -v chat "fill in the login form" # Verbose: show commands and their output
agent-browser --model openai/gpt-4o chat "take a screenshot" # Override the default AI model
agent-browser --json chat "open example.com" # Structured JSON output
```
Chat-specific options:
```bash
--model <name> # AI model (or AI_GATEWAY_MODEL env, default: anthropic/claude-sonnet-4.6)
-v, --verbose # Show tool commands and their raw output
-q, --quiet # Show only the AI text response (hide tool calls)
```
## Navigation
```bash
agent-browser back # Go back
agent-browser forward # Go forward
agent-browser reload # Reload page
agent-browser pushstate <url> # SPA client-side nav; auto-detects window.next.router.push,
# falls back to history.pushState + popstate
```
## Pre-navigation setup
Some flows need routes, cookies, or init scripts configured *before* the
first navigation (SSR debug, auth on protected origins, etc.). `open`
without a URL launches the browser but stays on `about:blank`, leaving
room to stage state. `batch` makes it one CLI invocation:
```bash
agent-browser batch \
'["open"]' \
'["network","route","*","--abort","--resource-type","script"]' \
'["cookies","set","--curl","cookies.curl","--domain","localhost"]' \
'["navigate","http://localhost:3000/target"]'
```
## React / Web Vitals
React commands require `--enable react-devtools` at launch (installs the
React DevTools hook before any page JS runs). `vitals` and `pushstate`
work on any site.
```bash
agent-browser open --enable react-devtools <url> # Launch with React hook installed
agent-browser react tree # Full component tree
agent-browser react inspect <fiberId> # Inspect one component
agent-browser react renders start # Begin fiber render recording
agent-browser react renders stop [--json] # Stop + print profile
agent-browser react suspense [--only-dynamic] [--json] # Suspense boundaries + classifier
# --only-dynamic hides the "static" list
agent-browser vitals [url] [--json] # LCP/CLS/TTFB/FCP/INP + hydration
```
Works on any React app (Next.js, Remix, Vite+React, CRA, TanStack Start,
React Native Web, etc.). `vitals` and `pushstate` are framework-agnostic.
## Init scripts
```bash
agent-browser open --init-script <path> # Register before first navigation (repeatable)
agent-browser addinitscript <js> # Register at runtime (returns identifier)
agent-browser removeinitscript <identifier> # Remove a previously registered init script
```
## Global options
```bash
--session <name> # Isolated browser session
--session-name <name> # Auto-save/restore session state (cookies, localStorage)
--profile <path> # Persistent browser profile directory
--state <path> # Load storage state from JSON file
--headers <json> # HTTP headers scoped to URL's origin
--executable-path <path> # Custom browser executable
--extension <path> # Load browser extension (repeatable)
--init-script <path> # Register a page init script before first navigation (repeatable)
--enable <feature> # Built-in init scripts: react-devtools (repeatable or comma-list)
--args <args> # Browser launch args (comma separated)
--user-agent <ua> # Custom User-Agent string
--proxy <url> # Proxy server URL
--proxy-bypass <hosts> # Hosts to bypass proxy
--ignore-https-errors # Ignore HTTPS certificate errors
--allow-file-access # Allow file:// URLs to access local files (Chromium only)
-p, --provider <name> # Browser provider (ios, browserbase, kernel, browseruse, browserless)
--device <name> # iOS device name (e.g., "iPhone 15 Pro")
--json # JSON output (for scripts)
--annotate # Annotated screenshot with numbered element labels
--screenshot-dir <path> # Default screenshot output directory (or AGENT_BROWSER_SCREENSHOT_DIR)
--screenshot-quality <n> # JPEG quality 0-100 (or AGENT_BROWSER_SCREENSHOT_QUALITY)
--screenshot-format <fmt> # Format: png (default), jpeg (or AGENT_BROWSER_SCREENSHOT_FORMAT)
--headed # Show browser window (not headless)
--cdp <port|url> # Connect via Chrome DevTools Protocol (port or WebSocket URL)
--auto-connect # Auto-discover and connect to running Chrome
--color-scheme <scheme> # Color scheme: dark, light, no-preference
--download-path <path> # Default download directory
--content-boundaries # Wrap page output in boundary markers for LLM safety
--max-output <chars> # Truncate page output to N characters
--allowed-domains <list> # Comma-separated allowed domain patterns
--action-policy <path> # Path to action policy JSON file
--confirm-actions <list> # Action categories requiring confirmation
--confirm-interactive # Interactive confirmation prompts (auto-denies if stdin is not a TTY)
--model <name> # AI model for chat (or AI_GATEWAY_MODEL env)
-v, --verbose # Show tool commands and their raw output (chat)
-q, --quiet # Show only AI text responses (chat)
--config <path> # Use a custom config file
--debug # Debug output
```
## Batch execution
Execute multiple commands in a single invocation. Commands can be passed as quoted arguments or piped as JSON via stdin.
```bash
# Argument mode: each quoted argument is a full command
agent-browser batch "open https://example.com" "snapshot -i" "screenshot"
# With --bail to stop on first error
agent-browser batch --bail "open https://example.com" "click @e1" "screenshot"
# Stdin mode: pipe commands as JSON
echo '[
["open", "https://example.com"],
["snapshot", "-i"],
["click", "@e1"],
["screenshot", "result.png"]
]' | agent-browser batch --json
```
<table>
<thead>
<tr><th>Option</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td><code>--bail</code></td><td>Stop on first error (default: continue all commands)</td></tr>
<tr><td><code>--json</code></td><td>Output results as a JSON array</td></tr>
</tbody>
</table>
## Command chaining
Chain commands with `&&` in a single shell invocation. The browser persists via a background daemon, so chaining works naturally and is more efficient than separate calls:
```bash
agent-browser open example.com && agent-browser wait --load networkidle && agent-browser snapshot -i
agent-browser fill @e1 "user@example.com" && agent-browser fill @e2 "pass" && agent-browser click @e3
agent-browser open example.com && agent-browser wait --load networkidle && agent-browser screenshot page.png
```
Use `&&` when you don't need to read intermediate output. Run commands separately when you need to parse output first (e.g., snapshot to discover refs, then interact with those refs).
## Local files
Open local files (PDFs, HTML) using `file://` URLs:
```bash
agent-browser --allow-file-access open file:///path/to/document.pdf
agent-browser --allow-file-access open file:///path/to/page.html
agent-browser screenshot output.png
```
The `--allow-file-access` flag enables JavaScript to access other local files. Chromium only.
-7
View File
@@ -1,7 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("configuration");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
-212
View File
@@ -1,212 +0,0 @@
# Configuration
Create an `agent-browser.json` file to set persistent defaults instead of repeating flags on every command.
## Config File Locations
agent-browser checks two locations, merged in priority order:
<table>
<thead>
<tr><th>Priority</th><th>Location</th><th>Scope</th></tr>
</thead>
<tbody>
<tr><td>1 (lowest)</td><td><code>~/.agent-browser/config.json</code></td><td>User-level defaults</td></tr>
<tr><td>2</td><td><code>./agent-browser.json</code></td><td>Project-level overrides</td></tr>
<tr><td>3</td><td><code>AGENT_BROWSER_*</code> env vars</td><td>Override config values</td></tr>
<tr><td>4 (highest)</td><td>CLI flags</td><td>Override everything</td></tr>
</tbody>
</table>
Project-level values override user-level values. Environment variables override both. CLI flags always win.
Use `--config <path>` or the `AGENT_BROWSER_CONFIG` environment variable to load a specific config file instead of the default locations:
```bash
agent-browser --config ./ci-config.json open example.com
AGENT_BROWSER_CONFIG=./ci-config.json agent-browser open example.com
```
## Example Config
```json
{
"headed": true,
"proxy": "http://localhost:8080",
"profile": "./browser-data",
"userAgent": "my-agent/1.0",
"ignoreHttpsErrors": true
}
```
A [JSON Schema](https://agent-browser.dev/schema.json) is available for IDE autocomplete and validation. Add a `$schema` key to your config file to enable it:
```json
{
"$schema": "https://agent-browser.dev/schema.json",
"headed": true
}
```
## All Options
Every CLI flag can be set in the config file using its camelCase equivalent:
<table>
<thead>
<tr><th>Config Key</th><th>CLI Flag</th><th>Type</th></tr>
</thead>
<tbody>
<tr><td><code>headed</code></td><td><code>--headed</code></td><td>boolean</td></tr>
<tr><td><code>json</code></td><td><code>--json</code></td><td>boolean</td></tr>
<tr><td><code>full</code></td><td><code>--full, -f</code></td><td>boolean</td></tr>
<tr><td><code>debug</code></td><td><code>--debug</code></td><td>boolean</td></tr>
<tr><td><code>session</code></td><td><code>--session</code></td><td>string</td></tr>
<tr><td><code>sessionName</code></td><td><code>--session-name</code></td><td>string</td></tr>
<tr><td><code>executablePath</code></td><td><code>--executable-path</code></td><td>string</td></tr>
<tr><td><code>extensions</code></td><td><code>--extension</code></td><td>string[]</td></tr>
<tr><td><code>profile</code></td><td><code>--profile</code></td><td>string</td></tr>
<tr><td><code>state</code></td><td><code>--state</code></td><td>string</td></tr>
<tr><td><code>proxy</code></td><td><code>--proxy</code></td><td>string</td></tr>
<tr><td><code>proxyBypass</code></td><td><code>--proxy-bypass</code></td><td>string</td></tr>
<tr><td><code>args</code></td><td><code>--args</code></td><td>string</td></tr>
<tr><td><code>userAgent</code></td><td><code>--user-agent</code></td><td>string</td></tr>
<tr><td><code>provider</code></td><td><code>-p, --provider</code></td><td>string</td></tr>
<tr><td><code>device</code></td><td><code>--device</code></td><td>string</td></tr>
<tr><td><code>ignoreHttpsErrors</code></td><td><code>--ignore-https-errors</code></td><td>boolean</td></tr>
<tr><td><code>allowFileAccess</code></td><td><code>--allow-file-access</code></td><td>boolean</td></tr>
<tr><td><code>cdp</code></td><td><code>--cdp</code></td><td>string</td></tr>
<tr><td><code>autoConnect</code></td><td><code>--auto-connect</code></td><td>boolean</td></tr>
<tr><td><code>colorScheme</code></td><td><code>--color-scheme</code></td><td>string (<code>dark</code>, <code>light</code>, <code>no-preference</code>)</td></tr>
<tr><td><code>downloadPath</code></td><td><code>--download-path</code></td><td>string</td></tr>
<tr><td><code>contentBoundaries</code></td><td><code>--content-boundaries</code></td><td>boolean</td></tr>
<tr><td><code>maxOutput</code></td><td><code>--max-output</code></td><td>number</td></tr>
<tr><td><code>allowedDomains</code></td><td><code>--allowed-domains</code></td><td>string[]</td></tr>
<tr><td><code>actionPolicy</code></td><td><code>--action-policy</code></td><td>string</td></tr>
<tr><td><code>confirmActions</code></td><td><code>--confirm-actions</code></td><td>string</td></tr>
<tr><td><code>confirmInteractive</code></td><td><code>--confirm-interactive</code></td><td>boolean</td></tr>
<tr><td><code>engine</code></td><td><code>--engine</code></td><td>string (<code>chrome</code>, <code>lightpanda</code>)</td></tr>
<tr><td><code>noAutoDialog</code></td><td><code>--no-auto-dialog</code></td><td>boolean</td></tr>
<tr><td><code>headers</code></td><td><code>--headers</code></td><td>string (JSON)</td></tr>
</tbody>
</table>
## Common Configurations
### Local Development
```json
{
"headed": true,
"profile": "./browser-data"
}
```
### Behind a Proxy
```json
{
"proxy": "http://proxy.corp.example.com:8080",
"proxyBypass": "localhost,*.internal.com",
"ignoreHttpsErrors": true
}
```
### CI / Devcontainer
```json
{
"args": "--no-sandbox,--disable-gpu",
"ignoreHttpsErrors": true
}
```
### iOS Testing
```json
{
"provider": "ios",
"device": "iPhone 16 Pro"
}
```
### AI Agent Security
```json
{
"contentBoundaries": true,
"maxOutput": 50000,
"allowedDomains": ["your-app.com", "*.your-app.com"],
"actionPolicy": "./policy.json"
}
```
## Overriding Boolean Options
Boolean flags accept an optional `true`/`false` value to override config settings:
```bash
agent-browser --headed false open example.com
```
A bare flag is equivalent to passing `true`:
```bash
agent-browser --headed open example.com # same as --headed true
agent-browser --headed true open example.com # explicit
```
This applies to all boolean flags: `--headed`, `--debug`, `--json`, `--ignore-https-errors`, `--allow-file-access`, `--auto-connect`, `--content-boundaries`, `--confirm-interactive`.
## Extensions Merging
Extensions from user-level and project-level configs are **concatenated**, not replaced. For example, if `~/.agent-browser/config.json` specifies `["/ext1"]` and `./agent-browser.json` specifies `["/ext2"]`, the result is `["/ext1", "/ext2"]`.
The `AGENT_BROWSER_EXTENSIONS` environment variable and CLI `--extension` flags follow the standard priority rules (env replaces config, CLI appends).
## Environment Variables
These environment variables configure additional daemon and runtime behavior:
<table>
<thead>
<tr><th>Variable</th><th>Description</th><th>Default</th></tr>
</thead>
<tbody>
<tr><td><code>AGENT_BROWSER_AUTO_CONNECT</code></td><td>Auto-discover and connect to a running Chrome instance.</td><td>(disabled)</td></tr>
<tr><td><code>AGENT_BROWSER_ALLOW_FILE_ACCESS</code></td><td>Allow <code>file://</code> URLs to access local files.</td><td>(disabled)</td></tr>
<tr><td><code>AGENT_BROWSER_COLOR_SCHEME</code></td><td>Color scheme preference (<code>dark</code>, <code>light</code>, <code>no-preference</code>).</td><td>(none)</td></tr>
<tr><td><code>AGENT_BROWSER_DOWNLOAD_PATH</code></td><td>Default directory for browser downloads.</td><td>(temp directory)</td></tr>
<tr><td><code>AGENT_BROWSER_DEFAULT_TIMEOUT</code></td><td>Default timeout in ms. Keep below 30000 to avoid IPC timeouts.</td><td><code>25000</code></td></tr>
<tr><td><code>AGENT_BROWSER_SESSION_NAME</code></td><td>Auto-save/load state persistence name.</td><td>(none)</td></tr>
<tr><td><code>AGENT_BROWSER_STATE_EXPIRE_DAYS</code></td><td>Auto-delete saved session states older than N days.</td><td><code>30</code></td></tr>
<tr><td><code>AGENT_BROWSER_ENCRYPTION_KEY</code></td><td>64-char hex key for AES-256-GCM session encryption.</td><td>(none)</td></tr>
<tr><td><code>AGENT_BROWSER_EXTENSIONS</code></td><td>Comma-separated browser extension paths. Extensions work in both headed and headless mode.</td><td>(none)</td></tr>
<tr><td><code>AGENT_BROWSER_HEADED</code></td><td>Show browser window instead of running headless (<code>1</code> to enable).</td><td>(disabled)</td></tr>
<tr><td><code>AGENT_BROWSER_STREAM_PORT</code></td><td>Override the WebSocket streaming port. By default, an OS-assigned port is used. Set this to bind to a specific port (e.g., <code>9223</code>).</td><td>OS-assigned</td></tr>
<tr><td><code>AGENT_BROWSER_IDLE_TIMEOUT_MS</code></td><td>Auto-shutdown the daemon after N ms of inactivity (no commands received). Useful for ephemeral environments.</td><td>(disabled)</td></tr>
<tr><td><code>AGENT_BROWSER_IOS_DEVICE</code></td><td>Default iOS device name for the <code>ios</code> provider.</td><td>(none)</td></tr>
<tr><td><code>AGENT_BROWSER_IOS_UDID</code></td><td>Default iOS device UDID for the <code>ios</code> provider.</td><td>(none)</td></tr>
<tr><td><code>AGENT_BROWSER_DEBUG</code></td><td>Enable debug output (<code>1</code> to enable).</td><td>(disabled)</td></tr>
<tr><td><code>AGENT_BROWSER_CONTENT_BOUNDARIES</code></td><td>Wrap page output in boundary markers for LLM safety.</td><td>(disabled)</td></tr>
<tr><td><code>AGENT_BROWSER_MAX_OUTPUT</code></td><td>Max characters for page output (truncates beyond limit).</td><td>(unlimited)</td></tr>
<tr><td><code>AGENT_BROWSER_ALLOWED_DOMAINS</code></td><td>Comma-separated allowed domain patterns (e.g., <code>example.com,*.example.com</code>).</td><td>(unrestricted)</td></tr>
<tr><td><code>AGENT_BROWSER_ACTION_POLICY</code></td><td>Path to action policy JSON file.</td><td>(none)</td></tr>
<tr><td><code>AGENT_BROWSER_CONFIRM_ACTIONS</code></td><td>Comma-separated action categories requiring confirmation.</td><td>(none)</td></tr>
<tr><td><code>AGENT_BROWSER_CONFIRM_INTERACTIVE</code></td><td>Enable interactive confirmation prompts (auto-denies if stdin is not a TTY).</td><td>(disabled)</td></tr>
<tr><td><code>AGENT_BROWSER_ENGINE</code></td><td>Browser engine to use: <code>chrome</code> (default), <code>lightpanda</code>.</td><td><code>chrome</code></td></tr>
<tr><td><code>AGENT_BROWSER_NO_AUTO_DIALOG</code></td><td>Disable automatic dismissal of <code>alert</code>/<code>beforeunload</code> dialogs.</td><td>(disabled)</td></tr>
<tr><td><code>AI_GATEWAY_URL</code></td><td>Vercel AI Gateway base URL.</td><td><code>https://ai-gateway.vercel.sh</code></td></tr>
<tr><td><code>AI_GATEWAY_API_KEY</code></td><td>API key for the Vercel AI Gateway. Required to enable AI chat.</td><td>(none)</td></tr>
<tr><td><code>AI_GATEWAY_MODEL</code></td><td>Default AI model for dashboard chat.</td><td><code>anthropic/claude-sonnet-4.6</code></td></tr>
</tbody>
</table>
## Error Handling
- **Auto-discovered config files** (`~/.agent-browser/config.json`, `./agent-browser.json`) that are missing are silently ignored.
- **`--config <path>`** with a missing or malformed file exits with an error.
- **Malformed JSON** in auto-discovered files prints a warning to stderr and continues without that file.
- **Unknown keys** are silently ignored for forward compatibility.
> **Tip:** If your project-level `agent-browser.json` contains environment-specific values (paths, proxies), consider adding it to `.gitignore`.
-7
View File
@@ -1,7 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("dashboard");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
-160
View File
@@ -1,160 +0,0 @@
# Observability Dashboard
Monitor agent-browser sessions in real time with a local web dashboard showing a live browser viewport and command activity feed.
## Usage
The dashboard is bundled into the binary and requires no separate install. Start the server and open any session:
```bash
agent-browser dashboard start
agent-browser open example.com
```
Then open `http://localhost:4848` or a proxied/forwarded dashboard URL such as `https://dashboard.agent-browser.localhost` in your browser to see the live dashboard.
All sessions automatically stream to the dashboard. No extra flags are needed. The browser stays on the dashboard origin while the server proxies per-session tabs, status, and stream traffic internally, so session ports do not need to be exposed.
### Custom stream port
By default each session binds its WebSocket stream server to an OS-assigned port. To use a specific port, set the `AGENT_BROWSER_STREAM_PORT` environment variable:
```bash
AGENT_BROWSER_STREAM_PORT=9223 agent-browser open example.com
```
You can also use the runtime commands to control streaming on a running session:
```bash
agent-browser stream enable --port 9223
agent-browser stream status
agent-browser stream disable
```
## Dashboard features
The dashboard is a single-page web app with three areas:
<table>
<thead>
<tr>
<th>Area</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Live viewport</strong></td>
<td>Real-time JPEG frames from the browser, rendered to a canvas element</td>
</tr>
<tr>
<td><strong>Activity feed</strong></td>
<td>Chronological stream of commands, results, and console messages with expandable details</td>
</tr>
<tr>
<td><strong>Session creation</strong></td>
<td>Create new sessions from the dashboard with local engines (Chrome, Lightpanda) or cloud providers (AgentCore, Browserbase, Browserless, Browser Use, Kernel)</td>
</tr>
<tr>
<td><strong>Status bar</strong></td>
<td>Connection status, viewport dimensions, and WebSocket endpoint</td>
</tr>
</tbody>
</table>
## WebSocket protocol
The dashboard connects to the same WebSocket endpoint used by [Streaming](/streaming), with additional message types for observability:
### Command events
Sent when a command begins executing:
```json
{
"type": "command",
"action": "click",
"id": "r123",
"params": { "selector": "@e5" },
"timestamp": 1711367000000
}
```
### Result events
Sent when a command finishes:
```json
{
"type": "result",
"id": "r123",
"action": "click",
"success": true,
"data": {},
"duration_ms": 45,
"timestamp": 1711367000045
}
```
### Console events
Sent when the browser logs to the console:
```json
{
"type": "console",
"level": "log",
"text": "Page loaded",
"args": [{"type": "string", "value": "Page loaded"}],
"timestamp": 1711367000100
}
```
The `args` array contains the raw CDP `Runtime.consoleAPICalled` arguments for programmatic access. Object arguments include preview data (e.g. `{userId: "abc", count: 42}` instead of `"Object"`).
These are in addition to the existing `frame`, `status`, and `error` message types documented on the [Streaming](/streaming) page.
## Architecture
The dashboard is a Next.js static export (`output: 'export'`) that produces plain HTML, CSS, and JS. It lives at `packages/dashboard/` in the monorepo and is built with:
```bash
pnpm build:dashboard
```
The dashboard is embedded into the CLI binary at compile time using `rust-embed`. Plain HTTP requests serve the embedded dashboard assets and same-origin API routes. Session-specific tabs, status, and stream WebSocket traffic are proxied through the dashboard server to loopback-only session ports.
## AI Chat
The dashboard includes an optional AI chat panel powered by the [Vercel AI Gateway](https://vercel.com/docs/ai-gateway). When enabled, a **Chat** tab appears in the right pane alongside Activity, Console, Network, Storage, and Extensions.
### Setup
The Chat tab is always visible. Set the API key to enable responses:
```bash
export AI_GATEWAY_API_KEY=gw_your_key_here
agent-browser dashboard start
```
Optionally override the gateway URL or model:
```bash
export AI_GATEWAY_URL=https://ai-gateway.vercel.sh # this is the default
export AI_GATEWAY_MODEL=openai/gpt-4o-mini # default: anthropic/claude-sonnet-4.6
```
### How it works
The Rust server proxies chat requests from the dashboard to the Vercel AI Gateway and streams responses back using the Vercel AI SDK's UI Message Stream protocol. The dashboard frontend uses `useChat` from `@ai-sdk/react` with `DefaultChatTransport`.
<table>
<thead>
<tr><th>Variable</th><th>Description</th><th>Default</th></tr>
</thead>
<tbody>
<tr><td><code>AI_GATEWAY_URL</code></td><td>Vercel AI Gateway base URL.</td><td><code>https://ai-gateway.vercel.sh</code></td></tr>
<tr><td><code>AI_GATEWAY_API_KEY</code></td><td>API key for the AI Gateway. Required to enable AI chat responses.</td><td>(none)</td></tr>
<tr><td><code>AI_GATEWAY_MODEL</code></td><td>Default AI model for chat requests.</td><td><code>anthropic/claude-sonnet-4.6</code></td></tr>
</tbody>
</table>
-7
View File
@@ -1,7 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("diffing");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
-175
View File
@@ -1,175 +0,0 @@
import { DiffDemo } from "@/components/diff-demo"
# Diffing
Compare page states to detect changes -- structurally via accessibility tree snapshots, visually via pixel comparison, or across two different URLs.
<DiffDemo />
## Commands
<table>
<thead>
<tr><th>Command</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td><code>diff snapshot</code></td><td>Compare current snapshot to last snapshot in session</td></tr>
<tr><td><code>diff snapshot --baseline &lt;file&gt;</code></td><td>Compare current snapshot to a saved file</td></tr>
<tr><td><code>diff screenshot --baseline &lt;file&gt;</code></td><td>Visual pixel diff against a baseline image</td></tr>
<tr><td><code>diff url &lt;url1&gt; &lt;url2&gt;</code></td><td>Compare two pages (snapshot + optional screenshot)</td></tr>
</tbody>
</table>
## Snapshot diff
Compares the accessibility tree between two points in time using a line-level text diff.
```bash
# Compare against the last snapshot taken in this session
agent-browser diff snapshot
# Compare against a saved baseline file
agent-browser diff snapshot --baseline before.txt
# Scope to a specific part of the page
agent-browser diff snapshot --selector "#main" --compact
```
Without `--baseline`, the command automatically compares against the most recent snapshot taken in the current session. This is the primary use case for agents verifying that an action had the intended effect.
### Options
<table>
<thead>
<tr><th>Flag</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td><code>-b, --baseline &lt;file&gt;</code></td><td>Path to a saved snapshot file to compare against</td></tr>
<tr><td><code>-s, --selector &lt;sel&gt;</code></td><td>Scope the current snapshot to a CSS selector or @ref</td></tr>
<tr><td><code>-c, --compact</code></td><td>Use compact snapshot format</td></tr>
<tr><td><code>-d, --depth &lt;n&gt;</code></td><td>Limit snapshot tree depth</td></tr>
</tbody>
</table>
### Output
The diff uses `+` for added lines and `-` for removed lines, similar to unified diff format. A summary line shows the count of additions, removals, and unchanged lines.
```
- button "Submit" [ref=e2]
+ button "Submit" [ref=e2] [disabled]
3 additions, 2 removals, 41 unchanged
```
## Screenshot diff
Compares the current page screenshot against a baseline image at the pixel level. Produces a diff image with changed pixels highlighted in red.
```bash
# Basic visual diff
agent-browser diff screenshot --baseline before.png
# Save diff image to a specific path
agent-browser diff screenshot --baseline before.png --output diff.png
# Adjust threshold and scope to element
agent-browser diff screenshot --baseline before.png --threshold 0.2 --selector "#hero"
```
### Options
<table>
<thead>
<tr><th>Flag</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td><code>-b, --baseline &lt;file&gt;</code></td><td>Baseline PNG/JPEG image to compare against (required)</td></tr>
<tr><td><code>-o, --output &lt;file&gt;</code></td><td>Path for the generated diff image (default: temp dir)</td></tr>
<tr><td><code>-t, --threshold &lt;0-1&gt;</code></td><td>Color distance threshold (default: 0.1). Higher = more tolerant</td></tr>
<tr><td><code>-s, --selector &lt;sel&gt;</code></td><td>Scope the current screenshot to an element</td></tr>
<tr><td><code>--full</code></td><td>Take a full-page screenshot</td></tr>
</tbody>
</table>
### Output
Reports the diff image path, number of different pixels, and mismatch percentage. The diff image shows unchanged pixels dimmed with changed pixels in red.
If the baseline and current images have different dimensions, the command reports a dimension mismatch instead of attempting pixel comparison.
## URL diff
Compares two pages by navigating to each in sequence and diffing the results.
```bash
# Compare two URLs (snapshot diff)
agent-browser diff url https://staging.example.com https://prod.example.com
# Include visual comparison
agent-browser diff url https://v1.example.com https://v2.example.com --screenshot
# Full-page screenshot comparison
agent-browser diff url https://v1.example.com https://v2.example.com --screenshot --full
```
The command navigates to the first URL, captures state, then navigates to the second URL and captures again. Snapshot diff is always included. Screenshot diff requires the `--screenshot` flag.
After completion, the browser remains on the second URL.
### Options
<table>
<thead>
<tr><th>Flag</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td><code>--screenshot</code></td><td>Also perform visual screenshot comparison</td></tr>
<tr><td><code>--full</code></td><td>Use full-page screenshots</td></tr>
<tr><td><code>--wait-until &lt;strategy&gt;</code></td><td>Navigation wait strategy: <code>load</code>, <code>domcontentloaded</code>, <code>networkidle</code> (default: <code>load</code>)</td></tr>
<tr><td><code>-s, --selector &lt;sel&gt;</code></td><td>Scope snapshots to a CSS selector or @ref</td></tr>
<tr><td><code>-c, --compact</code></td><td>Use compact snapshot format</td></tr>
<tr><td><code>-d, --depth &lt;n&gt;</code></td><td>Limit snapshot tree depth</td></tr>
</tbody>
</table>
## Use cases
### Verifying agent actions
The most common use case: confirm that an action (click, fill, submit) changed the page as expected.
```bash
agent-browser snapshot -i # Take interactive-only snapshot (baseline)
agent-browser fill @e3 "test@example.com"
agent-browser diff snapshot # Compare current snapshot to the baseline
```
### Monitoring for changes
Periodically compare a page against a saved baseline to detect updates.
```bash
# Save baseline
agent-browser open https://example.com && agent-browser snapshot > baseline.txt
# Later, check for changes
agent-browser open https://example.com && agent-browser diff snapshot --baseline baseline.txt
```
### Visual regression testing
Compare screenshots before and after a deploy to catch unintended visual changes.
```bash
agent-browser open https://staging.example.com && agent-browser screenshot baseline.png
# ... deploy happens ...
agent-browser open https://staging.example.com && agent-browser diff screenshot --baseline baseline.png
```
### Comparing environments
Diff staging against production to verify parity.
```bash
agent-browser diff url https://staging.example.com https://prod.example.com --screenshot
```
-7
View File
@@ -1,7 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("engines/chrome");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
-104
View File
@@ -1,104 +0,0 @@
# Chrome
Chrome (and Chromium) is the default browser engine. agent-browser discovers, launches, and manages the Chrome process automatically via the Chrome DevTools Protocol (CDP).
## Binary Discovery
When no `--executable-path` is provided, agent-browser searches for Chrome in this order:
<table>
<thead>
<tr><th>Platform</th><th>Locations checked</th></tr>
</thead>
<tbody>
<tr>
<td>macOS</td>
<td>
<code>/Applications/Google Chrome.app</code>,
<code>/Applications/Google Chrome Canary.app</code>,
<code>/Applications/Chromium.app</code>,
<code>/Applications/Brave Browser.app</code>,
Puppeteer cache (<code>~/.cache/puppeteer/chrome/</code> or <code>PUPPETEER_CACHE_DIR</code>),
Chrome for Testing cache
</td>
</tr>
<tr>
<td>Linux</td>
<td>
<code>google-chrome</code>,
<code>google-chrome-stable</code>,
<code>chromium-browser</code>,
<code>chromium</code> in PATH,
Puppeteer cache (<code>~/.cache/puppeteer/chrome/</code> or <code>PUPPETEER_CACHE_DIR</code>),
Chrome for Testing cache
</td>
</tr>
<tr>
<td>Windows</td>
<td>
<code>%LOCALAPPDATA%\Google\Chrome\Application\chrome.exe</code>,
<code>C:\Program Files\Google\Chrome\Application\chrome.exe</code>,
<code>C:\Program Files (x86)\...\chrome.exe</code>
</td>
</tr>
</tbody>
</table>
If Chrome is not found, run `agent-browser install` to download Chrome from Chrome for Testing.
## Usage
Chrome is the default engine -- no `--engine` flag is needed:
```bash
agent-browser open example.com
```
To be explicit:
```bash
agent-browser --engine chrome open example.com
```
## Custom Binary
Point to any Chromium-based browser with `--executable-path`:
```bash
agent-browser --executable-path /path/to/chromium open example.com
```
Or via environment variable:
```bash
export AGENT_BROWSER_EXECUTABLE_PATH=/path/to/chromium
agent-browser open example.com
```
## Chrome-Specific Features
These features are available only with Chrome:
<table>
<thead>
<tr><th>Feature</th><th>Flag</th></tr>
</thead>
<tbody>
<tr><td>Browser extensions</td><td><code>--extension &lt;path&gt;</code></td></tr>
<tr><td>Persistent profiles</td><td><code>--profile &lt;path&gt;</code> (sets Chrome's <code>--user-data-dir</code>)</td></tr>
<tr><td>Storage state</td><td><code>--state &lt;path&gt;</code></td></tr>
<tr><td>File URL access</td><td><code>--allow-file-access</code></td></tr>
<tr><td>Headed mode</td><td><code>--headed</code></td></tr>
<tr><td>Custom launch args</td><td><code>--args &lt;args&gt;</code></td></tr>
</tbody>
</table>
## Containers and CI
In Docker, CI runners, or other sandboxed environments, Chrome's user namespace sandbox may need to be disabled:
```bash
agent-browser --args "--no-sandbox" open example.com
```
agent-browser automatically adds `--no-sandbox` when it detects a container environment (Docker, Podman, running as root).
@@ -1,7 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("engines/lightpanda");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
-93
View File
@@ -1,93 +0,0 @@
# Lightpanda
[Lightpanda](https://lightpanda.io/) is a headless browser engine built from scratch in Zig for machines. It starts instantly, uses 10x less memory than Chrome, and executes 10x faster.
agent-browser manages Lightpanda the same way it manages Chrome -- spawning the process, connecting via CDP, and shutting it down. All downstream commands (snapshot, click, fill, screenshot, etc.) work through the same CDP protocol path.
## Installation
Install the Lightpanda binary before using it with agent-browser:
<table>
<thead>
<tr><th>Platform</th><th>Command</th></tr>
</thead>
<tbody>
<tr>
<td>macOS (Apple Silicon)</td>
<td><code>curl -L -o lightpanda https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-aarch64-macos && chmod a+x ./lightpanda</code></td>
</tr>
<tr>
<td>Linux (x86_64)</td>
<td><code>curl -L -o lightpanda https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-x86_64-linux && chmod a+x ./lightpanda</code></td>
</tr>
</tbody>
</table>
Move the binary somewhere in your `PATH` (e.g. `/usr/local/bin/lightpanda` or `~/.local/bin/lightpanda`).
See the [Lightpanda installation docs](https://lightpanda.io/docs/open-source/installation) for more options.
## Usage
Use the `--engine` flag to select Lightpanda:
```bash
agent-browser --engine lightpanda open example.com
agent-browser --engine lightpanda snapshot
agent-browser --engine lightpanda screenshot
```
Or set it as the default via environment variable:
```bash
export AGENT_BROWSER_ENGINE=lightpanda
agent-browser open example.com
```
Or in your `agent-browser.json` config:
```json
{
"engine": "lightpanda"
}
```
## Custom Binary Path
If the `lightpanda` binary is not in your `PATH`, use `--executable-path`:
```bash
agent-browser --engine lightpanda --executable-path /path/to/lightpanda open example.com
```
## Differences from Chrome
Lightpanda is a purpose-built headless engine. Some Chrome-specific features are not available:
<table>
<thead>
<tr><th>Feature</th><th>Status</th></tr>
</thead>
<tbody>
<tr><td>Extensions (<code>--extension</code>)</td><td>Not supported</td></tr>
<tr><td>Persistent profiles (<code>--profile</code>)</td><td>Not supported</td></tr>
<tr><td>Storage state (<code>--state</code>)</td><td>Not supported</td></tr>
<tr><td>File access (<code>--allow-file-access</code>)</td><td>Not supported</td></tr>
<tr><td>Headed mode (<code>--headed</code>)</td><td>Not applicable (headless only)</td></tr>
<tr><td>Screenshots</td><td>Depends on Lightpanda CDP support</td></tr>
</tbody>
</table>
agent-browser returns a clear error if you combine `--engine lightpanda` with unsupported flags.
## When to Use Lightpanda
Lightpanda is a good fit for:
- Fast web scraping and data extraction
- AI agent workflows where speed and low memory matter
- CI/CD environments with constrained resources
- High-volume parallel automation
Use Chrome when you need full browser fidelity, extensions, or persistent profiles.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

-374
View File
@@ -1,374 +0,0 @@
@import "tailwindcss";
@plugin "tailwindcss-animate";
@source "../../node_modules/streamdown/dist/index.js";
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, sans-serif;
--font-mono: var(--font-geist-mono), ui-monospace, "SF Mono", "Cascadia Mono", "Segoe UI Mono", Menlo, Consolas, monospace;
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-border: var(--border);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-sidebar: var(--sidebar);
}
:root {
--background: #fff;
--foreground: #171717;
--border: #e5e5e5;
--muted: #f5f5f5;
--muted-foreground: #737373;
--primary: #171717;
--primary-foreground: #fff;
--sidebar: #f5f5f5;
}
.dark {
--background: #0a0a0a;
--foreground: #f5f5f5;
--border: #262626;
--muted: #262626;
--muted-foreground: #a3a3a3;
--primary: #f5f5f5;
--primary-foreground: #0a0a0a;
--sidebar: #171717;
}
html {
scroll-padding-top: 4rem;
}
::selection {
background-color: #000;
color: #fff;
}
@media (prefers-color-scheme: dark) {
::selection {
background-color: #fff;
color: #000;
}
}
/* Article tables */
article table {
width: 100%;
font-size: 0.875rem;
margin-bottom: 1rem;
border-collapse: collapse;
}
article th {
border-bottom: 1px solid #e5e5e5;
padding: 0.5rem 0.75rem;
text-align: left;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #737373;
}
article td {
border-bottom: 1px solid #f5f5f5;
padding: 0.5rem 0.75rem;
color: #525252;
}
:is(.dark) article th {
border-bottom-color: #262626;
color: #a3a3a3;
}
:is(.dark) article td {
border-bottom-color: rgba(38, 38, 38, 0.5);
color: #a3a3a3;
}
button {
cursor: pointer;
}
/* Code blocks */
pre {
border: 1px solid var(--border);
border-radius: 4px;
padding: 0.875rem;
overflow-x: auto;
font-size: 0.8125rem;
line-height: 1.7;
}
pre:not(.shiki) {
background: var(--muted);
}
.code-block pre {
margin: 0;
}
.code-block {
margin-bottom: 1.25rem;
}
@media (max-width: 640px) {
pre {
font-size: 0.75rem;
padding: 0.75rem;
}
}
:not(pre) > code {
background: var(--muted);
padding: 0.125rem 0.375rem;
border-radius: 3px;
font-size: 0.875em;
}
/* Diff line highlighting */
.diff-add {
color: #00952d;
}
.diff-remove {
color: #f32e40;
}
.dark .diff-add {
color: #00ca50;
}
.dark .diff-remove {
color: #f32e40;
}
/* Shiki dual theme support */
.shiki,
.shiki span {
color: var(--shiki-light) !important;
background-color: var(--shiki-light-bg) !important;
}
.dark .shiki,
.dark .shiki span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
}
/* Prose */
.prose {
max-width: 100%;
}
.prose h1 {
font-size: 1.5rem;
font-weight: 600;
letter-spacing: -0.02em;
margin-bottom: 1.5rem;
color: var(--foreground);
}
@media (min-width: 640px) {
.prose h1 {
font-size: 1.75rem;
}
}
.prose h2 {
font-size: 1.125rem;
font-weight: 600;
margin-top: 3rem;
margin-bottom: 1rem;
color: var(--foreground);
}
.prose h2:first-child {
margin-top: 0;
}
.prose h3 {
font-size: 1rem;
font-weight: 600;
margin-top: 2rem;
margin-bottom: 0.75rem;
color: var(--foreground);
}
.prose .heading-anchor {
position: relative;
}
.prose .heading-anchor > a {
opacity: 0;
margin-left: 0.375rem;
color: var(--muted-foreground);
text-decoration: none;
font-weight: 400;
transition: opacity 0.15s ease;
}
.prose .heading-anchor:hover > a {
opacity: 1;
}
.prose .heading-anchor > a:hover {
color: var(--foreground);
}
.prose p {
margin-bottom: 1rem;
line-height: 1.65;
color: #525252;
font-size: 0.875rem;
}
:is(.dark) .prose p {
color: #a3a3a3;
}
.prose ul, .prose ol {
margin-bottom: 1rem;
padding-left: 1.25rem;
}
.prose ul {
list-style-type: disc;
}
.prose ol {
list-style-type: decimal;
}
.prose li {
margin-bottom: 0.25rem;
color: #525252;
font-size: 0.875rem;
line-height: 1.6;
}
:is(.dark) .prose li {
color: #a3a3a3;
}
.prose li strong {
color: var(--foreground);
font-weight: 500;
}
.prose a {
color: var(--foreground);
text-decoration: underline;
text-decoration-color: #d4d4d4;
text-underline-offset: 2px;
}
.prose a:hover {
text-decoration-color: var(--foreground);
}
:is(.dark) .prose a {
text-decoration-color: #525252;
}
:is(.dark) .prose a:hover {
text-decoration-color: var(--foreground);
}
.prose strong {
font-weight: 500;
color: var(--foreground);
}
.prose blockquote {
margin-bottom: 1rem;
border-left: 2px solid #e5e5e5;
padding-left: 1rem;
font-size: 0.875rem;
color: #737373;
}
:is(.dark) .prose blockquote {
border-left-color: #525252;
color: #a3a3a3;
}
.prose table {
width: 100%;
border-collapse: collapse;
margin: 1.5rem 0;
font-size: 0.8125rem;
}
.prose th, .prose td {
text-align: left;
padding: 0.625rem 0.875rem;
border-bottom: 1px solid var(--border);
}
.prose th {
font-weight: 500;
color: var(--muted-foreground);
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.025em;
}
.prose td {
color: var(--muted-foreground);
}
.prose td code {
color: var(--foreground);
}
/* Tool call shimmer animation */
@keyframes tool-shimmer {
0% { opacity: 0.5; }
50% { opacity: 1; }
100% { opacity: 0.5; }
}
.animate-tool-shimmer {
animation: tool-shimmer 1.5s ease-in-out infinite;
}
/* Override prose text color in chat so agent responses use primary foreground */
.docs-chat-content p,
.docs-chat-content li,
.docs-chat-content td,
.docs-chat-content th,
.docs-chat-content strong,
.docs-chat-content code {
color: var(--foreground);
}
/* Reset global pre styles inside chat so Streamdown's own styling takes effect */
.docs-chat-content pre {
border: none;
border-radius: 0;
padding: revert-layer;
}
/* Fix list rendering in chat content */
.docs-chat-content ul,
.docs-chat-content ol {
list-style-position: outside;
padding-left: 1.25em;
}
.docs-chat-content li > p {
display: inline;
margin: 0;
}
.docs-chat-content li {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
-7
View File
@@ -1,7 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("installation");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
-173
View File
@@ -1,173 +0,0 @@
# Installation
## Global installation (recommended)
Installs the native Rust binary for maximum performance:
```bash
npm install -g agent-browser
agent-browser install # Download Chrome from Chrome for Testing (first time)
```
This is the fastest option -- commands run through the native Rust CLI directly with sub-millisecond parsing overhead.
## Quick start (no install)
```bash
npx agent-browser install # Download Chrome (first time only)
npx agent-browser open example.com
```
## Project installation (local dependency)
For projects that want to pin the version in `package.json`:
```bash
npm install agent-browser
npx agent-browser install # Download Chrome (first time)
```
Then use via `npx` or `package.json` scripts.
## Homebrew (macOS)
```bash
brew install agent-browser
agent-browser install # Download Chrome (first time)
```
## Cargo (Rust)
```bash
cargo install agent-browser
agent-browser install # Download Chrome (first time)
```
Compiles from source (~2-3 min). Requires the Rust toolchain ([rustup.rs](https://rustup.rs)).
## From source
```bash
git clone https://github.com/vercel-labs/agent-browser
cd agent-browser
pnpm install
pnpm build
pnpm build:native
./bin/agent-browser install
pnpm link --global
```
## Linux dependencies
On Linux, install system dependencies:
```bash
agent-browser install --with-deps
```
## Updating
Upgrade to the latest version:
```bash
agent-browser upgrade
```
Detects your installation method (npm, Homebrew, or Cargo) and runs the appropriate update command automatically. Displays the version change on success, or informs you if you are already on the latest version.
## Doctor
`doctor` diagnoses your install and auto-cleans stale daemon files. Run it whenever something stops working unexpectedly, or after upgrades:
```bash
agent-browser doctor # Full diagnosis
agent-browser doctor --offline --quick # Local-only, fastest (~<1s)
agent-browser doctor --fix # Also run destructive repairs
agent-browser doctor --json # Structured output
```
It checks:
<table>
<thead>
<tr><th>Category</th><th>What it checks</th></tr>
</thead>
<tbody>
<tr><td>Environment</td><td>CLI version, platform, home directory, state and socket dirs, free disk space</td></tr>
<tr><td>Chrome</td><td>Chrome install path and version, cache dir, Puppeteer fallback, user-data dir and profile count, optional <code>lightpanda</code> engine</td></tr>
<tr><td>Daemons</td><td>Running daemons per session, stale <code>.sock</code> / <code>.pid</code> / <code>.version</code> / <code>.stream</code> files (auto-cleaned), version mismatch with the CLI, dashboard process liveness</td></tr>
<tr><td>Config</td><td><code>~/.agent-browser/config.json</code>, <code>./agent-browser.json</code>, and any file at <code>AGENT_BROWSER_CONFIG</code> parse as valid JSON</td></tr>
<tr><td>Security</td><td>Encryption key env var or <code>~/.agent-browser/.encryption-key</code> (with 0600 permissions on unix), state file count and age vs <code>AGENT_BROWSER_STATE_EXPIRE_DAYS</code>, action policy file</td></tr>
<tr><td>Providers</td><td>Env vars for Browserless, Browserbase, Browser Use, Kernel, AgentCore (AWS creds), Appium (for <code>--provider ios</code>), and <code>AI_GATEWAY_API_KEY</code> for chat</td></tr>
<tr><td>Network</td><td>Reachability of the Chrome for Testing CDN, AI Gateway (if configured), and any currently selected provider endpoint (skipped under <code>--offline</code>)</td></tr>
<tr><td>Launch test</td><td>Spawns a scratch session, launches headless Chrome, navigates to <code>about:blank</code>, then closes. Measures wall time (skipped under <code>--quick</code>)</td></tr>
</tbody>
</table>
Stale sidecar files are always cleaned. Destructive actions are opt-in via `--fix`:
<table>
<thead>
<tr><th>Check</th><th>What <code>--fix</code> does</th></tr>
</thead>
<tbody>
<tr><td>Chrome missing</td><td>Runs <code>agent-browser install</code></td></tr>
<tr><td>Version-mismatched daemons</td><td>Sends <code>close</code> to each and cleans files</td></tr>
<tr><td>Old state files</td><td>Deletes state files older than <code>AGENT_BROWSER_STATE_EXPIRE_DAYS</code> (default 30)</td></tr>
<tr><td>Missing encryption key</td><td>Generates a new key at <code>~/.agent-browser/.encryption-key</code> (0600, unix); never overwrites an existing key</td></tr>
</tbody>
</table>
Exit code is `0` if all checks pass (warnings are fine), `1` if any fail.
## Custom browser
Use a custom browser executable instead of bundled Chromium:
- **Serverless** - Use `@sparticuz/chromium` (~50MB vs ~684MB)
- **System browser** - Use existing Chrome installation
- **Custom builds** - Use modified browser builds
```bash
# Via flag
agent-browser --executable-path /path/to/chromium open example.com
# Via environment variable
AGENT_BROWSER_EXECUTABLE_PATH=/path/to/chromium agent-browser open example.com
```
### Serverless example
Use `@sparticuz/chromium` or similar to obtain a Chromium executable path, then pass it via `--executable-path` or `AGENT_BROWSER_EXECUTABLE_PATH`.
## AI agent setup
agent-browser works with any AI agent out of the box. For richer context:
### AI coding assistants (recommended)
Install the skill for your AI coding assistant:
```bash
npx skills add vercel-labs/agent-browser
```
This works with Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, Goose, OpenCode, and Windsurf. The skill is fetched from the repository and stays up to date automatically.
> **Do not** copy `SKILL.md` from `node_modules` -- it will become stale as new features are added. Always use `npx skills add` or reference the repository version.
### AGENTS.md / CLAUDE.md
Add to your instructions file:
```markdown
## Browser Automation
Use `agent-browser` for web automation. Run `agent-browser --help` for all commands.
Core workflow:
1. `agent-browser open <url>` - Navigate to page
2. `agent-browser snapshot -i` - Get interactive elements with refs (@e1, @e2)
3. `agent-browser click @e1` / `fill @e2 "text"` - Interact using refs
4. Re-snapshot after page changes
```
-7
View File
@@ -1,7 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("ios");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
-207
View File
@@ -1,207 +0,0 @@
# iOS Simulator
Control real Mobile Safari in the iOS Simulator for authentic mobile
web testing. Uses Appium with XCUITest for native automation.
## Requirements
- macOS with Xcode installed
- iOS Simulator runtimes (download via Xcode)
- Appium with XCUITest driver
## Setup
```bash
# Install Appium globally
npm install -g appium
# Install the XCUITest driver for iOS
appium driver install xcuitest
```
## List available devices
See all iOS simulators available on your system:
```bash
agent-browser device list
# Output:
# Available iOS Simulators:
#
# ○ iPhone 16 Pro (iOS 18.0)
# F21EEC0D-7618-419F-811B-33AF27A8B2FD
# ○ iPhone 16 Pro Max (iOS 18.0)
# 50402807-C9B8-4D37-9F13-2E00E782C744
# ○ iPad Pro 13-inch (M4) (iOS 18.0)
# 3A6C6436-B909-4593-866D-91D1062BB070
# ...
```
## Basic usage
Use the `-p ios` flag to enable iOS mode. The workflow is
identical to desktop:
```bash
# Launch Safari on iPhone 16 Pro
agent-browser -p ios --device "iPhone 16 Pro" open https://example.com
# Get snapshot with refs (same as desktop)
agent-browser -p ios snapshot -i
# Interact using refs
agent-browser -p ios tap @e1
agent-browser -p ios fill @e2 "text"
# Take screenshot
agent-browser -p ios screenshot mobile.png
# Close session (shuts down simulator)
agent-browser -p ios close
```
## Mobile-specific commands
```bash
# Swipe gestures
agent-browser -p ios swipe up
agent-browser -p ios swipe down
agent-browser -p ios swipe left
agent-browser -p ios swipe right
# Swipe with distance (pixels)
agent-browser -p ios swipe up 500
# Tap (alias for click, semantically clearer for touch)
agent-browser -p ios tap @e1
```
## Environment variables
Configure iOS mode via environment variables:
```bash
export AGENT_BROWSER_PROVIDER=ios
export AGENT_BROWSER_IOS_DEVICE="iPhone 16 Pro"
# Now all commands use iOS
agent-browser open https://example.com
agent-browser snapshot -i
agent-browser tap @e1
```
<table>
<thead>
<tr><th>Variable</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td><code>AGENT_BROWSER_PROVIDER</code></td><td>Set to <code>ios</code> to enable iOS mode</td></tr>
<tr><td><code>AGENT_BROWSER_IOS_DEVICE</code></td><td>Device name (e.g., "iPhone 16 Pro")</td></tr>
<tr><td><code>AGENT_BROWSER_IOS_UDID</code></td><td>Device UDID (alternative to device name)</td></tr>
</tbody>
</table>
## Supported devices
All iOS Simulators available in Xcode are supported, including:
- All iPhone models (iPhone 15, 16, 17, SE, etc.)
- All iPad models (iPad Pro, iPad Air, iPad mini, etc.)
- Multiple iOS versions (17.x, 18.x, etc.)
**Real devices** are also supported via USB connection (see below).
## Real device support
Appium can control Safari on real iOS devices connected via USB. This
requires additional one-time setup.
### 1. Get your device UDID
```bash
# List connected devices
xcrun xctrace list devices
# Or via system profiler
system_profiler SPUSBDataType | grep -A 5 "iPhone\|iPad"
```
### 2. Sign WebDriverAgent (one-time)
WebDriverAgent needs to be signed with your Apple Developer
certificate to run on real devices.
```bash
# Open the WebDriverAgent Xcode project
cd ~/.appium/node_modules/appium-xcuitest-driver/node_modules/appium-webdriveragent
open WebDriverAgent.xcodeproj
```
In Xcode:
1. Select the `WebDriverAgentRunner` target
2. Go to Signing & Capabilities
3. Select your Team (requires Apple Developer account, free tier works)
4. Let Xcode manage signing automatically
### 3. Use with agent-browser
```bash
# Connect device via USB, then use the UDID
agent-browser -p ios --device "<DEVICE_UDID>" open https://example.com
# Or use the device name if unique
agent-browser -p ios --device "John's iPhone" open https://example.com
```
### Real device notes
- First run installs WebDriverAgent to the device (may require Trust prompt on device)
- Device must be unlocked and connected via USB
- Slightly slower initial connection than simulator
- Tests against real Safari performance and behavior
- On first install, go to Settings → General → VPN & Device Management to trust the developer certificate
## Performance notes
- **First launch:** Takes 30-60 seconds to boot the simulator and start Appium
- **Subsequent commands:** Fast (simulator stays running)
- **Close command:** Shuts down simulator and Appium server
## Differences from desktop
<table>
<thead>
<tr><th>Feature</th><th>Desktop</th><th>iOS</th></tr>
</thead>
<tbody>
<tr><td>Browser</td><td>Chrome, Lightpanda</td><td>Safari only</td></tr>
<tr><td>Tabs</td><td>Supported</td><td>Single tab only</td></tr>
<tr><td>PDF export</td><td>Supported</td><td>Not supported</td></tr>
<tr><td>Screencast</td><td>Supported</td><td>Not supported</td></tr>
<tr><td>Swipe gestures</td><td>Not native</td><td>Native support</td></tr>
</tbody>
</table>
## Troubleshooting
### Appium not found
```bash
# Make sure Appium is installed globally
npm install -g appium
appium driver install xcuitest
# Verify installation
appium --version
```
### No simulators available
Open Xcode and download iOS Simulator runtimes from **Settings → Platforms**.
### Simulator won't boot
Try booting the simulator manually from Xcode or the Simulator app to
ensure it works, then retry with agent-browser.
-93
View File
@@ -1,93 +0,0 @@
import type { Metadata } from "next";
import { Inter, Geist_Mono } from "next/font/google";
import { GeistPixelSquare } from "geist/font/pixel";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
import { Header } from "@/components/header";
import { DocsSidebar } from "@/components/docs-sidebar";
import { DocsMobileNav } from "@/components/docs-mobile-nav";
import { CopyPageButton } from "@/components/copy-page-button";
import { DocsChat } from "@/components/docs-chat";
import { cookies } from "next/headers";
import { SpeedInsights } from "@vercel/speed-insights/next";
import { Analytics } from "@vercel/analytics/next";
const inter = Inter({
variable: "--font-inter",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
metadataBase: new URL("https://agent-browser.dev"),
title: {
default: "agent-browser | Browser Automation for AI",
template: "%s | agent-browser",
},
description: "Browser automation CLI for AI agents",
openGraph: {
type: "website",
locale: "en_US",
url: "https://agent-browser.dev",
siteName: "agent-browser",
title: "agent-browser | Browser Automation for AI",
description: "Browser automation CLI for AI agents",
images: [{ url: "/og", width: 1200, height: 630, alt: "agent-browser" }],
},
twitter: {
card: "summary_large_image",
title: "agent-browser | Browser Automation for AI",
description: "Browser automation CLI for AI agents",
images: ["/og"],
},
};
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const cookieStore = await cookies();
const chatOpen = cookieStore.get("docs-chat-open")?.value === "true";
const chatWidth = Number(cookieStore.get("docs-chat-width")?.value) || 400;
return (
<html lang="en" suppressHydrationWarning>
<head>
{chatOpen && (
<style
dangerouslySetInnerHTML={{
__html: `@media(min-width:640px){body{padding-right:${chatWidth}px}}`,
}}
/>
)}
</head>
<body
className={`${inter.variable} ${geistMono.variable} ${GeistPixelSquare.variable} bg-white text-neutral-900 antialiased dark:bg-neutral-950 dark:text-neutral-100`}
>
<ThemeProvider>
<Header />
<DocsMobileNav />
<div className="max-w-5xl mx-auto px-6 py-8 lg:py-12 flex gap-16">
<aside className="w-48 shrink-0 hidden lg:block sticky top-28 h-[calc(100vh-7rem)] overflow-y-auto">
<DocsSidebar />
</aside>
<div className="flex-1 min-w-0 max-w-2xl pb-20">
<div className="flex justify-end mb-4">
<CopyPageButton />
</div>
<article className="prose">{children}</article>
</div>
</div>
<DocsChat defaultOpen={chatOpen} defaultWidth={chatWidth} />
</ThemeProvider>
<SpeedInsights />
<Analytics />
</body>
</html>
);
}
-7
View File
@@ -1,7 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("native-mode");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
-5
View File
@@ -1,5 +0,0 @@
# Native Mode
agent-browser is now 100% native Rust by default. The Node.js/Playwright daemon has been removed.
This page is no longer relevant. See the main [documentation](/) for current architecture and usage.
-7
View File
@@ -1,7 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("next");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
-207
View File
@@ -1,207 +0,0 @@
# Next.js + Vercel
Run agent-browser from a Next.js app on Vercel using Vercel Sandbox.
A Linux microVM spins up on demand, runs agent-browser + Chrome, and
shuts down. No binary size limits, no Chromium bundling complexity.
## Setup
```bash
pnpm add @vercel/sandbox
```
## Server action
The Vercel Sandbox runs Amazon Linux. Chromium requires system libraries
that are not installed by default, so fresh sandboxes need a `dnf install`
step before agent-browser can launch Chrome. Use a sandbox snapshot
(below) to skip this entirely in production.
```ts
"use server";
import { Sandbox } from "@vercel/sandbox";
const snapshotId = process.env.AGENT_BROWSER_SNAPSHOT_ID;
const CHROMIUM_SYSTEM_DEPS = [
"nss", "nspr", "libxkbcommon", "atk", "at-spi2-atk", "at-spi2-core",
"libXcomposite", "libXdamage", "libXrandr", "libXfixes", "libXcursor",
"libXi", "libXtst", "libXScrnSaver", "libXext", "mesa-libgbm", "libdrm",
"mesa-libGL", "mesa-libEGL", "cups-libs", "alsa-lib", "pango", "cairo",
"gtk3", "dbus-libs",
];
function getSandboxCredentials() {
if (
process.env.VERCEL_TOKEN &&
process.env.VERCEL_TEAM_ID &&
process.env.VERCEL_PROJECT_ID
) {
return {
token: process.env.VERCEL_TOKEN,
teamId: process.env.VERCEL_TEAM_ID,
projectId: process.env.VERCEL_PROJECT_ID,
};
}
return {};
}
async function withBrowser<T>(
fn: (sandbox: InstanceType<typeof Sandbox>) => Promise<T>,
): Promise<T> {
const credentials = getSandboxCredentials();
const sandbox = snapshotId
? await Sandbox.create({
...credentials,
source: { type: "snapshot", snapshotId },
timeout: 120_000,
})
: await Sandbox.create({ ...credentials, runtime: "node24", timeout: 120_000 });
if (!snapshotId) {
await sandbox.runCommand("sh", [
"-c",
`sudo dnf clean all 2>&1 && sudo dnf install -y --skip-broken ${CHROMIUM_SYSTEM_DEPS.join(" ")} 2>&1 && sudo ldconfig 2>&1`,
]);
await sandbox.runCommand("npm", ["install", "-g", "agent-browser"]);
await sandbox.runCommand("npx", ["agent-browser", "install"]);
}
try {
return await fn(sandbox);
} finally {
await sandbox.stop();
}
}
export async function screenshotUrl(url: string) {
return withBrowser(async (sandbox) => {
await sandbox.runCommand("agent-browser", ["open", url]);
const ssResult = await sandbox.runCommand("agent-browser", [
"screenshot", "--json",
]);
const ssPath = JSON.parse(await ssResult.stdout())?.data?.path;
const b64Result = await sandbox.runCommand("base64", ["-w", "0", ssPath]);
const screenshot = (await b64Result.stdout()).trim();
await sandbox.runCommand("agent-browser", ["close"]);
return { ok: true, screenshot };
});
}
export async function snapshotUrl(url: string) {
return withBrowser(async (sandbox) => {
await sandbox.runCommand("agent-browser", ["open", url]);
const result = await sandbox.runCommand("agent-browser", [
"snapshot", "-i", "-c",
]);
const snapshot = await result.stdout();
await sandbox.runCommand("agent-browser", ["close"]);
return { ok: true, snapshot };
});
}
```
## Sandbox snapshots
Without optimization, each Sandbox run installs system dependencies +
agent-browser + Chromium from scratch (~30 seconds). A **sandbox snapshot**
is a saved VM image with everything pre-installed -- like a Docker image
for Vercel Sandbox. When `AGENT_BROWSER_SNAPSHOT_ID` is set, the sandbox
boots from that image instead of installing, bringing startup down to
sub-second.
This is different from an agent-browser *accessibility snapshot* (which
dumps a page's accessibility tree). A sandbox snapshot is a Vercel
infrastructure concept.
Create a sandbox snapshot by running the helper script once:
```bash
npx tsx scripts/create-snapshot.ts
```
The script spins up a fresh sandbox, installs system dependencies +
agent-browser + Chromium, saves the VM state, and prints the snapshot ID:
```
AGENT_BROWSER_SNAPSHOT_ID=snap_xxxxxxxxxxxx
```
Add this to your Vercel project environment variables (or `.env.local`
for local development). Recommended for any production deployment.
## Authentication
On Vercel deployments, the Sandbox SDK authenticates automatically via
OIDC. For local development, provide explicit credentials:
<table>
<thead>
<tr><th>Variable</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td><code>VERCEL_TOKEN</code></td><td>Vercel personal access token</td></tr>
<tr><td><code>VERCEL_TEAM_ID</code></td><td>Vercel team ID</td></tr>
<tr><td><code>VERCEL_PROJECT_ID</code></td><td>Vercel project ID</td></tr>
</tbody>
</table>
When all three are set, they are passed to `Sandbox.create()`. When
absent, the SDK falls back to `VERCEL_OIDC_TOKEN` (automatic on Vercel).
## Scheduled workflows (cron)
For recurring tasks like daily monitoring, use Vercel Cron Jobs:
```ts
// app/api/cron/monitor/route.ts
export async function GET() {
const result = await withBrowser(async (sandbox) => {
await sandbox.runCommand("agent-browser", [
"open", "https://example.com/pricing",
]);
const snap = await sandbox.runCommand("agent-browser", [
"snapshot", "-i", "-c",
]);
await sandbox.runCommand("agent-browser", ["close"]);
return await snap.stdout();
});
// Process results, send alerts, store data...
return Response.json({ ok: true, snapshot: result });
}
```
```json
// vercel.json
{
"crons": [
{ "path": "/api/cron/monitor", "schedule": "0 9 * * *" }
]
}
```
## Environment variables
<table>
<thead>
<tr><th>Variable</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td><code>AGENT_BROWSER_SNAPSHOT_ID</code></td><td>Sandbox snapshot ID for sub-second startup (see above)</td></tr>
<tr><td><code>VERCEL_TOKEN</code></td><td>Vercel personal access token (for local dev; OIDC is automatic on Vercel)</td></tr>
<tr><td><code>VERCEL_TEAM_ID</code></td><td>Vercel team ID (for local dev)</td></tr>
<tr><td><code>VERCEL_PROJECT_ID</code></td><td>Vercel project ID (for local dev)</td></tr>
</tbody>
</table>
## Demo app
A working demo with streaming progress UI, rate limiting, and a
deploy-to-Vercel button is at
[`examples/environments/`](https://github.com/vercel-labs/agent-browser/tree/main/examples/environments).
-16
View File
@@ -1,16 +0,0 @@
import { NextResponse } from "next/server";
import { getPageTitle, renderOgImage } from "../og-image";
export async function GET(
_request: Request,
{ params }: { params: Promise<{ slug: string[] }> },
) {
const { slug } = await params;
const title = getPageTitle(slug.join("/"));
if (!title) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
return renderOgImage(title);
}
-112
View File
@@ -1,112 +0,0 @@
import { ImageResponse } from "next/og";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
export { getPageTitle } from "@/lib/page-titles";
let fontCache: { geistRegular: Buffer; geistPixelSquare: Buffer } | null =
null;
async function loadFonts() {
if (fontCache) return fontCache;
const [geistRegular, geistPixelSquare] = await Promise.all([
readFile(join(process.cwd(), "public/Geist-Regular.ttf")),
readFile(join(process.cwd(), "public/GeistPixel-Square.ttf")),
]);
fontCache = { geistRegular, geistPixelSquare };
return fontCache;
}
export async function renderOgImage(title: string) {
const { geistRegular, geistPixelSquare } = await loadFonts();
return new ImageResponse(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
backgroundColor: "black",
padding: "60px 80px",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: "16px",
}}
>
<svg width="36" height="36" viewBox="0 0 16 16" fill="white">
<path fillRule="evenodd" clipRule="evenodd" d="M8 1L16 15H0L8 1Z" />
</svg>
<span
style={{
fontSize: 36,
color: "#666",
fontFamily: "Geist",
fontWeight: 400,
}}
>
/
</span>
<span
style={{
fontSize: 36,
fontFamily: "GeistPixelSquare",
fontWeight: 400,
color: "white",
}}
>
agent-browser
</span>
</div>
<div
style={{
display: "flex",
flex: 1,
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}
>
{title.split("\n").map((line, i) => (
<span
key={i}
style={{
fontSize: 72,
fontFamily: "Geist",
fontWeight: 400,
color: "white",
letterSpacing: "-0.02em",
textAlign: "center",
lineHeight: 1.2,
}}
>
{line}
</span>
))}
</div>
</div>,
{
width: 1200,
height: 630,
fonts: [
{
name: "Geist",
data: geistRegular.buffer as ArrayBuffer,
style: "normal",
weight: 400,
},
{
name: "GeistPixelSquare",
data: geistPixelSquare.buffer as ArrayBuffer,
style: "normal",
weight: 400,
},
],
},
);
}
-6
View File
@@ -1,6 +0,0 @@
import { getPageTitle, renderOgImage } from "./og-image";
export async function GET() {
const title = getPageTitle("")!;
return renderOgImage(title);
}
-65
View File
@@ -1,65 +0,0 @@
# agent-browser
Browser automation CLI designed for AI agents. Compact text output minimizes context usage. 100% native Rust.
```bash
npm install -g agent-browser # all platforms
brew install agent-browser # macOS
agent-browser install # Download Chrome (first time)
# or try without installing
npx agent-browser open example.com
```
## Features
- **Agent-first** - Compact text output uses fewer tokens than JSON, designed for AI context efficiency
- **Ref-based** - Snapshot returns accessibility tree with refs for deterministic element selection
- **Fast** - Native Rust CLI for instant command parsing
- **Complete** - 50+ commands for navigation, forms, screenshots, network, storage
- **Sessions** - Multiple isolated browser instances with separate auth
- **Cross-platform** - macOS, Linux, Windows with native binaries
## Works with
Claude Code, Cursor, GitHub Copilot, OpenAI Codex, Google Gemini, opencode, and any agent that can run shell commands.
## Example
```bash
# Navigate and get snapshot
agent-browser open example.com
agent-browser snapshot -i
# Output:
# - heading "Example Domain" [ref=e1]
# - link "More information..." [ref=e2]
# Interact using refs
agent-browser click @e2
agent-browser screenshot page.png
agent-browser close
```
## Why refs?
The `snapshot` command returns a compact accessibility tree where each element
has a unique ref like `@e1`, `@e2`. This provides:
- **Context-efficient** - Text output uses ~200-400 tokens vs ~3000-5000 for full DOM
- **Deterministic** - Ref points to exact element from snapshot
- **Fast** - No DOM re-query needed
- **AI-friendly** - LLMs parse text output naturally
## Architecture
Client-daemon architecture for optimal performance:
1. **Rust CLI** - Parses commands, communicates with daemon
2. **Native Daemon** - Pure Rust daemon using direct CDP, manages Chrome via Chrome DevTools Protocol
Daemon starts automatically and persists between commands.
## Platforms
Native Rust binaries for macOS (ARM64, x64), Linux (ARM64, x64), and Windows (x64).
-7
View File
@@ -1,7 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("profiler");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
-110
View File
@@ -1,110 +0,0 @@
# Profiler
Capture Chrome DevTools performance profiles during browser automation.
Use profiles to diagnose slow page loads, expensive JavaScript, layout thrashing,
and other performance bottlenecks in agentic workflows.
## Basic usage
```bash
# Start profiling
agent-browser profiler start
# Perform actions
agent-browser navigate https://example.com
agent-browser click "#button"
# Stop and save profile
agent-browser profiler stop ./trace.json
```
The output JSON file can be loaded into Chrome DevTools, Perfetto UI, or any
tool that accepts Chrome Trace Event format.
## Commands
<table>
<thead>
<tr><th>Command</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td><code>profiler start</code></td><td>Start recording a performance profile</td></tr>
<tr><td><code>profiler start --categories &lt;list&gt;</code></td><td>Start with custom trace categories</td></tr>
<tr><td><code>profiler stop [path]</code></td><td>Stop profiling and save to file</td></tr>
</tbody>
</table>
## Trace categories
The `--categories` flag accepts a comma-separated list of Chrome trace categories.
```bash
agent-browser profiler start --categories "devtools.timeline,v8.execute,blink.user_timing"
```
Default categories include `devtools.timeline`, `v8.execute`, `blink`,
`blink.user_timing`, `latencyInfo`, `renderer.scheduler`, `toplevel`, and
several `disabled-by-default-*` categories for detailed CPU profiling and
call stack analysis.
### Common categories
<table>
<thead>
<tr><th>Category</th><th>What it captures</th></tr>
</thead>
<tbody>
<tr><td><code>devtools.timeline</code></td><td>Standard DevTools performance events</td></tr>
<tr><td><code>v8.execute</code></td><td>Time spent running JavaScript</td></tr>
<tr><td><code>blink</code></td><td>Renderer events (layout, paint, style)</td></tr>
<tr><td><code>blink.user_timing</code></td><td><code>performance.mark()</code> and <code>performance.measure()</code> calls</td></tr>
<tr><td><code>latencyInfo</code></td><td>Input-to-display latency</td></tr>
<tr><td><code>disabled-by-default-v8.cpu_profiler</code></td><td>Sampling-based JS CPU profiling</td></tr>
</tbody>
</table>
## Output format
The output is a JSON file in Chrome Trace Event format:
```json
{
"traceEvents": [
{
"cat": "devtools.timeline",
"name": "RunTask",
"ph": "X",
"ts": 12345,
"dur": 100,
"pid": 1,
"tid": 1
}
],
"metadata": {
"clock-domain": "LINUX_CLOCK_MONOTONIC"
}
}
```
The `metadata.clock-domain` field reflects the host platform (Linux or macOS).
On Windows it is omitted.
## Viewing profiles
- **Chrome DevTools** -- Performance panel > Load profile
- **Perfetto** -- https://ui.perfetto.dev/ (drag and drop the JSON file)
- **Trace Viewer** -- `chrome://tracing` in any Chromium browser
## Use cases
- **Page load analysis** -- Profile navigation to identify slow resources, long tasks, or layout shifts
- **Interaction profiling** -- Measure the cost of clicks, form fills, and other user interactions
- **CI regression checks** -- Capture profiles per build and compare trace data over time
- **Agent workflow optimization** -- Find which steps in an agentic flow are most expensive
## Limitations
- Only works with Chromium-based browsers (Chrome, Edge). Not supported on Firefox or WebKit.
- Trace data accumulates in memory while profiling is active (capped at 5 million events). Stop profiling promptly after the area of interest.
- Data collection on stop has a 30-second timeout. If the browser is unresponsive, the stop command may fail.
- When no output path is provided, the profile is saved to an auto-generated path under the agent-browser temp directory.
@@ -1,7 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("providers/agentcore");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
-89
View File
@@ -1,89 +0,0 @@
# AgentCore
[AWS Bedrock AgentCore](https://aws.amazon.com/bedrock/agentcore/) provides cloud browser sessions with SigV4 authentication. Use it when running agent-browser in AWS environments or when you need managed cloud browsers backed by AWS infrastructure.
## Setup
Credentials are automatically resolved from:
1. Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`)
2. AWS CLI (`aws configure export-credentials`) which supports SSO, profiles, IAM roles, etc.
```bash
agent-browser -p agentcore open https://example.com
```
Or use environment variables for CI/scripts:
```bash
export AGENT_BROWSER_PROVIDER=agentcore
agent-browser open https://example.com
```
The `-p` flag takes precedence over `AGENT_BROWSER_PROVIDER`.
## Configuration
<table>
<thead>
<tr><th>Variable</th><th>Description</th><th>Default</th></tr>
</thead>
<tbody>
<tr><td><code>AGENTCORE_REGION</code></td><td>AWS region for the AgentCore endpoint</td><td><code>us-east-1</code></td></tr>
<tr><td><code>AGENTCORE_BROWSER_ID</code></td><td>Browser identifier</td><td><code>aws.browser.v1</code></td></tr>
<tr><td><code>AGENTCORE_PROFILE_ID</code></td><td>Browser profile for persistent state (cookies, localStorage)</td><td>(none)</td></tr>
<tr><td><code>AGENTCORE_SESSION_TIMEOUT</code></td><td>Session timeout in seconds</td><td><code>3600</code></td></tr>
<tr><td><code>AWS_PROFILE</code></td><td>AWS CLI profile for credential resolution</td><td><code>default</code></td></tr>
<tr><td><code>AWS_ACCESS_KEY_ID</code></td><td>AWS access key (checked before AWS CLI fallback)</td><td>(none)</td></tr>
<tr><td><code>AWS_SECRET_ACCESS_KEY</code></td><td>AWS secret key</td><td>(none)</td></tr>
<tr><td><code>AWS_SESSION_TOKEN</code></td><td>Temporary session token (for STS/SSO credentials)</td><td>(none)</td></tr>
</tbody>
</table>
## Browser Profiles
Use `AGENTCORE_PROFILE_ID` to persist browser state (cookies, localStorage) across sessions:
```bash
AGENTCORE_PROFILE_ID=my-profile agent-browser -p agentcore open https://example.com
```
When a profile is set, AgentCore stores and restores browser state automatically between sessions.
## Live View
When a session starts, AgentCore prints a Live View URL to stderr:
```
Session: abc123-def456
Live View: https://us-east-1.console.aws.amazon.com/bedrock-agentcore/browser/aws.browser.v1/session/abc123-def456#
```
Open this URL in your browser to watch the agent session in real time from the AWS Console.
## Credential Resolution
AgentCore uses lightweight manual SigV4 signing (no AWS SDK dependency). Credentials are resolved in order:
1. **Environment variables** (`AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY`, optionally `AWS_SESSION_TOKEN`)
2. **AWS CLI** (`aws configure export-credentials --format env`), which supports SSO, IAM roles, credential files, and profiles
If using SSO, run `aws sso login` before launching agent-browser. Set `AWS_PROFILE` to select a specific named profile.
## Example
```bash
# Basic usage (credentials auto-resolved via AWS CLI)
agent-browser -p agentcore open https://example.com
# With a browser profile for persistent login state
AGENTCORE_PROFILE_ID=my-profile agent-browser -p agentcore open https://x.com/home
# With explicit region
AGENTCORE_REGION=eu-west-1 agent-browser -p agentcore open https://example.com
# With SSO profile
AWS_PROFILE=my-sso-profile agent-browser -p agentcore open https://example.com
```
When enabled, agent-browser connects to an AgentCore cloud browser session instead of launching a local browser. All commands work identically.
@@ -1,7 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("providers/browser-use");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
@@ -1,24 +0,0 @@
# Browser Use
[Browser Use](https://browser-use.com) provides cloud browser infrastructure for AI agents. Use it when running agent-browser in environments where a local browser isn't available (serverless, CI/CD, etc.).
## Setup
```bash
export BROWSER_USE_API_KEY="your-api-key"
agent-browser -p browseruse open https://example.com
```
Or use environment variables for CI/scripts:
```bash
export AGENT_BROWSER_PROVIDER=browseruse
export BROWSER_USE_API_KEY="your-api-key"
agent-browser open https://example.com
```
The `-p` flag takes precedence over `AGENT_BROWSER_PROVIDER`.
When enabled, agent-browser connects to a Browser Use cloud session instead of launching a local browser. All commands work identically.
Get your API key from the [Browser Use Cloud Dashboard](https://cloud.browser-use.com/settings?tab=api-keys). Free credits are available to get started, with pay-as-you-go pricing after.
@@ -1,7 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("providers/browserbase");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
@@ -1,24 +0,0 @@
# Browserbase
[Browserbase](https://browserbase.com) provides remote browser infrastructure to make deployment of agentic browsing agents easy. Use it when running agent-browser in environments where a local browser isn't feasible.
## Setup
```bash
export BROWSERBASE_API_KEY="your-api-key"
agent-browser -p browserbase open https://example.com
```
Or use environment variables for CI/scripts:
```bash
export AGENT_BROWSER_PROVIDER=browserbase
export BROWSERBASE_API_KEY="your-api-key"
agent-browser open https://example.com
```
The `-p` flag takes precedence over `AGENT_BROWSER_PROVIDER`.
When enabled, agent-browser connects to a Browserbase session instead of launching a local browser. All commands work identically.
Get your API key from the [Browserbase Dashboard](https://browserbase.com/overview).
@@ -1,7 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("providers/browserless");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
@@ -1,39 +0,0 @@
# Browserless
[Browserless](https://browserless.io) provides cloud browser infrastructure with a Sessions API. Use it when running agent-browser in environments where a local browser isn't available.
## Setup
```bash
export BROWSERLESS_API_KEY="your-api-token"
agent-browser -p browserless open https://example.com
```
Or use environment variables for CI/scripts:
```bash
export AGENT_BROWSER_PROVIDER=browserless
export BROWSERLESS_API_KEY="your-api-token"
agent-browser open https://example.com
```
The `-p` flag takes precedence over `AGENT_BROWSER_PROVIDER`.
## Configuration
<table>
<thead>
<tr><th>Variable</th><th>Description</th><th>Default</th></tr>
</thead>
<tbody>
<tr><td><code>BROWSERLESS_API_KEY</code></td><td>API token (required)</td><td></td></tr>
<tr><td><code>BROWSERLESS_API_URL</code></td><td>Base API URL (for custom regions or self-hosted)</td><td><code>https://production-sfo.browserless.io</code></td></tr>
<tr><td><code>BROWSERLESS_BROWSER_TYPE</code></td><td>Type of browser to use (<code>chromium</code> or <code>chrome</code>)</td><td><code>chromium</code></td></tr>
<tr><td><code>BROWSERLESS_TTL</code></td><td>Session TTL in milliseconds</td><td><code>300000</code></td></tr>
<tr><td><code>BROWSERLESS_STEALTH</code></td><td>Enable stealth mode</td><td><code>true</code></td></tr>
</tbody>
</table>
When enabled, agent-browser connects to a Browserless cloud session instead of launching a local browser. All commands work identically.
Get your API token from the [Browserless Dashboard](https://browserless.io).
-7
View File
@@ -1,7 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("providers/kernel");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
-41
View File
@@ -1,41 +0,0 @@
# Kernel
[Kernel](https://www.kernel.sh) provides cloud browser infrastructure for AI agents with features like stealth mode and persistent profiles.
## Setup
```bash
export KERNEL_API_KEY="your-api-key"
agent-browser -p kernel open https://example.com
```
Or use environment variables for CI/scripts:
```bash
export AGENT_BROWSER_PROVIDER=kernel
export KERNEL_API_KEY="your-api-key"
agent-browser open https://example.com
```
The `-p` flag takes precedence over `AGENT_BROWSER_PROVIDER`.
## Configuration
<table>
<thead>
<tr><th>Variable</th><th>Description</th><th>Default</th></tr>
</thead>
<tbody>
<tr><td><code>KERNEL_API_KEY</code></td><td>API key (required)</td><td></td></tr>
<tr><td><code>KERNEL_HEADLESS</code></td><td>Run browser in headless mode</td><td><code>true</code></td></tr>
<tr><td><code>KERNEL_STEALTH</code></td><td>Enable stealth mode to avoid bot detection</td><td><code>false</code></td></tr>
<tr><td><code>KERNEL_TIMEOUT_SECONDS</code></td><td>Session timeout in seconds</td><td><code>300</code></td></tr>
<tr><td><code>KERNEL_PROFILE_NAME</code></td><td>Browser profile name for persistent cookies/logins</td><td>(none)</td></tr>
</tbody>
</table>
**Profile persistence:** When `KERNEL_PROFILE_NAME` is set, the profile will be created if it doesn't already exist. Cookies, logins, and session data are automatically saved back to the profile when the browser session ends, making them available for future sessions.
When enabled, agent-browser connects to a Kernel cloud session instead of launching a local browser. All commands work identically.
Get your API key from the [Kernel Dashboard](https://dashboard.onkernel.com).
-7
View File
@@ -1,7 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("quick-start");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
-90
View File
@@ -1,90 +0,0 @@
# Quick Start
## Core workflow
Every browser automation follows this pattern:
```bash
# 1. Navigate
agent-browser open example.com
# 2. Snapshot to get element refs
agent-browser snapshot -i
# Output:
# @e1 [heading] "Example Domain"
# @e2 [link] "More information..."
# 3. Interact using refs
agent-browser click @e2
# 4. Re-snapshot after page changes
agent-browser snapshot -i
```
## Common commands
```bash
agent-browser open example.com
agent-browser snapshot -i # Get interactive elements with refs
agent-browser click @e2 # Click by ref
agent-browser fill @e3 "test@example.com" # Fill input by ref
agent-browser get text @e1 # Get text content
agent-browser screenshot # Save to temp directory
agent-browser screenshot page.png # Save to specific path
agent-browser close
```
## Traditional selectors
CSS selectors and semantic locators also supported:
```bash
agent-browser click "#submit"
agent-browser fill "#email" "test@example.com"
agent-browser find role button click --name "Submit"
```
## Headed mode
Show browser window for debugging:
```bash
agent-browser open example.com --headed
```
## Wait for content
```bash
agent-browser wait @e1 # Wait for element
agent-browser wait --load networkidle # Wait for network idle
agent-browser wait --url "**/dashboard" # Wait for URL pattern
agent-browser wait 2000 # Wait milliseconds
```
## Command chaining
Chain commands with `&&` in a single shell call. The browser persists via a background daemon, so chaining is safe and efficient:
```bash
# Open, wait, and snapshot in one call
agent-browser open example.com && agent-browser wait --load networkidle && agent-browser snapshot -i
# Chain multiple interactions
agent-browser fill @e1 "user@example.com" && agent-browser fill @e2 "pass" && agent-browser click @e3
# Navigate and capture
agent-browser open example.com && agent-browser wait --load networkidle && agent-browser screenshot page.png
```
Use `&&` when you don't need intermediate output. Run commands separately when you need to parse output first (e.g., snapshot to discover refs before interacting).
## JSON output
For programmatic parsing in scripts:
```bash
agent-browser snapshot --json
agent-browser get text @e1 --json
```
Note: The default text output is more compact and preferred for AI agents.
-7
View File
@@ -1,7 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("security");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
-242
View File
@@ -1,242 +0,0 @@
# Security
agent-browser includes security features to protect against credential exposure, prompt injection via untrusted page content, and unauthorized browser actions.
All security features are opt-in. By default, agent-browser imposes no restrictions on navigation, actions, or output. Enable these features as needed for your deployment -- existing workflows are unaffected until you explicitly activate a feature.
## Threat Model
These features are designed to mitigate the following threats when an LLM-based agent drives a browser:
- **Credential exposure** -- Passwords stored in the auth vault are never included in LLM context. The CLI handles vault operations locally; credentials do not pass through the daemon's IPC channel.
- **Prompt injection via page content** -- Malicious pages can embed text that looks like tool output or system instructions. Content boundary markers (`--content-boundaries`) let the orchestrator distinguish trusted tool output from untrusted page content.
- **Unauthorized navigation / data exfiltration** -- A compromised or manipulated agent could navigate to attacker-controlled domains to exfiltrate data. The domain allowlist (`--allowed-domains`) blocks navigations, sub-resource requests, WebSocket connections, EventSource streams, and `sendBeacon` calls to non-allowed domains.
- **Unauthorized destructive actions** -- Action policy (`--action-policy`) and confirmation gating (`--confirm-actions`) prevent the agent from performing dangerous operations (eval, downloads, uploads) without explicit approval.
- **Context flooding** -- Large page outputs can overwhelm an LLM's context window. Output truncation (`--max-output`) caps the size of page-sourced content.
### Known limitations
- **WebSocket/EventSource blocking is best-effort.** It works by overriding browser constructors via an init script. If the `eval` action category is allowed, page scripts could theoretically restore the original constructors. Deny `eval` via `--action-policy` for maximum protection.
- **Domain filter timing on remote connections.** When connecting to a pre-existing browser via CDP or a cloud provider, pages may have already loaded content before the domain filter is installed. agent-browser navigates disallowed pages to `about:blank` after the filter is active, but resources loaded before that point are not retroactively blocked.
- **Content boundaries are defense-in-depth.** They rely on the LLM and orchestrator respecting the structural markers. A sufficiently capable adversarial page could attempt to mimic the boundary format, though the per-process CSPRNG nonce makes this impractical to predict.
- **Confirmation timeout.** Pending confirmations auto-deny after 60 seconds. Orchestrators must respond within that window.
- **Non-TTY auto-deny.** When `--confirm-interactive` is set but stdin is not a terminal (e.g., piped input), actions are automatically denied to prevent accidental approval in non-interactive contexts.
## Authentication Vault
Store credentials locally and reference them by name. The LLM never sees passwords.
```bash
# Save credentials (encrypted if AGENT_BROWSER_ENCRYPTION_KEY is set)
# Recommended: pipe password via stdin to avoid shell history / process listing exposure
echo "pass" | agent-browser auth save github --url https://github.com/login --username user --password-stdin
# Or pass directly (a warning will be shown)
agent-browser auth save github --url https://github.com/login --username user --password pass
# Login using saved credentials
agent-browser auth login github
# List saved profiles (names and URLs only, no secrets)
agent-browser auth list
# Show profile metadata
agent-browser auth show github
# Delete a profile
agent-browser auth delete github
```
`auth login` navigates with the `load` lifecycle event and then waits for form selectors to appear before filling/clicking. This makes delayed SPA login pages more reliable while avoiding `networkidle` hangs on pages with long-lived background requests.
Custom selectors can be specified if auto-detection fails:
```bash
agent-browser auth save myapp \
--url https://app.example.com/login \
--username user --password pass \
--username-selector "#email" \
--password-selector "#password" \
--submit-selector "button.login"
```
Profiles are stored in `~/.agent-browser/auth/` and always encrypted with AES-256-GCM. If `AGENT_BROWSER_ENCRYPTION_KEY` is not set, a key is auto-generated at `~/.agent-browser/.encryption-key` on first use. Back up this file or set the environment variable explicitly for portability.
File permissions are enforced on both Unix (`chmod 600`/`700`) and Windows (`icacls` restricted to the current user) to prevent other users from reading encryption keys or auth profiles.
## Content Boundary Markers
When `--content-boundaries` is enabled, all page-sourced output is wrapped in structural markers so LLMs can distinguish tool output from untrusted page content:
```
--- AGENT_BROWSER_PAGE_CONTENT nonce=a1b2c3d4 origin=https://example.com ---
[snapshot / text / html / eval output here]
--- END_AGENT_BROWSER_PAGE_CONTENT nonce=a1b2c3d4 ---
```
The nonce is a random value generated per CLI process invocation, making it unpredictable to page content that might attempt to spoof the boundary.
Enable via flag or environment variable:
```bash
agent-browser --content-boundaries snapshot
# or
export AGENT_BROWSER_CONTENT_BOUNDARIES=1
```
Affected output types: `snapshot`, `get text`, `get html`, `eval`, `console`.
In `--json` mode, boundary metadata is injected into the JSON response as a `_boundary` object containing `nonce` and `origin` fields, allowing orchestrators to verify provenance programmatically:
```json
{
"success": true,
"data": { "snapshot": "...", "origin": "https://example.com" },
"_boundary": { "nonce": "a1b2c3d4e5f6...", "origin": "https://example.com" }
}
```
## Domain Allowlist
Restrict which domains the browser can interact with, preventing redirect-based attacks and data exfiltration:
```bash
agent-browser --allowed-domains "example.com,*.example.com,github.com" open https://example.com
# or
export AGENT_BROWSER_ALLOWED_DOMAINS="example.com,*.example.com"
```
Supports exact match (`github.com`) and wildcard prefix (`*.example.com`, which also matches the bare domain `example.com`). Both page navigations and sub-resource requests (scripts, images, fetch, XHR, etc.) to non-allowed domains are blocked, preventing data exfiltration. WebSocket and EventSource connections are also blocked via constructor-level patching. Non-http(s) sub-resources (data URIs, blobs) are still allowed. When a request is blocked, the command returns an error.
> **Note:** The WebSocket/EventSource blocking is best-effort -- it works by overriding the browser constructors via an init script. If the `eval` action category is allowed, page scripts could theoretically restore the original constructors. For maximum protection, deny the `eval` category via `--action-policy` when using `--allowed-domains`.
Config file:
```json
{
"allowedDomains": ["example.com", "*.example.com", "github.com"]
}
```
> **CDN and third-party resources:** The domain filter blocks all sub-resource requests (scripts, stylesheets, images, fonts, fetch/XHR) to non-allowed domains. Most websites load assets from CDN domains. Include these in your allowlist or pages will break. For example:
>
> ```bash
> --allowed-domains "myapp.com,*.myapp.com,cdn.jsdelivr.net,fonts.googleapis.com,fonts.gstatic.com"
> ```
## Action Policy
Gate actions using a static policy file. The policy is enforced by the daemon -- denied actions fail immediately.
```bash
agent-browser --action-policy ./policy.json open https://example.com
# or
export AGENT_BROWSER_ACTION_POLICY=./policy.json
```
Example policy (permissive with specific denials):
```json
{
"default": "allow",
"deny": ["eval", "download", "upload"]
}
```
Example policy (restrictive):
```json
{
"default": "deny",
"allow": ["navigate", "snapshot", "click", "scroll", "wait", "get"]
}
```
<table>
<thead>
<tr><th>Category</th><th>Actions</th></tr>
</thead>
<tbody>
<tr><td><code>navigate</code></td><td>open, back, forward, reload, tab new</td></tr>
<tr><td><code>click</code></td><td>click, dblclick, tap</td></tr>
<tr><td><code>fill</code></td><td>fill, type, keyboard type/inserttext, select, check, uncheck</td></tr>
<tr><td><code>eval</code></td><td>eval, evalhandle, addscript, addinitscript, addstyle, expose, setcontent</td></tr>
<tr><td><code>download</code></td><td>download, waitfordownload</td></tr>
<tr><td><code>upload</code></td><td>upload</td></tr>
<tr><td><code>snapshot</code></td><td>snapshot, screenshot, pdf, diff</td></tr>
<tr><td><code>scroll</code></td><td>scroll, scrollintoview</td></tr>
<tr><td><code>wait</code></td><td>wait, waitforurl, waitforloadstate, waitforfunction</td></tr>
<tr><td><code>get</code></td><td>get text/html/url/title, count, isvisible, getbyrole, getbytext, getbylabel, etc.</td></tr>
<tr><td><code>interact</code></td><td>hover, focus, drag, press, keydown, keyup, mousemove, dispatch</td></tr>
<tr><td><code>network</code></td><td>network route/unroute, requests, har start/stop</td></tr>
<tr><td><code>state</code></td><td>state save/load, cookies set, storage set</td></tr>
</tbody>
</table>
Auth vault operations (`auth save`, `auth login`, `auth list`, `auth show`, `auth delete`) and other internal/meta operations bypass action policy enforcement since they are trusted local operations. Domain allowlist restrictions still apply to `auth login` navigations.
## Action Confirmation
For actions that require explicit approval, use `--confirm-actions` to specify categories that require confirmation:
```bash
# Orchestrator mode: returns confirmation_required response
agent-browser --confirm-actions eval,download eval "document.title"
# Then approve or deny:
agent-browser confirm c_8f3a1234
agent-browser deny c_8f3a1234
```
For interactive (human-in-the-loop) confirmation:
```bash
agent-browser --confirm-actions eval,download --confirm-interactive eval "document.title"
# Prompts: Allow? [y/N]
```
Pending confirmations auto-deny after 60 seconds.
> **Non-TTY behavior:** When `--confirm-interactive` is set but stdin is not a TTY (e.g., piped input or running inside an automated pipeline), actions are automatically denied. This prevents accidental approval in non-interactive contexts.
## Output Length Limits
Prevent context flooding by truncating large page outputs:
```bash
agent-browser --max-output 50000 get text body
# or
export AGENT_BROWSER_MAX_OUTPUT=50000
```
Affected output types: `snapshot`, `get text`, `get html`, `eval`, `console`.
## Environment Variables
<table>
<thead>
<tr><th>Variable</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td><code>AGENT_BROWSER_CONTENT_BOUNDARIES</code></td><td>Wrap page output in boundary markers</td></tr>
<tr><td><code>AGENT_BROWSER_MAX_OUTPUT</code></td><td>Max characters for page output</td></tr>
<tr><td><code>AGENT_BROWSER_ALLOWED_DOMAINS</code></td><td>Comma-separated allowed domain patterns</td></tr>
<tr><td><code>AGENT_BROWSER_ACTION_POLICY</code></td><td>Path to action policy JSON file</td></tr>
<tr><td><code>AGENT_BROWSER_CONFIRM_ACTIONS</code></td><td>Comma-separated action categories requiring confirmation</td></tr>
<tr><td><code>AGENT_BROWSER_CONFIRM_INTERACTIVE</code></td><td>Enable interactive confirmation prompts</td></tr>
<tr><td><code>AGENT_BROWSER_ENCRYPTION_KEY</code></td><td>64-char hex key for AES-256-GCM encryption (auth vault + sessions)</td></tr>
</tbody>
</table>
## Recommended Configuration
For production AI agent deployments:
```json
{
"contentBoundaries": true,
"maxOutput": 50000,
"allowedDomains": ["your-app.com", "*.your-app.com"],
"actionPolicy": "./policy.json"
}
```
-7
View File
@@ -1,7 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("selectors");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
-54
View File
@@ -1,54 +0,0 @@
# Selectors
## Refs (recommended)
Refs provide deterministic element selection from snapshots. Best for AI agents.
```bash
# 1. Get snapshot with refs
agent-browser snapshot
# Output:
# - heading "Example Domain" [ref=e1] [level=1]
# - button "Submit" [ref=e2]
# - textbox "Email" [ref=e3]
# - link "Learn more" [ref=e4]
# 2. Use refs to interact
agent-browser click @e2 # Click the button
agent-browser fill @e3 "test@example.com" # Fill the textbox
agent-browser get text @e1 # Get heading text
agent-browser hover @e4 # Hover the link
```
### Why refs?
- **Deterministic** - Ref points to exact element from snapshot
- **Fast** - No DOM re-query needed
- **AI-friendly** - LLMs can reliably parse and use refs
## CSS selectors
```bash
agent-browser click "#id"
agent-browser click ".class"
agent-browser click "div > button"
agent-browser click "[data-testid='submit']"
```
## Text & XPath
```bash
agent-browser click "text=Submit"
agent-browser click "xpath=//button[@type='submit']"
```
## Semantic locators
Find elements by role, label, or other semantic properties:
```bash
agent-browser find role button click --name "Submit"
agent-browser find label "Email" fill "test@test.com"
agent-browser find placeholder "Search..." fill "query"
agent-browser find testid "submit-btn" click
```
-7
View File
@@ -1,7 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("sessions");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
-267
View File
@@ -1,267 +0,0 @@
# Sessions
Run multiple isolated browser instances:
```bash
# Different sessions
agent-browser --session agent1 open site-a.com
agent-browser --session agent2 open site-b.com
# Or via environment variable
AGENT_BROWSER_SESSION=agent1 agent-browser click "#btn"
# List active sessions
agent-browser session list
# Output:
# Active sessions:
# -> default
# agent1
# Show current session
agent-browser session
```
## Session isolation
Each session has its own:
- Browser instance
- Cookies and storage
- Navigation history
- Authentication state
## Chrome profile reuse
The simplest way to reuse your existing login state: pass a Chrome profile name to `--profile`. agent-browser copies the profile to a temp directory (read-only snapshot) and launches Chrome with your existing cookies and sessions.
```bash
# List available Chrome profiles
agent-browser profiles
# Reuse your default Chrome profile's login state
agent-browser --profile Default open https://gmail.com
# Use a named profile (by display name or directory name)
agent-browser --profile "Work" open https://app.example.com
# Or via environment variable
AGENT_BROWSER_PROFILE=Default agent-browser open https://gmail.com
```
<table>
<thead>
<tr><th>Detail</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td>Supported browsers</td><td>Chrome, Chrome Canary, Chromium, Brave</td></tr>
<tr><td>What's copied</td><td>Cookies, local storage, extensions state (cache dirs excluded for speed)</td></tr>
<tr><td>Original profile</td><td>Never modified (read-only snapshot)</td></tr>
<tr><td>Cleanup</td><td>Temp copy deleted when browser closes</td></tr>
<tr><td>Windows note</td><td>Close Chrome before using <code>--profile &lt;name&gt;</code> if Chrome is running</td></tr>
</tbody>
</table>
## Persistent profiles
For a custom profile directory that persists state across browser restarts, pass a path to `--profile`:
```bash
# Use a persistent profile directory
agent-browser --profile ~/.myapp-profile open myapp.com
# Login once, then reuse the authenticated session
agent-browser --profile ~/.myapp-profile open myapp.com/dashboard
# Or via environment variable
AGENT_BROWSER_PROFILE=~/.myapp-profile agent-browser open myapp.com
```
The profile directory stores:
- Cookies and localStorage
- IndexedDB data
- Service workers
- Browser cache
- Login sessions
## Import auth from your browser
If you are already logged in to a site in Chrome, you can grab that auth state and reuse it in agent-browser. This is the fastest way to bypass login flows, OAuth, SSO, or 2FA.
**Step 1:** Start Chrome with remote debugging:
```bash
# macOS
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --remote-debugging-port=9222
# Linux
google-chrome --remote-debugging-port=9222
```
Log in to your target site(s) in this Chrome window.
`--remote-debugging-port` exposes full browser control on localhost. Any local process can connect. Only use on trusted machines and close Chrome when done.
**Step 2:** Connect and save the authenticated state:
```bash
agent-browser --auto-connect state save ./my-auth.json
```
**Step 3:** Use the saved auth in future sessions:
```bash
# Load auth at launch
agent-browser --state ./my-auth.json open https://app.example.com/dashboard
# Or load into an existing session
agent-browser state load ./my-auth.json
agent-browser open https://app.example.com/dashboard
```
Combine with `--session-name` so the imported auth auto-persists across restarts:
```bash
agent-browser --session-name myapp state load ./my-auth.json
# From now on, state auto-saves/restores for "myapp"
```
State files contain session tokens in plaintext. Add them to `.gitignore` and delete when no longer needed. For encryption at rest, see [State encryption](#state-encryption) below.
## Session persistence
Use `--session-name` to automatically save and restore cookies and localStorage across browser restarts:
```bash
# Auto-save/load state for "twitter" session
agent-browser --session-name twitter open twitter.com
# Login once, then state persists automatically
agent-browser --session-name twitter click "#login"
# Or via environment variable
export AGENT_BROWSER_SESSION_NAME=twitter
agent-browser open twitter.com
```
State files are stored in `~/.agent-browser/sessions/` and automatically loaded on daemon start.
### Session name rules
Session names must contain only alphanumeric characters, hyphens, and underscores:
```bash
# Valid session names
agent-browser --session-name my-project open example.com
agent-browser --session-name test_session_v2 open example.com
# Invalid (will be rejected)
agent-browser --session-name "../bad" open example.com # path traversal
agent-browser --session-name "my session" open example.com # spaces
agent-browser --session-name "foo/bar" open example.com # slashes
```
## State encryption
Encrypt saved state files (cookies, localStorage) using AES-256-GCM:
```bash
# Generate a 256-bit key (64 hex characters)
openssl rand -hex 32
# Set the encryption key
export AGENT_BROWSER_ENCRYPTION_KEY=<your-64-char-hex-key>
# State files are now encrypted automatically
agent-browser --session-name secure-session open example.com
# List states shows encryption status
agent-browser state list
```
## State auto-expiration
Automatically delete old state files to prevent accumulation:
```bash
# Set expiration (default: 30 days)
export AGENT_BROWSER_STATE_EXPIRE_DAYS=7
# Manually clean old states
agent-browser state clean --older-than 7
```
## State management commands
```bash
# List all saved states
agent-browser state list
# Show state summary (cookies, origins, domains)
agent-browser state show my-session-default.json
# Rename a state file
agent-browser state rename old-name new-name
# Clear states for a specific session name
agent-browser state clear my-session
# Clear all saved states
agent-browser state clear --all
# Manual save/load (for custom paths)
agent-browser state save ./backup.json
agent-browser state load ./backup.json
```
## Authenticated sessions
Use `--headers` to set HTTP headers for a specific origin:
```bash
# Headers scoped to api.example.com only
agent-browser open api.example.com --headers '{"Authorization": "Bearer <token>"}'
# Requests to api.example.com include the auth header
agent-browser snapshot -i --json
agent-browser click @e2
# Navigate to another domain - headers NOT sent
agent-browser open other-site.com
```
Useful for:
- **Skipping login flows** - Authenticate via headers
- **Switching users** - Different auth tokens per session
- **API testing** - Access protected endpoints
- **Security** - Headers scoped to origin, not leaked
## Multiple origins
```bash
agent-browser open api.example.com --headers '{"Authorization": "Bearer token1"}'
agent-browser open api.acme.com --headers '{"Authorization": "Bearer token2"}'
```
## Global headers
For headers on all domains:
```bash
agent-browser set headers '{"X-Custom-Header": "value"}'
```
## Environment variables
<table>
<thead>
<tr><th>Variable</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td><code>AGENT_BROWSER_SESSION</code></td><td>Browser session ID (default: "default")</td></tr>
<tr><td><code>AGENT_BROWSER_SESSION_NAME</code></td><td>Auto-save/load state persistence name</td></tr>
<tr><td><code>AGENT_BROWSER_ENCRYPTION_KEY</code></td><td>64-char hex key for AES-256-GCM encryption</td></tr>
<tr><td><code>AGENT_BROWSER_STATE_EXPIRE_DAYS</code></td><td>Auto-delete states older than N days (default: 30)</td></tr>
</tbody>
</table>
-7
View File
@@ -1,7 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("skills");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
-75
View File
@@ -1,75 +0,0 @@
# Skills
agent-browser ships with skills that teach AI coding agents how to use it for specific workflows. Install a skill and your agent in Cursor, Claude Code, or Codex can automate browser tasks without manual guidance.
## Installation
```bash
npx skills add vercel-labs/agent-browser
```
This installs a single discovery skill that teaches your agent about agent-browser and directs it to use the `agent-browser skills` CLI command for current instructions. The discovery skill contains trigger words so agents prefer agent-browser over built-in browser tools.
## CLI Command
Agents retrieve skill content at runtime using the `agent-browser skills` command. This always serves content matching the installed CLI version, so instructions never go stale.
<table>
<thead>
<tr>
<th>Command</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>agent-browser skills</code></td>
<td>List all available skills (same as <code>skills list</code>)</td>
</tr>
<tr>
<td><code>agent-browser skills list</code></td>
<td>List all available skills with names and descriptions</td>
</tr>
<tr>
<td><code>agent-browser skills get &lt;name&gt;</code></td>
<td>Output a skill's full content</td>
</tr>
<tr>
<td><code>agent-browser skills get &lt;name&gt; --full</code></td>
<td>Include references and templates alongside the skill</td>
</tr>
<tr>
<td><code>agent-browser skills get --all</code></td>
<td>Output every skill</td>
</tr>
<tr>
<td><code>agent-browser skills path [name]</code></td>
<td>Print the filesystem path to a skill directory</td>
</tr>
</tbody>
</table>
All commands support `--json` for structured output.
Set the `AGENT_BROWSER_SKILLS_DIR` environment variable to override the skills directory path.
## How It Works
The discovery skill installed via `npx skills add` is intentionally thin and stable. It makes agents aware of agent-browser, provides trigger words for activation, and points to the `agent-browser skills` command. Actual usage instructions, command references, workflows, and specialized knowledge all live in the CLI-served skills.
This design solves the version drift problem: the installed SKILL.md rarely changes, while the CLI always serves content matching its own version.
## Available Skills
- **core** — Core browser automation: navigation, snapshots, forms, screenshots, data extraction, sessions, authentication, diffing, and the full command reference. Start here for most browser tasks.
- **dogfood** — Systematic exploratory testing. Navigates an app like a real user, finds bugs and UX issues, and produces a structured report with screenshots and repro videos.
- **electron** — Automate any Electron app (VS Code, Slack, Discord, Figma, etc.) by connecting to its built-in Chrome DevTools Protocol port.
- **slack** — Browser-based Slack automation. Check unreads, navigate channels, search conversations, send messages, and extract data.
- **vercel-sandbox** — Run agent-browser + headless Chrome inside ephemeral Vercel Sandbox microVMs.
- **agentcore** — Run agent-browser on AWS Bedrock AgentCore cloud browsers.
Use `agent-browser skills list` to see all available skills, then `agent-browser skills get <name>` to load one. `agent-browser skills get core --full` is the recommended starting point for most browser tasks.
## Source
All skill files are in the [`skills/`](https://github.com/vercel-labs/agent-browser/tree/main/skills) and [`skill-data/`](https://github.com/vercel-labs/agent-browser/tree/main/skill-data) directories of the repository. The `skills/` directory holds the discovery stub that `npx skills add` installs; the `skill-data/` directory holds the runtime skill content served by the CLI.
-7
View File
@@ -1,7 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("snapshots");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
-123
View File
@@ -1,123 +0,0 @@
# Snapshots
The `snapshot` command returns a compact accessibility tree with refs for element interaction.
## Options
Filter output to reduce size:
```bash
agent-browser snapshot # Full accessibility tree
agent-browser snapshot -i # Interactive elements only (recommended)
agent-browser snapshot -c # Compact (remove empty elements)
agent-browser snapshot -d 3 # Limit depth to 3 levels
agent-browser snapshot -s "#main" # Scope to CSS selector
agent-browser snapshot -i -c -d 5 # Combine options
```
<table>
<thead>
<tr><th>Option</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td><code>-i, --interactive</code></td><td>Only interactive elements (buttons, links, inputs)</td></tr>
<tr><td><code>-u, --urls</code></td><td>Include href URLs for link elements</td></tr>
<tr><td><code>-c, --compact</code></td><td>Remove empty structural elements</td></tr>
<tr><td><code>-d, --depth</code></td><td>Limit tree depth</td></tr>
<tr><td><code>-s, --selector</code></td><td>Scope to CSS selector</td></tr>
</tbody>
</table>
## Output format
The default text output is compact and AI-friendly:
```bash
agent-browser snapshot -i
# Output:
# @e1 [heading] "Example Domain" [level=1]
# @e2 [button] "Submit"
# @e3 [input type="email"] placeholder="Email"
# @e4 [link] "Learn more"
```
## Using refs
Refs from the snapshot map directly to commands:
```bash
agent-browser click @e2 # Click the Submit button
agent-browser fill @e3 "a@b.com" # Fill the email input
agent-browser get text @e1 # Get heading text
```
## Ref lifecycle
Refs are invalidated when the page changes. Always re-snapshot after navigation or DOM updates:
```bash
agent-browser click @e4 # Navigates to new page
agent-browser snapshot -i # Get fresh refs
agent-browser click @e1 # Use new refs
```
## Annotated screenshots
For visual context alongside text snapshots, use `screenshot --annotate` to overlay numbered labels on interactive elements. Each label `[N]` maps to ref `@eN`:
In native mode, annotated screenshots currently work on the CDP-backed browser path (Chromium/Lightpanda). The Safari/WebDriver backend does not yet support `--annotate`.
```bash
agent-browser screenshot --annotate ./page.png
# -> Screenshot saved to ./page.png
# [1] @e1 button "Submit"
# [2] @e2 link "Home"
# [3] @e3 textbox "Email"
agent-browser click @e2
```
Annotated screenshots also cache refs, so you can interact with elements immediately. This is useful when the text snapshot is insufficient -- unlabeled icons, canvas content, or visual layout verification.
## Iframes
Snapshots automatically detect and inline iframe content. Each `Iframe` node in the main frame is resolved and its child accessibility tree is included directly beneath it. Refs assigned to elements inside iframes carry frame context, so interactions work without switching frames first.
```bash
agent-browser snapshot -i
# @e1 [heading] "Checkout"
# @e2 [Iframe] "payment-frame"
# @e3 [input] "Card number"
# @e4 [button] "Pay"
agent-browser fill @e3 "4111111111111111"
agent-browser click @e4
```
Only one level of iframe nesting is expanded. Cross-origin iframes that block accessibility tree access and empty iframes are silently omitted.
To scope a snapshot to a single iframe, switch into it first:
```bash
agent-browser frame @e2
agent-browser snapshot -i # Only elements inside that iframe
agent-browser frame main # Return to main frame
```
## Best practices
1. Use `-i` to reduce output to actionable elements
2. Re-snapshot after page changes to get updated refs
3. Scope with `-s` for specific page sections
4. Use `-d` to limit depth on complex pages
5. Use `screenshot --annotate` when visual context is needed alongside refs
## JSON output
For programmatic parsing in scripts:
```bash
agent-browser snapshot --json
# {"success":true,"data":{"snapshot":"...","refs":{"e1":{"role":"heading","name":"Title"},...}}}
```
Note: JSON uses more tokens than text output. The default text format is preferred for AI agents.
-7
View File
@@ -1,7 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("streaming");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
-258
View File
@@ -1,258 +0,0 @@
# Streaming
Stream the browser viewport via WebSocket for live preview or "pair browsing"
where a human can watch and interact alongside an AI agent.
## Streaming
Every session automatically starts a WebSocket stream server on an OS-assigned port. The server streams viewport frames and accepts input events (mouse, keyboard, touch).
To bind to a specific port, set `AGENT_BROWSER_STREAM_PORT`:
```bash
AGENT_BROWSER_STREAM_PORT=9223 agent-browser open example.com
```
You can also manage streaming at runtime:
```bash
agent-browser stream status # Show streaming state and bound port
agent-browser stream enable --port 9223 # Re-enable on a specific port
agent-browser stream disable # Stop streaming for the session
```
`stream status` returns the enabled state, active port, browser connection state, and whether screencasting is active. `stream disable` tears the server down and removes the session's `.stream` metadata file.
## Runtime status response
`agent-browser stream status --json` returns data like:
```json
{
"enabled": true,
"port": 9223,
"connected": true,
"screencasting": true
}
```
`connected` reports whether the daemon currently has a browser attached. `screencasting` reports whether frames are actively being produced for the stream server.
## Relationship to screencast commands
`stream enable` creates the WebSocket server and keeps it available for the session. WebSocket clients then trigger live frame delivery automatically.
The lower-level `screencast_start` and `screencast_stop` commands still control explicit CDP screencasts directly. Use them when you want a screencast without the WebSocket runtime server.
## WebSocket protocol
Connect to `ws://localhost:9223` to receive frames and send input.
### Frame messages
The server sends frame messages with base64-encoded images:
```json
{
"type": "frame",
"data": "<base64-encoded-jpeg>",
"metadata": {
"deviceWidth": 1280,
"deviceHeight": 720,
"pageScaleFactor": 1,
"offsetTop": 0,
"scrollOffsetX": 0,
"scrollOffsetY": 0
}
}
```
### Status messages
Connection and screencast status:
```json
{
"type": "status",
"connected": true,
"screencasting": true,
"viewportWidth": 1280,
"viewportHeight": 720
}
```
## Input injection
Send input events to control the browser remotely.
### Mouse events
```json
// Click
{
"type": "input_mouse",
"eventType": "mousePressed",
"x": 100,
"y": 200,
"button": "left",
"clickCount": 1
}
// Release
{
"type": "input_mouse",
"eventType": "mouseReleased",
"x": 100,
"y": 200,
"button": "left"
}
// Move
{
"type": "input_mouse",
"eventType": "mouseMoved",
"x": 150,
"y": 250
}
// Scroll
{
"type": "input_mouse",
"eventType": "mouseWheel",
"x": 100,
"y": 200,
"deltaX": 0,
"deltaY": 100
}
```
### Keyboard events
```json
// Key down
{
"type": "input_keyboard",
"eventType": "keyDown",
"key": "Enter",
"code": "Enter"
}
// Key up
{
"type": "input_keyboard",
"eventType": "keyUp",
"key": "Enter",
"code": "Enter"
}
// Type character
{
"type": "input_keyboard",
"eventType": "char",
"text": "a"
}
// With modifiers (1=Alt, 2=Ctrl, 4=Meta, 8=Shift)
{
"type": "input_keyboard",
"eventType": "keyDown",
"key": "c",
"code": "KeyC",
"modifiers": 2
}
```
### Touch events
```json
// Touch start
{
"type": "input_touch",
"eventType": "touchStart",
"touchPoints": [{ "x": 100, "y": 200 }]
}
// Touch move
{
"type": "input_touch",
"eventType": "touchMove",
"touchPoints": [{ "x": 150, "y": 250 }]
}
// Touch end
{
"type": "input_touch",
"eventType": "touchEnd",
"touchPoints": []
}
// Multi-touch (pinch zoom)
{
"type": "input_touch",
"eventType": "touchStart",
"touchPoints": [
{ "x": 100, "y": 200, "id": 0 },
{ "x": 200, "y": 200, "id": 1 }
]
}
```
## Programmatic API
For advanced use, control streaming directly via the TypeScript API:
```typescript
import { BrowserManager } from 'agent-browser';
const browser = new BrowserManager();
await browser.launch({ headless: true });
await browser.navigate('https://example.com');
// Start screencast with callback
await browser.startScreencast((frame) => {
console.log('Frame:', frame.metadata.deviceWidth, 'x', frame.metadata.deviceHeight);
// frame.data is base64-encoded image
}, {
format: 'jpeg', // or 'png'
quality: 80, // 0-100, jpeg only
maxWidth: 1280,
maxHeight: 720,
everyNthFrame: 1
});
// Inject mouse event
await browser.injectMouseEvent({
type: 'mousePressed',
x: 100,
y: 200,
button: 'left',
clickCount: 1
});
// Inject keyboard event
await browser.injectKeyboardEvent({
type: 'keyDown',
key: 'Enter',
code: 'Enter'
});
// Inject touch event
await browser.injectTouchEvent({
type: 'touchStart',
touchPoints: [{ x: 100, y: 200 }]
});
// Check if screencasting
console.log('Active:', browser.isScreencasting());
// Stop screencast
await browser.stopScreencast();
```
## Use cases
- **Pair browsing** - Human watches and assists AI agent in real-time
- **Remote preview** - View browser output in a separate UI
- **Recording** - Capture frames for video generation
- **Mobile testing** - Inject touch events for mobile emulation
- **Accessibility testing** - Manual interaction during automated tests
-178
View File
@@ -1,178 +0,0 @@
import { codeToHtml } from "shiki";
import { CopyButton } from "./copy-button";
const vercelDarkTheme = {
name: "vercel-dark",
type: "dark" as const,
colors: {
"editor.background": "transparent",
"editor.foreground": "#EDEDED",
},
settings: [
{
scope: ["comment", "punctuation.definition.comment"],
settings: { foreground: "#A1A1A1" },
},
{
scope: ["string", "string.quoted", "string.template", "punctuation.definition.string"],
settings: { foreground: "#00CA50" },
},
{
scope: ["constant.numeric", "constant.language.boolean", "constant.language.null"],
settings: { foreground: "#47A8FF" },
},
{
scope: ["keyword", "storage.type", "storage.modifier"],
settings: { foreground: "#FF4D8D" },
},
{
scope: ["keyword.operator", "keyword.control"],
settings: { foreground: "#FF4D8D" },
},
{
scope: ["entity.name.function", "support.function", "meta.function-call"],
settings: { foreground: "#C472FB" },
},
{
scope: ["variable", "variable.other"],
settings: { foreground: "#EDEDED" },
},
{
scope: ["variable.parameter"],
settings: { foreground: "#FF9300" },
},
{
scope: ["entity.name.tag", "support.class.component", "entity.name.type"],
settings: { foreground: "#FF4D8D" },
},
{
scope: ["punctuation", "meta.brace", "meta.bracket"],
settings: { foreground: "#EDEDED" },
},
{
scope: [
"support.type.property-name",
"entity.name.tag.json",
"meta.object-literal.key",
"punctuation.support.type.property-name",
],
settings: { foreground: "#FF4D8D" },
},
{
scope: ["entity.other.attribute-name"],
settings: { foreground: "#00CA50" },
},
{
scope: ["support.type.primitive", "entity.name.type.primitive"],
settings: { foreground: "#00CA50" },
},
],
};
const vercelLightTheme = {
name: "vercel-light",
type: "light" as const,
colors: {
"editor.background": "transparent",
"editor.foreground": "#171717",
},
settings: [
{
scope: ["comment", "punctuation.definition.comment"],
settings: { foreground: "#6B7280" },
},
{
scope: ["string", "string.quoted", "string.template", "punctuation.definition.string"],
settings: { foreground: "#067A6E" },
},
{
scope: ["constant.numeric", "constant.language.boolean", "constant.language.null"],
settings: { foreground: "#0070C0" },
},
{
scope: ["keyword", "storage.type", "storage.modifier"],
settings: { foreground: "#D6409F" },
},
{
scope: ["keyword.operator", "keyword.control"],
settings: { foreground: "#D6409F" },
},
{
scope: ["entity.name.function", "support.function", "meta.function-call"],
settings: { foreground: "#6E56CF" },
},
{
scope: ["variable", "variable.other"],
settings: { foreground: "#171717" },
},
{
scope: ["variable.parameter"],
settings: { foreground: "#B45309" },
},
{
scope: ["entity.name.tag", "support.class.component", "entity.name.type"],
settings: { foreground: "#D6409F" },
},
{
scope: ["punctuation", "meta.brace", "meta.bracket"],
settings: { foreground: "#6B7280" },
},
{
scope: [
"support.type.property-name",
"entity.name.tag.json",
"meta.object-literal.key",
"punctuation.support.type.property-name",
],
settings: { foreground: "#D6409F" },
},
{
scope: ["entity.other.attribute-name"],
settings: { foreground: "#067A6E" },
},
{
scope: ["support.type.primitive", "entity.name.type.primitive"],
settings: { foreground: "#067A6E" },
},
],
};
const PLACEHOLDER_PREFIX = "\u200B\u200B";
const PLACEHOLDER_SUFFIX = "\u200B\u200B";
function shieldPlaceholders(code: string): string {
return code.replace(/<([\w|]+)>/g, `${PLACEHOLDER_PREFIX}$1${PLACEHOLDER_SUFFIX}`);
}
function restorePlaceholders(html: string): string {
return html.replace(
new RegExp(`${PLACEHOLDER_PREFIX}([\\w|]+)${PLACEHOLDER_SUFFIX}`, "g"),
"&lt;$1&gt;",
);
}
interface CodeBlockProps {
code: string;
lang?: string;
}
export async function CodeBlock({ code, lang = "bash" }: CodeBlockProps) {
const trimmedCode = code.trim();
const shielded = shieldPlaceholders(trimmedCode);
let html = await codeToHtml(shielded, {
lang,
themes: {
light: vercelLightTheme,
dark: vercelDarkTheme,
},
defaultColor: false,
});
html = restorePlaceholders(html);
return (
<div className="code-block relative group">
<CopyButton code={trimmedCode} />
<div dangerouslySetInnerHTML={{ __html: html }} />
</div>
);
}
-40
View File
@@ -1,40 +0,0 @@
"use client";
import { useState } from "react";
interface CopyButtonProps {
code: string;
}
export function CopyButton({ code }: CopyButtonProps) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (error) {
console.error("Failed to copy to clipboard:", error);
// Optionally, you could set an error state or show a toast notification here
}
};
return (
<button
onClick={handleCopy}
className="absolute top-2 right-2 p-1.5 rounded text-[#666] hover:text-[#999] hover:bg-[#333] opacity-0 group-hover:opacity-100 transition-all"
aria-label="Copy code"
>
{copied ? (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M5 13l4 4L19 7" />
</svg>
) : (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
)}
</button>
);
}
-71
View File
@@ -1,71 +0,0 @@
"use client";
import { useState } from "react";
import { usePathname } from "next/navigation";
export function CopyPageButton() {
const pathname = usePathname();
const [state, setState] = useState<"idle" | "loading" | "copied">("idle");
const handleCopy = async () => {
setState("loading");
try {
const response = await fetch(
`/api/docs-markdown?path=${encodeURIComponent(pathname)}`,
);
if (!response.ok) {
throw new Error("Failed to fetch markdown");
}
const markdown = await response.text();
await navigator.clipboard.writeText(markdown);
setState("copied");
setTimeout(() => setState("idle"), 2000);
} catch {
setState("idle");
}
};
return (
<button
onClick={handleCopy}
disabled={state === "loading"}
className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs text-muted-foreground hover:text-foreground border border-border rounded-md hover:bg-muted transition-colors disabled:opacity-50"
aria-label="Copy page as Markdown"
>
{state === "copied" ? (
<>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</svg>
Copied
</>
) : (
<>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
Copy Page
</>
)}
</button>
);
}
-282
View File
@@ -1,282 +0,0 @@
"use client";
function DiffLine({ line }: { line: string }) {
if (line.startsWith("+ ")) {
return <div className="text-green-400">{line}</div>;
}
if (line.startsWith("- ")) {
return <div className="text-red-400">{line}</div>;
}
return <div className="opacity-50">{line}</div>;
}
function CommandLine({ children }: { children: string }) {
return (
<div>
<span className="opacity-40">$ </span>
{children}
</div>
);
}
function Terminal({ children }: { children: React.ReactNode }) {
return (
<div
className="rounded border font-mono text-[0.8125rem] leading-[1.7] overflow-x-auto"
style={{
background: "var(--card)",
borderColor: "var(--border)",
padding: "0.875rem",
}}
>
{children}
</div>
);
}
function PageMockup({
label,
buttonColor,
diffMode,
}: {
label: string;
buttonColor: string;
diffMode?: boolean;
}) {
const dimOpacity = diffMode ? 0.15 : 1;
return (
<div className="flex-1 min-w-0">
<div
className="text-[0.6875rem] font-medium mb-1.5 text-center"
style={{ color: "var(--muted-foreground)" }}
>
{label}
</div>
<svg
viewBox="0 0 160 120"
className="w-full rounded border"
style={{ borderColor: "var(--border)" }}
>
<rect width="160" height="120" fill={diffMode ? "#1a1a1a" : "#111"} />
{/* Nav bar */}
<rect
x="0"
y="0"
width="160"
height="16"
fill="#222"
opacity={dimOpacity}
/>
<rect
x="8"
y="5"
width="24"
height="6"
rx="1"
fill="#555"
opacity={dimOpacity}
/>
<rect
x="120"
y="5"
width="12"
height="6"
rx="1"
fill="#444"
opacity={dimOpacity}
/>
<rect
x="136"
y="5"
width="12"
height="6"
rx="1"
fill="#444"
opacity={dimOpacity}
/>
{/* Heading */}
<rect
x="20"
y="26"
width="80"
height="6"
rx="1"
fill="#666"
opacity={dimOpacity}
/>
{/* Subtext */}
<rect
x="30"
y="38"
width="60"
height="4"
rx="1"
fill="#444"
opacity={dimOpacity}
/>
{/* Input field */}
<rect
x="30"
y="52"
width="100"
height="14"
rx="2"
fill="#1a1a1a"
stroke="#333"
strokeWidth="0.5"
opacity={dimOpacity}
/>
{/* Button -- this is what changes */}
{diffMode ? (
<>
<rect
x="55"
y="76"
width="50"
height="14"
rx="2"
fill="#ef4444"
opacity="0.85"
/>
<rect
x="55"
y="76"
width="50"
height="14"
rx="2"
fill="none"
stroke="#ef4444"
strokeWidth="1.5"
strokeDasharray="3 2"
/>
</>
) : (
<rect
x="55"
y="76"
width="50"
height="14"
rx="2"
fill={buttonColor}
/>
)}
<text
x="80"
y="85.5"
textAnchor="middle"
fill="white"
fontSize="6"
fontFamily="system-ui, sans-serif"
opacity={diffMode ? 0.9 : 1}
>
Submit
</text>
{/* Footer line */}
<rect
x="40"
y="102"
width="80"
height="3"
rx="1"
fill="#333"
opacity={dimOpacity}
/>
</svg>
</div>
);
}
const snapshotDiffLines = [
" heading \"Sign Up\" [ref=e1]",
" text \"Create your account\" [ref=e2]",
"- textbox \"Email\" [ref=e3]",
"+ textbox \"Email\" [ref=e3]: \"test@example.com\"",
"- button \"Submit\" [ref=e4]",
"+ button \"Submit\" [ref=e4] [disabled]",
"+ status \"Sending...\" [ref=e7]",
" link \"Already have an account?\" [ref=e5]",
];
export function DiffDemo() {
return (
<div className="grid gap-8 my-8">
{/* Panel 1: Snapshot diff */}
<div>
<div
className="text-xs font-medium uppercase tracking-wider mb-3"
style={{ color: "var(--muted-foreground)" }}
>
Verify an action changed the page
</div>
<Terminal>
<div className="opacity-60 mb-2">
<CommandLine>agent-browser snapshot -i</CommandLine>
<CommandLine>
agent-browser fill @e3 &quot;test@example.com&quot;
</CommandLine>
<CommandLine>agent-browser click @e4</CommandLine>
</div>
<div className="mb-3">
<CommandLine>agent-browser diff snapshot</CommandLine>
</div>
<div
className="border-t pt-3"
style={{ borderColor: "var(--border)" }}
>
{snapshotDiffLines.map((line, i) => (
<DiffLine key={i} line={line} />
))}
<div className="mt-2 opacity-60">
<span className="text-green-400">3</span> additions,{" "}
<span className="text-red-400">2</span> removals,{" "}
<span>3</span> unchanged
</div>
</div>
</Terminal>
</div>
{/* Panel 2: Screenshot diff */}
<div>
<div
className="text-xs font-medium uppercase tracking-wider mb-3"
style={{ color: "var(--muted-foreground)" }}
>
Catch a visual regression
</div>
<Terminal>
<div className="mb-3">
<CommandLine>
agent-browser diff screenshot --baseline before-deploy.png
</CommandLine>
</div>
<div
className="border-t pt-3"
style={{ borderColor: "var(--border)" }}
>
<div className="text-red-400">
&#x2717; 2.37% pixels differ
</div>
<div className="opacity-50">
Diff image: ~/.agent-browser/tmp/diffs/diff-1708473621.png
</div>
<div className="opacity-50">
<span className="text-red-400">1,137</span> different /{" "}
48,000 total pixels
</div>
</div>
</Terminal>
<div className="flex gap-2 mt-3">
<PageMockup label="Baseline" buttonColor="#3b82f6" />
<PageMockup label="Current" buttonColor="#22c55e" />
<PageMockup label="Diff" buttonColor="#ef4444" diffMode />
</div>
</div>
</div>
);
}
-538
View File
@@ -1,538 +0,0 @@
"use client";
import {
useRef,
useEffect,
useState,
useCallback,
type PointerEvent as ReactPointerEvent,
} from "react";
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import { Streamdown } from "streamdown";
import Link from "next/link";
import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet";
const STORAGE_KEY = "docs-chat-messages";
const transport = new DefaultChatTransport({ api: "/api/docs-chat" });
const DESKTOP_DEFAULT_WIDTH = 400;
const DESKTOP_MIN_WIDTH = 300;
const DESKTOP_MAX_WIDTH = 700;
function setCookie(name: string, value: string) {
document.cookie = `${name}=${encodeURIComponent(value)};path=/;max-age=${60 * 60 * 24 * 365};samesite=lax`;
}
const TOOL_LABELS: Record<
string,
{ label: string; pastLabel: string; argKey?: string }
> = {
readFile: { label: "Reading", pastLabel: "Read", argKey: "path" },
bash: { label: "Running", pastLabel: "Ran", argKey: "command" },
};
function isToolPart(part: { type: string }): part is {
type: string;
toolCallId: string;
toolName?: string;
state: string;
input?: Record<string, unknown>;
output?: unknown;
errorText?: string;
} {
return part.type.startsWith("tool-") || part.type === "dynamic-tool";
}
function getToolName(part: { type: string; toolName?: string }): string {
if (part.type === "dynamic-tool") return part.toolName ?? "tool";
return part.type.replace(/^tool-/, "");
}
function ToolCallDisplay({
part,
}: {
part: {
type: string;
toolCallId: string;
toolName?: string;
state: string;
input?: Record<string, unknown>;
output?: unknown;
errorText?: string;
};
}) {
const toolName = getToolName(part);
const config = TOOL_LABELS[toolName] ?? {
label: toolName,
pastLabel: toolName,
};
const isDone = part.state === "output-available";
const isError = part.state === "output-error";
const isRunning = !isDone && !isError;
const displayLabel = isRunning ? config.label : config.pastLabel;
const args = (part.input ?? {}) as Record<string, unknown>;
const argValue = config.argKey ? args[config.argKey] : undefined;
const argPreview =
argValue != null
? String(argValue)
.replace(/^\/workspace\//, "/")
.replace(/\.md$/, "")
.replace(/\/index$/, "") || "/"
: "";
// Link to the docs page if it's a readFile path
const docsLink =
toolName === "readFile" && argPreview.startsWith("/") ? argPreview : null;
const argEl = argPreview ? (
docsLink ? (
<Link href={docsLink} className="truncate underline underline-offset-2">
{argPreview}
</Link>
) : (
<span className="truncate">{argPreview}</span>
)
) : null;
return (
<div className="text-xs py-0.5 min-w-0">
{isRunning ? (
<span className="inline-flex items-center gap-1 font-mono text-muted-foreground animate-tool-shimmer min-w-0 max-w-full">
<span className="shrink-0">{displayLabel}</span>
{argEl}
</span>
) : (
<span className="inline-flex items-center gap-1 font-mono text-muted-foreground/60 min-w-0 max-w-full">
<span className="shrink-0">{displayLabel}</span>
{argEl}
{isError && <span className="text-destructive">failed</span>}
</span>
)}
</div>
);
}
const SUGGESTIONS = [
"What is agent-browser?",
"How do I install it?",
"What commands are available?",
"How do snapshots work?",
"How do I use CDP mode?",
];
export function DocsChat({
defaultOpen = false,
defaultWidth = DESKTOP_DEFAULT_WIDTH,
}: {
defaultOpen?: boolean;
defaultWidth?: number;
}) {
const [open, setOpen] = useState(defaultOpen);
const [input, setInput] = useState("");
const [isDesktop, setIsDesktop] = useState(false);
const [hasMounted, setHasMounted] = useState(false);
const [desktopWidth, setDesktopWidth] = useState(
Math.min(DESKTOP_MAX_WIDTH, Math.max(DESKTOP_MIN_WIDTH, defaultWidth)),
);
const messagesScrollRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
const restoredRef = useRef(false);
const isDraggingRef = useRef(false);
const { messages, sendMessage, status, setMessages, error } = useChat({
transport,
});
const isLoading = status === "streaming" || status === "submitted";
const showMessages = messages.length > 0 || !!error || isLoading;
// Detect desktop vs mobile. Close sidebar on mobile if it was open from cookie.
useEffect(() => {
const mq = window.matchMedia("(min-width: 640px)");
setIsDesktop(mq.matches);
setHasMounted(true);
// If on mobile but sidebar was open from cookie, close it
if (!mq.matches && defaultOpen) {
setOpen(false);
}
const handler = (e: MediaQueryListEvent) => setIsDesktop(e.matches);
mq.addEventListener("change", handler);
return () => mq.removeEventListener("change", handler);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Persist open state to cookie (only after mount to avoid overwriting on mobile)
useEffect(() => {
if (hasMounted) {
setCookie("docs-chat-open", String(open));
}
}, [open, hasMounted]);
// Push page content on desktop when pane is open.
// Use padding on body so the page scrollbar stays at the viewport edge (behind the sidebar)
// instead of appearing right next to the sidebar's scrollbar.
useEffect(() => {
const body = document.body;
if (isDesktop && open) {
body.style.paddingRight = `${desktopWidth}px`;
if (!isDraggingRef.current) {
body.style.transition = "padding-right 150ms ease";
}
} else if (isDesktop) {
body.style.paddingRight = "0px";
body.style.transition = "padding-right 150ms ease";
}
return () => {
body.style.paddingRight = "0px";
body.style.transition = "";
};
}, [isDesktop, open, desktopWidth]);
// Resize handle drag
const handleResizePointerDown = useCallback(
(e: ReactPointerEvent<HTMLDivElement>) => {
e.preventDefault();
isDraggingRef.current = true;
document.documentElement.style.transition = "none";
const startX = e.clientX;
const startWidth = desktopWidth;
const onPointerMove = (ev: globalThis.PointerEvent) => {
const delta = startX - ev.clientX;
const newWidth = Math.min(
DESKTOP_MAX_WIDTH,
Math.max(DESKTOP_MIN_WIDTH, startWidth + delta),
);
setDesktopWidth(newWidth);
};
const onPointerUp = () => {
isDraggingRef.current = false;
document.documentElement.style.transition = "";
document.removeEventListener("pointermove", onPointerMove);
document.removeEventListener("pointerup", onPointerUp);
};
document.addEventListener("pointermove", onPointerMove);
document.addEventListener("pointerup", onPointerUp);
},
[desktopWidth],
);
// Persist width to cookie
useEffect(() => {
setCookie("docs-chat-width", String(desktopWidth));
}, [desktopWidth]);
// Restore messages from sessionStorage on mount
useEffect(() => {
if (restoredRef.current) return;
restoredRef.current = true;
try {
const stored = sessionStorage.getItem(STORAGE_KEY);
if (stored) {
const parsed = JSON.parse(stored);
if (Array.isArray(parsed) && parsed.length > 0) {
setMessages(parsed);
}
}
} catch {
// ignore parse errors
}
}, [setMessages]);
// Save completed messages to sessionStorage
useEffect(() => {
if (!restoredRef.current) return;
if (isLoading) return;
if (messages.length === 0) {
sessionStorage.removeItem(STORAGE_KEY);
return;
}
try {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(messages));
} catch {
// ignore quota errors
}
}, [messages, isLoading]);
// Cmd+K to open sidebar and focus prompt, Escape to close
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "i" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((prev) => {
if (!prev) {
setTimeout(() => inputRef.current?.focus(), 200);
}
return !prev;
});
}
if (e.key === "Escape" && open && isDesktop) {
setOpen(false);
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [open, isDesktop]);
// Auto-focus input when opened
useEffect(() => {
if (open) {
const timer = setTimeout(() => inputRef.current?.focus(), 200);
return () => clearTimeout(timer);
}
}, [open]);
// Auto-open when error occurs
useEffect(() => {
if (error) setOpen(true);
}, [error]);
// Scroll to bottom when messages change or error occurs
useEffect(() => {
const el = messagesScrollRef.current;
if (!el) return;
requestAnimationFrame(() => {
el.scrollTop = el.scrollHeight;
});
}, [messages, error]);
const handleSubmit = useCallback(
(e: React.FormEvent) => {
e.preventDefault();
if (!input.trim() || isLoading) return;
sendMessage({ text: input });
setInput("");
},
[input, isLoading, sendMessage],
);
const handleClear = useCallback(() => {
setMessages([]);
sessionStorage.removeItem(STORAGE_KEY);
}, [setMessages]);
const hasVisibleContent = (
parts: (typeof messages)[number]["parts"],
): boolean => {
return parts.some(
(p) => (p.type === "text" && p.text.length > 0) || isToolPart(p),
);
};
// Shared chat panel content used by both desktop and mobile
const chatPanel = (
<>
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-border/50 shrink-0">
<span className="text-sm font-medium">agent-browser Docs</span>
<div className="flex items-center gap-3">
{showMessages && (
<button
onClick={handleClear}
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
aria-label="Clear conversation"
>
Clear
</button>
)}
<button
onClick={() => setOpen(false)}
className="text-muted-foreground hover:text-foreground transition-colors"
aria-label="Close panel"
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
</div>
{/* Content: suggestions or messages */}
{showMessages ? (
<div
ref={messagesScrollRef}
className="flex-1 min-h-0 p-4 space-y-4 overflow-y-auto"
>
{messages.map((message) => {
if (!hasVisibleContent(message.parts)) return null;
return (
<div key={message.id}>
{message.role === "user" ? (
<div className="text-sm text-muted-foreground whitespace-pre-wrap leading-relaxed">
{message.parts
.filter(
(p): p is Extract<typeof p, { type: "text" }> =>
p.type === "text",
)
.map((p) => p.text)
.join("")}
</div>
) : (
<div className="space-y-2">
{message.parts.map((part, i) => {
if (part.type === "text" && part.text) {
return (
<div
key={i}
className="docs-chat-content text-sm text-foreground leading-relaxed prose prose-sm dark:prose-invert max-w-none"
>
<Streamdown>{part.text}</Streamdown>
</div>
);
}
if (isToolPart(part)) {
return (
<ToolCallDisplay key={part.toolCallId} part={part} />
);
}
return null;
})}
</div>
)}
</div>
);
})}
{error && (
<div className="text-sm text-destructive/80 bg-destructive/10 rounded-md px-3 py-2">
{(() => {
try {
const parsed = JSON.parse(error.message);
return parsed.message || parsed.error || error.message;
} catch {
return (
error.message || "Something went wrong. Please try again."
);
}
})()}
</div>
)}
</div>
) : (
<div className="flex-1 min-h-0 flex flex-col">
<div className="flex flex-wrap gap-2 p-4">
{SUGGESTIONS.map((s) => (
<button
key={s}
type="button"
onClick={() => {
sendMessage({ text: s });
}}
className="text-xs px-3 py-1.5 rounded-full border bg-secondary font-medium text-muted-foreground hover:text-foreground transition-colors"
>
{s}
</button>
))}
</div>
</div>
)}
{/* Input bar */}
<form
onSubmit={handleSubmit}
className="flex items-end gap-2 px-4 py-3 border-t border-border/50 shrink-0"
>
<textarea
ref={inputRef}
value={input}
onChange={(e) => {
setInput(e.target.value);
e.target.style.height = "auto";
e.target.style.height = `${e.target.scrollHeight}px`;
}}
rows={1}
enterKeyHint="send"
placeholder="Ask a question..."
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSubmit(e);
}
}}
className="flex-1 bg-transparent text-base sm:text-sm text-foreground outline-none disabled:opacity-50 resize-none max-h-32 leading-relaxed placeholder:text-muted-foreground"
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="bg-primary text-primary-foreground rounded-full p-1.5 hover:bg-primary/90 transition-colors disabled:opacity-30 shrink-0"
aria-label="Send message"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="12" y1="19" x2="12" y2="5" />
<polyline points="5 12 12 5 19 12" />
</svg>
</button>
</form>
</>
);
return (
<>
{/* Ask AI trigger button */}
{!open && (
<button
onClick={() => setOpen(true)}
className="fixed z-50 bottom-4 left-1/2 -translate-x-1/2 sm:left-auto sm:translate-x-0 sm:right-4 flex items-center gap-2 px-4 py-2 rounded-lg bg-primary text-primary-foreground shadow-lg hover:opacity-90 transition-opacity text-sm font-medium"
aria-label="Ask AI"
>
Ask AI
<kbd className="hidden sm:inline-flex items-center gap-0.5 text-xs opacity-60 font-mono">
<span>&#8984;</span>I
</kbd>
</button>
)}
{/* Desktop: resizable side pane -- always rendered, hidden on mobile via CSS */}
<aside
className={`hidden sm:flex fixed top-0 right-0 bottom-0 z-40 border-l border-border/50 bg-background transition-transform duration-150 ease-in-out ${open ? "translate-x-0" : "translate-x-full"}`}
style={{ width: desktopWidth }}
aria-hidden={!open}
>
{/* Resize handle */}
<div
onPointerDown={handleResizePointerDown}
className="absolute top-0 bottom-0 left-0 w-1.5 cursor-col-resize hover:bg-ring/30 active:bg-ring/50 transition-colors z-10"
/>
<div className="flex flex-col flex-1 min-w-0">{chatPanel}</div>
</aside>
{/* Mobile: Sheet overlay/drawer -- only after mount to avoid flash on desktop */}
{hasMounted && !isDesktop && (
<Sheet open={open} onOpenChange={setOpen}>
<SheetContent
side="right"
showCloseButton={false}
overlayClassName="bg-background!"
className="inset-0! w-full! h-full! max-w-none! border-l-0! p-0 flex flex-col"
style={{ backgroundColor: "var(--background)", opacity: 1 }}
>
<SheetTitle className="sr-only">AI Chat</SheetTitle>
{chatPanel}
</SheetContent>
</Sheet>
)}
</>
);
}
-81
View File
@@ -1,81 +0,0 @@
"use client";
import { useState, useMemo } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
Sheet,
SheetTrigger,
SheetContent,
SheetTitle,
} from "@/components/ui/sheet";
import { navigation, allDocsPages } from "@/lib/docs-navigation";
export function DocsMobileNav() {
const [open, setOpen] = useState(false);
const pathname = usePathname();
const currentPage = useMemo(() => {
const page = allDocsPages.find((p) => p.href === pathname);
return page ?? allDocsPages[0];
}, [pathname]);
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger className="lg:hidden sticky top-14 z-40 w-full px-6 py-3 bg-background/80 backdrop-blur-sm border-b border-border flex items-center justify-between focus:outline-none">
<div className="text-sm font-medium">{currentPage?.name}</div>
<div className="w-8 h-8 flex items-center justify-center">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-muted-foreground"
>
<line x1="8" y1="6" x2="21" y2="6" />
<line x1="8" y1="12" x2="21" y2="12" />
<line x1="8" y1="18" x2="21" y2="18" />
<line x1="3" y1="6" x2="3.01" y2="6" />
<line x1="3" y1="12" x2="3.01" y2="12" />
<line x1="3" y1="18" x2="3.01" y2="18" />
</svg>
</div>
</SheetTrigger>
<SheetContent side="left" showCloseButton={false} className="overflow-y-auto p-6">
<SheetTitle className="mb-6">Table of Contents</SheetTitle>
<nav className="space-y-6">
{navigation.map((section, sectionIndex) => (
<div key={section.title ?? sectionIndex}>
{section.title && (
<h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2">
{section.title}
</h4>
)}
<ul className="space-y-1">
{section.items.map((item) => (
<li key={item.href}>
<Link
href={item.href}
onClick={() => setOpen(false)}
className={`text-sm block py-2 transition-colors ${
pathname === item.href
? "text-primary font-medium"
: "text-muted-foreground hover:text-foreground"
}`}
>
{item.name}
</Link>
</li>
))}
</ul>
</div>
))}
</nav>
</SheetContent>
</Sheet>
);
}
-44
View File
@@ -1,44 +0,0 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
import { navigation } from "@/lib/docs-navigation";
export function DocsSidebar() {
const pathname = usePathname();
return (
<nav className="space-y-6 pb-8">
{navigation.map((section, sectionIndex) => (
<div key={section.title ?? sectionIndex}>
{section.title && (
<h4 className="text-xs font-normal text-muted-foreground/50 uppercase tracking-wider mb-2">
{section.title}
</h4>
)}
<ul className="space-y-1">
{section.items.map((item) => {
const isActive = pathname === item.href;
return (
<li key={item.href}>
<Link
href={item.href}
className={cn(
"text-sm transition-colors block py-1",
isActive
? "text-primary font-medium"
: "text-muted-foreground hover:text-foreground",
)}
>
{item.name}
</Link>
</li>
);
})}
</ul>
</div>
))}
</nav>
);
}
-86
View File
@@ -1,86 +0,0 @@
import Link from "next/link";
import { ThemeToggle } from "./theme-toggle";
import { Search } from "./search";
import { getStarCount } from "@/lib/github";
export async function Header() {
const stars = await getStarCount();
return (
<header className="sticky top-0 z-50 bg-white/90 backdrop-blur-sm dark:bg-neutral-950/90">
<div className="flex h-14 items-center justify-between px-4 gap-6">
<div className="flex items-center gap-2">
<Link href="https://vercel.com" title="Made with love by Vercel">
<svg
data-testid="geist-icon"
height="18"
strokeLinejoin="round"
viewBox="0 0 16 16"
width="18"
style={{ color: "currentcolor" }}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8 1L16 15H0L8 1Z"
fill="currentColor"
/>
</svg>
</Link>
<span className="text-neutral-300 dark:text-neutral-700">
<svg
data-testid="geist-icon"
height="16"
strokeLinejoin="round"
viewBox="0 0 16 16"
width="16"
style={{ color: "currentcolor" }}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.01526 15.3939L4.3107 14.7046L10.3107 0.704556L10.6061 0.0151978L11.9849 0.606077L11.6894 1.29544L5.68942 15.2954L5.39398 15.9848L4.01526 15.3939Z"
fill="currentColor"
/>
</svg>
</span>
<Link href="/">
<span
className="font-medium tracking-tight text-lg"
style={{ fontFamily: "var(--font-geist-pixel-square)" }}
>
agent-browser
</span>
</Link>
</div>
<nav className="flex items-center gap-4">
<Search />
<a
href="https://github.com/vercel-labs/agent-browser"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-sm text-neutral-500 hover:text-neutral-900 transition-colors dark:text-neutral-400 dark:hover:text-neutral-100"
>
<svg
viewBox="0 0 16 16"
className="h-4 w-4"
fill="currentColor"
aria-hidden="true"
>
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
</svg>
{stars && <span>{stars}</span>}
</a>
<a
href="https://www.npmjs.com/package/agent-browser"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-neutral-500 hover:text-neutral-900 transition-colors dark:text-neutral-400 dark:hover:text-neutral-100"
>
npm
</a>
<ThemeToggle />
</nav>
</div>
</header>
);
}
-265
View File
@@ -1,265 +0,0 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
type SearchResult = {
title: string;
href: string;
section: string;
snippet: string;
};
export function Search() {
const router = useRouter();
const [open, setOpen] = useState(false);
const [query, setQuery] = useState("");
const [results, setResults] = useState<SearchResult[]>([]);
const [loading, setLoading] = useState(false);
const [activeIndex, setActiveIndex] = useState(0);
const inputRef = useRef<HTMLInputElement>(null);
const listRef = useRef<HTMLDivElement>(null);
const abortRef = useRef<AbortController | null>(null);
const navigate = useCallback(
(href: string) => {
setOpen(false);
setQuery("");
setResults([]);
router.push(href);
},
[router],
);
useEffect(() => {
function onKeyDown(e: KeyboardEvent) {
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
setOpen((prev) => !prev);
}
}
document.addEventListener("keydown", onKeyDown);
return () => document.removeEventListener("keydown", onKeyDown);
}, []);
useEffect(() => {
if (open) {
setTimeout(() => inputRef.current?.focus(), 0);
} else {
setQuery("");
setResults([]);
}
}, [open]);
useEffect(() => {
const q = query.trim();
if (!q) {
setResults([]);
setLoading(false);
return;
}
setLoading(true);
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
const timeout = setTimeout(async () => {
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, {
signal: controller.signal,
});
if (res.ok) {
const data = await res.json();
setResults(data.results);
}
} catch {
// aborted or network error
} finally {
if (!controller.signal.aborted) {
setLoading(false);
}
}
}, 150);
return () => {
clearTimeout(timeout);
controller.abort();
};
}, [query]);
useEffect(() => {
setActiveIndex(0);
}, [results]);
function handleKeyDown(e: React.KeyboardEvent) {
if (e.key === "ArrowDown") {
e.preventDefault();
setActiveIndex((i) => Math.min(i + 1, results.length - 1));
} else if (e.key === "ArrowUp") {
e.preventDefault();
setActiveIndex((i) => Math.max(i - 1, 0));
} else if (e.key === "Enter" && results[activeIndex]) {
e.preventDefault();
navigate(results[activeIndex].href);
}
}
useEffect(() => {
const active = listRef.current?.querySelector("[data-active='true']");
active?.scrollIntoView({ block: "nearest" });
}, [activeIndex]);
const hasQuery = query.trim().length > 0;
return (
<>
<button
onClick={() => setOpen(true)}
className="hidden sm:flex items-center gap-2 rounded-md border border-border/50 bg-muted/50 px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground hover:border-foreground/25 transition-colors"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</svg>
Search docs
<kbd className="pointer-events-none ml-1 inline-flex items-center gap-0.5 rounded border border-border/50 bg-background px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">
<span>&#8984;</span>K
</kbd>
</button>
<button
onClick={() => setOpen(true)}
className="sm:hidden flex items-center text-muted-foreground hover:text-foreground transition-colors"
aria-label="Search docs"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</svg>
</button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent showCloseButton={false} className="gap-0 p-0 sm:max-w-lg">
<DialogTitle className="sr-only">Search documentation</DialogTitle>
<div className="flex items-center gap-2 border-b border-border/50 px-3">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="shrink-0 text-muted-foreground"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</svg>
<input
ref={inputRef}
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Search docs..."
className="flex-1 bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground"
/>
{query && (
<button
onClick={() => setQuery("")}
className="text-muted-foreground hover:text-foreground"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
)}
</div>
<div
ref={listRef}
className="max-h-[min(60vh,400px)] overflow-y-auto p-2"
>
{loading && hasQuery ? (
<div className="flex items-center justify-center py-6">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent" />
</div>
) : hasQuery && results.length === 0 ? (
<p className="py-6 text-center text-sm text-muted-foreground">
No results found.
</p>
) : !hasQuery ? (
<p className="py-6 text-center text-sm text-muted-foreground">
Type to search documentation...
</p>
) : (
results.map((item, i) => (
<button
key={item.href}
data-active={i === activeIndex}
onClick={() => navigate(item.href)}
onMouseEnter={() => setActiveIndex(i)}
className={cn(
"flex w-full flex-col gap-1 rounded-md px-3 py-2 text-left transition-colors",
i === activeIndex
? "bg-muted text-foreground"
: "text-foreground",
)}
>
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium">{item.title}</span>
{item.section && (
<span className="shrink-0 text-xs text-muted-foreground">
{item.section}
</span>
)}
</div>
{item.snippet && (
<span className="line-clamp-2 text-xs text-muted-foreground leading-relaxed">
{item.snippet}
</span>
)}
</button>
))
)}
</div>
</DialogContent>
</Dialog>
</>
);
}
-16
View File
@@ -1,16 +0,0 @@
"use client";
import { ThemeProvider as NextThemesProvider } from "next-themes";
export function ThemeProvider({ children }: { children: React.ReactNode }) {
return (
<NextThemesProvider
attribute="class"
defaultTheme="dark"
enableSystem
disableTransitionOnChange
>
{children}
</NextThemesProvider>
);
}
-61
View File
@@ -1,61 +0,0 @@
"use client";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return <div className="w-8 h-8" />;
}
return (
<button
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
className="w-8 h-8 flex items-center justify-center rounded-md text-neutral-500 hover:text-neutral-900 hover:bg-neutral-100 transition-colors dark:text-neutral-400 dark:hover:text-neutral-100 dark:hover:bg-neutral-800"
aria-label="Toggle theme"
>
{theme === "dark" ? (
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2" />
<path d="M12 20v2" />
<path d="m4.93 4.93 1.41 1.41" />
<path d="m17.66 17.66 1.41 1.41" />
<path d="M2 12h2" />
<path d="M20 12h2" />
<path d="m6.34 17.66-1.41 1.41" />
<path d="m19.07 4.93-1.41 1.41" />
</svg>
) : (
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
</svg>
)}
</button>
);
}
-92
View File
@@ -1,92 +0,0 @@
"use client";
import * as React from "react";
import { Dialog as DialogPrimitive } from "radix-ui";
import { cn } from "@/lib/utils";
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
);
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean;
}) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border border-border/50 p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
className,
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
);
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
);
}
export { Dialog, DialogPortal, DialogOverlay, DialogContent, DialogTitle };
-147
View File
@@ -1,147 +0,0 @@
"use client"
import * as React from "react"
import { Dialog as SheetPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
}
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
}
function SheetOverlay({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function SheetContent({
className,
children,
side = "right",
showCloseButton = true,
overlayClassName,
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left"
showCloseButton?: boolean
overlayClassName?: string
}) {
return (
<SheetPortal>
<SheetOverlay className={overlayClassName} />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r border-border/50 sm:max-w-sm",
side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className
)}
{...props}
>
{children}
{showCloseButton && (
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
)}
</SheetPrimitive.Content>
</SheetPortal>
)
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
)
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function SheetTitle({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
}
function SheetDescription({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}
-70
View File
@@ -1,70 +0,0 @@
export type NavItem = {
name: string;
href: string;
};
export type NavSection = {
title: string | null;
items: NavItem[];
};
export const navigation: NavSection[] = [
{
title: null,
items: [
{ name: "Introduction", href: "/" },
{ name: "Installation", href: "/installation" },
{ name: "Quick Start", href: "/quick-start" },
{ name: "Skills", href: "/skills" },
],
},
{
title: "Reference",
items: [
{ name: "Commands", href: "/commands" },
{ name: "Configuration", href: "/configuration" },
{ name: "Selectors", href: "/selectors" },
{ name: "Snapshots", href: "/snapshots" },
],
},
{
title: "Features",
items: [
{ name: "Sessions", href: "/sessions" },
{ name: "Dashboard", href: "/dashboard" },
{ name: "Diffing", href: "/diffing" },
{ name: "CDP Mode", href: "/cdp-mode" },
{ name: "Streaming", href: "/streaming" },
{ name: "Profiler", href: "/profiler" },
{ name: "iOS Simulator", href: "/ios" },
{ name: "Security", href: "/security" },
{ name: "Next.js + Vercel", href: "/next" },
{ name: "Native Mode", href: "/native-mode" },
],
},
{
title: "Providers",
items: [
{ name: "AgentCore", href: "/providers/agentcore" },
{ name: "Browser Use", href: "/providers/browser-use" },
{ name: "Browserbase", href: "/providers/browserbase" },
{ name: "Browserless", href: "/providers/browserless" },
{ name: "Kernel", href: "/providers/kernel" },
],
},
{
title: "Engines",
items: [
{ name: "Chrome", href: "/engines/chrome" },
{ name: "Lightpanda", href: "/engines/lightpanda" },
],
},
{
title: null,
items: [{ name: "Changelog", href: "/changelog" }],
},
];
export const allDocsPages: NavItem[] = navigation.flatMap(
(section) => section.items
);
-47
View File
@@ -1,47 +0,0 @@
/**
* Converts raw MDX content to clean Markdown suitable for AI agents.
*
* Strips export/import statements and standalone JSX divs with className
* attributes, passing everything else through as valid Markdown.
*/
export function mdxToCleanMarkdown(raw: string): string {
const lines = raw.split("\n");
const out: string[] = [];
let inJsxBlock = false;
let jsxDepth = 0;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith("export ") || trimmed.startsWith("import ")) {
continue;
}
if (
!inJsxBlock &&
trimmed.startsWith("<div ") &&
trimmed.includes("className=")
) {
inJsxBlock = true;
jsxDepth = 1;
continue;
}
if (inJsxBlock) {
const opens = (line.match(/<div[\s>]/g) || []).length;
const closes = (line.match(/<\/div>/g) || []).length;
jsxDepth += opens - closes;
if (jsxDepth <= 0) {
inJsxBlock = false;
jsxDepth = 0;
}
continue;
}
out.push(line);
}
let result = out.join("\n");
result = result.replace(/^\n+/, "\n").trim();
return result;
}
-39
View File
@@ -1,39 +0,0 @@
import type { Metadata } from "next";
import { PAGE_TITLES } from "./page-titles";
const DESCRIPTION =
"Browser automation CLI for AI agents";
export function pageMetadata(slug: string): Metadata {
const title = PAGE_TITLES[slug];
if (!title) return {};
const displayTitle = title.replace(/\n/g, " ");
const fullTitle = `${displayTitle} | agent-browser`;
const ogImageUrl = slug ? `/og/${slug}` : "/og";
return {
title: displayTitle,
openGraph: {
type: "website",
locale: "en_US",
siteName: "agent-browser",
title: fullTitle,
description: DESCRIPTION,
images: [
{
url: ogImageUrl,
width: 1200,
height: 630,
alt: `${displayTitle} - agent-browser`,
},
],
},
twitter: {
card: "summary_large_image",
title: fullTitle,
description: DESCRIPTION,
images: [ogImageUrl],
},
};
}
-32
View File
@@ -1,32 +0,0 @@
export const PAGE_TITLES: Record<string, string> = {
"": "Browser\nAutomation for AI",
installation: "Installation",
"quick-start": "Quick Start",
skills: "Skills",
commands: "Commands",
configuration: "Configuration",
selectors: "Selectors",
snapshots: "Snapshots",
sessions: "Sessions",
diffing: "Diffing",
"cdp-mode": "CDP Mode",
dashboard: "Dashboard",
streaming: "Streaming",
profiler: "Profiler",
ios: "iOS Simulator",
security: "Security",
"engines/chrome": "Chrome",
"engines/lightpanda": "Lightpanda",
next: "Next.js + Vercel",
"native-mode": "Native Mode",
"providers/agentcore": "AgentCore",
"providers/browser-use": "Browser Use",
"providers/browserbase": "Browserbase",
"providers/browserless": "Browserless",
"providers/kernel": "Kernel",
changelog: "Changelog",
};
export function getPageTitle(slug: string): string | null {
return slug in PAGE_TITLES ? PAGE_TITLES[slug]! : null;
}
-57
View File
@@ -1,57 +0,0 @@
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
// Lazy initialization to avoid errors when Redis env vars are not configured
let _minuteRateLimit: Ratelimit | null = null;
let _dailyRateLimit: Ratelimit | null = null;
function getRedis(): Redis | null {
const url = process.env.KV_REST_API_URL;
const token = process.env.KV_REST_API_TOKEN;
if (!url || !token) {
return null;
}
return new Redis({ url, token });
}
// No-op rate limiter for when Redis is not configured
const noopRateLimiter = {
limit: async () => ({ success: true, limit: 0, remaining: 0, reset: 0 }),
};
const MINUTE_LIMIT = Number(process.env.RATE_LIMIT_PER_MINUTE) || 10;
const DAILY_LIMIT = Number(process.env.RATE_LIMIT_PER_DAY) || 100;
// Requests per minute (sliding window)
export const minuteRateLimit = {
limit: async (identifier: string) => {
if (!_minuteRateLimit) {
const redis = getRedis();
if (!redis) return noopRateLimiter.limit();
_minuteRateLimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(MINUTE_LIMIT, "1 m"),
prefix: "ratelimit:minute",
});
}
return _minuteRateLimit.limit(identifier);
},
};
// Requests per day (fixed window)
export const dailyRateLimit = {
limit: async (identifier: string) => {
if (!_dailyRateLimit) {
const redis = getRedis();
if (!redis) return noopRateLimiter.limit();
_dailyRateLimit = new Ratelimit({
redis,
limiter: Ratelimit.fixedWindow(DAILY_LIMIT, "1 d"),
prefix: "ratelimit:daily",
});
}
return _dailyRateLimit.limit(identifier);
},
};
-66
View File
@@ -1,66 +0,0 @@
import { readFile } from "fs/promises";
import { join } from "path";
import { navigation } from "./docs-navigation";
import { mdxToCleanMarkdown } from "./mdx-to-markdown";
export type IndexEntry = {
title: string;
href: string;
section: string;
content: string;
};
let cached: IndexEntry[] | null = null;
function stripMarkdown(md: string): string {
return md
.replace(/```[\s\S]*?```/g, "")
.replace(/`[^`]+`/g, "")
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
.replace(/^#{1,6}\s+/gm, "")
.replace(/\*{1,3}([^*]+)\*{1,3}/g, "$1")
.replace(/<[^>]+>/g, "")
.replace(/\n{3,}/g, "\n\n")
.trim();
}
function mdxFileForSlug(slug: string): string {
const docsRoot = join(process.cwd(), "src", "app");
if (slug === "/") {
return join(docsRoot, "page.mdx");
}
const rest = slug.replace(/^\//, "");
return join(docsRoot, ...rest.split("/"), "page.mdx");
}
export async function getSearchIndex(): Promise<IndexEntry[]> {
if (cached) return cached;
const entries: IndexEntry[] = [];
for (const section of navigation) {
for (const item of section.items) {
try {
const raw = await readFile(mdxFileForSlug(item.href), "utf-8");
const md = mdxToCleanMarkdown(raw);
const content = stripMarkdown(md);
entries.push({
title: item.name,
href: item.href,
section: section.title ?? "",
content,
});
} catch {
entries.push({
title: item.name,
href: item.href,
section: section.title ?? "",
content: "",
});
}
}
}
cached = entries;
return entries;
}
-6
View File
@@ -1,6 +0,0 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
-34
View File
@@ -1,34 +0,0 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}