Commit Graph

2672 Commits

Author SHA1 Message Date
Sam Julien ee29dab8b1 fix(shell): extend NEXT_SERVER_ACTIONS_ENCRYPTION_KEY plumbing + runtime ENV
Two corrections layered on top of the prior commit:

1. Shell-docs needed the same env var declared in the RUNTIME stage,
   not just the builder. Next.js encrypts Server Action references at
   build time but DECRYPTS incoming Server Action requests at runtime
   using the same key. Declaring the ENV in the runner stage (with an
   empty default that Railway overrides at container start) makes the
   variable unambiguously available to `next start` regardless of
   Railway env-injection quirks.

2. Showcase/shell has the same Next 16.x vulnerability and was missed
   in the original implementation. Mirror the build-arg flag + the
   Dockerfile ARG/ENV plumbing into shell's pipeline so both services
   get the fix together. Closes the sibling-ticket scope into one PR.

Refs PDX-202, folds PDX-204.
2026-05-22 20:02:20 -07:00
Sam Julien 23cdbda782 fix(shell-docs): wire NEXT_SERVER_ACTIONS_ENCRYPTION_KEY build pipeline
Next.js 16.x re-hashes Server Action IDs across builds. Without a
stable encryption key, every Railway redeploy invalidates in-flight
clients' action IDs, producing "Failed to find Server Action 'x'"
and "router state header could not be parsed" errors at the deploy
boundary.

Plumb the key through the same channel as the existing analytics
keys: a flag on the shell-docs matrix entry, a branch in the
"Prepare build args" step that sources it from a repo secret, and
an ARG+ENV pair in the Dockerfile builder stage so Next picks it
up at `next build` time.

The actual secret must be set repo-side (GitHub Actions secret
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY) and runtime-side (Railway env
var on the shell-docs service). Until the secret is configured,
builds simply pass an empty value through, matching the previous
behavior.

