chrometools-mcp 2.5.0 → 3.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +420 -0
- package/COMPONENT_MAPPING_SPEC.md +1217 -0
- package/README.md +406 -38
- package/bridge/bridge-client.js +472 -0
- package/bridge/bridge-service.js +399 -0
- package/bridge/install.js +241 -0
- package/browser/browser-manager.js +107 -2
- package/browser/page-manager.js +226 -69
- package/docs/CHROME_EXTENSION.md +219 -0
- package/docs/PAGE_OBJECT_MODEL_CONCEPT.md +1756 -0
- package/extension/background.js +643 -0
- package/extension/content.js +715 -0
- package/extension/icons/create-icons.js +164 -0
- package/extension/icons/icon128.png +0 -0
- package/extension/icons/icon16.png +0 -0
- package/extension/icons/icon48.png +0 -0
- package/extension/manifest.json +58 -0
- package/extension/popup/popup.css +437 -0
- package/extension/popup/popup.html +102 -0
- package/extension/popup/popup.js +415 -0
- package/extension/recorder-overlay.css +93 -0
- package/index.js +3347 -2901
- package/models/BaseInputModel.js +93 -0
- package/models/CheckboxGroupModel.js +199 -0
- package/models/CheckboxModel.js +103 -0
- package/models/ColorInputModel.js +53 -0
- package/models/DateInputModel.js +67 -0
- package/models/RadioGroupModel.js +126 -0
- package/models/RangeInputModel.js +60 -0
- package/models/SelectModel.js +97 -0
- package/models/TextInputModel.js +34 -0
- package/models/TextareaModel.js +59 -0
- package/models/TimeInputModel.js +49 -0
- package/models/index.js +122 -0
- package/package.json +3 -2
- package/pom/apom-converter.js +267 -0
- package/pom/apom-tree-converter.js +515 -0
- package/pom/element-id-generator.js +175 -0
- package/recorder/page-object-generator.js +16 -0
- package/recorder/scenario-executor.js +80 -2
- package/server/tool-definitions.js +839 -713
- package/server/tool-groups.js +1 -1
- package/server/tool-schemas.js +367 -326
- package/server/websocket-bridge.js +447 -0
- package/utils/selector-resolver.js +186 -0
- package/utils/ui-framework-detector.js +392 -0
- package/RELEASE_NOTES_v2.5.0.md +0 -109
- package/npm_publish_output.txt +0 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bridge-client.js
|
|
3
|
+
*
|
|
4
|
+
* WebSocket client for MCP to connect to Bridge Service.
|
|
5
|
+
* Replaces the old websocket-bridge.js (which was a server).
|
|
6
|
+
*
|
|
7
|
+
* MCP is now a CLIENT that:
|
|
8
|
+
* - Connects to Bridge on port 9223
|
|
9
|
+
* - Receives full state on connect
|
|
10
|
+
* - Gets real-time updates (tabs, recordings)
|
|
11
|
+
* - Sends commands (start/stop recording, etc.)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import WebSocket from 'ws';
|
|
15
|
+
|
|
16
|
+
const BRIDGE_PORT = 9223;
|
|
17
|
+
const RECONNECT_DELAY = 2000;
|
|
18
|
+
const MAX_RECONNECT_ATTEMPTS = 5;
|
|
19
|
+
|
|
20
|
+
// State received from Bridge
|
|
21
|
+
let bridgeState = {
|
|
22
|
+
tabs: new Map(),
|
|
23
|
+
recordings: [],
|
|
24
|
+
recorderState: {
|
|
25
|
+
isRecording: false,
|
|
26
|
+
isPaused: false,
|
|
27
|
+
metadata: null,
|
|
28
|
+
secrets: {}
|
|
29
|
+
},
|
|
30
|
+
extensionConnected: false
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Connection state
|
|
34
|
+
let ws = null;
|
|
35
|
+
let isConnected = false;
|
|
36
|
+
let reconnectAttempts = 0;
|
|
37
|
+
let reconnectTimer = null;
|
|
38
|
+
|
|
39
|
+
// Callbacks
|
|
40
|
+
const messageCallbacks = new Map(); // requestId -> {resolve, reject, timeout}
|
|
41
|
+
const eventCallbacks = []; // Array of (eventType, payload) => void
|
|
42
|
+
|
|
43
|
+
// Handler for syncing active tab (set by page-manager)
|
|
44
|
+
let activeTabSyncHandler = null;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Debug log helper
|
|
48
|
+
*/
|
|
49
|
+
function debugLog(...args) {
|
|
50
|
+
if (process.env.DEBUG === '1') {
|
|
51
|
+
console.error('[bridge-client]', ...args);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Set handler for syncing active tab to Puppeteer's lastPage
|
|
57
|
+
*/
|
|
58
|
+
export function setActiveTabSyncHandler(handler) {
|
|
59
|
+
activeTabSyncHandler = handler;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Connect to Bridge Service
|
|
64
|
+
*/
|
|
65
|
+
export async function connectToBridge() {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
if (isConnected && ws?.readyState === WebSocket.OPEN) {
|
|
68
|
+
debugLog('Already connected to Bridge');
|
|
69
|
+
resolve(true);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
debugLog(`Connecting to Bridge on port ${BRIDGE_PORT}...`);
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
ws = new WebSocket(`ws://127.0.0.1:${BRIDGE_PORT}`);
|
|
77
|
+
|
|
78
|
+
const connectTimeout = setTimeout(() => {
|
|
79
|
+
debugLog('Connection timeout');
|
|
80
|
+
ws?.close();
|
|
81
|
+
resolve(false);
|
|
82
|
+
}, 5000);
|
|
83
|
+
|
|
84
|
+
ws.on('open', () => {
|
|
85
|
+
clearTimeout(connectTimeout);
|
|
86
|
+
isConnected = true;
|
|
87
|
+
reconnectAttempts = 0;
|
|
88
|
+
debugLog('Connected to Bridge');
|
|
89
|
+
console.error('[chrometools-mcp] Connected to Bridge Service');
|
|
90
|
+
resolve(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
ws.on('message', (data) => {
|
|
94
|
+
try {
|
|
95
|
+
const message = JSON.parse(data.toString());
|
|
96
|
+
handleBridgeMessage(message);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
debugLog('Failed to parse message:', error.message);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
ws.on('close', () => {
|
|
103
|
+
debugLog('Disconnected from Bridge');
|
|
104
|
+
isConnected = false;
|
|
105
|
+
ws = null;
|
|
106
|
+
scheduleReconnect();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
ws.on('error', (error) => {
|
|
110
|
+
clearTimeout(connectTimeout);
|
|
111
|
+
debugLog('Connection error:', error.message);
|
|
112
|
+
resolve(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
} catch (error) {
|
|
116
|
+
debugLog('Failed to create WebSocket:', error.message);
|
|
117
|
+
resolve(false);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Schedule reconnection attempt
|
|
124
|
+
*/
|
|
125
|
+
function scheduleReconnect() {
|
|
126
|
+
if (reconnectTimer) return;
|
|
127
|
+
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
128
|
+
debugLog(`Max reconnect attempts (${MAX_RECONNECT_ATTEMPTS}) reached`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
reconnectAttempts++;
|
|
133
|
+
debugLog(`Scheduling reconnect attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS}`);
|
|
134
|
+
|
|
135
|
+
reconnectTimer = setTimeout(async () => {
|
|
136
|
+
reconnectTimer = null;
|
|
137
|
+
await connectToBridge();
|
|
138
|
+
}, RECONNECT_DELAY);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Disconnect from Bridge
|
|
143
|
+
*/
|
|
144
|
+
export function disconnectFromBridge() {
|
|
145
|
+
if (reconnectTimer) {
|
|
146
|
+
clearTimeout(reconnectTimer);
|
|
147
|
+
reconnectTimer = null;
|
|
148
|
+
}
|
|
149
|
+
if (ws) {
|
|
150
|
+
ws.close();
|
|
151
|
+
ws = null;
|
|
152
|
+
}
|
|
153
|
+
isConnected = false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Handle messages from Bridge
|
|
158
|
+
*/
|
|
159
|
+
function handleBridgeMessage(message) {
|
|
160
|
+
debugLog('Received:', message.type);
|
|
161
|
+
|
|
162
|
+
// Check if this is a response to a pending request
|
|
163
|
+
if (message.requestId && messageCallbacks.has(message.requestId)) {
|
|
164
|
+
const { resolve, timeout } = messageCallbacks.get(message.requestId);
|
|
165
|
+
clearTimeout(timeout);
|
|
166
|
+
messageCallbacks.delete(message.requestId);
|
|
167
|
+
resolve(message);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Handle different message types
|
|
172
|
+
switch (message.type) {
|
|
173
|
+
case 'initial_state':
|
|
174
|
+
// Full state received on connect
|
|
175
|
+
updateFullState(message.payload);
|
|
176
|
+
break;
|
|
177
|
+
|
|
178
|
+
case 'tabs_sync':
|
|
179
|
+
bridgeState.tabs.clear();
|
|
180
|
+
message.payload.tabs?.forEach(tab => {
|
|
181
|
+
bridgeState.tabs.set(tab.tabId, tab);
|
|
182
|
+
});
|
|
183
|
+
notifyEventCallbacks('tabs_sync', message.payload);
|
|
184
|
+
syncActiveTab(message.payload.tabs);
|
|
185
|
+
break;
|
|
186
|
+
|
|
187
|
+
case 'tab_created':
|
|
188
|
+
bridgeState.tabs.set(message.payload.tabId, message.payload);
|
|
189
|
+
notifyEventCallbacks('tab_created', message.payload);
|
|
190
|
+
break;
|
|
191
|
+
|
|
192
|
+
case 'tab_closed':
|
|
193
|
+
bridgeState.tabs.delete(message.payload.tabId);
|
|
194
|
+
notifyEventCallbacks('tab_closed', message.payload);
|
|
195
|
+
break;
|
|
196
|
+
|
|
197
|
+
case 'tab_activated':
|
|
198
|
+
for (const [id, tab] of bridgeState.tabs) {
|
|
199
|
+
tab.active = (id === message.payload.tabId);
|
|
200
|
+
}
|
|
201
|
+
notifyEventCallbacks('tab_activated', message.payload);
|
|
202
|
+
syncActiveTabById(message.payload.tabId);
|
|
203
|
+
break;
|
|
204
|
+
|
|
205
|
+
case 'tab_updated':
|
|
206
|
+
if (message.payload.tab) {
|
|
207
|
+
bridgeState.tabs.set(message.payload.tabId, message.payload.tab);
|
|
208
|
+
}
|
|
209
|
+
notifyEventCallbacks('tab_updated', message.payload);
|
|
210
|
+
break;
|
|
211
|
+
|
|
212
|
+
case 'recorder_state_changed':
|
|
213
|
+
bridgeState.recorderState = { ...bridgeState.recorderState, ...message.payload };
|
|
214
|
+
notifyEventCallbacks('recorder_state_changed', message.payload);
|
|
215
|
+
break;
|
|
216
|
+
|
|
217
|
+
case 'action_recorded':
|
|
218
|
+
bridgeState.recordings.push(message.payload);
|
|
219
|
+
notifyEventCallbacks('action_recorded', message.payload);
|
|
220
|
+
break;
|
|
221
|
+
|
|
222
|
+
case 'recordings_cleared':
|
|
223
|
+
bridgeState.recordings = [];
|
|
224
|
+
notifyEventCallbacks('recordings_cleared', null);
|
|
225
|
+
break;
|
|
226
|
+
|
|
227
|
+
case 'extension_disconnected':
|
|
228
|
+
bridgeState.extensionConnected = false;
|
|
229
|
+
notifyEventCallbacks('extension_disconnected', null);
|
|
230
|
+
break;
|
|
231
|
+
|
|
232
|
+
case 'pong':
|
|
233
|
+
// Handled by requestId callback
|
|
234
|
+
break;
|
|
235
|
+
|
|
236
|
+
default:
|
|
237
|
+
debugLog('Unknown message type:', message.type);
|
|
238
|
+
notifyEventCallbacks(message.type, message.payload);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Update full state from initial_state message
|
|
244
|
+
*/
|
|
245
|
+
function updateFullState(state) {
|
|
246
|
+
bridgeState.tabs.clear();
|
|
247
|
+
state.tabs?.forEach(tab => {
|
|
248
|
+
bridgeState.tabs.set(tab.tabId, tab);
|
|
249
|
+
});
|
|
250
|
+
bridgeState.recordings = state.recordings || [];
|
|
251
|
+
bridgeState.recorderState = state.recorderState || bridgeState.recorderState;
|
|
252
|
+
bridgeState.extensionConnected = state.extensionConnected;
|
|
253
|
+
|
|
254
|
+
debugLog(`State updated: ${bridgeState.tabs.size} tabs, ${bridgeState.recordings.length} recordings`);
|
|
255
|
+
notifyEventCallbacks('state_updated', state);
|
|
256
|
+
|
|
257
|
+
// Sync active tab
|
|
258
|
+
syncActiveTab(state.tabs);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Sync Puppeteer's lastPage to active tab
|
|
263
|
+
*/
|
|
264
|
+
function syncActiveTab(tabs) {
|
|
265
|
+
if (!activeTabSyncHandler || !tabs) return;
|
|
266
|
+
|
|
267
|
+
const activeTab = tabs.find(t => t.active);
|
|
268
|
+
if (activeTab?.url) {
|
|
269
|
+
activeTabSyncHandler(activeTab.url).catch(err => {
|
|
270
|
+
debugLog(`Failed to sync lastPage: ${err.message}`);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function syncActiveTabById(tabId) {
|
|
276
|
+
if (!activeTabSyncHandler) return;
|
|
277
|
+
|
|
278
|
+
const tab = bridgeState.tabs.get(tabId);
|
|
279
|
+
if (tab?.url) {
|
|
280
|
+
activeTabSyncHandler(tab.url).catch(err => {
|
|
281
|
+
debugLog(`Failed to sync lastPage: ${err.message}`);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Notify registered event callbacks
|
|
288
|
+
*/
|
|
289
|
+
function notifyEventCallbacks(eventType, payload) {
|
|
290
|
+
eventCallbacks.forEach(callback => {
|
|
291
|
+
try {
|
|
292
|
+
callback(eventType, payload);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
debugLog('Event callback error:', error.message);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Send message to Bridge
|
|
301
|
+
*/
|
|
302
|
+
export function sendToBridge(message) {
|
|
303
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
304
|
+
debugLog('Cannot send: not connected');
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
ws.send(JSON.stringify(message));
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Send command and wait for response
|
|
313
|
+
*/
|
|
314
|
+
export function sendCommand(type, payload = {}, timeout = 5000) {
|
|
315
|
+
return new Promise((resolve, reject) => {
|
|
316
|
+
if (!isConnected) {
|
|
317
|
+
reject(new Error('Not connected to Bridge'));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const requestId = `req_${Date.now()}_${Math.random()}`;
|
|
322
|
+
|
|
323
|
+
const timeoutHandle = setTimeout(() => {
|
|
324
|
+
messageCallbacks.delete(requestId);
|
|
325
|
+
reject(new Error(`Command timeout: ${type}`));
|
|
326
|
+
}, timeout);
|
|
327
|
+
|
|
328
|
+
messageCallbacks.set(requestId, { resolve, reject, timeout: timeoutHandle });
|
|
329
|
+
|
|
330
|
+
sendToBridge({ type, payload, requestId });
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ============================================
|
|
335
|
+
// Public API (compatible with old websocket-bridge.js)
|
|
336
|
+
// ============================================
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Start connection to Bridge (replaces startWebSocketServer)
|
|
340
|
+
*/
|
|
341
|
+
export async function startWebSocketServer() {
|
|
342
|
+
await connectToBridge();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Stop connection (replaces stopWebSocketServer)
|
|
347
|
+
*/
|
|
348
|
+
export function stopWebSocketServer() {
|
|
349
|
+
disconnectFromBridge();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Check if connected to Bridge (replaces isExtensionConnected)
|
|
354
|
+
*/
|
|
355
|
+
export function isExtensionConnected() {
|
|
356
|
+
return isConnected && bridgeState.extensionConnected;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Check if connected to Bridge
|
|
361
|
+
*/
|
|
362
|
+
export function isBridgeConnected() {
|
|
363
|
+
return isConnected;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Get tabs from Bridge state
|
|
368
|
+
*/
|
|
369
|
+
export function getTabsFromExtension() {
|
|
370
|
+
return Array.from(bridgeState.tabs.values());
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Get active tab
|
|
375
|
+
*/
|
|
376
|
+
export function getActiveTabFromExtension() {
|
|
377
|
+
for (const tab of bridgeState.tabs.values()) {
|
|
378
|
+
if (tab.active) return tab;
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Get debug info
|
|
385
|
+
*/
|
|
386
|
+
export function getWsDebugInfo() {
|
|
387
|
+
return {
|
|
388
|
+
bridgeConnected: isConnected,
|
|
389
|
+
extensionConnected: bridgeState.extensionConnected,
|
|
390
|
+
readyState: ws?.readyState,
|
|
391
|
+
tabCount: bridgeState.tabs.size,
|
|
392
|
+
recordingsCount: bridgeState.recordings.length,
|
|
393
|
+
recorderState: bridgeState.recorderState
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Export extensionTabs for compatibility
|
|
399
|
+
*/
|
|
400
|
+
export const extensionTabs = bridgeState.tabs;
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Request tabs sync
|
|
404
|
+
*/
|
|
405
|
+
export function requestTabsSync() {
|
|
406
|
+
return sendToBridge({ type: 'get_tabs' });
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Switch tab via Bridge
|
|
411
|
+
*/
|
|
412
|
+
export function switchTabViaExtension(tabIdentifier) {
|
|
413
|
+
const tabs = Array.from(bridgeState.tabs.values());
|
|
414
|
+
let targetTab = null;
|
|
415
|
+
|
|
416
|
+
if (typeof tabIdentifier === 'number') {
|
|
417
|
+
targetTab = tabs[tabIdentifier];
|
|
418
|
+
} else if (typeof tabIdentifier === 'string') {
|
|
419
|
+
targetTab = tabs.find(t => t.url.toLowerCase().includes(tabIdentifier.toLowerCase()));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (targetTab) {
|
|
423
|
+
sendToBridge({
|
|
424
|
+
type: 'switch_tab',
|
|
425
|
+
payload: { tabId: targetTab.tabId }
|
|
426
|
+
});
|
|
427
|
+
return targetTab;
|
|
428
|
+
}
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Register callback for tab events
|
|
434
|
+
*/
|
|
435
|
+
export function onTabEvent(callback) {
|
|
436
|
+
eventCallbacks.push(callback);
|
|
437
|
+
return () => {
|
|
438
|
+
const index = eventCallbacks.indexOf(callback);
|
|
439
|
+
if (index > -1) eventCallbacks.splice(index, 1);
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Send recorder command
|
|
445
|
+
*/
|
|
446
|
+
export function sendRecorderCommand(command, options = {}) {
|
|
447
|
+
return sendToBridge({
|
|
448
|
+
type: `recorder_${command}`,
|
|
449
|
+
payload: options
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Send command and wait for response
|
|
455
|
+
*/
|
|
456
|
+
export function sendExtensionCommand(message, timeout = 5000) {
|
|
457
|
+
return sendCommand(message.type, message.payload, timeout);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Get recorder state
|
|
462
|
+
*/
|
|
463
|
+
export function getRecorderState() {
|
|
464
|
+
return bridgeState.recorderState;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Get recordings
|
|
469
|
+
*/
|
|
470
|
+
export function getRecordings() {
|
|
471
|
+
return bridgeState.recordings;
|
|
472
|
+
}
|