add skills to docs (#576)
This commit is contained in:
+1
-1
@@ -42,7 +42,7 @@
|
|||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.1.1",
|
"eslint-config-next": "16.1.1",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+12
-8
@@ -102,9 +102,9 @@ importers:
|
|||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4
|
specifier: ^4
|
||||||
version: 4.1.18
|
version: 4.1.18
|
||||||
tw-animate-css:
|
tailwindcss-animate:
|
||||||
specifier: ^1.4.0
|
specifier: ^1.0.7
|
||||||
version: 1.4.0
|
version: 1.0.7(tailwindcss@4.1.18)
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5
|
specifier: ^5
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
@@ -3467,6 +3467,11 @@ packages:
|
|||||||
tailwind-merge@3.4.0:
|
tailwind-merge@3.4.0:
|
||||||
resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==}
|
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:
|
tailwindcss@4.1.18:
|
||||||
resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==}
|
resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==}
|
||||||
|
|
||||||
@@ -3521,9 +3526,6 @@ packages:
|
|||||||
turndown@7.2.2:
|
turndown@7.2.2:
|
||||||
resolution: {integrity: sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==}
|
resolution: {integrity: sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==}
|
||||||
|
|
||||||
tw-animate-css@1.4.0:
|
|
||||||
resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
|
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -7827,6 +7829,10 @@ snapshots:
|
|||||||
|
|
||||||
tailwind-merge@3.4.0: {}
|
tailwind-merge@3.4.0: {}
|
||||||
|
|
||||||
|
tailwindcss-animate@1.0.7(tailwindcss@4.1.18):
|
||||||
|
dependencies:
|
||||||
|
tailwindcss: 4.1.18
|
||||||
|
|
||||||
tailwindcss@4.1.18: {}
|
tailwindcss@4.1.18: {}
|
||||||
|
|
||||||
tapable@2.3.0: {}
|
tapable@2.3.0: {}
|
||||||
@@ -7891,8 +7897,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@mixmark-io/domino': 2.2.0
|
'@mixmark-io/domino': 2.2.0
|
||||||
|
|
||||||
tw-animate-css@1.4.0: {}
|
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
prelude-ls: 1.2.1
|
prelude-ls: 1.2.1
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
+158
-115
@@ -1,117 +1,112 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "tw-animate-css";
|
@plugin "tailwindcss-animate";
|
||||||
|
|
||||||
@source "../../node_modules/streamdown/dist/index.js";
|
@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 {
|
:root {
|
||||||
--radius: 0.5rem;
|
--background: #fff;
|
||||||
--background: oklch(1.0 0 0);
|
--foreground: #171717;
|
||||||
--foreground: oklch(0.1 0 0);
|
--border: #e5e5e5;
|
||||||
--card: oklch(0.98 0 0);
|
--muted: #f5f5f5;
|
||||||
--card-foreground: oklch(0.1 0 0);
|
--muted-foreground: #737373;
|
||||||
--popover: oklch(0.98 0 0);
|
--primary: #171717;
|
||||||
--popover-foreground: oklch(0.1 0 0);
|
--primary-foreground: #fff;
|
||||||
--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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.0 0 0);
|
--background: #0a0a0a;
|
||||||
--foreground: oklch(0.98 0 0);
|
--foreground: #f5f5f5;
|
||||||
--card: oklch(0.08 0 0);
|
--border: #262626;
|
||||||
--card-foreground: oklch(0.98 0 0);
|
--muted: #262626;
|
||||||
--popover: oklch(0.08 0 0);
|
--muted-foreground: #a3a3a3;
|
||||||
--popover-foreground: oklch(0.98 0 0);
|
--primary: #f5f5f5;
|
||||||
--primary: oklch(0.98 0 0);
|
--primary-foreground: #0a0a0a;
|
||||||
--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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@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 {
|
html {
|
||||||
scrollbar-width: none;
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
html::-webkit-scrollbar {
|
::selection {
|
||||||
display: none;
|
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 */
|
/* Code blocks */
|
||||||
pre {
|
pre {
|
||||||
background: var(--card) !important;
|
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0.875rem;
|
padding: 0.875rem;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
font-family: var(--font-geist-mono), monospace;
|
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre:not(.shiki) {
|
||||||
|
background: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
.code-block pre {
|
.code-block pre {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
@@ -127,17 +122,26 @@ pre {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
|
||||||
font-family: var(--font-geist-mono), monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
:not(pre) > code {
|
:not(pre) > code {
|
||||||
background: var(--card);
|
background: var(--muted);
|
||||||
padding: 0.125rem 0.375rem;
|
padding: 0.125rem 0.375rem;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-size: 0.875em;
|
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 */
|
||||||
.prose {
|
.prose {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -145,9 +149,9 @@ code {
|
|||||||
|
|
||||||
.prose h1 {
|
.prose h1 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 1.5rem;
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,59 +162,102 @@ code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.prose h2 {
|
.prose h2 {
|
||||||
font-size: 0.875rem;
|
font-size: 1.125rem;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
letter-spacing: 0;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--muted-foreground);
|
|
||||||
margin-top: 3rem;
|
margin-top: 3rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h2:first-child {
|
||||||
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose h3 {
|
.prose h3 {
|
||||||
font-size: 0.875rem;
|
font-size: 1rem;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
opacity: 0.85;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose p {
|
.prose p {
|
||||||
margin-bottom: 1.25rem;
|
margin-bottom: 1rem;
|
||||||
line-height: 1.7;
|
line-height: 1.65;
|
||||||
color: var(--muted-foreground);
|
color: #525252;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:is(.dark) .prose p {
|
||||||
|
color: #a3a3a3;
|
||||||
|
}
|
||||||
|
|
||||||
.prose ul, .prose ol {
|
.prose ul, .prose ol {
|
||||||
margin-bottom: 1.25rem;
|
margin-bottom: 1rem;
|
||||||
padding-left: 1.25rem;
|
padding-left: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.prose ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose ol {
|
||||||
|
list-style-type: decimal;
|
||||||
|
}
|
||||||
|
|
||||||
.prose li {
|
.prose li {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.25rem;
|
||||||
color: var(--muted-foreground);
|
color: #525252;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:is(.dark) .prose li {
|
||||||
|
color: #a3a3a3;
|
||||||
|
}
|
||||||
|
|
||||||
.prose li strong {
|
.prose li strong {
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
opacity: 0.85;
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose a {
|
.prose a {
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
text-decoration-color: #d4d4d4;
|
||||||
text-underline-offset: 2px;
|
text-underline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose a:hover {
|
.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);
|
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 {
|
.prose table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
@@ -284,7 +331,3 @@ code {
|
|||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|||||||
+20
-19
@@ -1,18 +1,19 @@
|
|||||||
import type { Metadata } from "next";
|
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 { GeistPixelSquare } from "geist/font/pixel";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
import { MobileNavProvider } from "@/components/mobile-nav-context";
|
|
||||||
import { Header } from "@/components/header";
|
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 { DocsChat } from "@/components/docs-chat";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { SpeedInsights } from "@vercel/speed-insights/next";
|
import { SpeedInsights } from "@vercel/speed-insights/next";
|
||||||
import { Analytics } from "@vercel/analytics/next";
|
import { Analytics } from "@vercel/analytics/next";
|
||||||
|
|
||||||
const geist = Geist({
|
const inter = Inter({
|
||||||
variable: "--font-geist",
|
variable: "--font-inter",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -66,23 +67,23 @@ export default async function RootLayout({
|
|||||||
)}
|
)}
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body
|
||||||
className={`${geist.variable} ${geistMono.variable} ${GeistPixelSquare.variable} antialiased bg-background text-foreground`}
|
className={`${inter.variable} ${geistMono.variable} ${GeistPixelSquare.variable} bg-white text-neutral-900 antialiased dark:bg-neutral-950 dark:text-neutral-100`}
|
||||||
>
|
>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<MobileNavProvider>
|
<Header />
|
||||||
<Header />
|
<DocsMobileNav />
|
||||||
<div className="flex min-h-[calc(100vh-3.5rem)]">
|
<div className="max-w-5xl mx-auto px-6 py-8 lg:py-12 flex gap-16">
|
||||||
<Sidebar />
|
<aside className="w-48 shrink-0 hidden lg:block sticky top-28 h-[calc(100vh-7rem)] overflow-y-auto">
|
||||||
<main className="flex-1 overflow-auto">
|
<DocsSidebar />
|
||||||
<div className="max-w-2xl mx-auto px-4 sm:px-6 py-8 sm:py-12">
|
</aside>
|
||||||
<div className="prose">
|
<div className="flex-1 min-w-0 max-w-2xl pb-20">
|
||||||
{children}
|
<div className="flex justify-end mb-4">
|
||||||
</div>
|
<CopyPageButton />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
<article className="prose">{children}</article>
|
||||||
</div>
|
</div>
|
||||||
<DocsChat defaultOpen={chatOpen} defaultWidth={chatWidth} />
|
</div>
|
||||||
</MobileNavProvider>
|
<DocsChat defaultOpen={chatOpen} defaultWidth={chatWidth} />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
<SpeedInsights />
|
<SpeedInsights />
|
||||||
<Analytics />
|
<Analytics />
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -10,7 +10,10 @@ export async function CodeBlock({ code, lang = "bash" }: CodeBlockProps) {
|
|||||||
const trimmedCode = code.trim();
|
const trimmedCode = code.trim();
|
||||||
const html = await codeToHtml(trimmedCode, {
|
const html = await codeToHtml(trimmedCode, {
|
||||||
lang,
|
lang,
|
||||||
theme: "github-dark-default",
|
themes: {
|
||||||
|
light: "github-light-default",
|
||||||
|
dark: "github-dark-default",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -494,7 +494,7 @@ export function DocsChat({
|
|||||||
{!open && (
|
{!open && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setOpen(true)}
|
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 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"
|
aria-label="Ask AI"
|
||||||
>
|
>
|
||||||
Ask AI
|
Ask AI
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useMobileNav } from "./mobile-nav-context";
|
|
||||||
import { ThemeToggle } from "./theme-toggle";
|
import { ThemeToggle } from "./theme-toggle";
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const { isOpen, toggle } = useMobileNav();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-50 bg-background/90 backdrop-blur-sm">
|
<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 h-14 items-center justify-between px-4 gap-6">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link href="https://vercel.com" title="Made with love by Vercel">
|
<Link href="https://vercel.com" title="Made with love by Vercel">
|
||||||
@@ -25,10 +22,10 @@ export function Header() {
|
|||||||
clipRule="evenodd"
|
clipRule="evenodd"
|
||||||
d="M8 1L16 15H0L8 1Z"
|
d="M8 1L16 15H0L8 1Z"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
></path>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Link>
|
</Link>
|
||||||
<span className="text-border">
|
<span className="text-neutral-300 dark:text-neutral-700">
|
||||||
<svg
|
<svg
|
||||||
data-testid="geist-icon"
|
data-testid="geist-icon"
|
||||||
height="16"
|
height="16"
|
||||||
@@ -42,11 +39,14 @@ export function Header() {
|
|||||||
clipRule="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"
|
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"
|
fill="currentColor"
|
||||||
></path>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<span className="font-medium tracking-tight text-lg" style={{ fontFamily: "var(--font-geist-pixel-square)" }}>
|
<span
|
||||||
|
className="font-medium tracking-tight text-lg"
|
||||||
|
style={{ fontFamily: "var(--font-geist-pixel-square)" }}
|
||||||
|
>
|
||||||
agent-browser
|
agent-browser
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -56,7 +56,7 @@ export function Header() {
|
|||||||
href="https://github.com/vercel-labs/agent-browser"
|
href="https://github.com/vercel-labs/agent-browser"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
@@ -66,32 +66,17 @@ export function Header() {
|
|||||||
>
|
>
|
||||||
<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" />
|
<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>
|
</svg>
|
||||||
<span>14k</span>
|
<span>16k</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="https://www.npmjs.com/package/agent-browser"
|
href="https://www.npmjs.com/package/agent-browser"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="hidden sm:block text-sm text-muted-foreground hover:text-foreground transition-colors"
|
className="text-sm text-neutral-500 hover:text-neutral-900 transition-colors dark:text-neutral-400 dark:hover:text-neutral-100"
|
||||||
>
|
>
|
||||||
npm
|
npm
|
||||||
</a>
|
</a>
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
<button
|
|
||||||
onClick={toggle}
|
|
||||||
className="lg:hidden p-2 -mr-2 text-muted-foreground hover:text-foreground transition-colors"
|
|
||||||
aria-label="Toggle menu"
|
|
||||||
>
|
|
||||||
{isOpen ? (
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 6h16M4 12h16M4 18h16" />
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -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<MobileNavContextType | null>(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 (
|
|
||||||
<MobileNavContext.Provider value={{ isOpen, setIsOpen, toggle }}>
|
|
||||||
{children}
|
|
||||||
</MobileNavContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useMobileNav() {
|
|
||||||
const context = useContext(MobileNavContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useMobileNav must be used within a MobileNavProvider");
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
@@ -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 && (
|
|
||||||
<div
|
|
||||||
className="lg:hidden fixed inset-0 z-40 bg-background/80"
|
|
||||||
onClick={() => setIsOpen(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Sidebar */}
|
|
||||||
<aside
|
|
||||||
className={`
|
|
||||||
fixed lg:sticky top-14 left-0 z-50 lg:z-auto
|
|
||||||
w-56 lg:w-48 h-[calc(100vh-3.5rem)]
|
|
||||||
bg-background
|
|
||||||
transform transition-transform duration-150 ease-out
|
|
||||||
${isOpen ? "translate-x-0" : "-translate-x-full lg:translate-x-0"}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<div className="h-full overflow-y-auto py-5 pl-3 pr-5">
|
|
||||||
<nav className="space-y-4">
|
|
||||||
{navigation.map((section, sectionIndex) => (
|
|
||||||
<div key={section.title ?? sectionIndex}>
|
|
||||||
{section.title && (
|
|
||||||
<div className="px-2 pb-1 text-xs font-medium text-muted-foreground/60 uppercase tracking-wider">
|
|
||||||
{section.title}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
{section.items.map((item) => {
|
|
||||||
const isActive = pathname === item.href;
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
key={item.href}
|
|
||||||
href={item.href}
|
|
||||||
className={`block px-2 py-1.5 text-sm transition-colors ${
|
|
||||||
isActive
|
|
||||||
? "text-foreground"
|
|
||||||
: "text-muted-foreground hover:text-foreground"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,7 @@ export function ThemeToggle() {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
onClick={() => 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"
|
aria-label="Toggle theme"
|
||||||
>
|
>
|
||||||
{theme === "dark" ? (
|
{theme === "dark" ? (
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const navigation: NavSection[] = [
|
|||||||
{ name: "Introduction", href: "/" },
|
{ name: "Introduction", href: "/" },
|
||||||
{ name: "Installation", href: "/installation" },
|
{ name: "Installation", href: "/installation" },
|
||||||
{ name: "Quick Start", href: "/quick-start" },
|
{ name: "Quick Start", href: "/quick-start" },
|
||||||
|
{ name: "Skills", href: "/skills" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Converts raw MDX content to clean Markdown suitable for AI agents.
|
* Converts raw MDX content to clean Markdown suitable for AI agents.
|
||||||
*
|
*
|
||||||
* Transformations:
|
* Strips export/import statements and standalone JSX divs with className
|
||||||
* - Remove `export` statements (metadata, etc.)
|
* attributes, passing everything else through as valid Markdown.
|
||||||
* - Remove `import` statements
|
|
||||||
* - Strip standalone JSX divs with className attributes
|
|
||||||
* - Pass everything else through as-is (already valid Markdown)
|
|
||||||
*/
|
*/
|
||||||
export function mdxToCleanMarkdown(raw: string): string {
|
export function mdxToCleanMarkdown(raw: string): string {
|
||||||
const lines = raw.split("\n");
|
const lines = raw.split("\n");
|
||||||
@@ -16,12 +13,10 @@ export function mdxToCleanMarkdown(raw: string): string {
|
|||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
|
|
||||||
// Skip export and import statements
|
|
||||||
if (trimmed.startsWith("export ") || trimmed.startsWith("import ")) {
|
if (trimmed.startsWith("export ") || trimmed.startsWith("import ")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track JSX blocks (like callout divs) and skip them
|
|
||||||
if (
|
if (
|
||||||
!inJsxBlock &&
|
!inJsxBlock &&
|
||||||
trimmed.startsWith("<div ") &&
|
trimmed.startsWith("<div ") &&
|
||||||
@@ -33,7 +28,6 @@ export function mdxToCleanMarkdown(raw: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (inJsxBlock) {
|
if (inJsxBlock) {
|
||||||
// Count opening/closing div tags to handle nesting
|
|
||||||
const opens = (line.match(/<div[\s>]/g) || []).length;
|
const opens = (line.match(/<div[\s>]/g) || []).length;
|
||||||
const closes = (line.match(/<\/div>/g) || []).length;
|
const closes = (line.match(/<\/div>/g) || []).length;
|
||||||
jsxDepth += opens - closes;
|
jsxDepth += opens - closes;
|
||||||
@@ -47,7 +41,6 @@ export function mdxToCleanMarkdown(raw: string): string {
|
|||||||
out.push(line);
|
out.push(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up leading blank lines
|
|
||||||
let result = out.join("\n");
|
let result = out.join("\n");
|
||||||
result = result.replace(/^\n+/, "\n").trim();
|
result = result.replace(/^\n+/, "\n").trim();
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export const PAGE_TITLES: Record<string, string> = {
|
|||||||
"": "Headless Browser\nAutomation for AI",
|
"": "Headless Browser\nAutomation for AI",
|
||||||
installation: "Installation",
|
installation: "Installation",
|
||||||
"quick-start": "Quick Start",
|
"quick-start": "Quick Start",
|
||||||
|
skills: "Skills",
|
||||||
commands: "Commands",
|
commands: "Commands",
|
||||||
configuration: "Configuration",
|
configuration: "Configuration",
|
||||||
selectors: "Selectors",
|
selectors: "Selectors",
|
||||||
|
|||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user