Two demos that showcase how a platform team integrates CopilotKit:
auth gating and runtime config injection.
Auth — defaults UNAUTHENTICATED. First paint is a centered shadcn Card
with the demo token visible (`demo-token-123`) and a "Sign in" button.
<CopilotKit> doesn't mount until the user signs in; clicking the button
stores the token in localStorage and triggers a re-render that mounts
the chat with `Authorization: Bearer <token>` header attached. Reload
preserves the token; sign-out clears it and returns to the card.
Earlier drafts of this demo defaulted to authenticated with a
ChatErrorBoundary + onError-driven banner to recover from <CopilotChat>
's 401-on-mount crash. Both have been removed — gating the chat behind
`isAuthenticated` means it never mounts with bad creds, so the boundary
has no purpose.
Backend route uses `createCopilotRuntimeHandler` from
@copilotkit/runtime/v2 directly because the Next.js adapter does not
forward `hooks`. The `onRequest` hook validates the bearer token and
throws a Response(401) on missing/wrong tokens.
Agent Config — typed knobs (tone / expertise / responseLength) that
change the agent's behavior per turn. Pivoted to `useAgentContext` from
the original `<CopilotKit properties={...}>` transport, which silently
dropped the values in @ag-ui/langgraph 0.0.31 — those payloads landed
at the top level of the LangGraph stream and weren't routed into
RunnableConfig["configurable"]. A prior workaround that repacked them
there triggered LangGraph 0.6's "cannot specify both configurable and
context" 400.
useAgentContext is the supported LangGraph 0.6+ path for "frontend →
agent runtime context." A small ConfigContextRelay component sits inside
the provider and publishes the live toggles. The Python graph collapses
to a single static system prompt with three rulebooks; CopilotKitMiddleware
injects the context entry into the model's prompt automatically.
Four state-flow demos plus the multi-agent demo, all sharing the
page-as-entry-point convention with extracted suggestions.
- shared-state-streaming (Shared State: Streaming) —
StateStreamingMiddleware(state_key="document", tool="write_document",
tool_argument="document"). The argument name MUST match the state_key
for the partial-JSON streamer to index correctly. Fixed in this pass
(previous version had a name mismatch). Frontend renders `LIVE` badge
+ char counter so per-token streaming is visible.
- shared-state-read-write (Shared State: Read + Write) — bidirectional.
UI writes preferences via `agent.setState`; agent writes notes via a
`set_notes` tool that returns Command(update={...}). PreferencesInjector
middleware reads the state on every turn and injects it as a system
message. CopilotPopup layout, 2-col card UI, "Agent Scratch pad"
copy.
- shared-state-read (deprecated route stub) — kept for back-compat with
any external links; the canonical demo is shared-state-read-write.
- readonly-state-agent-context (Frontend Context Sharing) — frontend
publishes read-only context via useAgentContext (the LangGraph 0.6+
idiom). Backend has tools=[] and only CopilotKitMiddleware; read-only
is enforced by the absence of any state-write tool, not a flag.
- subagents (Sub-Agents) — supervisor + research / writer / critic
sub-agents. Per-tool useRenderTool registrations surface delegation
events to the chat. State has a delegations[] log; the Python side
only writes status="completed" so the type was tightened (dropped
unused "running" / "failed" legs from both Python TypedDict and the
TypeScript shape). Frontend infers active sub-agent from in-flight
tool calls via a defensive structural probe over agent.messages.
A single commit for the "agent-authored UI" cluster — five distinct
strategies, all sharing a common shape (declare a catalog, let the
agent pick + populate components):
- declarative-gen-ui (Declarative UI: A2UI) — A2UI dynamic schema. The
agent calls `generate_a2ui` (not the runtime's auto-injected
`render_a2ui`) which secondary-binds an internal render tool with
forced tool_choice, then returns operations via `a2ui.render(...)`.
Custom catalog (Card / StatusBadge / Metric / InfoRow / PrimaryButton
/ PieChart / BarChart) wired via `a2ui.catalog` on the provider.
- a2ui-fixed-schema (Declarative UI: A2UI Fixed Schema) — fixed
server-side schema. The "Book flight" button is an inert label; the
earlier draft tried a schema swap to a booked-confirmation but the
SDK doesn't yet expose `action_handlers` from Python. Removed
BOOKED_SCHEMA + booked_schema.json since they were dead weight.
Cleaned 8 (props as Record<string, any>) casts down to a single
shared `s()` helper.
- mcp-apps — MCP server-driven UI via activity renderers. The runtime's
`mcpApps.servers` config wires Excalidraw; agent has tools=[] and
the middleware emits activity events that the built-in
MCPAppsActivityRenderer auto-mounts as a sandboxed iframe.
- byoc-hashbrown (Declarative UI: Hashbrown) — streaming structured
output via @hashbrownai/react. Agent prompt locks output to JSON via
`response_format: json_object` with a `{ ui: [{ tag: { props } }] }`
contract. Custom slot override on messageView.assistantMessage parses
the streaming JSON.
- byoc-json-render (Declarative UI: json-render) — streaming hierarchical
JSON UI spec via @json-render/react with a Zod-validated catalog.
Catalog (defineCatalog) + registry (defineRegistry) split keeps the
schema as the single source of truth.
- open-gen-ui (Open Generative UI: Default) — runtime's
`openGenerativeUI` config injects a sandboxed UI tool; design-skill
override steers the agent toward educational visualizations. Built-in
OpenGenerativeUIActivityRenderer auto-mounts.
- open-gen-ui-advanced (Open Generative UI: Advanced) — adds frontend
sandboxFunctions registered on the provider; each Zod-typed handler
is exposed to the iframe via the host bridge. Suggestion titles read
as normal user prompts (no iframe-restriction LLM-prompt copy bleed).
Backends sit at src/agents/{a2ui_fixed,byoc_hashbrown_agent,
byoc_json_render_agent}.py and the MCP runtime at
src/app/api/copilotkit-byoc-hashbrown/route.ts.
Five demos exercising the per-tool / catch-all / agent-state rendering
patterns. The three tool-rendering cells share the tool_rendering_agent
graph; they differ only in how the frontend renders the same tool
calls.
- tool-rendering (Tool Rendering - Specific) — per-tool useRenderTool
for get_weather + search_flights, plus a useDefaultRenderTool wildcard
for everything else.
- tool-rendering-default-catchall (Tool Rendering - Default) — single
shadcn-styled wildcard via useDefaultRenderTool. Without registering
*some* renderer the runtime has no `*` entry and tool calls render
invisibly; this demo shows the minimum-viable shape.
- tool-rendering-custom-catchall (Tool Rendering - Custom Default) —
same single-wildcard shape, branded with a custom card.
Backend system prompt (src/agents/tool_rendering_agent.py) defaults to
ONE tool per user question. Chaining is opt-in via a "Chain tools"
suggestion that triggers an explicit-ask exception in the prompt — the
previous default-on-chaining generated extra unsolicited tool-call
cards on every turn.
- gen-ui-tool-based (Generative UI: useComponent) — useComponent for
render_bar_chart + render_pie_chart with Zod schemas; backend has
tools=[] and the runtime injects the tools.
- gen-ui-agent (Generative UI: Agent State) — agent-state-driven step
list. The Python graph plans steps via a `set_steps` tool that
returns Command(update={"steps": …}); the frontend reads via
useAgent({updates: [OnStateChanged]}) + a custom MessageList that
renders steps inside CopilotChat's messageView.children slot.
Removed: src/app/demos/{tool-rendering,gen-ui-agent}/agent.py — TODO
stubs; real graphs live in src/agents/.
Frontend Tools (in-app + async) demonstrate the spectrum of agent →
client tool calls:
- frontend-tools — useFrontendTool with a synchronous handler that
mutates page state. The agent calls `change_background` with any CSS
background value and the canvas re-paints. <CopilotSidebar /> layout;
default background is solid indigo so the canvas reads as a clean
start.
- frontend-tools-async — useFrontendTool with an async handler. The
agent calls `query_notes`, the handler awaits a 500ms simulated DB
query, and the agent uses the returned notes in its reply. Pure
frontend tool — backend has tools=[].
HITL family — three patterns for human-in-the-loop, all keyed on the
useHumanInTheLoop / useInterrupt primitives:
- hitl — step-feedback variant rendering inside the chat via
useHumanInTheLoop + useInterrupt (the v2 replacement for
useLangGraphInterrupt, which is not exported in v2).
- hitl-in-chat — time-slot picker variant. Backend graph (hitl_in_chat)
calls `interrupt({slots, …})` and resumes when the user picks. This
is the canonical "in-chat HITL" demo; the manifest entry points here.
- hitl-in-app — async useFrontendTool with an app-LEVEL approval modal
(rendered via createPortal OUTSIDE the chat). The completion callback
resolves the pending tool Promise with the user's decision. This is
HITL where the human surface is your app's UI, not the chat.
- gen-ui-interrupt — the lower-level useInterrupt primitive. Backend
(src/agents/interrupt_agent.py) has a real `schedule_meeting` tool
that emits an interrupt with topic / attendee / slots, and the
frontend renders an inline TimePickerCard.
Three "production-feel" chat demos that exercise the same convention
(page.tsx as entry point + suggestions extracted) and add their own
specialized hooks:
- multimodal — image + PDF uploads via CopilotChat attachments. Includes
a LegacyConverterShim (until @ag-ui/langgraph ships an updated
converter), magic-byte + LFS-pointer guard for safe content sniffing,
and sample-attachment-buttons that inject test images via DataTransfer
+ dispatch `change` event so screenshots / Playwright reproduce.
- voice — speech-to-text via @copilotkit/voice. Uses the V2 runtime
directly with [[...slug]] catch-all because `transcriptionService` is
V2-only. A guarded sample-audio-button injects deterministic sample
text via the textarea's native value setter (CopilotChat has no
controlled-input prop today). useSingleEndpoint={false} opts into the
V2 multi-endpoint protocol.
- beautiful-chat — flagship polished starter chat with brand fonts,
theme tokens, suggestion pills, generative-UI charts, and
enableAppMode / enableChatMode tools. Backend
(src/agents/beautiful_chat.py) wires query_data, manage_todos,
search_flights, and an A2UI dynamic generator (generate_a2ui) that
hits a secondary LLM for schema design. Model: gpt-5-mini.
The convention pass extracted suggestions into separate files for each
demo and slimmed page.tsx down to imports + provider + render.
Three demos showing the prebuilt component formats. All three share the
neutral assistant graph and follow the page-as-entry-point convention:
each page.tsx slimmed to imports + provider + the suggestions hook
mount + JSX, with `useConfigureSuggestions` extracted to its own file.
- agentic-chat (Prebuilt: CopilotChat) — full-page CopilotChat. Plain
text suggestions (joke / fun fact / limerick) — earlier drafts had a
"Weather in Paris" prompt but the agent has no weather tool; trimmed
to non-tool prompts so suggestions actually work.
- prebuilt-sidebar (Pre-Built: Sidebar) — <CopilotSidebar /> docked to
the edge of the viewport. Page content centered in mx-auto max-w-2xl
column with icon + heading + paragraph. The sidebar genuinely PUSHES
the page now — see the body width fix in src/app/globals.css from the
scaffolding commit.
- prebuilt-popup (Pre-Built: Popup) — <CopilotPopup /> with a floating
launcher. Same centered content shape as the sidebar demo.
Removed: src/app/demos/agentic-chat/agent.py — TODO stub with a
misleading docstring; the real graph is at src/agents/agentic_chat.py.
A pair of demos that exercise the same backend reasoning graph but
differ only in whether the frontend overrides the
`messageView.reasoningMessage` slot.
Backend (src/agents/reasoning_agent.py): uses a reasoning-capable OpenAI
model (gpt-5-mini by default, override via OPENAI_REASONING_MODEL) routed
through the Responses API so the model's chain-of-thought streams as
AG-UI REASONING_MESSAGE_* events with `role: "reasoning"`. The prompt
asks for a concrete physics answer, which reliably triggers reasoning;
meta-prompts like "show your reasoning step by step" produce no
reasoning summary because the model recognizes those as a request to
reveal chain-of-thought (which it refuses).
Frontend:
- reasoning-default/ — no slot override; built-in
CopilotChatReasoningMessage renders the "Thinking… / Thought for X"
header with an expandable content region.
- reasoning-custom/ — overrides `messageView.reasoningMessage` with a
ReasoningBlock (amber banner with `data-testid="reasoning-block"`).
The label flips from "Thinking…" while streaming to "Agent reasoning"
once the stream settles.
Suggestions live in their own files (per the page-as-entry-point
convention). Both demos share `agent="reasoning-default"` /
`agent="reasoning-custom"` against the same `reasoning_agent` graph,
registered in api/copilotkit/route.ts.
Removed:
- src/app/demos/agentic-chat-reasoning/ — replaced by reasoning-custom/
for naming clarity.
- src/app/demos/reasoning-default-render/ — earlier draft of the Default
demo with a slightly different page name.
- tests/e2e/agentic-chat-reasoning.spec.ts — replaced by
reasoning-custom.spec.ts.
Two paired demos showing the spectrum of "change the look" without
rewriting components:
CSS theming (chat-customization-css/) — HALCYON, a warm-paper editorial
brand. Two layers do the work:
1. v2 token overrides on `[data-copilotkit]` recolor every Tailwind
utility (cpk:bg-muted, cpk:text-foreground, …) the runtime renders.
2. Class-targeted styling on .copilotKitChat, .copilotKitMessage*,
.copilotKitInput, suggestions, scrollbar, welcome screen for the
editorial details that CSS variables alone can't express.
Aesthetic: cream parchment surface, sharp 90° corners, copper-ember
accents, italic display serif (Instrument Serif) + Fraunces body +
JetBrains Mono dispatch, paper-grain noise via inline SVG, mono masthead
pinned under the top edge. All selectors namespaced under
`.chat-css-demo-scope` — no leakage.
Slot atlas (chat-slots/) — every overrideable slot wrapped in a
dashed-outline marker. Markers nest correctly: each shows ONLY its own
label on hover via `:has(.slot-marker:hover)` rather than lighting up
every nested label as the cursor enters the outermost one. Each label is
a click-to-copy button (✓ flash on success) that copies the slot's
PascalCase component path (`Input.TextArea`,
`MessageView.AssistantMessage`, …) so a developer can paste straight
into IDE search. SuggestionPill, ScrollToBottomButton, and Feather slots
are all wired. CustomFeather has its own `FeatherCopyLabel` because the
default Feather uses position:absolute and can't share SlotMarker.
Both demos use the neutral assistant graph (chat-customization-css and
chat-slots are entries in the route.ts neutralAssistantCells list).
Full headless surface — a hand-rolled CopilotChat replacement that wires
every render hook on top of shadcn/ui primitives. Visual chrome matches
Headless: Simple so the two read as a paired sibling demo.
Architecture is progressive-disclosure: the entry file is a 30-line
HeadlessCompleteRoot that enumerates capabilities, each registered via
a focused hook module:
page.tsx
hooks/
use-tool-renderers.tsx — useRenderTool x3, useDefaultRenderTool
use-frontend-components.ts — useComponent (highlight_note)
use-headless-suggestions.ts — useConfigureSuggestions
chat/
chat.tsx, header.tsx, empty-state.tsx, composer.tsx,
suggestion-bar.tsx, message-list.tsx, message-user.tsx,
message-assistant.tsx, message-activity.tsx, typing-indicator.tsx
attachments/use-attachments-config.ts + attachment-preview.tsx
tools/weather-card.tsx, stock-card.tsx, chart-card.tsx,
generic-tool-card.tsx, highlight-note.tsx
Backend (src/agents/headless_complete.py): get_weather, get_stock_price,
get_revenue_chart tools; the chart tool replaces the previous Excalidraw
"Sketch a diagram" suggestion (MCP capability stays wired).
Bug fixes folded in:
- Tool-call cards stuck "running" forever — message-list.tsx indexes
role:"tool" messages by toolCallId and passes the matching ToolMessage
to renderToolCall so cards advance to "complete".
- Empty state was hugging the top of the viewport — Radix ScrollArea
wraps content in a `display: table` div that breaks h-full propagation.
Empty state now renders OUTSIDE the ScrollArea.
- SuggestionBar duplicated the empty-state prompts on first paint.
Hidden until the conversation starts.
Minimum-viable headless chat that wires only `useAgent` + `useCopilotKit`,
dressed in shadcn/ui primitives. Five small single-purpose files so a
reader can grok the surface in under a minute:
page.tsx — provider + <Chat />, ~10 lines
chat.tsx — useAgent + useCopilotKit + send loop
composer.tsx — Textarea + send button
empty-state.tsx — sparkles + sample prompts
message-bubble.tsx + typing-indicator.tsx — render pieces
Wires runtimeUrl="/api/copilotkit" + agent="headless-simple" against
the neutral assistant graph registered in route.ts.
Also adds the v2 catch-all route at copilotkit-mcp-apps/[[...slug]]/route.ts
(used by Headless: Complete in the next commit). v2 hooks POST to subpaths
like /v2/agent/run; the previous flat route 404'd, leaving headless demos
stuck on "Thinking…".
Foundational layer that the per-demo work in subsequent commits builds on.
- Manifest-driven landing page (src/app/page.tsx) — auto-generated grid
of demo cards from manifest.yaml, grouped by tag with explicit ordering
for chat-ui / interactivity / generative-ui / agent-state / multi-agent
/ headless / platform.
- Per-demo titles (src/middleware.ts + src/app/demos/layout.tsx).
- Diagnostic console gated to NODE_ENV=production in app/layout.tsx so
deployed showcases surface uncaught errors and iframe context.
- src/app/globals.css — Tailwind v4 @theme inline block that maps
showcase CSS variables into Tailwind theme tokens (without it, shadcn
utilities like bg-muted / text-foreground compile to nothing). body
intentionally NOT given width:100% so <CopilotSidebar /> can shrink
the document via marginInlineEnd.
- shadcn primitives under src/components/ui/ + src/lib/utils.ts.
- tsconfig.json — @/* path alias rooted at src/.
Removed:
- src/app/copilotkit-overrides.css (global override layer, superseded
by per-demo theming)
- src/app/api/copilotkit-mcp-apps/route.ts (replaced with
[[...slug]]/route.ts so v2 subpath POSTs like /v2/agent/run resolve)
Update scripts/docs to write directly into showcase/shell-docs/src/content/reference/
(consumed by /reference/[...slug]) instead of the legacy docs/content/docs tree.
Adds repo-root cwd anchoring, mkdir -p on the destination, allSettled so a single
missing source doesn't abort the run; generates 25 MDX pages (20 V1 + 5 SDK) and
shell-docs build passes.
Switch CLI command to `npx copilotkit@latest create --framework <id>` on all 8
framework quickstarts. Bypasses the unfiltered 18-framework picker and skips
the EIP prompt (which is mutually exclusive with --framework and today
scaffolds langgraph-python-threads regardless of which framework page the
user came from).
For LangGraph: keep an EIP callout since EIP=Yes does scaffold a LangGraph-
Python project today; note that threads support for other frameworks is
coming. For Microsoft Agent Framework and LangGraph (multi-variant): show
both --framework <variant> commands.
Apply the canonical V2 form:
- \`<CopilotKit>\` from \`@copilotkit/react-core\` (root, not /v2).
- styles from \`@copilotkit/react-core/styles.css\` (not react-ui/v2).
- drop \`@copilotkit/react-ui\` from npm install commands.
Add sample-audio-button region markers around the SampleAudioButton
component in the built-in-agent voice demo so the
<Snippet cell="voice" region="sample-audio-button" /> reference in
shell-docs/voice.mdx resolves to real code instead of a missing-snippet
warning.
Comment-only change; no runtime behavior change. Mirrors the canonical
in-place region pattern already used by langgraph-typescript, mastra,
ms-agent-python, claude-sdk-typescript, llamaindex, spring-ai,
pydantic-ai, claude-sdk-python, and crewai-crews per the Mastra Round
Pattern Decisions playbook.
## Summary
- The dynamic shields.io license badge in the top-of-README badge row
sometimes renders as **invalid** on github.com (I suspect some caching
or something but not gonna spend the time digging in).
- Downloaded the rendered SVG once and vendored it at
`assets/license-badge.svg`
## Summary
Three small pre-cutover cleanup fixes batched into one PR (consolidation
of the prior #4680, #4681, #4682 — same content, fewer review queues).
- `b3ec38720` — Add `<WhenFrameworkHas absent>` fallback to
`programmatic-control.mdx` for the 7 frameworks without
`interrupt_pattern` (ag2, agno, built-in-agent, crewai-crews,
google-adk, mastra, spring-ai), mirroring the canonical pattern from PR
#4496.
- `c080ca062` — Retarget `/migrate/1.10.X` redirect destination from
`/migrate` (which 404s) to `/migrate/v2`. `permanent: false` preserved.
- `e61e8c36d` — Add `mcp-server-setup.mdx` exclusion to docs sync
script. Shell-docs version is intentionally ahead of upstream (HTTP/SSE
Tabs + `mcp-remote` + Tadata callout); without exclusion the next sync
would clobber it.
Supersedes #4680, #4681, #4682.
## Test plan
- [ ] `nx run shell-docs:dev` and visit `/programmatic-control` while
switching the framework selector to non-native fws — verify fallback
callout renders and links to `/human-in-the-loop`.
- [ ] Visit `/migrate/1.10.X` — should redirect to `/migrate/v2` and
render the V2 migration page.
- [ ] Inspect `showcase/scripts/sync-docs-from-main.ts` PATH_EXCLUSIONS
for the `mcp-server-setup` regex.
## Summary
- Extract `CopilotKitContext` and `useCopilotKit` into standalone
`context.ts` in react-core, enabling cross-platform reuse without web
dependencies
- Add new `@copilotkit/react-native` package with lightweight provider,
polyfills, and streaming fetch
- All hooks (`useAgent`, `useFrontendTool`, `useHumanInTheLoop`, etc.)
are re-exported directly from react-core — no reimplementation
## Motivation
CopilotKit's React hooks are platform-agnostic, but the barrel import in
`@copilotkit/react-core` pulls in web-only dependencies (Radix UI, Lit,
A2UI renderer, react-dom, CSS). This makes the package unusable in React
Native without extensive Metro shimming.
By extracting the React context into a standalone entry point
(`@copilotkit/react-core/v2/context`), the new
`@copilotkit/react-native` package can provide its own lightweight
provider while reusing all existing hooks.
## What's in `@copilotkit/react-native`
| Export | Description |
|--------|-------------|
| `CopilotKitProvider` | Lightweight provider — no DOM, CSS, Radix, Lit,
or A2UI deps |
| `installStreamingFetch()` | XHR-based streaming fetch for
`response.body.getReader()` support |
| `@copilotkit/react-native/polyfills` | All polyfills at once
(ReadableStream, TextEncoder, crypto, DOMException, window.location) |
| `@copilotkit/react-native/polyfills/*` | Granular per-polyfill imports
(`/streams`, `/encoding`, `/crypto`, `/dom`, `/location`) for users who
need to avoid overriding their own shims |
| `useAgent`, `useFrontendTool`, etc. | Re-exported from react-core
(shared context) |
## Usage
```tsx
// index.js (entry point, before other imports)
import "@copilotkit/react-native/polyfills";
import { installStreamingFetch } from "@copilotkit/react-native";
installStreamingFetch();
// App.tsx
import { CopilotKitProvider, useAgent, useCopilotKit } from "@copilotkit/react-native";
function App() {
return (
<CopilotKitProvider runtimeUrl="https://your-server/api/copilotkit">
<ChatScreen />
</CopilotKitProvider>
);
}
```
## Test plan
- [x] `nx run react-core:build` passes
- [x] `nx run @copilotkit/react-native:build` passes
- [x] `nx run react-core:test` — all 1153 tests pass
- [x] Manual test in React Native app (tested during development with
bare RN 0.84 project)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Add to monorepo scope in release.config.json. Set version to 1.56.5,
correct ESM extensions in exports map, add check-types/publint/attw
scripts, align tsdown to ^0.20.3. Add react-native example glob to
pnpm-workspace.yaml. Regenerate lockfile preserving zod@3 and
langchain dependency versions.
## Summary
- Fix oxfmt formatting violation in
`deep-agents-finance-erp/use-request-approval.tsx` (failing `static /
quality`)
- Add missing `lucide-react` dependency to
`showcase/shell-docs/package.json` (failing `Showcase: Build & Push` for
shell-docs)
- Regenerate `langgraph-typescript` lockfile after
`@langchain/langgraph` bump to 1.3.0 (failing `Showcase: Build & Push`
for langgraph-typescript)
- Guard all Slack notification steps in `showcase_docs-sync`,
`test_smoke-starter`, and `showcase_qa-sync` workflows with
`env.SLACK_WEBHOOK != ''` so they skip gracefully when
`SLACK_WEBHOOK_OSS_ALERTS` is not set (failing `Showcase: Docs Sync` and
smoke tests)
## Test plan
- [x] `oxfmt --check` passes locally on the formatted file
- [x] `lucide-react` resolves via `npm ls` in shell-docs
- [x] `npm ci --dry-run` passes for langgraph-typescript with
regenerated lockfile
- [x] Pre-commit hooks (lint, test, publint, attw) pass locally
- [x] CI green on prior push (all 24 checks passed)
Extract CopilotKitContext, useCopilotKit, and LicenseContext into
src/v2/context.ts. Add src/v2/headless.ts barrel export for
platform-agnostic hooks. Add v2/context and v2/headless entry
points to tsdown config and package.json exports. Update all hook
imports to use the new context module. Always subscribe to onError
in web provider (matching RN pattern). Use batchedForceUpdate for
onMessagesChanged. Replace extraDeps spread with JSON.stringify in
useFrontendTool and useRenderTool dependency arrays.
Headless React Native wrapper for CopilotKit. Provides a lightweight
CopilotKitProvider and re-exports platform-agnostic hooks from
react-core without pulling web dependencies (no DOM, CSS, Radix,
Lit, A2UI). Includes XHR-based streaming fetch polyfill for Hermes
and granular polyfills for streams, encoding, crypto, DOM, and
location APIs.
Bump @langchain/langgraph-cli from 1.2.1 to 2.0.0 in the langgraph-js
starter and showcase langgraph-typescript integration. The transitive
dep @langchain/langgraph-api@1.2.1 imports STREAM_EVENTS_V3_MODES from
@langchain/langgraph/web, but that symbol was never published in any
release of @langchain/langgraph (including 1.3.0 currently pinned).
langgraph-api@2.0.0 drops the broken import entirely.
The shell-docs Docker build fails because link-to-copilot-cloud.tsx
imports from lucide-react but the package was never added to
shell-docs/package.json. Adding it at ^0.469.0 to match sibling
showcase packages.
## Summary
Ports the client-side telemetry stack from `docs/` into
`showcase/shell-docs/` and tracks every subsequent telemetry-related
upstream merge so the cutover from `docs.copilotkit.ai` to shell-docs
can ship without going dark on funnel attribution.
## What's included
### Telemetry stack port
- **Client-side PostHog** — `posthog-js` provider mounted in
`app/layout.tsx`, complements server-side middleware capture
- **GA4** — `react-ga4` pageview hook
- **HubSpot, Reo.dev** — Script tags in `<head>`
- **Scarf pixel** — page-load OSS attribution
- **RB2B** — canonical `<Script id="reb2b-script">` in `<head>` (matches
upstream after `#4646` LCP-trim refactor; see env var note below)
- **Framework selector instrumentation** — fires `framework_selected`
(net-new event, no upstream equivalent)
- **POSTHOG env rename** — `POSTHOG_PROJECT_KEY` → `POSTHOG_KEY` across
both shell and shell-docs middlewares, env-drives `POSTHOG_HOST` with
`eu.i.posthog.com` fallback
### Open Graph + routing
- **OG image route** — `app/og/[...slug]/route.tsx` for per-page
social-share images
- **vercel.json** — `app/og/**` `maxDuration: 60`
- **Internal-link sweep** — 30 `https://docs.copilotkit.ai/...`
self-references across 15 mdx files rewritten to relative paths so they
don't redirect-loop post-cutover
### Upstream parity tracking (each commit mirrors a merged docs/ PR)
- **Reverse-proxy PostHog through `/ingest`** (mirrors `#4554`) —
`next.config.ts` rewrites + provider hardcodes `/ingest` + middleware
excludes `/ingest` from matcher
- **Talk to Our Engineers nav button** (mirrors `#4590`) — fires
`talk_to_us_clicked` with `{ location: "docs_nav" }`
- **Cloud CTA repointed to dashboard.operations.copilotkit.ai** (mirrors
`#4592`) — UTM-tagged + fires `try_for_free_clicked` with `{ location
}`. Distinguishes `docs_navbar` (desktop) from `docs_navbar_mobile`.
`LinkToCopilotCloud` is preserved for MDX content references that still
point at `cloud.copilotkit.ai`
- **CLI command copy tracking** (mirrors `#4643` + follow-ups) —
`track-command-copy.ts` + `CopyTracker` provider monkey-patches
`navigator.clipboard.writeText`. Final shape: `cli_command_copied` with
`{ install_type, location? }`. `install_type` covers
`npx`/`pnpm`/`pip`/`uv`/`docker`/`curl`/`brew`/`helm`/`kubectl`/`make`/`bash`/`sh`
and friends
- **`capture_dead_clicks: false`** (mirrors `#4646` LCP trim) — drops
dead-clicks-autocapture.js
- **Drop `useRB2B` hook + use canonical `<Script>`** (mirrors `#4646`) —
single source of truth for RB2B injection
## Env vars required on the deployment target
Same project IDs as the current docs deployment for data continuity:
- `NEXT_PUBLIC_POSTHOG_KEY`, `POSTHOG_KEY`, `NEXT_PUBLIC_POSTHOG_HOST`
- **`NEXT_PUBLIC_REB2B_KEY`** — renamed from `NEXT_PUBLIC_RB2B_ID` to
match upstream's canonical Script. Same key value, different env var
name. **Deployment ops needs to set this.**
- `NEXT_PUBLIC_SCARF_PIXEL_ID`
- `NEXT_PUBLIC_REO_KEY`
- `NEXT_PUBLIC_GOOGLE_ANALYTICS_TRACKING_ID`
## Architecture notes
- Hook-based telemetry (GA4) and `CopyTracker` are consolidated into
`<AnalyticsClient />`, a single client boundary mounted from the
server-component layout. Mirrors upstream's `ProvidersWrapper` pattern.
- `<head>` carries the Reo.dev, HubSpot, and (gated on `REB2B_KEY`) RB2B
Script tags. PostHog provider wraps the body inside Suspense; Scarf
pixel sits at end of body.
## Test plan
- [ ] `nx run shell-docs:typecheck`, `lint`, `build` clean
- [ ] Cloud CTA click → confirm dashboard URL with UTMs lands and
`try_for_free_clicked` fires with the right `location`
- [ ] Talk to Our Engineers click → confirm `talk_to_us_clicked` fires
with `location: "docs_nav"` and routes to copilotkit.ai/contact-us
- [ ] PostHog dashboard: docs session continues to attribute via
`/ingest` reverse proxy
- [ ] Copy any code block → confirm `cli_command_copied` fires once with
the correct `install_type`
- [ ] RB2B, Reo.dev, HubSpot, GA4 scripts load (when their env vars are
set) and report events
- [ ] OG image route renders for 5 random pages
- [ ] Framework selector emits `framework_selected`
Two bugs in the Reo init <Script> in layout.tsx:
- If NEXT_PUBLIC_REO_KEY was unset, the script still ran with
e = "undefined" and fetched https://static.reo.dev/undefined/reo.js.
Sibling REB2B_KEY was correctly gated; Reo now matches.
- Bare interpolation into dangerouslySetInnerHTML — any future env
value containing " or </script> would break out of the inline
literal. Switches to JSON.stringify(REO_KEY) and reuses the
resulting binding inside Reo.init() so the key only appears once.
Pulls the dashboard.operations.copilotkit.ai href + UTM string back
into CLOUD_CTA so the desktop and mobile placements share a single
source of truth. UTM tweaks become a one-line change instead of two.
Two review fixes for framework-selector.tsx:
- Replaces the `posthog-js` singleton import with `usePostHog()` from
`posthog-js/react`, matching the rest of the codebase. Both work after
posthog.init() runs, but the singleton was an outlier.
- Renames the event from `framework_selected` to `docs.framework_selected`
to match the dotted-prefix convention used elsewhere in PostHog
Insights (oss.* / cloud.* / eip.* / pricing.cta_clicked). Cheap to do
now since the event is brand-new with no historical data.
The suppressor was carried over from the original telemetry-stack port
of upstream's posthog-provider.tsx. Its job was masking
ERR_BLOCKED_BY_CONTENT_BLOCKER noise from PostHog requests being
blocked by ad blockers targeting *.i.posthog.com directly.
This PR ships the /ingest reverse proxy (mirroring upstream #4554), so
PostHog calls now flow through the docs host and the underlying
hostname is invisible to ad blockers. With nothing to suppress, the
mask only hid real errors that mention "posthog". Three concrete
problems on top of the dead-code framing:
- Substring match on "posthog" swallows unrelated messages.
- isInitializedRef.current only guards the same component instance,
so HMR or StrictMode remounts re-capture the already-patched
console.error and layer wrappers on each remount.
- originalLog is captured but console.log is never patched.
Resolves PDX-112.
Mirrors upstream #4646 in two changes from the same PR:
- Drop the duplicate useRB2B hook in favor of the canonical
<Script id="reb2b-script"> in app/layout.tsx (gated on REB2B_KEY).
Env var renames from NEXT_PUBLIC_RB2B_ID (the hook's name) to
NEXT_PUBLIC_REB2B_KEY (upstream's canonical name) — deployment
ops will need updating.
- Set capture_dead_clicks: false on PostHog init so the
dead-clicks-autocapture.js bundle isn't pulled in on the critical
path.
Mirrors upstream #4643 + #4646 follow-ups. New trackCommandCopy helper
infers install_type from the leading token (covers npx/pnpm/pip/uv/
docker/curl/brew/helm/kubectl/make/bash/sh, falls back to "code"), and
the CopyTracker provider monkey-patches navigator.clipboard.writeText
once at app boot so every programmatic copy fires cli_command_copied
without per-component instrumentation. Preserves upstream's chained-
wrapper pattern so it coexists with Reo's writeText patch.
Final event shape is { install_type, location? } — the command body
and product props from the original draft were dropped upstream
(commit 0b7b3c77c) before merge.
Adds a Talk to Our Engineers CTA to the right side of the docs nav at
viewports >= 1400px and surfaces it in the mobile burger menu below
that. Fires posthog talk_to_us_clicked with { location: "docs_nav" }
on click, then navigates to copilotkit.ai/contact-us. Mirrors the
event the docs/ navbar fires so docs-originated clicks can be
segmented separately from website clicks (which use "nav").
Routes PostHog analytics through /ingest/* (rewrites to eu.i.posthog.com)
so requests bypass ad blockers and tracking-protection that target the
*.i.posthog.com hostname directly. Hardcodes POSTHOG_HOST = "/ingest"
in the provider, points ui_host at https://eu.posthog.com so PostHog UI
links still resolve, and excludes /ingest from the middleware matcher
so the proxy itself doesn't fire phantom pageviews. Mirrors the same
fix in docs/.
Sweeps 30 hardcoded https://docs.copilotkit.ai/* self-references across
15 mdx files in src/content/ to relative paths so they don't redirect-loop
once docs.copilotkit.ai cuts over to shell-docs.
Adds a Free Developer Access CTA to the brand nav (desktop + mobile
menu) that points at dashboard.operations.copilotkit.ai with UTM tags
and fires try_for_free_clicked with { location } on click.
Distinguishes docs_navbar (desktop) from docs_navbar_mobile so the
funnel can split the two layouts.
Also adds the LinkToCopilotCloud component for use by MDX content
references that deep-link into Copilot Cloud — those remain on
cloud.copilotkit.ai.