## What does this PR do?
Lets a developer open a saved Inspector thread in the live official
chat.
- New header action: **View in your app**
- Official React and Vue chat switch to that thread
- A pinned `threadId` does not block the switch
- **Stop viewing** or an app thread change restores the previous thread
- Example threads have no action
- Production builds hide the action
- Same agent only. No matching official chat shows an error in the
Inspector
Core owns a two-way EventClient bridge
(`@tanstack/devtools-event-client`). The root import is a no-op in
production.
Docs: Inspector guide, section **View a thread in your app**.
## Related PRs and Issues
-
https://linear.app/copilotkit/issue/OSS-871/new-features-add-a-new-view-thread-in-your-app-feature
## Checklist
- [x] I have read the Contribution Guide
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation
- [x] Allow edits by maintainers is checked
Open Inspector Event Snippets on localhost. You can compile, save, and
replay AG-UI events in chat. Chat shows a bookmark icon next to a tool
call, an A2UI block, or generative UI. Click the icon to save that turn
as a snippet.
## What does this PR do?
This PR adds the Inspector Event Snippets pane.
You can:
- Compile a snippet from a recipe (tool-call, reasoning, text, activity,
raw)
- Save snippets in origin-scoped localStorage
(`cpk:inspector:event-snippets`)
- Import and export snippets from the pane header
- Replay a snippet into live chat through Inspector-only Core inject
Each Run remints `messageId`, `parentMessageId`, `toolCallId`, and
`runId`. The second Run of the same snippet is a new turn.
On localhost, chat shows a bookmark icon beside a tool call, A2UI block,
or generative UI. The icon is absolutely positioned. It hangs to the
right when there is room. Otherwise it hangs to the left. The card stays
full chat width.
The React demo adds `sayHello`, `getTime`, `addNumbers`, and a **Call 3
tools** suggestion.
## Related PRs and Issues
- Linear
[OSS-874](https://linear.app/copilotkit/issue/OSS-874/new-features-also-allow-users-to-emit-specific-events-from-the)
## Checklist
- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation
- [x] "Allow edits by maintainers" is checked (lets us help iterate on
your PR directly — faster turnaround for everyone)
## Testing
### Commands run
1. Lefthook pre-commit ran `nx` targets `test`, `publint`, and `attw`
for 27 affected projects. All passed.
2. I did not run `pnpm test:pr` (full repo). Lefthook ran the affected
package matrix only.
### Manual test
1. Run `pnpm demo:react` from the repo root.
2. Open http://localhost:3000
3. Open Inspector and select Event Snippets.
4. In chat, click **Call 3 tools**. Confirm three tool cards at full
chat width, with the bookmark hanging outside the card.
5. Click a bookmark, then click Run twice. Chat shows a second turn with
new IDs.
### How this PR makes testing easy
- `packages/web-inspector/src/lib/__tests__/event-snippets.test.ts`
- `packages/core/src/__tests__/inspect-inject.test.ts` (covers two
injects)
- React demo: `examples/v2/react/demo/src/app/page.tsx`
## Linked issues
Linear
[OSS-874](https://linear.app/copilotkit/issue/OSS-874/new-features-also-allow-users-to-emit-specific-events-from-the)
## Risk / rollback
- If ID remint is wrong, a second Run can no-op or duplicate a turn.
- The save icon shows on localhost Inspector (or when `showDevConsole`
is `true`).
- Rollback: revert this PR.
## Public API change
**Before**
Angular has no Inspector service.
```ts
// no CopilotInspector export from @copilotkit/angular
```
**After**
```ts
import { CopilotInspector } from "@copilotkit/angular";
const inspector = inject(CopilotInspector);
inspector.openInspector({
messageId: "msg-1",
menu: "event-snippets",
});
```
React and Vue apps that already mount Inspector on localhost need no new
caller code. Chat wires the bookmark through Inspector context.
`@copilotkit/core` exports `ɵinjectInspectorEvents` for Inspector only.
App code must not call it. There is no public Core emit API.
- Raw recipe: the Events JSON input now calls requestUpdate, so Run and
Save stop being permanently disabled.
- Tool args recovery: one depth scan replaces the parse-every-prefix loop.
Truncated args from a streaming tool call now fail at once, not after
seconds of blocked main thread.
- Chat bookmark: hidden while the tool arguments are incomplete, so a
partial payload cannot be captured.
- saveEventSnippet: React, Vue, and Angular wrap the body, so a compile or
storage failure is reported instead of becoming an unhandled rejection.
- Vue and Angular now gate the in-chat affordances on a dev build plus
localhost, the same as React. showDevConsole: true on a staging URL no
longer puts a bookmark into a production chat.
## What does this PR do?
Makes the existing Vue tool-call memoization lint-valid without changing
rendering behavior.
The existing `v-memo` placement on the fallback renderer inside the
tool-call loop violates the `vue/valid-v-memo` placement constraint.
This refactor introduces a Vue-valid component boundary while preserving
the optimization contract:
- Named `#tool-call-<toolName>` and generic `#tool-call` consumer slots
remain reactive.
- Only the registered fallback renderer remains memoized.
- The fallback memo boundary matches the React counterpart's
renderer-level optimization.
The focused tests act as behavior-preservation and regression guards for
slot updates and fallback rerender prevention.
## Related PRs and Issues
- None.
## Verification
- `CI=1 pnpm exec nx run @copilotkit/vue:test -- --run
src/v2/components/chat/__tests__/CopilotChatToolCallsView.test.ts
--reporter=dot` — 14/14 passed.
- `CI=1 pnpm exec nx run @copilotkit/vue:check-types` — passed.
- `CI=1 pnpm exec nx run @copilotkit/vue:build` — passed.
- Direct ESLint from `packages/vue` on all touched source/test files —
passed.
- `git diff --check upstream/main...HEAD` — passed.
- `CI=1 pnpm exec nx run @copilotkit/vue:lint` remains blocked by 172
pre-existing errors in unrelated files; no package-wide lint cleanup is
included.
- The broad commit-hook suite encountered an unrelated SSR timeout; the
final tree was not changed afterward.
Coverage preserves generic and named slot updates, unchanged fallback
rerender prevention, tool-name changes, agent-specific renderer
selection, status/result behavior, and renderer precedence.
## Scope and exclusions
This is limited to the Vue tool-call rendering boundary, its parity
note, and focused tests. It does not change React behavior, package-wide
lint errors, attachment work, or unrelated rendering paths.
## Checklist
- [x] Contribution guide and package instructions reviewed.
- [x] Relevant Vue parity documentation updated.
- [x] Allow edits by maintainers is enabled.
Let the Inspector load a saved thread into the official React or Vue chat. Core owns a two-way EventClient bridge. Official chat configuration applies an in-memory override that wins over a pinned threadId. Production builds hide the action.
## Problem
Vue's `useAgent` implemented per-thread agent **cloning** — a mechanism
React never had. Passing a `threadId` silently handed you a copy of the
agent:
```ts
useAgent({ agentId: "assistant", threadId: "thread-1" }) // → a clone, keyed (agent, threadId)
```
The clones lived in a module-level `WeakMap` (`globalThreadCloneMap`),
so:
- Nothing tied a clone's lifetime to the scope that created it — they
were never released.
- Components had to *look up* which copy was live.
`CopilotChatMessageView` called `getThreadClone(registryAgent,
config.threadId) ?? registryAgent` just to find the agent actually being
rendered.
- `getThreadClone` / `globalThreadCloneMap` were exported from the
module purely so components could do that lookup.
Meanwhile React grew an explicit contract for the same use case in
#6141: a private *proxied* agent, registered under a local `agentId` and
routed to a `runtimeAgentId`.
## Change
Deletes cloning entirely and ports React #6141's contract to Vue.
`cloneForThread`, `getOrCreateThreadClone`, `getThreadClone` and
`globalThreadCloneMap` are gone — zero references remain, including in
prose.
`UseAgentProps` becomes a base plus a two-branch union, with the same
all-or-nothing rule React now enforces:
```ts
useAgent() // shared registry agent
useAgent({ agentId }) // shared registry agent
useAgent({ agentId, runtimeAgentId, threadId }) // private proxied agent
```
Every partial set — `{ agentId, threadId }`, `{ agentId, runtimeAgentId
}`, `{ runtimeAgentId, threadId }` — is a compile error, backed by the
same three runtime guards with the same messages for callers TypeScript
doesn't reach.
### Parity with #6141
| | React (#6141) | Vue (this PR) |
|---|---|---|
| scoped branch | `agentId` / `threadId` / `runtimeAgentId`, all
required `string` | same, as `MaybeRefOrGetter<string>` |
| unscoped branch | `agentId?: string`, `threadId?: undefined`,
`runtimeAgentId?: undefined` | identical |
| runtime guards | 3 | same 3, same messages |
| thread resolution | prop → chat config, gated on `hasExplicitThreadId`
| identical |
| proxy registration | balanced effect on core + both ids | same deps |
## Two Vue-specific details
Both are load-bearing and were found by tests failing, not by
inspection:
**The pin watcher's first source is `() => agent.value`, not `agent`.**
Vue sets `forceTrigger` when any array watch source is a shallow ref, so
passing the ref directly re-ran the pin on *every* `triggerRef(agent)` —
i.e. every streamed message — re-pinning the inherited thread over one
`CopilotChat` had deliberately set for the chat it renders. Two existing
suites cover this (`uses the explicit agentId and threadId over
inherited configuration`). React has no equivalent hazard because effect
deps compare by identity.
**`CopilotChat` assigns `agent.threadId` inside its `/connect`
watcher**, not a separate one. `CopilotKitCore.connectAgent` reads that
field *synchronously* (`run-handler.ts`) to decide whether a restore is
fresh, so a later assignment lets `/connect` address the previous thread
— skipping the messages/state reset and re-stamping its restore key with
the stale id. Same placement as React's `CopilotChat`.
`CopilotChatMessageView` now resolves the registry agent directly
instead of consulting the clone map, and reads `copilotkit.agents` so it
recomputes when the registry changes.
## What callers see
**One agent per `agentId`** — the model React has always had. Thread
isolation is now explicit instead of implicit: ask for it and you get a
real, separately-registered agent rather than a copy that appears out of
nowhere.
```ts
// before — silently produced a copy of the "assistant" agent
useAgent({ agentId: "assistant", threadId: "thread-1" })
// now — an explicit private agent of your own, routed to "assistant"
useAgent({ agentId: "chat-1", runtimeAgentId: "assistant", threadId: "thread-1" })
```
Nothing in this repo needed updating: `CopilotChat`, `use-capabilities`,
`use-interrupt` and all six example apps already used `{ agentId }`.
`<CopilotChat agentId threadId>` is unchanged for consumers.
## Tests
`use-agent-thread-isolation.test.ts` (433 lines) covered clone semantics
that no longer exist; it's replaced by
`use-agent-thread-pinning.test.ts`, which pins the new invariants — one
instance per `agentId` never a copy, config-thread pinning gated on
explicitness, and all three all-or-nothing guards.
Four component suites used `getThreadClone` purely as a lookup to find
the agent under test and now read from the registry.
`MockMCPProxyAgent` recorded `addMessage` **only inside its `clone()`
override**, so those assertions were passing only because cloning
existed. The recording moves onto the class. `clone()` itself is left
intact everywhere — `CopilotKitCore`'s `SuggestionEngine` still clones
agents (`packages/core/src/core/suggestion-engine.ts`), so removing
those overrides would have planted a latent trap.
## Deliberately not included
Found while reviewing this area, real, but out of scope — each wants its
own change:
- `useAgent`'s header watcher **replaces** `agent.headers` instead of
calling `copilotkit.applyHeadersToAgent()`, dropping per-agent
construction-time headers. Regresses #5635 in Vue; React does this
correctly.
- `credentials` never reach a provisional agent.
- No `onAgentsChanged` subscription anywhere in `packages/vue`, so `()
=> copilotkit.value.agents` as a watch source never re-evaluates on
registry change.
- `/connect` is skipped for a plain `HttpAgent` — the `hasCustomConnect`
prototype comparison matches every real agent. Vue-only, no React
equivalent.
- `CopilotThreadsDrawer.ssr.test.ts` is a latent flake (5s timeout on a
dynamic import; passes in isolation).
GHSA-72qq-p3r5-f7wq (CVSS 9.3). web_core <= 0.10.1 passed an agent-supplied
`openUrl` argument straight to `window.open()` with no scheme allowlist, so a
Button whose `functionCall` named a `javascript:` URI executed arbitrary script
in the host origin when a user clicked it. The Basic Catalog is the default, so
no non-default configuration was required to be exposed.
We pinned 0.9.0 exactly, as a runtime dependency of two published packages
(@copilotkit/a2ui-renderer, @copilotkit/vue) and transitively of
@copilotkit/react-core and @copilotkit/angular, so downstream users could not
upgrade out of it on their own. 0.10.4 keeps the ./v0_9 and
./v0_9/basic_catalog entrypoints we import; the only symbol dropped from v0_9
is FrameworkSignal, which we never referenced.
Add regression tests over both renderers that reach the sink independently
(React and Lit). They assert that javascript: and data: URIs never reach
window.open, that https URLs still open with noopener,noreferrer, and that a
blocked scheme leaves the surface mounted rather than escaping into the click
handler. Verified they fail against 0.9.0 and pass against 0.10.4.
Ports PR #6141's React contract to Vue and deletes the per-thread cloning Vue
used instead.
Cloning is gone: cloneForThread, getOrCreateThreadClone, getThreadClone and the
module-level globalThreadCloneMap. Those clones were reachable only through a
WeakMap, components had to look them up to find the agent actually in use, and
nothing tied a clone's lifetime to the scope that created it.
In its place, the same two-shape contract React now has:
useAgent() // shared registry agent
useAgent({ agentId }) // shared registry agent
useAgent({ agentId, runtimeAgentId, threadId }) // private proxied agent
UseAgentProps becomes a base plus a two-branch union, so every partial set —
{ agentId, threadId }, { agentId, runtimeAgentId }, { runtimeAgentId, threadId }
— is a compile error, with the same three runtime guards and the same messages as
React for callers TypeScript doesn't reach. Vue's branches take
MaybeRefOrGetter where React takes plain values; the shapes are otherwise
identical.
Thread resolution mirrors React exactly: an explicit `threadId` prop wins,
otherwise the chat configuration's thread gated on `hasExplicitThreadId`, so a
ThreadsProvider-minted placeholder UUID never overwrites the agent's own.
Two Vue-specific details, both load-bearing:
- The pin watcher's first source is `() => agent.value`, not `agent`. Vue sets
`forceTrigger` when any array watch source is a shallow ref, which would re-run
the pin on every `triggerRef(agent)` — i.e. every streamed message — and re-pin
the inherited thread over one CopilotChat had deliberately set for the chat it
renders. Two suites cover this ("uses the explicit agentId and threadId over
inherited configuration").
- CopilotChat assigns `agent.threadId` inside its /connect watcher rather than a
separate one, because CopilotKitCore.connectAgent reads that field
synchronously to decide whether a restore is fresh; a later assignment would
let /connect address the previous thread. React does the same, in the same
place.
CopilotChatMessageView resolves the registry agent directly instead of consulting
the clone map, and reads `copilotkit.agents` so it recomputes when the registry
changes.
Tests: use-agent-thread-isolation.test.ts covered clone semantics that no longer
exist; use-agent-thread-pinning.test.ts replaces it with the new invariants —
one instance per agentId, config-thread pinning gated on explicitness, and all
three all-or-nothing guards. Four component suites used getThreadClone purely as
a lookup and now read from the registry. MockMCPProxyAgent recorded addMessage
only inside its clone() override, so those assertions passed only because
cloning existed; the recording moves onto the class. clone() itself is left
intact everywhere, since CopilotKitCore's SuggestionEngine still clones agents
(packages/core/src/core/suggestion-engine.ts).
Call-site enumeration: cloneForThread and getOrCreateThreadClone were
module-private, zero references. getThreadClone and globalThreadCloneMap were
used by CopilotChatMessageView (rewritten) and 4 test files (retargeted); zero
remain, including in prose. useAgent's other callers, use-capabilities and
use-interrupt, pass `{ agentId }` only and match the unscoped branch unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The `completeOnMount` gate added in d70d48a561 named the `mcp-app-iframe`
testid, which only Angular's `copilot-mcp-apps-widget` declared. react-core
and vue build the sandbox iframe imperatively with no testid, so every
React/Vue integration timed the turn out at 30s with
`reason=surface-missing` and never reached `assertIframePresent` — whose
`iframe[sandbox]` fallback would have passed. D5 + D6 `mcp-apps` went red on
all 18 integrations that support the feature (first_failure_at 2026-07-28
23:03Z) while the demos rendered correctly by hand.
Fixed on both sides of the contract:
- react-core and vue now set `data-testid="mcp-app-iframe"` and a `title` on
the host-created iframe, matching Angular. Pinned by a test in each package.
- `completeOnMount` accepts CSS `selectors` alongside `testIds`, so the probe
settles on the same cascade its module doc and assertion already use
(`[data-testid="mcp-app-iframe"], iframe[sandbox]`). A comma-joined entry is
one conjunctive surface whose branches `querySelectorAll` unions, so the
delta/`minNewMounts` semantics are unchanged and `testIds` is now sugar for
the equivalent selector. This half greens the fleet on the next sweep
without waiting for a package release, since the integrations pin
@copilotkit/react-core 1.61.2.
A spec naming no surface now throws instead of burning the turn budget and
reporting a misleading `surface-missing`.
Verified against live staging: after clicking the pill, the old gate matched
0 elements and the cascade matched 1 (the sandboxed iframe was there all
along). Also recorded in showcase/GOTCHAS.md.
## What does this PR do?
Fixes a Vue `DataCloneError` that occurred when uploaded attachment
sources crossed the `structuredClone` boundary in core. Vue’s deep
`ref()` conversion wrapped nested attachment sources in reactive
proxies; `useAttachments` now keeps the attachment container shallow
with `shallowRef()`, preserving externally supplied sources as raw
cloneable values before they reach AG-UI/core payloads.
The change is intentionally Vue-only: core and React are untouched
because the defect is caused by Vue’s reactivity behavior at the
framework boundary. Focused regressions cover both the attachment hook
and `CopilotChat` submission path, including non-reactivity and
successful `structuredClone` behavior.
## Related PRs and Issues
- [CopilotKit issue
#3](https://github.com/enekesabel/CopilotKit/issues/3)
## Verification
- `pnpm nx run @copilotkit/vue:check-types` — passed.
- `pnpm nx run @copilotkit/vue:test --
src/v2/hooks/__tests__/use-attachments.test.ts
src/v2/components/chat/__tests__/CopilotChat.attachments.test.ts` —
passed, 18 tests in 2 files.
- `pnpm nx run @copilotkit/vue:build` — passed.
- Pre-commit package gate (`test-and-check-packages`) — passed: 1073
tests, publint, and attw.
- `pnpm nx run @copilotkit/vue:lint` — remains blocked by 171
pre-existing errors across unrelated Vue files; no lint errors were
introduced in the changed files.
- `git diff --check upstream/main...HEAD` — passed.
## Scope and exclusions
- Changed files are limited to
`packages/vue/src/v2/hooks/use-attachments.ts`, its focused hook and
`CopilotChat` tests, and the related `packages/vue/PARITY.md` and
`packages/vue/AGENTS.md` guidance.
- No core, React, workflow, or package-wide lint cleanup is included.
- The `PARITY.md` change removes accidental table-format churn and
retains only the meaningful attachment parity note.
## Checklist
- [x] I have read the [Contribution
Guide](https://github.com/CopilotKit/CopilotKit/blob/main/CONTRIBUTING.md)
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation
- [x] "Allow edits by maintainers" is checked (lets us help iterate on
your PR directly — faster turnaround for everyone)