diff --git a/docs/src/app/api/search/route.ts b/docs/src/app/api/search/route.ts
new file mode 100644
index 0000000..5125db7
--- /dev/null
+++ b/docs/src/app/api/search/route.ts
@@ -0,0 +1,69 @@
+import { NextRequest, NextResponse } from "next/server";
+import { getSearchIndex } from "@/lib/search-index";
+
+export async function GET(req: NextRequest) {
+ const q = req.nextUrl.searchParams.get("q")?.trim().toLowerCase();
+
+ if (!q) {
+ return NextResponse.json({ results: [] });
+ }
+
+ const index = await getSearchIndex();
+ const terms = q.split(/\s+/).filter(Boolean);
+
+ const results = index
+ .map((entry) => {
+ const titleLower = entry.title.toLowerCase();
+ const contentLower = entry.content.toLowerCase();
+
+ const titleMatch = terms.every((t) => titleLower.includes(t));
+ const contentMatch = terms.every((t) => contentLower.includes(t));
+
+ if (!titleMatch && !contentMatch) return null;
+
+ let snippet = "";
+ if (contentMatch) {
+ const firstTermIdx = Math.min(
+ ...terms.map((t) => {
+ const idx = contentLower.indexOf(t);
+ return idx === -1 ? Infinity : idx;
+ }),
+ );
+ if (firstTermIdx !== Infinity) {
+ const start = Math.max(0, firstTermIdx - 40);
+ const end = Math.min(entry.content.length, firstTermIdx + 120);
+ snippet =
+ (start > 0 ? "..." : "") +
+ entry.content.slice(start, end).replace(/\n/g, " ") +
+ (end < entry.content.length ? "..." : "");
+ }
+ }
+
+ return {
+ title: entry.title,
+ href: entry.href,
+ section: entry.section,
+ snippet,
+ score: titleMatch ? 2 : 1,
+ };
+ })
+ .filter(
+ (
+ r,
+ ): r is {
+ title: string;
+ href: string;
+ section: string;
+ snippet: string;
+ score: number;
+ } => r !== null,
+ )
+ .sort((a, b) => b.score - a.score)
+ .slice(0, 20)
+ .map(({ score: _, ...rest }) => rest);
+
+ return NextResponse.json(
+ { results },
+ { headers: { "Cache-Control": "public, max-age=60" } },
+ );
+}
diff --git a/docs/src/app/globals.css b/docs/src/app/globals.css
index 04c72fb..843d35f 100644
--- a/docs/src/app/globals.css
+++ b/docs/src/app/globals.css
@@ -16,6 +16,7 @@
--color-muted-foreground: var(--muted-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
+ --color-sidebar: var(--sidebar);
}
:root {
@@ -26,6 +27,7 @@
--muted-foreground: #737373;
--primary: #171717;
--primary-foreground: #fff;
+ --sidebar: #f5f5f5;
}
.dark {
@@ -36,6 +38,7 @@
--muted-foreground: #a3a3a3;
--primary: #f5f5f5;
--primary-foreground: #0a0a0a;
+ --sidebar: #171717;
}
html {
@@ -129,6 +132,23 @@ pre:not(.shiki) {
font-size: 0.875em;
}
+/* Diff line highlighting */
+.diff-add {
+ color: #00952d;
+}
+
+.diff-remove {
+ color: #f32e40;
+}
+
+.dark .diff-add {
+ color: #00ca50;
+}
+
+.dark .diff-remove {
+ color: #f32e40;
+}
+
/* Shiki dual theme support */
.shiki,
.shiki span {
diff --git a/docs/src/components/code-block.tsx b/docs/src/components/code-block.tsx
index a0c888a..9817d70 100644
--- a/docs/src/components/code-block.tsx
+++ b/docs/src/components/code-block.tsx
@@ -1,6 +1,156 @@
import { codeToHtml } from "shiki";
import { CopyButton } from "./copy-button";
+const vercelDarkTheme = {
+ name: "vercel-dark",
+ type: "dark" as const,
+ colors: {
+ "editor.background": "transparent",
+ "editor.foreground": "#EDEDED",
+ },
+ settings: [
+ {
+ scope: ["comment", "punctuation.definition.comment"],
+ settings: { foreground: "#A1A1A1" },
+ },
+ {
+ scope: ["string", "string.quoted", "string.template", "punctuation.definition.string"],
+ settings: { foreground: "#00CA50" },
+ },
+ {
+ scope: ["constant.numeric", "constant.language.boolean", "constant.language.null"],
+ settings: { foreground: "#47A8FF" },
+ },
+ {
+ scope: ["keyword", "storage.type", "storage.modifier"],
+ settings: { foreground: "#FF4D8D" },
+ },
+ {
+ scope: ["keyword.operator", "keyword.control"],
+ settings: { foreground: "#FF4D8D" },
+ },
+ {
+ scope: ["entity.name.function", "support.function", "meta.function-call"],
+ settings: { foreground: "#C472FB" },
+ },
+ {
+ scope: ["variable", "variable.other"],
+ settings: { foreground: "#EDEDED" },
+ },
+ {
+ scope: ["variable.parameter"],
+ settings: { foreground: "#FF9300" },
+ },
+ {
+ scope: ["entity.name.tag", "support.class.component", "entity.name.type"],
+ settings: { foreground: "#FF4D8D" },
+ },
+ {
+ scope: ["punctuation", "meta.brace", "meta.bracket"],
+ settings: { foreground: "#EDEDED" },
+ },
+ {
+ scope: [
+ "support.type.property-name",
+ "entity.name.tag.json",
+ "meta.object-literal.key",
+ "punctuation.support.type.property-name",
+ ],
+ settings: { foreground: "#FF4D8D" },
+ },
+ {
+ scope: ["entity.other.attribute-name"],
+ settings: { foreground: "#00CA50" },
+ },
+ {
+ scope: ["support.type.primitive", "entity.name.type.primitive"],
+ settings: { foreground: "#00CA50" },
+ },
+ ],
+};
+
+const vercelLightTheme = {
+ name: "vercel-light",
+ type: "light" as const,
+ colors: {
+ "editor.background": "transparent",
+ "editor.foreground": "#171717",
+ },
+ settings: [
+ {
+ scope: ["comment", "punctuation.definition.comment"],
+ settings: { foreground: "#6B7280" },
+ },
+ {
+ scope: ["string", "string.quoted", "string.template", "punctuation.definition.string"],
+ settings: { foreground: "#067A6E" },
+ },
+ {
+ scope: ["constant.numeric", "constant.language.boolean", "constant.language.null"],
+ settings: { foreground: "#0070C0" },
+ },
+ {
+ scope: ["keyword", "storage.type", "storage.modifier"],
+ settings: { foreground: "#D6409F" },
+ },
+ {
+ scope: ["keyword.operator", "keyword.control"],
+ settings: { foreground: "#D6409F" },
+ },
+ {
+ scope: ["entity.name.function", "support.function", "meta.function-call"],
+ settings: { foreground: "#6E56CF" },
+ },
+ {
+ scope: ["variable", "variable.other"],
+ settings: { foreground: "#171717" },
+ },
+ {
+ scope: ["variable.parameter"],
+ settings: { foreground: "#B45309" },
+ },
+ {
+ scope: ["entity.name.tag", "support.class.component", "entity.name.type"],
+ settings: { foreground: "#D6409F" },
+ },
+ {
+ scope: ["punctuation", "meta.brace", "meta.bracket"],
+ settings: { foreground: "#6B7280" },
+ },
+ {
+ scope: [
+ "support.type.property-name",
+ "entity.name.tag.json",
+ "meta.object-literal.key",
+ "punctuation.support.type.property-name",
+ ],
+ settings: { foreground: "#D6409F" },
+ },
+ {
+ scope: ["entity.other.attribute-name"],
+ settings: { foreground: "#067A6E" },
+ },
+ {
+ scope: ["support.type.primitive", "entity.name.type.primitive"],
+ settings: { foreground: "#067A6E" },
+ },
+ ],
+};
+
+const PLACEHOLDER_PREFIX = "\u200B\u200B";
+const PLACEHOLDER_SUFFIX = "\u200B\u200B";
+
+function shieldPlaceholders(code: string): string {
+ return code.replace(/<([\w|]+)>/g, `${PLACEHOLDER_PREFIX}$1${PLACEHOLDER_SUFFIX}`);
+}
+
+function restorePlaceholders(html: string): string {
+ return html.replace(
+ new RegExp(`${PLACEHOLDER_PREFIX}([\\w|]+)${PLACEHOLDER_SUFFIX}`, "g"),
+ "<$1>",
+ );
+}
+
interface CodeBlockProps {
code: string;
lang?: string;
@@ -8,13 +158,16 @@ interface CodeBlockProps {
export async function CodeBlock({ code, lang = "bash" }: CodeBlockProps) {
const trimmedCode = code.trim();
- const html = await codeToHtml(trimmedCode, {
+ const shielded = shieldPlaceholders(trimmedCode);
+ let html = await codeToHtml(shielded, {
lang,
themes: {
- light: "github-light-default",
- dark: "github-dark-default",
+ light: vercelLightTheme,
+ dark: vercelDarkTheme,
},
+ defaultColor: false,
});
+ html = restorePlaceholders(html);
return (
diff --git a/docs/src/components/docs-chat.tsx b/docs/src/components/docs-chat.tsx
index 4332d36..62e28d9 100644
--- a/docs/src/components/docs-chat.tsx
+++ b/docs/src/components/docs-chat.tsx
@@ -261,7 +261,7 @@ export function DocsChat({
// Cmd+K to open sidebar and focus prompt, Escape to close
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
- if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
+ if (e.key === "i" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((prev) => {
if (!prev) {
@@ -327,7 +327,7 @@ export function DocsChat({
const chatPanel = (
<>
{/* Header */}
-
+
agent-browser Docs
{showMessages && (
@@ -443,7 +443,7 @@ export function DocsChat({
{/* Input bar */}