Skip to content

Commit a598a76

Browse files
committed
Implement Neural Link Client Core (Tree & Props) #8170
1 parent 5326a8e commit a598a76

File tree

4 files changed

+196
-11
lines changed

4 files changed

+196
-11
lines changed

ai/mcp/server/neural-link/openapi.yaml

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,68 @@ paths:
6767
schema:
6868
$ref: '#/components/schemas/ErrorResponse'
6969

70+
/component/vdom/get:
71+
post:
72+
summary: Get Component VDOM Tree
73+
operationId: get_vdom_tree
74+
x-pass-as-object: true
75+
description: Retrieves the VDOM tree of a component.
76+
requestBody:
77+
content:
78+
application/json:
79+
schema:
80+
$ref: '#/components/schemas/GetComponentTreeRequest'
81+
responses:
82+
'200':
83+
description: The VDOM tree structure
84+
content:
85+
application/json:
86+
schema:
87+
type: object
88+
'400':
89+
description: Invalid request body
90+
content:
91+
application/json:
92+
schema:
93+
$ref: '#/components/schemas/ErrorResponse'
94+
'500':
95+
description: Internal server error
96+
content:
97+
application/json:
98+
schema:
99+
$ref: '#/components/schemas/ErrorResponse'
100+
101+
/component/vnode/get:
102+
post:
103+
summary: Get Component VNode Tree
104+
operationId: get_vnode_tree
105+
x-pass-as-object: true
106+
description: Retrieves the VNode tree of a component.
107+
requestBody:
108+
content:
109+
application/json:
110+
schema:
111+
$ref: '#/components/schemas/GetComponentTreeRequest'
112+
responses:
113+
'200':
114+
description: The VNode tree structure
115+
content:
116+
application/json:
117+
schema:
118+
type: object
119+
'400':
120+
description: Invalid request body
121+
content:
122+
application/json:
123+
schema:
124+
$ref: '#/components/schemas/ErrorResponse'
125+
'500':
126+
description: Internal server error
127+
content:
128+
application/json:
129+
schema:
130+
$ref: '#/components/schemas/ErrorResponse'
131+
70132
/page/reload:
71133
post:
72134
summary: Reload Page
@@ -143,6 +205,12 @@ components:
143205
GetComponentTreeRequest:
144206
type: object
145207
properties:
208+
rootId:
209+
type: string
210+
description: Optional root component ID. Defaults to the App's MainView if available.
211+
depth:
212+
type: integer
213+
description: Depth limit. 1=Self, 2=Self+Children, -1=Infinite.
146214
windowId:
147215
type: string
148216
description: The target window ID

