From 7c42b8d12d642edcb721e3bda294ddc4cb201672 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Wed, 24 Jun 2026 16:02:13 +0200 Subject: [PATCH 1/3] docs: section version badges, repo-aware stars, product OG cards, Python SDK - Sidebar version badge per product section: Relayfile v0.10.12, Relayloop v0.3.6 (from the latest ai-hist release) - GitHub stars badge follows the active docs section: relayfile under /docs/file, relayhistory under /docs/loop, relay elsewhere - Product docs get their own OG cards (Relayfile/Relayloop eyebrow) instead of inheriting the "Headless Slack for Agents" default - Add Relayfile Python SDK page (relayfile-sdk, RelayFileClient + on_write) Co-Authored-By: Claude Opus 4.8 --- web/app/docs/file/[slug]/og.png/route.tsx | 48 +++++++++ web/app/docs/file/[slug]/page.tsx | 16 +++ web/app/docs/layout.tsx | 4 +- web/app/docs/loop/[slug]/og.png/route.tsx | 48 +++++++++ web/app/docs/loop/[slug]/page.tsx | 16 +++ web/components/DocsGitHubStarsBadge.tsx | 59 +++++++++++ web/components/GitHubStars.tsx | 34 ++++++- web/components/docs/DocsNav.tsx | 4 + web/components/docs/docs.module.css | 11 ++ web/content/docs/file/python-sdk.mdx | 118 ++++++++++++++++++++++ web/lib/product-docs-nav.ts | 5 + 11 files changed, 359 insertions(+), 4 deletions(-) create mode 100644 web/app/docs/file/[slug]/og.png/route.tsx create mode 100644 web/app/docs/loop/[slug]/og.png/route.tsx create mode 100644 web/components/DocsGitHubStarsBadge.tsx create mode 100644 web/content/docs/file/python-sdk.mdx diff --git a/web/app/docs/file/[slug]/og.png/route.tsx b/web/app/docs/file/[slug]/og.png/route.tsx new file mode 100644 index 0000000..2757e8d --- /dev/null +++ b/web/app/docs/file/[slug]/og.png/route.tsx @@ -0,0 +1,48 @@ +import { ImageResponse } from 'next/og'; +import { notFound } from 'next/navigation'; + +import { DefaultVariant, loadBrandFonts, OG_SIZE } from '../../../../../lib/og/template'; +import { getProductDoc, getProductDocSlugs } from '../../../../../lib/product-docs'; +import { fileSection } from '../../../../../lib/product-docs-nav'; + +export const runtime = 'nodejs'; +// Prerender one card per Relayfile doc at build time. +export const dynamic = 'force-static'; + +type RouteContext = { + params: Promise<{ slug: string }>; +}; + +export function generateStaticParams() { + return getProductDocSlugs(fileSection).map((slug) => ({ slug })); +} + +/** + * Per-doc Open Graph card for Relayfile: the default variant with a "Relayfile" + * eyebrow plus the doc title and description — so these pages get a product card + * instead of the site-wide Agent Relay default. + */ +export async function GET(_request: Request, { params }: RouteContext) { + const { slug } = await params; + const doc = getProductDoc(fileSection.id, slug); + + if (!doc) { + notFound(); + } + + const { fonts, headingFamily, bodyFamily } = await loadBrandFonts(); + + return new ImageResponse( + , + { + ...OG_SIZE, + ...(fonts.length > 0 ? { fonts } : {}), + } + ); +} diff --git a/web/app/docs/file/[slug]/page.tsx b/web/app/docs/file/[slug]/page.tsx index fa325e9..eda8aa4 100644 --- a/web/app/docs/file/[slug]/page.tsx +++ b/web/app/docs/file/[slug]/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next'; import { ProductDocPage } from '../../../../components/docs/ProductDocPage'; +import { ogImage } from '../../../../lib/og-meta'; import { fileSection, getProductDoc, @@ -27,6 +28,8 @@ export async function generateMetadata({ params }: PageProps): Promise return { title: 'Not Found' }; } + const ogPath = `/docs/${SECTION_ID}/${slug}/og.png`; + return { title: `${doc.frontmatter.title} — Relayfile`, description: doc.frontmatter.description, @@ -34,6 +37,19 @@ export async function generateMetadata({ params }: PageProps): Promise canonical: absoluteUrl(`/docs/${SECTION_ID}/${slug}`), types: { 'text/markdown': getProductDocMarkdownUrl(SECTION_ID, slug) }, }, + openGraph: { + title: `${doc.frontmatter.title} — Relayfile`, + description: doc.frontmatter.description, + url: absoluteUrl(`/docs/${SECTION_ID}/${slug}`), + type: 'article', + images: [ogImage(ogPath, `${doc.frontmatter.title} — Relayfile docs`)], + }, + twitter: { + card: 'summary_large_image', + title: `${doc.frontmatter.title} — Relayfile`, + description: doc.frontmatter.description, + images: [absoluteUrl(ogPath)], + }, }; } diff --git a/web/app/docs/layout.tsx b/web/app/docs/layout.tsx index b79cdc5..68f71ff 100644 --- a/web/app/docs/layout.tsx +++ b/web/app/docs/layout.tsx @@ -3,7 +3,7 @@ import type { ReactNode } from 'react'; import { DocsLanguageProvider } from '../../components/docs/DocsLanguageContext'; import { DocsNav } from '../../components/docs/DocsNav'; import { DocsSearch } from '../../components/docs/DocsSearch'; -import { GitHubStarsBadge } from '../../components/GitHubStars'; +import { DocsGitHubStarsBadgeServer } from '../../components/GitHubStars'; import { SiteFooter } from '../../components/SiteFooter'; import { SiteNav } from '../../components/SiteNav'; import styles from '../../components/docs/docs.module.css'; @@ -24,7 +24,7 @@ export default function DocsLayout({ children }: { children: ReactNode }) {
} - actions={} + actions={} mobileMenuContent={} hideMobileDocsLink /> diff --git a/web/app/docs/loop/[slug]/og.png/route.tsx b/web/app/docs/loop/[slug]/og.png/route.tsx new file mode 100644 index 0000000..a4557da --- /dev/null +++ b/web/app/docs/loop/[slug]/og.png/route.tsx @@ -0,0 +1,48 @@ +import { ImageResponse } from 'next/og'; +import { notFound } from 'next/navigation'; + +import { DefaultVariant, loadBrandFonts, OG_SIZE } from '../../../../../lib/og/template'; +import { getProductDoc, getProductDocSlugs } from '../../../../../lib/product-docs'; +import { loopSection } from '../../../../../lib/product-docs-nav'; + +export const runtime = 'nodejs'; +// Prerender one card per Relayloop doc at build time. +export const dynamic = 'force-static'; + +type RouteContext = { + params: Promise<{ slug: string }>; +}; + +export function generateStaticParams() { + return getProductDocSlugs(loopSection).map((slug) => ({ slug })); +} + +/** + * Per-doc Open Graph card for Relayloop: the default variant with a "Relayloop" + * eyebrow plus the doc title and description — so these pages get a product card + * instead of the site-wide Agent Relay default. + */ +export async function GET(_request: Request, { params }: RouteContext) { + const { slug } = await params; + const doc = getProductDoc(loopSection.id, slug); + + if (!doc) { + notFound(); + } + + const { fonts, headingFamily, bodyFamily } = await loadBrandFonts(); + + return new ImageResponse( + , + { + ...OG_SIZE, + ...(fonts.length > 0 ? { fonts } : {}), + } + ); +} diff --git a/web/app/docs/loop/[slug]/page.tsx b/web/app/docs/loop/[slug]/page.tsx index 224e9cf..5b9bba9 100644 --- a/web/app/docs/loop/[slug]/page.tsx +++ b/web/app/docs/loop/[slug]/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next'; import { ProductDocPage } from '../../../../components/docs/ProductDocPage'; +import { ogImage } from '../../../../lib/og-meta'; import { getProductDoc, getProductDocMarkdownUrl, @@ -27,6 +28,8 @@ export async function generateMetadata({ params }: PageProps): Promise return { title: 'Not Found' }; } + const ogPath = `/docs/${SECTION_ID}/${slug}/og.png`; + return { title: `${doc.frontmatter.title} — Relayloop`, description: doc.frontmatter.description, @@ -34,6 +37,19 @@ export async function generateMetadata({ params }: PageProps): Promise canonical: absoluteUrl(`/docs/${SECTION_ID}/${slug}`), types: { 'text/markdown': getProductDocMarkdownUrl(SECTION_ID, slug) }, }, + openGraph: { + title: `${doc.frontmatter.title} — Relayloop`, + description: doc.frontmatter.description, + url: absoluteUrl(`/docs/${SECTION_ID}/${slug}`), + type: 'article', + images: [ogImage(ogPath, `${doc.frontmatter.title} — Relayloop docs`)], + }, + twitter: { + card: 'summary_large_image', + title: `${doc.frontmatter.title} — Relayloop`, + description: doc.frontmatter.description, + images: [absoluteUrl(ogPath)], + }, }; } diff --git a/web/components/DocsGitHubStarsBadge.tsx b/web/components/DocsGitHubStarsBadge.tsx new file mode 100644 index 0000000..7c5153e --- /dev/null +++ b/web/components/DocsGitHubStarsBadge.tsx @@ -0,0 +1,59 @@ +'use client'; + +import { usePathname } from 'next/navigation'; + +import { getProductSectionForPath } from '../lib/product-docs-nav'; +import s from './github-stars.module.css'; + +export type DocsStarRepo = { + /** Product section id (`file`, `loop`) or `null` for the default Agent Relay repo. */ + id: string | null; + href: string; + label: string; + count: string | null; +}; + +function GithubIcon() { + return ( + + ); +} + +/** + * GitHub stars badge that targets the repo for the docs section currently being + * viewed: Relayfile under `/docs/file`, Relayloop under `/docs/loop`, and Agent + * Relay everywhere else. Star counts are fetched on the server and handed in; + * this component just picks the right one for the active path. + */ +export function DocsGitHubStarsBadge({ repos }: { repos: DocsStarRepo[] }) { + const pathname = usePathname() ?? '/docs'; + const section = getProductSectionForPath(pathname); + const entry = + repos.find((r) => r.id === (section?.id ?? null)) ?? repos.find((r) => r.id === null) ?? repos[0]; + + if (!entry) return null; + + return ( + + + {entry.count ? ( + + + {entry.count} + + ) : null} + + ); +} diff --git a/web/components/GitHubStars.tsx b/web/components/GitHubStars.tsx index 4409f07..cb734ee 100644 --- a/web/components/GitHubStars.tsx +++ b/web/components/GitHubStars.tsx @@ -1,9 +1,13 @@ +import { productSections } from '../lib/product-docs-nav'; +import { DocsGitHubStarsBadge, type DocsStarRepo } from './DocsGitHubStarsBadge'; import s from './github-stars.module.css'; type GitHubRepoResponse = { stargazers_count?: number; }; +const DEFAULT_REPO = 'agentworkforce/relay'; + function GithubIcon() { return ( {productSection.label} + {productSection.version && ( + v{productSection.version} + )}

