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({ )} - -
-
- -
-
-
- {children} -
-
-
+
+ +
+ +
+
+ +
+
{children}
- - +
+ 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 ( + + ); +} 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 && (
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 */} - - - ); -} 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 (