/** * 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} ); }