/** * CommandHandler — Processes incoming CLI commands and returns structured results. * * Receives parsed Command objects from the WebSocket server, looks up * elements in the registry, executes actions via ActionExecutor, and * returns CommandResponse objects. */ import { BackHandler } from 'react-native'; import type { Command, CommandResponse, AgentKitConfig } from './types'; import { ElementRegistry } from './ElementRegistry'; import { actionExecutor } from './ActionExecutor'; import { getNavigationRef } from './AgentKitProvider'; export class CommandHandler { private config: AgentKitConfig; constructor(config: AgentKitConfig) { this.config = config; } /** * Process a command and return a response. */ async handle(command: Command): Promise { const baseResponse = { command: command.cmd, id: command.id, target: command.target, timestamp: Date.now(), }; try { switch (command.cmd) { case 'ping': return { ...baseResponse, success: true, result: { pong: true, version: '0.1.0' }, }; case 'list': return this.handleList(command, baseResponse); case 'state': return this.handleState(baseResponse); case 'tap': return this.handleTap(command, baseResponse); case 'longPress': return this.handleLongPress(command, baseResponse); case 'type': return this.handleType(command, baseResponse); case 'clear': return this.handleClear(command, baseResponse); case 'scroll': return this.handleScroll(command, baseResponse); case 'toggle': return this.handleToggle(command, baseResponse); case 'setValue': return this.handleSetValue(command, baseResponse); case 'swipe': return this.handleSwipe(command, baseResponse); case 'read': return this.handleRead(command, baseResponse); case 'back': return this.handleBack(baseResponse); case 'wait': return this.handleWait(command, baseResponse); case 'find': return this.handleFind(command, baseResponse); default: return { ...baseResponse, success: false, result: {}, error: `Unknown command: ${command.cmd}`, }; } } catch (e: any) { return { ...baseResponse, success: false, result: {}, error: `Command execution error: ${e.message}`, }; } } private handleList( command: Command, base: Omit ): CommandResponse { let elements = ElementRegistry.getAll(); if (command.filterType) { elements = elements.filter((el) => el.type === command.filterType); } return { ...base, success: true, result: { elements, count: elements.length, }, }; } private handleState( base: Omit ): CommandResponse { const screenState = ElementRegistry.getScreenState(); return { ...base, success: true, result: { screenState }, screenState, }; } private handleTap( command: Command, base: Omit ): CommandResponse { if (!command.target) { return { ...base, success: false, result: {}, error: 'tap command requires a target', }; } const element = ElementRegistry.findByTarget(command.target); if (!element) { return { ...base, success: false, result: {}, error: `Element not found: ${command.target}`, }; } // If the element is a switch/toggle with no onPress, use toggle instead const actionResult = !element.handlers.onPress && element.handlers.onValueChange ? actionExecutor.toggle(element) : actionExecutor.tap(element); return { ...base, success: actionResult.success, result: actionResult.data || {}, error: actionResult.error, ...(this.config.includeScreenState && { screenState: ElementRegistry.getScreenState(), }), }; } private handleToggle( command: Command, base: Omit ): CommandResponse { if (!command.target) { return { ...base, success: false, result: {}, error: 'toggle command requires a target', }; } const element = ElementRegistry.findByTarget(command.target); if (!element) { return { ...base, success: false, result: {}, error: `Element not found: ${command.target}`, }; } const actionResult = actionExecutor.toggle(element); return { ...base, success: actionResult.success, result: actionResult.data || {}, error: actionResult.error, ...(this.config.includeScreenState && { screenState: ElementRegistry.getScreenState(), }), }; } private handleSetValue( command: Command, base: Omit ): CommandResponse { if (!command.target) { return { ...base, success: false, result: {}, error: 'setValue command requires a target', }; } if (command.value === undefined) { return { ...base, success: false, result: {}, error: 'setValue command requires a value (number)', }; } const element = ElementRegistry.findByTarget(command.target); if (!element) { return { ...base, success: false, result: {}, error: `Element not found: ${command.target}`, }; } const actionResult = actionExecutor.setValue(element, command.value); return { ...base, success: actionResult.success, result: actionResult.data || {}, error: actionResult.error, ...(this.config.includeScreenState && { screenState: ElementRegistry.getScreenState(), }), }; } private handleSwipe( command: Command, base: Omit ): CommandResponse { if (!command.target) { return { ...base, success: false, result: {}, error: 'swipe command requires a target', }; } if (!command.direction) { return { ...base, success: false, result: {}, error: 'swipe command requires a direction (left, right, up, down)', }; } const element = ElementRegistry.findByTarget(command.target); if (!element) { return { ...base, success: false, result: {}, error: `Element not found: ${command.target}`, }; } const actionResult = actionExecutor.swipe(element, command.direction); return { ...base, success: actionResult.success, result: actionResult.data || {}, error: actionResult.error, ...(this.config.includeScreenState && { screenState: ElementRegistry.getScreenState(), }), }; } private handleLongPress( command: Command, base: Omit ): CommandResponse { if (!command.target) { return { ...base, success: false, result: {}, error: 'longPress command requires a target', }; } const element = ElementRegistry.findByTarget(command.target); if (!element) { return { ...base, success: false, result: {}, error: `Element not found: ${command.target}`, }; } const actionResult = actionExecutor.longPress(element); return { ...base, success: actionResult.success, result: actionResult.data || {}, error: actionResult.error, ...(this.config.includeScreenState && { screenState: ElementRegistry.getScreenState(), }), }; } private handleType( command: Command, base: Omit ): CommandResponse { if (!command.target) { return { ...base, success: false, result: {}, error: 'type command requires a target', }; } if (command.text === undefined) { return { ...base, success: false, result: {}, error: 'type command requires text', }; } const element = ElementRegistry.findByTarget(command.target); if (!element) { return { ...base, success: false, result: {}, error: `Element not found: ${command.target}`, }; } const actionResult = actionExecutor.type(element, command.text); return { ...base, success: actionResult.success, result: actionResult.data || {}, error: actionResult.error, ...(this.config.includeScreenState && { screenState: ElementRegistry.getScreenState(), }), }; } private handleClear( command: Command, base: Omit ): CommandResponse { if (!command.target) { return { ...base, success: false, result: {}, error: 'clear command requires a target', }; } const element = ElementRegistry.findByTarget(command.target); if (!element) { return { ...base, success: false, result: {}, error: `Element not found: ${command.target}`, }; } const actionResult = actionExecutor.clear(element); return { ...base, success: actionResult.success, result: actionResult.data || {}, error: actionResult.error, ...(this.config.includeScreenState && { screenState: ElementRegistry.getScreenState(), }), }; } private handleScroll( command: Command, base: Omit ): CommandResponse { if (!command.target) { return { ...base, success: false, result: {}, error: 'scroll command requires a target', }; } const element = ElementRegistry.findByTarget(command.target); if (!element) { return { ...base, success: false, result: {}, error: `Element not found: ${command.target}`, }; } const direction = command.direction || 'down'; const amount = command.amount || 300; const actionResult = actionExecutor.scroll(element, direction, amount); return { ...base, success: actionResult.success, result: actionResult.data || {}, error: actionResult.error, }; } private handleRead( command: Command, base: Omit ): CommandResponse { if (!command.target) { return { ...base, success: false, result: {}, error: 'read command requires a target', }; } const element = ElementRegistry.findByTarget(command.target); if (!element) { return { ...base, success: false, result: {}, error: `Element not found: ${command.target}`, }; } const actionResult = actionExecutor.read(element); return { ...base, success: actionResult.success, result: actionResult.data || {}, }; } private handleBack( base: Omit ): CommandResponse { try { const navRef = getNavigationRef(); if (navRef?.current?.canGoBack?.()) { navRef.current.goBack(); return { ...base, success: true, result: { navigatedBack: true }, ...(this.config.includeScreenState && { screenState: ElementRegistry.getScreenState(), }), }; } // Fallback: try Android hardware back BackHandler.exitApp(); return { ...base, success: true, result: { navigatedBack: true, note: 'No navigationRef — used BackHandler fallback', }, ...(this.config.includeScreenState && { screenState: ElementRegistry.getScreenState(), }), }; } catch (e: any) { return { ...base, success: false, result: {}, error: `Back navigation failed: ${e.message}`, }; } } private async handleWait( command: Command, base: Omit ): Promise { if (!command.target) { return { ...base, success: false, result: {}, error: 'wait command requires a target', }; } const timeout = command.timeout || 5000; const pollInterval = 200; const startTime = Date.now(); while (Date.now() - startTime < timeout) { const element = ElementRegistry.findByTarget(command.target); if (element) { return { ...base, success: true, result: { found: true, element: element.info, waitTime: Date.now() - startTime, }, }; } await this.sleep(pollInterval); } return { ...base, success: false, result: { found: false, waitTime: timeout }, error: `Timed out waiting for element: ${command.target} (${timeout}ms)`, }; } private handleFind( command: Command, base: Omit ): CommandResponse { let results = ElementRegistry.getAll(); if (command.filterType) { results = results.filter((el) => el.type === command.filterType); } if (command.filterText) { const lower = command.filterText.toLowerCase(); results = results.filter( (el) => el.label.toLowerCase().includes(lower) || el.value?.toLowerCase().includes(lower) || el.id.toLowerCase().includes(lower) ); } if (command.target) { const lower = command.target.toLowerCase(); results = results.filter( (el) => el.label.toLowerCase().includes(lower) || el.value?.toLowerCase().includes(lower) || el.id.toLowerCase().includes(lower) ); } return { ...base, success: true, result: { elements: results, count: results.length, }, }; } private sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } }