Commit Graph

42 Commits

Author SHA1 Message Date
Maxim 4c17a8fe8c fix(react-native): key the messages fingerprint on object content
`messagesFingerprint`'s content key collapsed every object to 0, so an
in-place content replacement that kept the same message id was invisible to
every memo derived from it. Its comment claimed to mirror react-core's
`messagesMemoKey`, which stopped being true when react-core #6325
(de0a659b2d) taught that key to serialize object content.

Serialize object content here too, keeping the length-not-value treatment for
string and array content so large text and base64 attachment payloads are
still never re-serialized per render. Serialization is guarded: the
fingerprint runs on every render and `JSON.stringify` throws on a circular
structure, which this component is already required to tolerate (see the
existing "does not throw on tool content that cannot be JSON-serialised"
assertion) — react-core stringifies unguarded, so the guard is a deliberate
and documented divergence.

Not a live stale-render bug via the activity path: same-id object content
comes from an ACTIVITY_SNAPSHOT replace, and `role: "activity"` never reaches
`listItems`, which builds rows for `user` and `assistant` only. Object content
DOES reach a renderer through the `role: "tool"` correlation, though, which is
what the added test drives: an object tool result replaced in place used to
leave the renderer showing the first object's serialization.

The comment no longer claims to mirror a moving target — it records what the
key captures, why the object branch exists, and that the two implementations
are independent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 17:09:29 +02:00
Maxim c7d176f264 test(react-native): make the #4893 entry guard fail on violations, not on growth
The headless import-graph guard pinned the resolved graph EXACTLY — the
11-module list and the 8 bare specifiers, both `toEqual`. That catch-all was
deliberate (a heavy dependency nobody enumerated still had to be looked at),
but it also went red on innocent growth: adding any first-party `src/` module
to the headless graph failed it, on someone else's unrelated PR. A guard that
fails on innocent changes gets deleted by the third person who hits it, and
then it guards nothing.

Express the catch-all over PACKAGES instead of MODULES: the graph may only
reach packages a headless consumer is guaranteed to be able to resolve — this
package's `dependencies` plus its NON-optional `peerDependencies`, read from
package.json rather than hand-copied. That is precisely the promise the
headless entry sells ("bundles with nothing stubbed in metro.config.js"), so
it still fails on any new third-party edge, on every optional peer, on a
devDependency, and on a Node builtin — while a new first-party module or
another import of an already-sanctioned package is free.

The two other things the pin bought are kept explicitly:

- Comment stripping. The eight phantom specifiers JSDoc examples used to
  harvest were all self-references, and this package's own name is not in the
  guaranteed set, so a `stripComments` regression still fails here.
- Non-vacuity. Every remaining graph assertion is a deny-list, and a deny-list
  over a truncated graph passes for the wrong reason, so a subset floor
  asserts the walk still reaches the provider, the polyfills and the
  react-core headless edge.

Not changed: comment stripping itself, the import()/require()/require.resolve
extraction, non-literal loader flagging, emitted-extension resolution, the
loud failure on unresolvable edges, the entry-presence tests, the #4893
fat-entry ban (still the assertion that catches `@copilotkit/react-core/v2`)
or the heavy-dependency ban. The runtime-export `beforeAll` is untouched.

Proven both directions: a new first-party module passes the loosened guard and
fails the old pin; `@copilotkit/react-core/v2`, `shiki`, an unenumerated
devDependency edge and a truncated walk each fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 17:08:29 +02:00
Maxim 00caf5fa7b docs(react-native): fix the chat memo comment's identity rationale
`messagesFingerprint`'s header JSDoc and the matching inline comment near the
`listItems` memo justified keying on message CONTENT with a claim that is false:
that `agent.messages` is mutated in place throughout, and that "the AG-UI apply
pipeline reuses one array for a whole run".

It does not. `@ag-ui/client`'s `AbstractAgent.processApplyEvents` REASSIGNS
`this.messages = applied.messages` for every applied event, so a streaming run
hands down a new array — and new message, `toolCall` and `function` objects — per
delta. Verified against a real AG-UI run by the PR reviewer, and confirmed here in
@ag-ui/client 0.0.57's `AbstractAgent`. The old grep behind the claim ("assigning
`.messages` in packages/core/src hits test files only") is accurate but proves
nothing: `@ag-ui/client` is a dependency, outside that tree.

The fix itself stands. Identity is unreliable in BOTH directions, which is the
actual rationale: it changes on the apply path, and it does NOT change on the
paths these memos exist to serve — core splices tool results in place
(`agent.messages.splice(insertAt, 0, toolMessage)`,
packages/core/src/core/run-handler.ts:931, :1080), `AbstractAgent.addMessage` is a
`this.messages.push(...)`, and `useAgent` re-renders with a bare `forceUpdate()`
(packages/react-core/src/v2/hooks/use-agent.tsx:382-396). A signal that both
misses changes and fires without them cannot be a dependency, so the derivations
must key on content.

Comments only: three sites reworded (the JSDoc, the "cannot be used" pointer at
the `messagesKey` call, and the inline note on the `listItems` memo). `git diff`
touches no behaviour, type or dependency array — every changed line is a comment.
The `contentKey` length-vs-value paragraph is left alone; another change owns it.

Note: the same false claim is in commit 77ed31c437's body, which cannot be
rewritten, and in a GitHub review comment.

Not run in this worktree: it has no node_modules, and the change is comment-only,
so it cannot affect types, lint or tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 17:00:24 +02:00
Maxim 4b25cf34b8 test(react-native): stop the entry guard racing its own module load
The #4893 entry-surface guard timed out nondeterministically at full test
parallelism ("Test timed out in 5000ms" on `await import("../headless")`),
which four independent agents each worked around with --testTimeout or
--maxWorkers=2. A flaky hard gate is a gate people learn to ignore.

