## Release angular v0.5.0
**Scope:** `angular` | **Bump:** `minor`
---
### How this release process works
1. **This PR was created automatically** by the "release / create-pr"
workflow.
It bumped the `angular` packages to `0.5.0`
and generated AI-enhanced release notes.
2. **CI runs on this PR** — the full test suite (unit tests, lint, type
checks, build)
must pass before merging. This is the review gate.
3. **Review the release notes** in `release-notes.md` in this PR.
If a Notion draft was created, you can edit the release notes there
before merging.
4. **When this PR is merged**, the `release / publish` workflow
automatically:
- Builds all packages
- Publishes the `angular` packages to npm at version `0.5.0`
- Creates git tag `angular/v0.5.0`
- Creates a GitHub Release with the final release notes
### Before merging
- [ ] CI is green (tests, lint, types, build)
- [ ] Version bumps look correct
- [ ] Release notes are accurate (edit in Notion if a draft was created)
---
> **Do not merge until CI is fully green.** The full test suite runs
automatically on this PR.
An agent had no trustworthy way to learn who was speaking. The actor reaches
it only inside a context entry — prose in the prompt — so an agent that needs
to act as that person has to read the sentence and copy the id into a tool
argument, which makes the model the source of an identity and lets message
text influence whose account gets used.
The actor now travels in `forwardedProps.channelActor` on every run of a turn,
including a resume, where a resume carries whoever pressed the button rather
than whoever started the turn. The Thread supplies it from its own ingress, so
a caller cannot pass an identity of its own choosing.
AG-UI messages carry an optional author and putting it there would have been
the smaller change, but that field is part of the message list handed to a
model provider and providers validate it: OpenAI accepts only `[A-Za-z0-9_-]`
in a message author, Teams actor ids contain a colon, and adapters bound an
actor id at 512 characters and nothing else. Sanitizing an id to fit is worse
than failing, because a mangled id still reads as an identity and answers as
the wrong person. `forwardedProps` never enters the conversation, so no
provider inspects it and the value stays what the adapter reported.
`id` is provider-scoped, so `platform` travels with it — a consumer keying
anything per person keys on both. A turn that named nobody carries no
`channelActor` key at all rather than one holding an empty string.
An empty user id passed the truthiness guard, so the post went out to this
turn's own recipient and reported success for a recipient the caller never
identified. Comparing the requested id with the recipient directly refuses it
alongside a request naming somebody else.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
## Release monorepo v1.70.0
**Scope:** `monorepo` | **Bump:** `minor`
---
### How this release process works
1. **This PR was created automatically** by the "release / create-pr"
workflow.
It bumped the `monorepo` packages to `1.70.0`
and generated AI-enhanced release notes.
2. **CI runs on this PR** — the full test suite (unit tests, lint, type
checks, build)
must pass before merging. This is the review gate.
3. **Review the release notes** in `release-notes.md` in this PR.
If a Notion draft was created, you can edit the release notes there
before merging.
4. **When this PR is merged**, the `release / publish` workflow
automatically:
- Builds all packages
- Publishes the `monorepo` packages to npm at version `1.70.0`
- Creates git tag `monorepo/v1.70.0`
- Creates a GitHub Release with the final release notes
### Before merging
- [ ] CI is green (tests, lint, types, build)
- [ ] Version bumps look correct
- [ ] Release notes are accurate (edit in Notion if a draft was created)
---
> **Do not merge until CI is fully green.** The full test suite runs
automatically on this PR.
One `@bot say hi` in a Slack channel produced about fifty identical
replies in eight seconds. The loop is in shared ingress code, so it
reaches any Channels app on Slack, not just the one that found it.
## What happens
`message_changed` does not mean somebody edited the text. Slack also
sends it when a reply lands in the message's thread, when a link
unfurls, and when attachments are added.
Every one of those carries a fresh `revisionId`, and that is exactly
what the engine's inbound dedup keys on — deliberately, so a real edit
is never swallowed as a duplicate. So each announcement arrived as a
fresh mention and started another turn: the bot answered, the answer
revised the parent message, the revision asked the same question again,
and the next answer revised it again.
Instrumenting one mention showed eleven turns reaching the app. One was
the original message. Ten were revisions of it.
## The fix
A revision is a turn only when its text actually differs.
`previous_message` is the direct evidence where Slack sends it. Where it
does not, `edited` stands in: Slack stamps that only when a person
edited the message, and the metadata-only revisions that cause the loop
have neither. Edited mentions still reach the app, deletes are
untouched, and the normalizer stays pure — so this covers the
Intelligence-side webhook ingress that shares it as well as the local
adapter.
## Verification
- `packages/channels-slack`: 414 tests pass, three of them new.
- Each new test fails without the guard (checked by reverting it).
- Found and confirmed against a live Slack workspace, running an OpenTag
Composio integration. After the fix: one mention, one agent run, one
reply.
The app that hit this carries a local workaround — it ignores any turn
that is not the first revision of a message — which it can drop once
this ships.
## 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 -->
The managed adapter declared `supportsEphemeral: false` and implemented no
private post, so `Thread.postEphemeral` had nothing behind it on the
managed path. Anything needing one message for one person had to leave the
managed path entirely — an app in this position ended up holding its own
Slack tokens and running a direct adapter for a single card's link.
`postEphemeral` now emits the `slack.message.ephemeral` delivery effect and
the capability says true.
Who receives it is not the caller's choice. The delivery boundary posts to
the turn's own fenced recipient and rejects an effect whose asserted user
disagrees, so a request naming somebody else is refused here rather than
sent — the boundary would refuse it anyway, and failing locally can say
which user was expected. A turn Intelligence could not attribute to a
person is refused for the same reason.
Teams returns `null`, the contract's "native unsupported" answer, so a
caller's DM fallback still engages instead of a private message silently
becoming a public one.
No `MessageRef` comes back: Slack answers an ephemeral post with
`message_ts`, which no API accepts, so there is nothing to update or delete
later. `usedFallback` is always false — the managed path has no DM route.
Requires the `slack.message.ephemeral` effect in Intelligence
(CopilotKit/Intelligence#1008), which must ship first. Until it does, the
effect is rejected at the boundary rather than silently dropped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 -->
## Why
Angular had no counterpart to `useComponent` in
`@copilotkit/react-core/v2` and `@copilotkit/vue/v2`. Letting an agent
display one of your components meant either `registerFrontendTool` with
a handler the component does not need, or `registerRenderToolCall`,
which draws a tool the agent already has and therefore requires that
tool to exist in the agent.
That gap is load-bearing for OSS-1034, which ends the default onboarding
proof at a component rendered through the frontend's own registration.
Every other supported frontend ships `useComponent`; Angular was the
only one that could not express the shape.
## What changed
**`FrontendToolConfig.handler` is now optional**, and
`CopilotKit.#bindClientTool` returns the config unwrapped when it is
absent. This is the actual missing capability. Core already has the
render-only path — a tool that declares no handler gets an empty tool
result and the turn completes (`packages/core/src/core/run-handler.ts`,
the `if (tool.handler)` guards around lines 854 and 1112). Angular bound
a wrapper unconditionally:
\`\`\`ts
handler: (args, context) =>
runInInjectionContext(injector, () => handler(args, context)),
\`\`\`
so a display-only tool called \`undefined\` on its first tool call. A
stub handler is the wrong fix in the other direction: whatever it
returns becomes the tool's result in the thread, which is a fabricated
result rather than an absent one.
**`registerComponent`** is that shape with the model-facing description
built for it — the same prefix react-core and vue build, so the tool
reads identically to the model whichever frontend registered the
component. It carries no `handler` field at all: a display-only
component that quietly ran application code would be a different
feature, and a caller who wants both wants `registerFrontendTool`.
The component stays an ordinary `ToolRenderer` with a required
`toolCall` signal input reading `toolCall().args`. No new component
contract, and it renders through the existing `pickToolCallHandler` →
`NgComponentOutlet` path unchanged.
Because the tool is declared by the frontend and forwarded over AG-UI, a
display-only component needs nothing added to the agent — on any
framework, Python included.
\`\`\`ts
registerComponent({
name: "show_incident",
description: "Show one incident from the incident table.",
parameters: z.object({ id: z.string(), severity: z.string() }),
component: IncidentCardComponent,
});
\`\`\`
## Docs
- New `reference/angular/functions/registerComponent.mdx`, wired into
the reference index callout and the `public-api.mdx` summary.
- A display-only section and a new path-table row in the Angular
generative-UI guide.
- `packages/angular/API.md` — the `public-api-documentation.spec.ts`
gate fails on any undocumented export, which is how the omission
surfaced immediately.
The reference page also carries the grounding warning: the model fills
these props from what it knows, so a component rendered over records the
application does not hold looks identical to a correct one in a browser
and in a screenshot.
## Testing
Five tests written before the implementation, confirmed red (`(0 ,
registerComponent) is not a function` ×4, plus the handler-less binding
test) with the 14 pre-existing registration tests still passing, then
green.
- `pnpm nx run @copilotkit/angular:test` — 49 files, 321 passed, 1
skipped.
- `pnpm nx build @copilotkit/angular` — passes, so widening `handler` to
optional broke no caller.
- `showcase/shell-docs` — 66/68 files, 480 tests pass. The 2 failures
are pre-existing on `main` and read files this branch does not touch (a
shared inspector snippet, and mastra tool-rendering content).
- `pnpm run check-format` — the 22 reported files are all pre-existing;
none is in this diff.
Refs OSS-1034
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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