Fixes#6383. Fixes#6243.
Both issues land in the same 35 lines of `useCopilotReadable`, so they
are fixed together. This PR also covers a third defect neither issue
reports.
All of it traces to a single commit: 80dffec4e7 ("feat: Reimplement
CopilotKit on top of refreshed internals (v1.50.0)", #2638), which
repointed the hook from the v1 context tree onto the v2 flat context
store. The pre-1.50 implementation was correct on every count below.
## Fixes
**`available` was missing from the effect deps** (#6383)
The effect body read `available` but the deps were `[description, value,
convert]`, so toggling between `"enabled"` and `"disabled"` after mount
did nothing. It is back in the deps, along with the `available =
"enabled"` default the port dropped.
**`convert` was called with one argument** (#6243)
`(convert ?? JSON.stringify)(value)` invoked a user's `(description,
value) => string` as `convert(value)`, so it received the value as
`description` and `undefined` as `value`. The branches are now split
rather than passing two arguments to the combined expression —
`JSON.stringify(description, value)` would treat the second argument as
a *replacer*, not a value.
**`dependencies` was accepted and ignored** (#6243)
The second positional argument was destructured but never reached the
deps array. Now spread, matching `useCopilotAdditionalInstructions`.
**The `found` dedup branch was dead code** (unreported)
It compared `JSON.stringify({ description, value })` against a stored
entry whose `value` had already been serialized by `addContext`
(`packages/core/src/core/context-store.ts:36`). That never matches — for
objects or strings — so the branch and its cleanup-skipping early return
were unreachable. Deleted rather than repaired: making the comparison
work would newly let component A's unmount remove a context entry
component B is still relying on. The test `keeps separate entries for
identical readables in two components` locks that in, and it passes
against the pre-fix hook, which is what confirms the branch never fired.
## `parentId` / `categories`
Both are still in `UseCopilotReadableOptions` and were still documented
— the top-of-file JSDoc example was a `parentId` tutorial — but the same
v1.50 commit dropped them from the hook body. They have been no-ops
since.
This PR does not implement them. Real support needs parent/child
modelling in the v2 context store, which is flat by design
(`getContextForAgent` emits `{ description, value }` only). Instead both
are marked `@deprecated` and the JSDoc example is rewritten to document
behavior that exists. Tracked in #6408.
## Not addressed
Two pre-existing behaviors left alone to keep this a bugfix:
- `value` is in the deps raw, so an inline object literal re-registers
the entry on every render. Pre-1.50 depended on the serialized string
instead.
- The hook returns `undefined` on first render, since the ref is
assigned inside the effect.
## Testing
`useCopilotReadable` had no test file. This adds one — 12 tests, using a
fake that mirrors `ContextStore` semantics (`addContext` assigns an id
and stores the already-serialized value).
Full project suite — `nx run @copilotkit/react-core:test`:
```
Test Files 124 passed (124)
Tests 1487 passed (1487)
NX Successfully ran target test for project @copilotkit/react-core and 17 tasks it depends on
```
Each fix is covered by a test that fails against the pre-fix hook.
Reverting only `use-copilot-readable.ts` and re-running the new file:
```
✓ registers the context on mount
✓ removes the context on unmount
✓ available > registers nothing when mounted as disabled
× available > removes the context when flipped to disabled after mount
→ expected [ { description: 'employees', …(1) } ] to deeply equal []
× available > re-adds the context when flipped back to enabled
→ expected [] to deeply equal [ { description: 'employees', …(1) } ]
× convert > is called with (description, value) in that order
→ expected "spy" to be called with arguments: [ 'employees', …(1) ]
× convert > is used in place of JSON.stringify
→ Cannot read properties of undefined (reading 'map')
✓ convert > serializes the value alone when convert is omitted
× dependencies > re-runs the effect when a dependency changes
→ expected "spy" to be called 2 times, but got 1 times
✓ dependencies > does not re-run the effect when the dependency is unchanged
✓ re-registers when the description changes
✓ keeps separate entries for identical readables in two components
Test Files 1 failed (1)
Tests 5 failed | 7 passed (12)
```
The two that still pass pre-fix are deliberate: `serializes the value
alone when convert is omitted` guards the `JSON.stringify` replacer trap
in the fix itself, and `keeps separate entries…` is the evidence that
the `found` branch was dead.
With the fix applied:
```
✓ src/hooks/__tests__/use-copilot-readable.test.tsx (12 tests) 15ms
Test Files 1 passed (1)
Tests 12 passed (12)
```
Types — `pnpm --filter @copilotkit/react-core check-types`:
```
> @copilotkit/react-core@1.66.2 check-types
> tsc --noEmit
```
(no diagnostics)
Formatting — `oxfmt --check` on both files:
```
Checking formatting...
All matched files use the correct format.
Finished in 16ms on 2 files using 18 threads.
```
`oxlint` reports one warning, on `...(dependencies || [])` in the deps
array. The same pattern already warns in
`use-copilot-additional-instructions.ts`, `use-frontend-tool.ts` and
`use-coagent-state-render.ts`; CI runs `oxlint .` without
`--deny-warnings`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The `convert` and `dependencies` fixes were independently found and
fixed first by @jwgrsol in #6246, opened a week before this PR. Credited
below.
Co-authored-by: jwgrsol <wefhio1985@gmail.com>
## What does this PR do?
`@copilotkit/react-native` maintained a **private tool-call render
registry** (`hooks/RenderToolContext.tsx`) alongside the canonical one
that `CopilotKitCoreReact` already provides — and which every React
Native app already ships, unused. This PR deletes the fork and points
React Native at the shared registry.
That fork caused three bugs:
| Bug | Symptom | Cause |
|---|---|---|
| **Tool renders never streamed** | A component registered with
`useRenderTool` / `useComponent` painted nothing until the tool call
completed | `CopilotChat` used `JSON.parse` on the argument buffer.
While a model writes a tool call that buffer is *invalid JSON by design*
— AG-UI delivers `TOOL_CALL_ARGS` deltas that are concatenated
client-side — so the parse threw on every delta, warned, and fell back
to `{}` |
| **`useComponent` rendered nowhere** | Silently, with no error | It
writes to core's registry; React Native's chat read React Native's
private `Map` |
| **Chat history degraded** | Navigating away from the registering
screen turned earlier tool calls into a `Called: <name>` placeholder |
The private `Map` deleted renderers on unmount; core deliberately keeps
them |
`@copilotkit/react-core` has used `partialJSONParse` on this path since
v2 shipped. React Native diverged because `useRenderToolCall` was
excluded from its re-exports on the stated grounds that it "depends on
DOM elements via `DefaultToolCallRenderer`" — a claim that was never
true of the hook itself. It was only ever reachable through the fat
`/v2` entry, whose weight is the real hazard (#4893). #5883 moved it
into `/v2/headless` on 2026-07-23; the exclusion comment was rewritten
the next day without revisiting the reason.
### What changed
- **One registry.** `useRenderTool` registers through `useFrontendTool`
into `CopilotKitCoreReact.renderToolCalls`. `CopilotChat` and any custom
surface consume react-core's `useRenderToolCall`.
- **Types are derived, not declared.** `RenderToolProps` is now
`React.ComponentProps<ReactToolCallRenderer<T>["render"]>`, so React
Native cannot drift from `ReactToolCallRenderer` — the contract every
registered renderer is actually invoked against. Change that contract
and `check-types` names every React Native renderer the change breaks.
React Native narrows only the *return* type to `ReactElement | null`,
which `FlatList`'s `renderItem` genuinely requires.
_Scope of that guarantee (corrected during review):_ it does **not**
extend to the type react-core publicly exports under the same name.
Web's `RenderToolProps<S>`
(`react-core/src/v2/hooks/use-render-tool.tsx`) is a separate
hand-declared union, generic over a schema, carrying arguments under
`parameters` (not `args`) and declaring `status` as string literals
rather than `ToolCallStatus` members. Both divergences are live today
and nothing type-checks them shut — the one place the shapes meet,
react-core's own bridge, compiles because a string-enum member is
assignable to its own literal type but not the reverse. Aligning web's
alias is a breaking web API change, filed separately.
- **`RenderToolContext.tsx` deleted** (−150 lines), along with 15 tests
that described the removed subsystem. One of them — `unregisters the
render function on unmount` — asserted the chat-history bug as a
requirement.
- **Two structural CI guards for #4893**, in opposite directions: a test
failing if any React Native source imports the fat `/v2` entry, and a
script failing if react-core's `/v2/headless` or `/v2/context` chunks
ever link shiki/mermaid/cytoscape/katex/streamdown. Both were verified
able to fail by deliberately introducing the regression. These are
*structural* assertions, not size budgets — `dev-docs/bundle-size.md`
freezes `limit` fields until OSS-122.
- **`react-native` added to the bundle-size glob**, which it had never
been in, plus a `size:headless` measurement.
React Native also gains capabilities it lacked: render props inferred
from your schema, `name`/`toolCallId` on render props, and `result` on
completed calls.
**Corrected during review — two capabilities this originally claimed are
not delivered:**
- **Wildcard (`"*"`) renderers do not work on React Native.** Because
`useRenderTool` routes through `useFrontendTool` (which calls
`addTool`), `name: "*"` registers a frontend tool literally named `*` —
advertised to the model, and colliding with core's separate
wildcard-executable-tool path. react-core's `useRenderTool` is
renderer-only and special-cases the wildcard; React Native's is not. The
guide now advises against it.
- **`followUp` (and `available`) are not forwarded**, and the handler's
`context` argument is dropped, so `stopAgent()`'s abort signal is
unreachable from an RN handler.
Both are tracked in § Known limitations for the follow-up that converges
React Native onto react-core's hooks — deleting RN's `useRenderTool` in
favour of re-exporting `useFrontendTool` (tool + renderer) and
react-core's `useRenderTool` (renderer-only, wildcard-capable). That is
an API change with its own migration note, so it is not in this PR.
### ⚠️ Breaking (in a minor)
`useRenderToolRegistry` and `RenderToolProvider` are **removed**. Both
are documented on the docs site, so this is a real break — see the
`BREAKING CHANGE:` footer on `db67ccf`, which is what the release notes
derive from, plus the rewritten reference pages.
```diff
- const registry = useRenderToolRegistry();
- const renderer = registry.get(toolCall.function.name);
- return renderer ? renderer({ args, status }) : null;
+ const renderToolCall = useRenderToolCall();
+ return renderToolCall({ toolCall });
```
Also note two semantic changes: `args` is `Partial<T>` **only** while
`status` is `"inProgress"`, and a render function is now captured at
registration — if it closes over changing values you must declare them
in `deps` (React Native previously refreshed the closure on every
render).
**Known limitation:** agent-scoped renderer resolution does not take
effect on React Native. `CopilotChatConfigurationProvider` is not in
RN's provider tree, so `agentId` always resolves to the default.
Renderers still resolve by name; two agents registering the same tool
name resolve arbitrarily. Filed separately.
### A data point worth recording
Adding `useRenderToolCall` to the measured headless entry moved the
bundle **92.8 kB → 92.7 kB**. Flat. The hook React Native spent months
not using was already inside the chunk every RN app resolves whole —
Metro doesn't tree-shake, so the fork never saved a byte. It cost them.
### Testing
- `@copilotkit/react-native`: **253 passing / 22 files** ·
`@copilotkit/react-core`: **1480 passing / 123 files** · `check-types`
and `build` green for both.
- Each of the three bugs has a deterministic test driving a real
`CopilotKitCoreReact` — no mocking of the code under test.
- Both #4893 guards carry mutation evidence: introduce the regression,
watch them fail, revert, watch them pass.
### Follow-up
`useRenderTool`'s JSDoc is split across two blocks, which orphans the
primary description from IDE hover (the `@param deps` warning still
surfaces). One-line fix, deliberately left out of the final fix wave.
## Related PRs and Issues
- **Supersedes #6346** (@davidmckayv) — its diagnoses were correct and
its test assertions are ported here, re-driven through the real registry
rather than a mocked local one. Credited via `Co-Authored-By` on
`4104bd1`.
- Addresses the React Native half of **#4893**.
- Builds on **#5883**, which created the lean `/v2/headless` entry this
PR consumes.
## Checklist
- [x] I have read the Contribution Guide
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation
- [x] "Allow edits by maintainers" is checked
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Problem
`@copilotkit/react-core` ships **two independent copies** of the v2
context module, so `useLicenseContext` imported from
`@copilotkit/react-core/v2/context` returns the default forever —
`status: null` even when `/info` reports `licenseStatus: "valid"`.
Reported downstream as a chat-history sidebar that never loads, because
`useThreads` is gated on license status.
`src/v2/context.ts` is compiled by two separate tsdown builds:
| Build | Output | Contains |
|---|---|---|
| `entry: ["src/index.tsx", "src/v2/index.ts"]` | `dist/` shared chunk |
inlined copy **A** |
| `entry: {context: "src/v2/context.ts"}` | `dist/v2/context.*` |
standalone copy **B** |
There is no import edge between them, so `createContext()` runs twice.
`CopilotKitProvider` lives in the shared chunk and publishes to **A**;
`@copilotkit/react-core/v2/context` exports **B**, which nothing ever
provides.
Verified against the published 1.66.4 artifact:
```
$ grep -n "createContext" dist/v2/context.mjs
104:const CopilotKitContext = createContext(null);
124:const LicenseContext = createContext({
$ grep -n "createContext" dist/copilotkit-nRjRp2_5.mjs # inside //#region src/v2/context.ts
1522:const CopilotKitContext = createContext(null);
1544:const LicenseContext = createContext({
$ grep -E '^import .*from "[^"]*context[^"]*"' dist/copilotkit-nRjRp2_5.mjs
# (empty — no import edge)
```
`CopilotKitContext` is duplicated identically, so `useCopilotKit`
imported from that subpath throws `"useCopilotKit must be used within
CopilotKitProvider"`. The subpath was effectively unusable for web
consumers; license was just the *silent* failure mode.
**Compounding defect:** `src/v2/providers/index.ts` enumerates its
exports by name and omits `useLicenseContext` (even though
`CopilotKitProvider.tsx:19` re-exports it). So the live copy had **no
public import path at all**, leaving consumers with no correct
alternative.
Not a 1.66.x regression — broken since c3c30969e4 (2026-05-06), the
commit that introduced the split.
## Fix
1. **`tsdown.config.ts`** — hoist the existing `externalize-context`
plugin and apply it to the `dist/` build. The headless build already
used it for exactly this reason ("ensuring a shared React context
instance at runtime"); it was simply never applied here. One instance
now. UMD builds stay self-contained by design.
2. **`src/v2/providers/index.ts`** — export `useLicenseContext`.
3. **`scripts/context-singleton-preflight.mjs`** *(new)* — build-time
guard, wired into `build`.
4. **`src/v2/providers/__tests__/providers-exports.test.ts`** *(new)*.
### Why a guard
This bug class is invisible to every gate we have. On the broken build,
`tsc`, 1471 vitest tests, `publint` and `attw` were **all green** while
the published package shipped two contexts — vitest imports *source*,
where only one module exists. The guard keys off the `//#region
src/v2/context.ts` banner tsdown emits per inlined module, and
self-checks: if that banner convention ever changes it fails loudly
rather than silently passing everything.
## Testing
**End-to-end reproduction against the built dist** — provider from
`/v2`, hook from `/v2/context`, exactly as a consumer app wires it. This
is the test that most directly encodes the reported bug.
Against a **pre-fix** build (rebuilt from the parent commit's
`tsdown.config.ts`):
```
× useLicenseContext sees server-reported 'valid', not the default
→ expected 'null' to be 'valid'
× useLicenseContext sees server-reported 'expired', not the default
→ expected 'null' to be 'expired'
```
That `'null'` is precisely the reported symptom — a valid license read
as `status: null`, permanently disabling license-gated features.
Against this branch:
```
✓ src/v2/__tests__/dist-context-singleton.test.tsx (2 tests)
```
It also confirms the self-reference resolves under a real bundler
(Vite), and it degrades to a loud skip when no dist is present (verified
by removing `dist/v2/index.css`): nx `test.dependsOn` is `^build`, so
this package's own build is not guaranteed to have run before `test`.
The hard gate is therefore the preflight, which runs as part of `build`.
**Both build-level guards proven red→green — not merely green.**
Preflight against the **actual published 1.66.4 dist** (expected fail):
```
$ node scripts/context-singleton-preflight.mjs .../copilotkit-react-core-1.66.4/dist
context-singleton-preflight: src/v2/context.ts is bundled into 2 unexpected file(s):
- copilotkit-nRjRp2_5.mjs
- copilotkit-sitn7Oe8.cjs
exit=1
```
Preflight on this branch's build (expected pass):
```
$ node scripts/context-singleton-preflight.mjs
context-singleton-preflight: OK — src/v2/context.ts bundled only into 4 allowed target(s).
exit=0
```
New export test with the fix line removed (expected fail):
```
× exports the provider hooks as runtime functions
→ useLicenseContext should be exported as a runtime function: expected 'undefined' to be 'function'
```
Emitted-bundle verification after the fix:
```
$ grep -c "checkFeature: () => true" dist/copilotkit-*.mjs # shared chunk no longer defines it
0
$ grep -o 'from "@copilotkit/react-core/v2/context"' dist/copilotkit-*.mjs | head -1
from "@copilotkit/react-core/v2/context"
$ grep -o 'require("@copilotkit/react-core/v2/context")' dist/copilotkit-*.cjs | head -1
require("@copilotkit/react-core/v2/context")
```
UMD must stay self-contained (own copy, no external import) — confirmed
unchanged:
```
dist/index.umd.js: ownCopy=1 externalImport=0
dist/v2/index.umd.js: ownCopy=1 externalImport=0
```
Full gates:
```
$ vitest run
Test Files 124 passed (124)
Tests 1475 passed (1475)
$ tsc --noEmit # exit 0
$ oxlint <changed files> # Found 0 warnings and 0 errors.
$ oxfmt # clean
$ publint . # clean (only pre-existing repository.url suggestion)
$ attw --pack . --profile node16
"@copilotkit/react-core" node16 CJS/ESM 🟢 bundler 🟢
"@copilotkit/react-core/v2" node16 CJS/ESM 🟢 bundler 🟢
"@copilotkit/react-core/v2/context" node16 CJS/ESM 🟢 bundler 🟢
"@copilotkit/react-core/v2/headless" node16 CJS/ESM 🟢 bundler 🟢
```
Bundle-size impact is negligible: `dist/v2/context.mjs` is 4.6 KB, and
the `bundle-size` / `copilotchat-import-size` CI checks both pass.
## Reviewer note — one behavioral trade-off
v1 and v2 share the emitted chunk, so `@copilotkit/react-core` (v1) now
**transitively depends on package self-reference**. I verified this
resolves under both ESM and CJS (above), and it's the same mechanism
`/v2/headless` already ships. Every `exports`-map-aware resolver handles
it, but a legacy `main`-only resolver (webpack 4) would not. Flagging
explicitly rather than assuming, since v1 is fully supported.
Avoiding it entirely would mean splitting v1 and v2 into separate
bundles, which duplicates the whole shared chunk — strictly worse. Happy
to take that route if we still support webpack-4-era consumers.
## Workaround for consumers on 1.66.4
```tsx
import { useCopilotKit } from "@copilotkit/react-core/v2"; // NOT /v2/context
export function useLicenseStatusCompat() {
const { copilotkit } = useCopilotKit();
const [status, setStatus] = useState(copilotkit.licenseStatus);
useEffect(() => {
const sync = () => setStatus(copilotkit.licenseStatus);
const sub = copilotkit.subscribe({ onRuntimeConnectionStatusChanged: sync });
sync(); // catch-up — /info may resolve before we subscribe
return () => sub.unsubscribe();
}, [copilotkit]);
return status;
}
```
The `sync()` catch-up matters: `useCopilotKit` registers its re-render
subscription in an effect with no catch-up read, and a provider-only
catch-up (`CopilotKitProvider.tsx:670-696`) won't re-render a
`useCopilotKit`-only consumer since `contextValue` doesn't change.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## What does this PR do?
Fixes stale Copilot Runtime documentation links that still point to
`/concepts/copilot-runtime` and now route users to the existing
`/backend/copilot-runtime` page.
This updates both the source JSDoc and the generated reference MDX so
the current docs content and future regenerated reference docs stay
aligned.
## Related PRs and Issues
- Closes#2082
## Testing
- `rg -n "concepts/copilot-runtime" packages
showcase/shell-docs/src/content` returns no matches
- `rg -n "backend/copilot-runtime"
packages/runtime/src/lib/runtime/copilot-runtime.ts
packages/react-core/src/components/copilot-provider/copilotkit-props.tsx
showcase/shell-docs/src/content/reference/v1/classes/CopilotRuntime.mdx
showcase/shell-docs/src/content/reference/v1/components/CopilotKit.mdx`
- `git diff --check`
## Checklist
- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation
- [x] "Allow edits by maintainers" is checked (lets us help iterate on
your PR directly — faster turnaround for everyone)
`headless-type-exports.test-d.ts` asserted nothing. Its only check was a
value-position annotation (`const inProgress: RendererProps = { … }`), which is
an assignability check, so degrading `RendererProps` to `any` produced zero
`tsc` errors. And `status` was pinned through a force-cast
(`"inProgress" as RendererProps["status"] & "inProgress"`), which collapses to
whatever the left side already is and suppresses the comparison outright.
Verified against the live divergence the guard exists to catch: changing
`ReactToolCallRenderer["render"]`'s `status` from the `ToolCallStatus` enum
members to bare string literals produced six errors in unrelated files and
ZERO in the guard file. Those six are incidental to this package — React
Native, the consumer this contract protects, has no such incidental users, so
on that side the drift would have been entirely silent.
Rewritten on `expectTypeOf` (already the type-assertion idiom here, see
`hooks/__tests__/use-agent-types.test.tsx`), with every positive assertion as
`toEqualTypeOf` — exact type identity, no assignability, no `as` casts. The
expected props union is spelled out independently of the type under test so the
comparison is a real detector rather than a tautology. Now pinned: the exact
props union, an explicit `not.toBeAny()` tripwire, the arm keys (`args`, not
`parameters`), and `status` as the enum in both directions.
Also pins the known divergence between react-core's two same-named public
types — the canonical renderer props (`args`, `ToolCallStatus`) and public
`RenderToolProps` (`parameters`, string literals) — as a change-detector, so
converging them becomes a deliberate, visible edit instead of silent drift.
Proven by re-applying each degradation and confirming `tsc` fails: `any` (4
errors), the enum → literal drift (3), `status` → `string` (2), `args` →
`parameters` (2), and export removal (TS2305). All proof mutations reverted.
Coverage note: this guard canNOT catch the RN `export type`-on-a-value bug.
That failure is invisible to `tsc` by construction, and it lives in react-native's
entry, which no react-core assertion can reach. It needs a runtime guard in
that package — react-core's equivalent is the sibling runtime test
`headless-exports.test.ts`.
The file is read by `tsc` only (tsconfig includes `src/**/*`); vitest's
`include` globs do not match a `.test-d.ts` basename and the package sets no
`test.typecheck`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Renders CopilotKitProvider from the built `/v2` entry and reads
`useLicenseContext` from the built `/v2/context` subpath — the exact
consumer wiring that was broken. On a pre-fix build it asserts
`expected 'null' to be 'valid'`, reproducing the reported symptom of a
license-gated feature never activating.
Skipped (loudly) when no built dist is present: nx `test.dependsOn` is
`^build`, so this package's own build is not guaranteed to have run.
The hard gate remains scripts/context-singleton-preflight.mjs, which
runs as part of `build`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`src/v2/context.ts` was compiled into two independent bundles. The build
that emits `dist/` (entries `src/index.tsx` + `src/v2/index.ts`) inlined
it into the shared chunk, while a second build emitted the standalone
`dist/v2/context.*`. Nothing linked them, so `createContext()` ran twice
and the package shipped two distinct React contexts.
`CopilotKitProvider` lives in the shared chunk, so it published to the
inlined copy. Anything importing from `@copilotkit/react-core/v2/context`
read the orphaned copy that no provider ever populated, and so saw the
defaults forever: `useLicenseContext().status` stayed `null` even when
`/info` reported `licenseStatus: "valid"`, permanently disabling
license-gated features such as `useThreads`. `CopilotKitContext` was
duplicated the same way, so `useCopilotKit` imported from that subpath
threw "must be used within CopilotKitProvider".
The headless build already externalized the module for exactly this
reason; the plugin was simply never applied to the `dist/` build. Hoist
it and apply it there too. The UMD builds stay self-contained by design.
Compounding this, `src/v2/providers/index.ts` enumerates its exports by
name and omitted `useLicenseContext`, so the live copy had no public
import path at all and consumers had no correct alternative. Export it.
Add a build-time guard, because this class of bug is invisible to every
existing gate: tsc, vitest (which imports source, where only one module
exists), publint and attw were all green while the published package
shipped two contexts. The guard fails against the real published 1.66.4
dist and passes on this build.
Broken since c3c30969e4 (2026-05-06), the commit that introduced the
split — not a 1.66.x regression.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Final review fix wave for the RN render-tool convergence branch.
Substantive:
- Extend packages/react-core/scripts/assert-headless-purity.mjs to also scan the
built /v2/context chunk (context.mjs/context.cjs), not just /v2/headless.
/v2/context carries CopilotKitCoreReact and is imported by react-native, so a
future shiki/mermaid/katex leak through it would bloat RN bundles (#4893) while
neither hard-fail guard fired. Comment and failure message updated to name both
RN-reachable entries. Mutation-verified against context.mjs.
- Document the closure-staleness convergence: render is now captured at
registration (passed into useFrontendTool) and only refreshed when deps change,
no longer re-read every render. Consumers whose render closes over changing
state must now pass deps. Documented in the useRenderTool JSDoc, the
useRenderTool.mdx reference, and the changeset migration notes.
Minor sweep:
- CopilotChat extraData now lists what renderItem actually reads
({ isRunning, renderToolCall, toolMessages }); drop unused executingToolCallIds.
- headless-type-exports.test-d.ts imports React explicitly instead of relying on
the ambient UMD global.
- useRenderTool.mdx migration heading no longer names the uncut 1.67.0 version.
- Changeset marks @copilotkit/react-core minor (new public type export), matching
its body.
Co-Authored-By: Claude <noreply@anthropic.com>
Four defects in `useCopilotReadable` were introduced together in 80dffec4e7
(v1.50.0), when the hook was repointed from the v1 context tree onto the v2
flat context store. The pre-1.50 implementation was correct on every count.
- `available` was missing from the effect's dependency array, so toggling it
between "enabled" and "disabled" after mount did nothing. It is back in the
deps, alongside the restored `available = "enabled"` default.
- `convert` was invoked as `convert(value)` against its declared
`(description, value)` signature, so a user's function received the value as
`description` and `undefined` as `value`. The branches are now split, because
`JSON.stringify(description, value)` would treat the value as a replacer.
- The `dependencies` positional argument was destructured but never reached the
deps array, matching the pattern already used by
`useCopilotAdditionalInstructions`.
- The `found` dedup branch is removed. It compared a raw value against a stored
entry whose value had already been serialized by `addContext`, so it never
matched and its early return was unreachable. Repairing the comparison rather
than deleting it would newly let one component's unmount remove a context
entry another component still depends on.
`parentId` and `categories` are marked @deprecated: both were dropped from the
hook body by the same commit but left in the options type and the JSDoc, which
advertised a nested-context feature that has been a no-op since v1.50.0. The
JSDoc example is rewritten to document behavior that actually exists. Real
hierarchy support needs parent/child modelling in the v2 context store and is
tracked separately.
Adds the hook's first test file: 12 tests, five of which fail against the
pre-fix implementation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: jwgrsol <wefhio1985@gmail.com>
The sibling of the core-follow-up assertion, missed in the previous commit: it
required the follow-up invocation to repeat the originating run id on the wire,
which is exactly what this change stops doing.
It now asserts the follow-up happened and left the id to the transport. Logical
identity is covered where it now lives — StateManager's re-stamp test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AG-UI's TOOL_CALL_START handler appends to the parent assistant message's
`toolCalls` without checking whether an entry with that id is already present,
so whenever a start event is applied twice — which the human-in-the-loop flow
triggers when the run syncs after `respond()` — the message carries the same
call twice. The second copy has EMPTY arguments, because a start event carries
none; the args arrive afterwards as TOOL_CALL_ARGS deltas addressed to the first.
Rendering both produced a phantom duplicate card in the transcript (in the
banking skin: a second "Open policy exception" with no transaction id or code)
plus a React "Encountered two children with the same key" warning, since the
call id is the render key in CopilotChatToolCallsView.
Verified against a local Intelligence stack that the server emits exactly ONE
TOOL_CALL_START for the affected id, so this is client-side state, not a stream
defect. React StrictMode is not involved (the demo disables it).
Extends the existing deduplicateMessages() — which already collapses duplicate
message ids from streaming re-delivery — to also collapse duplicate call ids
within a message, preferring whichever copy actually carries arguments. Applied
outside the merge branch too, because the duplicate also lands on a message that
was never itself duplicated. Returns the original array when there is nothing to
collapse, so memoized consumers do not re-render needlessly.
Does not change the underlying agent state, which still holds the duplicate;
that needs an idempotency guard in the AG-UI start handler.
5 regression tests, verified red before green. Full react-core suite: 1480 passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Summary
Adds a `react-version` matrix axis (**18**, **19**) to the unit-test
workflow so react-core, react-ui, and a2ui-renderer are exercised across
the full **supported peer range** (`^18 || ^19`), not just the
repo-default React 19.
This is a **reconstruction of the durable parts of #4221**
(@tylerslaton) onto current `main`. That PR went stale (~5,000 commits
behind, conflicting) and never landed. Rather than rebase it, this
rebuilds its design fresh — and deliberately **scopes to the supported
React range**: React 17 is dropped, because it is no longer a supported
peer version and carried ~80% of the original PR's complexity
(polyfills, `use-sync-external-store` source shims, `jsx-runtime`
aliases, a legacy `renderHook` fallback).
## What surfaced
Dropping R17 and validating R18 revealed a **latent React 18
incompatibility on current `main`**: the `window = {}` test pattern
crashes React 18's concurrent renderer with `"Should not already be
working."` mid-commit, which then corrupts the scheduler for the rest of
the file — **22 failures across 5 files** under React 18. (React 19
happens to tolerate the empty-window swap, so it was invisible until
now.)
The original PR fixed this but mislabeled it R17-only; it's actually
needed for R18, a *supported* version. So the matrix earned its keep on
day one.
Replacing `window = {}` with `stubWindowLocation()` is the load-bearing
fix — it resolves the crash cascade. Separately, **two** tests differ
under R18 purely in *render scheduling*, and are handled by narrow
version gates:
| Test | React 18 behavior | Why it's not a bug |
|---|---|---|
| `renderCustomMessages` → "executes multiple renderers in order" |
`executionOrder` is `["first", "first"]` | Renderer double-invoke.
`second` still never runs, which is the actual contract. |
| `use-human-in-the-loop` → `statusHistory` | `inProgress → executing →
inProgress → complete` | Transient backwards transition from extra
effect runs. Start, end, and the set of observed statuses are all still
correct. |
**No assertion tolerates a different state value.** An earlier revision
of this PR also relaxed the three-turn state-snapshot assertion to
accept `Turn: 2` on R18; @tylerslaton correctly flagged that as an
observable-behavior difference rather than a scheduling artifact.
Re-verified against a real 18.3.1 install — the strict `Turn: 3`
assertion passes **25/25** consecutive runs — so that gate was
unnecessary and has been removed (`a227f46a8`). The two gates above were
re-tested the same way and both genuinely reproduce.
## Changes
| File | What |
|---|---|
| `.github/workflows/test_unit.yml` | `react-version: ["18","19"]` axis.
R19 installs frozen; R18 overrides the root `pnpm.overrides` React
version and installs unfrozen. Adds a guard verifying the installed
React matches the matrix leg, and suffixes `NX_CI_EXECUTION_ID` with the
React version. Layered on top of the existing nx-affected selection
logic. |
| `test-helpers/stub-window-location.ts` *(new)* | Clears
`window.location` (so the localhost auto-open-inspector heuristic skips)
while keeping the real jsdom window — the safe replacement for `window =
{}`. |
| `use-agent-error-state`, `CopilotKitProvider.onError`,
`CopilotKitProvider.test` | Swap `window = {}` for
`stubWindowLocation()`. |
| `use-human-in-the-loop.e2e`, `renderCustomMessages.e2e` | Two
React-version-gated assertions, both **render-scheduling only** (see
table above). State assertions stay strict on every leg. |
No dependency or lockfile changes. None of the R17-only machinery from
#4221.
## CI cost
Full runs go from 3 legs (node 20/22/24) to **6** (node × react). On
PRs, nx-affected still scopes what actually builds/tests; the full 6×
only hits `workflow_dispatch` or when `test_unit.yml` itself changes (so
this PR runs all 6). This is the honest price of adding R18 coverage.
## Testing
Run locally in a worktree via the exact install-override logic the
workflow uses — `react`/`react-dom` → 18.3.1,
`@types/react`/`@types/react-dom` → `^18`, `@testing-library/react` →
`^14.3.1`, `streamdown>react` → 18.3.1, then `pnpm install
--no-frozen-lockfile`. Installed versions confirmed by resolving from
`packages/react-core` (18.3.1 / 19.2.3, `@testing-library/react` 14.3.1
on the R18 leg).
| Check | Result |
|---|---|
| react-core full suite @ React 18.3.1 | **117 files, 1433/1433
passing** ✓ |
| react-core full suite @ React 19.2.3 | **117 files, 1433/1433
passing** ✓ |
| Strict `Turn: 3` state-snapshot assertion @ R18, ×25 runs | **25 pass
/ 0 fail** — gate removed as unnecessary |
| `executionOrder` gate reverted to strict @ R18 | **fails**
(`['first','first']`) — gate justified |
| HITL `statusHistory` gate reverted to strict @ R18 | **fails** (extra
`inProgress`) — gate justified |
| `oxlint` (project-aware) | **0 warnings, 0 errors** — unchanged from
`main` |
| `oxfmt --check` | clean |
| Workflow YAML parse + lefthook commit hooks (lint-fix, package tests,
commitlint) | green |
Before the `window` fix, the R18 leg was **22 failing across 5 files**;
it is now fully green.
Credit to @tylerslaton for the original design in #4221, and for
catching the over-relaxed state assertion in review.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
ACTIVITY_SNAPSHOT replace updates keep the same message id while mutating
object content. The messages memo fingerprint used contentKey=0 for any
non-string/non-array content, so generative-UI and progress activity
renderers stayed on the first frame until some other message list change
forced a refresh.
Serialize object content in the fingerprint (multimodal attachments remain
on the array length branch) and add a regression test for same-id replaces.
The `completeOnMount` gate added in d70d48a561 named the `mcp-app-iframe`
testid, which only Angular's `copilot-mcp-apps-widget` declared. react-core
and vue build the sandbox iframe imperatively with no testid, so every
React/Vue integration timed the turn out at 30s with
`reason=surface-missing` and never reached `assertIframePresent` — whose
`iframe[sandbox]` fallback would have passed. D5 + D6 `mcp-apps` went red on
all 18 integrations that support the feature (first_failure_at 2026-07-28
23:03Z) while the demos rendered correctly by hand.
Fixed on both sides of the contract:
- react-core and vue now set `data-testid="mcp-app-iframe"` and a `title` on
the host-created iframe, matching Angular. Pinned by a test in each package.
- `completeOnMount` accepts CSS `selectors` alongside `testIds`, so the probe
settles on the same cascade its module doc and assertion already use
(`[data-testid="mcp-app-iframe"], iframe[sandbox]`). A comma-joined entry is
one conjunctive surface whose branches `querySelectorAll` unions, so the
delta/`minNewMounts` semantics are unchanged and `testIds` is now sugar for
the equivalent selector. This half greens the fleet on the next sweep
without waiting for a package release, since the integrations pin
@copilotkit/react-core 1.61.2.
A spec naming no surface now throws instead of burning the turn budget and
reporting a misleading `surface-missing`.
Verified against live staging: after clicking the pill, the old gate matched
0 elements and the cascade matched 1 (the sandboxed iframe was there all
along). Also recorded in showcase/GOTCHAS.md.
UseAgentProps left `agentId`, `runtimeAgentId`, and `threadId` independently
optional, so every unsafe or pointless combination compiled and only
`threadId`-without-`runtimeAgentId` was caught — at runtime, on render. Given
the bug this branch fixes was a silently-ignored `threadId` prop, the type is
where it should have been caught.
Split UseAgentProps into a base plus a two-branch union so exactly two shapes
are valid:
useAgent() // shared agent
useAgent({ agentId }) // shared agent
useAgent({ agentId, runtimeAgentId, threadId }) // private agent, pinned thread
Every partial combination is now a compile error, each for its own reason.
`threadId` alone would scope a thread onto a shared singleton (the original
review finding). `runtimeAgentId` without `threadId` registers a private agent
that behaves like the shared one, minus nothing but a registration and a local
id to keep unique. And without an explicit `agentId`, the proxy registers under
the chat configuration's agentId or DEFAULT_AGENT_ID — ids that already belong
to real agents — which either throws `already registered` or silently shadows
one, depending on whether runtime discovery has landed.
The runtime throws stay as backstops for callers TypeScript doesn't cover (plain
JS, `as any`, props widened to `string | undefined` at a call boundary), and now
cover all three cases, each failing before any registration happens.
Adds use-agent-types.test.tsx pinning the compile-time half of the contract
across the accept/reject matrix, plus contract tests for the two new throws. The
`@ts-expect-error` on each deliberately-bad call doubles as an assertion that the
type still rejects it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Realigns the inspector/memory work onto the banking demo as it shipped in
#6136 (ChatGPT-style shell, gen-UI beats, durable-memory self-learning) and
#6202 (README refresh).
All six conflicts were the same collision: this branch removes the bespoke
Glass Engine inspector, while #6136 kept and rebuilt around it.
- run-handler.ts: kept both sides (our CopilotKitCoreCatalogComponent and
main's MAX_FOLLOW_UP_DEPTH landed at the same spot).
- wrapper.tsx / layout.tsx: took main's rewritten provider tree and
right-hand icon rail, minus the Glass Engine providers, pane, and
telescope toggle. Also dropped main's `padClass` (it reserved space for
the Glass pane and referenced a now-removed `glassActive`) and
`<ProactiveNotice />` (main removed it; the import is already gone).
- memory-tab.tsx, lib/intelligence/memory.ts: confirmed the deletions.
Their only remaining importers were the bespoke inspector and the
banking-local /api/memories routes, all removed here. seed-memories.ts
is unaffected: it POSTs to INTELLIGENCE_API_URL, not the local route.
- README.md: kept our product-inspector section over main's Glass Engine
availability/activation prose, and documented the Capabilities tab.
Drive-by fixes to comment rot the migration created: user-id.ts and the
copilotkit route doc comments referenced the deleted Memory-panel proxies,
and the README pointed the presenter-reset control at the removed
telescope toggle.
Also replaces a literal NUL byte in capabilityKey() with a unicode escape.
The raw control character made tsc/grep/diff treat run-handler.ts as a
binary file, which hid this very merge's conflict markers from grep.
Behavior is unchanged.
Co-Authored-By: Claude <noreply@anthropic.com>
The harness's loosely-typed runtimeTransport string isn't assignable to
ProxiedCopilotRuntimeAgentConfig.transport (CopilotRuntimeTransport); leave it
at the "auto" default since these tests assert threadId/runtimeAgentId only.
Fixes the check-types failure.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Writing a per-hook threadId onto an agent resolved by agentId alone mutates a
shared singleton, so two useAgent calls that share an agentId clobber each
other's thread (review feedback from @mme). Require runtimeAgentId when threadId
is provided: the hook then registers a private proxied agent (agentId ->
runtimeAgentId via CopilotKitCore.registerProxiedAgent) and scopes the threadId
to that instance instead of a shared one. Register/unregister run as one
balanced, StrictMode-safe effect, exposing the proxy via state so the hook
swaps from the provisional stand-in deterministically. Passing threadId without
runtimeAgentId now throws. Updates the React Native demo to the new API.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CopilotPopup built its `chatView` override inside a `useMemo` keyed on
`width`/`height`. Consumers driving those props from a drag-to-resize
handle (committing new dimensions on mouseup) minted a new component
function per resize; rendering a new element type at that slot makes
React unmount and remount the whole chat subtree. The remount resets
scrollTop to 0, then `initial="smooth"` re-animates the message list
top-to-bottom on every resize, from any scroll position.
Give the override a stable module-scope identity and pass the popup
shell props (header, toggle, width, height, clickOutsideToClose,
defaultOpen) through React context. Resizing is now a plain style
update on CopilotPopupView with no remount, so scroll position holds.
Add a regression test asserting the chat subtree stays mounted (mount
count stays at 1) across width/height changes: it fails on the prior
code (one extra mount per resize) and passes with the fix.
## What does this PR do?
Provisional agents returned by `useAgent` now receive the provider's
`credentials` setting when they are created. Cached provisional agents
also refresh that setting when the provider configuration changes, while
keeping the same agent instance.
The regression test covers both the initial value and a later `include`
to `omit` update on the cached provisional agent.
### Verification
- `pnpm nx run @copilotkit/react-core:test`
- `pnpm nx run @copilotkit/react-core:check-types`
- `pnpm nx run @copilotkit/react-core:build`
- `pnpm check-format`
## Related PRs and Issues
- Closes https://github.com/CopilotKit/CopilotKit/issues/6116
## Checklist
- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [ ] If the PR changes or adds functionality, I have updated the
relevant documentation
- [x] "Allow edits by maintainers" is checked (lets us help iterate on
your PR directly — faster turnaround for everyone)
The React 18 leg accepted `Turn: 2` on the third-turn state snapshot, on the
theory that effect batching could freeze a stale snapshot in the renderer's
closure. Re-verified against a real 18.3.1 install: the strict assertion
passes 25/25 runs, so the tolerance was unnecessary and would have masked a
genuine stale-state regression on a supported React version.
The other two React 18 gates in this PR are kept — both reproduce and are
render-scheduling artifacts rather than observable state:
- renderCustomMessages executionOrder: ["first", "first"] (double invoke)
- HITL statusHistory: inProgress → executing → inProgress → complete
Verified: react-core 1433/1433 under React 18.3.1 and 1433/1433 under 19.2.3.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
## What does this PR do?
Fixes the mobile CopilotChat v2 textarea caret jump by avoiding repeated
destructive input measurements after textarea sizing measurements are
already
warm.
On mobile viewports, `evaluateLayout` always expands the input. It was
also
calling `ensureMeasurements()` on every evaluation, and that helper
temporarily assigns `textarea.value = ""` before restoring the value.
This can
move the browser selection to the end while typing in the middle of the
input.
`adjustTextareaHeight()` already lazily calls `ensureMeasurements()`
when the
measurement cache is empty, so the mobile branch can rely on that
existing path
without re-measuring on every keystroke.
This PR also adds a regression test that mocks the mobile viewport and
verifies
that warm mobile re-evaluation no longer assigns an empty string to the
textarea value.
## Related PRs and Issues
Fixes#4150
## Test plan
- [x] `corepack pnpm -C packages/react-core exec vitest run
src/v2/components/chat/__tests__/CopilotChatInput.test.tsx`
- [x] `corepack pnpm exec oxfmt --check
packages/react-core/src/v2/components/chat/CopilotChatInput.tsx
packages/react-core/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx`
- [x] `git diff --check`
Note: a normal pre-commit started the repo-wide `pnpm run test && pnpm
run check:packages`
hook and was terminated because it exceeded the scope needed for this
focused
fix. The commit was created with `--no-verify` after the targeted checks
above
passed.
## Checklist
- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation (not applicable; bug fix with regression test
only)
- [x] "Allow edits by maintainers" is checked (lets us help iterate on
your PR directly - faster turnaround for everyone)
UseAgentProps had no `threadId`, yet the shipped V2 React Native demo
calls `useAgent({ agentId: "default", threadId })`. Because the prop
didn't exist, it was silently dropped: the demo's thread state (and its
"New Chat" reset) never reached the agent, which ran under its own
auto-minted UUID. threadId was only ever sourced from a surrounding
`CopilotChatConfigurationProvider`, which the headless RN demo has none.
Accept an optional `threadId` on UseAgentProps and honor it. Resolution
precedence: an explicit `threadId` prop wins; otherwise fall back to the
chat configuration's threadId (gated on hasExplicitThreadId, as before).
When the prop is omitted, behavior is unchanged — fully backward
compatible. This makes the RN demo's existing usage work and unblocks
headless callers that have no chat-configuration provider in the tree.
Extends the threadId-propagation contract test with coverage for the
prop path (no provider, prop-over-config precedence, prop re-sync).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## What & why
`@copilotkit/react-core`'s `/v2` entry (and the root entry) re-exports
from a
single **monolithic shared chunk**, so importing *any* symbol — even one
hook —
pulls the built-in chat-message rendering stack (`streamdown` → shiki,
`mermaid`,
`cytoscape`, `katex`) into the consumer's bundle: **~3 MB gzip / ~15 MB
raw**,
with no way to tree-shake it. Consumers who build a fully custom chat UI
and only
use hooks pay the full cost. (Issue #4893.)
A separate lean build entry — `@copilotkit/react-core/v2/headless` —
already
ships those hooks in their own small chunk **without** that stack (it's
how
`@copilotkit/react-native` mounts CopilotKit). This PR makes it usable
for a
custom web UI.
Measured with esbuild (react/react-dom external):
| import | bundled JS |
|---|---|
| hooks from `@copilotkit/react-core/v2` | **~2.96 MB** gzip (≈
importing full `CopilotChat`) |
| same hooks from `@copilotkit/react-core/v2/headless` | **~0.03 MB**
gzip |
> Note on the report: `@copilotkit/a2ui-renderer` doesn't bundle the
rich-text
> stack (its deps are `@a2ui/web_core`, `lit`, `clsx`, `zod`). The
weight is
> entirely `streamdown` (shiki/mermaid/cytoscape) + `katex`, pulled by
the
> built-in `CopilotChat*` message components.
## Changes
- **`headless.ts`** — export `useCopilotKit` + `useRenderToolCall` (both
DOM-free
and rendering-stack-free); fix `UseAgentUpdate` (a runtime `enum`) being
re-exported via `export type`, which stripped its runtime value under
`isolatedModules` (so `useAgent`'s `updates` option was unusable from
headless
— a bug `tsc` can't catch). `useDefaultRenderTool` /
`useRenderCustomMessages`
/ `useRenderActivityMessage` stay in `/v2` (web-only markup or
`a2ui-renderer`).
- **Remove a `tailwind-merge` leak** — extract the tailwind-free ref
helpers
(`shallowEqual` / `useShallowStableRef`) into
`lib/shallow-stable-ref.ts` so the
headless graph no longer pulls `tailwind-merge` via
`CopilotChatConfigurationProvider`.
- **Test** — a small vitest export-surface test guarding the hook
surface and the
`UseAgentUpdate` runtime value.
## Verification
`react-core` typecheck + vitest (1427) and `react-native` typecheck
pass. Shipped
`dist/v2/headless.mjs` imports only `react`, `@ag-ui/client`,
`@copilotkit/core`,
`@copilotkit/shared`, `@copilotkit/react-core/v2/context`, `zod` — no
rendering
stack.
## Notes / follow-ups
- Headless hooks read a **different React context** than the prebuilt
`/v2`
`<CopilotKitProvider>`, so the two can't be mixed — mount a lean
provider over
`/v2/context` (as `@copilotkit/react-native` does). Making the `/v2`
provider
reuse the standalone `/v2/context` singleton would remove that sharp
edge and
is the natural follow-up.
- Existing bundle-size CI (`compressed-size-action`) already tracks
`headless.mjs`,
so no new size tooling is added here.
Addresses #4893.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
`CopilotChatInput`'s `AddMenuButton` renders its trigger as nested Radix
`asChild` slots:
```tsx
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>{button}</DropdownMenuTrigger>
</TooltipTrigger>
```
Radix `asChild` slots forward a ref to their child.
`DropdownMenuTrigger` and `TooltipTrigger` were plain function
components, so under React 18.3 the forwarded ref triggers this warning
on every render (reported in #5744):
```
Warning: Function components cannot be given refs. Attempts to access this ref will fail.
Did you mean to use React.forwardRef()?
```
## Change
Wrap `DropdownMenuTrigger` and `TooltipTrigger` in `React.forwardRef`,
forwarding the ref to the underlying Radix primitive — mirroring
`forwardRef` usage already present in this package (e.g. `Button`,
`CopilotChatInput.TextArea`). No behavior change; the ref now reaches a
DOM node and the warning is gone.
## Scope
Limited to the two primitives in the reported warning path. The other
shadcn/ui primitives in `react-core` follow the same React-19
(no-`forwardRef`) style and would warn identically when used as
`asChild` children under React 18.3 — happy to extend this to the rest
if you'd prefer full React 18.3 coverage.
## Verification
- `nx build @copilotkit/react-core` succeeds (compiles source +
generates type declarations).
- Pre-commit `test-and-check-packages` passes.
Fixes#5744
Add a `react-version` axis (18, 19) to the unit test workflow, spanning
the supported peer range (^18 || ^19) declared by react-core, react-ui,
and a2ui-renderer. React 19 installs against the committed lockfile;
React 18 overrides the root pnpm React version and installs unfrozen. A
guard step verifies the installed React matches the matrix leg.
Fixes a latent React 18 incompatibility the new matrix surfaces: the
`window = {}` test pattern crashes React 18's concurrent renderer with
"Should not already be working." mid-commit (22 failures across 5 files
on current main). Replace it with a `stubWindowLocation` helper that
clears `window.location` while keeping the real jsdom window intact. Add
React-version-gated assertions where R18 effect batching legitimately
differs from R19.
Reconstructs the durable parts of #4221 (Tyler Slaton) onto current
main, scoped to the supported React range — React 17 is dropped, as it
is no longer a supported peer version and carried the bulk of that PR's
complexity.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`useAgent` always returns a fully-constructed `AbstractAgent`: a provisional
stand-in while the runtime is still connecting (or in an error state), swapped
for the real agent once the `/info` sync resolves. The returned type claimed
`agent` was always the real agent, giving consumers no way to tell the two
apart — so one-time subscriptions (e.g. `onRunFinalized`) registered during the
provisional window landed on the placeholder and missed events until the effect
re-ran after the swap.
Add an `isReady` flag to the return value: `false` while the agent is
provisional, `true` once the real (or locally-registered) agent is bound.
Additive and backward compatible.
Also fix the docs' "Event Subscription" example, which used an empty
`useEffect` dependency array and therefore never re-subscribed when the agent
reference changed.
Note: the original crash from #5000 ("Cannot read properties of undefined
(reading 'subscribers')") no longer reproduces on `main` — the provisional-agent
work (#5533/#5635) guarantees a fully-constructed agent, so `subscribe()` is
always safe. The added tests lock in that no-crash behavior and cover the new
`isReady` transition.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The catalog-component registration effect read `rawCatalog.components.values()`
for any `.components`-shaped object, registering its components onto core so
they became toggleable in the inspector. But `filteredCatalog` only filters
genuine `Catalog` instances, passing a non-`Catalog` object through unfiltered.
This diverged: disabling such a component showed it disabled in the inspector
yet the model still saw/painted it, breaking the "disabled = invisible to
model" guarantee (and `.components.values()` could throw at mount if
`.components` isn't a Map).
Guard the registration effect on `rawCatalog instanceof Catalog`, mirroring the
`filteredCatalog` guard: a non-`Catalog` object now registers nothing (not
toggleable) and is not filtered — consistent, no divergence, no mount-throw.
Real catalogs from `createCatalog()` are `Catalog` instances and are unchanged.
Adds a covering test rendering the provider with a non-`Catalog` catalog object
carrying a component: asserts it does not throw at mount and registers nothing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes#5775.
## Problem
After #5099 scoped the streamdown markdown/table styles under
`[data-copilotkit] [data-streamdown="…"]`, the **table action controls**
(copy / download) are still unstyled for hosts that import
`@copilotkit/react-core/v2/styles.css` but don't also ship streamdown's
raw Tailwind utilities. streamdown renders the controls row, per-button
wrappers, trigger buttons, dropdown popovers and menu items with
unprefixed utilities (`flex`, `items-center`, `justify-end`, `gap-1`,
`cursor-pointer`, `p-1`, …) and **no stable `data-streamdown`
attribute**, so CopilotKit's packaged CSS didn't cover them — the
controls rendered as vertically stacked plain icons instead of a
right-aligned row.
## Fix
Add scoped fallback selectors under `[data-copilotkit]
[data-streamdown="table-wrapper"]`, targeting the controls chrome
**structurally** (since it has no `data-streamdown` hook):
- controls row → `> div:first-child:not(:last-child)` (flex,
right-aligned, gap)
- per-button wrapper → `… > div` (relative, positions the popover)
- trigger buttons → `… > div > button` (matches the code-block
copy/download button styling)
- dropdown popover → `… > div > div`
- popover menu items → `… > div > div > button`
The controls row is `table-wrapper`'s first child **only when controls
are enabled**; `:not(:last-child)` leaves a control-less table (whose
single child is the scroll container, already styled by #5099)
untouched.
## Verification
- **Compiles.** Built `globals.css` through the Tailwind v4 CLI — every
`@apply` resolves (e.g. `bg-background` → `var(--background)`,
`shadow-lg` → the shadow vars, `min-w-[120px]` → `min-width:120px`) and
all five rules emit with correct values.
- **Selectors match the real DOM.** A new DOM test
(`streamdown-table-controls.test.tsx`) renders a real `<Streamdown>`
table and asserts the controls row is `table-wrapper`'s first-non-only
child, carries no `data-streamdown` attribute, and contains the trigger
buttons under `> div > button` — i.e. the scoped selectors target real
elements. This also guards against streamdown markup drift.
- **Selector presence** guarded by `streamdown-styles.test.ts`
(whitespace-robust).
- Full `styles/__tests__` suite green; `oxlint`/`oxfmt` clean.
## Note (out of scope, discovered while fixing)
At runtime in streamdown `1.6.11` the `<table>` element is stamped
`data-streamdown="table-wrapper"` (not `"table"`): `MarkdownTable`
passes `data-streamdown="table-wrapper"` as a prop that leaks through
`...rest` onto the `<table>`, overriding the intended `"table"`. So
#5099's `[data-streamdown="table"]` selector currently matches nothing,
and the table also matches the `table-wrapper` rules. The controls fix
here is unaffected (its child-combinator selectors don't match the
table's `thead`/`tbody` children), but the `[data-streamdown="table"]`
selector is worth a separate follow-up / upstream report.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Addresses two P1s on #5940: the re-home path (a) injected unscoped frontend
tools/readable context into the background run — `buildFrontendTools`/
`getContextForAgent` include entries with no agentId, so the thread-A run
received thread-B's live context and could execute global frontend handlers
against B — and (b) lost continuity across multiple queued follow-ups (fresh
per-item proxy + enqueue-time snapshot, so run 2 never saw run 1's result).
Both stem from *running* the stale follow-up. Switch to skip-stale: when the
shared agent's threadId no longer matches the thread the follow-up was enqueued
for, drop it (with a warning) rather than run it against the now-foreground
thread. This removes the proxy/registerProxiedAgent machinery entirely and
resolves both P1s by construction. The MCP app still gets its ui/message ack at
enqueue time; only the optional agent turn on an abandoned thread is skipped.
Removes the re-home unit/integration tests; the e2e regression test (no
cross-thread run after a switch) and simplified unit tests cover the behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drives the real CopilotKitCore + RunHandler + ProxiedCopilotRuntimeAgent via
registerProxiedAgent against a mocked transport (mirrors
proxied-runtime-transport.test.ts). Asserts the re-homed run reaches the runtime
addressed to the ORIGINAL threadId (not the foreground one), carries the captured
message, runs on an isolated instance whose events never reach the shared agent,
and unregisters the transient proxy after. Closes the delegate/replay-lifecycle
gap the mocked-host unit tests couldn't reach — in CI, no live runtime needed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Queued MCP app ui/message follow-up work executed against whatever thread the
shared registry agent pointed at when the queue drained. If the host switched
threads while the follow-up was queued (agent busy), the run — and its streamed
events — leaked into the now-foreground thread.
Capture the thread context at enqueue and route the follow-up through
ɵrunMcpFollowUp: same thread runs live on the shared agent (unchanged); a
changed thread re-homes the run onto an isolated registerProxiedAgent sibling
pinned to the original thread (own event stream, persists + reconciles on
return); a changed thread on a non-runtime agent drops the follow-up rather
than leaking it.
Regression from 762370a4e5 (revert of per-thread activity-renderer clone
routing, #3630); uses the sanctioned registerProxiedAgent primitive (#4629)
instead of reintroducing implicit clones.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
After #5099 the table wrapper/cells are styled via [data-streamdown] selectors,
but the copy/download controls row, button wrappers, trigger buttons, dropdown
popovers and menu items render with raw Tailwind utilities and no stable
data-streamdown attribute — so hosts that import @copilotkit/react-core/v2
styles without shipping streamdown's own utilities saw them unstyled (icons
stacked vertically instead of a right-aligned row).
Add scoped fallback selectors under [data-copilotkit] [data-streamdown=
"table-wrapper"], targeting the controls chrome structurally. The controls row
is the first child ONLY when controls are enabled, so :not(:last-child) leaves a
control-less table (single child = the scroll container) untouched.
Verified against streamdown 1.6.11's actual rendered DOM (a new DOM test guards
that the structure the selectors assume still holds) and by compiling the CSS.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial review found the original provider tests passed even with the fix
removed: a single committed mount yields exactly one /info whether the ctor or
an effect fires it, and the ctor's fetch is several microtasks deep so ordering
can't distinguish it — only the multi-instance (discarded-render) case differs,
which Testing Library can't reproduce.
- Add CopilotKitProvider.deferWiring.test.tsx (mocked core): asserts the provider
constructs with `deferInitialConnection: true` and calls `connect()` from an
effect. This FAILS if the deferral wiring is dropped (verified).
- Keep the two real-core tests as normal-mount regression guards (one /info on
mount; idempotent under StrictMode) and document that the multi-instance proof
lives in core-defer-runtime-connection.test.ts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>