- Extract findFrameworksWithCell into @/lib/docs-render so the docs
catch-all and framework-scoped catch-all share one implementation
instead of carrying a near-identical local copy in each (finding #3).
- Validate /docs/integrations/<slug> against the registry with
notFound() when the slug is unknown. Previously a crafted URL like
/docs/integrations/fake-framework silently fell through to an empty
nav tree, indistinguishable from a valid integration with no scoped
content (finding #4).
- Sort integrations by sort_order then slug before picking the
animated preview URL. Registry iteration order alone isn't
deterministic w.r.t. the visual priority the docs UI shows
everywhere else, so the preview now matches (finding #5).
Two additions in snippet.tsx:
1. parseLineRange now accepts comma-separated segments like
`lines="1-5,10-15"`. Each segment is validated and sliced
independently; discontinuous sections are stitched with a visible
`// ...` gap marker so readers see the jump. Invalid segments fail
the whole prop (clear error beats partial rendering). Single-range
and open-ended ("A-") forms continue to work unchanged.
2. resolveHljsLanguage now has explicit entries for tsx (→ typescript),
jsx (→ javascript), and sh / bash / shell (→ bash). These are the
common bundler-emitted hints that previously returned null and
fell through to highlightAuto — producing noisy one-shot warnings
and non-deterministic highlighting.
The __itemsCache has no invalidation hook and lives for the life of
the Node process. That's correct for the current `next start`
deployment but silently fragile if we ever add ISR to /reference —
the cache would serve stale items forever.
Replace the two-line inline comment with a JSDoc block that spells
out the prod vs dev behaviour and the ISR caveat, so future changes
know where the assumption lives.
Three loosely-coupled shim hardenings in one commit (all in mdx-registry.tsx):
- Tooltip / TooltipProvider were children-only shims that silently
dropped Radix-style `content` / `label` props. Route through
stub() so dev gets a one-shot warning with the dropped prop names.
- YouTubeVideo + IframeSwitcher now validate author-controlled URLs
via a shared validateIframeSrc() helper: only https:// is accepted,
malformed URLs fall back to rendering nothing, and dev gets a
targeted warning. YouTubeVideo also enforces the 11-char video-id
shape so a stray value can't traversal-inject query params into the
embed URL.
- Link shim now detects external hrefs (https://..., mailto:..., etc.)
and routes them through a plain <a> with sensible defaults
(target=_blank, rel=noopener noreferrer). Internal hrefs continue to
use next/link for client-side navigation. next/link should never be
used for external URLs — it spuriously prefetches them.
The inlineSnippets regex deliberately only matches strict self-closing
JSX with an optional `components={...}` attr, which confused readers
into thinking tags like `<Snippet region="x" />` fall through by
accident. Document the two-path design (SNIPPET_MAP inlining vs MDX
component map) so future edits don't try to "fix" the selectivity.
No behaviour change. SNIPPET_MAP aliases were already documented
in-place (finding noted earlier); this commit covers the regex comment
only.
readTitle/readMeta are backed by process-scoped Maps. Without a dev
bypass, editing an MDX title or meta.json required a server restart to
see the change — the nav sidebar would keep showing the old title.
Match the convention already used in reference-items.ts: gate both the
read and write paths on isProd() so `next dev` re-reads the source on
every request. Production behaviour is unchanged.
Both props were accepted but ignored (underscore-destructured). scope
was originally intended to switch resolution logic, but both branches
collapsed to the same href rule. fallbackHref was a pre-hydration
placeholder that became unnecessary once the provider made framework
URL-derived.
Remove them from the public SidebarLinkProps and from the only caller
(docs-page-view), which was computing a scope based on slugHrefPrefix
solely to pass it through.
cell and region are authored on the interface for MDX usage + tooling
but the component doesn't consume them at runtime — the parent MDX
renderer pre-renders one <Snippet> per framework on the server and
emits them as children. Mark them optional and document the contract.
Also filter React.Children.toArray output to valid elements before the
index→framework map, so stray whitespace text nodes injected by MDX
don't shift the mapping and render the wrong snippet.
iOS Safari does not reliably fire mousedown for taps landing outside
focusable UI, so the panel stayed open on mobile. Mirror the existing
click-outside handler on touchstart and widen the handler type to
MouseEvent | TouchEvent. stripFrameworkPrefix already restricts its
match to the first path segment (docstring makes this explicit).
- Dev-only console.warn when author-supplied items.length diverges from
the number of <Tab> children — silent drop of extra entries hid bugs.
- Dev-only console.warn on duplicate labels — React keys on label below
so duplicates broke reconciliation.
- Tab button keys now include the index so duplicate labels don't cause
React key collisions.
- Added a useEffect that re-seeds the active tab when the set of labels
changes (e.g. MDX edit swaps titles, HMR reload). Previously useState
initialized once on mount, so a tab whose label disappeared left the
panel stuck empty.
Previously any valid child element received the injected __index/__total
props — stray non-Step valid elements (e.g. spacer divs) would have those
props passed straight to the DOM, triggering React warnings. Now filter
to elements whose type === Step before cloning.
Also add role='list' to the Steps container and role='listitem' to each
Step so assistive tech can enumerate the sequence.
Silent fallback to 'info' masked typos in MDX authors' type props. Now
emit a dev-only console.warn listing the known types when an unknown
one slips past TypeScript (e.g. from raw MDX string literals).
- activeBrandFromPath now matches /ag-ui exactly or /ag-ui/..., not any
path starting with those six chars — prevents misclassifying a future
/ag-ui-* slug as AG-UI.
- Mobile menu panel z-index bumped to z-[51] so layering above the z-50
backdrop doesn't rely on DOM sibling order.
- BrandNavProps (frameworkOptions, frameworkCategoryOrder) were unused
dead API — removed from the interface and function signature. Call
site passes no props, so no caller changes.
React.Children.map only visits *direct* children. When an author
wraps nested <PropertyReference> in a <div> or <Fragment> (a common
MDX pattern), the nested references silently lost `collapsable:
true` propagation because the walk stopped at the wrapper.
Replace the single-level `React.Children.map` with
`deepMapPropertyReferences`, a recursive helper that:
- clones + enhances any PropertyReference at any depth with
`collapsable: true`
- preserves arbitrary wrapper elements (div, Fragment, other
components) and recurses into their children
- stops recursing once it hits a PropertyReference — that
reference's own render will deep-walk ITS children on its turn,
which is the correct nesting semantic
Updates the JSDoc above the enhanced-children block to reflect the
new behavior — nested wrappers are now supported.
Both FrameworkGuardedContent and RouterPivot used to render against
`storedFramework` on the very first client render, when it is always
null (localStorage is read in a mount effect inside
FrameworkProvider). A returning user who picked LangChain would
briefly see the full pivot grid + MDX body flash in before
storedFramework flipped from null to "langgraph-python" and the
redirect fired.
Gate both components behind a local `hasHydrated` flag flipped in a
mount useEffect. Pre-hydration we render null (for the MDX body) or
a minimal "Loading…" placeholder (for the pivot itself), so
returning users go straight to the redirect placeholder instead of
flashing content that's about to disappear. Fresh visitors see the
pivot on the next tick — imperceptible in practice.
Previously readStoredFramework returned null for both "never set" and
"localStorage unavailable" — consumers couldn't distinguish a fresh
visitor from a private-mode browser. Introduce a tri-state: keep
storedFramework: string | null for the value, and add
storageAvailable: boolean so UIs can branch on "we can't persist
your pick" separately from "you haven't picked yet".
Also subscribe to window `storage` events so that when another tab
clears or changes selectedFramework, this tab's stored state stays
in sync (StoredFrameworkHighlight badge + RouterPivot redirect
react immediately without a reload).
Strengthens the comment above the URL-persistence effect explaining
why `stored` is intentionally excluded from the deps array (prevents
a setState ping-pong loop), and adds behavioral "Covered by:" notes
above each non-trivial fix.
## Summary
- Merges #4085 (Atai's no-docker-restructure feature port — merge-ready
variant) as the primary base.
- Renames `showcase/shell-internal/` → `showcase/shell-dashboard/` +
provisions Railway service at `dashboard.showcase.copilotkit.ai`.
- Renames `showcase/shell-dojolike/` → `showcase/shell-dojo/` + renames
Railway service in place (domain unchanged).
- Extracts MDX-docs infrastructure (`/docs`, `/[framework]`, `/ag-ui`,
`/reference` + components, libs, content) from `showcase/shell/` into
new standalone `showcase/shell-docs/` package; provisions Railway
service at `docs.showcase.copilotkit.ai`; adds 301 redirects on shell
for every moved route (including 17 per-framework slugs enumerated from
`registry.json`).
- Fixes `starter-langroid` `/api/health` 503 (sed stdout wrappers +
readiness gate); adds red-green regression guards in
`starter-consistency.test.ts`.
## DNS state
- `dashboard.showcase.copilotkit.ai` CNAME → `6u9icvje.up.railway.app` —
✅ live in Route53.
- `docs.showcase.copilotkit.ai` CNAME → `t6ge0qwv.up.railway.app` —
pending Route53 add.
## Railway services
- `showcase-shell-dashboard` (ID `4d5dfd74-be61-40b2-8564-b53b7dd4c15b`)
— provisioned, image pending first deploy.
- `showcase-shell-docs` (ID `7badfb8d-4228-414c-9145-b4026803714f`) —
provisioned, image pending first deploy.
- `showcase-shell-dojo` (ID `7ad1ece7-2228-49cd-8a78-bddf30322907`) —
renamed from shell-dojolike; already live.
## Closes / supersedes
- Supersedes #4084 (docker-restructure variant; preserved on
`jpr5/ftg-shell-ops` for future consideration, not merged here).
- True-merges #4085 via `--no-ff` — that PR auto-closes on this PR's
merge.
## Known non-blockers
- `/ag-ui/introduction` throws a server-component `onMouseEnter` runtime
error on `next start`. Same issue exists in shell today — pre-existing,
not a regression. Follow-up needed.
- `showcase/shell-dojo` lacks `package-lock.json` and `.gitignore`;
trivial follow-up for parity with siblings.
- `search-modal` retains the flat `search-index.json` shape (not the
extensible `{version:1, sources:[...]}` shape). Generator is the single
point of change if multi-shell search is wanted later.
- oxfmt has a pre-existing oscillation bug on 14 `.mdx` files with
`<Steps>` structures; sidestepped by mirroring the existing
`showcase/shell/src/content/**` ignore policy to the new
`showcase/shell-docs/src/content/**` path.
## Test plan
- [ ] CI green end-to-end (Validate Showcase, unit tests on 20/22/24,
format, oxlint, commitlint, starter-smoke).
- [ ] \`showcase_deploy.yml\` builds shell-dashboard + shell-docs images
to GHCR and Railway pulls them successfully.
- [ ] Add \`docs.showcase\` CNAME in Route53
(\`t6ge0qwv.up.railway.app\`) — without this,
docs.showcase.copilotkit.ai won't resolve and Railway won't provision
its TLS cert.
- [ ] Verify \`dashboard.showcase.copilotkit.ai/\` returns 200.
- [ ] Verify \`docs.showcase.copilotkit.ai/\` returns 200 and
\`/langgraph-python/quickstart\` renders.
- [ ] Verify \`showcase.copilotkit.ai/\` returns grid (200);
\`/docs/foo\` 301s to \`docs.showcase.copilotkit.ai/foo\`;
\`/react/foo\` 301s; \`/integrations\` and \`/matrix\` do NOT redirect.
- [ ] \`starter-langroid\` redeploy via \`showcase-deploy\` workflow:
\`/api/health\` returns 200.
## Summary
- Rename `FAIL=N` → `DRIFT=N` in the weekly showcase pin-drift Slack
alert; "FAIL" reads as a regression even when N matches the ratchet
baseline.
- Restructure three paths: `[stable]` (match), `[REGRESSION]` (count or
hash grew), `[IMPROVED, ratchet me]` (count shrunk).
- Red path retains run URL link. Ratchet logic, baseline file, and
hashing unchanged.
## Test plan
- [ ] Verify `.github/workflows/<name>.yml` YAML parses.
- [ ] Confirm next weekly run posts `[stable]` framing (or dry-run the
step manually if feasible).
- [ ] No change to `showcase/scripts/fail-baseline.json` or ratchet
logic.
The weekly Showcase pin-drift Slack alert previously read
"📉 Showcase pin-drift (weekly): FAIL=N
(baseline N) [ok]". The token "FAIL=N" read as a regression even when N
matched the ratchet baseline exactly (a stable week), generating recurring
false-alarm fatigue on #oss-alerts.
Reframe the notification text into three explicit states:
- [stable] — DRIFT=N matches baseline AND hash matches
- [REGRESSION] — DRIFT=N grew OR hash mismatch (set drifted); keeps the
🚨 emoji + run URL for actionable alerts
- [IMPROVED, ratchet me] — DRIFT=N shrank; reminder to update the baseline
Presentation-only. The ratchet logic in validate-pins.ts, the baseline file
(showcase/scripts/fail-baseline.json), and the FAIL-line hashing are
unchanged.
If shell-docs/src/content/{reference,ag-ui,docs} didn't exist the
script silently produced a tiny index with no warning — operators
only found out by noticing Cmd-K search returning nothing.
Warn per missing directory and exit non-zero if ALL scan roots are
missing (that means we're running outside a prepared tree, e.g.
shell-docs didn't emit into the expected layout).
The main-deploy step already uses '// empty' on PRIOR_DEPLOY_ID but
the verify step extracted DEPLOY_ID / STATUS / DOMAIN without it, so
when Railway's edges array was transiently empty these became the
literal string 'null'. That breaks the prior-vs-fresh comparison and
the DOMAIN emptiness guard in inconsistent ways. Add '// empty' to
all three jq filters so empty-edge responses normalize to '' the
same way everywhere.
The watch loop logged '[watch] bundle failed' once and then fell
silent — repeat failures looked like success, and recoveries were
invisible (no news = assumed fine). Track the last error in module
scope so we distinguish first-failure from repeat-failure, and log an
explicit 'bundle recovered' note when the next green run clears the
state. Makes dev-mode transitions visible instead of silent.
A malformed docs-links.json was only console.warn'd and treated as
empty — build ran green while the override file silently rotted.
Accept an errors accumulator and push parse failures into it so
main()'s non-zero exit path fires. Missing file and stale-shape
tolerance are unchanged (both are legitimate states).
Without a timeout on the inner fetch, a hung upstream would stall the
entire docs-probe run indefinitely (Node's fetch has no default
timeout). Add a 10s AbortController. The bare catch also left 'error'
states opaque — log URL + error kind so operators can tell an
abort from DNS from TLS.
The bare catch returned 'unknown' with no trace, making it impossible
to distinguish DNS failures from aborts from TLS errors in the unknown
bucket. Log the URL plus a kind:code pair so spikes of 'unknown' are
actually diagnosable.
mockE2E/mockSmoke/mockQA were called unconditionally from main(), so
production dashboards shipped seeded test data in columns that aren't
wired to real CI yet. Gate each behind GENERATE_STATUS_MOCK_*
env vars (default off → emit null / 'unknown'). Print a prominent
banner to stderr naming which columns are mock whenever any gate is
on, so operators can't discover stub data by surprise.
A bare catch swallowed JSON.parse failures, silently returning [] and
making every /<slug> framework redirect disappear. Throw in production
(registry is a required build-time artifact) and console.warn in dev
(so a transient mid-write doesn't kill the dev loop). Also throw if
the file is missing in production for the same reason.
The cross-starter consistency test had two AGENT_URL matchers:
function re8000() {
return new RegExp(AGENT_URL_LOCALHOST_8000_RE.source, flags);
}
function re8123() {
return new RegExp(
AGENT_URL_LOCALHOST_8000_RE.source.replace(/8000\\b/, "8123\\b"),
flags,
);
}
The 8123 variant does regex-source string munging — brittle: the
replacement target `8000\\b` has to stay in lockstep with the
shared regex's literal source, and a future port format change would
snap in a subtle way (the test could keep passing while matching
subtly wrong content, or start failing for reasons unrelated to the
starter it's guarding).
Replace the `.source.replace` hack with an exported factory
`makeAgentUrlLocalhostPortRE(port)` in `generate-starters.ts` and
have both `re8000`/`re8123` use it. The existing
`AGENT_URL_LOCALHOST_8000_RE` constant is preserved (now delegating to
the factory) so any external import stays happy.
Bonus: the factory validates port bounds so a typo can't silently
produce a pattern that never matches.
Two orthogonal bugs in the demo-content bundler:
1) README regions were silently dropped. `collectDemoFiles` pulls the
demo-dir root README out into the `readme` field rather than
appending to `files`, but it STILL wrote any `@region[...]`
slices it found inside the README into `perFileRegions` under the
README's bundled path. The region-collation loop at write time
iterated `fileOrder = files.map(f => f.filename)`, which by
construction never contains the README path — so those regions
went nowhere. Walk any `perFileRegions` keys not in fileOrder
after the main pass, so README regions still land in the public
map. Preserves the stable file-order rule (README contributions
come after source files, alphabetical tiebreak).
2) Binary-like files were being passed through
`fs.readFileSync(abs, "utf-8")` and then stringified into the
bundle. A PNG or font file typically produces a mess of
replacement characters in `demo-content.json` — waste at best,
visible corruption at worst when the Code tab ever tried to
render them. Extend `SKIP_EXTENSIONS` with the usual binary
suspects (images, fonts, archives, media, PDFs) so the walker
drops them at scan time.
The canonical schema in `scripts/generate-registry.ts` + per-package
`docs-links.json` calls the shell path `shell_docs_path`. probe-docs
was still reading ONLY the legacy `shell_docs_url` key, so any
future cleanup of `shared/feature-registry.json` to the canonical
key would silently turn every shell-docs status into 'missing'.
Prefer `shell_docs_path`, fall back to `shell_docs_url` so older
registry snapshots still contribute. Emit a one-shot dev-mode warning
when we see only the legacy key, so a lingering registry stays visible
without spamming CI logs.
Two small but load-bearing hygiene fixes in the notify job's shell:
1) The matrix-legs collection pipeline used `jq -r '...' 2>/dev/null`,
swallowing any jq parse or schema errors before the surrounding
pipeline substitution could see them. If the gh-api payload shape
ever shifts (schema rev, partial response, encoding glitch), we
now want the error in the job log so an operator knows WHY notify
fell back to softer wording — the existing `-z "$pairs"` guard
still handles the empty-output path without relying on suppression.
2) The duplicated inline `truncate_csv` helper was declaring
`local ws=...` on its OWN line AFTER `local budget=...` on the
function's first line. Redeclaring `local` inside the same
function body is legal but prints 'local: not in a function'
warnings on some bash versions (3.x, noexec edge cases) and is
outright forbidden by `set -eu` hardening profiles. Collapse `ws`
into the single `local` declaration at the top and use a plain
assignment where it's computed.
The BUILD outcome classifier read:
elif [ "$BUILD" = "failure" ] || [ -n "$BUILD" ]; then
After the earlier `success` + `skipped && detect=success` branches
had claimed the obvious green cases, any non-empty BUILD string fell
through the `-n` test and was tagged FAILURE — including values like
"skipped" that reach this branch when detect-changes did NOT emit
`success` (e.g. cancelled pre-stage races or a partial matrix skip).
That means the Slack bot would occasionally cry red at runs that never
failed.
Simplify to an exact string match on "failure"; the final `else`
branch already handles the empty/unknown case by emitting OUTCOME=skip,
which is the correct disposition for indeterminate state.
The rest of the showcase workflows already pin `actions/checkout@v4`.
This one lagged on v3, which is now long-deprecated (Node 16 runner
EOL). Bringing it in line with the fleet avoids a future forced bump
when GitHub retires v3 runners entirely.
Remove every trace of the defunct test-integration-tmp service from the
showcase deploy workflow:
- drop the `test-integration-tmp` entry from the workflow_dispatch
inputs.service dropdown,
- drop the detect-changes `test_integration_tmp` output + paths-filter
entry, and
- delete the entire build-test_integration_tmp job (including its
placeholder RAILWAY_SERVICE_ID literal).
Matches the jpr5/test_integration_tmp_leak cleanup — the test harness
leak has been patched at its source (create-integration.test.ts), but
these workflow vestiges were never part of that fix and only survived
because nothing read them after the regression guard landed.
Replace the argv-sniffing 'process.argv.includes("build")' check with
process.env.NEXT_PHASE === "phase-production-build", the Next.js
canonical signal for a production build. The argv approach is fragile:
it breaks under wrappers, programmatic invocation, or any tool that
invokes next via a different argv shape.
COMMIT_SHA defaulted to 'unknown', so a caller that forgot to pass
--build-arg COMMIT_SHA would silently ship an image whose footer,
health checks, and error reports all report 'unknown'. Add a guard in
the builder stage that refuses to proceed when COMMIT_SHA is unset or
still the 'unknown' default.
showcase/scripts/ now ships a package-lock.json (committed here for the
first time), so the Docker builder can use 'npm ci' for deterministic
installs instead of 'npm install'. Update the comment to match actual
state and copy the lockfile into the scripts stage.
The runner stage was copying the entire builder node_modules tree,
shipping tailwindcss, postcss, tsx, typescript, and every @types/*
package into the production image. Add 'npm prune --omit=dev' after
the node_modules copy (and also copy package-lock.json so prune runs
deterministically) to strip dev-only packages from the runtime image.
The ag-ui and reference pages each carried their own near-identical
stripImportsFenceAware copy with a TODO(dedup) flag. The canonical
implementation already exists in docs-render.tsx as stripLeadingImports
and has better semantics — it only strips imports that appear in the
top-of-file header region (before the first non-import content line),
rather than any import anywhere outside a fence. That distinction
matters for doc bodies that legitimately discuss imports in prose
outside code fences.
Export stripLeadingImports from docs-render and point both pages at it;
remove the local duplicates.
Card's type accepted icon, className, and children but the component
body never read them — silent prop drop. Render icon above the title,
merge className onto the wrapper, and render children below the
description so MDX authors get the behavior the type signature
promises.