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)
The #4893 hard-fail gate's loader-call detector gave WRONG VERDICTS IN BOTH
DIRECTIONS. It layered two regexes — a comment/string/template alternation that
blanked only the comment branch, and `\b(?:import|require(?:\.resolve)?)\s*\(`
over the result — then classified an argument as static from the FIRST CHARACTER
after the paren. All nine shapes below were reproduced against the real gate
before the rewrite:
false FAIL throw new Error("use require(path) instead")
false FAIL `import(${x})` inside a template
false FAIL o.import(y) / mod.require(x) (member calls, not loaders)
false PASS /https:\/\//; …import(n) (the regex's `//` blanked the
rest of the line, hiding a
real dynamic call)
false PASS import(`stream${n}`) (merely STARTS with a quote)
false PASS import("zo" + n) (same)
false PASS import(`${base}/v2/index.mjs`) (same — the fat entry)
false PASS __require(name) (no \b inside `__require`)
Replaced with `scanSource`, a single-pass tokenizer that classifies every
character as code / comment / string / template / regex and returns a
length-preserving masked view plus a literal-span list. The one surviving regex
now only ever sees code, so import-shaped TEXT cannot reach it at all; an
argument counts as static only when it is one COMPLETE literal with no
concatenation or interpolation; `__require` is matched; and a member call is
rejected both by lookbehind and by a whitespace-skipping back-scan (so
`m\n .import(x)` is not a loader either).
Proven in both directions: nine innocent/violation pairs run through the real
`assertEntryPurity`, each innocent form CLEAN and each matching real violation
FAIL. Re-proved end-to-end by prepending `import "streamdown"` to the real
dist/v2/headless.mjs — exit 1 naming all five families — then restoring it
byte-identically. On the untouched dist the scan sees 66 loader calls in the
`.cjs` graph and classifies all 66 static, so it passes because it LOOKED.
Also adds the first `.cjs` fixtures: every existing fixture was `.mjs`, leaving
the script's `format: "cjs"` branch and the `require()` shape asserted by
nothing. Tests 24 → 47.
`stripComments` is renamed `maskNonCode`, since it now blanks literals and
regexes too; it had no caller outside this script and its test. The RN guard
keeps its own copy, untouched.
dev-docs/bundle-size.md: the four holes a sibling agent documented as known
limitations this round are closed and removed from that list; what genuinely
remains (regex-vs-division heuristic, no JSX/TS, indirect loaders) replaces them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both scripts decided "am I the entrypoint?" by comparing `import.meta.url` to a
`file://`-concatenated `process.argv[1]`. `import.meta.url` is percent-encoded
and symlink-resolved; raw argv[1] is neither. So the comparison was false for
any checkout path containing a space, for any invocation through a symlink
(macOS /tmp is one), and on Windows — and a false guard skipped the whole CLI
block. Reproduced before fixing: the #4893 purity gate and the bundle-size
measurement both exited 0 having printed nothing and asserted nothing, which is
worse than a gate with holes because it manufactures confidence. The guard was
added by this PR so the modules could export internals to their new negative
tests; making the gates testable introduced a way for them not to run.
Both now compare real filesystem paths through an exported `isEntrypoint`:
`fileURLToPath` defeats the encoding and Windows forms, `fs.realpathSync` on
both sides defeats symlinks, and a `path.resolve` fallback keeps a nonexistent
argv[1] from throwing.
Each `node --test` suite gains five entry-guard tests, including an end-to-end
spawn of the real script through a symlinked package-root alias whose name
contains a space — the only case that catches the call site regressing back to
a string comparison (verified: it fails against the old expression). The unit
cases assert the naive comparison really would have failed, so none of them can
pass vacuously. Both negative gates were re-proven to still bite: a doctored
dist entry pulling streamdown fails the purity gate, and a stubbed dist entry
trips the measurement's plausibility floor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
## 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)
`assert-headless-purity.mjs` resolved its dist directory with
`import.meta.dirname`, which landed in Node 20.11 and is `undefined` below it.
The root package.json declares `engines: { "node": ">=18" }`, so a contributor
or runner on Node 18 hit this hard-fail CI gate as:
TypeError [ERR_INVALID_ARG_TYPE]: The "paths[0]" argument must be of type
string. Received undefined
at Object.resolve (node:path:1115:7)
at .../scripts/assert-headless-purity.mjs:71:19
— a stack trace into node internals, at module load, that names neither the
gate nor the real problem. Reproduced against a real Node 18.20.8.
Switch to `path.dirname(fileURLToPath(import.meta.url))`, which both sibling
scripts in this CI job already use (react-core's measure-copilotchat.mjs and
react-native's measure-headless.mjs), so all three read the same and none of
them carries a hidden runtime floor its own package does not declare.
Verified under real Node 18.20.8: the script now walks all four entries (650 /
646 / 649 / 645 modules) and exits 0, and still exits 1 with the full
`links the heavy render stack` report when a forbidden dep is injected into a
dist entry. The metafile-driven graph walk, the loud failure on unresolvable
edges and all 17 negative tests are untouched (`test:scripts`: 19 pass).
Skill-staleness check (reskinnable-demo CLAUDE.md rule): not applicable — this
touches packages/react-core, nothing under .claude/skills/reskin/.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`scripts/assert-headless-purity.mjs` is a hard-fail CI gate, and it did not do
what its header said. It read four built entry files and asked
`code.includes(dep)`. That is weaker than the claim in both directions, and every
item below was reproduced against a real build before this rewrite:
1. It never followed an edge out of those four files. Re-exporting one hook from
the fat `@copilotkit/react-core/v2` entry — which links shiki, mermaid,
cytoscape, katex and streamdown — left `dist/v2/headless.mjs` importing that
entry by name, and the gate printed "clean" for all four files, exit 0. Same
for a heavy dep reached through `@copilotkit/core`, which is external to this
build: the entry says only `from "@copilotkit/core"` and there is nothing to
grep. A split-out relative chunk escaped identically.
2. The header claimed the check "follows into node_modules". It followed nothing
— not node_modules, not a relative sibling chunk.
3. `code.includes(dep)` is unanchored, so it matched comments and strings. Not
hypothetical in either direction: the built artifact is comment-PRESERVING
(233 lines of block comments survive in dist/v2/headless.mjs), and the five
banned tokens sit in `src/v2/headless.ts`'s own banner. They are absent from
dist only because that module is a re-export shell whose banner attaches to no
retained code — moving the same sentence into a module that ships code
hard-failed CI on all five tokens while linking none of them.
The gate now drives esbuild with `metafile: true` over each built entry and
matches on the RESOLVED graph, so it follows relative chunk edges and into
node_modules for real, resolves `exports` maps, subpaths and pnpm symlinks, and
cannot be fooled or tripped by a comment. Matching is anchored at the package
name (`@shikijs/langs` and `cytoscape-fcose` count; `shikimori` does not) and
also covers a forbidden dep left external, which resolves to no graph input at
all. Unresolvable edges FAIL LOUDLY instead of reading as clean, as does a graph
that does not contain its own entry.
One edge shape survives a bundler: `import(name)` with a non-literal argument,
which esbuild leaves alone without even warning. For that the gate reads text —
the only place it does — over the graph's first-party files, using the
`stripComments` helper ported from the sibling RN guard so a documented
counter-example cannot trip it.
Adds `scripts/__tests__/assert-headless-purity.test.mjs` (17 tests, wired into
`test:scripts` next to measure-copilotchat's), because a hard-fail gate with no
coverage of its own failure mode is how this shipped. Proven after the fix: both
false negatives now exit 1, a clean build exits 0, and a banned token that
appears only in a comment exits 0.
esbuild is already this package's devDependency and already runs in the same CI
job, so the gate needs no workflow change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`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)
## Summary
Preserve the logical run ID when a legacy `useCopilotAction({
renderAndWaitForResponse })` frontend tool resolves and
`processAgentResult` starts its recursive follow-up.
The run handler binds each internal continuation handoff to the exact
follow-up invocation, cancels it when setup fails or no run starts, and
keeps the handoff out of the public `CopilotKitCore.runAgent` contract.
The public regression drives the legacy hook through its
`useHumanInTheLoop` and `useFrontendTool` path, renders the approval
control, resolves it, and verifies both agent calls use the same
generated ID.
Closes https://github.com/CopilotKit/CopilotKit/issues/3456
## Changes
- Preserve the originating ID across recursive frontend-tool follow-up
runs
- Keep the existing legacy HITL registration and response behavior
unchanged
- Add core follow-up coverage and a public `useCopilotAction` regression
- Retain the existing standard/legacy interrupt and StateManager
coverage from the earlier fix
## Test plan
- [x] `pnpm -C packages/react-core exec vitest run
src/hooks/__tests__/use-copilot-action.e2e.test.tsx`
- [x] `pnpm -C packages/core exec vitest run
src/__tests__/core-follow-up.test.ts`
- [x] `pnpm -C packages/react-core exec vitest run
src/v2/hooks/__tests__/use-interrupt.test.tsx`
- [x] `pnpm -C packages/core exec vitest run
src/__tests__/state-manager.test.ts`, 39 tests passed
- [x] `pnpm -C packages/react-core exec vitest run`, 123 files and 1475
tests passed
- [x] `pnpm -C packages/core exec vitest run`, 58 files and 625 tests
passed
- [x] `pnpm -C packages/core run check-types`
- [x] `pnpm -C packages/react-core run check-types`
- [x] `pnpm exec oxfmt --check` on all eight changed source/test files
- [x] `pnpm exec oxlint` on all eight changed source/test files, 5
pre-existing warnings and 0 errors
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>
`CopilotKitIntelligence` required `apiUrl` and `wsUrl` on every construction,
so the two correct hosts had to be found and copied by hand — which is how an
agent came to invent them. Both now default to CopilotKit's managed platform,
making `new CopilotKitIntelligence({ apiKey })` the whole managed-service setup.
Overrides are unchanged for self-hosted and non-production deployments, with two
guards that the previous required-field signature made unnecessary:
- A blank value counts as unset. These URLs are usually wired from env vars, and
a declared-but-empty variable arrives as `""`, which would otherwise produce
host-relative requests instead of falling back to the managed platform.
- Setting only one of the pair warns. The API and realtime planes are separate
hosts, so a lone override silently splits the client across two deployments —
and that failure surfaces as a hang, not an error.
Sweeps the doc, skill, README, and example surfaces to the short form so the
copy-paste path no longer hands anyone URLs to get wrong, and reattaches the
`CopilotKitIntelligence` class JSDoc, which was orphaned above an interface and
so never appeared on hover.
Linear: OSS-638
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)