## Summary `useDefaultRenderTool`'s `render` was typed to return `React.ReactElement`. A caller who wants to render only *some* tool calls therefore could not return `null` to suppress the built-in default for the rest — the value flowed through correctly at runtime, but the type rejected it. This widens the public `render` return type, and the wrapper local that carries the user's value, to `React.ReactElement | null`. ```diff - render?: (props: DefaultRenderProps) => React.ReactElement; + render?: (props: DefaultRenderProps) => React.ReactElement | null; ``` The reference page hand-writes the same signature, so it is updated to match, with one behavior bullet describing what `null` does. ## Scope, and its relationship to #6533 #6533 already widens the same return type to `React.ReactElement | null` in `defineToolCallRenderer.ts` and `use-render-tool.tsx`. It does **not** touch `use-default-render-tool.tsx`, which is the remaining gap and the whole of this PR. There is **no file overlap**, so the two can land in either order. A structural sweep of `react-core/src/v2` for renders still typed `=> React.ReactElement` with no `| null` confirms this leaves nothing behind on this surface: ``` types/defineToolCallRenderer.ts:40,48,56 <- #6533 hooks/use-render-tool.tsx:41,72,109 <- #6533 hooks/use-default-render-tool.tsx:152 <- the deliberate bridge cast, below hooks/use-interrupt.tsx:89 <- different surface, out of scope components/chat/CopilotChatMessageView.tsx:418 <- different surface, out of scope ``` The `as unknown as` cast into `useRenderTool` is deliberately left in place: `useRenderTool` still requires a `ReactElement` return on `main` (verified again after the rebase — `use-render-tool.tsx:41`). Once #6533 lands, that cast can be tightened. The bridge comment is updated to say so. `DefaultToolCallRenderer`'s own return type stays `React.ReactElement` — the built-in default always renders an element. The Vue counterpart needs no equivalent change: its `render` already returns `VNodeChild`, which admits `null`, and `reference/vue/hooks/useDefaultRenderTool.mdx` already matches. ## Why the guard is a type test, not a runtime test TypeScript types are erased, so a `null`-returning render forwards identically before and after the widening. The runtime test passes against un-widened source, which makes it worthless as a guard for this change. So the real guard is `use-default-render-tool-types.test-d.ts`, using the `expectTypeOf` + `toEqualTypeOf` convention already documented in `v2/__tests__/headless-type-exports.test-d.ts`. `toEqualTypeOf` is required rather than assignability: a function returning `ReactElement` **is** assignable to one returning `ReactElement | null`, so an assignability check would pass against the un-widened type and assert nothing. The `.test-d.ts` basename is outside vitest's `include` globs, so nothing there executes; `tsc --noEmit` (`check-types`) is what reads it. Confirmed on this base: ``` $ vitest list --filesOnly | grep -c "test-d" 0 $ grep -n include -A4 packages/react-core/vitest.config.mjs include: [ "src/**/__tests__/**/*.{test,spec}.{ts,tsx}", "src/**/*.{test,spec}.{ts,tsx}", ], $ grep include packages/react-core/tsconfig.json "include": ["src/**/*"], ``` The runtime test is kept as well, since it still covers prop adaptation and forwarding. ## Testing All numbers below were re-measured after the rebase onto `main` (`42494df`). **Mutation check of the type guard** — revert the widening in the source, confirm the guard goes red: ``` ########## RUN A: rebased HEAD as-is ########## total errors: 63 --- errors in touched files --- none ########## RUN B: MUTATION - widening reverted in source ########## total errors: 65 --- guard file errors (expect FAIL) --- use-default-render-tool-types.test-d.ts(26,3): error TS2344: Type '((props: DefaultRenderProps) => ReactElement<...> | null) | undefined' does not satisfy the constraint '"Expected: undefined, Actual: never" | "Expected: function, Actual: never"'. use-default-render-tool.test.tsx(150,30): error TS2322: Type 'Mock<({ status }: DefaultRenderProps) => null>' is not assignable to type '(props: DefaultRenderProps) => ReactElement<...>'. Type 'null' is not assignable to type 'ReactElement<...>'. ``` The guard fails when the widening is reverted, and the two new errors are exactly the guard plus the runtime test's own use of it. Nothing else moves. **Typecheck** (`tsc -p packages/react-core --noEmit`) — error set byte-identical to pristine `origin/main` in the same worktree, none in the touched files: ``` ########## RUN C: pristine origin/main baseline ########## total errors on pristine main: 63 === diff: pristine-main errors vs HEAD errors === IDENTICAL -> the change introduces no new type errors ``` The 63 are pre-existing worktree noise: `react-core` resolves `@copilotkit/core` and `@copilotkit/shared` from a sibling checkout's `dist`, so unrelated exports read as missing. They are present on pristine `origin/main` in the same worktree, which is what the diff above shows. **Target test file:** ``` ✓ src/v2/hooks/__tests__/use-default-render-tool.test.tsx (13 tests) 38ms Test Files 1 passed (1) Tests 13 passed (13) ``` **Broader `src/v2/hooks` + `src/v2/types`** — failure counts identical to pristine `origin/main` in the same worktree, plus exactly the one new passing test: ``` === BASELINE (pristine origin/main in this worktree) === Test Files 22 failed | 17 passed (39) Tests 9 failed | 201 passed (210) === WITH my change === Test Files 22 failed | 17 passed (39) Tests 9 failed | 202 passed (211) ``` **Lint:** `oxlint packages/react-core/src/v2/hooks/` — `Found 62 warnings and 0 errors.` (all pre-existing exhaustive-deps warnings, none in the touched files). **Formatting:** `oxfmt --check` on the three source/test files — `All matched files use the correct format.` `oxfmt` does not process `.mdx`, so the reference page is out of its scope. **Public-API manifest:** no regeneration needed — `scripts/release/public-api/manifest.v1.json` records no type signatures and does not mention `useDefaultRenderTool` (`grep -c ReactElement` → `0`). ## Provenance Extracted from #5509 (@ataibarkai), which is 3915 commits behind `main` and being closed. That PR also changed `defineToolCallRenderer`'s schema default from `def.name === "*" && !def.args ? z.any() : def.args` to `def.args ?? z.any()`. **That change is deliberately not carried here** — it alters runtime behavior for named renderers declared without `args` (from `args: undefined` to `args: z.any()`) and deserves its own PR and its own verification. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Custom renderers can now return `null` to suppress output when no UI should be displayed. * **Documentation** * Updated `useDefaultRenderTool` guidance to describe null-return behavior and selectively rendering tool calls. * **Tests** * Added coverage confirming null-render behavior and forwarded renderer properties. * Added compile-time validation for supported renderer return types. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
Shell Docs
showcase/shell-docs is the Next.js app that builds and serves
docs.copilotkit.ai. Author CopilotKit product documentation here, not in the retired
top-level docs/ app.
Run Locally
Shell-docs is a standalone npm-based app. You do not need a root install just to run the docs app locally.
cd showcase/scripts
npm install
cd ../shell-docs
npm install
npm run dev
The local dev server runs on port 3003.
http://localhost:3003
The shell-docs npm lifecycle generates registry, demo-content, setup-content, and search
data before dev, build, and typecheck.
Validate Changes
Run these from showcase/shell-docs:
npm run build
npm run typecheck
npm run test
For repo-level CI parity, prefer Nx when a shell-docs target is available in the current checkout and root dependencies are installed. For normal shell-docs local development, the npm commands above are the canonical path.
Authoring Recipes
Showcase-Driven Framework Docs
Showcase-driven frameworks use docs_mode: generated. The docs are assembled from showcase
registry/generated data, demos, source regions, shared/root MDX, snippets, and sparse
framework overrides.
To update showcase-driven docs:
- Edit the showcase source of truth: manifests, demos, feature coverage, source regions, or registry inputs.
- Edit shared/root MDX only when the change applies across generated frameworks.
- Add sparse framework overrides only for real framework-specific differences.
- Do not hand-edit generated files under
src/data/frameworks/. - Validate routes, sidebar state, search results, snippets, and framework switching.
Authored Framework Docs
Authored frameworks use docs_mode: authored. The framework owns an MDX tree under
src/content/docs/integrations/<docsFolder>/ with a meta.json sidebar.
To update authored docs:
- Check
getDocsFolder()insrc/lib/registry.ts; the URL slug and folder name may differ. - Edit the MDX page under
src/content/docs/integrations/<docsFolder>/. - Update that folder's
meta.jsonwhen adding, removing, or moving pages. - Reuse shared snippets from
src/content/snippets/when content should stay consistent across frameworks. - Validate the framework route, sidebar, search result, and any shared snippet render.
Reference Docs
Edit API reference pages under src/content/reference/.
The v2 reference does not use meta.json; navigation is generated by walking the tree and
reading each page's title and description frontmatter. Only the legacy reference/v1/
tree uses meta.json.
Snippets
Reusable snippets live under src/content/snippets/. Snippets may be rendered by root docs,
authored framework pages, and showcase-driven framework pages, so keep them general unless
the path is intentionally framework-specific.
Frontend Applicability
Frontend routes use page-level applicability metadata, independent from where the content is authored. A page can be authored MDX, showcase-generated content, mirrored protocol docs, or reference content and still be universal across frontends.
Use the frontend field in page frontmatter or meta.json when a root doc should appear in
non-React frontend docs:
universal— render the same page under/<frontend>/....frontend-variant— render only when a matching page exists undersrc/content/docs/frontends/<frontend>/....hide— omit the page from frontend-scoped docs.
Do not use "showcase-driven" as a proxy for frontend availability. Showcase derivation is an authoring/source detail; frontend applicability controls routing and sidebar inclusion.
AG-UI Mirrored Docs
AG-UI protocol docs are authored upstream in ag-ui-protocol/ag-ui. The
src/content/ag-ui/ tree is a downstream mirror rendered on the CopilotKit docs host.
Change AG-UI docs upstream first, then sync the mirror back into shell-docs.
Top-Level Docs Symlink
The repository's top-level docs/ path is a symlink to showcase/shell-docs/ for
contributor muscle memory. It is not a separate docs app. Do not recreate the old
docs/content/docs/ tree; author CopilotKit docs in showcase/shell-docs/src/content/.