diff --git a/src/components/TabsScript.astro b/src/components/TabsScript.astro
index 5a0f781b..c9db5216 100644
--- a/src/components/TabsScript.astro
+++ b/src/components/TabsScript.astro
@@ -312,6 +312,8 @@
// Check if the code content is tall enough to collapse
if (scrollArea.scrollHeight <= CODE_COLLAPSE_HEIGHT) return;
+ // Highlighted lines are the point of the block, so never hide them.
+ if (block.querySelector('.shiki.has-highlighted')) return;
// Mark as collapsed
block.setAttribute('data-code-collapsed', 'true');
diff --git a/src/components/docs/ExampleLinkBar.astro b/src/components/docs/ExampleLinkBar.astro
index 7ac9ed7c..7897e1f1 100644
--- a/src/components/docs/ExampleLinkBar.astro
+++ b/src/components/docs/ExampleLinkBar.astro
@@ -1,17 +1,17 @@
---
-// "Prefer to read code?" bar with a View-on-GitHub button, extracted from the
-// inline HTML in the agentOS quickstart so every product's quickstart can use it.
+// "See the full example on GitHub" bar with a View-on-GitHub button. Every
+// docs page and guide that has a complete example links it with this bar, so
+// the wording stays the same everywhere.
interface Props {
href: string;
- label?: string;
}
-const { href, label = "Prefer to read code? Clone the example repository." } = Astro.props;
+const { href } = Astro.props;
---
-
{label}
+
See the full example on GitHub.
();
+ if (typeof ranges === "string") {
+ for (const range of ranges.split(",")) {
+ const [start, end = start] = range.split("-").map(Number);
+ for (let line = start; line <= end; line++) lines.add(line);
+ }
+ }
+ return {
+ name: "highlight-lines",
+ pre(node) {
+ if (lines.size > 0) this.addClassToHast(node, "has-highlighted");
+ },
+ line(node, line) {
+ if (lines.has(line)) this.addClassToHast(node, "highlighted");
+ },
+ };
+}
+
function rehypeShiki() {
return async (tree) => {
await docsHighlighter();
@@ -236,7 +263,10 @@ function rehypeShiki() {
const result = highlighter.codeToHtml(textNode.value, {
lang,
themes: { light: theme.name, dark: 'github-dark' },
- transformers: [transformerNotationFocus()],
+ transformers: [
+ transformerNotationFocus(),
+ transformerHighlightLines(node.properties.highlightLines),
+ ],
});
// Store the highlighted HTML in a property instead of the text node
// This prevents MDX from interpreting the HTML as JSX
diff --git a/src/mdx/remark-code-snippet.ts b/src/mdx/remark-code-snippet.ts
index 23c9b971..e64d2bdf 100644
--- a/src/mdx/remark-code-snippet.ts
+++ b/src/mdx/remark-code-snippet.ts
@@ -21,6 +21,10 @@ import { resolve, extname } from "node:path";
* `// docs:start name` … `// docs:end name` (markers are stripped, region is
* dedented).
*
+ * Highlight regions with `highlight="name"` (or `highlight="a,b"`): the snippet
+ * still shows the whole file (or `region`), and every other line is dimmed so
+ * the reader sees where the highlighted code sits in context.
+ *
* Paths are relative to the repo that owns the page. Product docs are symlinked
* in from their own repos, so an agentOS page's `examples/...` means agentOS's
* examples; the caller supplies a resolver that maps the MDX file's path to the
@@ -96,7 +100,8 @@ export function loadSnippet(
root: string,
file: string,
region?: string,
-): { lang: string; value: string } {
+ highlight?: string,
+): { lang: string; value: string; highlightLines?: string } {
const abs = resolve(root, file);
let source: string;
try {
@@ -110,9 +115,68 @@ export function loadSnippet(
return {
lang: LANG_BY_EXT[extname(abs).toLowerCase()] ?? "text",
value: extractRegion(source, region, file),
+ highlightLines: highlight
+ ? highlightedLines(source, region, highlight, file)
+ : undefined,
};
}
+/**
+ * The 1-based line numbers, as ranges like `3-7,12-14`, that the regions in
+ * `highlight` occupy in the code `extractRegion` returns for `region`.
+ */
+function highlightedLines(
+ source: string,
+ region: string | undefined,
+ highlight: string,
+ file: string,
+): string {
+ const names = new Set(
+ highlight
+ .split(",")
+ .map((name) => name.trim())
+ .filter(Boolean),
+ );
+ const found = new Set();
+ const open = new Set();
+ const ranges: Array<[number, number]> = [];
+ // With `region`, numbering starts inside that region.
+ let shown = region === undefined;
+ let lineNumber = 0;
+
+ for (const line of source.split("\n")) {
+ const m = line.match(MARKER);
+ if (m) {
+ const [, kind, name] = m;
+ if (name === region) shown = kind === "start";
+ if (name && names.has(name)) {
+ if (kind === "start") {
+ open.add(name);
+ found.add(name);
+ } else {
+ open.delete(name);
+ }
+ }
+ continue;
+ }
+ if (!shown) continue;
+ lineNumber += 1;
+ if (open.size === 0) continue;
+ const last = ranges.at(-1);
+ if (last && last[1] === lineNumber - 1) last[1] = lineNumber;
+ else ranges.push([lineNumber, lineNumber]);
+ }
+
+ for (const name of names) {
+ if (!found.has(name)) {
+ throw new Error(
+ `: highlight region "${name}" not found in ${file}`,
+ );
+ }
+ }
+ return ranges.map(([a, b]) => (a === b ? `${a}` : `${a}-${b}`)).join(",");
+}
+
function getAttr(
node: {
attributes?: Array<{ type?: string; name?: string; value?: unknown }>;
@@ -168,13 +232,17 @@ export function remarkCodeSnippet(resolveRoot?: RootResolver) {
const region = getAttr(el, "region");
const title = getAttr(el, "title");
const lang = getAttr(el, "lang");
+ const highlight = getAttr(el, "highlight");
- const snippet = loadSnippet(embedRoot, file, region);
+ const snippet = loadSnippet(embedRoot, file, region, highlight);
// `file=` in the meta drives the basename tab label + GitHub source
// link in the Code component; `title=` overrides the label.
const meta = [`file="${file}"`];
if (title) meta.push(`title="${title}"`);
+ if (snippet.highlightLines) {
+ meta.push(`highlight="${snippet.highlightLines}"`);
+ }
parent.children[index] = {
type: "code",
diff --git a/src/sitemap/products.ts b/src/sitemap/products.ts
index 952c6e7a..53449bbf 100644
--- a/src/sitemap/products.ts
+++ b/src/sitemap/products.ts
@@ -87,15 +87,47 @@ function productSidebars(meta: ProductMetadata) {
const bundle = bundleSidebars(meta.id);
// A shared bundle's sidebar is authored against its source product's routes
// (`/agentos/docs/...`); serve it under this product's instead.
+ const ownDocs = mirrorGuides(bundle.docs, bundle.guides ?? []);
const docs = meta.bundleOf
- ? rerootPrefix(bundle.docs, `/${meta.bundleOf}/docs`, `/${meta.id}/docs`)
- : bundle.docs;
+ ? rerootPrefix(ownDocs, `/${meta.bundleOf}/docs`, `/${meta.id}/docs`)
+ : ownDocs;
return {
docs,
integrations: bundle.integrations ?? [],
};
}
+/**
+ * Fills a `"mirror": "guides"` group in a bundle's docs sidebar with the
+ * bundle's own guides, so a docs tab can list them without repeating them:
+ *
+ * { "title": "Guides", "collapsible": true, "mirror": "guides" }
+ *
+ * The pages keep their `/guides/...` hrefs, so they open on the Guides tab.
+ */
+function mirrorGuides(node: T, guides: SidebarItem[]): T {
+ if (Array.isArray(node)) {
+ return node.map((item) => mirrorGuides(item, guides)) as unknown as T;
+ }
+ if (!node || typeof node !== "object") return node;
+ const { mirror, ...rest } = node as Record;
+ if (mirror === undefined) {
+ return Object.fromEntries(
+ Object.entries(rest).map(([key, value]) => [
+ key,
+ mirrorGuides(value, guides),
+ ]),
+ ) as T;
+ }
+ if (mirror !== "guides") {
+ throw new Error(`sidebar.json: unknown "mirror" value ${JSON.stringify(mirror)}`);
+ }
+ return {
+ ...rest,
+ pages: guides.flatMap((group) => ("pages" in group ? group.pages : [group])),
+ } as T;
+}
+
function rerootPrefix(node: T, from: string, to: string): T {
if (Array.isArray(node)) {
return node.map((item) => rerootPrefix(item, from, to)) as unknown as T;
@@ -146,10 +178,13 @@ export function deploySidebar(): SidebarItem[] {
];
}
+/** Guides tab groups in sidebar order, after General. Groups not listed follow in product order. */
+const GUIDE_GROUP_ORDER = ["Architecture", "Agents", "Guides"];
+
/**
* The Guides tab's sidebar: the website's overview, then every bundle's
- * `guides` groups in product order. Groups with the same title merge into one,
- * at the position the title first appears, keeping each bundle's page order.
+ * `guides` groups, ordered by `GUIDE_GROUP_ORDER`. Groups with the same title
+ * merge into one, keeping each bundle's page order.
*/
export function guidesSidebar(): SidebarItem[] {
const overview: SidebarSection = {
@@ -169,6 +204,11 @@ export function guidesSidebar(): SidebarItem[] {
else merged.push({ ...item, pages: [...item.pages] });
}
}
+ const rank = (group: SidebarSection) => {
+ const index = GUIDE_GROUP_ORDER.indexOf(group.title);
+ return index === -1 ? GUIDE_GROUP_ORDER.length : index;
+ };
+ merged.sort((a, b) => rank(a) - rank(b));
return [overview, ...merged];
}
diff --git a/src/styles/main.css b/src/styles/main.css
index 01204686..755f6034 100644
--- a/src/styles/main.css
+++ b/src/styles/main.css
@@ -90,6 +90,10 @@
@apply opacity-100;
}
+.shiki.has-highlighted .line:not(.highlighted) {
+ @apply opacity-40;
+}
+
@layer utilities {
/* Cursor-following border glow for porcelain pills (deploy row, stack chips).
Lights ONLY the pill's 1px border ring as the pointer moves — not the fill.