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

296 lines
8.5 KiB
TypeScript

import React, {
createContext,
useCallback,
useContext,
useEffect,
useRef,
} from "react";
import type { ReactNode } from "react";
import { useAgent } from "@copilotkit/react-core/v2/headless";
import { useCopilotKit } from "@copilotkit/react-core/v2/context";
import { DEFAULT_AGENT_ID, randomUUID } from "@copilotkit/shared";
import type { InputContent } from "@copilotkit/shared";
import type { CopilotKitCoreErrorCode } from "@copilotkit/core";
import { useAttachments } from "./hooks/use-attachments";
import type { NativeAttachmentsConfig } from "./hooks/use-attachments";
import type { Attachment } from "@copilotkit/shared";
// ---------------------------------------------------------------------------
// Context
// ---------------------------------------------------------------------------
export interface CopilotChatContextValue {
/** The resolved agent instance. */
agent: any;
/** Whether the agent is currently running. */
isRunning: boolean;
/** Current messages in the conversation. */
messages: any[];
/** Currently selected attachments (uploading + ready). */
attachments: Attachment[];
/** Whether attachments are enabled. */
attachmentsEnabled: boolean;
/** Open the native document picker to add files. */
openPicker: () => Promise<void>;
/** Remove an attachment by ID. */
removeAttachment: (id: string) => void;
/**
* Submit a message with optional attachments.
* Handles consuming ready attachments, building InputContent[],
* calling agent.addMessage, and running the agent.
*/
submitMessage: (text: string) => Promise<void>;
}
const CopilotChatCtx = createContext<CopilotChatContextValue | null>(null);
/**
* Hook to access the CopilotChat context from child components.
* Must be called inside a `<CopilotChat>` component tree.
*/
export function useCopilotChatContext(): CopilotChatContextValue {
const ctx = useContext(CopilotChatCtx);
if (!ctx) {
throw new Error(
"useCopilotChatContext must be used within a <CopilotChat> component",
);
}
return ctx;
}
// ---------------------------------------------------------------------------
// Props
// ---------------------------------------------------------------------------
export interface CopilotChatBaseProps {
/**
* The agent ID to use for this chat session.
* Matches the web SDK's CopilotChat `agentId` prop.
*
* Resolution order: `agentId` > `agentName` > `"default"`
*/
agentId?: string;
/**
* @deprecated Use `agentId` instead. `agentName` is kept for backwards
* compatibility and will be removed in a future release.
*/
agentName?: string;
/**
* Thread ID for this chat session. When provided, the chat will resume
* the specified thread. Matches the web SDK's CopilotChat `threadId` prop.
*/
threadId?: string;
/**
* Error handler scoped to this chat's agent. Fires in addition to the
* provider-level onError (does not suppress it). Receives only errors
* whose context.agentId matches this chat's agent.
*/
onError?: (event: {
error: Error;
code: CopilotKitCoreErrorCode;
context: Record<string, any>;
}) => void | Promise<void>;
/**
* Throttle interval (in milliseconds) for re-renders triggered by message
* change notifications. Overrides the provider-level `defaultThrottleMs`
* for this chat instance. Forwarded to the internal `useAgent()` hook.
*
* @default undefined -- inherits from provider `defaultThrottleMs`;
* if that is also unset, re-renders are unthrottled.
*/
throttleMs?: number;
/**
* Enable multimodal file attachments (images, audio, video, documents).
* Pass a NativeAttachmentsConfig object to configure file picking behavior.
*/
attachments?: NativeAttachmentsConfig;
/**
* Optional children rendered inside the chat context.
*/
children?: ReactNode;
}
export interface CopilotChatProps extends CopilotChatBaseProps {
/** Passthrough props are forwarded to consumers via the agent context. */
[key: string]: unknown;
}
/**
* Headless CopilotChat component for React Native.
*
* Wires up the `useAgent` hook with `agentId` resolution and renders children.
* Unlike the web SDK's CopilotChat, this component does not render any UI
* elements -- consumers provide their own React Native views.
*
* Children can access chat state via `useCopilotChatContext()`.
*
* ```tsx
* import { CopilotChat, useCopilotChatContext } from "@copilotkit/react-native";
*
* function MyChatUI() {
* const { messages, submitMessage, attachments, openPicker } = useCopilotChatContext();
* // ... render your UI
* }
*
* <CopilotChat agentId="my-agent" attachments={{ enabled: true }}>
* <MyChatUI />
* </CopilotChat>
* ```
*/
export function CopilotChat({
agentId,
agentName,
threadId,
onError,
throttleMs,
attachments: attachmentsConfig,
children,
..._rest
}: CopilotChatProps) {
const resolvedAgentId = agentId ?? agentName ?? DEFAULT_AGENT_ID;
// Deprecation warning (dev only, fires once per mount)
const warnedRef = useRef(false);
useEffect(() => {
if (
agentName !== undefined &&
agentId === undefined &&
!warnedRef.current
) {
warnedRef.current = true;
if (typeof __DEV__ === "undefined" || __DEV__) {
console.warn(
"[CopilotKit] agentName is deprecated, use agentId instead",
);
}
}
}, [agentName, agentId]);
const { agent } = useAgent({ agentId: resolvedAgentId, throttleMs });
// Set threadId on the agent when provided
useEffect(() => {
if (threadId) {
agent.threadId = threadId;
}
}, [agent, threadId]);
// onError subscription -- forward core errors scoped to this chat's agent
const { copilotkit } = useCopilotKit();
const onErrorRef = useRef(onError);
useEffect(() => {
onErrorRef.current = onError;
}, [onError]);
useEffect(() => {
if (!onErrorRef.current) return;
const subscription = copilotkit.subscribe({
onError: (event) => {
// Only forward errors that match this chat's agent
if (
event.context?.agentId === resolvedAgentId ||
!event.context?.agentId
) {
onErrorRef.current?.({
error: event.error,
code: event.code,
context: event.context,
});
}
},
});
return () => {
subscription.unsubscribe();
};
}, [copilotkit, resolvedAgentId]);
// Attachments
const {
attachments: selectedAttachments,
enabled: attachmentsEnabled,
openPicker,
removeAttachment,
consumeAttachments,
} = useAttachments({ config: attachmentsConfig });
// Submit handler -- mirrors web CopilotChat.tsx lines 234-288
const submitMessage = useCallback(
async (value: string) => {
// Block if uploads in progress
const hasUploading = selectedAttachments.some(
(a) => a.status === "uploading",
);
if (hasUploading) {
console.error(
"[CopilotKit] Cannot send while attachments are uploading",
);
return;
}
const readyAttachments = consumeAttachments();
if (readyAttachments.length > 0) {
const contentParts: InputContent[] = [];
if (value.trim()) {
contentParts.push({ type: "text", text: value });
}
for (const att of readyAttachments) {
contentParts.push({
type: att.type,
source: att.source,
metadata: {
...(att.filename ? { filename: att.filename } : {}),
...att.metadata,
},
} as InputContent);
}
agent.addMessage({
id: randomUUID(),
role: "user",
content: contentParts,
});
} else {
agent.addMessage({
id: randomUUID(),
role: "user",
content: value,
});
}
try {
await copilotkit.runAgent({ agent });
} catch (error) {
console.error("CopilotChat: runAgent failed", error);
}
},
// copilotkit is intentionally excluded -- it is a stable ref that never changes.
// eslint-disable-next-line react-hooks/exhaustive-deps
[agent, selectedAttachments, consumeAttachments],
);
const contextValue: CopilotChatContextValue = {
agent,
isRunning: agent.isRunning,
messages: agent.messages,
attachments: selectedAttachments,
attachmentsEnabled,
openPicker,
removeAttachment,
submitMessage,
};
return (
<CopilotChatCtx.Provider value={contextValue}>
{children}
</CopilotChatCtx.Provider>
);
}