mirror of
https://github.com/CopilotKit/CopilotKit.git
synced 2026-09-14 16:26:20 +08:00
refactor: extract react-core context and headless hook exports
Extract CopilotKitContext, useCopilotKit, and LicenseContext into src/v2/context.ts. Add src/v2/headless.ts barrel export for platform-agnostic hooks. Add v2/context and v2/headless entry points to tsdown config and package.json exports. Update all hook imports to use the new context module. Always subscribe to onError in web provider (matching RN pattern). Use batchedForceUpdate for onMessagesChanged. Replace extraDeps spread with JSON.stringify in useFrontendTool and useRenderTool dependency arrays.
This commit is contained in:
@@ -46,6 +46,14 @@
|
||||
"require": "./dist/v2/index.cjs"
|
||||
},
|
||||
"./package.json": "./package.json",
|
||||
"./v2/context": {
|
||||
"import": "./dist/v2/context.mjs",
|
||||
"require": "./dist/v2/context.cjs"
|
||||
},
|
||||
"./v2/headless": {
|
||||
"import": "./dist/v2/headless.mjs",
|
||||
"require": "./dist/v2/headless.cjs"
|
||||
},
|
||||
"./v2/styles.css": "./dist/v2/index.css"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useEffect, useReducer } from "react";
|
||||
import { CopilotKitCoreReact } from "./lib/react-core";
|
||||
import type { CopilotKitCoreReactConfig } from "./lib/react-core";
|
||||
import type { LicenseContextValue } from "@copilotkit/shared";
|
||||
|
||||
// Re-export so headless.ts (and consumers) reference the same type declaration.
|
||||
export { CopilotKitCoreReact };
|
||||
export type { CopilotKitCoreReactConfig };
|
||||
|
||||
export interface CopilotKitContextValue {
|
||||
copilotkit: CopilotKitCoreReact;
|
||||
/**
|
||||
* Set of tool call IDs currently being executed.
|
||||
* This is tracked at the provider level to ensure tool execution events
|
||||
* are captured even before child components mount.
|
||||
*/
|
||||
executingToolCallIds: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
export const EMPTY_SET: ReadonlySet<string> = new Set();
|
||||
|
||||
export const CopilotKitContext = createContext<CopilotKitContextValue | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
export const useCopilotKit = (): CopilotKitContextValue => {
|
||||
const context = useContext(CopilotKitContext);
|
||||
const [, forceUpdate] = useReducer((x: number) => x + 1, 0);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useCopilotKit must be used within CopilotKitProvider");
|
||||
}
|
||||
useEffect(() => {
|
||||
const subscription = context.copilotkit.subscribe({
|
||||
onRuntimeConnectionStatusChanged: () => {
|
||||
forceUpdate();
|
||||
},
|
||||
});
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
// License context — shared between web and RN providers.
|
||||
// Default is permissive (all features allowed) — providers override via createLicenseContextValue.
|
||||
// Inlined here to avoid a runtime import from @copilotkit/shared, which pulls in
|
||||
// Node-only deps (jose) that break React Native's Metro bundler.
|
||||
export const LicenseContext = createContext<LicenseContextValue>({
|
||||
status: null,
|
||||
license: null,
|
||||
checkFeature: () => true,
|
||||
getLimit: () => null,
|
||||
} as LicenseContextValue);
|
||||
|
||||
export const useLicenseContext = (): LicenseContextValue =>
|
||||
useContext(LicenseContext);
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Headless (platform-agnostic) exports from @copilotkit/react-core/v2.
|
||||
*
|
||||
* No CSS, no web UI components, no DOM dependencies.
|
||||
* Used by @copilotkit/react-native.
|
||||
*/
|
||||
|
||||
// Re-export from context (which is external in this build) so the .d.ts
|
||||
// references the same type declaration. This avoids a nominal type mismatch
|
||||
// caused by private class members being declared in two separate .d.ts files.
|
||||
export { CopilotKitCoreReact } from "./context";
|
||||
export type { CopilotKitCoreReactConfig } from "./context";
|
||||
|
||||
// Chat configuration provider (no UI, just context)
|
||||
export {
|
||||
CopilotChatConfigurationProvider,
|
||||
useCopilotChatConfiguration,
|
||||
CopilotChatDefaultLabels,
|
||||
type CopilotChatLabels,
|
||||
type CopilotChatConfigurationValue,
|
||||
type CopilotChatConfigurationProviderProps,
|
||||
} from "./providers/CopilotChatConfigurationProvider";
|
||||
|
||||
// Platform-agnostic hooks
|
||||
export { useAgent, type UseAgentUpdate } from "./hooks/use-agent";
|
||||
export { useFrontendTool } from "./hooks/use-frontend-tool";
|
||||
export { useComponent } from "./hooks/use-component";
|
||||
export { useHumanInTheLoop } from "./hooks/use-human-in-the-loop";
|
||||
export { useInterrupt, type UseInterruptConfig } from "./hooks/use-interrupt";
|
||||
export { useSuggestions } from "./hooks/use-suggestions";
|
||||
export { useConfigureSuggestions } from "./hooks/use-configure-suggestions";
|
||||
export {
|
||||
useAgentContext,
|
||||
type AgentContextInput,
|
||||
type JsonSerializable,
|
||||
} from "./hooks/use-agent-context";
|
||||
export {
|
||||
useThreads,
|
||||
type Thread,
|
||||
type UseThreadsInput,
|
||||
type UseThreadsResult,
|
||||
} from "./hooks/use-threads";
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCopilotKit } from "../providers/CopilotKitProvider";
|
||||
import { useCopilotKit } from "../context";
|
||||
import { useLayoutEffect, useMemo } from "react";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCopilotKit } from "../providers/CopilotKitProvider";
|
||||
import { useCopilotKit } from "../context";
|
||||
import { useCopilotChatConfiguration } from "../providers/CopilotChatConfigurationProvider";
|
||||
import { useMemo, useEffect, useReducer, useRef } from "react";
|
||||
import { DEFAULT_AGENT_ID } from "@copilotkit/shared";
|
||||
@@ -290,7 +290,7 @@ export function useAgent({
|
||||
};
|
||||
|
||||
if (updateFlags.includes(UseAgentUpdate.OnMessagesChanged)) {
|
||||
handlers.onMessagesChanged = forceUpdate;
|
||||
handlers.onMessagesChanged = batchedForceUpdate;
|
||||
}
|
||||
|
||||
if (updateFlags.includes(UseAgentUpdate.OnStateChanged)) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { useCopilotKit } from "../providers/CopilotKitProvider";
|
||||
import { useCopilotKit } from "../context";
|
||||
import { useCopilotChatConfiguration } from "../providers/CopilotChatConfigurationProvider";
|
||||
import { DEFAULT_AGENT_ID } from "@copilotkit/shared";
|
||||
import type {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect } from "react";
|
||||
import { useCopilotKit } from "../providers/CopilotKitProvider";
|
||||
import { useCopilotKit } from "../context";
|
||||
import type { ReactFrontendTool } from "../types/frontend-tool";
|
||||
|
||||
const EMPTY_DEPS: ReadonlyArray<unknown> = [];
|
||||
@@ -42,5 +42,5 @@ export function useFrontendTool<
|
||||
// Depend on stable keys by default and allow callers to opt into
|
||||
// additional dependencies for dynamic tool configuration.
|
||||
// tool.available is included so toggling availability re-registers the tool.
|
||||
}, [tool.name, tool.available, copilotkit, extraDeps.length, ...extraDeps]);
|
||||
}, [tool.name, tool.available, copilotkit, JSON.stringify(extraDeps)]);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCopilotKit } from "../providers/CopilotKitProvider";
|
||||
import { useCopilotKit } from "../context";
|
||||
import type { ReactFrontendTool } from "../types/frontend-tool";
|
||||
import type { ReactHumanInTheLoop } from "../types/human-in-the-loop";
|
||||
import type { ReactToolCallRenderer } from "../types/react-tool-call-renderer";
|
||||
|
||||
@@ -5,7 +5,7 @@ import React, {
|
||||
useMemo,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { useCopilotKit } from "../providers/CopilotKitProvider";
|
||||
import { useCopilotKit } from "../context";
|
||||
import { useAgent } from "./use-agent";
|
||||
import type {
|
||||
InterruptEvent,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useMemo, useSyncExternalStore } from "react";
|
||||
import { ToolCall, ToolMessage } from "@ag-ui/core";
|
||||
import { ToolCallStatus } from "@copilotkit/core";
|
||||
import { useCopilotKit } from "../providers/CopilotKitProvider";
|
||||
import { useCopilotKit } from "../context";
|
||||
import { useCopilotChatConfiguration } from "../providers/CopilotChatConfigurationProvider";
|
||||
import { DEFAULT_AGENT_ID } from "@copilotkit/shared";
|
||||
import { partialJSONParse } from "@copilotkit/shared";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from "react";
|
||||
import type { StandardSchemaV1, InferSchemaOutput } from "@copilotkit/shared";
|
||||
import { useCopilotKit } from "../providers/CopilotKitProvider";
|
||||
import { useCopilotKit } from "../context";
|
||||
import { defineToolCallRenderer } from "../types/defineToolCallRenderer";
|
||||
|
||||
const EMPTY_DEPS: ReadonlyArray<unknown> = [];
|
||||
@@ -180,5 +180,5 @@ export function useRenderTool<S extends StandardSchemaV1>(
|
||||
copilotkit.addHookRenderToolCall(renderer);
|
||||
|
||||
// No cleanup removal — keeps renderer for chat history, same as useFrontendTool
|
||||
}, [config.name, copilotkit, extraDeps.length, ...extraDeps]);
|
||||
}, [config.name, copilotkit, JSON.stringify(extraDeps)]);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Suggestion } from "@copilotkit/core";
|
||||
import { useCopilotKit } from "../providers/CopilotKitProvider";
|
||||
import { useCopilotKit } from "../context";
|
||||
import { useCopilotChatConfiguration } from "../providers/CopilotChatConfigurationProvider";
|
||||
import { DEFAULT_AGENT_ID } from "@copilotkit/shared";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCopilotKit } from "../providers/CopilotKitProvider";
|
||||
import { useCopilotKit } from "../context";
|
||||
import {
|
||||
CopilotKitCoreRuntimeConnectionStatus,
|
||||
ɵcreateThreadStore,
|
||||
|
||||
@@ -4,8 +4,6 @@ import type { AbstractAgent } from "@ag-ui/client";
|
||||
import type { FrontendTool } from "@copilotkit/core";
|
||||
import type React from "react";
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
type ReactNode,
|
||||
useMemo,
|
||||
useEffect,
|
||||
@@ -14,6 +12,14 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
// Context extracted to ../context.ts for cross-platform reuse (React Native)
|
||||
import {
|
||||
CopilotKitContext,
|
||||
type CopilotKitContextValue,
|
||||
LicenseContext,
|
||||
} from "../context";
|
||||
export type { CopilotKitContextValue } from "../context";
|
||||
export { CopilotKitContext, useLicenseContext } from "../context";
|
||||
import { z } from "zod";
|
||||
import { CopilotKitInspector } from "../components/CopilotKitInspector";
|
||||
import type { Anchor } from "@copilotkit/web-inspector";
|
||||
@@ -81,34 +87,6 @@ const GENERATE_SANDBOXED_UI_DESCRIPTION =
|
||||
"3. html (streams in live — the user watches the UI build as HTML is generated)\n" +
|
||||
"4. jsFunctions (reusable helper functions)\n" +
|
||||
"5. jsExpressions (applied one-by-one — the user sees each expression take effect)";
|
||||
|
||||
// Define the context value interface - idiomatic React naming
|
||||
export interface CopilotKitContextValue {
|
||||
copilotkit: CopilotKitCoreReact;
|
||||
/**
|
||||
* Set of tool call IDs currently being executed.
|
||||
* This is tracked at the provider level to ensure tool execution events
|
||||
* are captured even before child components mount.
|
||||
*/
|
||||
executingToolCallIds: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
// Empty set for default context value
|
||||
const EMPTY_SET: ReadonlySet<string> = new Set();
|
||||
|
||||
// Create the CopilotKit context
|
||||
const CopilotKitContext = createContext<CopilotKitContextValue>({
|
||||
copilotkit: null!,
|
||||
executingToolCallIds: EMPTY_SET,
|
||||
});
|
||||
|
||||
const LicenseContext = createContext<LicenseContextValue>(
|
||||
createLicenseContextValue(null),
|
||||
);
|
||||
|
||||
export const useLicenseContext = (): LicenseContextValue =>
|
||||
useContext(LicenseContext);
|
||||
|
||||
// Provider props interface
|
||||
export interface CopilotKitProviderProps {
|
||||
children: ReactNode;
|
||||
@@ -642,15 +620,17 @@ export const CopilotKitProvider: React.FC<CopilotKitProviderProps> = ({
|
||||
}, [onError]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!onErrorRef.current) return;
|
||||
|
||||
const subscription = copilotkit.subscribe({
|
||||
onError: (event) => {
|
||||
onErrorRef.current?.({
|
||||
error: event.error,
|
||||
code: event.code,
|
||||
context: event.context,
|
||||
});
|
||||
if (onErrorRef.current) {
|
||||
onErrorRef.current(event);
|
||||
} else {
|
||||
console.error(
|
||||
`[CopilotKit] Error (${event.code}):`,
|
||||
event.error,
|
||||
event.context ?? {},
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -816,25 +796,5 @@ export const CopilotKitProvider: React.FC<CopilotKitProviderProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
// Hook to use the CopilotKit instance - returns the full context value
|
||||
export const useCopilotKit = (): CopilotKitContextValue => {
|
||||
const context = useContext(CopilotKitContext);
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useCopilotKit must be used within CopilotKitProvider");
|
||||
}
|
||||
useEffect(() => {
|
||||
const subscription = context.copilotkit.subscribe({
|
||||
onRuntimeConnectionStatusChanged: () => {
|
||||
forceUpdate();
|
||||
},
|
||||
});
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return context;
|
||||
};
|
||||
// Re-export useCopilotKit from context for backward compatibility
|
||||
export { useCopilotKit } from "../context";
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { defineConfig } from "tsdown";
|
||||
import path from "path";
|
||||
|
||||
// Resolved path to src/v2/context.ts — used to redirect the headless build's
|
||||
// relative ../context imports to the external @copilotkit/react-core/v2/context
|
||||
// package path, ensuring a shared React context instance at runtime.
|
||||
const contextModulePath = path.resolve(import.meta.dirname, "src/v2/context");
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
@@ -21,10 +27,79 @@ export default defineConfig([
|
||||
exports: {
|
||||
customExports: (exports) => ({
|
||||
...exports,
|
||||
"./v2/context": {
|
||||
import: "./dist/v2/context.mjs",
|
||||
require: "./dist/v2/context.cjs",
|
||||
},
|
||||
"./v2/headless": {
|
||||
import: "./dist/v2/headless.mjs",
|
||||
require: "./dist/v2/headless.cjs",
|
||||
},
|
||||
"./v2/styles.css": "./dist/v2/index.css",
|
||||
}),
|
||||
},
|
||||
},
|
||||
// v2/context is built separately into dist/v2/ so it produces a standalone
|
||||
// file instead of being absorbed into shared chunks.
|
||||
{
|
||||
entry: {
|
||||
context: "src/v2/context.ts",
|
||||
},
|
||||
format: ["esm", "cjs"],
|
||||
dts: true,
|
||||
sourcemap: true,
|
||||
target: "es2022",
|
||||
outDir: "dist/v2",
|
||||
external: ["react", "@copilotkit/core", "@copilotkit/shared"],
|
||||
},
|
||||
// v2/headless: platform-agnostic hooks + CopilotKitCoreReact, used by
|
||||
// @copilotkit/react-native. All @copilotkit/* deps are external — they
|
||||
// contain no Node-only code that would break Metro. Keeping them external
|
||||
// (rather than inlining) ensures the CopilotKitCoreReact class is the same
|
||||
// nominal type as the one in v2/context, avoiding unsafe `as unknown as` casts.
|
||||
{
|
||||
entry: {
|
||||
headless: "src/v2/headless.ts",
|
||||
},
|
||||
format: ["esm", "cjs"],
|
||||
dts: true,
|
||||
sourcemap: true,
|
||||
target: "es2022",
|
||||
outDir: "dist/v2",
|
||||
plugins: [
|
||||
{
|
||||
name: "externalize-context",
|
||||
resolveId(source, importer) {
|
||||
// When any file imports ../context or ./context, redirect to
|
||||
// the external package path so the context singleton is shared.
|
||||
if (importer && /context(\.ts)?$/.test(source)) {
|
||||
const resolved = path.resolve(path.dirname(importer), source);
|
||||
if (
|
||||
resolved === contextModulePath ||
|
||||
resolved === contextModulePath + ".ts"
|
||||
) {
|
||||
return {
|
||||
id: "@copilotkit/react-core/v2/context",
|
||||
external: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
],
|
||||
external: [
|
||||
"react",
|
||||
"@ag-ui/client",
|
||||
"@ag-ui/core",
|
||||
"@copilotkit/core",
|
||||
"@copilotkit/shared",
|
||||
"@copilotkit/react-core/v2/context",
|
||||
"uuid",
|
||||
"zod",
|
||||
"rxjs",
|
||||
],
|
||||
},
|
||||
{
|
||||
entry: ["src/index.tsx"],
|
||||
format: ["umd"],
|
||||
|
||||
Reference in New Issue
Block a user