diff --git a/docs/package.json b/docs/package.json
index dda5e2e..a57fa7b 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -42,7 +42,7 @@
"eslint": "^9",
"eslint-config-next": "16.1.1",
"tailwindcss": "^4",
- "tw-animate-css": "^1.4.0",
+ "tailwindcss-animate": "^1.0.7",
"typescript": "^5"
}
}
diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml
index 00770ca..0f82fdb 100644
--- a/docs/pnpm-lock.yaml
+++ b/docs/pnpm-lock.yaml
@@ -102,9 +102,9 @@ importers:
tailwindcss:
specifier: ^4
version: 4.1.18
- tw-animate-css:
- specifier: ^1.4.0
- version: 1.4.0
+ tailwindcss-animate:
+ specifier: ^1.0.7
+ version: 1.0.7(tailwindcss@4.1.18)
typescript:
specifier: ^5
version: 5.9.3
@@ -3467,6 +3467,11 @@ packages:
tailwind-merge@3.4.0:
resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==}
+ tailwindcss-animate@1.0.7:
+ resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==}
+ peerDependencies:
+ tailwindcss: '>=3.0.0 || insiders'
+
tailwindcss@4.1.18:
resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==}
@@ -3521,9 +3526,6 @@ packages:
turndown@7.2.2:
resolution: {integrity: sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==}
- tw-animate-css@1.4.0:
- resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
-
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@@ -7827,6 +7829,10 @@ snapshots:
tailwind-merge@3.4.0: {}
+ tailwindcss-animate@1.0.7(tailwindcss@4.1.18):
+ dependencies:
+ tailwindcss: 4.1.18
+
tailwindcss@4.1.18: {}
tapable@2.3.0: {}
@@ -7891,8 +7897,6 @@ snapshots:
dependencies:
'@mixmark-io/domino': 2.2.0
- tw-animate-css@1.4.0: {}
-
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
diff --git a/docs/src/app/api/docs-markdown/route.ts b/docs/src/app/api/docs-markdown/route.ts
new file mode 100644
index 0000000..bffe3bf
--- /dev/null
+++ b/docs/src/app/api/docs-markdown/route.ts
@@ -0,0 +1,40 @@
+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 });
+ }
+}
diff --git a/docs/src/app/globals.css b/docs/src/app/globals.css
index 49655ef..e7312a7 100644
--- a/docs/src/app/globals.css
+++ b/docs/src/app/globals.css
@@ -1,117 +1,112 @@
@import "tailwindcss";
-@import "tw-animate-css";
+@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);
+}
+
:root {
- --radius: 0.5rem;
- --background: oklch(1.0 0 0);
- --foreground: oklch(0.1 0 0);
- --card: oklch(0.98 0 0);
- --card-foreground: oklch(0.1 0 0);
- --popover: oklch(0.98 0 0);
- --popover-foreground: oklch(0.1 0 0);
- --primary: oklch(0.1 0 0);
- --primary-foreground: oklch(1.0 0 0);
- --secondary: oklch(0.92 0 0);
- --secondary-foreground: oklch(0.1 0 0);
- --muted: oklch(0.92 0 0);
- --muted-foreground: oklch(0.45 0 0);
- --accent: oklch(0.92 0 0);
- --accent-foreground: oklch(0.1 0 0);
- --destructive: oklch(0.55 0.2 25);
- --destructive-foreground: oklch(1.0 0 0);
- --border: oklch(0.85 0 0);
- --input: oklch(0.85 0 0);
- --ring: oklch(0.6 0 0);
- --chat-bg: oklch(0.95 0 0);
+ --background: #fff;
+ --foreground: #171717;
+ --border: #e5e5e5;
+ --muted: #f5f5f5;
+ --muted-foreground: #737373;
+ --primary: #171717;
+ --primary-foreground: #fff;
}
.dark {
- --background: oklch(0.0 0 0);
- --foreground: oklch(0.98 0 0);
- --card: oklch(0.08 0 0);
- --card-foreground: oklch(0.98 0 0);
- --popover: oklch(0.08 0 0);
- --popover-foreground: oklch(0.98 0 0);
- --primary: oklch(0.98 0 0);
- --primary-foreground: oklch(0.0 0 0);
- --secondary: oklch(0.15 0 0);
- --secondary-foreground: oklch(0.98 0 0);
- --muted: oklch(0.15 0 0);
- --muted-foreground: oklch(0.6 0 0);
- --accent: oklch(0.15 0 0);
- --accent-foreground: oklch(0.1 0 0);
- --destructive: oklch(0.65 0.2 25);
- --destructive-foreground: oklch(0.98 0 0);
- --border: oklch(0.25 0 0);
- --input: oklch(0.25 0 0);
- --ring: oklch(0.4 0 0);
- --chat-bg: oklch(0.25 0 0);
+ --background: #0a0a0a;
+ --foreground: #f5f5f5;
+ --border: #262626;
+ --muted: #262626;
+ --muted-foreground: #a3a3a3;
+ --primary: #f5f5f5;
+ --primary-foreground: #0a0a0a;
}
-@custom-variant dark (&:is(.dark *));
-
-@theme inline {
- --radius-sm: calc(var(--radius) - 4px);
- --radius-md: calc(var(--radius) - 2px);
- --radius-lg: var(--radius);
- --radius-xl: calc(var(--radius) + 4px);
- --radius-2xl: calc(var(--radius) + 8px);
- --color-background: var(--background);
- --color-foreground: var(--foreground);
- --color-card: var(--card);
- --color-card-foreground: var(--card-foreground);
- --color-popover: var(--popover);
- --color-popover-foreground: var(--popover-foreground);
- --color-primary: var(--primary);
- --color-primary-foreground: var(--primary-foreground);
- --color-secondary: var(--secondary);
- --color-secondary-foreground: var(--secondary-foreground);
- --color-muted: var(--muted);
- --color-muted-foreground: var(--muted-foreground);
- --color-accent: var(--accent);
- --color-accent-foreground: var(--accent-foreground);
- --color-destructive: var(--destructive);
- --color-destructive-foreground: var(--destructive-foreground);
- --color-border: var(--border);
- --color-input: var(--input);
- --color-ring: var(--ring);
- --font-sans: var(--font-geist);
- --font-mono: var(--font-geist-mono);
-}
-
-* {
- border-color: var(--border);
-}
-
-body {
- background: var(--background);
- color: var(--foreground);
- font-family: var(--font-geist), system-ui, sans-serif;
-}
-
-/* Hide page scrollbar */
html {
- scrollbar-width: none;
+ scroll-behavior: smooth;
}
-html::-webkit-scrollbar {
- display: none;
+::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 {
- background: var(--card) !important;
border: 1px solid var(--border);
border-radius: 4px;
padding: 0.875rem;
overflow-x: auto;
- font-family: var(--font-geist-mono), monospace;
font-size: 0.8125rem;
line-height: 1.7;
}
+pre:not(.shiki) {
+ background: var(--muted);
+}
+
.code-block pre {
margin: 0;
}
@@ -127,17 +122,26 @@ pre {
}
}
-code {
- font-family: var(--font-geist-mono), monospace;
-}
-
:not(pre) > code {
- background: var(--card);
+ background: var(--muted);
padding: 0.125rem 0.375rem;
border-radius: 3px;
font-size: 0.875em;
}
+/* 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%;
@@ -145,9 +149,9 @@ code {
.prose h1 {
font-size: 1.5rem;
- font-weight: 500;
+ font-weight: 600;
letter-spacing: -0.02em;
- margin-bottom: 0.5rem;
+ margin-bottom: 1.5rem;
color: var(--foreground);
}
@@ -158,59 +162,102 @@ code {
}
.prose h2 {
- font-size: 0.875rem;
- font-weight: 500;
- letter-spacing: 0;
- text-transform: uppercase;
- color: var(--muted-foreground);
+ 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: 0.875rem;
- font-weight: 500;
+ font-size: 1rem;
+ font-weight: 600;
margin-top: 2rem;
margin-bottom: 0.75rem;
color: var(--foreground);
- opacity: 0.85;
}
.prose p {
- margin-bottom: 1.25rem;
- line-height: 1.7;
- color: var(--muted-foreground);
+ 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: 1.25rem;
+ margin-bottom: 1rem;
padding-left: 1.25rem;
}
+.prose ul {
+ list-style-type: disc;
+}
+
+.prose ol {
+ list-style-type: decimal;
+}
+
.prose li {
- margin-bottom: 0.5rem;
- color: var(--muted-foreground);
+ 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);
- opacity: 0.85;
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;
@@ -284,7 +331,3 @@ code {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
-
-button {
- cursor: pointer;
-}
diff --git a/docs/src/app/layout.tsx b/docs/src/app/layout.tsx
index 02075a5..445b084 100644
--- a/docs/src/app/layout.tsx
+++ b/docs/src/app/layout.tsx
@@ -1,18 +1,19 @@
import type { Metadata } from "next";
-import { Geist, Geist_Mono } from "next/font/google";
+import { Inter, Geist_Mono } from "next/font/google";
import { GeistPixelSquare } from "geist/font/pixel";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
-import { MobileNavProvider } from "@/components/mobile-nav-context";
import { Header } from "@/components/header";
-import { Sidebar } from "@/components/sidebar";
+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 geist = Geist({
- variable: "--font-geist",
+const inter = Inter({
+ variable: "--font-inter",
subsets: ["latin"],
});
@@ -66,23 +67,23 @@ export default async function RootLayout({
)}
-
-
-
-
-
-
-
+
+
+
+
diff --git a/docs/src/app/skills/page.mdx b/docs/src/app/skills/page.mdx
new file mode 100644
index 0000000..eaaf074
--- /dev/null
+++ b/docs/src/app/skills/page.mdx
@@ -0,0 +1,60 @@
+import { pageMetadata } from "@/lib/page-metadata"
+
+export const metadata = pageMetadata("skills")
+
+# 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.
+
+## Available Skills
+
+- **agent-browser** — General browser automation: navigation, snapshots, forms, screenshots, data extraction, sessions, authentication, diffing, and the full command reference.
+- **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. This is how agent-browser drives native desktop apps like the Slack macOS app.
+- **slack** — Browser-based Slack automation. Check unreads, navigate channels, search conversations, send messages, and extract data — no API tokens needed.
+
+## Installation
+
+```bash
+npx skills add vercel-labs/agent-browser --skill agent-browser
+npx skills add vercel-labs/agent-browser --skill dogfood
+npx skills add vercel-labs/agent-browser --skill electron
+npx skills add vercel-labs/agent-browser --skill slack
+```
+
+After installing, your AI agent will automatically activate the right skill when it encounters a matching request.
+
+## agent-browser
+
+The core skill. Teaches agents the full agent-browser API: the navigate-snapshot-interact-re-snapshot workflow, all commands, command chaining, authentication (auth vault and state persistence), sessions, diffing, JavaScript evaluation, annotated screenshots, semantic locators, and configuration.
+
+Example agent interactions:
+
+- "Open example.com and fill out the contact form"
+- "Take a screenshot of the dashboard after logging in"
+- "Compare staging and production versions of the homepage"
+
+## dogfood
+
+A structured workflow for exploratory testing. The agent opens a target URL, systematically explores the app (navigating pages, testing forms, clicking buttons, checking console errors), and documents every issue it finds with:
+
+- Numbered repro steps
+- Step-by-step screenshots
+- Repro videos for interactive bugs
+- Severity classification
+
+The output is a markdown report in an output directory, ready to hand to the responsible team. Run it with a single prompt like "dogfood vercel.com" or "QA http://localhost:3000 — focus on the billing page".
+
+## electron
+
+Electron apps (VS Code, Slack, Discord, Figma, Notion, Spotify, etc.) are built on Chromium and expose a Chrome DevTools Protocol (CDP) port that agent-browser can connect to. This skill teaches agents how to launch or connect to any Electron app, then use the standard snapshot-interact workflow to automate it.
+
+Electron apps are built on Chromium, so they expose a Chrome DevTools Protocol (CDP) port that agent-browser can connect to. Launch the app with `--remote-debugging-port`, connect, and use the standard snapshot-interact workflow. This is the foundation that the **slack** skill builds on.
+
+## slack
+
+Browser-based Slack automation. Connects to an existing Slack session (via `agent-browser connect 9222`) or opens Slack in a new browser, then uses snapshots and element refs to navigate the UI. Covers checking unreads, navigating channels and DMs, searching conversations, extracting message data, and taking screenshots — all without needing Slack API tokens or bot setup.
+
+## Source
+
+All skill files are in the [`skills/`](https://github.com/vercel-labs/agent-browser/tree/main/skills) directory of the repository.
diff --git a/docs/src/components/code-block.tsx b/docs/src/components/code-block.tsx
index 7c1ef4f..a0c888a 100644
--- a/docs/src/components/code-block.tsx
+++ b/docs/src/components/code-block.tsx
@@ -10,7 +10,10 @@ export async function CodeBlock({ code, lang = "bash" }: CodeBlockProps) {
const trimmedCode = code.trim();
const html = await codeToHtml(trimmedCode, {
lang,
- theme: "github-dark-default",
+ themes: {
+ light: "github-light-default",
+ dark: "github-dark-default",
+ },
});
return (
diff --git a/docs/src/components/copy-page-button.tsx b/docs/src/components/copy-page-button.tsx
new file mode 100644
index 0000000..daafaac
--- /dev/null
+++ b/docs/src/components/copy-page-button.tsx
@@ -0,0 +1,71 @@
+"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 (
+
+ {state === "copied" ? (
+ <>
+
+
+
+ Copied
+ >
+ ) : (
+ <>
+
+
+
+
+ Copy Page
+ >
+ )}
+
+ );
+}
diff --git a/docs/src/components/docs-chat.tsx b/docs/src/components/docs-chat.tsx
index e11985a..4332d36 100644
--- a/docs/src/components/docs-chat.tsx
+++ b/docs/src/components/docs-chat.tsx
@@ -494,7 +494,7 @@ export function DocsChat({
{!open && (
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 border bg-background text-primary shadow-lg hover:bg-primary hover:text-primary-foreground transition-colors text-sm font-medium"
+ 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
diff --git a/docs/src/components/docs-mobile-nav.tsx b/docs/src/components/docs-mobile-nav.tsx
new file mode 100644
index 0000000..6c9b503
--- /dev/null
+++ b/docs/src/components/docs-mobile-nav.tsx
@@ -0,0 +1,81 @@
+"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 (
+
+
+ {currentPage?.name}
+
+
+
+
+
+
+
+
+
+
+
+
+ Table of Contents
+
+ {navigation.map((section, sectionIndex) => (
+
+ {section.title && (
+
+ {section.title}
+
+ )}
+
+ {section.items.map((item) => (
+
+ 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}
+
+
+ ))}
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/docs/src/components/docs-sidebar.tsx b/docs/src/components/docs-sidebar.tsx
new file mode 100644
index 0000000..c4642bf
--- /dev/null
+++ b/docs/src/components/docs-sidebar.tsx
@@ -0,0 +1,44 @@
+"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 (
+
+ {navigation.map((section, sectionIndex) => (
+
+ {section.title && (
+
+ {section.title}
+
+ )}
+
+ {section.items.map((item) => {
+ const isActive = pathname === item.href;
+ return (
+
+
+ {item.name}
+
+
+ );
+ })}
+
+
+ ))}
+
+ );
+}
diff --git a/docs/src/components/header.tsx b/docs/src/components/header.tsx
index 39e4a00..1704fdb 100644
--- a/docs/src/components/header.tsx
+++ b/docs/src/components/header.tsx
@@ -1,14 +1,11 @@
"use client";
import Link from "next/link";
-import { useMobileNav } from "./mobile-nav-context";
import { ThemeToggle } from "./theme-toggle";
export function Header() {
- const { isOpen, toggle } = useMobileNav();
-
return (
-
+
@@ -25,10 +22,10 @@ export function Header() {
clipRule="evenodd"
d="M8 1L16 15H0L8 1Z"
fill="currentColor"
- >
+ />
-
+
+ />
-
+
agent-browser
@@ -56,7 +56,7 @@ export function Header() {
href="https://github.com/vercel-labs/agent-browser"
target="_blank"
rel="noopener noreferrer"
- className="hidden sm:flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
+ 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"
>
- 14k
+ 16k
npm
-
- {isOpen ? (
-
-
-
- ) : (
-
-
-
- )}
-
diff --git a/docs/src/components/mobile-nav-context.tsx b/docs/src/components/mobile-nav-context.tsx
deleted file mode 100644
index a08d8d0..0000000
--- a/docs/src/components/mobile-nav-context.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-"use client";
-
-import { createContext, useContext, useState, useEffect } from "react";
-import { usePathname } from "next/navigation";
-
-type MobileNavContextType = {
- isOpen: boolean;
- setIsOpen: (open: boolean) => void;
- toggle: () => void;
-};
-
-const MobileNavContext = createContext
(null);
-
-export function MobileNavProvider({ children }: { children: React.ReactNode }) {
- const [isOpen, setIsOpen] = useState(false);
- const pathname = usePathname();
-
- useEffect(() => {
- setIsOpen(false);
- }, [pathname]);
-
- useEffect(() => {
- const handleEscape = (e: KeyboardEvent) => {
- if (e.key === "Escape") setIsOpen(false);
- };
- document.addEventListener("keydown", handleEscape);
- return () => document.removeEventListener("keydown", handleEscape);
- }, []);
-
- const toggle = () => setIsOpen(!isOpen);
-
- return (
-
- {children}
-
- );
-}
-
-export function useMobileNav() {
- const context = useContext(MobileNavContext);
- if (!context) {
- throw new Error("useMobileNav must be used within a MobileNavProvider");
- }
- return context;
-}
diff --git a/docs/src/components/sidebar.tsx b/docs/src/components/sidebar.tsx
deleted file mode 100644
index c85144e..0000000
--- a/docs/src/components/sidebar.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-"use client";
-
-import Link from "next/link";
-import { usePathname } from "next/navigation";
-import { useMobileNav } from "./mobile-nav-context";
-import { navigation } from "@/lib/docs-navigation";
-
-export function Sidebar() {
- const pathname = usePathname();
- const { isOpen, setIsOpen } = useMobileNav();
-
- return (
- <>
- {/* Mobile overlay */}
- {isOpen && (
- setIsOpen(false)}
- />
- )}
-
- {/* Sidebar */}
-
-
-
- {navigation.map((section, sectionIndex) => (
-
- {section.title && (
-
- {section.title}
-
- )}
-
- {section.items.map((item) => {
- const isActive = pathname === item.href;
- return (
-
- {item.name}
-
- );
- })}
-
-
- ))}
-
-
-
- >
- );
-}
diff --git a/docs/src/components/theme-toggle.tsx b/docs/src/components/theme-toggle.tsx
index 653ca58..ebad02b 100644
--- a/docs/src/components/theme-toggle.tsx
+++ b/docs/src/components/theme-toggle.tsx
@@ -18,7 +18,7 @@ export function ThemeToggle() {
return (
setTheme(theme === "dark" ? "light" : "dark")}
- className="w-8 h-8 flex items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
+ 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" ? (
diff --git a/docs/src/lib/docs-navigation.ts b/docs/src/lib/docs-navigation.ts
index 4cf4b14..ee16b22 100644
--- a/docs/src/lib/docs-navigation.ts
+++ b/docs/src/lib/docs-navigation.ts
@@ -15,6 +15,7 @@ export const navigation: NavSection[] = [
{ name: "Introduction", href: "/" },
{ name: "Installation", href: "/installation" },
{ name: "Quick Start", href: "/quick-start" },
+ { name: "Skills", href: "/skills" },
],
},
{
diff --git a/docs/src/lib/mdx-to-markdown.ts b/docs/src/lib/mdx-to-markdown.ts
index af7a6c7..0dddfa1 100644
--- a/docs/src/lib/mdx-to-markdown.ts
+++ b/docs/src/lib/mdx-to-markdown.ts
@@ -1,11 +1,8 @@
/**
* Converts raw MDX content to clean Markdown suitable for AI agents.
*
- * Transformations:
- * - Remove `export` statements (metadata, etc.)
- * - Remove `import` statements
- * - Strip standalone JSX divs with className attributes
- * - Pass everything else through as-is (already valid Markdown)
+ * 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");
@@ -16,12 +13,10 @@ export function mdxToCleanMarkdown(raw: string): string {
for (const line of lines) {
const trimmed = line.trim();
- // Skip export and import statements
if (trimmed.startsWith("export ") || trimmed.startsWith("import ")) {
continue;
}
- // Track JSX blocks (like callout divs) and skip them
if (
!inJsxBlock &&
trimmed.startsWith("]/g) || []).length;
const closes = (line.match(/<\/div>/g) || []).length;
jsxDepth += opens - closes;
@@ -47,7 +41,6 @@ export function mdxToCleanMarkdown(raw: string): string {
out.push(line);
}
- // Clean up leading blank lines
let result = out.join("\n");
result = result.replace(/^\n+/, "\n").trim();
return result;
diff --git a/docs/src/lib/page-titles.ts b/docs/src/lib/page-titles.ts
index 379315b..e4b21d7 100644
--- a/docs/src/lib/page-titles.ts
+++ b/docs/src/lib/page-titles.ts
@@ -2,6 +2,7 @@ export const PAGE_TITLES: Record = {
"": "Headless Browser\nAutomation for AI",
installation: "Installation",
"quick-start": "Quick Start",
+ skills: "Skills",
commands: "Commands",
configuration: "Configuration",
selectors: "Selectors",
diff --git a/skills/electron/SKILL.md b/skills/electron/SKILL.md
new file mode 100644
index 0000000..8de5718
--- /dev/null
+++ b/skills/electron/SKILL.md
@@ -0,0 +1,212 @@
+---
+name: electron
+description: Automate Electron desktop apps (VS Code, Slack, Discord, Figma, Notion, Spotify, etc.) using agent-browser via Chrome DevTools Protocol. Use when the user needs to interact with an Electron app, automate a desktop app, connect to a running app, control a native app, or test an Electron application. Triggers include "automate Slack app", "control VS Code", "interact with Discord app", "test this Electron app", "connect to desktop app", or any task requiring automation of a native Electron application.
+allowed-tools: Bash(agent-browser:*), Bash(npx agent-browser:*)
+---
+
+# Electron App Automation
+
+Automate any Electron desktop app using agent-browser. Electron apps are built on Chromium and expose a Chrome DevTools Protocol (CDP) port that agent-browser can connect to, enabling the same snapshot-interact workflow used for web pages.
+
+## Core Workflow
+
+1. **Launch** the Electron app with remote debugging enabled
+2. **Connect** agent-browser to the CDP port
+3. **Snapshot** to discover interactive elements
+4. **Interact** using element refs
+5. **Re-snapshot** after navigation or state changes
+
+```bash
+# Launch an Electron app with remote debugging
+open -a "Slack" --args --remote-debugging-port=9222
+
+# Connect agent-browser to the app
+agent-browser connect 9222
+
+# Standard workflow from here
+agent-browser snapshot -i
+agent-browser click @e5
+agent-browser screenshot slack-desktop.png
+```
+
+## Launching Electron Apps with CDP
+
+Every Electron app supports the `--remote-debugging-port` flag since it's built into Chromium.
+
+### macOS
+
+```bash
+# Slack
+open -a "Slack" --args --remote-debugging-port=9222
+
+# VS Code
+open -a "Visual Studio Code" --args --remote-debugging-port=9223
+
+# Discord
+open -a "Discord" --args --remote-debugging-port=9224
+
+# Figma
+open -a "Figma" --args --remote-debugging-port=9225
+
+# Notion
+open -a "Notion" --args --remote-debugging-port=9226
+
+# Spotify
+open -a "Spotify" --args --remote-debugging-port=9227
+```
+
+### Linux
+
+```bash
+slack --remote-debugging-port=9222
+code --remote-debugging-port=9223
+discord --remote-debugging-port=9224
+```
+
+### Windows
+
+```bash
+"C:\Users\%USERNAME%\AppData\Local\slack\slack.exe" --remote-debugging-port=9222
+"C:\Users\%USERNAME%\AppData\Local\Programs\Microsoft VS Code\Code.exe" --remote-debugging-port=9223
+```
+
+**Important:** If the app is already running, quit it first, then relaunch with the flag. The `--remote-debugging-port` flag must be present at launch time.
+
+## Connecting
+
+```bash
+# Connect to a specific port
+agent-browser connect 9222
+
+# Or use --cdp on each command
+agent-browser --cdp 9222 snapshot -i
+
+# Auto-discover a running Chromium-based app
+agent-browser --auto-connect snapshot -i
+```
+
+After `connect`, all subsequent commands target the connected app without needing `--cdp`.
+
+## Tab Management
+
+Electron apps often have multiple windows or webviews. Use tab commands to list and switch between them:
+
+```bash
+# List all available targets (windows, webviews, etc.)
+agent-browser tab
+
+# Switch to a specific tab by index
+agent-browser tab 2
+
+# Switch by URL pattern
+agent-browser tab --url "*settings*"
+```
+
+## Common Patterns
+
+### Inspect and Navigate an App
+
+```bash
+open -a "Slack" --args --remote-debugging-port=9222
+sleep 3 # Wait for app to start
+agent-browser connect 9222
+agent-browser snapshot -i
+# Read the snapshot output to identify UI elements
+agent-browser click @e10 # Navigate to a section
+agent-browser snapshot -i # Re-snapshot after navigation
+```
+
+### Take Screenshots of Desktop Apps
+
+```bash
+agent-browser connect 9222
+agent-browser screenshot app-state.png
+agent-browser screenshot --full full-app.png
+agent-browser screenshot --annotate annotated-app.png
+```
+
+### Extract Data from a Desktop App
+
+```bash
+agent-browser connect 9222
+agent-browser snapshot -i
+agent-browser get text @e5
+agent-browser snapshot --json > app-state.json
+```
+
+### Fill Forms in Desktop Apps
+
+```bash
+agent-browser connect 9222
+agent-browser snapshot -i
+agent-browser fill @e3 "search query"
+agent-browser press Enter
+agent-browser wait 1000
+agent-browser snapshot -i
+```
+
+### Run Multiple Apps Simultaneously
+
+Use named sessions to control multiple Electron apps at the same time:
+
+```bash
+# Connect to Slack
+agent-browser --session slack connect 9222
+
+# Connect to VS Code
+agent-browser --session vscode connect 9223
+
+# Interact with each independently
+agent-browser --session slack snapshot -i
+agent-browser --session vscode snapshot -i
+```
+
+## Color Scheme
+
+Playwright overrides the color scheme to `light` by default when connecting via CDP. To preserve dark mode:
+
+```bash
+agent-browser connect 9222
+agent-browser --color-scheme dark snapshot -i
+```
+
+Or set it globally:
+
+```bash
+AGENT_BROWSER_COLOR_SCHEME=dark agent-browser connect 9222
+```
+
+## Troubleshooting
+
+### "Connection refused" or "Cannot connect"
+
+- Make sure the app was launched with `--remote-debugging-port=NNNN`
+- If the app was already running, quit and relaunch with the flag
+- Check that the port isn't in use by another process: `lsof -i :9222`
+
+### App launches but connect fails
+
+- Wait a few seconds after launch before connecting (`sleep 3`)
+- Some apps take time to initialize their webview
+
+### Elements not appearing in snapshot
+
+- The app may use multiple webviews. Use `agent-browser tab` to list targets and switch to the right one
+- Use `agent-browser snapshot -i -C` to include cursor-interactive elements (divs with onclick handlers)
+
+### Cannot type in input fields
+
+- Try `agent-browser keyboard type "text"` to type at the current focus without a selector
+- Some Electron apps use custom input components; use `agent-browser keyboard inserttext "text"` to bypass key events
+
+## Supported Apps
+
+Any app built on Electron works, including:
+
+- **Communication:** Slack, Discord, Microsoft Teams, Signal, Telegram Desktop
+- **Development:** VS Code, GitHub Desktop, Postman, Insomnia
+- **Design:** Figma, Notion, Obsidian
+- **Media:** Spotify, Tidal
+- **Productivity:** Todoist, Linear, 1Password
+
+If an app is built with Electron, it supports `--remote-debugging-port` and can be automated with agent-browser.