Measured, not guessed. The import is a one-time module-graph load whose
VARIANCE — not its mean — broke the default budget: ~0.7-1.1s for this file
alone and ~0.9-1.8s inside the full 22-file suite (n=8 each), but 4568ms on
the run straight after a cold `nx build`, i.e. 91% of the 5000ms budget spent
on an otherwise idle machine. The cost is resolve/transform plus cold-page-
cache I/O over the ~283 KB of workspace dist that vitest.config.mjs inlines
via `server.deps.inline: [/@copilotkit/]` (core ~218 KB, react-core
v2/headless ~55 KB, v2/context, shared); bare-Node `import()` of the
equivalent prebuilt dist is 461ms, so evaluation is not the expensive part.

Four separate tests each awaited that same import, so all four raced one
cost against one budget — and when the first lost the race the other three
inherited its in-flight import and timed out with it, which is why the
observed signature was three simultaneous failures rather than one. They now
share a single explicitly-budgeted `beforeAll`, so the cost lives in exactly
one place and each test reports ~0ms.

The hook is nested rather than top-level on purpose: its failure domain must
cover only the tests that need the module, or an import failure would take
down the fs-only graph tests too — the same blast-radius problem as blind
spot #4, just relocated into a hook.

No assertion is weakened: comment stripping, import()/require() extraction,
the exact resolved-graph pin and the revived existence test are untouched,
and the guard still fails on a real violation (injecting a lazy
`import("@copilotkit/react-core/v2")` into src/streaming-fetch.ts trips 3
assertions; reverted).

Verification: 5 consecutive full-suite runs at DEFAULT parallelism, no
--maxWorkers or --testTimeout override, 271/271 passing in 3.97-5.87s wall
each; plus 3 concurrent full suites (30 workers on 10 cores) all green, and
one pass at load average 235. `check-types` clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 23:20:50 +02:00
Maxim 4315adb1e7 test(react-native): make the tool-result assertions read the result
Two tests claimed to prove a tool RESULT reaches its renderer and neither
read it. "reports complete and passes the result through" rendered through a
registrar printing only status and args, so replacing the correlated tool
message's content with a constant left it green; its in-place-mutation twin
had the same hole. The integration test's only result assertion built its
tool message as `{ content: "ok" }` behind an `as never`, so it carried no
toolCallId — the id production correlates a result to a call by.

Both now render status, args AND result together, and a new negative case
gives a tool call a result belonging to a DIFFERENT call. That last one is
the only detector for a lookup that ignores the map key: every fixture in
the file matched on id, so an unkeyed "hand out any tool result we have"
lookup passed the whole suite unchanged.

Fixtures move to src/__mocks__/tool-fixtures.ts. toolMessage() takes
toolCallId as a required positional argument, so no fixture can omit the
correlation, and assistantToolCall() returns a typed AssistantMessage —
which retires the `as never`, an `as unknown as Message`, and three `any`s
in the touched files. A properly-typed ToolMessage typechecks at that call
site unchanged; the cast was convenience, not a type-system limit.

Test-only: CopilotChat.tsx is untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 23:10:00 +02:00
Maxim d144757d8a test(react-native): guard the entry surface's export kinds against type-only stripping
`export type { X }` strips X's runtime binding. Five runtime values shipped from
`src/headless.ts` inside `export type` blocks — the `ToolCallStatus`,
`UseAgentUpdate`, `CopilotKitCoreErrorCode` and
`CopilotKitCoreRuntimeConnectionStatus` enums, and the `AbstractAgent` class —
while the reference docs told consumers to import and branch on them. Nothing in
the repo could see it: the package built, typechecked, linted and passed its
suite, because nothing here consumed its own entry the way a consumer does.

react-core's `headless-type-exports.test-d.ts` cannot cover this. Export kind is
a property of the re-exporting module, and that guard reads react-core's entry —
react-core's own `UseAgentUpdate` was already a correct value export while RN's
was wrong. The guard has to live on the RN side and read RN's own entries.

Adds `src/__tests__/headless-value-exports.test.ts`, in three layers:

- §1 asserts each of the five is a present runtime binding of the expected
  `typeof`, with its enum members nameable, on BOTH `@copilotkit/react-native`
  and `@copilotkit/react-native/headless`. A stripped export is an absent module
  binding, so a runtime test is the direct instrument and cannot be faked by a
  cast or an expect-error.
- §2 needs no symbol list: it parses both entry sources, and for every symbol
  re-exported type-only it imports the module that symbol came from and fails if
  that module has a runtime binding for it. A future contributor who adds a new
  enum re-export inside an `export type { … }` block is caught without anyone
  updating §1, and the failure names the symbol, the source module and the fix.
  A floor on the parsed specifier count keeps a rotted parser from passing
  vacuously.
- §3 type-checks the consumer-visible symptom (enum-member comparison on a
  render-prop `status`, `extends AbstractAgent`, `instanceof`). The file lives
  under `src/`, so `check-types` compiles it and a regression also fails there
  with TS1362 naming the symbol. Its bodies are lazy on purpose: a module-scope
  `extends` would crash collection and hide §1/§2's guided messages.

Proven by transiently restoring the defect three ways — the `AbstractAgent`
class, `ToolCallStatus` moved into an `export type` block, and `UseAgentUpdate`
via the inline `type ` prefix. Each produced 4 failing tests plus TS1362;
`src/headless.ts` is byte-identical to before.

RN suite 23 files / 276 tests (baseline 22 / 261, so +15 and no change
elsewhere); `nx run @copilotkit/react-native:check-types` clean; oxfmt and
oxlint clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 23:08:19 +02:00
Maxim 0ea71fc684 test(react-native): make the #4893 entry guard see what it claimed to see
The headless-entry import-graph guard was weaker than the PR claimed. Four
blind spots, each verified to let a real violation pass (or to flag a
non-violation), each now covered by a test:

