Files
copilotkit__copilotkit/packages/react-native/src/headless.ts
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

133 lines
5.3 KiB
TypeScript

/**
* @copilotkit/react-native/headless
*
* Lean entry for headless React Native consumers: just the provider and the
* platform-agnostic agent/tool hooks, with NONE of the prebuilt chat UI.
*
* The default barrel (`@copilotkit/react-native`) re-exports the prebuilt chat
* components (`CopilotChat` / `CopilotModal` / `CopilotSidebar` / `CopilotPopup`,
* which import `@gorhom/bottom-sheet`) and `useAttachments` (which imports
* `expo-document-picker` + `expo-file-system`). Those are declared as optional
* peer dependencies, but a static re-export still forces Metro to resolve them
* at bundle time — so a consumer that only uses `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`.
*
* Import from here to skip that stack entirely:
* ```tsx
* import { CopilotKitProvider, useAgent, useFrontendTool } from "@copilotkit/react-native/headless";
* ```
*
* Mirrors `@copilotkit/react-core/v2/headless` (issue #4893 / PR #5883), which
* this entry builds on. The default barrel re-exports everything here plus the
* chat UI, so existing imports from `@copilotkit/react-native` are unchanged.
*/
// Auto-install polyfills so consumers don't need a manual import.
// Must run before any CopilotKit code that relies on ReadableStream / fetch streaming.
import "./polyfills";
// React Native provider (no web deps, no bottom-sheet, no expo native modules)
export { CopilotKitProvider } from "./CopilotKitProvider";
export type { CopilotKitNativeProviderProps } from "./CopilotKitProvider";
// Provider props alias (mirrors web's CopilotKitProviderProps)
export type { CopilotKitNativeProviderProps as CopilotKitProviderProps } from "./CopilotKitProvider";
// Re-export context and hooks from react-core (platform-agnostic)
export {
useCopilotKit,
useLicenseContext,
CopilotKitContext,
type CopilotKitContextValue,
} from "@copilotkit/react-core/v2/context";
// Re-export hooks that work without web deps
// These consume the CopilotKitContext which our provider sets
export {
useAgent,
useFrontendTool,
useComponent,
useHumanInTheLoop,
useInterrupt,
useSuggestions,
useConfigureSuggestions,
useAgentContext,
useThreads,
useCapabilities,
defineToolCallRenderer,
CopilotChatDefaultLabels,
// Runtime enum, not a type: it is passed BY VALUE to useAgent's `updates`
// option (`updates: [UseAgentUpdate.OnMessagesChanged]`). `export type` would
// strip the runtime binding and leave consumers unable to name a member.
// react-core's own headless entry exports it as a value for the same reason.
UseAgentUpdate,
type UseInterruptConfig,
type AgentContextInput,
type JsonSerializable,
type Thread,
type UseThreadsInput,
type UseThreadsResult,
type CopilotChatLabels,
type CopilotChatConfigurationValue,
type InterruptEvent,
type InterruptHandlerProps,
type InterruptRenderProps,
type Interrupt,
type ResumeEntry,
type ResumeStatus,
type ReactFrontendTool,
type ReactHumanInTheLoop,
type RenderToolInProgressProps,
type RenderToolExecutingProps,
type RenderToolCompleteProps,
} from "@copilotkit/react-core/v2/headless";
// Re-export core runtime ENUMS as values. Each of these is `export enum` in
// @copilotkit/core, and each is the type of a prop or field a consumer has to
// branch on — `status === ToolCallStatus.Executing`,
// `status === CopilotKitCoreRuntimeConnectionStatus.Connected`. An enum-typed
// field rejects a bare string literal, so a type-only re-export leaves the
// member names unreachable and the field impossible to compare against.
export {
ToolCallStatus,
CopilotKitCoreRuntimeConnectionStatus,
CopilotKitCoreErrorCode,
} from "@copilotkit/core";
// Re-export core types commonly needed
export type { Suggestion, FrontendTool } from "@copilotkit/core";
// AbstractAgent is a runtime CLASS, not a type: it is the AG-UI extension point
// consumers subclass (`class MyAgent extends AbstractAgent`) and test with
// `instanceof`. @ag-ui/client is a dependency of this package rather than a peer,
// so a consumer cannot reliably import the class from there directly — a
// type-only re-export left the extension point unreachable from RN.
export { AbstractAgent } from "@ag-ui/client";
// Re-export AG-UI types for consumer convenience (matches web SDK surface)
export type {
Message,
AssistantMessage as AssistantMessageType,
ToolCall,
ToolMessage,
AgentCapabilities,
} from "@ag-ui/client";
// Render tool registration (React Native flavour: RN-typed render function,
// registered into react-core's canonical registry — no local registry).
export { useRenderTool } from "./hooks/useRenderTool";
export type { UseRenderToolOptions } from "./hooks/useRenderTool";
export type {
RenderToolProps,
RenderToolFunction,
} from "./hooks/render-tool-types";
// Render tool consumption. react-core's hook is platform-agnostic — it pulls no
// DOM and no chat-UI stack, and it returns ReactElement | null, which is exactly
// what FlatList's renderItem requires. Use it to render a registered component
// on any surface, chat or not.
export { useRenderToolCall } from "@copilotkit/react-core/v2/headless";
export type { ReactToolCallRenderer } from "@copilotkit/react-core/v2/headless";