Commit Graph

553 Commits

Author SHA1 Message Date
Maxim edee011099 Merge branch 'main' into refactor/rn-render-tool-hooks 2026-09-09 21:20:48 +02:00
Tyler Slaton bba4113b8e fix(react-core): improve Inspector message shortcuts 2026-09-09 11:49:42 -07:00
Maxim b2b6bfbc3e refactor(react-native)!: converge render-tool hooks onto react-core
React Native's `useRenderTool` was not react-core's `useRenderTool`. Its entire
body forwarded to a different hook, `useFrontendTool`, while wearing the other
one's name. core has two: `useFrontendTool` registers a tool AND its renderer
via `addTool`; `useRenderTool` registers a renderer ONLY via
`addHookRenderToolCall`, and special-cases `"*"` into a schema-less fallback.

Because RN's alias took the `addTool` path, `name: "*"` registered a frontend
tool literally named `*` and advertised it to the model. Eight further symptoms
share that single cause: a render-only registration was advertised and shadowed
a same-named server tool; `addTool` evicted an existing same-named
`useFrontendTool` handler with only a `console.warn`; `description` and
`parameters` were required though both are tool fields; `followUp` and
`available` were accepted upstream but not forwarded; `handler` dropped core's
second (context) argument, leaving `stopAgent()`'s abort signal unreachable;
`agentId` changes never re-registered; two structurally incompatible
`RenderTool*Props` families shipped side by side; and `hooks/index.ts` was a
dead barrel with no build entry, no exports mapping and no importer.

Deletes RN's hook and re-exports core's two instead, so each capability has
exactly one implementation and RN carries no render-tool API of its own.

Deleting rather than re-pointing the name is deliberate. Re-pointing is the
dangerous shape: a `{ name, parameters, render }` call with no handler would
keep compiling and silently stop registering the tool.

Two of the nine are documented rather than fixed, so this is not a clean sweep:
`agentId` is still absent from both core hooks' re-registration check, so the
`deps` workaround stands; and `useDefaultRenderTool` keeps a narrower render
return than the hooks converged here.

Adds the guard the entry-surface suite was missing. It asserted only that
`useRenderTool` was *present* on the headless entry, never which hook it was, so
it would have stayed green through this entire change while verifying nothing
about it — proven by mutation: an RN-local hook re-grown under the name fails the
new identity assertion while 18 other guards in that file stay green.

Also rewrites the RN render-tool suite around what RN still owns. The previous
file tested forwarding into `useFrontendTool` through a double that modelled so
little its own header comment recorded that deleting the `deps`, `handler` and
`agentId` forwarding left it fully green. Assertions now read core's observable
state: `getTool`, `core.tools`, a real `runTool` rejection, painted DOM through
the real `useRenderToolCall`, and the tool list core hands an agent on a real
run.

