Files
Markus Ecker 2d7f188b29 feat(vue): mirror React's useAgent thread scoping, remove thread cloning
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>
2026-07-29 16:40:31 +02:00
..