ai/mcp/server/neural-link/services/ConnectionService.mjs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,11 +231,37 @@ class ConnectionService extends Base {
231231
/**
232232
* Retrieves the full component tree of the application.
233233
* @param {Object} opts The options object.
234+
* @param {Number} [opts.depth] The depth limit.
235+
* @param {String} [opts.rootId] Optional root component ID.
234236
* @param {String} [opts.windowId] The target window ID.
235237
* @returns {Promise<Object>} The component tree structure.
236238
*/
237-
async getComponentTree({windowId}) {
238-
return await this.#call(windowId, 'get_component_tree', {})
239+
async getComponentTree({depth, rootId, windowId}) {
240+
return await this.#call(windowId, 'get_component_tree', {depth, rootId})
241+
}
242+
243+
/**
244+
* Retrieves the VDOM tree of a component.
245+
* @param {Object} opts The options object.
246+
* @param {Number} [opts.depth] The depth limit.
247+
* @param {String} [opts.rootId] Optional root component ID.
248+
* @param {String} [opts.windowId] The target window ID.
249+
* @returns {Promise<Object>} The VDOM tree structure.
250+
*/
251+
async getVdomTree({depth, rootId, windowId}) {
252+
return await this.#call(windowId, 'get_vdom_tree', {depth, rootId})
253+
}
254+
255+
/**
256+
* Retrieves the VNode tree of a component.
257+
* @param {Object} opts The options object.
258+
* @param {Number} [opts.depth] The depth limit.
259+
* @param {String} [opts.rootId] Optional root component ID.
260+
* @param {String} [opts.windowId] The target window ID.
261+
* @returns {Promise<Object>} The VNode tree structure.
262+
*/
263+
async getVnodeTree({depth, rootId, windowId}) {
264+
return await this.#call(windowId, 'get_vnode_tree', {depth, rootId})
239265
}
240266

241267
/**

ai/mcp/server/neural-link/services/toolService.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ const openApiFilePath = path.join(__dirname, '../openapi.yaml');
1010
const serviceMapping = {
1111
get_component_property: ConnectionService.getComponentProperty.bind(ConnectionService),
1212
get_component_tree : ConnectionService.getComponentTree.bind(ConnectionService),
13+
get_vdom_tree : ConnectionService.getVdomTree.bind(ConnectionService),
14+
get_vnode_tree : ConnectionService.getVnodeTree.bind(ConnectionService),
1315
reload_page : ConnectionService.reloadPage.bind(ConnectionService),
1416
set_component_property: ConnectionService.setComponentProperty.bind(ConnectionService)
1517
};

src/ai/Client.mjs

Lines changed: 98 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -119,28 +119,91 @@ class Client extends Base {
119119
* @returns {Promise<*>} The result of the operation
120120
*/
121121
async handleRequest(method, params) {
122+
let me = this,
123+
component;
124+
122125
switch (method) {
123-
case 'reload_page':
124-
Neo.Main.reloadWindow();
125-
return {status: 'reloading'};
126+
case 'get_component_property':
127+
component = Neo.getComponent(params.id);
128+
if (!component) throw new Error(`Component not found: ${params.id}`);
129+
return {value: me.safeSerialize(component[params.property])};
126130

127131
case 'get_component_tree':
128-
// TODO: Implement actual tree retrieval
129-
return {root: 'viewport'};
132+
return {tree: me.serializeComponent(me.getComponentRoot(params.rootId), params.depth || -1)};
130133

131-
case 'get_component_property':
132-
// TODO: Implement property retrieval
133-
return {value: null};
134+
case 'get_vdom_tree':
135+
component = me.getComponentRoot(params.rootId);
136+
if (!component) throw new Error('Root component not found');
137+
return {vdom: component.vdom};
138+
139+
case 'get_vnode_tree':
140+
component = me.getComponentRoot(params.rootId);
141+
if (!component) throw new Error('Root component not found');
142+
return {vnode: component.vnode};
143+
144+
case 'reload_page':
145+
Neo.Main.reloadWindow();
146+
return {status: 'reloading'};
134147

135148
case 'set_component_property':
136-
// TODO: Implement property setting
149+
component = Neo.getComponent(params.id);
150+
if (!component) throw new Error(`Component not found: ${params.id}`);
151+
component[params.property] = params.value;
137152
return {success: true};
138153

139154
default:
140155
throw new Error(`Unknown method: ${method}`);
141156
}
142157
}
143158

159+
/**
160+
* @param {String} [rootId]
161+
* @returns {Neo.component.Base|null}
162+
*/
163+
getComponentRoot(rootId) {
164+
if (rootId) {
165+
return Neo.getComponent(rootId)
166+
}
167+
168+
const apps = Object.values(Neo.apps || {});
169+
170+
if (apps.length > 0) {
171+
return apps[0].mainView
172+
}
173+
174+
return null
175+
}
176+
177+
/**
178+
* @param {*} value
179+
* @returns {*}
180+
*/
181+
safeSerialize(value) {
182+
const type = Neo.typeOf(value);
183+
184+
if (type === 'NeoInstance') {
185+
return {
186+
neoInstance: true,
187+
id : value.id,
188+
className : value.className
189+
}
190+
}
191+
192+
if (type === 'Object') {
193+
const result = {};
194+
Object.entries(value).forEach(([k, v]) => {
195+
result[k] = this.safeSerialize(v)
196+
});
197+
return result
198+
}
199+
200+
if (type === 'Array') {
201+
return value.map(v => this.safeSerialize(v))
202+
}
203+
204+
return value
205+
}
206+
144207
/**
145208
* Sends a JSON-RPC response
146209
* @param {Number|String} id
@@ -155,6 +218,32 @@ class Client extends Base {
155218
}))
156219
}
157220
}
221+
222+
/**
223+
* @param {Neo.component.Base} component
224+
* @param {Number} maxDepth
225+
* @param {Number} currentDepth
226+
* @returns {Object}
227+
*/
228+
serializeComponent(component, maxDepth, currentDepth=1) {
229+
if (!component) return null;
230+
231+
const result = {
232+
id : component.id,
233+
className: component.className,
234+
ntype : component.ntype
235+
};
236+
237+
if (maxDepth === -1 || currentDepth < maxDepth) {
238+
const children = Neo.manager.Component.getChildren(component);
239+
240+
if (children && children.length > 0) {
241+
result.items = children.map(child => this.serializeComponent(child, maxDepth, currentDepth + 1))
242+
}
243+
}
244+
245+
return result
246+
}
158247
}
159248

160249
export default Neo.setupClass(Client);

0 commit comments

Comments
 (0)