// DocsLayout.jsx — v0.20.0 (was v0.19.0 / roadmap item #30). // // Left-side flyout nav + content area for the `/docs/*` route tree: // // /docs → redirect to /docs/user-guide // /docs/user-guide → DOCS.md (existing v0.14.0 content) // /docs/specs → client-side redirect to first configured spec // /docs/specs/:name → a single framework spec (v0.20.0) // /docs/sessions → redirect to /docs/sessions/about // /docs/sessions/about → README.md from the sessions repo // /docs/sessions/:nnnn → per-session overview (nav-only navigation) // /docs/sessions/:nnnn/:file → per-transcript view // // v0.20.0 changes (Session 0018.0): // - Adds a "Specs" section between User Guide and Sessions, driven // by `/api/docs/specs/manifest`. // - Sessions render a nested tree: each session row has the // session's transcripts nested under it as their own nav rows // (labeled by `.N` ordinal). The transcript list is fetched per // session via `/api/docs/sessions/:nnnn/index` (cached server- // side, so the manifest+index fan-out is cheap on subsequent // loads). Always-expanded; no collapse toggle (current scale is // under twenty sessions — well under the threshold where lazy // expansion would pay). // // The flyout is a persistent left sidebar on desktop and a slide-out // drawer on mobile (toggled by the icon button in the docs header). // // Amplitude analytics (per SPEC §21): // - track('Doc Viewed', { section: '...' }) on each sub-route mount; // the sub-route component owns the fire. // - flyout buttons + links carry `aria-label` + `data-amp-track-name` // so autocapture rows are readable rather than ":nth-child(7)". import { useEffect, useState, useCallback } from 'react' import { Link, useNavigate, useLocation, Outlet } from 'react-router-dom' import { getSessionsManifest, getSessionIndex, getSpecsManifest } from '../api.js' // Extract the `.N` ordinal from a transcript filename: // "SESSION-0014.1-TRANSCRIPT-...md" → "0014.1" // "SESSION-0013.1.1-TRANSCRIPT-...md" → "0013.1.1" (nested subagent) // Returns the bare filename as fallback if the expected shape isn't // matched (which shouldn't happen — the backend index endpoint // filters by the same regex). function transcriptOrdinal(filename) { const m = /^SESSION-(\d{4}\.\d+(?:\.\d+)*)-TRANSCRIPT/.exec(filename) return m ? m[1] : filename } export default function DocsLayout({ authenticated }) { const [manifest, setManifest] = useState(null) const [manifestState, setManifestState] = useState('loading') // loading | ok | error const [sessionFiles, setSessionFiles] = useState({}) // { nnnn: [filename, ...] } const [specs, setSpecs] = useState([]) const [specsState, setSpecsState] = useState('loading') // loading | ok | error const [drawerOpen, setDrawerOpen] = useState(false) const [reloadTick, setReloadTick] = useState(0) const navigate = useNavigate() const location = useLocation() // Manifest fetch — drives the Sessions section. useEffect(() => { let active = true setManifestState('loading') getSessionsManifest() .then(payload => { if (!active) return setManifest(payload || {}) setManifestState('ok') }) .catch(() => { if (!active) return setManifest({}) setManifestState('error') }) return () => { active = false } }, [reloadTick]) // Per-session transcript lists — fan out from the manifest. Always- // expanded means we pre-fetch every session's index alongside the // manifest, gated on the manifest having loaded successfully. The // backend's 5-minute content TTL makes the repeat cost negligible. useEffect(() => { if (manifestState !== 'ok' || !manifest) return let active = true const nnnnList = Object.keys(manifest).sort() Promise.all( nnnnList.map(nnnn => getSessionIndex(nnnn) .then(payload => [nnnn, (payload && payload.files) || []]) .catch(() => [nnnn, []]) ) ).then(pairs => { if (!active) return setSessionFiles(Object.fromEntries(pairs)) }) return () => { active = false } }, [manifest, manifestState]) // Specs fetch — drives the Specs section. Independent of sessions. useEffect(() => { let active = true setSpecsState('loading') getSpecsManifest() .then(payload => { if (!active) return setSpecs((payload && payload.specs) || []) setSpecsState('ok') }) .catch(() => { if (!active) return setSpecs([]) setSpecsState('error') }) return () => { active = false } }, [reloadTick]) // Close the mobile drawer on every navigation so a click in the nav // doesn't strand the user on a drawer-open view. useEffect(() => { setDrawerOpen(false) }, [location.pathname]) const retryManifest = useCallback(() => { setReloadTick(t => t + 1) }, []) return (
Docs {!authenticated && ( Home )}
{drawerOpen && (
) } function DocsNav({ manifest, manifestState, sessionFiles, specs, specsState, onRetry, currentPath, }) { const isActive = (path) => currentPath === path || currentPath.startsWith(path + '/') const isExactly = (path) => currentPath === path const sessionKeys = Object.keys(manifest || {}).sort() return ( ) }