1- import Base from '../core/Base.mjs' ;
2- import ClassSystemUtil from '../util/ClassSystem.mjs' ;
3- import Socket from '../data/connection/WebSocket.mjs' ;
4- import StoreManager from '../manager/Store.mjs' ;
1+ import Base from '../core/Base.mjs' ;
2+ import ClassSystemUtil from '../util/ClassSystem.mjs' ;
3+ import ComponentService from './client/ComponentService.mjs' ;
4+ import DataService from './client/DataService.mjs' ;
5+ import RuntimeService from './client/RuntimeService.mjs' ;
6+ import Socket from '../data/connection/WebSocket.mjs' ;
57
68/**
79 * The AI Client establishes a WebSocket connection to the Neural Link MCP Server.
@@ -40,6 +42,17 @@ class Client extends Base {
4042 * @protected
4143 */
4244 isConnected = false
45+ /**
46+ * Map JSON-RPC method prefixes to service instances
47+ * @member {Object} serviceMap
48+ * @protected
49+ */
50+ serviceMap = null
51+ /**
52+ * @member {Object} services=null
53+ * @protected
54+ */
55+ services = null
4356 /**
4457 * @member {Neo.data.connection.WebSocket|null} socket=null
4558 * @protected
@@ -52,13 +65,37 @@ class Client extends Base {
5265 construct ( config ) {
5366 super . construct ( config ) ;
5467
68+ let me = this ;
69+
70+ me . services = {
71+ component : Neo . create ( ComponentService , { client : me } ) ,
72+ data : Neo . create ( DataService , { client : me } ) ,
73+ runtime : Neo . create ( RuntimeService , { client : me } )
74+ } ;
75+
76+ me . serviceMap = {
77+ get_component : me . services . component ,
78+ get_vdom : me . services . component ,
79+ get_vnode : me . services . component ,
80+ query_component : me . services . component ,
81+ set_component : me . services . component ,
82+
83+ get_record : me . services . data ,
84+ inspect_store : me . services . data ,
85+ list_stores : me . services . data ,
86+
87+ get_drag : me . services . runtime ,
88+ get_window : me . services . runtime ,
89+ reload_page : me . services . runtime
90+ } ;
91+
5592 Neo . currentWorker . on ( {
56- connect : this . onAppWorkerWindowConnect ,
57- disconnect : this . onAppWorkerWindowDisconnect ,
58- scope : this
93+ connect : me . onAppWorkerWindowConnect ,
94+ disconnect : me . onAppWorkerWindowDisconnect ,
95+ scope : me
5996 } ) ;
6097
61- this . connect ( ) ;
98+ me . connect ( ) ;
6299 }
63100
64101 /**
@@ -200,210 +237,26 @@ class Client extends Base {
200237 * @returns {Promise<*> } The result of the operation
201238 */
202239 async handleRequest ( method , params ) {
203- let me = this ,
204- component ;
205-
206- switch ( method ) {
207- case 'get_component_property' :
208- component = Neo . getComponent ( params . id ) ;
209- if ( ! component ) throw new Error ( `Component not found: ${ params . id } ` ) ;
210- return { value : me . safeSerialize ( component [ params . property ] ) } ;
211-
212- case 'query_component' :
213- let { selector, rootId} = params ,
214- matches = [ ] ;
215-
216- if ( rootId ) {
217- component = Neo . getComponent ( rootId ) ;
218- if ( ! component ) throw new Error ( `Root component not found: ${ rootId } ` ) ;
219- // down() returns a single item or array based on returnFirstMatch param.
220- // We want all matches, so we pass false.
221- matches = component . down ( selector , false )
222- } else {
223- matches = Neo . manager . Component . find ( selector )
224- }
225-
226- if ( ! Array . isArray ( matches ) ) {
227- matches = matches ? [ matches ] : [ ]
228- }
229-
230- return {
231- components : matches . map ( c => ( {
232- id : c . id ,
233- className : c . className ,
234- ntype : c . ntype
235- } ) )
236- } ;
237-
238- case 'get_component_tree' :
239- return { tree : me . serializeComponent ( me . getComponentRoot ( params . rootId ) , params . depth || - 1 ) } ;
240-
241- case 'get_drag_state' :
242- const dragCoordinator = Neo . manager ?. DragCoordinator ;
243-
244- if ( dragCoordinator ) {
245- return {
246- activeTargetZone : dragCoordinator . activeTargetZone ? {
247- id : dragCoordinator . activeTargetZone . id ,
248- sortGroup : dragCoordinator . activeTargetZone . sortGroup ,
249- windowId : dragCoordinator . activeTargetZone . windowId
250- } : null ,
251- sortZones : Array . from ( dragCoordinator . sortZones . entries ( ) ) . map ( ( [ group , map ] ) => ( {
252- group,
253- windows : Array . from ( map . keys ( ) )
254- } ) )
255- }
256- }
257-
258- return { } ;
259-
260- case 'get_vdom_tree' :
261- component = me . getComponentRoot ( params . rootId ) ;
262- if ( ! component ) throw new Error ( 'Root component not found' ) ;
263- return { vdom : component . vdom } ;
264-
265- case 'get_vnode_tree' :
266- component = me . getComponentRoot ( params . rootId ) ;
267- if ( ! component ) throw new Error ( 'Root component not found' ) ;
268- return { vnode : component . vnode } ;
269-
270- case 'get_record' :
271- let { recordId, storeId} = params ,
272- record ;
273-
274- if ( storeId ) {
275- const store = Neo . get ( storeId ) ;
276- if ( ! store ) throw new Error ( `Store not found: ${ storeId } ` ) ;
277- record = store . get ( recordId )
278- } else {
279- const matches = [ ] ;
280- StoreManager . items . forEach ( store => {
281- const rec = store . get ( recordId ) ;
282- if ( rec ) matches . push ( rec )
283- } ) ;
284-
285- if ( matches . length > 1 ) {
286- throw new Error ( `Multiple records found with ID ${ recordId } . Please specify storeId.` )
287- } else if ( matches . length === 1 ) {
288- record = matches [ 0 ]
289- }
290- }
291-
292- if ( ! record ) throw new Error ( `Record not found: ${ recordId } ` ) ;
293-
294- return record . toJSON ( ) ;
295-
296- case 'get_window_info' :
297- const windowManager = Neo . manager ?. Window ;
298-
299- if ( windowManager ) {
300- return {
301- windows : windowManager . items . map ( win => ( {
302- id : win . id ,
303- appName : win . appName ,
304- chrome : win . chrome ,
305- innerRect : win . innerRect ,
306- outerRect : win . outerRect
307- } ) )
308- }
309- }
310-
311- return { windows : [ ] } ;
312-
313- case 'inspect_store' :
314- const store = Neo . get ( params . storeId ) ;
315- if ( ! store ) throw new Error ( `Store not found: ${ params . storeId } ` ) ;
316-
317- const items = [ ] ;
318- const limit = Math . min ( store . count , 50 ) ;
319-
320- for ( let i = 0 ; i < limit ; i ++ ) {
321- const record = store . getAt ( i ) ;
322- if ( record ) {
323- items . push ( record . toJSON ( ) )
324- }
325- }
326-
327- return {
328- id : store . id ,
329- count : store . count ,
330- model : store . model ?. className || 'N/A' ,
331- filters : store . exportFilters ?. ( ) || [ ] ,
332- sorters : store . exportSorters ?. ( ) || [ ] ,
333- items
334- } ;
335-
336- case 'list_stores' :
337- return {
338- stores : StoreManager . items . map ( s => ( {
339- id : s . id ,
340- model : s . model ?. className || 'N/A' ,
341- count : s . count ,
342- isLoaded : s . isLoaded
343- } ) )
344- } ;
345-
346- case 'reload_page' :
347- Neo . Main . reloadWindow ( ) ;
348- return { status : 'reloading' } ;
349-
350- case 'set_component_property' :
351- component = Neo . getComponent ( params . id ) ;
352- if ( ! component ) throw new Error ( `Component not found: ${ params . id } ` ) ;
353- component [ params . property ] = params . value ;
354- return { success : true } ;
355-
356- default :
357- throw new Error ( `Unknown method: ${ method } ` ) ;
358- }
359- }
360-
361- /**
362- * @param {String } [rootId]
363- * @returns {Neo.component.Base|null }
364- */
365- getComponentRoot ( rootId ) {
366- if ( rootId ) {
367- return Neo . getComponent ( rootId )
368- }
369-
370- const apps = Object . values ( Neo . apps || { } ) ;
371-
372- if ( apps . length > 0 ) {
373- return apps [ 0 ] . mainView
374- }
375-
376- return null
377- }
378-
379- /**
380- * @param {* } value
381- * @returns {* }
382- */
383- safeSerialize ( value ) {
384- const type = Neo . typeOf ( value ) ;
385-
386- if ( type === 'NeoInstance' ) {
387- return {
388- neoInstance : true ,
389- id : value . id ,
390- className : value . className
240+ let me = this ,
241+ service = null ,
242+ prefix ;
243+
244+ // Find matching service based on prefix
245+ // e.g. "get_component_property" -> matches "get_component" prefix
246+ for ( prefix in me . serviceMap ) {
247+ if ( method . startsWith ( prefix ) ) {
248+ service = me . serviceMap [ prefix ] ;
249+ break
391250 }
392251 }
393252
394- if ( type === 'Object' ) {
395- const result = { } ;
396- Object . entries ( value ) . forEach ( ( [ k , v ] ) => {
397- result [ k ] = this . safeSerialize ( v )
398- } ) ;
399- return result
400- }
253+ const fnName = Neo . snakeToCamel ( method ) ;
401254
402- if ( type === 'Array ' ) {
403- return value . map ( v => this . safeSerialize ( v ) )
255+ if ( service && typeof service [ fnName ] === 'function ' ) {
256+ return service [ fnName ] ( params )
404257 }
405258
406- return value
259+ throw new Error ( `Unknown method: ${ method } ` ) ;
407260 }
408261
409262 /**
@@ -455,32 +308,6 @@ class Client extends Base {
455308 } ) )
456309 }
457310 }
458-
459- /**
460- * @param {Neo.component.Base } component
461- * @param {Number } maxDepth
462- * @param {Number } currentDepth
463- * @returns {Object }
464- */
465- serializeComponent ( component , maxDepth , currentDepth = 1 ) {
466- if ( ! component ) return null ;
467-
468- const result = {
469- id : component . id ,
470- className : component . className ,
471- ntype : component . ntype
472- } ;
473-
474- if ( maxDepth === - 1 || currentDepth < maxDepth ) {
475- const children = Neo . manager . Component . getChildren ( component ) ;
476-
477- if ( children && children . length > 0 ) {
478- result . items = children . map ( child => this . serializeComponent ( child , maxDepth , currentDepth + 1 ) )
479- }
480- }
481-
482- return result
483- }
484311}
485312
486313export default Neo . setupClass ( Client ) ;
0 commit comments