Files
Ben Taylor 407249d4fc fix(react-core): let a custom catch-all renderer return null (#6942)
## 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 -->
2026-09-08 17:27:44 -05:00
..

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:

  1. Edit the showcase source of truth: manifests, demos, feature coverage, source regions, or registry inputs.
  2. Edit shared/root MDX only when the change applies across generated frameworks.
  3. Add sparse framework overrides only for real framework-specific differences.
  4. Do not hand-edit generated files under src/data/frameworks/.
  5. 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:

  1. Check getDocsFolder() in src/lib/registry.ts; the URL slug and folder name may differ.
  2. Edit the MDX page under src/content/docs/integrations/<docsFolder>/.
  3. Update that folder's meta.json when adding, removing, or moving pages.
  4. Reuse shared snippets from src/content/snippets/ when content should stay consistent across frameworks.
  5. 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 under src/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.

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/.