## What
Issue #3388 asked for a way to put a card into the chat transcript from
frontend code, without a tool call and without adding to the
conversation the model reads.
**That already ships.** A message with `role: "activity"` renders
standalone in the transcript, and `AbstractAgent.prepareRunAgentInput`
strips every activity message from the run payload:
```js
prepareRunAgentInput(e) {
let t = structuredClone_(this.messages).filter(e => e.role !== `activity`);
...
}
```
The gap was documentation. `renderActivityMessages` is only documented
for **backend-emitted** activities (mastra background-tasks, a2a,
mcp-apps), so the frontend-driven path was undiscoverable.
This PR adds the missing guide page and a test that pins the behavior.
## Changes
| File | Why |
| --- | --- |
| `showcase/shell-docs/.../generative-ui/frontend-cards.mdx` | New
"Frontend-Driven Cards" guide |
| `showcase/shell-docs/.../generative-ui/meta.json` | Sidebar entry
(6-line insertion) |
| `packages/react-core/.../CopilotChatFrontendActivityCard.e2e.test.tsx`
| Pins both halves of the contract |
No source changes. Behavior is unchanged; this documents and locks what
already works.
## The non-obvious part
The card must be added via the agent returned by `useAgent()`. An agent
instance constructed and held outside React is **not** the instance the
chat renders, so messages added to it silently never appear. This cost
me a debugging round while verifying, and it is called out as a warning
callout in the docs.
## Testing
**1. New test passes against clean `origin/main`** (run in a worktree at
`96cf7aa55f`, with `@copilotkit/shared` and `@copilotkit/core` rebuilt
from the worktree so the test is not reading a stale dist):
```
✓ src/v2/components/chat/__tests__/CopilotChatFrontendActivityCard.e2e.test.tsx (2 tests) 72ms
Test Files 1 passed (1)
Tests 2 passed (2)
```
**2. Mutation-checked, so neither assertion is self-fulfilling.**
Drop the renderer registration → the render test fails:
```
× renders a card added from frontend code, with no tool call 1068ms
Tests 1 failed | 1 passed (2)
```
Swap the card from `role: "activity"` to `role: "assistant"` → it
reappears in the payload, so the exclusion is real and specific to
`activity`:
```
AssertionError: expected [ 'user', 'assistant' ] to deeply equal [ 'user' ]
```
**3. Neighboring test unaffected on the same base:**
```
✓ src/v2/components/chat/__tests__/CopilotChatMessageView.test.tsx (16 tests) 53ms
Tests 16 passed (16)
```
**4. Independent probe of the filter** against the pinned
`@ag-ui/client` 0.0.57:
```
agent.messages roles: [ 'user', 'activity' ]
run input roles : [ 'user' ]
```
**5. `tsc --noEmit`** — zero errors in the new file. Remaining errors in
this workspace are in files this PR does not touch
(`MCPAppsActivityRenderer.tsx`, `CopilotKitInspector.tsx`) and are
artifacts of a hand-assembled local `node_modules`; CI has the real
install.
**6. `oxfmt --check`** — clean.
**7. Docs checks** — `meta.json` validated as JSON; internal link uses
the house `/generative-ui/...` form (no `/docs` prefix); `Callout
type="warn"` matches the dominant existing usage; import paths verified
against the real `@copilotkit/react-core/v2` barrel exports.
## Follow-up
Leaving #3388 open until this lands, then closing it with a pointer to
the new page.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- Added support for frontend-driven activity cards that render in chat
transcripts without being sent to the agent or language model.
- Added documentation covering activity card renderers, schemas,
registration, payload filtering, snapshots, and limitations.
- Added a new “Frontend-Driven” section to the Generative UI
documentation navigation.
- **Tests**
- Added end-to-end coverage for activity card rendering and payload
exclusion.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Closes#3334 (OSS-524).
## Problem
v1 `<CopilotSidebar>` exposed `open` and `onSetOpen`. Those props let a
host open and close the chat from its own UI. v2 shipped only
`defaultOpen`. The reporter wanted a button in their own nav bar to
close the sidebar.
The reporter's stated root cause is now stale. `shouldCreateModalState`
no longer exists. Since CPK-7152 the provider syncs both directions:
`setAndSync` upward, and an effect downward. A host that wraps its
layout in `<CopilotChatConfigurationProvider>` and calls `setModalOpen`
therefore does drive the sidebar on current `main`. I verified that
before writing any code.
Two things are genuinely missing. The first is the ergonomic API that v1
had. The second is documentation for the outer-provider pattern that
already works.
Two earlier community attempts (#3729, #6418) were closed unmerged.
## What changed
`open` and `onOpenChange` on `<CopilotSidebar>` and `<CopilotPopup>`:
- `open` pins what the surface renders, from the first frame.
- `onOpenChange` reports every request to open or close: the toggle
button, click-outside, Escape, and the drawer's mobile mutual-exclusion.
It fires with or without `open`, so it also works as a plain
notification on the uncontrolled path.
- `defaultOpen` is unchanged. If both are passed, `open` wins.
Two design choices are worth review.
**1. A context-overriding scope, not a fourth mode in the provider.**
`ControlledModalOpenScope` replaces `isModalOpen` and `setModalOpen` for
the subtree below the provider that owns the state. The resolution chain
inside `CopilotChatConfigurationProvider` stays untouched: own state,
parent sync, drawer mutual-exclusion, and the `ɵregisterModalCloser`
stack. The scope's setter still calls the underlying one, so those side
effects keep running. It also registers itself as the modal closer, so
the drawer's mobile exclusion reaches the host instead of flipping state
that nothing displays. The alternative was a controlled branch threaded
through `resolvedIsModalOpen`, `setAndSync`, and the sync effect. That
adds a fourth interacting mode to the code CPK-7152 just stabilized.
**2. The props reach the views by context, not as props.**
`<CopilotSidebar>` hands its view to `<CopilotChat>` as a memoized
`chatView` component. Adding `open` to that memo's deps mints a new
element type per toggle, and React then remounts the whole chat subtree.
That is the same class of bug #6173 fixed for popup resize. There is a
regression test for it.
Scope note: I included `<CopilotPopup>` because it shares the mechanism
and the same docs page. The issue named only the sidebar.
## Testing
**New suite, 15 tests** (`CopilotSidebar.controlledOpen.test.tsx`). It
covers the controlled contract, the unchanged uncontrolled path, and the
remount guard.
```
✓ src/v2/components/chat/__tests__/CopilotSidebar.controlledOpen.test.tsx (15 tests) 155ms
Test Files 1 passed (1)
Tests 15 passed (15)
```
**Mutation-checked.** I broke each mechanism to confirm that the tests
really fail.
| Mutation | Result |
| --- | --- |
| Drop `ControlledModalOpenScope`, keep only the seeded default | 5
failed: both `onOpenChange` reports, both host-driven open/close cases,
the popup report |
| Implement through the memoized override instead (add `open` to the
`useMemo` deps) | 1 failed: the remount guard, `expected 4 to be 1`, one
extra mount per flip |
| Drop the `open ?? defaultOpen` seeding | 1 failed: "stays put when the
host stops controlling open" |
I also mutation-checked the pre-existing two-way sync before I started.
That confirmed the outer-provider workaround really works on `main`,
instead of only appearing to.
**Full `@copilotkit/react-core` suite.** No regressions.
```
Test Files 141 passed | 1 skipped (142)
Tests 1604 passed | 2 skipped (1606)
EXIT=0
```
**Adjacent suites re-run explicitly**: sidebar position, sidebar and
popup slots, popup resize-remount, drawer launcher, and the provider's
own 43 tests.
```
Test Files 6 passed (6)
Tests 117 passed (117)
```
**Typecheck.** `tsc --noEmit` in `packages/react-core` gave `exit=0`
with no output. The tsconfig includes `src/**/*`, so the new test file
is typechecked too.
**Format and lint.** `oxfmt --check packages/react-core/src/v2` reported
"All matched files use the correct format." `oxlint` on the touched
files reported 0 errors.
**Pre-commit hooks.** They ran for real on both commits.
```
NX Successfully ran targets test, publint, attw for 2 projects and 20 tasks they depend on
✔️ test-and-check-packages (15.33 seconds)
```
## Docs
- `prebuilt-components/chat-controls.mdx` now leads with the controlled
pair. Its example drives the sidebar from a nav button outside it, which
is the shape #3334 asked about. The `useCopilotChatConfiguration` route
stays, reframed as the option for callers who prefer not to lift the
state.
- `reference/components/CopilotSidebar.mdx` and `CopilotPopup.mdx` gain
`open` and `onOpenChange`. Both pages documented `defaultOpen` as
`false`, but both surfaces mount open, so I corrected that. A new test
per surface pins the real default.
## Not in this PR
- Vue and Angular parity for the same props.
- The `width` prop of `<CopilotSidebar>` still sits in the memo deps of
the `chatView` override. A live-resized sidebar therefore remounts the
chat subtree, the way the popup did before #6173. That is pre-existing
and out of scope here.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- Added controlled open-state support for chat popups and sidebars
through `open` and `onOpenChange`.
- Preserved uncontrolled usage with `defaultOpen`, while allowing
externally managed visibility and toggle requests.
- Improved coordination between modal and mobile drawer behavior.
- **Documentation**
- Added usage guidance and reference details for controlled and
uncontrolled open-state management.
- **Tests**
- Added coverage for initial visibility, toggle callbacks, controlled
updates, default behavior, and preserving the chat subtree.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## What does this PR do?
Fixes#3330 — fenced markdown code blocks render as one collapsed line
in the packaged v2 React UI.
### Root cause
streamdown renders one `<span>` per source line inside
`pre[data-streamdown="code-block-body"] > code`, and leaves **no newline
characters** in the text. The line break comes entirely from the raw
Tailwind utility `block` on that span:
```js
// streamdown 1.6.11, dist/code-block-*.js
var v = cn("block", "before:content-[counter(line)]", ...);
```
CopilotKit builds Tailwind with `@import "tailwindcss" prefix(cpk)`, so
`.block` is never emitted into `dist/v2/index.css` — only `.cpk\:block`
is. Every line therefore renders inline and the block collapses onto one
row.
The line spans carry no `data-streamdown` attribute, so the rule has to
be scoped structurally, the same way the table action controls were in
#5944:
```css
[data-copilotkit] [data-streamdown="code-block-body"] > code > span {
@apply cpk:block;
}
```
### Why the earlier attempts did not work
Three previous PRs (#3441, #3615, #5387) added `whitespace-pre` to the
`<pre>`. That is a no-op: the UA stylesheet already applies
`white-space: pre` to `<pre>`, nothing in the packaged CSS overrides it,
and there are no newlines in the text for it to preserve.
### Knowingly not fixed here
- **Line-number gutter.** streamdown's `before:content-[counter(line)]
before:w-4 before:mr-4 …` utilities are unprefixed too, so the gutter
never renders. That is cosmetic, and the repo's existing scoped rules do
not port it either.
- **The pre-highlight loading skeleton** (`space-y-4`, `divide-y`,
`animate-spin`) is unprefixed as well — a brief flash of unstyled
skeleton before shiki resolves.
- **The broader class of bug.** Every unprefixed streamdown utility has
to be hand-ported like this. streamdown 2.x adds a `prefix` prop that
would fix the whole surface at once, and #5147 proposes removing the
bundled renderer entirely. Both are larger calls than this bug fix.
## Testing
**1. Live browser verification.** Built `dist/v2/index.css` from
`origin/main` and from this branch, rendered streamdown 1.6.11's actual
code-block DOM against each, and measured layout in Chromium:
| | `white-space` on `<pre>` | line-span `display` | distinct rendered
rows | `<pre>` height |
|---|---|---|---|---|
| main | `pre` | `inline` | **1** | 52px |
| this PR | `pre` | `block` | **5** | 112px |
Indentation is preserved after the fix (`spans[1].textContent` starts
with two spaces).
**2. Compiled CSS.** `tailwindcss -i src/v2/styles/globals.css -o … -m`
emits exactly:
```css
[data-copilotkit] [data-streamdown=code-block-body]>code>span{display:block}
```
**3. Tests** — `pnpm -C packages/react-core exec vitest run
src/v2/styles`
```
✓ src/v2/styles/__tests__/streamdown-styles.test.ts (3 tests) 2ms
✓ src/v2/styles/__tests__/streamdown-table-controls.test.tsx (1 test) 37ms
✓ src/v2/styles/__tests__/streamdown-code-block-lines.test.tsx (1 test) 430ms
Test Files 3 passed (3)
Tests 5 passed (5)
```
Two tests, following the split established by #5944 — a source-string
test that the selector exists, and a DOM test that streamdown still
renders the structure that selector assumes (so a streamdown markup
change fails loudly instead of silently un-fixing this).
**4. Mutation-checked both tests.** Removing the CSS rule fails the
string test:
```
× Streamdown styles > ships a scoped display rule for code block lines (#3330) 3ms
Tests 1 failed | 2 passed (3)
```
Pointing the DOM test at a selector streamdown does not render fails it:
```
× Streamdown code block lines DOM (#3330) > renders one line span per source line 428ms
Tests 1 failed (1)
```
**5. Formatting** — `oxfmt --check` clean on all three files; `git diff
--check` clean.
`tsc --noEmit` in this worktree reports 58 pre-existing errors, all from
a stale cross-package `@copilotkit/core` dist; none are in the changed
files (which are CSS plus tests).
## Related PRs and Issues
Fixes#3330
Supersedes #3441, #3615, #5387, #5996 (all added a no-op
`whitespace-pre`)
## Checklist
- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation (not applicable: scoped visual bug fix)
- [x] "Allow edits by maintainers" is checked
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **Bug Fixes**
- Fixed fenced code blocks collapsing into a single line in the packaged
UI.
- Code lines now render vertically as separate rows with the correct
layout styling.
- **Tests**
- Added regression coverage to verify code-line rendering and scoped
styles for code blocks.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Hydrating an existing thread through `/connect` fails on a
**self-hosted** runtime whenever that thread's history contains a run
that ended in `RUN_ERROR`.
## The defect
A `/connect` response is a *replay* of a thread's history, so it can
legitimately carry several past runs back to back — including an errored
run followed by a later `RUN_STARTED`. The base `AbstractAgent` connect
pipeline pushes that stream through `verifyEvents`, which enforces
AG-UI's **single run** lifecycle rules and rejects the sequence
outright:
```
Cannot send event type 'RUN_STARTED': The run has already errored with 'RUN_ERROR'. No further events can be sent.
```
The user-visible effect is the one reported in #4943:
`agent_connect_failed` on reload, and the existing thread never hydrates
its prior messages.
`IntelligenceAgent` already omitted `verifyEvents` from its connect
pipeline for exactly this reason (its JSDoc spells it out). But
`ProxiedCopilotRuntimeAgent.connectAgent` only takes that path in
`RUNTIME_MODE_INTELLIGENCE` — self-hosted (`RUNTIME_MODE_SSE`) fell
through to `super.connectAgent()` and inherited the single-run
verification. So the managed product was fine and self-hosting was not.
## The fix
`ɵconnectWithoutEventVerification`
(`packages/core/src/utils/connect-replay.ts`) holds the
verifyEvents-free pipeline, and **both** paths now use it.
`transformChunks` is still applied — message reassembly is needed either
way.
This is a de-duplication rather than a third copy:
`IntelligenceAgent.connectAgent` drops ~100 lines of hand-replicated
base pipeline (including its private-field `any` escape hatch) and keeps
only its canonical-run-id handling before delegating. Net
`intelligence-agent.ts` change is −101 lines.
### Fidelity to the base implementation
The helper was diffed statement-by-statement against the **real**
`AbstractAgent.connectAgent` in `@ag-ui/client@0.0.57` (recovered from
the shipped source map), not just against `IntelligenceAgent`'s replica.
`verifyEvents` is the only intended difference.
That diff caught a defect in the first push: the base special-cases
`AGUIConnectNotImplementedError` (swallow → `EMPTY`) and the replica did
not. `IntelligenceAgent` never needed it — it always implements
`connect()` — so the gap was invisible there, but on the SSE path it is
load-bearing: `run-handler.ts:447-450` documents that `await
agent.detachActiveRun()` only stopped deadlocking because that error
path still reaches the pipeline's finalize block. Routing it through
`onError` would also fire run-failure callbacks on every subscriber for
a benign condition. Restored, with a regression test.
Also confirmed that dropping `verifyEvents` cannot alter a well-formed
replay: it is a pure gate — 18 `return of(event)` pass-throughs, 42
error paths, and zero `endWith` / `startWith` / `tap` side effects. It
only removes the single-run rejection.
The existing upstream TODO still stands and is carried over:
`@ag-ui/client@0.0.57`'s `connectAgent(parameters?, subscriber?)` takes
no option to skip verification, so this override is still the only way
to express "this stream is a replay, not a run."
## On the second half of #4943
The issue also reports that the legacy chat path doesn't copy the
resolved `threadId` onto the agent before connect/run. **That half is
already fixed on `main`** — the #5041/#4739 fix put `agent.threadId =
resolvedThreadId` in v2 `useAgent`, and `useCopilotChatInternal`
delegates to that same hook. Nothing more was needed.
It was untested, though, and untestable from the suite that looked like
it covered it: `use-copilot-chat-internal-connect.test.tsx` mocks
`useAgent` wholesale, so it cannot observe threadId propagation at all.
This PR adds `legacy-chat-explicit-threadid.test.tsx`, which drives the
legacy hook through the **real** `useAgent` under a real `<CopilotKit>`,
covering both the explicit-threadId case and the "don't adopt a
non-explicit placeholder UUID" case.
It reads the agent off `useCopilotChatInternal()`'s own return value
rather than calling `useAgent` in the probe. That distinction matters:
the first version of this test did call `useAgent`, so the probe itself
performed the assignment under test and the test passed **even with
`useCopilotChatInternal()` removed entirely**. The current version is
mutation-checked — disabling the assignment in v2 `useAgent` fails it
(`expected 'dc051f13-…' to be 'cookie-backed-thread'`).
Contributor PR #4969 proposed a manual assignment for this half; it is
now redundant.
## Testing
Worktree caveat, stated up front: this worktree symlinks the primary
checkout's `node_modules`, so `@copilotkit/shared` and
`@copilotkit/core` resolve to that checkout's **stale `dist`**. That
produces failures unrelated to this change; each is baselined against
clean `main` in the same environment below. CI installs fresh and is the
authoritative gate.
**1. Reproduces the reported failure before the fix.** The new core
test, run on unmodified `origin/main`, fails with the exact error from
the issue:
```
FAIL src/__tests__/proxied-connect-replay-multi-run.test.ts > hydrates a thread whose replayed history contains an errored run
AssertionError: promise rejected "Error: Cannot send event type 'RUN_STARTE…" instead of resolving
Caused by: Error: Cannot send event type 'RUN_STARTED': The run has already errored with 'RUN_ERROR'. No further events can be sent.
```
**2. Passes after the fix**, hydrating both runs' messages (`["msg-1",
"msg-2"]`):
```
✓ src/__tests__/proxied-connect-replay-multi-run.test.ts (1 test) 11ms
Test Files 1 passed (1)
```
**3. Connect-not-implemented guard, fail-first.** With the guard
removed, the new second test fails exactly as the base contract
predicts:
```
× swallows AGUIConnectNotImplementedError instead of failing the run
AssertionError: promise rejected "Error: Connect not implemented. This meth…" instead of resolving
```
**4. Full `@copilotkit/core` suite** — this is the evidence the
`IntelligenceAgent` extraction is behavior-identical, since
`intelligence-agent.test.ts` exercises that path heavily:
```
Test Files 59 passed (59)
Tests 635 passed (635)
```
(excludes `core-inspector-metadata.test.ts`; its 20 failures are the
stale-`shared`-dist artifact — verified identical on clean `main`: `20
failed | 2 passed`, missing export `InspectorMetadataV1`)
**5. `@copilotkit/react-core` — new + adjacent existing suites:**
```
✓ src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx (7 tests)
✓ src/hooks/__tests__/legacy-chat-explicit-threadid.test.tsx (2 tests)
✓ src/components/copilot-provider/__tests__/v1-explicit-threadid-bridge.test.tsx (5 tests)
Test Files 3 passed (3)
Tests 14 passed (14)
```
Full react-core suite: `8 failed | 1492 passed (1500)`. All 8 are in
`use-interrupt` / `use-pin-to-send` / `CopilotChatView.pinToSend` — none
touch connect replay or threadId, and clean `main` in this worktree
fails the identical 8 (`8 failed | 37 passed (45)` for those three files
alone).
**6. `@copilotkit/vue`** (affected via core): `100 passed (100)` files,
`1072 passed (1072)` tests.
**7. Types, lint, format:**
```
tsc -p packages/core/tsconfig.json --noEmit → no errors in any changed file
oxlint <5 changed files> → Found 0 warnings and 0 errors
oxfmt --check <5 changed files> → All matched files use the correct format
```
The only remaining `tsc` errors are 4 pre-existing stale-dist ones in
`agent-registry.ts` / `types.ts` (`InspectorMetadataV1`), untouched by
this PR.
Fixes#4943
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved thread hydration when reconnecting to histories containing
multiple runs, including runs that previously ended in error.
* Prevented unsupported connection errors from being reported as run
failures.
* Ensured connection state is properly finalized after replaying a
thread.
* Legacy chat components now correctly reuse an explicitly provided
thread ID while preserving generated IDs when none is provided.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
v1 exposed `open` + `onSetOpen`, so a host could open and close the chat
from its own UI. v2 shipped only `defaultOpen`, leaving the open state
reachable exclusively from inside the chat subtree. Restores the
controlled pair on `<CopilotSidebar>` and `<CopilotPopup>`:
- `open` pins what the surface renders, from the first frame.
- `onOpenChange` reports every request to open or close (toggle button,
click-outside, Escape, the drawer's mobile mutual-exclusion). It fires
with or without `open`, so it also works as a plain notification.
Implemented as `ControlledModalOpenScope`, which overrides the chat
configuration context for the subtree, rather than as a fourth mode
inside CopilotChatConfigurationProvider's modal-state resolution. The
provider's own state, parent sync, drawer mutual-exclusion and
modal-closer registry are untouched: the wrapped setter still calls the
underlying one, so those side effects keep running, and it registers
itself as the modal closer so the drawer reaches the host.
The props travel to the views by context, not through the memoized
`chatView` override. Threading a changing `open` through that override
would mint a new element type per toggle and remount the whole chat
subtree, which is the class of bug already fixed for popup resize.
Closes#3334
Activity messages (role: "activity") already render standalone in the
transcript and are stripped from the run payload by
AbstractAgent.prepareRunAgentInput, so frontend code can put a card in the
chat without a tool call and without polluting the conversation. That was
only ever documented for backend-emitted activities, so the frontend-driven
path was undiscoverable — issue #3388 asked for a feature that already ships.
Adds a Generative UI guide page for the pattern and a react-core test that
pins both halves of the contract: the card renders, and it never reaches the
agent.
The non-obvious part, and the reason this needs documenting rather than a
one-line answer: the card must be added via the agent from useAgent(). An
agent instance constructed and held outside React is not the instance the
chat renders, so messages added to it silently never appear.
Refs #3388
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fenced code blocks rendered as a single collapsed line in the packaged v2
UI. streamdown emits one <span> per source line inside
`pre[data-streamdown="code-block-body"] > code` and leaves no newline in
the text, so the line break comes entirely from the raw Tailwind utility
`block` on that span. CopilotKit builds Tailwind with `prefix(cpk)`, so
`.block` never reaches `dist/v2/index.css` and every line ran inline.
Scope the display rule structurally, because the line spans carry no
`data-streamdown` attribute of their own.
Adding `whitespace-pre` to the <pre>, as earlier attempts did, changes
nothing: the UA stylesheet already sets `white-space: pre` there and
there are no newlines left to preserve.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
## What does this PR do?
Hooks can now expose a frontend tool to browser agents through the
WebMCP browser API, next to the normal agent registration. Set `webmcp:
true`, or pass `{ annotations }` for WebMCP hints:
```ts
useFrontendTool({
name: "searchOrders",
description: "Search the signed-in user's orders by status",
parameters: z.object({ status: z.enum(["open", "shipped", "delivered"]) }),
handler: async ({ status }) => searchOrders(status),
webmcp: { annotations: { readOnlyHint: true } },
});
```
How it works:
1. `FrontendTool` in `@copilotkit/core` gains the `webmcp` option. A new
`WebMCPRegistry` registers the tool on `document.modelContext` with its
name, description, input schema, and annotations. `execute` runs the
tool's own handler. The handler context has no `agent` there.
2. Every tool registry change in `RunHandler` reconciles the WebMCP
registrations. The same availability rules apply as for the agent tool
list. Removing a tool aborts its registration signal, and the browser
then unregisters it.
3. Each adapter picks the option up from core: v2 `useFrontendTool`
(React, Vue, React Native), the v1 `useCopilotAction` and
`useFrontendTool` wrappers (React, Vue), and Angular's
`registerFrontendTool`. Where WebMCP is not available (SSR, React
Native, browsers without the API), registration is a no-op.
The `webmcp` prop is documented on the React, Vue, and Angular reference
pages in shell-docs.
## Related PRs and Issues
- None.
## 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
- [ ] "Allow edits by maintainers" is checked (lets us help iterate on
your PR directly — faster turnaround for everyone)
## Testing
**Commands run**
- `pnpm nx run-many -t check-types
--projects=@copilotkit/core,@copilotkit/react-core,@copilotkit/vue,@copilotkit/angular`
— all pass.
- Full test suites: core (829 tests), vue (103), and angular pass.
react-core passes standalone (1589 tests). Under the lefthook pre-commit
hook, react-core flakes on pre-existing e2e tests (A2UI, MCP Apps) that
do not touch this code. Those tests pass when run alone.
**Manual test**
Requires Chrome 149+ with the WebMCP origin trial, or the testing flag.
1. Enable `chrome://flags/#enable-webmcp-testing`, then relaunch Chrome.
2. In an app that uses CopilotKit, register a tool with `webmcp: true`.
3. Run `await document.modelContext.getTools()` in DevTools. The tool is
listed with its schema and annotations.
4. Unmount the hook. Run the command again. The tool is gone.
**How this PR makes testing easy**
The behavior has automated tests on this branch:
- `packages/core/src/core/__tests__/run-handler-webmcp.test.ts` — 15
tests with a `document.modelContext` stub: registration, annotations,
unregistration, availability rules, name collisions, stale-rejection
races, and handler execution.
-
`packages/react-core/src/v2/hooks/__tests__/use-frontend-tool-webmcp.test.tsx`
and the mirrored
`packages/vue/src/v2/hooks/__tests__/use-frontend-tool-webmcp.test.ts` —
pass-through, re-registration, and agent-scoped cases at the hook level.
- `packages/vue/src/hooks/__tests__/use-frontend-tool-webmcp.test.ts` —
reactive `webmcp` getters through the v1 Vue API.
## Risk / rollback
Low. The feature is opt-in per tool. Without `webmcp`, no code path
changes. Where WebMCP is unsupported, registration is a no-op. Revert
this PR to roll back.
## Public API change
New optional `webmcp` prop on frontend tool registrations. Existing call
sites do not change.
**Before**
```ts
useFrontendTool({
name: "searchOrders",
description: "Search orders by status",
parameters: z.object({ status: z.string() }),
handler: async ({ status }) => searchOrders(status),
});
```
**After**
```ts
useFrontendTool({
name: "searchOrders",
description: "Search orders by status",
parameters: z.object({ status: z.string() }),
handler: async ({ status }) => searchOrders(status),
webmcp: { annotations: { readOnlyHint: true } },
});
```
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Tools can now be exposed to browser agents through WebMCP.
* Added support for custom annotations and automatic parameter schema
generation.
* WebMCP registrations stay synchronized as tools are added, removed,
enabled, or updated.
* Available across Angular, React, and Vue tool APIs.
* WebMCP reuses existing handlers and safely does nothing when
unavailable.
* **Documentation**
* Added usage guidance and examples for configuring WebMCP-enabled
tools.
* Documented that WebMCP invocations do not include an agent context.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
The Headless UI console notice told developers about "premium features" and
pointed at /premium/overview. The tier is called CopilotKit Intelligence now, so
the notice named a product that no longer exists. It now uses the same sentence
the Headless UI docs page uses.
The docs links in react-core, web-inspector and the runtime skill reference move
from /premium/* to /intelligence/*. They worked through the redirects added in
#6818, but each cost a hop and carried the old name.
One of them was broken, not just stale: the "Show me how" button on the missing
public API key error opened /premium/overview#getting-access. That heading was
deleted on 2026-06-16 in 449237af0c, so the button had been landing at the top
of the page for two and a half months. It now points at #plans-and-access, the
section that answers how to get a key.
Tests assert these hrefs, so they move with the strings.
Refs OSS-1085
The comment described an http/https/mailto/tel allowlist, but ui/open-link uses a
denylist (javascript:/data:/vbscript:/blob:/file:). Align the comment with the
actual contract so it does not mislead a future change to the scheme policy.
- Add e2e tests pinning the ui/initialize contract (the compile-time tie to the
spec): a well-formed initialize returns the host context and the negotiated MCP
Apps protocol version; an initialize missing required fields (e.g.
appCapabilities) is rejected with -32603; a widget sending a different
protocol-version string gets the host's MCP Apps version back, not its own
echoed. (2025-06-18 is a base-MCP-protocol version, independent from the MCP
Apps protocol 2026-01-26; it is what the old hand-rolled host hardcoded.)
- Nit: load the bridge via `import(...).catch(rethrow)` with inferred types
instead of `typeof import(...)` annotations, removing three
consistent-type-imports warnings.
- Nit: restore the "ui/message: No agent available" warning log on the no-agent
path, for parity with the hand-rolled host and the oncalltool guard.
## What changed
- Add standalone `CPK_TELEMETRY_ID` support to Runtime v1 and v2.
- Keep telemetry opt-out, sampling, Segment, and legacy license fallback
behavior.
- Fetch structured Intelligence entitlements and map them to current
client status.
- Share concurrent entitlement lookups, retry short-lived failures, and
reject stale grants.
- Make managed React, Angular, and Vue thread UIs use Runtime
entitlement authority.
- Keep assistant feedback stable when unrelated Inspector settings
change.
- Update Runtime, telemetry, self-hosting, and Web Inspector docs.
## Why
Managed Intelligence projects use a project API key for product access
and a non-secret telemetry ID for attribution. Offline license tokens
remain a self-hosted entitlement concern.
Starter-template and AgentCore changes live in #6188.
## Companion PRs
- Starter templates: #6188
- CopilotKit/Intelligence#628
- CopilotKit/oss-path-to-production#226
## Review corrections
- Scope shared entitlement attempts to one API key and endpoint.
- Ignore stale attempts after credentials change.
- Bound retries after short denials and transport failures.
- Accept telemetry IDs only when they match the public identifier
contract.
- Read Inspector context in its button, so unrelated label changes do
not rerender assistant feedback.
## Validation
- React Core full suite: 1,537 Vitest tests and 47 script tests passed.
- Runtime, Core, Shared, Angular, Vue, and Web Inspector focused suites
passed.
- React Core typecheck and build passed after the final rebase.
- Direct builds and type checks passed for Angular, Core, Runtime,
Shared, Vue, and Web Inspector.
- Shell docs typecheck and production build passed.
- Changed Vue files passed ESLint.
- `git diff --check` passed.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- Added structured runtime entitlement support for managed and
self-hosted deployments.
- Feature access and usage limits now reflect active entitlements, with
legacy license compatibility.
- Added runtime entitlement diagnostics to the Inspector’s Threads view.
- Added runtime-scoped telemetry identities and configurable telemetry
ID support.
- **Bug Fixes**
- Licensing interfaces remain in a loading state during retryable
entitlement outages.
- Improved recovery after runtime connection, target, or transport
changes.
- Prevented stale entitlement data from granting access after refresh
failures.
- **Documentation**
- Documented entitlement statuses, telemetry identity precedence,
sampling, and Inspector telemetry behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Re-derived against current main from **#4259** (mxmzb), which diagnosed
this in April. That branch is 7,386 commits behind and conflicts, so
this ports the mechanism rather than rebasing it.
## The bug
`useFrontendTool` registered its tool inside a `useEffect`. React
flushes passive effects **child-first in tree order**, so a component
mounted *before* the registering component runs its own `useEffect`
against an empty tool list.
That is the cross-page-navigation failure: a page mounts `CopilotChat`
and its tool-registering components in one commit, `CopilotChat`'s
connect effect fires first, and the connect request goes out carrying no
frontend tools.
## The fix
Register in `useLayoutEffect`. Layout effects run during commit, ahead
of every passive effect regardless of tree order, closing the window.
This is not a new pattern here — **`useAgentContext` already registers
via `useLayoutEffect`** (`use-agent-context.tsx:2`). The context half of
this hook family was fixed; the tool half was not. This makes them
consistent.
## Testing
### The original test did not detect the bug
Worth recording. #4259 shipped `use-frontend-tool-timing.test.tsx`,
which mounts the tool registrar **before** the observing component. I
ported it verbatim and ran it against unmodified main:
```
✓ src/v2/hooks/__tests__/use-frontend-tool-timing.test.tsx (1 test) 10ms
Test Files 1 passed (1)
```
It passes without the fix. React runs the registrar's effect first in
that order, so the observer always sees the tool. The PR's own comment
concedes the point — *"the result depends on component ordering and may
be absent."*
The test here mounts the consumer **first**, which is the shape that
actually breaks, and says so in the file so nobody reorders it back.
### RED → GREEN on the real surface
**RED** (current main, `useEffect`):
```
× registers the tool before an earlier-mounted sibling's useEffect runs
AssertionError: expected [] to include 'timingTestTool'
Tests 1 failed (1)
```
The empty array is the bug: the consumer's effect saw no tools.
**GREEN** (`useLayoutEffect`):
```
✓ src/v2/hooks/__tests__/use-frontend-tool-timing.test.tsx (1 test) 12ms
Tests 1 passed (1)
```
### No regressions
Full `src/v2/hooks` suite, same environment, with and without the
change:
| | Test files | Tests |
|---|---|---|
| without fix | 8 failed / 29 passed | **37 failed** / 283 passed |
| with fix | 7 failed / 30 passed | **36 failed** / 284 passed |
The delta is exactly the new test. The remaining 36 failures are
pre-existing in my local worktree (stale cross-package `dist`
resolution), identical on both sides.
## Notes
- **SSR:** `useLayoutEffect` warns during server rendering. These hooks
run inside `CopilotKitProvider`'s client context, and the sibling
`useAgentContext` already uses a bare `useLayoutEffect`, so this follows
the established pattern rather than introducing an isomorphic wrapper.
- **Scope:** #4259 also carried a second, independent mechanism — an
`ensureToolMiddleware` fallback that injects tools/context into direct
`agent.runAgent()` calls bypassing `copilotkit.runAgent()`
(`run-handler.ts` +44, plus `core.ts`, `agent-registry.ts`, Angular's
`agent.ts`, `use-agent.tsx`). That addresses a **different** failure and
deserves its own PR, tests and review. It is deliberately **not**
included here and should not be considered resolved by this.
Credit to @mxmzb for the diagnosis.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Frontend tools are now registered earlier, ensuring availability
before the interface is displayed.
* Improved consistency when components access frontend tools during
initial effects.
* Renderer cleanup now occurs at the appropriate stage when components
are removed.
* **Tests**
* Added coverage verifying frontend tools are available to
earlier-mounted components.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Two frontend-tool defects that share a shape: a tool that was registered
correctly still never reached the agent, or reached the render without
the ability to respond.
## `useFrontendTool` tools dropped when the runtime enables
`openGenerativeUI` (#4952)
Tools reached the core registry through two owners that shared one array
— the provider via `setTools()`, hooks via `addTool()` — and `setTools`
replaced the array wholesale. Any provider re-sync after mount therefore
wiped every hook-registered tool.
A runtime with `openGenerativeUI: true` made it reproduce on **every**
mount: `/info` flips the flag asynchronously, the provider re-derives
its tool list to add `generateSandboxedUi`, and the re-sync dropped the
app's own tools. The agent then received only `generateSandboxedUi`,
exactly as reported.
The fix splits the two owners into their own buckets, merged on read
with hook entries winning. This mirrors what `CopilotKitCoreReact`
already does for render tool calls (`react-core.ts`) — tools simply
never got the same treatment. Because the clobber lived in core rather
than in one provider, **Vue had the identical bug and is fixed by the
same change** — verified end to end, not by shape:
`CopilotKitProvider.vue:461` assigns `runtimeOpenGenerativeUIEnabled`
from the core, `:297` derives `openGenerativeUIActive`, `:328`/`:354`
add the built-in to `allTools`, and `:492` re-syncs it through
`setTools` behind the same `didMountRef` skip. Angular is unaffected: it
never calls `setTools`, passes `tools` only through the constructor
(`copilotkit.ts:152`), and registers just the *renderers* from
`config.tools` (`:224`) — no second registration.
`addTool` now shadows a provider tool of the same name instead of
refusing to register, and only warns when another imperative
registration already holds the name. It also no longer pushes onto the
array the provider passed in, and `initialize` copies that array like
`setTools` already did.
**One intentional behavior change worth a reviewer's eye:** `setTools`
now replaces provider-owned tools only, so `setTools([])` no longer
clears tools registered through `addTool`. That narrowing *is* the fix,
but anyone calling `setTools([])` as a "clear everything" would now need
`removeTool` per tool. Nothing in the repo does (core, react-core, vue,
angular suites all pass).
## Catch-all actions could not wait for a response (#1746)
`getActionConfig` short-circuited on `name === "*"` before checking for
a wait-render, so a catch-all declaring `renderAndWaitForResponse` was
silently downgraded to render-only — no `respond`, and in practice a
`render is not a function` throw, since the render-only path reads
`action.render`. Handling N human-in-the-loop tools required N hooks.
A catch-all with a wait-render now routes to the human-in-the-loop path.
Core already had the execution half
(`getWildcardTool`/`executeWildcardTool`), so this is routing, not new
machinery. Two supporting changes make it usable:
- The HITL render props now carry **the name of the tool actually being
called**. It equals the registration name for a normal action, but a
catch-all needs it to tell N tools apart, and `"*"` was being written
over it in both the v1 wrapper and the v2 hook.
- `CatchAllFrontendAction` accepts
`renderAndWaitForResponse`/`renderAndWait` alongside `render`, mutually
exclusive as on `FrontendAction`, with `CatchAllActionRenderPropsWait`
exported.
Also: a wildcard tool is no longer advertised to the agent. It is a
local catch-all handler for calls with no exact match, so offering the
model a tool literally named `*` was never meaningful. Latent before
this PR (nothing in React registered a wildcard *tool*); catch-all HITL
activates it.
## Synced with `main` (2026-08-31)
`main` moved react-core's v1 tree under `src/v1-deprecated/` while this
branch was open, so the merge had exactly two conflicts, both
relocations:
- `use-default-tool.ts`'s `DistributiveOmit` change re-applied on the
moved file, keeping main's deprecation banner.
- the catch-all HITL e2e test moved into
`src/v1-deprecated/hooks/__tests__/`, with its `../../v2/...` imports
re-rooted to `../../../v2/...` to match its sibling
`use-copilot-action.e2e.test.tsx`.
The defect is still live on current `main` —
`CopilotKitProvider.tsx:838` still calls `copilotkit.setTools(allTools)`
— and both regression tests still bite there. The suites, the
before/after checks, `tsc --noEmit`, `oxlint` and `oxfmt` below were all
re-run on the merged tree; the browser walkthrough under **Live
verification** is from the pre-merge branch and was not repeated.
## Testing
**New regression tests**
- `packages/core/src/core/__tests__/run-handler-tool-registry.test.ts` —
13 tests: `addTool` survives `setTools`, hook precedence, agent-scoped
vs global, `removeTool` across both buckets, remount re-registration,
capability toggles surviving a re-sync, provider ordering, caller-array
aliasing in both directions, wildcard never advertised.
-
`packages/react-core/src/v2/providers/__tests__/CopilotKitProvider.openGenerativeUIToolLoss.test.tsx`
— drives the **real** core over a stubbed `/info` that returns
`openGenerativeUIEnabled: true`, asserting the hook tool survives.
-
`packages/react-core/src/v1-deprecated/hooks/__tests__/use-copilot-action-catch-all-hitl.e2e.test.tsx`
— end-to-end through the real provider and core: catch-all gets the real
tool name and a live `respond`, the tool result lands on the original
`toolCallId`, the follow-up run fires, and `*` is absent from
`runInputs[0].tools`.
**Both new tests were confirmed to fail before the fix — re-confirmed
after syncing `main`,** by checking the touched sources out at
`origin/main` and rebuilding core's dist:
```
× keeps the hook tool once the runtime turns openGenerativeUI on
→ expected [ 'generateSandboxedUi' ] to include 'sayHello'
```
and before the routing fix, the catch-all test failed with the exact
defect from the issue:
```
× gives the catch-all render a live respond and the real tool name
→ TypeError: render is not a function
❯ render src/hooks/use-render-tool-call.ts:44:22
```
**Suites (all green)**
```
@copilotkit/core 67 files, 799 tests passed
@copilotkit/react-core 137 files, 1558 tests passed
@copilotkit/vue 101 files, 1092 tests passed
@copilotkit/angular 49 files, 317 tests passed (1 skipped)
```
`tsc --noEmit` clean for `core` and `react-core`; `oxlint` 0 errors on
the changed files (16 warnings, all pre-existing); `oxfmt` applied.
**Live verification** — `examples/v2/react/demo` in a browser against
built dists, with a temporary stub AG-UI agent (no LLM key) that echoes
the tool names it receives, a runtime configured `openGenerativeUI:
true`, no `openGenerativeUI` prop on the provider, one
`useCopilotAction` frontend tool and one `useCopilotAction({ name: "*",
renderAndWaitForResponse })`:
```
TOOLS_RECEIVED: ["generateSandboxedUi","sayHello"] <- #4952: hook tool survived; no "*" leaked
catch-all handling: book_call status: executing <- #1746: real tool name, live respond
[click "Pick Tuesday"]
catch-all handling: book_call status: complete
TOOL_RESULT_RECEIVED: "{"slot":"tuesday"}" <- follow-up run received the result
```
0 console errors. The stub route and page were scratch and are not in
this branch.
## Notes for reviewers
- Community PR #4967 also targets #4952 by merging in the provider
instead. I took the core-layer fix because the clobber is in core's
registry and every framework provider hits it — patching one provider
leaves Vue broken. Happy to reconcile.
- Pre-existing and deliberately **not** changed here:
`useCopilotAction({ name: "*" })` render props do not infer, because the
hook's parameter is a union TypeScript cannot contextually type. This
already affected plain `render` before this PR (verified), so the new
tests and docs annotate props explicitly. Fixing it needs a
`useCopilotAction` overload — worth a follow-up.
- Deliberate small duplications, flagged rather than abstracted:
`WILDCARD_TOOL_NAME` is a one-line const in both core and react-core's
v2 HITL hook (sharing it would mean a new public export from core), and
the link between "a catch-all render receives `name`" and "the v1 HITL
wrapper supplies it" is a cast rather than a type — making it typed
means adding `name` to the public `ActionRenderPropsWait`, which is
wider than this fix.
- #4759 is left with contributor PR #5308, and #6101 needs its own
design pass since it introduces new public API.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- Added catch-all human-in-the-loop actions that can handle unregistered
tools and wait for user responses.
- Catch-all action renderers now receive the actual invoked tool name
and arguments.
- Added support for wait-aware catch-all rendering types.
- **Bug Fixes**
- Preserved frontend tools when provider tool lists are refreshed.
- Prevented duplicate tools and ensured registered tools take
precedence.
- Hidden wildcard tools from agent-advertised tool lists while keeping
them available for handling requests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Self-review catch. The extracted pipeline was faithful to IntelligenceAgent's
copy but not to the base AbstractAgent.connectAgent it replaced on the
self-hosted path, which special-cases one error:
catchError((error) => {
this.isRunning = false;
if (!(error instanceof AGUIConnectNotImplementedError)) {
return this.onError(input, error, subscribers);
}
return EMPTY;
})
IntelligenceAgent never needed it — it always implements connect() — so the
omission was invisible there. On the SSE path it is load-bearing:
run-handler.ts awaits detachActiveRun() before every run and documents that
this only stops deadlocking because the ConnectNotImplementedError path
reaches the finalize block. Routing it through onError would also fire
run-failure callbacks on every subscriber for a benign condition.
Restores the guard, adds a regression test (verified fail-first: without the
guard connectAgent() rejects with "Connect not implemented"), and matches the
base's `void this.onFinalize(...)`.
Also makes the legacy-chat threadId test actually test its claim. It read
agent.threadId from its own useAgent() call, so the probe performed the very
assignment under test — it passed even with useCopilotChatInternal() removed
entirely. It now reads the agent off the hook's own return value, and is
mutation-checked: disabling the assignment in v2 useAgent fails it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A /connect response replays a thread's history, so it can carry several
past runs back to back — including a run that ended in RUN_ERROR followed
by a later RUN_STARTED. The base AbstractAgent connect pipeline runs the
stream through verifyEvents, which enforces single-run lifecycle rules and
rejects that sequence outright:
Cannot send event type 'RUN_STARTED': The run has already errored with
'RUN_ERROR'. No further events can be sent.
IntelligenceAgent already omitted verifyEvents from its connect pipeline
for this reason, but self-hosted runtimes (RUNTIME_MODE_SSE) fell through
to super.connectAgent() and so never hydrated such a thread.
Extract that verifyEvents-free pipeline into a shared helper used by both
paths, rather than keeping two copies of a delicate 60-line pipeline.
IntelligenceAgent keeps its canonical-run-id handling and delegates the
rest.
Also adds legacy-chat threadId coverage: the pre-existing connect suite
mocks useAgent wholesale, so nothing exercised the real propagation the
CopilotPopup path depends on.
Fixes#4943
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
useFrontendTool now registers the tool renderer in a layout effect, but
useHumanInTheLoop still removed that renderer from a passive effect cleanup.
React runs each phase's cleanups before that phase's effects, but runs the
entire layout phase ahead of the entire passive phase. With the two split
across phases, a keyed remount ordered the outgoing instance's removal after
the incoming instance's registration:
add (new, layout) -> remove (old, passive)
which deleted the renderer that had just been added and left the HITL tool
unrenderable. Moving the teardown to useLayoutEffect restores the correct
remove-then-add ordering.
Caught by the existing 'should maintain executing state across component
remount' test in use-human-in-the-loop.e2e.test.tsx.
The v1 react-core tree moved under src/v1-deprecated/, so the two conflicts
were relocations: use-default-tool.ts's DistributiveOmit change re-applied on
the moved file, and the catch-all HITL e2e test moved into
src/v1-deprecated/hooks/__tests__/ with its v2 imports re-rooted.
- Sandbox handshake race: load the ext-apps bridge (dynamic import) BEFORE
creating and attaching the sandbox iframe. The proxy posts sandbox-proxy-ready
once during srcdoc execution, and the PostMessageTransport must be listening
(connect) when it fires. Awaiting the import after the iframe was attached let
a slow import miss that notification, leaving the widget blank. There is now no
event-loop yield between attaching the iframe and connecting.
- ui/open-link scheme hardening (XSS): the ext-apps schema validates url as a
string only, so a widget could pass javascript:/data:/blob: etc. Parse the url
and refuse a denylist of script-executing / attacker-HTML schemes (javascript,
data, vbscript, blob, file) before window.open. A denylist is used on purpose so
custom-scheme deep links (myapp:, whatsapp:, ...) and https universal links keep
working, since window.open on those hands off to an OS handler rather than
executing in the page. This matches the Anthropic Software Directory policy
(https origins + owned custom URI schemes) and the MCP spec's prudent-host
guidance.
- Tests: reject a disallowed scheme without calling window.open; allow a
custom-scheme deep link.
- @modelcontextprotocol/sdk is now a non-optional peerDependency (matching how
ext-apps declares it) instead of an optional one. ext-apps ships as a direct
dependency and hard-peers the sdk, so the requirement is already inherited by
every consumer; the optional flag only hid that and dropped the install-time
signal. react-core does not use the sdk directly (type import only), so it
stays out of our dependencies; ext-apps remains the direct dependency.
- Guard the lazy bridge import with a try/catch that rethrows naming the packages
and the install command, so a missing or version-skewed peer surfaces as an
actionable error instead of an opaque module-resolution rejection in an effect.
## What does this PR do?
Fixes the v1 compatibility render path so an assistant message can
render every tool call instead of only `toolCalls[0]`.
The returned lazy renderer now:
- matches each tool call with its corresponding tool result;
- renders all registered tool-call renderers in one fragment;
- removes `null` render results and returns `null` when no tool has a
renderer.
Keeping the fragment behind the existing lazy-renderer callback
preserves the exported `useLazyToolRenderer` return signature. Filtering
before returning also avoids attaching empty generative UI, so
caller-provided subcomponents are not suppressed when no renderer is
registered.
Regression tests cover multiple tool calls, per-call result matching,
the all-unhandled case, and a mixed handled/unhandled message.
## Related PRs and Issues
- Fixes#2946
## Verification
- `pnpm nx run @copilotkit/react-core:test` (133 files, 1,530 Vitest
tests plus 47 script tests)
- `pnpm nx run @copilotkit/react-core:check-types`
- `pnpm exec oxfmt --check
packages/react-core/src/v1-deprecated/hooks/use-lazy-tool-renderer.tsx
packages/react-core/src/v1-deprecated/hooks/__tests__/use-lazy-tool-renderer.test.tsx`
## Checklist
- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] Documentation is unchanged because this restores existing v1
behavior without changing the public API
- [x] "Allow edits by maintainers" is checked
## Problem
`useAgentNodeName` must update React consumers when AG-UI node events
arrive, and `useLangGraphInterrupt.enabled()` must receive the node
where an interrupt actually occurred.
Current `main` includes the basic ref-to-state reactivity fix from
[ffd1580](https://github.com/CopilotKit/CopilotKit/commit/ffd15801d6d),
but that commit explicitly leaves #1426 open: a later `RUN_FINISHED` can
still replace the interrupting node with `"end"`, and v1 consumers can
still be hidden behind `useCoAgent`'s memoized return value.
## What remains in this PR
Rebased onto current `main` (`e9387e0`) after the v1 source migration,
this PR contains only the remaining behavior:
- Preserve the last active node when `RUN_FINISHED` reports `outcome:
"interrupt"`.
- Preserve it for the legacy `on_interrupt` custom-event flow as well.
- Continue transitioning successful and failed runs to `"end"`; reset
new runs and agent switches to `"start"`.
- Add `nodeName` to the `useCoAgent` return-value memo dependencies so
v1 consumers receive the reactive update.
- Share `INTERRUPT_EVENT_NAME` between the hook and interrupt
implementation.
The public hook signatures and AG-UI protocol remain unchanged.
## Preview workflow
- Disabled pkg-pr-new's generated all-package StackBlitz template;
package preview install URLs remain available.
## Changes
- `packages/react-core/src/v1-deprecated/hooks/use-agent-nodename.ts`
- `packages/react-core/src/v1-deprecated/hooks/use-coagent.ts`
-
`packages/react-core/src/v1-deprecated/hooks/__tests__/use-agent-nodename.test.tsx`
- `packages/react-core/src/v2/types/interrupt.ts`
- `packages/react-core/src/v2/hooks/use-interrupt.tsx`
- `.github/workflows/publish-commit.yml`
## Verification
- Full React Core Vitest suite: **131 files, 1520 tests passed**.
- Preview workflow: Nx formatting and YAML parsing passed.
- `nx run @copilotkit/react-core:check-types --skipNxCache`: passed,
including all 33 dependency tasks.
- `git diff --check origin/main...HEAD`: passed.
- The composite React Core test target then reaches the existing
Windows-only script baseline: 8 path-normalization failures plus 2
symlink-permission failures. These are outside this PR's files; the
complete Vitest suite passes before that script stage.
## Scope
This intentionally does not change the AG-UI event protocol, runtime
event ordering, HITL workflow, v1/v2 compatibility layer, or the
separate node tracking in `use-coagent-state-render-bridge.tsx`.
Fixes#1426
useFrontendTool registered its tool in a useEffect. React flushes passive
effects child-first in tree order, so a consumer mounted before the
registering component runs its own useEffect against an empty tool list.
That is the cross-page-navigation failure: a page mounts CopilotChat and its
tool-registering components in a single commit, CopilotChat's connect effect
fires first, and the connect request carries no frontend tools.
Register in useLayoutEffect instead. Layout effects run during commit, ahead
of every passive effect regardless of tree order, which closes the window.
This matches useAgentContext, which already registers via useLayoutEffect.
Adds a regression test that mounts the consumer FIRST -- mounting it second
passes with either hook and proves nothing.
Re-derived against current main from mxmzb's #4259, which diagnosed this.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Load the ext-apps bridge lazily. AppBridge/PostMessageTransport now come from a
dynamic import() inside Effect 1, with a type-only import at the top. A
<CopilotKit> app no longer pays the ~40-50 kB gzipped ext-apps cost unless it
actually renders an MCP App. Verified: the built output has no static ext-apps
import, only import("@modelcontextprotocol/ext-apps/app-bridge").
- Move @modelcontextprotocol/sdk out of dependencies. It is now an optional
peerDependency (mirroring how ext-apps declares it) plus a devDependency for
our own build, so the sdk tree (express, hono, jose, ajv, cross-spawn) is no
longer an install/audit surface for every React consumer. Nothing in the
bundle reaches sdk at runtime; the only sdk usage is a type import.
- Raise the zod peer floor to >=3.25. The ext-apps/sdk schema slice imports
zod/v4 and zod/v4-mini, which only exist in zod >= 3.25; the old >=3.0.0 peer
let a consumer on zod 3.24 hit an unresolvable import at build time.
- Seed the host context at AppBridge construction (hostContext option) instead of
calling setHostContext after connect, so it is deterministically in place when
the widget's ui/initialize is handled (the seam #6689 needs to advertise
displayMode / availableDisplayModes at initialize).
- Restore the full cross-frontend testid surface-contract comment.
- Split the oncalltool guard so "no server hash" and "no agent" report distinctly.
The add-menu ("+") button's tooltip hardcoded the string "Add attachments",
so `labels.chatInputToolbarAddButtonLabel` only retitled the menu item and
the tooltip stayed English. That blocked full localization of CopilotChat
without replacing the whole add-button slot.
Every other tooltip in the v2 chat surface is already label-driven, and the
Angular implementation already derives this tooltip from the same label, so
this was an oversight rather than a deliberate split.
The "/" shortcut glyph stays hardcoded — it is a key name, not prose.
Fixes#6750
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Moves the published packages from 0.0.57 to the current AG-UI release across
@ag-ui/client, core, encoder and proto — 27 declarations in 18 packages.
0.0.59 is the first release carrying the subagent protocol surface
(SUBAGENT_STARTED/FINISHED/ERROR, subagentRunId) along with the null-omission
cleanup, so this is the dependency CopilotKit's subagent work needs.
Scope is packages/** plus the release script noted below. The examples and
showcases sit on a spread of older pins (0.0.40 through 0.0.58) and are left
alone.
One behavioural change comes with the bump. channels-core ships
sanitizeAgentEventStream because @ag-ui/client used to reject a TOOL_CALL_START
carrying parentMessageId: null — the shape @ag-ui/langgraph emits for an
interrupt-triggering tool call. 0.0.59 accepts that null and treats it as
absent, so the two tests asserting the run dies WITHOUT the sanitizer no longer
hold. They now assert the run survives, and the one at agent level still checks
the tool call actually arrives so it cannot pass vacuously. The sanitizer is
untouched and its coercion tests are unchanged; it is simply no longer the
thing keeping such a run alive.
The bump also broke the packed Angular consumer matrix. That job generates a
smoke app from scripts/release/lib/angular-package.ts, whose manifest restated
"@ag-ui/client": "0.0.57" as a literal while packages/angular moved to 0.0.59.
pnpm then installed both copies and the app failed to compile:
TS2322: Type 'SmokeAgent' is not assignable to type 'AbstractAgent'.
Types have separate declarations of a private property '_debug'.
The smoke app imports AbstractAgent directly, so it has to resolve the identical
copy the library ships against. Read that version off the packed manifest --
which verify-angular-package.ts already parses for the Angular support contract
-- instead of restating it, so no future AG-UI bump can desynchronise it.
The oxlint CI job failed on the ext-apps migration:
- copilotkit/no-single-arg-zod-record (error): the ui/message _meta field used
the single-arg z.record(z.any()), which is a compile-time error against Zod 4.
Use the two-arg form z.record(z.string(), z.any()).
- Remove the dead hand-rolled JSON-RPC message types left over from before the
AppBridge migration (no longer referenced).
Also normalize type-only imports and formatting in the touched e2e tests.