1. Only `import … from "x"` was matched, so a lazy optional-peer
   `require("@copilotkit/react-core/v2")` or `await import(…)` — which Metro
   follows and bundles identically — defeated the guard entirely. Static,
   bare side-effect, dynamic `import()` and `require()`/`require.resolve()`
   are all extracted now, and a loader whose argument is not a string
   literal is reported as unanalyzable rather than silently skipped.

2. Matching ran on raw text, so doc comments counted as imports. Not
   hypothetical: the guard was harvesting EIGHT specifiers
   (`@copilotkit/react-native`, `…/headless`, `…/polyfills` and its five
   subpaths) that no source file imports — half the reported bare-specifier
   set — purely from JSDoc examples. In the other direction, writing a
   "don't do this: import from @copilotkit/react-core/v2" counter-example
   in a doc comment failed the build. Comments are stripped first now, via
   a single left-to-right pass that matches string/template literals with
   the same alternation so a `//` inside a string stays a string.

3. `resolveLocal` returned null for an edge it could not resolve and the
   caller dropped it, so an unresolvable specifier read as "clean" while
   hiding the whole subgraph behind it. Proven: a real
   `export … from "@copilotkit/react-core/v2"` reached through an ESM-style
   `"./probe-heavy.js"` edge passed the old guard. Emitted-extension
   specifiers now resolve, and anything still unresolvable FAILS LOUDLY
   instead of vanishing. The resolved file set and bare-specifier set are
   also asserted EXACTLY, so a new edge has to be looked at deliberately
   rather than only being caught if someone thought to deny-list it.

4. The graph was walked in the `describe` body, so a missing entry file
   threw at collection time and every test in the file — including the one
   asserting the entry exists — never ran (`Tests  no tests`). The walk is
   lazy and memoized per entry now, and the existence assertion reports.

Every fix was proven by mutation in both directions: the violation passes
the old guard, fails the new one, and clean source still passes. Also drops
`localFiles`, which no test ever read.

Scope note: the ~5s `await import("../headless")` timeout flake in this
file is deliberately untouched — it is owned separately. Runs used
`--testTimeout=60000`.

RN suite 267 passed / 22 files (was 261; +6 new tests);
`nx run @copilotkit/react-native:check-types` clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 22:58:20 +02:00
Maxim 025b8d5979 test(react-native): make the useRenderTool suite detect its own forwardings
`useRenderTool` is a thin forwarder onto react-core's `useFrontendTool`, and
its suite mocked exactly that hook. The double only modelled `name` and
`render`, so deleting the `deps`, `handler` AND `agentId` forwarding from the
hook each left the suite fully green — it could not detect a regression in any
of the three things the hook exists to forward.

Drop the four `vi.mock` blocks and drive a real `CopilotKitCoreReact` through
the shared `TestCopilotKit` harness, the way the sibling
`render-tool-call.integration.test.tsx` already does, then assert on core's own
observable behaviour instead of a mock's call arguments:

- handler — `core.runTool()`, i.e. core's real `executeToolHandler` path, so the
  handler is proven to RUN and its return value proven to become the tool result
- agentId — the tool resolves for its agent and must NOT resolve as a global
  tool, and the renderer entry carries the agentId that keys it
- deps — a render closure over a serialisable dep re-registers and the PAINTED
  text changes, observed through react-core's real `useRenderToolCall`

Each mutation now fails exactly one test. Also pins the documented sharp edge
that `useFrontendTool` compares deps with `JSON.stringify`, so a function dep
collapses to a constant and can never re-register — a test asserting otherwise
would assert a behaviour the code cannot deliver, and pinning it makes a change
of comparator fail loudly.

Test-only: `useRenderTool.ts` is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 22:57:09 +02:00
Maxim 63e7fa7fda fix(react-native): make the chat list's extraData contract true and explicit
CopilotChat's `extraData` memo carried a comment claiming it held "the exact
inputs renderItem reads", but it listed only { isRunning, renderToolCall,
toolMessages } while renderItem also read `listItems` — it answered "am I the
last row?" by index-reading the array's tail, and depended on `listItems` in
its own useCallback deps. The comment was false and the stated
row-memoisation contract was incomplete.

The defect is documentation and fragility, NOT observable behaviour. Verified
against the real react-native 0.85.2 sources in the pnpm store:

- FlatList is a PureComponent (Libraries/Lists/FlatList.js:307), and `data`
  is one of the props it shallow-compares. `data={listItems}` is the same
  reference, so any rebuild of `listItems` re-renders FlatList on its own.
- In the default non-strictMode path FlatList's render() uses `this._renderer`
  rather than `this._memoizedRenderer` (FlatList.js:682), allocating a fresh
  `renderProp` on every render, which is handed to every cell.
- VirtualizedList._pushCells passes that `renderItem` plus `item` to each
  CellRenderer, which is itself a PureComponent
  (VirtualizedListCellRenderer.js:63). `extraData` is NOT a cell prop.
- The `listItems` memo allocates fresh item objects on every rebuild, so each
  cell's `item` prop also differs. Cells therefore invalidate through
  `data`/`item` even under the narrowest path (strictMode with a memoizeOne
  hit on renderItem and extraData).

So no stale last-row / stranded-loading-indicator state is reachable, and no
covering test is added: the behaviour is unchanged, and the package's test
FlatList is a mock that re-invokes renderItem for every row on every parent
render, so it cannot express cell memoisation in the first place.