{productSection.tagline}

diff --git a/web/components/docs/docs.module.css b/web/components/docs/docs.module.css index 56cbf8b..f2211b5 100644 --- a/web/components/docs/docs.module.css +++ b/web/components/docs/docs.module.css @@ -319,6 +319,17 @@ color: var(--primary); } +.productHeaderVersion { + font-size: 0.68rem; + font-weight: 600; + letter-spacing: 0.01em; + color: var(--fg-muted); + border: 1px solid var(--border, rgba(116, 184, 226, 0.22)); + border-radius: 999px; + padding: 0.05rem 0.4rem; + line-height: 1.4; +} + .productHeaderTagline { margin: 0.4rem 0 0; font-size: 0.8rem; diff --git a/web/content/docs/file/python-sdk.mdx b/web/content/docs/file/python-sdk.mdx new file mode 100644 index 0000000..13495a6 --- /dev/null +++ b/web/content/docs/file/python-sdk.mdx @@ -0,0 +1,118 @@ +--- +title: 'Python SDK' +description: 'Use relayfile-sdk: RelayFileClient (sync and async) for reads, writes, and bulk operations against a Relayfile workspace, plus on_write for event subscriptions.' +--- + +`relayfile-sdk` is the Python surface for the Relayfile data plane — the same file operations as the TypeScript [`@relayfile/sdk`](/docs/file/sdk) client, in idiomatic Python. It exposes `RelayFileClient` (and `AsyncRelayFileClient`) for reads and writes, and an `on_write` helper for subscribing to change events. The control-plane flow (login, mount) lives in the CLI and the TypeScript SDK; the Python SDK is for agents that read and write a workspace they already have a token for. + +```bash +pip install relayfile-sdk +``` + +The import namespace is `relayfile`. + +## RelayFileClient + +Construct a client with a base URL and a token (a string, or a callable that returns one for auto-refresh): + +```python +from relayfile import RelayFileClient + +client = RelayFileClient( + "https://api.relayfile.dev", + lambda: os.environ["RELAYFILE_TOKEN"], +) +``` + +### Reads + +```python +# list a subtree +tree = client.list_tree("rw_123", {"path": "/notion", "depth": 2}) + +# read one file +page = client.read_file("rw_123", "/notion/pages/roadmap__abc.json") +``` + +### Writes + +`write_file` takes a `WriteFileInput` with a `base_revision` for optimistic concurrency (`If-Match` semantics). Use `"*"` for create-or-overwrite, or pass the last revision you read to detect conflicts: + +```python +from relayfile import RelayFileClient, WriteFileInput, RevisionConflictError + +try: + client.write_file(WriteFileInput( + workspace_id="rw_123", + path="/linear/issues/AGE-12__fix-login-bug.json", + base_revision="*", + content='{"state": "In Review"}', + content_type="application/json", + )) +except RevisionConflictError: + # another writer won the optimistic-concurrency check — re-read and retry + ... +``` + +`bulk_write` writes many files in one unconditional create-or-overwrite request, and `delete_file` removes a file (and queues the provider delete) with its own `base_revision`. + +### Async + +`AsyncRelayFileClient` mirrors the same surface with `await`: + +```python +from relayfile import AsyncRelayFileClient + +async with AsyncRelayFileClient("https://api.relayfile.dev", token) as client: + page = await client.read_file("rw_123", "/linear/issues/AGE-12__fix-login-bug.json") +``` + +## Subscribing with on_write + +`on_write` subscribes a handler to change events on a path pattern, over the same WebSocket stream as the CLI's `listen`. Use it as a decorator or a direct call; it returns an unsubscribe function. Each event carries `path`, `operation`, and `source` (filter out `source == "agent"` to ignore your own writes): + +```python +from relayfile import on_write + +unsubscribe = on_write( + "/linear/issues/by-state/triage/**", + lambda evt: handle(evt.path), + client=client, + workspace_id="rw_123", + operations=("create", "update"), +) +``` + +See [Events and webhooks](/docs/file/events) for the event model and [Real-time sync](/docs/file/realtime-sync) for how change events fan out. + +## Workspace primitives + +The SDK also ships the canonical path helpers used by digest, layout, and writeback tooling — `DIGEST_PATHS`, `is_digest_path`, `provider_layout_path`, and `resource_schema_path` — so you build paths the same way the server does: + +```python +from relayfile import provider_layout_path, resource_schema_path + +layout = client.read_file("rw_123", provider_layout_path("linear")) +schema = client.read_file("rw_123", resource_schema_path("linear", "issues/AGE-16__abc/comments")) +``` + + + The Python SDK is the data-plane client. For logging in, joining a workspace, and mounting, use the [CLI](/docs/file/cli) or the TypeScript [`RelayfileSetup`](/docs/file/sdk#relayfilesetup) — then hand the resulting token to `RelayFileClient`. + + +## Where to go next + + + + The TypeScript client and the `RelayfileSetup` control plane. + + + The normalized event model `on_write` delivers. + + + How write operations map to provider PATCH / CREATE / DELETE. + + + The HTTP surface the client calls. + + diff --git a/web/lib/product-docs-nav.ts b/web/lib/product-docs-nav.ts index 8c1113d..e265bf0 100644 --- a/web/lib/product-docs-nav.ts +++ b/web/lib/product-docs-nav.ts @@ -27,6 +27,8 @@ export interface ProductDocSection { tagline: string; /** GitHub org/repo, used for the sidebar source link. */ repo: string; + /** Current published version, shown as a badge in the sidebar header. */ + version?: string; nav: NavGroup[]; } @@ -35,6 +37,7 @@ export const fileSection: ProductDocSection = { label: 'Relayfile', tagline: 'The event layer for AI agents.', repo: 'AgentWorkforce/relayfile', + version: '0.10.12', nav: [ { title: 'Start', @@ -67,6 +70,7 @@ export const fileSection: ProductDocSection = { title: 'SDK & agents', items: [ { title: 'TypeScript SDK', slug: 'sdk' }, + { title: 'Python SDK', slug: 'python-sdk' }, { title: 'Agent frameworks', slug: 'agents' }, ], }, @@ -96,6 +100,7 @@ export const loopSection: ProductDocSection = { label: 'Relayloop', tagline: 'The system of record for how your team works with AI agents.', repo: 'AgentWorkforce/relayhistory', + version: '0.3.6', nav: [ { title: 'Start', From 6615aa52b16ece57c1fe5a4a332b63b208d29d9e Mon Sep 17 00:00:00 2001 From: Khaliq Date: Wed, 24 Jun 2026 20:23:26 +0200 Subject: [PATCH 2/3] docs: show product version in TOC instead of the Agent Relay v8 switcher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The right-hand TOC panel rendered the Agent Relay version switcher (v8.0.0 ↔ v7.1.1) on every docs page, including Relayfile/Relayloop where it is meaningless. Hide it on product sections and show the section's own version (Relayfile v0.10.12, Relayloop v0.3.6) instead. Co-Authored-By: Claude Opus 4.8 --- web/components/docs/TableOfContents.tsx | 12 +++++++++++- web/components/docs/docs.module.css | 14 ++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/web/components/docs/TableOfContents.tsx b/web/components/docs/TableOfContents.tsx index a9b1e2f..b52d38c 100644 --- a/web/components/docs/TableOfContents.tsx +++ b/web/components/docs/TableOfContents.tsx @@ -1,8 +1,10 @@ 'use client'; import { useEffect, useState } from 'react'; +import { usePathname } from 'next/navigation'; import type { TocItem } from '../../lib/docs'; +import { getProductSectionForPath } from '../../lib/product-docs-nav'; import { useDocsLanguage } from './DocsLanguageContext'; import { DocsVersionSelect } from './DocsVersionSelect'; import styles from './docs.module.css'; @@ -10,6 +12,8 @@ import styles from './docs.module.css'; export function TableOfContents({ items }: { items: TocItem[] }) { const [activeId, setActiveId] = useState(''); const { language, setLanguage } = useDocsLanguage(); + const pathname = usePathname() ?? '/docs'; + const productSection = getProductSectionForPath(pathname); useEffect(() => { const headings = items.map((item) => document.getElementById(item.id)).filter(Boolean) as HTMLElement[]; @@ -47,7 +51,13 @@ export function TableOfContents({ items }: { items: TocItem[] }) { - + {!productSection && } + {productSection?.version && ( +
+ Version + v{productSection.version} +
+ )} {items.length > 0 && (