BREAKING CHANGE: removes `useRenderTool`, `RenderToolProps`,
`RenderToolFunction` and `UseRenderToolOptions` from @copilotkit/react-native.
A tool-plus-renderer registration becomes `useFrontendTool` with an otherwise
identical object; renderer-only registration and the `"*"` wildcard become
core's `useRenderTool`; render props rename `args` to `parameters`. The
migration table, and the analysis of which call shapes fail loudly versus
silently, live in
showcase/shell-docs/src/content/reference/react-native/hooks/useRenderTool.mdx —
this repo's release-note collector reads only commit subjects, so a footer is
not a consumer-facing channel (see #6479) and the docs page is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 19:32:42 +02:00
Maxim dce8ee7578 refactor(react-core): let a tool renderer return null
`ReactToolCallRenderer.render` is a `React.ComponentType`, and a function
component returns `ReactNode` — of which `null` is a member. So `null` already
worked at the point a renderer is actually invoked; only the two build-side
helpers forbade it, leaving core stricter on the way in than on the way out.

Widens the render return to `React.ReactElement | null` at six annotations:
`use-render-tool.tsx` (both overloads plus the implementation config) and
`types/defineToolCallRenderer.ts` (both overloads plus the implementation
signature). Both files are required — widening only the hook yields TS2322,
because it passes `config.render`'s result straight into the helper.

Non-breaking. Widening what a caller-supplied callback may return can only
accept more code than before, so every renderer returning an element still
compiles. Tested in both directions: a renderer returning `null` registers and
paints nothing, and one returning an element behaves as before. Note that
vitest transpiles without type-checking, so `tsc --noEmit` (which CI runs via
static_quality.yml's check-types job) is the only guard against a re-narrowing.

Scope: this widens `useRenderTool` and `defineToolCallRenderer` only. It does
NOT make core uniformly permissive — `useDefaultRenderTool` still requires
`ReactElement`, so core's two wildcard entry points now differ. That divergence
fails loudly (TS2322) rather than silently, and is left to its own change since
that hook carries four narrow annotations plus an existing cast. Note
`defineToolCallRenderer` is re-exported from @copilotkit/react-native, so this
change reaches React Native consumers too — it is not web-internal.

Not purely type-level: oxlint's consistent-type-imports rule converted this
file's `ToolCallStatus` import to `import type`, which removes a runtime
`@copilotkit/core` import from the emitted JS. Harmless, since core is imported
from many other react-core modules and no initialisation order changes — but
worth stating rather than filing under "types".

Also re-homes a coverage pin that @copilotkit/react-native's suite carried:
despite living in RN it tested core's `JSON.stringify(extraDeps)` comparator,
and no react-core test covered the case, so removing RN's copy would have left
it unpinned repo-wide. It records a documented sharp edge, not behaviour worth
preserving.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 19:32:42 +02:00
Benjamin Taylor 1422019862 test(react-core): import the A2UI renderer once, not inside every test
The first test in A2UIMessageRenderer.test.tsx timed out on the
Node 24 / React 19 unit shard while the same commit passed on every other
shard. The test body is about ten milliseconds of work.

The cost was the `await import("../a2ui/A2UIMessageRenderer.js")` inside
the test. That import pulls in the whole @copilotkit/a2ui-renderer graph,
which vitest.config.mjs inlines, and vitest charges the one-time transform
to whichever test runs first. Measured locally: the first test took 502ms
of the 5000ms default timeout, and the other seventeen took 0 to 15ms
each. Under CI load the same cost reached 4798ms on a passing shard.

All eleven dynamic imports named the same module, and the file calls no
vi.resetModules(), so every one already resolved to a single cached
instance. The laziness bought nothing and cost the first test its budget.
One static import moves the work to collection, which no test timeout
bounds. Measured after the change: the first test takes 53ms, and the
import phase grows from 27ms to 468ms, which is where that work belongs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 09:19:50 -05:00
Benjamin Taylor a38a3a7e92 fix(react-core): register v1 readables before sibling effects run
useCopilotReadable published its context in a useEffect. React flushes
passive effects child-first in tree order, so a consumer mounted before
the readable runs its own useEffect against an empty context store.

That is the cross-page-navigation failure: a page mounts the chat and its
readable-publishing components in one commit, the chat's connect effect
fires first, and the connect request carries no context.

Register in useLayoutEffect instead. Layout effects run during commit,
ahead of every passive effect regardless of tree order, which closes the
window. Register and cleanup stay in the one effect, so both sides remain
in the same phase. This matches the v2 siblings useAgentContext and
useFrontendTool, the latter fixed the same way in f9b306aa4e.

Adds a regression test that mounts the consumer FIRST -- mounting it
second passes with either hook and proves nothing. The test fails on the
unmodified hook with "expected [] to include 'employees'".

Completes the half of mxmzb's #4259 that f9b306aa4e did not cover.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 22:54:51 -05:00
Benjamin Taylor 274983d4f8 fix(docs): make the redirect suffix-aware and repair dead links the regeneration surfaced
Self-review follow-ups on the reference-docs regeneration.

The LangGraphAgent redirect only covered the bare path. A raw Markdown request
reaches redirects before the .md/.mdx rewrite, so a request for
/reference/v1/sdk/python/LangGraphAgent.md would have 404'd for the LLM routes.
Use permanentRedirectsWithSuffixes, which is what the rest of the redirect
table does.

Refreshing the pages also republishes their JSDoc links, and three of those
pointed at pages that do not exist. They were invisible while the pages were
frozen; regenerating makes them live 404s, so fix them at the source:

- use-coagent-state-render.ts linked to /coagents/videos/perplexity-clone, a
  legacy URL with no content, no redirect and no rewrite. Point at
  /generative-ui/state-rendering, the canonical guide the published page
  already named.
- copilotkit-props.tsx linked to
  /coagents/shared/guides/langgraph-platform-authentication, which likewise
  does not exist. Point at /auth, which is how the rest of the docs link to
  that guide.
- use-copilot-chat.ts was flipped to
  /reference/v2/hooks/useCopilotChatHeadless_c by the URL canonicalization in
  33f669ba7b, but there is no v2 page of that name — reference/hooks has no
  headless entry. Restore the v1 page, which exists and is what the published
  page links to today. If v1 readers should be pushed to a v2 page instead,
  that page has to be written first.

Every internal link the regeneration newly publishes now resolves to a content
file.

The three package edits are JSDoc comment lines only; no code changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 21:58:48 -05:00
Benjamin Taylor e823409f96 fix(react-core): name the agent on CopilotKitProvider
`@copilotkit/react-core/v2` re-exports the v1 `<CopilotKit>` provider, and that
was the only provider carrying an agent prop. So a v2 application that wanted
to name its agent at the provider level had to reach for the v1 compatibility
component, and the reporter had to read the installed type definitions to find
that the v2 equivalent lives on `<CopilotChat agentId>` instead.

Accept `agentId` on `CopilotKitProvider`. It publishes a bare string context
that is the last fallback before `DEFAULT_AGENT_ID`, so `<CopilotChat agentId>`,
`<CopilotChatConfigurationProvider agentId>`, and an explicit `agentId` argument
to `useAgent`/`useSuggestions` all still win.

The default deliberately does NOT arrive through a root
`CopilotChatConfigurationProvider`. That provider also owns a thread: it
resolves a threadId, minting a UUID when none is given, and the top-most one
owns the imperative active-thread override. Wrapping the application in one
hands every descendant chat the same inherited threadId, so two sibling chats
share a transcript. A test renders two sibling chats under the provider and
pins that they keep their own threads.

Docs: say plainly on the `CopilotKit` reference page that it is the v1
provider, and note the prop rename on the provider-and-handler-pairs page.
Both pages claimed that released versions of `<CopilotKit>` pin
`useSingleEndpoint` to `true`; that pin was removed in 1.70.2, so both
providers now negotiate the transport when the prop is omitted.

Fixes OSS-1133

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 18:40:40 -05:00
Benjamin Taylor b0f349fcfb fix(a2ui): report a surface whose root component never resolves (closes OSS-1057)
Both renderers begin walking a surface at the component with id "root" and
treat an id they cannot find as not arrived yet, painting an animated
placeholder. That is right while operations stream. Once operations have
stopped it is not waiting, it is stuck — and every existing check calls it
healthy: the surface exists, processMessages does not throw, the component
type is never reached so the "Unknown component" branch cannot fire, and
surfaceHasRenderableContent says yes on the strength of components plus a
non-empty data model, so onReady fires and the never-painted report is
suppressed by its own guard. A complete, accepted payload therefore animates
a grey box forever with nothing in the console.

Reports it on the existing paint deadline, measured from the last operations
to land, so a root still missing when it expires is a root that is not
coming. The check reads the live components model rather than scanning the
operations for the id, which covers every way a root can fail to resolve —
not only a payload that never named one — and the message says which of the
two it is.

Keeps the fixed root id: A2UI v0.9 dropped v0.8's rootComponentId, and
createSurface carries only surfaceId, catalogId and theme, so a payload has
no way to declare its own entry point. Deriving one instead would pick
silently and wrongly whenever several components are unreferenced. The id
moves to a single ROOT_COMPONENT_ID constant so the three sites that
hard-coded the string, and the new report, agree by construction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 17:49:22 -05:00
Benjamin Taylor 77dc28ea2c fix(a2ui): resolve a surface id the same way in both renderer paths (refs OSS-1048)
The two paths disagreed. React read a top-level `operation.surfaceId` first and
only then the nested v0.9 keys. The web-components path read the nested keys
only, via normalizeOperations, and never looked at a top-level id at all. So
one payload grouped under its own id in React and under "default" in the Lit
and Angular renderers.

Nested wins in both now, with a top-level id as the fallback when the payload
carries none.

Nested is the correct half of that choice, not a coin toss: MessageProcessor
creates the surface from the nested id. Grouping by a top-level id instead
files the operations against a surface that createSurface never made, and an
unknown surface id renders A2UIRenderer's null fallback. That is a card that
paints nothing, which is what the previous commit taught the renderer to
report.

So the old React order could produce the silence, and the missing-surface
report is what the new test uses to prove the grouping agrees with what got
created.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 17:42:52 -05:00
Benjamin Taylor 0f7ee3501a fix(react-core): report an A2UI card that receives operations and paints nothing (refs OSS-1048)
Two ways an A2UI surface can render nothing without saying a word.

The operations arrive and no paint follows. The renderer already waits 8s for
the surface to report its first paint before dropping the loader, so reaching
that fallback is itself the signal that nothing painted. Report it there, and
use what surfaceHasRenderableContent already knows to say which half is
missing: no updateComponents at all, or bound components whose updateDataModel
never carried a value.

The operations name a surface that was never created. A2UIRenderer renders its
fallback for an unknown surface id and that defaults to null, so the card is
absent and the log is empty. processMessages is synchronous, so a surface
still missing after it was never created.

The second report is deferred a task and re-checked, because operations stream
and a snapshot can reach the processor before the createSurface that gives it
somewhere to go. Removing both the deferral and the re-check makes the
mid-stream test fail.

Neither report covers a surface that exists and holds complete components and
still draws nothing. That case needs the component catalog, which lives in
@a2ui/web_core, and it stays silent for now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 17:42:52 -05:00
Benjamin Taylor 22fa691e80 fix(react-core): warn when a tool call has no renderer instead of rendering nothing (refs OSS-1048)
A tool call that matches no registered renderer returns null. Nothing else
happens: no console output, no empty-state card, and a finished turn. The only
signal is a blank message container in the chat, which a developer has to
notice in the DOM and then guess at.

Report it in development. The warning names the tool the agent called, lists
the renderer names that are registered, and points at useRenderTool and
useDefaultRenderTool. When the cause is a name that does not match, that is
the whole diagnosis.

Rendering behavior is unchanged. Auto-painting a default card would leak tool
names and raw args into production chat, which is why the resolver returns
null, and that decision stands.

The report is deferred one task past the commit that recorded the miss, then
re-checks the registry. useRenderTool registers from an effect in the
component that renders the chat, and React runs child effects before parent
ones, so at effect time the resolver can see an empty registry even though the
app did register a renderer. Removing that re-check makes two of the new tests
fail on exactly that false positive.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 17:42:52 -05:00
Benjamin Taylor 2031643fa1 fix(react-core): wait for the user on humanInTheLoop provider tools
The `humanInTheLoop` prop on `CopilotKitProvider` registered a handler that
warned and resolved `undefined` the moment the agent invoked it, and it
registered the renderer unwrapped, so the render never received a working
`respond`. A tool declared that way jumped straight to Complete over dead
controls while the agent was told the tool had succeeded. The placeholder is
unchanged since the first v2 provider commit (a2ef51aaf3), so this documented
prop has never waited on the user. It fails the same way on both transports.

Match the `useHumanInTheLoop` semantics: park the tool call until the render
calls `respond`, expose `respond` only while the status is Executing, and
reject with 'Human-in-the-loop interaction aborted' when the run is aborted so
core records an explicit error tool result (#5554). Pending interactions are
keyed by tool call id, and a wildcard registration keeps the invoked tool's
name rather than "*".

Two existing unit tests asserted the placeholder behavior (the console warning
and render identity). They now assert the waiting contract and that the wrapper
renders the caller's component.

Mirrors the Vue fix in #6527. Related: #4953 (the provider-prop half of that
report) and #4955.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 17:31:28 -05:00
Benjamin Taylor 82cd7c01f7 fix(react-core): let a custom catch-all renderer return null
`useDefaultRenderTool`'s `render` was typed to return `React.ReactElement`,
so a caller who wanted to render only some tool calls could not return
`null` to suppress the built-in default for the rest. The value already
flowed through correctly at runtime; only the type rejected it.

Widen the public `render` return type, and the wrapper local that carries
the user's value, to `React.ReactElement | null`. The `as unknown as` cast
into `useRenderTool` stays, because `useRenderTool` still requires a
`ReactElement` return on main; PR #6533 widens that hook, after which the
cast can be tightened.

Guarded by a `.test-d.ts` assertion rather than a runtime test: types are
erased, so a null-returning render forwards identically before and after
the widening and a runtime test would assert nothing.

Extracted from #5509, which is otherwise stale.

Co-authored-by: Atai Barkai <atai.barkai@gmail.com>
2026-09-08 16:39:06 -05:00
Ben Taylor 42494df607 docs(react-core): stop pointing v1 appendMessage at non-public sendMessage (#6940)
## Summary

The v1 `useCopilotChat` JSDoc tells readers to use `sendMessage` instead
of `appendMessage`. `sendMessage` is not part of the public v1 return
type, so following that advice does not compile.

`packages/react-core/src/v1-deprecated/hooks/use-copilot-chat.ts:109`
explicitly omits it:

```ts
export type UseCopilotChatReturn = Omit<
  UseCopilotChatReturnInternal,
  | "messages"
  | "sendMessage"     // <- the JSDoc points readers here
  ...
```

`sendMessage` exists only on `useCopilotChatInternal`. The public hook
returns `appendMessage`, which is the working v1 programmatic-send path.

This replaces the misdirection with the v2 migration pointer and states
plainly what `appendMessage` is for. Comment-only change — no runtime
effect.

Found while closing #4215, where a user was told by our own docs to call
a method we do not export.

## The published page does not change yet

This fixes the source of truth. The generated page cannot be refreshed
until #6939 is resolved: running the generator today would also embed
the internal v1 deprecation banner ("AI CODING AGENTS: Never copy,
suggest, or generate these v1 APIs") into 20 public reference pages. I
deliberately excluded the regenerated `.mdx` files from this PR rather
than ship that. Once #6939 lands, a regenerate publishes this wording.

## Testing

**1. `oxfmt --check` — pass**

```
$ ./node_modules/.bin/oxfmt --check packages/react-core/src/v1-deprecated/hooks/use-copilot-chat.ts
Checking formatting...
All matched files use the correct format.
Finished in 33ms on 1 files using 18 threads.
```

**2. Generator reads this file successfully (26/26)** — confirms the
JSDoc edit is picked up, and that the only thing blocking publication is
#6939, not this change:

```
$ ./node_modules/.bin/tsx scripts/docs/gen.ts
Successfully autogenerated showcase/shell-docs/src/content/reference/v1/hooks/useCopilotChat.mdx from packages/react-core/src/v1-deprecated/hooks/use-copilot-chat.ts
All reference docs processed (26/26 succeeded)
```

The regenerated page contained the new wording as expected; I then
reverted the generated files per the section above.

**3. Diff is a single comment hunk** — verified with `git diff --stat`:
`1 file changed, 4 insertions(+), 1 deletion(-)`, all inside a JSDoc
block. No exported symbol, type, or runtime line touched, so no
typecheck or test surface is affected.

**4. Claim verified against `origin/main`,** not a local branch: `git
show
origin/main:packages/react-core/src/v1-deprecated/hooks/use-copilot-chat.ts`
confirms both the misdirecting line (`:70`) and the `Omit` (`:109-121`).

## Notes

- No changeset — comment-only.
- Branch name is a leftover misnomer
(`ben1/oss-docs-generator-v1-paths`); the change is the wording fix
only.
- @ataibarkai's #6653/#6654/#6655 stack would move this file back to
`packages/react-core/src/hooks/`. If that stack lands, this one-line
change needs carrying forward into the reapply.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Documentation**
  - Clarified the deprecated `appendMessage` option in `useCopilotChat`.
  - Documented its role for programmatic sending in v1.
  - Clarified the migration path for AG-UI format users moving to v2.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-08 16:16:43 -05:00
Benjamin Taylor 0217f45d73 docs(react-core): stop pointing v1 appendMessage at non-public sendMessage
The v1 `useCopilotChat` JSDoc told readers to use `sendMessage` instead of
`appendMessage`, but `UseCopilotChatReturn` omits `sendMessage` from the
public return type, so following that advice does not compile. Point at the
v2 migration path instead, and state that `appendMessage` is the public v1
programmatic-send path.

Reported via #4215.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 13:21:39 -05:00
Doniyor 62c895fee2 fix(react-core): default attachment uploads to one at a time
`maxConcurrentUploads` defaulted to 3, which changed when a public
`onUpload` is called with no code change on the app's side: a handler
written when uploads were serial could suddenly see the next file start
before the previous one finished. Concurrency is now something the app
asks for, and `maxConcurrentUploads: 3` restores the pool.

Queueing the whole selection up front is kept at every limit — it shows
the user what they picked rather than changing a contract.

The default test now pins one-at-a-time; a separate test pins that
`maxConcurrentUploads: 3` really runs three. Docs, the `AttachmentsConfig`
JSDoc and the react-core skill reference say `1`.
2026-09-07 15:02:30 +05:00
Doniyor c237f29dbb fix(react-core): share the upload pool across processFiles calls
The worker pool was per `processFiles` call, so a paste landing while a
dropped selection was still uploading opened its own set of workers —
two overlapping selections could run 2× the limit, and
`maxConcurrentUploads: 1` gave one upload per call rather than one at a
time.

Move the queue and the worker count onto the hook: workers are counted,
not owned by a call, and a call tops the pool up to the limit instead of
starting a fresh one. Each call still resolves when its own files have
settled.

Also pin `Infinity` as "no limit" with a test, and say in the docs that
the limit covers everything in flight rather than each batch.
2026-09-07 15:02:30 +05:00
Doniyor 62067b76d1 feat(react-core): upload attachments concurrently
`processFiles` walked the valid files in a `for` loop and awaited each
upload inside it, so `onUpload` was called for one file only after the
previous had finished — attaching 8 files to a chat cost 8 sequential
round trips to whatever storage the app uploads to.

Queue the whole selection first, then drain it with a bounded worker
pool: `maxConcurrentUploads` on `AttachmentsConfig` sets the bound and
defaults to 3, and `1` restores one-at-a-time uploads for an endpoint
that wants them. `onUpload` may now be called concurrently.

Queueing up front also means a file waiting for a free slot is already
visible as `uploading` rather than appearing once its upload starts.

The Vue and Angular bindings read the same config type and still upload
serially; they can follow separately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 15:02:30 +05:00
Ben Taylor 8c629c147b docs(showcase): document frontend-driven activity cards (refs #3388) (#6904)
## 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 -->
2026-09-04 17:32:15 -05:00
Ben Taylor 5fd08a824e feat(react-core): controlled open/onOpenChange props for CopilotSidebar and CopilotPopup (#6905)
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 -->
2026-09-04 15:39:28 -05:00
Ben Taylor 603bc16cf3 fix(react-core): restore code block line breaks in packaged CSS (#3330) (#6902)
## 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 -->
2026-09-04 15:38:59 -05:00
Ben Taylor 4da2d7e34e fix(core): hydrate self-hosted threads whose /connect replay contains an errored run (#6528)
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 -->
2026-09-04 15:37:42 -05:00
Benjamin Taylor 91abd959f7 feat(react-core): controlled open/onOpenChange props for sidebar and popup
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
2026-09-04 15:11:41 -05:00
Benjamin Taylor 3672d007ae docs(showcase): document frontend-driven activity cards, lock the behavior with a test
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>
2026-09-04 15:05:30 -05:00
Benjamin Taylor b5a6d05ae9 fix(react-core): restore code block line breaks in packaged CSS (#3330)
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>
2026-09-04 14:57:50 -05:00
Mike Ryan 2cde6b97f7 fix(runtime): preserve single-route resource context 2026-09-04 10:36:27 -07:00
Mike Ryan 840ad3c14a feat(runtime): support Intelligence over one route 2026-09-04 10:36:27 -07:00
Alem Tuzlak 8a5a976f1a feat(core): expose webmcp-enabled frontend tools to browser agents (#6847)
## 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 -->
2026-09-03 14:15:29 +02:00
github-actions[bot] a31daf3c78 style: auto-fix formatting 2026-09-02 15:21:27 +00:00
Alem Tuzlak 808113b923 fix(core): keep webmcp registrations stable across stale rejections and reactive changes 2026-09-02 17:19:34 +02:00
Alem Tuzlak aeea432cb1 feat(adapters): support the webmcp option in react, vue, and angular tool hooks 2026-09-02 16:26:12 +02:00
Lukas Moschitz 9bd7acc5ce fix(shared): drop the retired "premium" tier name from the console notice
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
2026-09-02 12:19:50 +02:00
Maximiliano Korp 25cbb372aa docs(react-core): update Learning Container deprecation guidance 2026-09-01 14:01:28 -07:00
yannj-fr 82c2ffbfc6 test(react-core): fix stale allowlist wording in the open-link scheme test
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.
2026-09-01 16:03:14 +02:00
Yann Jouanin b12ba3e3a5 Merge branch 'main' into feat/migrate-ext-app-package 2026-09-01 15:40:55 +02:00
yannj-fr 2544f1ff4c test(react-core): cover ui/initialize negotiation + address round-three nits
- 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.
2026-09-01 15:40:09 +02:00
Max Korp b07b1320f2 feat(runtime): use managed Intelligence authority (#6098)
## 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 -->
2026-08-31 11:55:42 -07:00
Ben Taylor 6ae23c0df0 fix(react-core): register frontend tools before sibling effects run (#6794)
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 -->
2026-08-31 13:00:20 -05:00
Maximiliano Korp 57a9229e7d fix(react-core): preserve inspector button props 2026-08-31 10:46:15 -07:00
Maximiliano Korp cd7748f25b fix(runtime): harden entitlement resolution 2026-08-31 10:46:15 -07:00
Mike Ryan 2d26a9de1a fix(react-core): isolate assistant inspector context updates 2026-08-31 10:46:13 -07:00
Mike Ryan f1ac08938a feat(runtime): use managed Intelligence authority 2026-08-31 10:46:12 -07:00
Benjamin Taylor c5107bee53 fix(core): keep the connect-not-implemented escape hatch in the shared pipeline
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>
2026-08-31 08:11:21 -05:00
Benjamin Taylor 35616f46a2 fix(core): hydrate self-hosted threads whose replay contains an errored run
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>
2026-08-31 08:11:01 -05:00
Benjamin Taylor 37e69105d8 fix(react-core): tear down HITL renderer in the same effect phase it is registered
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.
2026-08-31 08:08:48 -05:00
Benjamin Taylor ebbba19101 Merge origin/main into ben1/fe-tool-registry-4952-1746
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.
2026-08-31 07:58:44 -05:00
yannj-fr 661fb1981d fix(react-core): address CodeRabbit findings on the MCP Apps bridge
- 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.
2026-08-31 13:30:26 +02:00
yannj-fr f794386528 Merge remote-tracking branch 'upstream/main' into feat/migrate-ext-app-package
# Conflicts:
#	pnpm-lock.yaml
2026-08-31 08:15:25 +02:00
yannj-fr 80d530620c refactor(react-core): make the MCP SDK a hard peer and guard the lazy import
- @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.
2026-08-31 08:12:12 +02:00