mirror of
https://github.com/CopilotKit/CopilotKit.git
synced 2026-09-14 16:26:20 +08:00
tyler/workflow-observer-example
565 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
50245e56de | test(react-core): correct feedback memoization coverage | ||
|
|
6c955686eb |
feat(react-core): expose AG-UI raw event to feedback callbacks (#6289)
## Summary React v2 feedback callbacks receive an assistant message without the trace metadata carried by the direct AG-UI event that created it. This slice exposes that metadata to thumbs callbacks without changing canonical messages or future run inputs. ## Root cause AG-UI keeps `rawEvent` on events while reducer-created assistant messages remain protocol-clean. `StateManager` sees the direct start event but previously discarded its correlation before `CopilotChatMessageView` forwarded the message to feedback callbacks. ## Changes - Store defined direct `TEXT_MESSAGE_START.rawEvent` metadata by agent, thread, and message. - Replace repeated scoped entries and prune them with message removal and lifecycle cleanup. - Return a cloned sidecar value through `CopilotKitCore.getRawEventForMessage`. - Enrich only thumbs-up and thumbs-down callback arguments at click time across flat and virtualized rendering. - Add production-path regressions and document the callback-only type. ## Out of scope Canonical messages, future `RunAgentInput.messages`, render props, message identity, stream ordering, snapshots, transformed chunks, persistence, GraphQL, legacy React, Vue, Angular, and standardized trace semantics remain outside this slice. ## Related PRs and Issues Addresses #3039. The callback-only scope follows https://github.com/CopilotKit/CopilotKit/issues/3039#issuecomment-5086936452. Related trace-correlation contract: #4634. ## Test plan - [x] StateManager sidecar tests, 10 passed. Covers direct capture, falsey values, replacement, scope isolation, cleanup, snapshots, and chunks. - [x] React v2 feedback tests, 4 passed. Covers real callback routing, canonical and outbound cleanliness, render identity, and flat/virtual paths. - [x] Full package suites, 625 core tests, 1,475 React Core tests, and 2 script tests passed. - [x] Typecheck, formatting, lint, and whitespace validation passed; lint reported five pre-existing warnings. - [ ] CI green (`static / quality`, `test / unit` on Node 20/22/24). ## Notes The clean-base behavioral half of the reproduction remains unproved because temporary worktree setup hung behind unrelated Git processes. The PR makes no base execution claim for that half. |
||
|
|
27431412e6 |
fix(react-core): stop the compat CopilotKit wrapper pinning useSingleEndpoint (#6605)
Refs [OSS-888](https://linear.app/copilotkit/issue/OSS-888). ## The failure A correctly assembled v2 integration 404s on its first browser request while every static check passes and `GET /info` returns 200. `packages/react-core/src/v2/index.ts:28` re-exports the **v1-compat** `CopilotKit` wrapper, so it is the provider most integrations reach for. That wrapper pinned: ```tsx useSingleEndpoint={props.useSingleEndpoint ?? true} ``` which overrode the core's `"auto"` negotiation and forced single-route transport. But **every** v2 handler defaults to `mode: "multi-route"` (`endpoints/hono.ts:95`; `createCopilotEndpoint` is an alias at `:90`). Nothing serves the single-route envelope the client sends, so the runtime 404s while the provider looks connected. ## What this is *not* The library defaults do not actually disagree. `CopilotKitProvider` (the real v2 provider) leaves the flag undefined → `"auto"`, which probes `GET /info` and falls back to the single-route envelope (`core/agent-registry.ts` `fetchRuntimeInfoAutoDetect`) — it works against **either** handler mode. Only the compat wrapper defeated that. So this is one line of override, not a defaults mismatch needing a direction chosen. ## Why four onboarding runs hit it, not one The library bug alone doesn't explain a 100% failure rate. The shipped `react-core` skill does: `packages/react-core/skills/react-core/references/provider-setup.md` — bundled in the npm tarball (`files: ["dist","skills"]`) — **mandated** the compat wrapper, **forbade** `CopilotKitProvider` as "a subset of the functionality", and mentioned `useSingleEndpoint` **zero times** across ~10 code samples. An agent following it wrote the 404 configuration every time. Meanwhile `skills/copilotkit-setup/SKILL.md` got it right, so the two shipped skills contradicted each other and nothing gated either against the code. ## The change **Commit 1 — the library fix.** The prop already arrives through `v2Props`, so dropping the override lets it stay `undefined` and inherit `"auto"`. An explicit `useSingleEndpoint` still wins in both directions. **Commit 2 — the docs and skills.** Correcting the default made ~15 pages' explanations false. Code samples that pass `{false}` stay valid (they pin what negotiation would find anyway), so this corrects the *explanations* rather than the samples — keeping every page true both before and after release. Includes dropping the now-false causal claim from the single-route-envelope diagnostic added in #6579. ## Compatibility Safe for existing v1 apps. A v1 app on a single-route-only handler (`copilotRuntimeNextJSAppRouterEndpoint` and friends) now does one `GET /info` that 404s, then falls back to single-route and works. Cost is one extra request on connect. One edge case worth a reviewer's eye: if a deployment's `runtimeUrl` + `/info` returns 200 from something that is *not* a multi-route CopilotKit runtime (a catch-all proxy serving HTML, say), `"auto"` would resolve to `rest`. Setting `useSingleEndpoint` explicitly remains the escape hatch. Conventional-commit note: this lands as `fix`, but it *does* change a public default. Flag if you'd rather it carried a minor bump. ## Tests - New `copilotkit-transport-default.test.tsx` — omitted → `"auto"`, `{true}` → `"single"`, `{false}` → `"rest"`. Confirmed RED first (`expected 'single' to be 'auto'`). - `CopilotChat.readinessGate.test.tsx` depended on the old default to avoid a REST probe. Single-route transport is a **precondition of that fixture**, not the behaviour under test, so it now pins the flag explicitly and its stale comments are corrected. Its coverage (readiness gate across the real SSE boundary) is unchanged. - `react-core` 1512 passed · `runtime` 2073 passed · `core` 668 passed. - `pnpm check:plugin-skills` in sync (`skills/react-core/` is the generated mirror). `showcase/shell-docs`'s own vitest suite fails to load 35 files with `Cannot find package 'react/jsx-dev-runtime'` — reproduced identically on unmodified `origin/main`, so it is environmental in this checkout and unrelated. All 183 tests that do run pass. ## Not addressed here Nothing gates a shipped skill against the code it documents, which is why `provider-setup.md` could contradict both the library and the sibling skill indefinitely. Worth its own ticket. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
b8b19834a2 |
fix(runtime): unify the Intelligence key name and publish the wiring (refs OSS-881) (#6595)
## What does this PR do? Closes the naming and documentation half of [OSS-881](https://linear.app/copilotkit/issue/OSS-881). Paired with CopilotKit/Intelligence#890, which adds `copilotkit verify` and tightens the evaluation rubric. ### 1. One name for the Intelligence key **Three** names for one value were live in CopilotKit's own documentation, and following the wrong one with a CLI-provisioned project yields an undefined key: | Name | Where | Code readers | | --- | --- | --- | | `INTELLIGENCE_API_KEY` | what `copilotkit project select` writes; all 34 integration examples; the docs site | 34 | | `COPILOTKIT_INTELLIGENCE_API_KEY` | 7 Channels package READMEs + packaged skills | **0** | | `COPILOTKIT_API_KEY` | `examples/slack`, `examples/teams`, and the TSDoc on `CopilotKitIntelligence` itself | 2 | `INTELLIGENCE_API_KEY` wins — it is the name the CLI provisions, and changing it would break every scaffolded project in the wild. - `COPILOTKIT_INTELLIGENCE_API_KEY` is **retired outright**. Nothing ever read it, so there is nothing to keep compatible. - `COPILOTKIT_API_KEY` stays **readable as a deprecated alias** in the two examples that consume it, so an existing `.env` keeps working, and is documented as deprecated everywhere it appears. The third name was the worst placed: it was in the TSDoc on `CopilotKitIntelligence`, which is what an IDE shows on hover. This was not only untidy. The CLI's own `channels-preflight` accepts `INTELLIGENCE_API_KEY` or `COPILOTKIT_API_KEY` — **not** `COPILOTKIT_INTELLIGENCE_API_KEY`, the name the Channels READMEs told people to set. So following a Channels README verbatim made `copilotkit channels` warn that no runtime API key was present while the key sat visibly in `.env`. After this PR the documented name is one preflight accepts. > [!NOTE] > `NEXT_PUBLIC_COPILOTKIT_API_KEY` is a **different value** — the legacy Copilot Cloud public key — and is deliberately left alone. ### 2. A real defect, not just naming skew `skills/runtime/references/intelligence-mode.md` documented `organizationId` as a `CopilotKitIntelligence` option, sourced from two further env names (`COPILOTKIT_INTELLIGENCE_ORG_ID`, `COPILOTKIT_ORG_ID`). `CopilotKitIntelligenceConfig` has no such field — the copy-pasteable sample it appeared in **would not compile**. Removed from the samples, and the prose telling readers to fetch a value for it corrected. That file is the only place those two names ever existed, which is very likely why the failing validation run reported that "the runtime reads `COPILOTKIT_INTELLIGENCE_API_KEY` and `COPILOTKIT_INTELLIGENCE_ORG_ID`". ### 3. Publish the Intelligence wiring The wiring instructions existed only inside `node_modules/@copilotkit/runtime/skills/`, and the only docs pages mentioning `CopilotKitIntelligence` at all were the two Channels frontends — so a developer on the plain web path had no page to reach it from. Adds **`/premium/connect-your-runtime`**: the wiring itself, how to confirm the credential is actually consumed, the self-hosted both-URLs-or-neither rule, and a troubleshooting table. Linked into both navs, and the skills reference now points at the published page. ### 4. A guard so it cannot drift back `scripts/validate-intelligence-env-names.ts` (`pnpm check:intelligence-env-names`), wired to lefthook and a new workflow. The workflow is **intentionally unfiltered**. The two workflows that would otherwise cover this both filter: `plugin-skills-check` by `paths:`, and `static/quality` by `paths-ignore: examples/**` — which is exactly where the deprecated alias lives. Scoping the job would re-open the hole it exists to close. Legitimate alias sites live in `ALIAS_ALLOWLIST`. ## Related PRs and Issues - [OSS-881](https://linear.app/copilotkit/issue/OSS-881) — needs **both** PRs; neither closes it alone - CopilotKit/Intelligence#890 — items 1 and 4 (`copilotkit verify` + rubric contract 1.3.0) ## Verification - Full lefthook pre-commit ran green: `check-plugin-skills`, `lint-fix`, the new `check-intelligence-env-names`, and `test`/`publint`/`attw` across **25 projects**. - `examples/slack` `managed.test.ts` extended to cover **both** the canonical name and the alias fallback, and proven non-vacuous — removing the fallback turns the new test red. - The drift guard proven non-vacuous the same way: reintroducing a retired name fails it, exit 1. - `oxfmt` and `oxlint` clean on every file touched (0 errors). ## 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 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
f30d3bfae5 |
docs: correct the useSingleEndpoint default across docs and shipped skills
The compat `<CopilotKit>` wrapper no longer pins `useSingleEndpoint` to `true`,
so every statement that it "defaults to single-route" or that a multi-route
backend "needs `{false}`" is now wrong. Code samples that pass `{false}`
explicitly stay valid — they pin what negotiation would find anyway — so this
corrects the explanations rather than the samples, keeping the pages true both
before and after the release.
The shipped `react-core` skill is the load-bearing one. `provider-setup.md`
mandated the wrapper, forbade `CopilotKitProvider` as "a subset of the
functionality", and never mentioned `useSingleEndpoint` across ~10 samples — so
an agent following it wrote the 404 configuration every time. It now documents
the transport and stops steering readers off the negotiating provider.
Also drops the false causal claim from the runtime's single-route-envelope
diagnostic (added in #6579), which named the wrapper's old default as the cause.
`skills/react-core/` is the generated mirror of `packages/react-core/skills/`,
synced with `pnpm sync:plugin-skills`.
Refs OSS-888.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
dc73af1dc4 |
fix(react-core): stop the compat CopilotKit wrapper pinning useSingleEndpoint
The `CopilotKit` wrapper is re-exported from `@copilotkit/react-core/v2`
(`v2/index.ts`), so it is the provider most integrations reach for. It pinned
`useSingleEndpoint={props.useSingleEndpoint ?? true}`, which overrode the core's
`"auto"` negotiation and forced single-route transport.
Every v2 handler defaults to `mode: "multi-route"`, so the pinned default made
the first browser request 404 while `GET /info` still returned 200 and looked
healthy. Four independent onboarding runs hit it and all four fixed it the same
way, with `useSingleEndpoint={false}`.
The prop already arrives through `v2Props`, so dropping the override lets it
stay `undefined` and resolve to `"auto"` — probe `GET /info`, fall back to the
single-route envelope — which works against either handler mode. An explicit
`useSingleEndpoint` still wins in both directions.
`CopilotChat.readinessGate.test.tsx` depended on the old default to avoid a REST
probe. Single-route transport is a precondition of that fixture rather than the
behaviour under test, so it now pins the flag explicitly and its comments are
corrected.
Refs OSS-888.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
aa3fb29dce | chore: release monorepo v1.68.3 | ||
|
|
b0233c4eb0 | chore: release monorepo v1.68.2 | ||
|
|
bef2c440ba |
fix(react-core): gate CopilotChat submission on runtime readiness (empty assistant response) (#6576)
## Problem Fleet-wide "empty assistant response": the assistant-message container mounts but never receives text. #5801 (first released 1.63.0) deferred the runtime `/info` call to a React effect, widening the "provisional agent" window; 1.63.2 exposed an `isReady` signal on `useAgent` but `CopilotChat` never consumed it. A chat submitted during the provisional window is committed to the provisional agent and then lost when `/info` swaps in the real agent — the user message and streamed assistant text disappear, so the assistant bubble renders empty. This was confirmed with a controlled SSE A/B: stock 1.68.1 does forward `TEXT_MESSAGE_START → TEXT_MESSAGE_CONTENT → TEXT_MESSAGE_END` (the runtime is fine — not the in-memory runner / #5837), but the `/info` agent swap drops the rendered messages; restoring a readiness guard makes the identical SSE render correctly. ## Fix `CopilotChat` now consumes `isReady` from `useAgent` and withholds `onSubmitMessage` until the runtime is ready: ``` - const { agent } = useAgent({ ... }); + const { agent, isReady } = useAgent({ ... }); ... - onSubmitMessage: onSubmitInput, + onSubmitMessage: isReady ? onSubmitInput : undefined, ``` `CopilotChatInput` already derives `canSend` (and its Enter handler) from `onSubmitMessage`, so withholding it while not-ready (a) disables the send control and (b) makes Enter a no-op that **preserves** the composer text — the message can't be committed to the doomed provisional agent. No runtime/runner changes; no fixture re-recording. ## Red–green proof New test `CopilotChat.readinessGate.test.tsx` drives the real readiness race against the real `CopilotChat` submit path: holds the runtime in Connecting (deferred `/info`), sends during the provisional window, then resolves `/info` (the real status-change re-render that flips `isReady`) and asserts the message survives to render an assistant response. - **RED** (fix reverted): the chat body contains only chrome text — no user message, no assistant response (the empty-container symptom). - **GREEN** (fix applied): assistant text renders; passes 3× consecutively (deterministic). - Mutation-verified: reverting the fix reproduces RED. ## Verification - react-core: **1468 tests pass** (0 regressions; 3 pre-existing web-inspector `localStorage` jsdom-env file errors are unrelated and present with and without this change). - react-ui: **69 tests pass**. - react-core typecheck (`tsc --noEmit`): **0 errors**. ## Follow-up (not in this PR) The showcase D4 probe driver (`showcase/harness/src/probes/drivers/d4-chat-roundtrip.ts`) should wait for the send control to be enabled before pressing Enter (poll `[data-testid="copilot-send-button"]` `disabled === false` after typing). Omitted here because it can't be red-green'd without a live showcase backend. Note this fix makes the follow-up more relevant: with send gated, a probe that types + Enters during the provisional window now silently no-ops. Ref: #5801 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017t7HsmM31NHNmUQrHF47pm |
||
|
|
e34bdb9fc3 |
fix(react-core): hide suggestion pills until ready; rework SSE test to real wrapper
Withholding onSelectSuggestion left the pill visually enabled but inert, silently dropping a click during the provisional (!isReady) window. Hide the pills until isReady (pass an empty suggestions list so the view's existing hasSuggestions gate keeps them off-screen); retain the handler gate as defense-in-depth for custom chatView slots. The suggestion-gate test now asserts the pill is absent while provisional and appears/works once ready. Rework the production-shaped SSE regression to render the real public CopilotKit wrapper (components/copilot-provider/copilotkit) with runtimeUrl + agent="agentic_chat", advertising agentic_chat in the mocked single-endpoint info response and relying on the wrapper's default useSingleEndpoint=true — removing the synthetic GET /info 404 fallback so the test exercises the same provider chain and POST info/agent-run path as Showcase. |
||
|
|
6f58b2c6a4 |
fix(runtime): unify the Intelligence key name and publish the wiring (refs OSS-881)
Three names for one value were live in CopilotKit's own documentation, and following the wrong one with a CLI-provisioned project yields an undefined key: - `INTELLIGENCE_API_KEY` — what `copilotkit project select` writes, used by all 34 integration examples and the docs site. - `COPILOTKIT_INTELLIGENCE_API_KEY` — the seven Channels package READMEs and the packaged skills. Nothing ever read it. - `COPILOTKIT_API_KEY` — the Slack and Teams examples, and the TSDoc on `CopilotKitIntelligence` itself, which is what an IDE shows on hover. `INTELLIGENCE_API_KEY` wins, because it is the name the CLI provisions and changing it would break every scaffolded project in the wild. `COPILOTKIT_INTELLIGENCE_API_KEY` is retired outright — no code read it. `COPILOTKIT_API_KEY` stays readable as a deprecated alias in the two examples that consume it, so an existing `.env` keeps working, and is documented as deprecated everywhere it appears. The skills reference also documented `organizationId`, sourced from a fourth and fifth env name, as a `CopilotKitIntelligence` option. It is not one: `CopilotKitIntelligenceConfig` has no such field, so the copy-pasteable sample it appeared in would not compile. Removed from the samples, and the prose that told readers to fetch a value for it corrected. The Intelligence wiring itself was published only inside `node_modules/@copilotkit/runtime/skills/`, and the only docs pages showing `CopilotKitIntelligence` were the two Channels frontends — so a developer on the plain web path had no page to reach it from. Adds `/premium/connect-your-runtime`, which covers the wiring, how to confirm the credential is actually consumed, and the self-hosted two-URL rule. `scripts/validate-intelligence-env-names.ts` keeps this from drifting back. It runs unfiltered in CI on purpose: the two workflows that would otherwise cover it filter paths, and static/quality ignores `examples/**` — exactly where the deprecated alias lives. |
||
|
|
f8160e09b9 | fix(react-core): gate suggestion submission on readiness + production-shaped SSE regression test | ||
|
|
9b0e3dc88a | fix(react-core): gate CopilotChat submission on runtime readiness (empty assistant response) | ||
|
|
f1b26e4aa8 | fix(react-core): hide local inspector action in production | ||
|
|
367e7bda15 | feat(react-core): add local message inspector links | ||
|
|
ffd15801d6 |
fix(react-core): re-render consumers when the agent node changes
useAgentNodeName tracked the current node in a ref and returned nodeNameRef.current. Mutating a ref schedules no render, so a component reading useCoAgent().nodeName kept showing whichever node was current at its last render and never updated on its own -- it only appeared to work when something unrelated happened to re-render it. Backing the value with state fixes that. Adds the coverage the hook never had: transitions, run-start reset, run-error, and unsubscribe on unmount. Note this does NOT explain GH #1426 (interrupt agentMetadata.nodeName reporting the previous node). useInterrupt only evaluates the `enabled` predicate from a useEffect/useMemo keyed on its `pending` state, so the interrupt's own state update always forces a render before the predicate runs -- and the pre-fix code reads the ref correctly at that point. That report needs a different explanation; it is left open. Refs #1426 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1f9b60b231 | chore: release monorepo v1.68.1 | ||
|
|
e6864b6bdd | chore: release monorepo v1.68.0 | ||
|
|
eb3f430ae1 | feat(runtime): mark Learning config experimental | ||
|
|
99da13de53 | fix(runtime): preserve Learning compatibility contracts | ||
|
|
a9f283ab55 | feat(runtime): assign threads to Learning Containers | ||
|
|
44d54c65d6 |
fix(react-core): repair useCopilotReadable effect deps, convert args, and dependencies (#6409)
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:
|
||
|
|
47ad5e34a3 |
refactor(react-native)!: converge tool-call rendering onto CopilotKit's shared registry (#6438)
## 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)
|
||
|
|
4b17ea7d35 |
fix(scripts): tokenize before hunting loader calls in the purity gate
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> |
||
|
|
1b39c12e36 |
fix(scripts): stop the headless CLI gates skipping themselves on odd paths
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> |
||
|
|
01c7283210 | fix(core): prevent duplicate interrupt tool results (#6201) | ||
|
|
528dea6483 |
fix(react-core): ship a single v2 context instance (#6440)
## 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
|
||
|
|
04c4198a14 |
docs: fix Copilot Runtime reference links (#5296)
## 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) |
||
|
|
6540848745 | chore: refresh agent artifacts for 1.67.1 | ||
|
|
1853a24d00 | fix(skills): align public guidance with current APIs | ||
|
|
b4cfcf6f98 |
fix(react-core): stop the purity gate crashing opaquely on the declared Node floor
`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>
|
||
|
|
e7f3d7644d |
fix(react-core): make the #4893 purity gate scan the graph it claimed to scan
`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> |
||
|
|
5a6bf1dc2b |
test(react-core): make the headless type guard actually detect type drift
`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>
|
||
|
|
10d8f43829 | chore: release monorepo v1.67.1 | ||
|
|
48312f4d65 | chore: release monorepo v1.67.0 | ||
|
|
7e563b97c7 |
test(react-core): reproduce the duplicated v2 context end-to-end
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> |
||
|
|
35502a5d07 | Merge branch 'main' into fix/react-core-activity-contentkey-memo | ||
|
|
c738b7f640 |
fix(react-core): ship a single v2 context instance
`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
|
||
|
|
f4031f3a62 |
fix(rn): extend /v2/context purity guard and document render closure-staleness
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> |
||
|
|
68a30c2535 | test(react-core): hard-fail if the /v2/headless chunk links the render stack (#4893) | ||
|
|
d5ace8b2ce | feat(react-core): export ReactToolCallRenderer from the /v2/headless entry | ||
|
|
ec06d87fb4 |
fix(react-core): repair useCopilotReadable effect deps, convert args, and dependencies
Four defects in `useCopilotReadable` were introduced together in
|
||
|
|
b40602e698 | chore: release monorepo v1.66.4 | ||
|
|
cfc5cfe727 | chore: release monorepo v1.66.3 | ||
|
|
6188404f3a |
test(react-core): assert the legacy HITL follow-up goal, not the wire id
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> |
||
|
|
eee580f6da |
fix(react-core): collapse duplicate tool-call ids in the message view
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> |
||
|
|
55aaad21a6 |
test(react-core): add React 18 + 19 unit-test CI matrix (#6060)
## 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)
|
||
|
|
7e502dd4a4 |
fix(react-core): preserve logical run identity across HITL resolve (#6296)
## 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
|
||
|
|
12a55f3c46 | Merge branch 'main' into fix/react-core-activity-contentkey-memo | ||
|
|
53b772552f | chore: release monorepo v1.66.2 |