/**
* AgentKitProvider — React context provider that wraps the app to enable AgentKit.
*
* This is the main integration point for developers. Wrap your app with this
* provider and the library handles everything: element introspection, command
* server, and action execution.
*
* Usage:
* ```tsx
* import { AgentKitProvider } from 'react-native-agentkit';
*
* function App() {
* return (
*
*
*
* );
* }
* ```
*/
import React, {
createContext,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { View, InteractionManager } from 'react-native';
import type { AgentKitConfig, ElementInfo, ElementHandlers } from './types';
import { DEFAULT_CONFIG } from './types';
import { ElementRegistry } from './ElementRegistry';
import { BridgeServer } from './WSServer';
import { RelayTransport } from './RelayTransport';
import { introspect } from './Introspector';
import { setDebugEnabled, logInfo, logDebug } from './logger';
import {
installNativeDialogInterceptor,
uninstallNativeDialogInterceptor,
} from './NativeDialogInterceptor';
// ─── Context ─────────────────────────────────────────────────────────────────
export interface AgentKitContextValue {
/** Whether the bridge server is currently running */
isRunning: boolean;
/** Whether the relay transport is connected */
isRelayConnected: boolean;
/** Number of registered elements */
elementCount: number;
/** Manually register an element */
registerElement: (
id: string,
info: Omit,
ref: React.RefObject,
handlers?: ElementHandlers
) => string;
/** Manually unregister an element */
unregisterElement: (id: string) => void;
/** Force a re-scan of the component tree */
rescan: () => void;
}
export const AgentKitContext = createContext({
isRunning: false,
isRelayConnected: false,
elementCount: 0,
registerElement: () => '',
unregisterElement: () => {},
rescan: () => {},
});
// ─── Props ───────────────────────────────────────────────────────────────────
export interface AgentKitProviderProps {
children: React.ReactNode;
/** WebSocket server port (default: 8347) */
port?: number;
/** Enable debug logging (default: false) */
debug?: boolean;
/** Element scan interval in ms (default: 1000) */
scanInterval?: number;
/** Include non-interactive elements in scans (default: false) */
includeNonInteractive?: boolean;
/** Auto-include screen state in command responses (default: true) */
includeScreenState?: boolean;
/** Disable the bridge entirely (e.g. in production) */
enabled?: boolean;
/** Cloud relay server URL for production (e.g. "ws://relay.example.com:8347") */
relayUrl?: string;
/** Channel ID for relay pairing (default: "default") */
channelId?: string;
/** Shared secret for relay channel authentication */
channelSecret?: string;
/** Navigation container ref for back navigation support */
navigationRef?: React.RefObject;
}
/** Module-level navigation ref for CommandHandler to access */
let _navigationRef: React.RefObject | null = null;
export function getNavigationRef(): React.RefObject | null {
return _navigationRef;
}
// ─── Provider Component ──────────────────────────────────────────────────────
export function AgentKitProvider({
children,
port,
debug,
scanInterval,
includeNonInteractive,
includeScreenState,
enabled = true,
relayUrl,
channelId,
channelSecret,
navigationRef,
}: AgentKitProviderProps) {
// Store navigation ref for CommandHandler to access
_navigationRef = navigationRef ?? null;
const rootRef = useRef(null);
const serverRef = useRef(null);
const relayRef = useRef(null);
const scanTimerRef = useRef | null>(null);
const [isRunning, setIsRunning] = useState(false);
const [isRelayConnected, setIsRelayConnected] = useState(false);
const [elementCount, setElementCount] = useState(0);
// Build config from props
const config: AgentKitConfig = {
port: port ?? DEFAULT_CONFIG.port,
debug: debug ?? DEFAULT_CONFIG.debug,
scanInterval: scanInterval ?? DEFAULT_CONFIG.scanInterval,
includeNonInteractive:
includeNonInteractive ?? DEFAULT_CONFIG.includeNonInteractive,
includeScreenState: includeScreenState ?? DEFAULT_CONFIG.includeScreenState,
relayUrl,
channelId: channelId ?? DEFAULT_CONFIG.channelId,
channelSecret,
};
// ─── Introspection scan ──────────────────────────────────────────────
const runScan = useCallback(() => {
if (!rootRef.current) return;
InteractionManager.runAfterInteractions(() => {
const elements = introspect(rootRef, config.includeNonInteractive);
ElementRegistry.bulkUpdate(elements);
// Use registry size (includes both introspected + manually-registered)
setElementCount(ElementRegistry.size);
logDebug(`Scan complete: ${ElementRegistry.size} elements`);
});
}, [config.includeNonInteractive]);
// ─── Server lifecycle ────────────────────────────────────────────────
useEffect(() => {
if (!enabled) return;
setDebugEnabled(config.debug);
// Create and start the server
const server = new BridgeServer(config);
serverRef.current = server;
server.start();
setIsRunning(true);
// Install native dialog interceptor (alerts + permissions)
// Only suppresses native dialogs when an agent is actually connected
installNativeDialogInterceptor(
() => relayRef.current?.isConnected ?? false
);
logInfo('AgentKitProvider mounted');
// Start relay transport if configured
let relay: RelayTransport | null = null;
if (config.relayUrl) {
relay = new RelayTransport({
relayUrl: config.relayUrl,
channelId: config.channelId,
channelSecret: config.channelSecret,
onMessage: (msg) => server.processMessage(msg),
onStatusChange: setIsRelayConnected,
});
relayRef.current = relay;
relay.start();
logInfo(`Relay transport enabled → ${config.relayUrl}`);
}
// Start periodic scanning
// Delay the first scan to ensure the tree is rendered
const initialScanTimeout = setTimeout(() => {
runScan();
// Then scan periodically
scanTimerRef.current = setInterval(runScan, config.scanInterval);
}, 500);
return () => {
clearTimeout(initialScanTimeout);
if (scanTimerRef.current) {
clearInterval(scanTimerRef.current);
scanTimerRef.current = null;
}
if (relay) {
relay.stop();
relayRef.current = null;
setIsRelayConnected(false);
}
server.stop();
serverRef.current = null;
setIsRunning(false);
ElementRegistry.clear();
uninstallNativeDialogInterceptor();
logInfo('AgentKitProvider unmounted');
};
}, [enabled]); // eslint-disable-line react-hooks/exhaustive-deps
// ─── Context value ───────────────────────────────────────────────────
const registerElement = useCallback(
(
id: string,
info: Omit,
ref: React.RefObject,
handlers?: ElementHandlers
): string => {
const registeredId = ElementRegistry.register(id, info, ref, handlers);
setElementCount(ElementRegistry.size);
return registeredId;
},
[]
);
const unregisterElement = useCallback((id: string) => {
ElementRegistry.unregister(id);
setElementCount(ElementRegistry.size);
}, []);
const rescan = useCallback(() => {
runScan();
}, [runScan]);
const contextValue: AgentKitContextValue = {
isRunning,
isRelayConnected,
elementCount,
registerElement,
unregisterElement,
rescan,
};
// If disabled, just render children without the bridge
if (!enabled) {
return <>{children}>;
}
return (
{children}
);
}