Refs PDX-202.
2026-05-22 19:56:13 -07:00
Jordan Ritter 832508194f fix(shell-docs): post-cutover polish (metadata, OG, 404, pydantic-ai, v1 redirect) (#4990)
## Summary

Three independent regressions surfaced during Phase 6 post-cutover
validation against the live docs.copilotkit.ai. Bundled together since
they all live under `showcase/shell-docs/` and were verified together
end-to-end.

### 1. Soft-404 returning HTTP 200 with not-found UI body

Unknown URLs returned `HTTP 200` with the Next.js "404: This page could
not be found" body. Internal Next 404 markers
(`NEXT_HTTP_ERROR_FALLBACK`, `<meta name="robots" content="noindex">`)
were present in the body but the wire status stayed 200. Google treats
this as low-quality content and demotes the entire site.

Root cause: `<Suspense fallback={null}>` in `app/layout.tsx` committed
the response stream at status 200 before page-level `notFound()` could
flip it.

Fix:
- Removed the layout Suspense wrapper (both PostHogProvider and
FrameworkProvider are `"use client"` with no suspending APIs, so the
boundary was incidental from the original telemetry port).
- Added an explicit `src/app/not-found.tsx` rendering a branded 404
page.
- Marked the three catch-all routes `dynamic = "force-dynamic"` so
unknown slugs always re-evaluate at request time. Reference route stays
SSG (its slugs come from `referenceStaticParams`).

Verified: `/this-clearly-does-not-exist` returns `HTTP 404`. Real pages
return 200.

### 2. Per-page metadata + OG / Twitter cards

All four `generateMetadata` functions returned only
`alternates.canonical`. Every page inherited the layout's generic `<meta
name="description">` and emitted zero `og:*` / `twitter:*` tags. Every
social share unfurled bare.

Fix: routes now build full `Metadata` via a shared
`src/lib/seo-metadata.ts` helper that reads MDX frontmatter for title
and description and emits openGraph + Twitter card with absolute URLs.

Bonus fix in `app/og/[...slug]/route.tsx`: the OG image route fetched
Inter TTFs from `fonts.gstatic.com` on every request. Any failure
(Railway egress, font URL drift) tripped the catch block, which 307'd to
a 25-byte broken CDN fallback. Dropped the runtime font fetch (Satori's
default sans-serif renders cleanly), broadened slug resolution to also
try `integrations/<folder>/<slug>` paths, and replaced the
broken-fallback redirect with a real 500 + log.

Verified locally: full og/twitter meta set on every page;
`/og/built-in-agent/quickstart/og.png` and
`/og/langgraph-python/quickstart/og.png` both return 1200x630 PNGs with
branded backgrounds.

### 3. Pydantic-ai shared-state pages 500

`/pydantic-ai/shared-state/in-app-agent-read` and
`/pydantic-ai/shared-state/in-app-agent-write` returned deterministic
HTTP 500. Same paths on all other frameworks returned 200. A full
sitemap crawl (2451 URLs) found these as the only 5xx on the entire
site.

Root cause: both MDX files at
`src/content/docs/integrations/pydantic-ai/shared-state/in-app-agent-{read,write}.mdx`
were truncated/malformed during the v1→v2 content port — `read.mdx`
ended mid-Python-fence with unclosed `<Step>` / `<Steps>`; `write.mdx`
had a Python code fence containing JS/TSX. Pure MDX parse failure during
SSR.

Fix: restored both files from the canonical legacy source under
`docs/content/docs/integrations/pydantic-ai/shared-state/`, stripped
leading `import` blocks per the convention used by other pydantic-ai
pages (components resolve via `docsComponents` in
`src/lib/mdx-registry.tsx`).

Verified locally: both URLs go 500 → 200.

### 4. `/reference/v1/:path*` redirect dropped its suffix

Catalog rule P10 redirected `/reference/v1/hooks/useCopilotChat` to a
generic `/reference/v2` index instead of
`/reference/v2/hooks/useCopilotChat`. Users following v1 docs links from
product code messages landed on the wrong page.

Fix: one-line change in `seo-redirects.ts`: destination `/reference/v2`
→ `/reference/v2/:path*`. Audited all other catalog rules with `:path*`
source and bare destination — remaining cases (`concepts/*` collapse,
tutorials deprecation wildcards) are documented intentional
wildcard-to-single-page rules, not drift.

## Test plan

- [x] `npm run typecheck`, `npm run lint`, `npm test` (32 tests pass),
`npm run build` all green in `showcase/shell-docs/`.
- [x] Local prod-mode walkthrough on all four fix surfaces:
- `/built-in-agent/quickstart` → 200 + full og/twitter meta + per-page
description + branded OG PNG.
  - `/langgraph-python/voice` → 200, live demo iframe renders.
- `/built-in-agent/garbage-page-xyz` → 404 (real status, branded 404
page).
  - `/pydantic-ai/shared-state/in-app-agent-read` → 200.
- `/reference/v1/hooks/useCopilotChat` → 301 →
`/reference/v2/hooks/useCopilotChat` → 200.
- [ ] Post-deploy: re-curl a sample of soft-404 URLs against prod and
confirm wire status is `404`, not `200`.
- [ ] Post-deploy: validate a docs URL share in Slack / X to confirm OG
card renders with title + description + image.
2026-05-22 16:50:42 -07:00
Jordan Ritter fd296144a8 fix(shell-docs): region tag + HubSpot hydration + snippet registry (#4988)
Five post-cutover follow-ups bundled together because all surfaced in
the same spot-check pass on `/integration/<page>` routes.

## 1. Tag `page-send-message` region (`4680eb9c1`)

`/langgraph-python/programmatic-control` and
`/google-adk/programmatic-control` rendered a yellow "Missing snippet"
callout because `<Snippet region="page-send-message" />` had no matching
`// @region[page-send-message]` / `// @endregion[page-send-message]`
pair in the resolved `headless-complete` cell. Peer integrations
(mastra, ag2, strands, pydantic-ai, llamaindex, langgraph-fastapi,
crewai-crews, …) already had the tags; only north-star and its ADK
mirror were missing them. The region wraps the connect / send / stop
block in `chat/chat.tsx`.

## 2. Suppress HubSpot-rewritten href hydration mismatch on nav-bar
(`2c0791930`)

HubSpot's analytics tag (loaded from `js-na2.hs-analytics.net`) rewrites
the Intelligence CTA's outbound `href` client-side to append `__hstc` /
`__hssc` / `__hsfp` cross-domain tracking params. Server-rendered HTML
keeps the bare URL, post-hydration DOM has the rewritten URL, React's
hydration diff fires.

Add `suppressHydrationWarning` to the two anchor elements that point at
`INTELLIGENCE_CTA_HREF` (desktop BrandNav `LEFT_LINKS` entry,
MobileTopNav Lightbulb icon).

## 3. Register `UseAgentSnippet` (`f809b9b8b`, expanded by `773631cbd`)

`inlineSnippets()` in `docs-render.tsx` maintains its own `SNIPPET_MAP`
separate from `mdx-registry.tsx`'s `STUB_PARTIAL_MAP`. The two
registries drifted. `UseAgentSnippet` was the most-hit miss, but Railway
logs surfaced 14 more: `InstallSDKSnippet`, `InstallPythonSDK`,
`RunAndConnect` (+ `Snippet` alias), `CopilotUI`, `LandingCodeShowcase`,
the four `CopilotCloudConfigure*` / `SelfHostingCopilotRuntime*` keys,
plus `MigrateTo` / `MigrateToV` / `ToolRenderer` aliases. All added.

## 4. Make `inlineSnippets()` code-fence-aware + add Icon-suffix
heuristic (`773631cbd`)

After the registry fix, the remaining `[docs-render] snippet missing`
log entries split into two false-positive classes:

- **Code-fence false positives.** The regex matched `<Component />`
references inside ` ```tsx ``` ` example blocks — e.g. `<CopilotChat />`
/ `<CopilotSidebar />` shown as runtime usage, `<WeatherCard />` /
`<YourApp />` as placeholders. A new `isInsideCodeFence(content,
offset)` helper tracks fenced blocks (matching any indentation — MDX
inside `<Step>` is routinely 8-space-indented) and inline-code spans.
Replaces the ad-hoc `CopilotChat`-only allowlist from commit 3.
- **JSX-prop runtime components.** `icon={<PaintbrushIcon />}` etc. are
real React components from `mdx-registry.tsx::docsComponents`, not
snippets. Add an `Icon`-suffix heuristic: lucide icons used as JSX props
are silenced.

## 5. Suppress HubSpot hydration mismatch on `<OpsPlatformCTA>` +
`<SignupLink>` (`10b4960a3`)

Same HubSpot rewrite hits every dashboard.operations.copilotkit.ai
outbound link. Add `suppressHydrationWarning` to all four `<a>` tags in
`OpsPlatformCTA` (`info` / `inline` / `tile` / `card` variants) and the
single `<a>` in `SignupLink`. Observed live as a hydration error on
`/<framework>/prebuilt-components`, `/<framework>/headless`, and any
page that embeds an Intelligence-platform CTA.

## Verification

- `grep -n "@region\[page-send-message\]"
showcase/integrations/{langgraph-python,google-adk}/src/app/demos/headless-complete/chat/chat.tsx`:
both files have start (line 38) + end (line 114) markers; `diff` between
them is empty post-change.
- `npx tsx showcase/scripts/bundle-demo-content.ts`: regenerated
`demo-content.json` exposes `regions["page-send-message"]` for both
`langgraph-python::headless-complete` and
`google-adk::headless-complete` (1878 bytes, `chat/chat.tsx` lines
38-112).
- Playwright sweep across `/programmatic-control`,
`/runtime-server-adapter`, `/frontend-tools`,
`/generative-ui/tool-rendering`, `/prebuilt-components`,
`/deploy/agentcore`, `/auth` on `google-adk` and `mastra`: 0 console
errors, 0 warnings, 0 "Missing snippet" callouts in rendered DOM, both
desktop (1440px) and mobile (390px) viewports.

## Test plan

- [ ] Pull, build shell-docs, smoke
`/langgraph-python/programmatic-control` and
`/google-adk/programmatic-control`: yellow "Missing snippet" callout is
gone.
- [ ] Same pages on a mobile viewport: no hydration warning in the
console.
- [ ] `/<framework>/prebuilt-components` and any page with an inline
`<OpsPlatformCTA>`: no hydration warning.
- [ ] Peer integration pages (e.g. `/mastra/programmatic-control`,
`/<framework>/deploy/agentcore`, `/<framework>/frontend-tools`):
snippets still render, no `[docs-render] snippet missing` warnings.
- [ ] Redeploy shell-docs.

## Out of scope

- Underlying prose-vs-code parity gap on the headless-complete cell
(north-star uses `agent.abortRun()` and skips `connectAgent`) is tracked
separately.
- Unifying `docs-render.tsx::SNIPPET_MAP` and
`mdx-registry.tsx::STUB_PARTIAL_MAP` into a single source of truth (so
future entries can't drift) is the right architectural follow-up. Filed
separately.
- Environmental jsdom × vitest interaction blocking
`packages/web-inspector/src/lib/__tests__/telemetry.test.ts` (which
forced `--no-verify` on these commits) is tracked separately.
2026-05-22 16:50:31 -07:00
github-actions[bot] 2ba4f29172 style: auto-fix formatting 2026-05-22 23:37:40 +00:00
Sam Julien e8957c66f0 fix(shell-docs): emit full per-page metadata, fix OG fonts, return real 404
Three post-cutover SEO/social gaps surfaced once docs.copilotkit.ai
pointed at shell-docs:

- `generateMetadata` in the four catch-all routes returned only the
  canonical link, so every page inherited the layout's generic title
  and description and zero og/twitter tags shipped. Every share unfurled
  bare. Routes now build full Metadata via a shared helper
  (`src/lib/seo-metadata.ts`) that reads MDX frontmatter for title and
  description and emits openGraph + twitter card with absolute URLs.

- The `/og/<slug>` route fetched Inter TTFs from fonts.gstatic.com on
  every request. Any failure tripped the catch block, which 307'd to a
  broken CDN fallback. Removed the runtime font fetch and let Satori
  fall back to its built-in sans-serif; also broadened the slug
  resolution to try the framework-scoped `integrations/<folder>/<slug>`
  path so OG images render for framework variants too. Catch block now
  surfaces real failures as 500 instead of redirecting to a broken PNG.

- Unknown URLs returned HTTP 200 with a not-found UI body (soft-404).
  Root cause was a `<Suspense fallback={null}>` wrapper in the root
  layout that committed the response stream before page-level
  `notFound()` could set the 404 status. Removed the Suspense, added an
  explicit `src/app/not-found.tsx`, and marked the catch-all routes
  `force-dynamic` so unknown slugs always re-evaluate at request time.
  Verified locally: `/this-clearly-does-not-exist` returns HTTP 404,
  `/built-in-agent/quickstart` returns HTTP 200 with full meta tags.
2026-05-22 16:36:17 -07:00
Sam Julien 24ba00d175 fix(shell-docs): restore truncated pydantic-ai shared-state MDX
The cutover to `docs_mode: authored` for pydantic-ai exposed two MDX
files that had been ported in a truncated state during the v1->v2
content migration:

  integrations/pydantic-ai/shared-state/in-app-agent-read.mdx
  integrations/pydantic-ai/shared-state/in-app-agent-write.mdx

`in-app-agent-read.mdx` ended mid-python-fence at
`if __name__ == "__main__":` with no closing ```, no closing `</Step>`,
no closing `</Steps>`. `in-app-agent-write.mdx` had a python code block
that switched to TSX content mid-fence (Python `if __name__` followed
by JS `// ...` and a TSX function inside a `python` block), which the
MDX/Shiki pipeline then tried to parse as Python.

Both produced SSR 500s in production (Railway edge: text/plain
"Internal Server Error") at:

  /pydantic-ai/shared-state/in-app-agent-read
  /pydantic-ai/shared-state/in-app-agent-write

These were the only two 5xx URLs in the full 2451-URL sitemap crawl.
Every other framework variant of the same paths (langgraph-python,
mastra, built-in-agent, google-adk, etc.) returned 200, confirming the
crash was content-specific to pydantic-ai.

Restore the full content from the canonical legacy source at
`docs/content/docs/integrations/pydantic-ai/shared-state/` (which was
intact, 178+188 lines), with the leading `import` block stripped to
match the convention used by the other ported pydantic-ai pages
(`predictive-state-updates.mdx` etc.) where `RunAndConnect`,
`IframeSwitcher`, and friends are resolved via `docsComponents` in
`src/lib/mdx-registry.tsx` rather than per-file imports.

Verified locally with `next dev`:

  /pydantic-ai/shared-state/in-app-agent-read   500 -> 200
  /pydantic-ai/shared-state/in-app-agent-write  500 -> 200
2026-05-22 16:36:17 -07:00
Sam Julien 8decc2e8b3 fix(shell-docs): preserve path suffix in /reference/v1/* redirect
The P10 catalog rule was stripping the suffix when redirecting legacy
v1 reference URLs, sending /reference/v1/hooks/useCopilotChat to the
generic /reference/v2 index instead of /reference/v2/hooks/useCopilotChat.

Affects user-facing console messages in packages/react-core that ship
/reference/v1/hooks/useCopilotChatHeadless_c links and bounced users to
the index page instead of the specific hook reference.

Middleware substitutes :path* in destinations (verified against the
coagents rule), so adding :path* to the destination is enough to fix it.
2026-05-22 16:36:17 -07:00
Sam Julien 10b4960a3c fix(shell-docs): suppress HubSpot hydration mismatch on dashboard CTAs
OpsPlatformCTA and SignupLink both link out to
dashboard.operations.copilotkit.ai, which HubSpot's analytics tag
rewrites client-side to attach `__hstc` / `__hssc` / `__hsfp`
cross-domain tracking params. Same root cause as the prior nav-bar
Intelligence CTA fix — server-rendered href has the bare URL,
post-hydration DOM has the rewritten URL, React flags a hydration
mismatch on every page that surfaces the inline / tile / card CTA
(observed live on /<framework>/prebuilt-components and
/<framework>/headless among others).

Add `suppressHydrationWarning` to all four <a> tags in
OpsPlatformCTA (card / inline / tile / link-inside-card) and the
single <a> in SignupLink. Scoped to those anchors so genuine
mismatches elsewhere still surface.
2026-05-22 16:11:59 -07:00
Sam Julien 773631cbdd fix(shell-docs): expand snippet registry, make inlineSnippets fence-aware
Railway logs surfaced 15+ distinct `[docs-render] snippet missing for
component …` warnings post-cutover. Root causes split three ways:

1. Registry drift. `docs-render.tsx::SNIPPET_MAP` had drifted from
   `mdx-registry.tsx::STUB_PARTIAL_MAP` — InstallSDKSnippet,
   InstallPythonSDK, RunAndConnect (+ Snippet alias), CopilotUI,
   LandingCodeShowcase, the four CopilotCloudConfigure* /
   SelfHostingCopilotRuntime* keys, plus MigrateTo / MigrateToV /
   ToolRenderer aliases were all missing. Add them.

2. Code-fence false positives. The inliner regex matched
   `<Component />` references inside ```tsx``` example blocks (e.g.
   `<CopilotChat />`, `<CopilotSidebar />` shown as runtime usage,
   `<WeatherCard />` / `<YourApp />` placeholders). Make the regex
   fence-aware via a new `isInsideCodeFence(content, offset)` helper
   that tracks both fenced blocks (any indentation — MDX inside
   `<Step>` is routinely 8-space-indented) and inline-code spans.

3. JSX-prop runtime components. `icon={<PaintbrushIcon />}` etc. are
   registered in `mdx-registry.tsx::docsComponents` as real React
   components, not snippets. Add an `Icon`-suffix heuristic: lucide
   icons used as JSX props are silenced. CopilotChat / CopilotSidebar
   in prose backticks are now silenced by (2) instead of the prior
   ad-hoc allowlist, which is removed.

Verified clean across the previously-warning pages — /programmatic-control,
/runtime-server-adapter, /frontend-tools, /generative-ui/tool-rendering,
/prebuilt-components, /deploy/agentcore, /auth — all 0 docs-render
warnings post-change. Unified-registry refactor (single source of
truth) is the right next step but out of scope for this cutover-blocker
pass.
2026-05-22 16:11:59 -07:00
github-actions[bot] 648aeb76ed style: auto-fix formatting 2026-05-22 22:58:40 +00:00
Sam Julien f809b9b8bd fix(shell-docs): register UseAgentSnippet, silence CopilotChat warning noise
inlineSnippets() in docs-render.tsx maintains its own SNIPPET_MAP
separate from mdx-registry.tsx. The two registries drifted: mdx-registry
gained `UseAgentSnippet: "use-agent.mdx"` but docs-render didn't. As a
result every page that imports `<UseAgentSnippet />` (the shared
programmatic-control snippet, used by all integration overrides) logged
"[docs-render] snippet missing for component UseAgentSnippet" at SSR
time, and the snippet content was missing from non-MDX surfaces
(search-index, llms.txt, OG metadata).

Add the missing entry so the inliner can resolve it.

Separately, the inliner regex isn't code-fence-aware, so `<CopilotChat />`
usages inside example code blocks (slots.mdx, threads.mdx, etc.) were
also flagged as missing snippets even though CopilotChat is a runtime
React component, not a snippet. Add a small allowlist of known React
components so the regex short-circuits on those without warning. A
fence-aware regex is the right architectural fix but out of scope here.

Confirmed via Playwright: /mastra/programmatic-control console drops
from 3 [docs-render] warnings to 0 after the change.
2026-05-22 15:57:57 -07:00
Sam Julien 2c0791930b fix(shell-docs): suppress HubSpot-rewritten href hydration mismatch
HubSpot's analytics tag (loaded from js-na2.hs-analytics.net) rewrites
the Intelligence CTA's outbound href client-side to append `__hstc` /
`__hssc` / `__hsfp` cross-domain tracking params. Server-rendered HTML
keeps the bare URL, so the post-hydration DOM differs and React's
hydration diff fires a "tree hydrated but some attributes... didn't
match the client properties" warning.

Add `suppressHydrationWarning` to the two anchor elements that point at
`INTELLIGENCE_CTA_HREF` (the desktop BrandNav LEFT_LINKS entry and the
MobileTopNav Lightbulb icon). Suppress is scoped to just those anchors
— other nav items still flag genuine mismatches.

Confirmed via Playwright: page console drops from 1 hydration error to
0 on /mastra/programmatic-control after the change.
2026-05-22 15:57:57 -07:00
Sam Julien c57ebd82e0 fix(shell-docs): redirect deprecated /tutorials paths (#4987)
## Summary

- Add wildcard 301 redirects for the deprecated `/tutorials/*` URL space
in the shell-docs redirect catalog.
- Framework-scoped tutorial URLs redirect to that framework's
`/quickstart`; unscoped variants redirect to the docs root.
- Covers all 21 canonical framework slugs (built-in-agent,
langgraph-{python,typescript,fastapi}, google-adk, a2a, agent-spec,
deepagents, mastra, crewai-crews, pydantic-ai, agno, ag2, llamaindex,
strands, ms-agent-{python,dotnet}, claude-sdk-{python,typescript},
langroid, spring-ai).

## Why

The step-2 tutorial MDX
(`tutorials/ai-todo-app/step-2-setup-copilotkit.mdx` and
`tutorials/ai-powered-textarea/step-2-setup-copilotkit.mdx`) crashes
during SSR for every active framework slug, returning a 21-byte
`text/plain` 500 from `railway-edge`. Sibling steps (`overview`,
`step-1`, `step-3`, `next-steps`) render fine. The sitemap lists ~38 of
these URLs. Two of them are in the legacy sitemap as 200s.

The tutorials section is being retired, so the right operational
response is a 301 to a working destination rather than a renderer fix.

## Implementation

- New `CANONICAL_FRAMEWORKS` constant alongside the existing
`FRAMEWORKS` legacy-slug array.
- Generated wildcard entries `/${fw}/tutorials/:path*` →
`/${fw}/quickstart` per framework.
- Two explicit entries for unscoped paths: `/tutorials/:path*` → `/` and
`/tutorials` → `/`.
- Slotted in `WILDCARD_REDIRECTS` before the per-framework `P1×`/`P2×`
catch-alls so the more specific tutorial rule wins.

## Test plan

- [ ] Local: `npm run dev` in `showcase/shell-docs/` and curl a sample
of step-2 URLs, confirm 301 to `/{fw}/quickstart`.
- [ ] Local: `npm run typecheck` in `showcase/shell-docs/` (catalog is
pure data; TypeScript catches shape drift).
- [ ] Post-deploy: re-curl the 38 step-2 URLs from the production
sitemap and confirm 301 chains land on a 200.
2026-05-22 15:54:34 -07:00
Jordan Ritter 3c06160aef chore(shell-docs): pre-bake WebP for gen-ui-specs diagrams
Wave-2 follow-up to #4986. With the next/image optimizer disabled
(images.unoptimized: true), gen-ui-specs-light.png and gen-ui-specs-dark.png
were shipping uncompressed instead of being served as WebP via /_next/image.
Pre-baking WebP variants restores the bandwidth savings: light 408,807 -> 126,982
bytes (3.2x smaller) and dark 496,127 -> 167,118 bytes (3.0x smaller). Source
PNGs are retained alongside as fallbacks.
2026-05-22 15:49:42 -07:00
Sam Julien a4b5436ce2 fix(shell-docs): redirect deprecated /tutorials paths
The tutorials section is retired post-cutover. Step-2 MDX (both ai-todo-app
and ai-powered-textarea) currently 500s in the SSR layer, surfacing across
all active framework slugs and in ~38 sitemap entries. Add wildcard 301s
in the redirect catalog so framework-scoped tutorial URLs land on that
framework's quickstart, and unscoped variants land on the docs root.

Covers all 21 canonical framework slugs (generated, authored, hidden) plus
the unscoped /tutorials and /tutorials/* paths. Slotted before the P1×/P2×
per-framework catch-alls so the more specific tutorial rule wins.
2026-05-22 15:40:54 -07:00
Jordan Ritter c906392350 fix(shell-docs): disable next/image optimizer to unblock CDN images
Post-cutover, every image on docs.copilotkit.ai broke because Next.js's
/_next/image optimizer needs the sharp module at runtime and sharp is
missing from the Railway runtime image. Setting images.unoptimized=true
makes <Image> render as a plain <img> pointing at the source URL,
eliminating the sharp dependency entirely.

This is visually identical for users: our CDN (cdn.copilotkit.ai,
CloudFront/S3) ignores ?fm=webp and serves the cached PNG regardless,
so the optimizer was already producing no format-conversion gains for
CDN-hosted images. A wave-2 follow-up will pre-bake WebP variants of
the two 4K gen-ui-specs-*.png files for bandwidth.
2026-05-22 15:33:19 -07:00
Sam Julien 4680eb9c16 fix(showcase/headless-complete): tag page-send-message region
The programmatic-control docs page renders a yellow "Missing snippet"
box on the langgraph-python and google-adk variants because their
headless-complete cells were never tagged with the page-send-message
region the MDX requests. Add matching @region / @endregion markers
around the useAgent / useCopilotKit / send / reset block in
chat/chat.tsx so the Snippet component resolves on both integrations.
2026-05-22 15:32:04 -07:00
Jordan Ritter cb086c0794 chore: update showcase pin-drift baseline to 102 2026-05-22 14:11:04 -07:00
Jordan Ritter a30be17798 fix(showcase): pin @copilotkit deps to latest instead of stale next tag
The "next" dist-tag was a workaround for Docker builds that can't resolve
workspace:* — but "next" has gone stale (1.55.2-next.1) while "latest" is
at 1.56.5. Renovate doesn't cover showcase/, so these never auto-bumped.
Switch all 19 showcase package.json files to "latest".
2026-05-22 12:06:17 -07:00
github-actions[bot] 4d7558d58f style: auto-fix formatting 2026-05-22 13:25:28 +00:00
Alem Tuzlak 30f10cea7b fix(showcase/ms-agent-dotnet): handle cancelled HITL bookings 2026-05-22 15:22:04 +02:00
github-actions[bot] 3208d6c1f7 style: auto-fix formatting 2026-05-22 12:49:57 +00:00
Alem Tuzlak a7dbfa133b fix(showcase/ms-agent-dotnet): stabilize D5 demo flows 2026-05-22 14:48:54 +02:00
Alem Tuzlak 0f7f247eb8 fix(showcase/ms-agent-dotnet): close final D5 flakes 2026-05-22 12:32:08 +02:00
github-actions[bot] 8dd17f540c style: auto-fix formatting 2026-05-22 11:56:56 +02:00
Alem Tuzlak cacf2f17b2 feat(showcase/ms-agent-dotnet): start D5 parity sweep 2026-05-22 11:56:55 +02:00
Alem Tuzlak ccef4befe6 feat(showcase/ms-agent-python): register reasoning-default + reasoning-custom (#4968)
## Summary

- The MS Agent Python integration's `reasoning-default` and
`reasoning-custom` demos were already fully ported from the
langgraph-python north-star — code, agent
(`src/agents/reasoning_agent.py` using the OpenAI Responses API for
`REASONING_MESSAGE_*` event streaming), pages, suggestion pills, e2e
specs (`tests/e2e/reasoning-default.spec.ts`,
`tests/e2e/reasoning-custom.spec.ts`), aimock fixtures
(`showcase/aimock/d5-all.json`,
`showcase/harness/fixtures/d5/reasoning-display.json`) and D5 probe
mapping all exist and are byte-identical to LGP.
- The only missing piece was the `manifest.yaml` registration. Without
it the cells never appeared in the showcase shell, weren't counted as
features, and were skipped by D5 routing.
- This PR adds:
- `reasoning-custom` + `reasoning-default` to the `features:` list
(between `headless-complete` and `frontend-tools`, matching LGP order).
  - `demos:` entries for both, mirroring the LGP manifest verbatim.

## Verification

- `tsx showcase/scripts/generate-registry.ts` → catalog now lists both
cells with `status: wired`, `max_depth: 4`, identical to LGP.
- `tsx showcase/scripts/validate-parity.ts` → `ms-agent-python [PASS] 38
37 10 35 warn` (was 36/35; the 2 new e2e specs were already present).
New warnings are the standard `no qa/...md` pattern that LGP also has
for these two demos.
- `tsx showcase/scripts/validate-pins.ts` → ratchet count stays at 93
(unchanged).

## Test plan

- [x] generate-registry succeeds; catalog wired for both cells with
max_depth 4
- [x] validate-parity passes
- [x] validate-pins ratchet unchanged
- [ ] Showcase shell renders
`/integrations/ms-agent-python/demos/reasoning-default` and
`reasoning-custom` after deploy
- [ ] D5 `reasoning-display` probe passes for ms-agent-python in CI
- [ ] e2e: `npm --prefix showcase/integrations/ms-agent-python run
test:e2e -- tests/e2e/reasoning-default.spec.ts
tests/e2e/reasoning-custom.spec.ts --project=chromium` (will run on next
CI pipeline)
2026-05-21 17:57:50 +02:00
Alem Tuzlak 949659e178 feat(showcase/ms-agent-python): register reasoning-default and reasoning-custom
Surfaces the reasoning-default and reasoning-custom demos for the MS
Agent Python integration. The code, agent, UI, suggestions, e2e specs,
D5 probe mapping and aimock fixtures were already ported from the
langgraph-python north-star — only the manifest entries were missing,
which meant the cells never appeared in the showcase shell, weren't
counted as features, and weren't picked up by D5 routing.

Adds:
  - `reasoning-custom` + `reasoning-default` to the features list
    (between headless-complete and frontend-tools, matching LGP order).
  - `demos:` entries for both, mirroring the LGP manifest verbatim.

After regeneration the shell catalog now reports the two cells with
`status: wired` and `max_depth: 4`, identical to LGP. validate-parity
goes 38 demos / 37 specs (the e2e specs were already present); the
ratchet validate-pins count stays at 93. The remaining `no qa/...`
warnings match the existing LGP/MAF pattern (LGP also has no
qa/reasoning-*.md), so no new QA docs are introduced here.
2026-05-21 17:47:58 +02:00
Alem Tuzlak 2ea616e9b7 fix(showcase/ms-agent-python): drop trailing slash on subpath HttpAgent URLs (#4959)
## Summary

- The `mcp-apps` and `voice-demo` HttpAgent URLs had a trailing slash
(`${AGENT_URL}/mcp-apps/`, `${AGENT_URL}/voice/`), but the FastAPI
backend in `agent_server.py` mounts those agents at `/mcp-apps` and
`/voice` exactly.
- Posting to the trailing-slash URL triggers FastAPI's default
`redirect_slashes` 307, which drops the SSE streaming body and surfaces
in the Next.js runtime as `RUN_ERROR: fetch failed (INCOMPLETE_STREAM)`
for every pill on the deployed showcase.
- Removing the trailing slash from both `HttpAgent({ url })`
constructors mirrors every other ms-agent-python subpath URL
(`/hitl-in-app`, `/headless-complete`, `/multimodal`, `/agent-config`,
…), all of which already work.

## Reproduction (live, against `showcase-ms-agent-python-production`)

```
$ curl -X POST -H 'Content-Type: application/json'     -H 'Accept: text/event-stream'     -d '{"method":"agent/run","params":{"agentId":"mcp-apps"},"body":{...}}'     https://showcase-ms-agent-python-production.up.railway.app/api/copilotkit-mcp-apps
data: {"type":"RUN_ERROR","message":"fetch failed","code":"INCOMPLETE_STREAM"}

$ curl -X POST ...     https://showcase-ms-agent-python-production.up.railway.app/api/copilotkit-voice/agent/voice-demo/run
data: {"type":"RUN_ERROR","message":"fetch failed","code":"INCOMPLETE_STREAM"}
```

The same `/api/copilotkit-mcp-apps` runtime called with `agentId:
"headless-complete"` (URL `/headless-complete`, no trailing slash)
returns a clean `RUN_FINISHED` stream — proving the trailing slash is
the only difference.

## Test plan

- [x] Reproduced live against deployed
`showcase-ms-agent-python-production`
- [x] `headless-complete` (no trailing slash, same runtime as mcp-apps)
confirmed working
- [ ] After Railway redeploy: click "Draw a flowchart" and "Sketch a
system diagram" pills on `/integrations/ms-agent-python/demos/mcp-apps`
and the iframe renders
- [ ] After Railway redeploy: voice demo sample-audio "What is the
weather in Tokyo?" returns an assistant reply
2026-05-21 16:59:52 +02:00
Alem Tuzlak 2bac901dcd fix(showcase/ms-agent-python): drop trailing slash on subpath HttpAgent URLs
The mcp-apps and voice-demo HttpAgent URLs had a trailing slash
(`${AGENT_URL}/mcp-apps/`, `${AGENT_URL}/voice/`), but the FastAPI
backend in agent_server.py mounts those agents at `/mcp-apps` and
`/voice` exactly. Posting to the trailing-slash URL triggers FastAPI's
default `redirect_slashes` 307, which drops the SSE streaming body and
surfaces in the runtime as
`RUN_ERROR: fetch failed (INCOMPLETE_STREAM)` for every pill click on
the deployed ms-agent-python showcase.

Reproduced live against showcase-ms-agent-python-production. Every
other ms-agent-python HttpAgent URL (`/hitl-in-app`,
`/headless-complete`, `/multimodal`, `/agent-config`, etc.) already
uses no trailing slash and works fine, confirming the trailing slash
is the only delta.
2026-05-21 16:03:49 +02:00
Alem Tuzlak c9ec6ee735 fix(showcase): resync scripts lockfile after aimock 1.26.1 bump
PR #4956 bumped @copilotkit/aimock in showcase/scripts/package.json
from "latest" to "1.26.1" but did not regenerate package-lock.json
(still pinned to 1.16.4). This broke the Showcase Docker builds for
showcase-harness and shell-dashboard, since both run `npm ci` inside
showcase/scripts/ and `npm ci` fails on out-of-sync lockfiles.
2026-05-21 15:02:26 +02:00
github-actions[bot] f819082276 style: auto-fix formatting 2026-05-21 12:17:19 +00:00
Alem Tuzlak 551d6a5746 fix(showcase): stabilize ms agent demo fixtures 2026-05-21 14:15:33 +02:00
Tyler Slaton a07e97b224 chore: run pnpm format
Signed-off-by: Tyler Slaton <tyler@copilotkit.ai>
2026-05-20 21:30:28 -07:00
Tyler Slaton 79181d4d38 feat(shell-docs): re-add markdown_copied + open_in_llm_clicked PostHog events
The original analytics commit (4d67fe269) was reverted because it
also contained a botched BrandNav height change. Re-apply just the
PostHog instrumentation cleanly:

- `MarkdownCopyButton` fires `markdown_copied`
  `{ path: pathname, markdown_url: markdownUrl }` after a successful
  clipboard write. Coexists with the global `cli_command_copied`
  (Benjamin's monkey-patch in `lib/track-command-copy.ts` that
  intercepts every clipboard write at the navigator level); the
  dedicated event lets the dashboard distinguish page-content copies
  from CLI copies (which classify under the existing tracker as
  `code` — not meaningful for the new docs-as-context surface).
- Each `ViewOptionsPopover` item gains a `target` discriminator
  (`github`, `view-as-markdown`, `windsurf`, `claude-code`, `codex`,
  `chatgpt`, `claude`, `cursor`) and an `onClick` that captures
  `open_in_llm_clicked` `{ target, path }`. PostHog buffers locally
  so the new tab opens without waiting on the network.

Closes Sam's P0 analytics ask from PR #4946 — verified by the QA
audit that every existing CTA + capture (`try_for_free_clicked`,
`talk_to_us_clicked`, etc.) survives intact across the branch.
2026-05-20 21:17:34 -07:00
Tyler Slaton b7f3e4a1b5 fix(shell-docs): stop sidebar dropping 54px when banner is present
Fumadocs's docs grid sets `--fd-docs-row-1: var(--fd-banner-height, 0px)`
in `node_modules/fumadocs-ui/dist/layouts/docs/slots/container.js:25`,
and the sidebar wrapper uses that value as both its sticky-top offset
AND its in-grid top offset (`top-(--fd-docs-row-1)` plus
`h-[calc(var(--fd-docs-height)-var(--fd-docs-row-1))]`). That design
assumes the banner is sticky / fixed at the viewport top — so the
sidebar starts BELOW the banner.

shell-docs renders the banner in NORMAL body flow above BrandNav.
Banner pushing things down via flow is sufficient; the extra
`--fd-docs-row-1` offset double-counts the banner height. Result:
when the banner appears, BrandNav drops 54px (correct, flow) AND
the sidebar drops an ADDITIONAL 54px below BrandNav (wrong) —
visible as a yawning gap between BrandNav's bottom edge and the
sidebar's framework picker that didn't exist when the banner was
dismissed.

Pin `--fd-docs-row-1: 0px` on `#nd-docs-layout` for md+ so the
sidebar tracks BrandNav's bottom edge consistently, banner or no
banner. The mobile branch (where MobileTopNav is `position: fixed`)
keeps its existing `padding-top: var(--fd-nav-height)` so the docs
grid clears the fixed mobile nav — independent concern, untouched.

Verified at 1440×900 with banner present: BrandNav bottom = 142,
sidebar top = 166, gap = 24px (matches `main`'s `md:mt-6`). Without
banner: BrandNav bottom = 88, sidebar top = 112, gap = 24px. Same
gap in both cases.
2026-05-20 21:15:38 -07:00
Tyler Slaton 885f5cd036 Revert "feat(shell-docs): wire markdown_copied / open_in_llm_clicked + roomier BrandNav"
This reverts commit 4d67fe2691.
2026-05-20 21:12:28 -07:00
Tyler Slaton 00b0fc684d Revert "fix(shell-docs): give sidebar framework picker more headroom inside the card"
This reverts commit 070916302a.
2026-05-20 21:12:27 -07:00
Tyler Slaton 070916302a fix(shell-docs): give sidebar framework picker more headroom inside the card
The sidebar's first child (the SidebarBanner that hosts the framework
picker pill) had `padding: 1rem 1rem 0 1rem`. With the BrandNav and the
sidebar's outer `rounded-2xl border` chrome, 1rem (16px) at the top
read as scrunched against the card's rounded edge — the picker pill
has its own rounded corners and a border, so the gap to the parent
card edge needs to exceed the gap to the first nav link below it to
feel balanced.

Bump the top padding to 1.5rem (24px). Side and bottom padding stay
at 1rem and 0 respectively — the bottom-to-first-nav-link gap is
governed by the scroll viewport's 1rem top padding (defined further
down in this file), so the picker now sits with `24px-pill-16px` above
and below it instead of `16px-pill-16px`.
2026-05-20 21:09:29 -07:00
Tyler Slaton 4d67fe2691 feat(shell-docs): wire markdown_copied / open_in_llm_clicked + roomier BrandNav
Two follow-ups in one commit since they share the same shell-docs scope.

(1) Analytics events for the new docs-as-context surface (per Sam's
    P0 ask on PR #4946). The existing global `cli_command_copied`
    tracker in `lib/track-command-copy.ts` monkey-patches every
    `navigator.clipboard.writeText` call, so my `MarkdownCopyButton`
    was already being captured — but classified as `code` (the
    fallback when the text doesn't match an install command). That's
    not useful for the new "Copy Markdown" affordance.

    - `MarkdownCopyButton` fires `markdown_copied`
      `{ path: pathname, markdown_url: markdownUrl }` after a
      successful clipboard write. Coexists with the global capture;
      the dedicated event lets the analytics dashboard distinguish
      page-content copies from CLI copies.
    - Each `ViewOptionsPopover` item gains a `target` discriminator
      (`github`, `view-as-markdown`, `windsurf`, `claude-code`,
      `codex`, `chatgpt`, `claude`, `cursor`) and an `onClick` that
      captures `open_in_llm_clicked` `{ target, path }`. PostHog
      buffers locally so the new tab opens without waiting on the
      network.

(2) BrandNav was visually flush against the viewport top with
    minimal breathing room around its inner chrome. Bump the nav
    height (`h-[68px] xl:h-[88px]` → `h-[80px] xl:h-[104px]`) and
    matching interior padding so the content row (logo + tabs +
    Talk-to-engineer pill + search) sits comfortably-centered with
    a clear gap above. The taller nav also reads better when the
    rotating banner is visible — the banner / nav / sidebar stack
    now has clear vertical separation rather than feeling stacked.

    Update `--fd-nav-height` accordingly:
    - mobile (unchanged): 56px
    - md (768-1280px): 80px (was 88px hardcoded, which didn't match
      BrandNav's actual md height of 68px — a pre-existing mismatch)
    - xl+ (≥1280px): 104px (was 88px, now matches BrandNav's xl
      height)

    Update the `--fd-docs-height` calcs to track the same numbers
    (68 → 80, 88 → 104) so the sticky sidebar grid-area's height
    stays correct after the BrandNav grows.

Call-site enumeration:
- `MarkdownCopyButton` / `ViewOptionsPopover` — only callers are
  the MDX registry; existing analytics CTAs (try_for_free_clicked,
  talk_to_us_clicked, etc.) untouched.
- `--fd-nav-height` — read by `#nd-docs-layout`'s `pt-(...)` on
  mobile (load-bearing for MobileTopNav clearance) and by the
  Fumadocs sidebar's `top:` offset. Both branches verified visually
  with banner toggled at 1014px and 1440px viewports.
- `--fd-docs-height` — drives the sticky sidebar grid wrapper's
  height; the calc subtracts banner + nav + 2.25rem margin.
2026-05-20 21:05:29 -07:00
Tyler Slaton 1cdaae9369 chore: merge origin/main into tyler/jolly-liskov-74539a
Resolves merge conflict in `showcase/shell-docs/src/components/mobile-top-nav.tsx`:
- v16 of fumadocs moved `SidebarTrigger` from
  `components/layout/sidebar` to `components/sidebar/base` (this PR's
  upgrade). Keep the v16 path.
- `main` added Calendar / Lightbulb icons + `usePostHog` import for the
  expanded mobile CTAs (Get-Intelligence-free + Talk-to-Engineer pill).
  Keep those — they're referenced by the file body.

Combined resolution = main's import set with v16's import path for
SidebarTrigger. Other auto-merged files (brand-nav, snippet,
mdx-registry, etc.) merged cleanly; typecheck passes.
2026-05-20 20:47:21 -07:00
Tyler Slaton 23c0a8453c fix(shell-docs): theme-init handles 'system' value + snippet headers use language-correct comment syntax
CR Round 3 surfaced two more real bucket-(a) findings.

(1) `app/layout.tsx` theme-init script — When a user explicitly picks
    the "system" theme via the next-themes API, the persisted value in
    `localStorage.theme` is the literal string `"system"`, not absent.
    The previous inline script only fell back to `matchMedia(...)` when
    the value was unset (`!t`); for a system-mode user on a dark-
    preferring OS, the script would skip the matchMedia branch (because
    `t === "system"` is truthy), then skip the `.dark` class application
    (because `t !== "dark"`), and the page would paint in light before
    next-themes resolves post-hydration. The light-flash this script
    exists to prevent. Extend the fallback condition to `!t ||
    t === "system"` so the matchMedia path also handles the explicit-
    system case.

(2) `lib/llm-text.ts` Snippet file headers — `resolveSnippet`
    hardcoded `// <filename>` as the in-fence header regardless of the
    snippet's language. For Python regions this emits `//` (integer
    division — invalid syntax), for YAML / Bash / TOML it emits the
    wrong comment marker, for JSON it emits literal `//` (no comments
    allowed in spec JSON). An LLM ingesting `/llms-full.txt` sees what
    looks like real code from the file but with a broken first line.
    Add a `fileHeaderComment(language, text)` helper that picks the
    right comment shape:
    - `#` for Python / Bash / YAML / TOML / Ruby / R / Dockerfile / etc.
    - `/* ... */` for CSS / SCSS / Less
    - `<!-- ... -->` for HTML / XML / Markdown / MDX
    - `-- ` for SQL
    - empty (drop the header) for JSON / JSONC
    - `//` for C-family (TS / JS / Java / Go / Rust / C# / etc.) — the
      previous behavior, preserved as the default.
    Apply across all three snippet emission paths (region, file, file
    + lines). Pass the language through to the helper; when the helper
    returns empty (JSON case), skip the header line entirely so the
    fenced block contains only the code.

Call-site enumeration:
- theme-init inline script — no external callers; the next-themes
  ThemeProvider reads/writes localStorage on its own schedule, our
  script only seeds the `.dark` class pre-hydration. Behavior change
  is strictly additive (one extra matchMedia call when t === "system").
- `fenceFor` — unchanged signature.
- `fileHeaderComment` (new) — used only within `resolveSnippet`. Three
  call sites, all in the same function, all updated.
- `resolveSnippet` — three return paths updated; output shape change
  is invisible to all current callers (`renderPageToLlmText`,
  `inlineSnippets`) which treat the return value as opaque markdown.
2026-05-20 20:40:17 -07:00
Tyler Slaton aad4807213 fix(shell-docs): restore throw in MarkdownCopyButton so failed copies don't show ✓
CR Round 3 caught a real regression I introduced in commit `0186ae9f2`.

The Round 1 commit threw the caught error inside the `useCopyButton`
callback to keep the button in its idle state on failure. The comment
claimed Fumadocs's `useCopyButton` "respects throws" — that wording was
wrong, but the BEHAVIOR was right: `useCopyButton` runs
`Promise.resolve(callback()).then(() => setChecked(true))` with no
`.catch()`, so a rejected callback skips the `.then()` and the button
stays in its idle (Copy) state. Cost: one unhandled rejection in the
browser console per failure.

Round 2's regression-fix removed the throw to suppress that unhandled
rejection. Net effect: the callback now returns normally on failure,
the outer `.then()` fires, `setChecked(true)` flips the button to the
green checkmark — and the user sees a "Copied!" indicator on a copy
that actually failed. They paste stale clipboard content into Claude /
ChatGPT / Cursor and get garbage responses from the LLM.

Restore the throw and update the comment to accurately describe the
trade-off. Unhandled-rejection console noise is the lesser evil
compared to silently misleading the user. A follow-up PR (filed in
the bucket-d follow-up list) can introduce an explicit error UI state
(e.g. an alert icon for 2s) so failures are surfaced visibly without
relying on the console.

Also correct the unrelated comment on the JSX prop-spread order — it
claimed `className` "takes precedence" over caller-passed `className`,
but `className={cn(buttonVariants(...), props.className)}` MERGES the
caller's value via `cn`. `disabled` and `onClick` DO take precedence
(they're declared after `{...props}`); `className` is merged. Tighten
the comment to match.

Call-site enumeration:
- `MarkdownCopyButton` — used by `mdx-registry.tsx`. No caller passes
  `disabled` or `onClick` today; the prop-spread order change is
  purely defensive. No caller passes a `className` that would conflict
  with the merge; `cn` handles tailwind-merge precedence correctly.
- Browser unhandled-rejection behavior — verified that Fumadocs's
  `useCopyButton` (read at `node_modules/fumadocs-ui/dist/utils/use-copy-button.js`)
  does NOT attach a `.catch`, so the throw produces a single
  unhandled-rejection log per failed click; no infinite loop.
2026-05-20 20:37:00 -07:00
Tyler Slaton 64ffd19d8c fix(showcase): CR Round 2 cleanup — ADK reasoning graph name + dead CSS + comment + log tag
CR Round 2 confirmation surfaced one bucket (a) finding plus three
bucket (b) trivials worth rolling in together.

(a) `google-adk/src/app/demos/reasoning-{default,custom}/page.tsx`
    comments said "Both demos share the same backend (`reasoning_agent`
    graph)". That graph name is the langgraph-python convention —
    `reasoning_agent.py` in LGP — but the ADK demo doesn't have a
    graph by that name. `src/agents/registry.py:144-145` maps both
    `reasoning-custom` and `reasoning-default` to
    `AgentSpec(_thinking_chat)`, where `_thinking_chat` is built via
    `build_thinking_chat_agent`. Round 1 fixed the same class of bug
    in langgraph-typescript (which uses `agentic-chat-reasoning`) but
    missed ADK; this is the matching fix.

(b1) `.../headless-simple/chat.tsx` (3 files) emitted
    `console.error("[headless-simple] ...", err)` with no
    integration-slug prefix. A user testing demos across frameworks
    in the same browser session couldn't tell which integration's
    runAgent failed. Tag with the framework slug:
    `[google-adk:headless-simple]`, `[langgraph-python:headless-simple]`,
    `[langgraph-typescript:headless-simple]`.

(b2) `globals.css` lines 133-137 — the `.shell-docs-sidebar
    p[class*="sidebar-item-offset"] svg` rule (4×4 icons in accent
    purple) was dead in fumadocs v16. The v16 sidebar emits separator
    `<p>` elements with `inline-flex items-center gap-2` instead of
    the v15 `sidebar-item-offset` class fragment; the live rule on
    `p.inline-flex.gap-2 svg` (added earlier in this PR) already
    handles the same styling at the correct 16×16 size. Drop the
    dead rule.

(b3) `page-actions.tsx` — the regression-fix commit
    (`0186ae9f2`) wedged `getClientBaseUrl()` between the cache-
    describing block comment and the actual `cache = new Map(...)`
    declaration. The comment now sits above its own subject again;
    `getClientBaseUrl()` keeps its own JSDoc above its definition.

Call-site enumeration:
- ADK `_thinking_chat` reference — verified in
  `showcase/integrations/google-adk/src/agents/registry.py` (line
  144-145 + `build_thinking_chat_agent` import on line 23 + builder
  invocation on line 108). Comment-only change; no symbol signatures
  touched.
- Headless log tags — only the literal log string changes; no other
  call site reads it.
- `globals.css` dead rule — verified no other selector in the file
  depends on the removed lines (the section-header SVG color is set
  by the surviving `p.inline-flex.gap-2 svg` rule).
- `page-actions.tsx` comment move — no functional change.
2026-05-20 20:20:12 -07:00
Tyler Slaton 0186ae9f28 fix(shell-docs): unbreak preview build + harden related regressions
Three regressions from the earlier CR Round 1 fix batch + a related
miss the same round didn't catch.

1. `components/ai/page-actions.tsx` is `"use client"`; importing
   `getBaseUrl` from `@/lib/sitemap-helpers` pulled `fs` / `path` /
   `gray-matter` into the client bundle and broke the build entirely
   ("Module not found: Can't resolve 'fs'"). The whole point of
   `getBaseUrl` is the 2-line env-var read + trailing-slash strip — no
   filesystem work — so inline a `getClientBaseUrl()` helper here with a
   pointer to the canonical server-side version. `sitemap-helpers.ts`
   stays untouched so other server-side callers keep their convenience.

2. The same file re-threw caught errors from `fetchMarkdown` /
   `clipboard.writeText` on the assumption that Fumadocs's
   `useCopyButton` would treat the rejection as "don't flip the
   `checked` state". It doesn't — there's no `.catch()` on the
   internal promise (verified in
   `fumadocs-ui/dist/utils/use-copy-button.js`), so the throw produced
   an unhandled rejection (browser console noise + Sentry spam) AND
   gave the user no visible failure indicator either way. Log and
   swallow at this layer; a follow-up PR can introduce an explicit
   error UI if we want "Copy failed" to surface.

3. `.claude/launch.json` routed `shell` to port 3004 by passing
   `-- --port 3004` to `npm --prefix showcase/shell run dev`. But
   shell's `dev` script ends with `npx -y concurrently -k -n
   bundle,next "tsx ... --watch" "next dev"` — the trailing
   `--port 3004` was parsed by `concurrently`, not `next dev`, so
   `next dev` still bound 3000 and the original collision with `docs`
   persisted. Switch to `bash -c "PORT=3004 npm --prefix showcase/shell
   run dev"` so the env var passes through `concurrently` into
   `next dev` (which natively reads PORT).

Call-site enumeration:
- `getClientBaseUrl` (new) — only used inside the same file. No
  external callers to update.
- `getBaseUrl` (untouched in `@/lib/sitemap-helpers`) — server-side
  callers (sitemap routes, `llms-full.txt` route, `llms.txt` route)
  unchanged; verified via grep that no `"use client"` file imports it.
- `MarkdownCopyButton` — error now logged once via `console.error`
  and swallowed; the button stays in its idle state.
- `.claude/launch.json` `shell` entry — `runtimeExecutable` flipped
  from `npm` to `bash`; harness reads these as opaque strings.
2026-05-20 20:12:03 -07:00
Tyler Slaton 04c6383990 fix(shell-docs): resolve launch.json port collision + harden preview script + untrack next-env.d.ts
`.claude/launch.json` declared port 3000 for both \`docs\` (Next.js at
docs/) and \`shell\` (Next.js at showcase/shell/) — only one could
actually start at a time, and Next's auto-port-fallback would land
\`shell\` on whatever was free without the launch config knowing.
Reassign \`shell\` to port 3004 (next free slot after the existing
3001/2/3 cluster) and pass \`-- --port 3004\` through \`npm run dev\`
so the runtime port matches the declared port.

\`.claude/preview/shell-docs.sh\` had a blanket
\`|| { echo "(may have failed — expected)" }\` after \`pnpm install\` that
swallowed every install failure, not just the documented \`lefthook\`
prepare-hook one. A real failure (network down, lockfile drift) would
get silently absorbed and then explode much later at the \`npx tsx\`
generator step with a confusing \`Cannot find module\` error. Verify
\`$SCRIPTS_DIR/node_modules\` exists after the install attempt; bail
with a clear instruction if it doesn't.

\`showcase/shell-docs/next-env.d.ts\` is a Next.js-auto-generated file
whose contents differ between \`next dev\` (\`./.next/dev/types/...\`)
and \`next build\` (\`./.next/types/...\`). Per Next.js's own
recommendation it should never be checked in — the v16 path change
would otherwise produce dirty trees on every build/dev switch, and a
clean checkout's typecheck would fail because the imported
\`.next/dev/types/routes.d.ts\` is itself gitignored. Add the file to
\`.gitignore\` (matching the existing \`docs/next-env.d.ts\` entry) and
\`git rm --cached\` to untrack the committed copy. Next regenerates it
on first \`next dev\`/\`next build\`.

Call-site enumeration:
- \`.claude/launch.json\` — no callers within the repo; the
  \`/run\` slash command reads it as data. Port change is non-breaking
  for any other tooling that doesn't bind to 3000 for \`shell\`.
- \`.claude/preview/shell-docs.sh\` — the lefthook installer is the
  only thing that runs it (besides interactive users); both flows
  benefit from the loud failure.
- \`next-env.d.ts\` — no source file imports from it; the file is a
  TypeScript \`/// <reference\` declaration consumed by tsc only,
  regenerated on each build/dev.
2026-05-20 20:01:16 -07:00
Tyler Slaton 086e68c88b fix(showcase): log runAgent errors in headless-simple; correct LGT reasoning graph name
The Headless Simple demo's `chat.tsx` swallowed every `runAgent`
rejection with an empty arrow catch:

    void copilotkit.runAgent({ agent }).catch(() => {});

This is the canonical "two hooks, your design system" example users
copy-paste as a starting point — silent swallow modeled broken practice
to every CopilotKit user, and the @region[use-agent-simple] block we
inline into `/<framework>/headless` docs surfaces the anti-pattern as
the recommended snippet. Replace the empty catch with a
`console.error("[headless-simple] runAgent failed", err)` so network
failures, transport disconnects, and runtime errors surface in the
developer's console. Applied across google-adk, langgraph-python, and
langgraph-typescript variants.

`langgraph-typescript/src/app/demos/reasoning-default/page.tsx` had a
comment claiming the demo backed onto the `reasoning_agent` graph, but
the LGT route map in `src/app/api/copilotkit/route.ts` actually points
both `reasoning-default` and `reasoning-custom` at the
`agentic-chat-reasoning` graph (the companion `reasoning-custom/page.tsx`
comment already gets this right). The `reasoning_agent` label is the
Python / ADK convention. Update the comment to match the TS route map.

Call-site enumeration:
- `copilotkit.runAgent` (in headless-simple/chat.tsx, 3 files) — the
  return value is `Promise<void>`; existing callers don't await it, so
  swapping the catch is non-breaking. The previous `void` operator
  already discarded the promise value, so the runtime behavior of the
  surrounding `send()` is unchanged.
- LGT `reasoning-default` page.tsx — comment-only change, no symbol
  signatures touched.
2026-05-20 19:59:23 -07:00
Tyler Slaton 3700b885b0 fix(shell-docs): guard SidebarFolderStatePreserver synthetic clicks + fix PopoverClose export
`SidebarFolderStatePreserver` had two silent `catch {}` blocks (read /
write of the saved state map) — log via `console.warn` so a user whose
folders keep resetting can diagnose the underlying storage failure
(SecurityError on third-party iframes / privacy mode, QuotaExceeded,
corrupted JSON).

The restore-on-mount effect called `trigger.click()` to flip Radix's
state to the saved value. That synthetic click bubbles to the
delegated `#nd-sidebar` click handler, which then records the new
state — but if Radix's `data-state` hadn't updated by the next
`requestAnimationFrame` (transient animation, mount race), the
recorded value could overwrite the user's saved preference with the
live value the restore just tried to flip. Add a module-level
`WeakSet<HTMLButtonElement>` of in-progress synthetic clicks; the
delegated handler skips entries in the set. The flag is cleared on the
next rAF, by which point any genuine user click will fire against an
unmarked trigger.

`popover.tsx` exported `PopoverClose = PopoverPrimitive.PopoverClose`,
but Radix UI's actual export is `PopoverPrimitive.Close`. The
expression resolved to `undefined`, so any caller rendering
`<PopoverClose />` would have thrown React's "Element type is invalid:
expected a string ... but got undefined" error. The shadcn-style
scaffold the Fumadocs CLI generated had the symbol name wrong; fix the
re-export to `PopoverPrimitive.Close`.

Call-site enumeration:
- `SidebarFolderStatePreserver` — used only by `ShellDocsLayout`.
  Behavior change is purely additive (logs on previously-silent
  errors; suppresses synthetic clicks the previous code already
  intended to be no-ops).
- `PopoverClose` — confirmed via grep that no caller exists yet; this
  is a defensive fix to a fresh scaffold.
- `Popover`, `PopoverTrigger`, `PopoverContent` — unchanged.
2026-05-20 19:57:26 -07:00
Tyler Slaton 9417b2e74f fix(shell-docs): emit framework root URLs in llms.txt; log silent reads; drop dead CONTENT_DIR fallback
`getAllLlmPages` was silently dropping framework root pages from
`/llms.txt`. `walkMdx` strips trailing `/index` from yielded slugs, so
`integrations/<folder>/index.mdx` arrived as `slug === ""` — and the
`if (!slug) continue` guard in section 2 then skipped it. Result: LLM
crawlers walking `/llms.txt` never saw `/langgraph-python`,
`/built-in-agent`, etc., so the framework landing pages were invisible
to the LLM index even though they're the canonical entry points.

Treat empty slug as the framework root and emit it as the bare
integration URL (mirroring how sections 3 and 4 already handle
reference/ag-ui index pages). The `loadSlug` falls back to
`integrations/<folder>/index` so `loadDoc()` can still resolve the
source.

Two silent `catch {}` blocks in the same file were dropping read /
parse errors with no diagnostic — `readMetaFromFile` would mask
malformed YAML (page appears bare in `/llms.txt` with no signal to the
author) and `readSource` would mask filesystem errors (the body gets
quietly dropped by the route handler's `if (!body) continue` guard).
Add `console.error` with a `[llm-text]` prefix in both, matching the
pattern `readTitle` in `docs-render.tsx` already uses.

`findExistingMdx` in `llms-mdx/[[...slug]]/route.ts` had a `void
CONTENT_DIR;` dead statement with a comment promising a `CONTENT_DIR`
fallback that didn't exist — the `import { CONTENT_DIR }` was only
kept alive by that no-op. Remove the dead statement, drop the unused
import, and log when the path-traversal guard rejects a candidate (the
previous silent `continue` meant a typo'd slug just 404'd with no log
to correlate the request).

`llms.txt/route.ts` had a comment claiming "no explicit revalidate
directive — Next defaults to dynamic" directly above
`export const revalidate = false`. The directive is intentional (cache
the slow filesystem walk indefinitely on the server; let the
per-response Cache-Control header drive CDN/client freshness) — update
both `llms.txt` and `llms-full.txt` comments to explain the two-cache
layering instead of contradicting it.

Call-site enumeration:
- `getAllLlmPages` — used by `/llms.txt`, `/llms-full.txt`, and
  `/llms-mdx` route handlers. No caller depends on whether framework
  roots are present; widening the result set is purely additive.
- `readMetaFromFile`, `readSource` — file-local helpers; only
  `getAllLlmPages` / `renderPageToLlmText` call them. New `console.error`
  is additive.
- `CONTENT_DIR` (removed import) — confirmed via grep that
  `/llms-mdx/[[...slug]]/route.ts` no longer references the symbol;
  `loadDoc` is still imported from the same module.
2026-05-20 19:55:58 -07:00