Instead, make the contract honest. The tail id becomes a named `lastItemId`
memo; renderItem reads that scalar and deps on it rather than closing over
`listItems` and indexing it; `extraData` now lists exactly the four values
renderItem closes over, and the comment states why `listItems` is absent
(it is the `data` prop, which already invalidates cells). As a side benefit
renderItem's identity is now stable across `listItems` rebuilds that do not
move the tail.

Call-Site Enumeration (Procedure 2 step 8):
- `extraData` — grep over packages/react-native/src shows exactly two sites,
  the memo itself and the `extraData={extraData}` prop on the list. No test
  and no other module reads its keys. A caller-supplied `FlatListComponent`
  (the documented BottomSheetFlatList case) receives it, but RN treats
  extraData as an opaque re-render marker and never inspects its shape, so
  adding `lastItemId` is not observable to any consumer.
- `renderItem` / `lastItemId` — local to CopilotChat; neither is exported.
- `isLoading` on AssistantMessage — value-identical by construction, since
  `lastItemId` IS `listItems[listItems.length - 1]?.id`.
- No change to CopilotChatProps or to any entry-point export.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 22:31:47 +02:00
Maxim 7baed27370 fix(react-native): export the headless entry's runtime values as values
`src/headless.ts` re-exported five runtime values inside `export type { … }`
blocks. A type-only re-export strips the runtime binding, so the symbol is
unimportable and — for the enums — the field it types cannot be compared
against at all, because an enum-typed field rejects a bare string literal.
`RenderToolProps["status"]` is `ToolCallStatus`, so a consumer of this PR's own
render-prop contract had no working way to branch on `status`.

Determined empirically, not by reading: a throwaway probe imported all 32
re-exported symbols as values under the package's real tsconfig. The 27 that
are genuine types reported TS2693 ("only refers to a type"); five did not, and
those five are the ones moved. Declaration sites confirm each:

  ToolCallStatus                        packages/core/src/types.ts:14      export enum
  CopilotKitCoreErrorCode               packages/core/src/core/core.ts:99  export enum
  CopilotKitCoreRuntimeConnectionStatus packages/core/src/core/core.ts:320 export enum
  UseAgentUpdate                        packages/react-core/src/v2/hooks/use-agent.tsx:13
                                                                           export enum
  AbstractAgent                         @ag-ui/client                      declare abstract class

Nothing else changed kind: Suggestion, FrontendTool, Message, ToolCall,
ToolMessage, AgentCapabilities, ResumeStatus, Interrupt, ResumeEntry, the
Interrupt*/RenderTool*/Thread*/CopilotChat* prop and config types,
ReactFrontendTool, ReactHumanInTheLoop, ReactToolCallRenderer and
CopilotKitContextValue are all genuine types and stay `export type`.

`src/index.ts` does `export * from "./headless"`, which republishes values and
types alike, so both published entry points are fixed. Verified in the built
output: all five appear without a `type` prefix in dist/headless.d.mts and
dist/index.d.mts, and as runtime bindings in headless.mjs, index.mjs and
index.cjs.

Negative control: with the pre-fix headless.ts the same probe produced ten
TS1362 errors ("cannot be used as a value because it was exported using
'export type'") across both entries; with the fix, zero.

This makes the already-merged docs on this branch true. The RN reference pages
write `import { ToolCallStatus } from "@copilotkit/react-native"` and
`status === ToolCallStatus.Executing` (useRenderTool.mdx:170, useFrontendTool.mdx:129,
useHumanInTheLoop.mdx:86) and `import { useAgent, UseAgentUpdate }` with
`updates: [UseAgentUpdate.OnMessagesChanged]` (useAgent.mdx:234). None of those
imports resolved before this commit.

AbstractAgent is a deliberate inclusion, not scope creep: it is a runtime class
and the AG-UI extension point consumers subclass, and @ag-ui/client is a
dependency of this package rather than a peer, so a consumer cannot reliably
import it from there directly. It carries no bundle cost — the headless entry
already imports @ag-ui/client transitively through
@copilotkit/react-core/v2/headless, and esbuild tree-shakes an unused
re-export, so scripts/measure-headless.mjs still reports 92.7 kB gzip.

Forced test change, in scope only because the fix causes it: the value
re-export makes headless.ts the first runtime importer of @copilotkit/core in
this package's graph, so `import "../index"` now evaluates real core, which
named-imports RUNTIME_MODE_SSE and friends from @copilotkit/shared.
headless-integration.test.tsx replaced that module wholesale with a two-key
factory, so the import threw. Fixed by spreading importOriginal() instead of
replacing — the form vitest's own error message prescribes — leaving
createLicenseContextValue the only stubbed member. No assertion, case or
coverage changed.

Call-Site Enumeration (Procedure 2 step 8) — `grep -rn` per symbol across
packages/ plus every importer of @copilotkit/react-native in the repo. Every
site holds, because type -> value is a widening: an `import type` of a value
export is still legal.

  ToolCallStatus
    packages/react-native/src/headless.ts:94 — the changed export. Holds.
    No other site in packages/ or examples/ names it. Nothing imported it
    before, which is the bug.
  CopilotKitCoreRuntimeConnectionStatus
    packages/react-native/src/headless.ts:95 — the changed export. Holds.
    No other site.
  CopilotKitCoreErrorCode
    packages/react-native/src/headless.ts:96 — the changed export. Holds.
    CopilotKitProvider.tsx:12,37 / CopilotChat.tsx:13,93 / CopilotPopup.tsx:26,223
    — all `import type … from "@copilotkit/core"`, used only in a `code:` field
    position. They import from core directly, not through this entry, and a
    type position is unaffected by the re-export kind. Hold.
  UseAgentUpdate
    packages/react-native/src/headless.ts:65 — the changed export. Holds.
    packages/react-core/src/v2/headless.ts:41 — already a value export there,
    with a comment giving this exact reason; this commit makes RN agree with it
    rather than diverge. Holds.
  AbstractAgent
    packages/react-native/src/headless.ts:107 — the changed export. Holds.
    packages/react-native/src/__mocks__/test-copilotkit.tsx:22,41,42 — already
    imports the class as a VALUE from @ag-ui/client and subclasses it, i.e. it
    had to bypass this entry to do what the entry now permits. Unchanged and
    still passing. Holds.
    packages/react-core/src/v2/**, packages/channels-telegram/** — all import
    from @ag-ui/client directly; none route through @copilotkit/react-native.
    Hold.
  Importers of @copilotkit/react-native outside the package
    examples/v2/react-native/demo/{App.tsx,src/ChatScreen.tsx,index.js} — import
    CopilotKitProvider, useAgent, useCopilotKit, useFrontendTool and the
    polyfills entry. None of the five symbols appears anywhere in the demo, so
    nothing to break; the demo is now able to import them. Holds.
  Surface guards
    src/__tests__/headless-entry-surface.test.ts — its `not.toHaveProperty`
    denylist covers the chat/attachment exports and the two removed registry
    symbols; none of the five is listed, and its bare-specifier bans
    (@gorhom/bottom-sheet, expo-*, shiki/mermaid/katex/a2ui-renderer, non-headless
    react-core entries) are unaffected by adding @copilotkit/core and
    @ag-ui/client edges. Passes unchanged.

Verification: `pnpm nx run @copilotkit/react-native:check-types` succeeds
(tsc --noEmit, 0 errors). `npx vitest run --reporter=dot` — 22 files, 253
tests, all passing. `npx oxfmt --check` clean; `npx oxlint` 0 errors and 2
warnings, both pre-existing in the touched test file (no-shadow on a mocked
`React`, no-this-in-sfc).

Note on a pre-existing flake: headless-entry-surface.test.ts hits the 5000ms
default testTimeout on `await import("../headless")` when the machine is loaded.
Measured 6 serial runs each way on the same box — pre-fix headless.ts failed
4 of 6, post-fix 3 of 6, identical timeout signature — so it predates this
change and is load-induced, not caused by it. Every run above used
`--testTimeout=60000`; raising that default (or making those assertions
static) is worth a follow-up, and is not this commit's to make.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 22:31:46 +02:00
Maxim 5c09f51967 fix(react-native): stop a tool result RN cannot type from reading as empty
`toolMessages` rebuilt every tool message with
`content: typeof m.content === "string" ? m.content : ""`, so any non-string
content became `""` — indistinguishable from a tool that genuinely returned
nothing, with nothing logged. Renderers receive `result: string` and cannot tell
the two apart.

Static typing says the branch is unreachable: `ToolMessageSchema.content` is
`z.string()`, the SSE transport zod-parses every TOOL_CALL_RESULT before it
reaches `agent.messages`, and core stringifies non-string handler results itself
(`JSON.stringify(result)`, run-handler.ts:831/1014) before inserting the tool
message. So this is a defensive branch, not a live data-loss path — but core
keeps the same hedge (`normalizeToolResultContent` accepts `unknown` and unwraps
arrays of text parts), because unvalidated producers exist: restored thread
history, a non-SSE transport, and app code casting on `addMessage`. Rather than
delete the branch, make it loud and lossless: serialise non-string content the
way core already represents non-string results, and warn in dev (`__DEV__`
guard, matching src/CopilotChat.tsx and streaming-fetch.ts). null/undefined
still render as `""` — nothing to lose — but now warn instead of passing
silently. Never throws from the render path.

Call-Site Enumeration (semantics of ToolMessage.content in RN's map):
- Producer: the `toolMessages` memo, packages/react-native/src/components/CopilotChat.tsx.
  Module-local const; no other module imports it.
- In-file consumers: the `extraData` memo (map identity only, never reads
  content) and `renderItem`, which passes `toolMessages.get(tc.id)` to
  `renderToolCall`.
- react-core: `useRenderToolCall`
  (packages/react-core/src/v2/hooks/use-render-tool-call.tsx) forwards
  `toolMessage.content` as `result` (:53) and compares it in the memo
  comparator (:91-93). Both still receive a string.
- Downstream: app renderers registered through RN's `useRenderTool` ->
  `useFrontendTool`, typed by `ReactToolCallRenderer` whose Complete branch
  declares `result: string`. That contract is unchanged.
- Behaviour delta is confined to non-string content; the string path is
  byte-identical, and `""` stays `""` and stays silent.

Tests: 6 cases in CopilotChatToolCalls.test.tsx cover verbatim strings, an
empty result staying empty and silent, array/object serialisation with one
warning, null warning, and non-serialisable content not throwing.
RN suite 259/259, check-types clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 22:14:33 +02:00
Maxim 77ed31c437 fix(react-native): key the chat's message memos on content, not array identity
`CopilotChat` derived both `toolMessages` (toolCallId -> ToolMessage) and
`listItems` from `useMemo(..., [messages])`, where `messages` is `agent.messages`.
That dependency never changes on the path the memos exist to serve, so the
headline fix of this PR was inert:

- Core inserts tool results by MUTATING IN PLACE — `agent.messages.splice(insertAt,
  0, toolMessage)` (packages/core/src/core/run-handler.ts:931, :1080). Nothing in
  production reassigns `.messages`; grepping for that assignment in
  packages/core/src hits test files only.
- `AbstractAgent.addMessage` is a `this.messages.push(...)`, and AG-UI's apply
  pipeline reassigns the SAME array object for the whole run, so identity changes
  at most once per run and then never again.
- `useAgent` re-renders with a bare `forceUpdate()` on `onMessagesChanged`
  (packages/react-core/src/v2/hooks/use-agent.tsx:385-397); it does not hand down
  a new array either.

Net effect: both memos froze at whatever the first render of a run saw. Tool
renderers kept receiving `result: undefined` with a status that never reached
`complete`, and any assistant message or tool call appended mid-run never reached
the flat list at all (`listItems` only ever recomputed when `isRunning` flipped).

Both memos are now keyed on a lightweight content fingerprint — ids, roles,
content length, `toolCallId`, and tool-call ids plus argument lengths — mirroring
react-core's web `messagesMemoKey`
(packages/react-core/src/v2/components/chat/CopilotChat.tsx:983). Length rather
than value so large text and base64 attachment payloads are not re-serialized
every render, and the fingerprint is recomputed per render so typing in the
composer still does not rebuild the transcript.

Why 253 tests were green over this: every RN chat suite drives messages by
re-rendering `TestCopilotKit` with a NEW array, which DOES change identity, so
those tests pass regardless of the dependency. The two added tests mutate in
place through `agent.addMessage` instead — the same push core's paths bottom out
in — and fail against the pre-fix source:

  AssertionError: expected 'inProgress:Rooftop' to be 'complete:Rooftop'
  TestingLibraryElementError: Unable to find an element by: [data-testid="places"]

`TestCopilotKit` gains an optional `agentRef` prop to publish the stable agent so
a test can reach that path.

Call-Site Enumeration
- `TestCopilotKitProps` (new OPTIONAL `agentRef`; no existing site needs a change):
  - packages/react-native/src/components/__tests__/CopilotChatToolCalls.test.tsx (10 uses)
  - packages/react-native/src/hooks/__tests__/render-tool-call.integration.test.tsx (3 uses)
  - no other `<TestCopilotKit` in packages/, examples/ or showcase/
- `messagesFingerprint`: new module-private helper, 1 definition + 1 call, both in
  packages/react-native/src/components/CopilotChat.tsx. Not exported.
- No public RN export or `CopilotChatProps` field changed; `extraData` and
  `renderItem` keep their existing shapes and pick the corrected values up
  through their existing deps.

Verification: @copilotkit/react-native 255/255 tests in 22 files; `tsc --noEmit`
clean. Left untouched by design: the non-string tool-content coercion and
`extraData` omitting `listItems`, both owned by other changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 22:09:19 +02:00
Maxim 8bafd4870c docs(react-native): narrow the render-prop drift guarantee to what holds
The header claimed deriving `RenderToolProps` from `ReactToolCallRenderer` made
drift between RN and web impossible, and that `check-types` would catch any
divergence. Both halves are false as written.

react-core publicly exports its own `RenderToolProps<S>`
(src/v2/hooks/use-render-tool.tsx:9-36) which is generic over a schema, carries
arguments under `parameters` rather than `args`, and types `status` as the string
literals "inProgress" / "executing" / "complete" rather than as `ToolCallStatus`
members. RN's derived type differs from it in both the payload field name and
the `status` type, today, on the same branch that made the claim.

`check-types` cannot see that divergence: nothing relates the two types, and the
one place they meet — react-core's bridge at use-render-tool.tsx:178-186 —
spreads the enum-typed props into the literal-typed slot and compiles, because a
string-enum member is assignable to its own literal type. Web's public `status`
is a widening of the canonical contract, not a derivation from it.

Rewritten to claim only the defensible guarantee: RN's props cannot drift from
`ReactToolCallRenderer`, the contract renderers are actually invoked against.
The residual divergence from web's public type is now stated explicitly, along
with the fact that RN's entry point re-exports web's three `RenderTool*Props`
arms so both shapes ship under similar names. The trailing paragraph now names
the states as `ToolCallStatus` members, matching how the reference page
describes them.

Comment-only; no type declaration changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 21:50:37 +02:00
Maxim 803ef2d7ab docs(react-native): correct the inverted pre-refactor history in render-tool-types
The header JSDoc on the new derived `RenderToolProps` claimed the deleted RN
type had "args unconditionally partial". The opposite was true: the old
`RenderToolContext.tsx` declared `args: T`, so RN promised the FULL argument
object at every status — the drift this refactor fixes is that the canonical
contract ADDS an `"inProgress"` state in which `args` narrows to `Partial<T>`.

Stating the drift backwards in the very file that defines the contract misleads
anyone reasoning about the migration, so the historical claims are now spelled
out concretely and verified against the deleted type:

- old `status` was `"executing" | "complete"` with no `"inProgress"` member
- old type omitted `name` and `toolCallId` entirely
- old `args` was unconditionally `T`, never `Partial<T>`

Comment-only; no type declaration changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 21:38:17 +02:00
github-actions[bot] 8c9bcb9140 style: auto-fix formatting 2026-08-08 15:33:51 +00:00
Maxim f4031f3a62 fix(rn): extend /v2/context purity guard and document render closure-staleness
Final review fix wave for the RN render-tool convergence branch.

Substantive:
- Extend packages/react-core/scripts/assert-headless-purity.mjs to also scan the
  built /v2/context chunk (context.mjs/context.cjs), not just /v2/headless.
  /v2/context carries CopilotKitCoreReact and is imported by react-native, so a
  future shiki/mermaid/katex leak through it would bloat RN bundles (#4893) while
  neither hard-fail guard fired. Comment and failure message updated to name both
  RN-reachable entries. Mutation-verified against context.mjs.
- Document the closure-staleness convergence: render is now captured at
  registration (passed into useFrontendTool) and only refreshed when deps change,
  no longer re-read every render. Consumers whose render closes over changing
  state must now pass deps. Documented in the useRenderTool JSDoc, the
  useRenderTool.mdx reference, and the changeset migration notes.

Minor sweep:
- CopilotChat extraData now lists what renderItem actually reads
  ({ isRunning, renderToolCall, toolMessages }); drop unused executingToolCallIds.
- headless-type-exports.test-d.ts imports React explicitly instead of relying on
  the ambient UMD global.
- useRenderTool.mdx migration heading no longer names the uncut 1.67.0 version.
- Changeset marks @copilotkit/react-core minor (new public type export), matching
  its body.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-08 16:57:25 +02:00
Maxim 4104bd19b5 test(react-native): port #6346's render-tool regressions to the converged path
Assertions originally written by David McKay in PR #6346, re-driven through
CopilotKitCoreReact's registry instead of a mocked local one.

Co-Authored-By: David McKay <davidmckayv@users.noreply.github.com>
2026-08-08 16:17:52 +02:00
Maxim db67ccfc29 refactor(react-native)!: remove the local render-tool registry, consume react-core's hooks
BREAKING CHANGE: useRenderToolRegistry and RenderToolProvider are removed.
Render tools now register into CopilotKitCoreReact.renderToolCalls, the same
registry react-core uses, so a React Native-specific registry and its provider
no longer exist. Use useRenderToolCall() to render a registered component
anywhere in your app, including non-chat surfaces, and delete RenderToolProvider
from your tree — CopilotKitProvider no longer installs it and nothing needs it.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-08 16:12:01 +02:00
Maxim 9c6d5b138e fix(react-native): correct SeededAgent.run return type and restore tool-call coverage
- SeededAgent.run now declares Observable<BaseEvent> return (via
  ReturnType<AbstractAgent["run"]>) so check-types passes; throw-only body
  avoids a runtime rxjs import the RN bundler cannot resolve.
- TestCopilotKit accepts optional executingToolCallIds.
- Restore executing-status, empty-string-args, and unrepairable-JSON-args
  coverage as tests driven through the real CopilotKitCoreReact.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-08 16:00:58 +02:00
Maxim 296cd4461c fix(react-native): stream tool-call renders and pass results through, via react-core's renderer
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-08 15:53:08 +02:00
Maxim ebf0f94fb8 refactor(react-native): register render tools into core's registry, derive props from react-core 2026-08-08 14:27:02 +02:00
Maxim 94b8357f3c test(react-native): lock the #4893 fat-entry ban in the import-graph guard 2026-08-08 14:07:43 +02:00
github-actions[bot] 5baf058e0f style: auto-fix formatting 2026-07-23 21:58:31 +00:00
David McKay 0a582df4dd fix(react-native): add /headless subpath so custom-UI consumers skip chat/attachment native deps
The `@copilotkit/react-native` barrel statically re-exports the prebuilt chat
UI (CopilotChat / CopilotModal / CopilotSidebar / CopilotPopup, which import
`@gorhom/bottom-sheet`) and `useAttachments` (which imports
`expo-document-picker` + `expo-file-system`). Those are optional peer deps, but
a static re-export still forces Metro to resolve them at bundle time. A headless
consumer that uses only `CopilotKitProvider` + `useAgent` + `useFrontendTool`
(a fully custom UI) had to install every chat/attachment native dep or stub them
in `metro.config.js`, or the release bundle fails with
`Unable to resolve module expo-document-picker`.

Add a lean `@copilotkit/react-native/headless` entry that re-exports only the
provider, the platform-agnostic hooks, the render-tool registry, and the
core/AG-UI types — none of the chat UI or `useAttachments` — so those native
deps never enter the bundle graph and the metro-stub workaround is retired.

Mirrors `@copilotkit/react-core/v2/headless` (#5883): a standalone entry file,
wired into the tsdown entry list, the package.json `exports` map, and
`sideEffects` (it side-effect-imports the polyfills). The default barrel now
does `export * from "./headless"` and layers the chat UI on top, so it stays
fully backward compatible. Adds a static import-graph regression test asserting
the headless graph never reaches the chat/attachment modules or their native
peer deps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 14:56:43 -07:00
Tyler Slaton a13c3ee663 chore: merge main into PR 5480 2026-06-23 20:50:16 -07:00
Austin Merrick 4ba201b5c4 fix: repair check-types across all packages and gate it in CI
Repairs TypeScript check-types across the monorepo and adds a CI gate so
regressions are caught going forward:

- core: bundler module resolution and strict-mode fixes
- sdk-js: bundler module resolution; keep codegen, formatter, packaging working
- react-core: fixes across components, hooks, and tests
- react-native: restore catch binding referenced by TypeError cause
- runtime: repair check-types and bound AI SDK schema inference
- web-inspector: nodenext import extensions, export Anchor
- remaining packages and node example: assorted check-types repairs
- deps: add missing type-only devDependencies
- license context driven from /info licenseStatus
- ci: run check-types in the static quality workflow

Squashed from 12 commits for a single, easily-revertable change.
2026-06-23 15:26:47 -07:00
Alem Tuzlak 43fdba74aa feat: AG-UI standard interrupt support in useInterrupt + BuiltInAgent
Adds the AG-UI standard interrupt flow (RUN_FINISHED outcome:interrupt + resume array) alongside the legacy on_interrupt path.

- core: forward the standard resume array through runAgent.
- react-core / vue / react-native: useInterrupt handles standard interrupts with resolve()/cancel(), surfaces the primary + full interrupt set, and persists each resolved tool-backed interrupt as a tool-result message so multi-turn conversations stay well-formed (no dangling tool call -> no tool-call loop).
- runtime BuiltInAgent: native interrupts for the aisdk + tanstack factory paths via each SDK's needsApproval primitive (tool-approval-request / CUSTOM approval-requested -> outcome:interrupt); classic interrupt-tool emission + ctx.interrupt() factory primitive; idempotent resume injection mapped to each SDK's native tool-result; getCapabilities advertises humanInTheLoop.interrupts.
- docs: document standard interrupt support.

Verified across core/react-core/runtime unit suites and a real-model multi-turn run on both aisdk and tanstack.
2026-06-23 20:14:17 +02:00
Jordan Ritter 39166deea1 test(react-native): import vitest globals in TypingIndicator testid test
Matches the explicit-import convention used by sibling tests in this
package (e.g. copilot-chat-agentid.test.tsx, streaming-fetch.test.ts).
Removes 3 tsc "Cannot find name 'describe'/'it'/'expect'" errors
without changing runtime behavior — vitest globals already provided
at runtime via vitest.config.mjs (globals: true).
2026-05-30 10:10:38 -07:00
Jordan Ritter f43c3f3f5b feat(ui): add stable testids to error banner and loading indicator
Adds purely additive data-testid markers to the error and loading UI
surfaces across the frontend framework packages (react-core, react-ui,
react-native, angular, vue) so e2e tests can deterministically detect
errored-out vs still-loading states. Without these, e2e probes hit
~30-60s timeouts instead of failing fast.

Testids (aligned with existing repo convention; copilot-<kebab>):
- copilot-error-banner on react-core BannerErrorDisplay (toast
  provider) and UsageBanner, plus react-ui legacy in-chat ErrorMessage.
- copilot-loading-cursor on react-ui legacy LoadingIcon sites
  (Messages.tsx, AssistantMessage.tsx), angular
  CopilotChatMessageViewCursor, react-native TypingIndicator (via
  RN testID convention), and vue CopilotChatMessageView. The v2
  react-core Cursor already exposed this testid; this change broadens
  it to every frontend framework so a single selector works across all.

Vue's prior copilot-chat-cursor testid is renamed to
copilot-loading-cursor for cross-framework consistency; the two e2e
tests in packages/vue that referenced the old name are updated.

No behavior, rendering, or styling changes. Adds small static
source-asserting tests in each touched package that verify the markers
stay in place.
2026-05-30 10:02:26 -07:00
Jordan Ritter 39ec297af4 feat(react-native): wire UI components into package exports and config
Add peer dependencies, export new components and hooks from package entry point, integrate RenderToolProvider into CopilotKitProvider, configure vitest and tsdown, add usage documentation.
2026-05-22 14:26:27 -07:00
Jordan Ritter c7775fed7e feat(react-native): add CopilotChat and CopilotModal components
FlatList-based chat interface and bottom-sheet modal overlay. Includes suggestion pills, keyboard avoidance, custom FlatList support, and comprehensive test coverage.
2026-05-22 14:26:26 -07:00
Jordan Ritter 24db94492b feat(react-native): add useRenderTool hook and RenderToolContext
Hook for rendering custom tool UIs in React Native with a store-based context provider. Includes tests for register/unregister, subscriber notification, and error handling.
2026-05-22 14:26:26 -07:00
Jordan Ritter f99f65f136 feat(react-native): add Markdown, AssistantMessage, UserMessage, and TypingIndicator components
Message bubble components with streaming markdown support, typing indicator animation, and timestamp formatting. Includes unit tests and edge case coverage.
2026-05-22 14:26:26 -07:00
Jordan Ritter b6ad624c94 style: apply formatter to vue package and other unformatted files 2026-05-13 17:06:47 -07:00
Jordan Ritter 47ceb39588 test(react-native): comprehensive test suite — 155 tests across 10 files
useAttachments unit tests, CopilotChat attachment integration,
Popup/Sidebar prop forwarding, headless integration, full
pick-to-submit flow.
2026-05-13 15:39:00 -07:00
Jordan Ritter 20184e1324 feat(react-native): export full v2 API surface with attachment support
15+ type re-exports from headless layer. expo-document-picker and
expo-file-system as optional peer deps. InterruptEvent,
ReactFrontendTool, ReactHumanInTheLoop added to headless.ts.
2026-05-13 15:38:55 -07:00
Jordan Ritter 018a696c05 feat(react-native): add useAttachments hook with NativeFileInput type
React Native attachment hook using expo-document-picker and
expo-file-system. Replaces web FileReader/HTMLInputElement APIs.
2026-05-13 15:38:49 -07:00
Jordan Ritter ec5ef21f9e feat(react-native): add CopilotChat with attachment support and CopilotPopup
CopilotChat: headless chat with useAttachments integration,
useCopilotChatContext hook, submitMessage with InputContent[].
CopilotPopup: floating overlay with FAB, attachment prop forwarding.
2026-05-13 15:38:45 -07:00
Maxim d09618a00e feat(react-native): add CopilotModal and CopilotSidebar components 2026-05-13 15:38:34 -07:00
Maxim 73431de037 fix(react-native): streaming crash fix and agentId consistency
XHR callbacks deferred to JS thread to prevent iOS crash. agentId
prop added for consistency with web SDK. Dynamic headers function
and credentials prop on CopilotKitProvider.
2026-05-13 15:38:30 -07:00
Maxim 31061a1d23 test: add react-native tests and update react-core test imports
93 tests for react-native: streaming-fetch (36), provider (17),
polyfills (20), headless integration (9 provider + error boundary).
Update react-core test vi.mock paths from providers/CopilotKitProvider
to context module. Update useAgent throttle tests for batched
forceUpdate.
2026-05-06 16:42:15 -07:00
Maxim 94c9c1d4c3 feat: add @copilotkit/react-native package
Headless React Native wrapper for CopilotKit. Provides a lightweight
CopilotKitProvider and re-exports platform-agnostic hooks from
react-core without pulling web dependencies (no DOM, CSS, Radix,
Lit, A2UI). Includes XHR-based streaming fetch polyfill for Hermes
and granular polyfills for streams, encoding, crypto, DOM, and
location APIs.
2026-05-06 16:41:51 -07:00