@mindexec/cli 0.2.152 → 0.2.153

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindexec/cli",
3
- "version": "0.2.152",
3
+ "version": "0.2.153",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -21,14 +21,15 @@
21
21
  "scripts": {
22
22
  "start": "node launch-bridge.cjs",
23
23
  "dev": "node launch-bridge.cjs --watch",
24
- "test:syntax": "node --check server.js && node --check remote-hub.js && node --check codex-runtime.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/auth-session-smoke.mjs && node --check scripts/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs && node --check scripts/remote-fleet-render-smoke.mjs && node --check scripts/remote-http-smoke.mjs && node --check scripts/remote-frame-ws-smoke.mjs && node --check scripts/remote-agent-ws-smoke.mjs && node --check scripts/remote-agent-managed-smoke.mjs && node --check scripts/remote-registry-follower-smoke.mjs && node --check scripts/remote-agent-package-smoke.mjs && node --check scripts/remote-fast-live-rate-smoke.mjs && node --check scripts/remote-fast-mdm-browser-smoke.mjs",
24
+ "test:syntax": "node --check server.js && node --check remote-hub.js && node --check codex-runtime.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/auth-session-smoke.mjs && node --check scripts/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs && node --check scripts/remote-fleet-render-smoke.mjs && node --check scripts/remote-http-smoke.mjs && node --check scripts/remote-frame-ws-smoke.mjs && node --check scripts/remote-input-ws-smoke.mjs && node --check scripts/remote-agent-ws-smoke.mjs && node --check scripts/remote-agent-managed-smoke.mjs && node --check scripts/remote-registry-follower-smoke.mjs && node --check scripts/remote-agent-package-smoke.mjs && node --check scripts/remote-fast-live-rate-smoke.mjs && node --check scripts/remote-fast-mdm-browser-smoke.mjs",
25
25
  "test:auth": "node scripts/auth-session-smoke.mjs",
26
26
  "test:remote": "node scripts/remote-hub-smoke.mjs",
27
27
  "test:remote:scale": "node scripts/remote-hub-scale-smoke.mjs",
28
28
  "test:remote:render": "node scripts/remote-fleet-render-smoke.mjs",
29
- "test:remote:http": "node scripts/remote-http-smoke.mjs",
30
- "test:remote:frame-ws": "node scripts/remote-frame-ws-smoke.mjs",
31
- "test:remote:agent-ws": "node scripts/remote-agent-ws-smoke.mjs",
29
+ "test:remote:http": "node scripts/remote-http-smoke.mjs",
30
+ "test:remote:frame-ws": "node scripts/remote-frame-ws-smoke.mjs",
31
+ "test:remote:input-ws": "node scripts/remote-input-ws-smoke.mjs",
32
+ "test:remote:agent-ws": "node scripts/remote-agent-ws-smoke.mjs",
32
33
  "test:remote:managed": "node scripts/remote-agent-managed-smoke.mjs",
33
34
  "test:remote:registry": "node scripts/remote-registry-follower-smoke.mjs",
34
35
  "test:remote:package": "node scripts/remote-agent-package-smoke.mjs",
@@ -528,7 +528,8 @@ async function loadCss3DManager() {
528
528
  status: 200,
529
529
  json: async () => ({
530
530
  bridgeToken: 'render-smoke-bridge-token',
531
- remoteFrameWsPath: '/api/remote/frames/ws'
531
+ remoteFrameWsPath: '/api/remote/frames/ws',
532
+ remoteInputWsPath: '/api/remote/input/ws'
532
533
  })
533
534
  };
534
535
  },
@@ -1180,21 +1181,44 @@ try {
1180
1181
  code: 'KeyA',
1181
1182
  repeat: false
1182
1183
  });
1183
- await wait(20);
1184
- const inputCalls = dotNetCalls.filter(call => call.methodName === 'SendRemoteFleetInputFromJs');
1185
- assert.ok(inputCalls.length >= 3, 'control popup should send pointer and keyboard input through JS interop');
1186
- const pointerDown = inputCalls.find(call => call.args[2]?.type === 'pointerDown');
1187
- const keyDown = inputCalls.find(call => call.args[2]?.type === 'keyDown');
1184
+ controlShell.dispatchEvent({
1185
+ type: 'keyup',
1186
+ key: 'A',
1187
+ code: 'KeyA',
1188
+ repeat: false
1189
+ });
1190
+ await wait(50);
1191
+ const inputWsSends = diagnostics.webSocketSends
1192
+ .filter(send => send.url.includes('/api/remote/input/ws'))
1193
+ .map(send => {
1194
+ try {
1195
+ return JSON.parse(send.payload);
1196
+ } catch {
1197
+ return null;
1198
+ }
1199
+ })
1200
+ .filter(Boolean);
1201
+ assert.ok(inputWsSends.length >= 3, 'control popup should send pointer and keyboard input over the input WebSocket');
1202
+ const pointerDown = inputWsSends.find(message => message.input?.type === 'pointerDown');
1203
+ const keyDown = inputWsSends.find(message => message.input?.type === 'keyDown');
1204
+ const keyUp = inputWsSends.find(message => message.input?.type === 'keyUp');
1188
1205
  assert.ok(pointerDown);
1189
1206
  assert.ok(keyDown);
1190
- assert.equal(pointerDown.args[0], 'remote-fleet-render-smoke');
1191
- assert.equal(pointerDown.args[1], patchTarget.DeviceId);
1192
- assert.equal(pointerDown.args[2].button, 'left');
1193
- assert.ok(pointerDown.args[2].controlLeaseId);
1194
- assert.ok(pointerDown.args[2].normalizedX >= 0 && pointerDown.args[2].normalizedX <= 1);
1195
- assert.ok(pointerDown.args[2].normalizedY >= 0 && pointerDown.args[2].normalizedY <= 1);
1196
- assert.equal(keyDown.args[2].key, 'A');
1197
- assert.equal(keyDown.args[2].code, 'KeyA');
1207
+ assert.ok(keyUp);
1208
+ assert.equal(pointerDown.nodeId, 'remote-fleet-render-smoke');
1209
+ assert.equal(pointerDown.deviceId, patchTarget.DeviceId);
1210
+ assert.equal(pointerDown.input.button, 'left');
1211
+ assert.ok(pointerDown.input.controlLeaseId);
1212
+ assert.ok(pointerDown.input.normalizedX >= 0 && pointerDown.input.normalizedX <= 1);
1213
+ assert.ok(pointerDown.input.normalizedY >= 0 && pointerDown.input.normalizedY <= 1);
1214
+ assert.equal(keyDown.input.key, 'A');
1215
+ assert.equal(keyDown.input.code, 'KeyA');
1216
+ assert.equal(keyUp.input.key, 'A');
1217
+ assert.equal(keyUp.input.code, 'KeyA');
1218
+ assert.equal(
1219
+ dotNetCalls.filter(call => call.methodName === 'SendRemoteFleetInputFromJs').length,
1220
+ 0,
1221
+ 'WebSocket input path should not use the slower DotNet input fallback in the happy path');
1198
1222
  manager.closeRemoteFleetControlPopupForTest('render-smoke');
1199
1223
  await wait(20);
1200
1224
  assert.equal(document.body.querySelector('[data-remote-fleet-control-popup="true"]'), null);
@@ -0,0 +1,263 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from 'node:assert/strict';
4
+ import net from 'node:net';
5
+ import { spawn } from 'node:child_process';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { WebSocket } from 'ws';
9
+
10
+ const BRIDGE_TOKEN = 'remote-input-ws-smoke-token';
11
+ const PAIR_TOKEN = 'remote-input-ws-pair-token';
12
+ const DEVICE_ID = 'remote-input-ws-device';
13
+
14
+ function wait(ms) {
15
+ return new Promise(resolve => setTimeout(resolve, ms));
16
+ }
17
+
18
+ async function waitFor(predicate, timeoutMs = 10000, label = 'condition') {
19
+ const startedAt = Date.now();
20
+ while (Date.now() - startedAt < timeoutMs) {
21
+ const value = await predicate();
22
+ if (value) {
23
+ return value;
24
+ }
25
+ await wait(50);
26
+ }
27
+
28
+ throw new Error(`Timed out waiting for ${label}.`);
29
+ }
30
+
31
+ async function findFreePort() {
32
+ return await new Promise((resolve, reject) => {
33
+ const server = net.createServer();
34
+ server.unref();
35
+ server.once('error', reject);
36
+ server.listen(0, '127.0.0.1', () => {
37
+ const address = server.address();
38
+ const port = typeof address === 'object' && address ? address.port : 0;
39
+ server.close(() => resolve(port));
40
+ });
41
+ });
42
+ }
43
+
44
+ async function fetchJson(url, options = {}) {
45
+ const response = await fetch(url, {
46
+ ...options,
47
+ headers: {
48
+ ...(options.body ? { 'Content-Type': 'application/json' } : {}),
49
+ 'X-Bridge-Token': BRIDGE_TOKEN,
50
+ ...(options.headers || {})
51
+ }
52
+ });
53
+ const payload = await response.json().catch(() => null);
54
+ return { ok: response.ok, status: response.status, payload };
55
+ }
56
+
57
+ async function waitForBridge(baseUrl, details) {
58
+ const startedAt = Date.now();
59
+ while (Date.now() - startedAt < 30000) {
60
+ try {
61
+ const result = await fetchJson(`${baseUrl}/api/remote/status`);
62
+ if (result.ok && result.payload?.started === true) {
63
+ return result.payload;
64
+ }
65
+ } catch {
66
+ // Server is still starting.
67
+ }
68
+ await wait(100);
69
+ }
70
+
71
+ throw new Error(`Timed out waiting for LocalBridge.\n${details()}`);
72
+ }
73
+
74
+ function spawnBridge({ bridgePort, remoteHubPort }) {
75
+ const bridgeRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
76
+ const env = {
77
+ ...process.env,
78
+ BRIDGE_PORT: String(bridgePort),
79
+ BRIDGE_TOKEN,
80
+ BRIDGE_REQUIRE_TOKEN: '1',
81
+ MINDEXEC_REMOTE_HUB: '1',
82
+ REMOTE_HUB_HOST: '127.0.0.1',
83
+ REMOTE_HUB_PORT: String(remoteHubPort),
84
+ REMOTE_HUB_PAIR_TOKEN: PAIR_TOKEN,
85
+ NO_COLOR: '1'
86
+ };
87
+
88
+ const child = spawn(process.execPath, ['server.js'], {
89
+ cwd: bridgeRoot,
90
+ stdio: ['ignore', 'pipe', 'pipe'],
91
+ windowsHide: true,
92
+ env
93
+ });
94
+
95
+ let stdout = '';
96
+ let stderr = '';
97
+ child.stdout.on('data', chunk => {
98
+ stdout += chunk.toString();
99
+ });
100
+ child.stderr.on('data', chunk => {
101
+ stderr += chunk.toString();
102
+ });
103
+
104
+ const exitPromise = new Promise(resolve => child.once('exit', resolve));
105
+ const stop = async () => {
106
+ if (child.exitCode === null && !child.killed) {
107
+ child.kill('SIGTERM');
108
+ await Promise.race([exitPromise, wait(5000)]);
109
+ if (child.exitCode === null && !child.killed) {
110
+ child.kill('SIGKILL');
111
+ }
112
+ }
113
+ };
114
+
115
+ return {
116
+ baseUrl: `http://127.0.0.1:${bridgePort}`,
117
+ details: () => `stdout=${stdout}\nstderr=${stderr}`,
118
+ stop
119
+ };
120
+ }
121
+
122
+ async function openWebSocket(url) {
123
+ const ws = new WebSocket(url);
124
+ await new Promise((resolve, reject) => {
125
+ ws.once('open', resolve);
126
+ ws.once('error', reject);
127
+ });
128
+ return ws;
129
+ }
130
+
131
+ async function main() {
132
+ const bridgePort = await findFreePort();
133
+ const remoteHubPort = await findFreePort();
134
+ const bridge = spawnBridge({ bridgePort, remoteHubPort });
135
+ let agentWs = null;
136
+ let inputWs = null;
137
+
138
+ try {
139
+ await waitForBridge(bridge.baseUrl, bridge.details);
140
+
141
+ const receivedCommands = [];
142
+ agentWs = await openWebSocket(`ws://127.0.0.1:${remoteHubPort}/remote-agent`);
143
+ agentWs.on('message', data => {
144
+ const text = Buffer.isBuffer(data) ? data.toString('utf8') : String(data || '');
145
+ if (!text.trim().startsWith('{')) {
146
+ return;
147
+ }
148
+
149
+ const message = JSON.parse(text);
150
+ if (message.command === 'input.control') {
151
+ receivedCommands.push(message);
152
+ }
153
+ });
154
+ agentWs.send(JSON.stringify({
155
+ type: 'hello',
156
+ pairToken: PAIR_TOKEN,
157
+ deviceId: DEVICE_ID,
158
+ deviceName: 'Remote Input WS Smoke',
159
+ hostname: 'remote-input-ws-smoke',
160
+ platform: process.platform,
161
+ arch: process.arch,
162
+ pid: process.pid,
163
+ agentVersion: '0.0.0-input-ws-smoke',
164
+ runtime: 'node-smoke',
165
+ capabilities: {
166
+ status: true,
167
+ thumbnail: true,
168
+ liveStream: true,
169
+ binaryFrames: true,
170
+ control: true,
171
+ computerAgent: true,
172
+ taskDispatch: true,
173
+ aiAssist: false
174
+ }
175
+ }));
176
+ agentWs.send(JSON.stringify({
177
+ type: 'status',
178
+ status: {
179
+ uptimeSec: 1,
180
+ totalMem: 2,
181
+ freeMem: 1,
182
+ timestamp: new Date().toISOString()
183
+ }
184
+ }));
185
+
186
+ await waitFor(async () => {
187
+ const result = await fetchJson(`${bridge.baseUrl}/api/remote/devices`);
188
+ return result.payload?.devices?.find(device => device.deviceId === DEVICE_ID && device.connected === true);
189
+ }, 10000, 'device registration');
190
+
191
+ inputWs = await openWebSocket(`${bridge.baseUrl.replace(/^http:/, 'ws:')}/api/remote/input/ws?token=${encodeURIComponent(BRIDGE_TOKEN)}`);
192
+ inputWs.send(JSON.stringify({
193
+ type: 'input',
194
+ deviceId: DEVICE_ID,
195
+ requestId: 'pointer-move',
196
+ input: {
197
+ type: 'pointerMove',
198
+ normalizedX: 0.42,
199
+ normalizedY: 0.54,
200
+ controlLeaseId: 'input-ws-smoke-lease'
201
+ }
202
+ }));
203
+ inputWs.send(JSON.stringify({
204
+ type: 'input',
205
+ deviceId: DEVICE_ID,
206
+ requestId: 'key-down',
207
+ input: {
208
+ type: 'keyDown',
209
+ key: 'A',
210
+ code: 'KeyA',
211
+ repeat: false,
212
+ controlLeaseId: 'input-ws-smoke-lease'
213
+ }
214
+ }));
215
+ inputWs.send(JSON.stringify({
216
+ type: 'input',
217
+ deviceId: DEVICE_ID,
218
+ requestId: 'key-up',
219
+ input: {
220
+ type: 'keyUp',
221
+ key: 'A',
222
+ code: 'KeyA',
223
+ repeat: false,
224
+ controlLeaseId: 'input-ws-smoke-lease'
225
+ }
226
+ }));
227
+
228
+ await waitFor(() => receivedCommands.length >= 3, 10000, 'input.control commands');
229
+ const commandTypes = receivedCommands.map(command => command.payload?.type);
230
+ assert.deepEqual(commandTypes.slice(0, 3), ['pointerMove', 'keyDown', 'keyUp']);
231
+ assert.equal(receivedCommands[0].payload.normalizedX, 0.42);
232
+ assert.equal(receivedCommands[0].payload.normalizedY, 0.54);
233
+ assert.equal(receivedCommands[1].payload.key, 'A');
234
+ assert.equal(receivedCommands[1].payload.code, 'KeyA');
235
+ assert.equal(receivedCommands[2].payload.key, 'A');
236
+ assert.equal(receivedCommands[2].payload.code, 'KeyA');
237
+
238
+ const status = await fetchJson(`${bridge.baseUrl}/api/status?remoteInput=ws`);
239
+ assert.equal(status.ok, true);
240
+ assert.equal(status.payload?.remoteInputWsPath, '/api/remote/input/ws');
241
+ assert.ok(status.payload?.remoteInputWs?.clientCount >= 1, JSON.stringify(status.payload?.remoteInputWs));
242
+ assert.ok(status.payload?.remoteInputWs?.inputsQueued >= 3, JSON.stringify(status.payload?.remoteInputWs));
243
+ assert.ok(status.payload?.remoteInputWs?.pointerMovesQueued >= 1, JSON.stringify(status.payload?.remoteInputWs));
244
+ assert.ok(status.payload?.remoteInputWs?.keyEventsQueued >= 2, JSON.stringify(status.payload?.remoteInputWs));
245
+ assert.equal(status.payload?.remoteInputWs?.lastDeviceId, DEVICE_ID);
246
+ assert.equal(status.payload?.remoteInputWs?.lastInputType, 'keyUp');
247
+
248
+ console.log('Remote input WebSocket smoke OK');
249
+ } finally {
250
+ if (inputWs && inputWs.readyState === WebSocket.OPEN) {
251
+ inputWs.close();
252
+ }
253
+ if (agentWs && agentWs.readyState === WebSocket.OPEN) {
254
+ agentWs.close();
255
+ }
256
+ await bridge.stop();
257
+ }
258
+ }
259
+
260
+ main().catch(error => {
261
+ console.error(error);
262
+ process.exitCode = 1;
263
+ });
package/server.js CHANGED
@@ -2125,10 +2125,13 @@ app.get('/api/local-files/:id', async (req, res) => {
2125
2125
  const httpServer = createServer(app);
2126
2126
  const wss = new WebSocketServer({ noServer: true });
2127
2127
  const remoteFrameWss = new WebSocketServer({ noServer: true });
2128
+ const remoteInputWss = new WebSocketServer({ noServer: true });
2128
2129
  const wsClients = new Set();
2129
2130
  const remoteFrameClients = new Set();
2131
+ const remoteInputClients = new Set();
2130
2132
  const shellJobs = new Map();
2131
2133
  let remoteFrameClientSeq = 0;
2134
+ let remoteInputClientSeq = 0;
2132
2135
  const remoteFrameWsDiagnostics = {
2133
2136
  opened: 0,
2134
2137
  closed: 0,
@@ -2157,6 +2160,25 @@ const remoteFrameWsDiagnostics = {
2157
2160
  lastAutoStartStarted: 0,
2158
2161
  lastAutoStartSkipped: 0
2159
2162
  };
2163
+ const remoteInputWsDiagnostics = {
2164
+ opened: 0,
2165
+ closed: 0,
2166
+ errors: 0,
2167
+ messagesReceived: 0,
2168
+ messagesInvalid: 0,
2169
+ inputsQueued: 0,
2170
+ inputsFailed: 0,
2171
+ pointerMovesQueued: 0,
2172
+ keyEventsQueued: 0,
2173
+ lastOpenAt: '',
2174
+ lastCloseAt: '',
2175
+ lastErrorAt: '',
2176
+ lastInputAt: '',
2177
+ lastDeviceId: '',
2178
+ lastInputType: '',
2179
+ lastCommandId: '',
2180
+ lastError: ''
2181
+ };
2160
2182
  const REMOTE_FRAME_WS_AUTO_START_LIMIT = 120;
2161
2183
  const REMOTE_FRAME_WS_DEFAULT_FPS = 12;
2162
2184
  const REMOTE_FRAME_WS_DEFAULT_MAX_WIDTH = 960;
@@ -2183,6 +2205,14 @@ httpServer.on('upgrade', (req, socket, head) => {
2183
2205
  return;
2184
2206
  }
2185
2207
 
2208
+ if (parsed.pathname === '/api/remote/input/ws'
2209
+ && (!bridgeAuthRequired || token === bridgeToken)) {
2210
+ remoteInputWss.handleUpgrade(req, socket, head, (ws) => {
2211
+ remoteInputWss.emit('connection', ws, req);
2212
+ });
2213
+ return;
2214
+ }
2215
+
2186
2216
  {
2187
2217
  socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
2188
2218
  socket.destroy();
@@ -2552,6 +2582,195 @@ remoteFrameWss.on('connection', (ws, req) => {
2552
2582
  });
2553
2583
  });
2554
2584
 
2585
+ function sendRemoteInputClientJson(ws, payload) {
2586
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
2587
+ return false;
2588
+ }
2589
+
2590
+ try {
2591
+ ws.send(JSON.stringify(payload));
2592
+ return true;
2593
+ } catch {
2594
+ return false;
2595
+ }
2596
+ }
2597
+
2598
+ function normalizeRemoteInputWsPayload(payload = {}) {
2599
+ const outer = payload && typeof payload === 'object' ? payload : {};
2600
+ const inputSource = outer.input && typeof outer.input === 'object'
2601
+ ? outer.input
2602
+ : outer.Input && typeof outer.Input === 'object'
2603
+ ? outer.Input
2604
+ : outer;
2605
+ const input = { ...inputSource };
2606
+ input.type = String(input.type ?? input.Type ?? '').trim();
2607
+ const deviceId = String(
2608
+ outer.deviceId
2609
+ ?? outer.DeviceId
2610
+ ?? input.deviceId
2611
+ ?? input.DeviceId
2612
+ ?? ''
2613
+ ).trim();
2614
+ delete input.deviceId;
2615
+ delete input.DeviceId;
2616
+
2617
+ return {
2618
+ deviceId,
2619
+ input,
2620
+ requestId: String(outer.requestId ?? outer.RequestId ?? '').trim(),
2621
+ ack: outer.ack === true || outer.Ack === true
2622
+ };
2623
+ }
2624
+
2625
+ function serializeRemoteInputWsDiagnostics() {
2626
+ const clients = [];
2627
+ for (const client of remoteInputClients) {
2628
+ clients.push({
2629
+ id: client.remoteInputClientId || '',
2630
+ readyState: client.readyState,
2631
+ connectedAt: client.remoteInputConnectedAt || '',
2632
+ received: Number(client.remoteInputReceivedCount || 0),
2633
+ queued: Number(client.remoteInputQueuedCount || 0),
2634
+ failed: Number(client.remoteInputFailedCount || 0),
2635
+ bufferedAmount: Number(client.bufferedAmount || 0),
2636
+ lastInputAt: client.remoteInputLastInputAt || '',
2637
+ lastDeviceId: client.remoteInputLastDeviceId || '',
2638
+ lastInputType: client.remoteInputLastInputType || ''
2639
+ });
2640
+ }
2641
+
2642
+ return {
2643
+ protocol: 'mindexec.remote.input.json.v1',
2644
+ path: '/api/remote/input/ws',
2645
+ clientCount: remoteInputClients.size,
2646
+ ...remoteInputWsDiagnostics,
2647
+ clients
2648
+ };
2649
+ }
2650
+
2651
+ remoteInputWss.on('connection', (ws) => {
2652
+ remoteInputClients.add(ws);
2653
+ ws.remoteInputClientId = `riws-${++remoteInputClientSeq}`;
2654
+ ws.remoteInputConnectedAt = new Date().toISOString();
2655
+ ws.remoteInputReceivedCount = 0;
2656
+ ws.remoteInputQueuedCount = 0;
2657
+ ws.remoteInputFailedCount = 0;
2658
+ remoteInputWsDiagnostics.opened += 1;
2659
+ remoteInputWsDiagnostics.lastOpenAt = ws.remoteInputConnectedAt;
2660
+
2661
+ ws.on('message', data => {
2662
+ const receivedAt = new Date().toISOString();
2663
+ remoteInputWsDiagnostics.messagesReceived += 1;
2664
+ ws.remoteInputReceivedCount = (ws.remoteInputReceivedCount || 0) + 1;
2665
+
2666
+ let payload = null;
2667
+ try {
2668
+ const text = Buffer.isBuffer(data) ? data.toString('utf8') : String(data || '');
2669
+ payload = JSON.parse(text);
2670
+ } catch {
2671
+ remoteInputWsDiagnostics.messagesInvalid += 1;
2672
+ sendRemoteInputClientJson(ws, {
2673
+ type: 'RemoteInputError',
2674
+ timestamp: receivedAt,
2675
+ error: 'invalid-json'
2676
+ });
2677
+ return;
2678
+ }
2679
+
2680
+ const messageType = String(payload?.type || payload?.Type || '').trim().toLowerCase();
2681
+ if (messageType === 'ping') {
2682
+ sendRemoteInputClientJson(ws, {
2683
+ type: 'RemoteInputPong',
2684
+ timestamp: receivedAt
2685
+ });
2686
+ return;
2687
+ }
2688
+
2689
+ if (messageType !== 'input' && messageType !== 'remote.input') {
2690
+ remoteInputWsDiagnostics.messagesInvalid += 1;
2691
+ sendRemoteInputClientJson(ws, {
2692
+ type: 'RemoteInputError',
2693
+ timestamp: receivedAt,
2694
+ error: 'unsupported-message'
2695
+ });
2696
+ return;
2697
+ }
2698
+
2699
+ const normalized = normalizeRemoteInputWsPayload(payload);
2700
+ if (!normalized.deviceId || !normalized.input.type) {
2701
+ remoteInputWsDiagnostics.messagesInvalid += 1;
2702
+ sendRemoteInputClientJson(ws, {
2703
+ type: 'RemoteInputResult',
2704
+ timestamp: receivedAt,
2705
+ ok: false,
2706
+ requestId: normalized.requestId,
2707
+ deviceId: normalized.deviceId,
2708
+ inputType: normalized.input.type,
2709
+ error: !normalized.deviceId ? 'missing-device-id' : 'missing-input-type'
2710
+ });
2711
+ return;
2712
+ }
2713
+
2714
+ const result = remoteHub.sendInputControl(normalized.deviceId, normalized.input);
2715
+ const ok = result?.ok === true;
2716
+ const inputType = String(normalized.input.type || '').trim();
2717
+ remoteInputWsDiagnostics.lastInputAt = receivedAt;
2718
+ remoteInputWsDiagnostics.lastDeviceId = normalized.deviceId;
2719
+ remoteInputWsDiagnostics.lastInputType = inputType;
2720
+ remoteInputWsDiagnostics.lastCommandId = String(result?.commandId || '');
2721
+ ws.remoteInputLastInputAt = receivedAt;
2722
+ ws.remoteInputLastDeviceId = normalized.deviceId;
2723
+ ws.remoteInputLastInputType = inputType;
2724
+
2725
+ if (ok) {
2726
+ remoteInputWsDiagnostics.inputsQueued += 1;
2727
+ ws.remoteInputQueuedCount = (ws.remoteInputQueuedCount || 0) + 1;
2728
+ if (/^pointermove$/i.test(inputType)) {
2729
+ remoteInputWsDiagnostics.pointerMovesQueued += 1;
2730
+ }
2731
+ if (/^key(up|down)$/i.test(inputType)) {
2732
+ remoteInputWsDiagnostics.keyEventsQueued += 1;
2733
+ }
2734
+ } else {
2735
+ remoteInputWsDiagnostics.inputsFailed += 1;
2736
+ remoteInputWsDiagnostics.lastError = String(result?.error || 'input-failed');
2737
+ ws.remoteInputFailedCount = (ws.remoteInputFailedCount || 0) + 1;
2738
+ }
2739
+
2740
+ const shouldAck = normalized.ack === true || !ok || !/^pointermove$/i.test(inputType);
2741
+ if (shouldAck) {
2742
+ sendRemoteInputClientJson(ws, {
2743
+ type: 'RemoteInputResult',
2744
+ timestamp: receivedAt,
2745
+ ok,
2746
+ requestId: normalized.requestId,
2747
+ deviceId: normalized.deviceId,
2748
+ inputType,
2749
+ commandId: result?.commandId || '',
2750
+ error: result?.error || ''
2751
+ });
2752
+ }
2753
+ });
2754
+
2755
+ ws.on('close', () => {
2756
+ remoteInputClients.delete(ws);
2757
+ remoteInputWsDiagnostics.closed += 1;
2758
+ remoteInputWsDiagnostics.lastCloseAt = new Date().toISOString();
2759
+ });
2760
+ ws.on('error', error => {
2761
+ remoteInputClients.delete(ws);
2762
+ remoteInputWsDiagnostics.errors += 1;
2763
+ remoteInputWsDiagnostics.lastErrorAt = new Date().toISOString();
2764
+ remoteInputWsDiagnostics.lastError = String(error?.message || error || 'input-ws-error');
2765
+ });
2766
+ sendRemoteInputClientJson(ws, {
2767
+ type: 'RemoteInputSocketReady',
2768
+ timestamp: new Date().toISOString(),
2769
+ protocol: 'mindexec.remote.input.json.v1',
2770
+ clientId: ws.remoteInputClientId
2771
+ });
2772
+ });
2773
+
2555
2774
  // Helper: Validate path is within workspace
2556
2775
  function validatePath(requestedPath) {
2557
2776
  const fullPath = resolveWorkspaceBoundPath(requestedPath);
@@ -10279,11 +10498,13 @@ app.get('/api/status', async (req, res) => {
10279
10498
  wsToken,
10280
10499
  wsPath: '/events',
10281
10500
  remoteFrameWsPath: '/api/remote/frames/ws',
10501
+ remoteInputWsPath: '/api/remote/input/ws',
10282
10502
  bridgeToken,
10283
10503
  bridgeTokenHeader,
10284
10504
  bridgeAuthRequired,
10285
10505
  remoteHub: remoteHub.getStatus({ includeSecrets: false }),
10286
10506
  remoteFrameWs: serializeRemoteFrameWsDiagnostics(),
10507
+ remoteInputWs: serializeRemoteInputWsDiagnostics(),
10287
10508
  remoteAgent: serializeRemoteAgentState(),
10288
10509
  remoteRegistryFollower: serializeRemoteRegistryFollowerState(),
10289
10510
  remoteRegistryRealtime: serializeRemoteRegistryRealtimeState(),
@@ -12127,6 +12348,34 @@ async function shutdownBridge(signal) {
12127
12348
  // Ignore if already closed
12128
12349
  }
12129
12350
 
12351
+ for (const client of remoteFrameClients) {
12352
+ try {
12353
+ client.close();
12354
+ } catch {
12355
+ // Ignore close race
12356
+ }
12357
+ }
12358
+
12359
+ for (const client of remoteInputClients) {
12360
+ try {
12361
+ client.close();
12362
+ } catch {
12363
+ // Ignore close race
12364
+ }
12365
+ }
12366
+
12367
+ try {
12368
+ remoteFrameWss.close();
12369
+ } catch {
12370
+ // Ignore if already closed
12371
+ }
12372
+
12373
+ try {
12374
+ remoteInputWss.close();
12375
+ } catch {
12376
+ // Ignore if already closed
12377
+ }
12378
+
12130
12379
  try {
12131
12380
  stopRemoteRegistryFollower();
12132
12381
  } catch {
@@ -13494,6 +13494,8 @@
13494
13494
  const REMOTE_FLEET_LIVE_FRAME_REFRESH_MS = Math.round(1000 / REMOTE_FLEET_MONITOR_LIVE_FPS);
13495
13495
  const REMOTE_FLEET_CONTROL_LIVE_FPS = 20;
13496
13496
  const REMOTE_FLEET_CONTROL_FRAME_REFRESH_MS = Math.round(1000 / REMOTE_FLEET_CONTROL_LIVE_FPS);
13497
+ const REMOTE_FLEET_CONTROL_INPUT_MOVE_MS = 16;
13498
+ const REMOTE_FLEET_CONTROL_INPUT_QUEUE_LIMIT = 64;
13497
13499
  const REMOTE_FLEET_THUMBNAIL_FRAME_REFRESH_MS = 2000;
13498
13500
  const REMOTE_FLEET_FRAME_CANVAS_MAX_DPR = 2;
13499
13501
  const REMOTE_FLEET_FRAME_BLOB_CACHE_MS = 10000;
@@ -13829,6 +13831,22 @@
13829
13831
  return url.toString();
13830
13832
  }
13831
13833
 
13834
+ function buildRemoteFleetInputWsUrl(status) {
13835
+ const baseUrl = String(status?.__remoteFleetBridgeBaseUrl || window.location?.origin || '').trim();
13836
+ const path = String(status?.remoteInputWsPath || '/api/remote/input/ws').trim() || '/api/remote/input/ws';
13837
+ if (!baseUrl) {
13838
+ return '';
13839
+ }
13840
+
13841
+ const url = new URL(path, baseUrl);
13842
+ url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
13843
+ const bridgeToken = String(status?.bridgeToken || '').trim();
13844
+ if (bridgeToken) {
13845
+ url.searchParams.set('token', bridgeToken);
13846
+ }
13847
+ return url.toString();
13848
+ }
13849
+
13832
13850
  async function parseRemoteFleetBinaryFrameMessage(data) {
13833
13851
  let buffer = null;
13834
13852
  if (data instanceof ArrayBuffer) {
@@ -15729,6 +15747,8 @@
15729
15747
  activeRemoteFleetControlPopup = null;
15730
15748
  session.active = false;
15731
15749
  detachRemoteFleetControlBinaryFrameSession(session);
15750
+ closeRemoteFleetControlInputSocket(session, 'close');
15751
+ session.inputCleanup?.();
15732
15752
  if (session.timer) {
15733
15753
  clearTimeout(session.timer);
15734
15754
  session.timer = null;
@@ -15910,34 +15930,268 @@
15910
15930
  }
15911
15931
  }
15912
15932
 
15913
- function sendRemoteFleetControlInput(session, payload, throttleMove = false) {
15914
- if (!session?.active || !payload?.type) {
15933
+ function closeRemoteFleetControlInputSocket(session, reason = 'close') {
15934
+ if (!session) {
15915
15935
  return;
15916
15936
  }
15917
15937
 
15918
- const send = next => {
15919
- if (!session.active) {
15920
- return;
15938
+ if (session.inputWsOpenTimer) {
15939
+ clearTimeout(session.inputWsOpenTimer);
15940
+ session.inputWsOpenTimer = null;
15941
+ }
15942
+
15943
+ const ws = session.inputWs;
15944
+ session.inputWs = null;
15945
+ session.inputWsConnecting = false;
15946
+ if (ws && ws.readyState !== 3) {
15947
+ try {
15948
+ ws.close();
15949
+ } catch {
15950
+ // Best-effort input socket cleanup.
15921
15951
  }
15922
- invokeDotNetAsync('SendRemoteFleetInputFromJs', session.nodeId, session.deviceId, {
15923
- ...next,
15924
- controlLeaseId: session.controlLeaseId,
15925
- issuedAt: new Date().toISOString()
15926
- }).catch(error => {
15952
+ }
15953
+
15954
+ if (reason === 'close') {
15955
+ session.inputQueue = [];
15956
+ }
15957
+ }
15958
+
15959
+ function createRemoteFleetControlInput(session, payload) {
15960
+ return {
15961
+ ...payload,
15962
+ controlLeaseId: session.controlLeaseId,
15963
+ issuedAt: new Date().toISOString()
15964
+ };
15965
+ }
15966
+
15967
+ function sendRemoteFleetControlInputFallback(session, input) {
15968
+ if (!session?.active || !input?.type) {
15969
+ return;
15970
+ }
15971
+
15972
+ invokeDotNetAsync('SendRemoteFleetInputFromJs', session.nodeId, session.deviceId, {
15973
+ ...input,
15974
+ controlLeaseId: input.controlLeaseId || session.controlLeaseId,
15975
+ issuedAt: input.issuedAt || new Date().toISOString()
15976
+ }).catch(error => {
15977
+ if (session.active) {
15927
15978
  session.status.textContent = error?.message || 'Input failed';
15979
+ }
15980
+ });
15981
+ }
15982
+
15983
+ function drainRemoteFleetControlInputQueueToFallback(session) {
15984
+ if (!session?.inputQueue?.length) {
15985
+ return;
15986
+ }
15987
+
15988
+ const queued = session.inputQueue.splice(0);
15989
+ queued.forEach(envelope => sendRemoteFleetControlInputFallback(session, envelope.input));
15990
+ }
15991
+
15992
+ function enqueueRemoteFleetControlInput(session, envelope) {
15993
+ if (!session) {
15994
+ return;
15995
+ }
15996
+
15997
+ if (!Array.isArray(session.inputQueue)) {
15998
+ session.inputQueue = [];
15999
+ }
16000
+
16001
+ const inputType = String(envelope?.input?.type || '').toLowerCase();
16002
+ if (inputType === 'pointermove') {
16003
+ for (let i = session.inputQueue.length - 1; i >= 0; i -= 1) {
16004
+ if (String(session.inputQueue[i]?.input?.type || '').toLowerCase() === 'pointermove') {
16005
+ session.inputQueue.splice(i, 1);
16006
+ }
16007
+ }
16008
+ }
16009
+
16010
+ session.inputQueue.push(envelope);
16011
+ while (session.inputQueue.length > REMOTE_FLEET_CONTROL_INPUT_QUEUE_LIMIT) {
16012
+ const moveIndex = session.inputQueue.findIndex(item =>
16013
+ String(item?.input?.type || '').toLowerCase() === 'pointermove');
16014
+ session.inputQueue.splice(moveIndex >= 0 ? moveIndex : 0, 1);
16015
+ }
16016
+ }
16017
+
16018
+ function flushRemoteFleetControlInputQueue(session) {
16019
+ const ws = session?.inputWs;
16020
+ if (!session?.active || !ws || ws.readyState !== 1 || !session.inputQueue?.length) {
16021
+ return;
16022
+ }
16023
+
16024
+ const queued = session.inputQueue.splice(0);
16025
+ for (const envelope of queued) {
16026
+ if (!sendRemoteFleetControlInputEnvelope(session, envelope)) {
16027
+ enqueueRemoteFleetControlInput(session, envelope);
16028
+ break;
16029
+ }
16030
+ }
16031
+ }
16032
+
16033
+ function sendRemoteFleetControlInputEnvelope(session, envelope) {
16034
+ const ws = session?.inputWs;
16035
+ if (!session?.active || !ws || ws.readyState !== 1) {
16036
+ return false;
16037
+ }
16038
+
16039
+ const inputType = String(envelope?.input?.type || '').toLowerCase();
16040
+ if (inputType === 'pointermove' && Number(ws.bufferedAmount || 0) > 256 * 1024) {
16041
+ window.RuntimeTrace?.emit?.('remote.control.input.drop', {
16042
+ reason: 'ws-buffered',
16043
+ deviceId: session.deviceId,
16044
+ bufferedAmount: Number(ws.bufferedAmount || 0)
15928
16045
  });
16046
+ return true;
16047
+ }
16048
+
16049
+ try {
16050
+ ws.send(JSON.stringify(envelope));
16051
+ return true;
16052
+ } catch {
16053
+ return false;
16054
+ }
16055
+ }
16056
+
16057
+ function handleRemoteFleetControlInputSocketFailure(session) {
16058
+ if (!session?.active) {
16059
+ return;
16060
+ }
16061
+
16062
+ closeRemoteFleetControlInputSocket(session, 'failed');
16063
+ session.inputWsUnavailableUntil = Date.now() + 1000;
16064
+ drainRemoteFleetControlInputQueueToFallback(session);
16065
+ }
16066
+
16067
+ function ensureRemoteFleetControlInputSocket(session) {
16068
+ if (!session?.active || typeof WebSocket !== 'function') {
16069
+ return false;
16070
+ }
16071
+
16072
+ const ws = session.inputWs;
16073
+ if (ws && (ws.readyState === 0 || ws.readyState === 1)) {
16074
+ return true;
16075
+ }
16076
+
16077
+ if (session.inputWsConnecting === true) {
16078
+ return true;
16079
+ }
16080
+
16081
+ if ((session.inputWsUnavailableUntil || 0) > Date.now()) {
16082
+ return false;
16083
+ }
16084
+
16085
+ const connectToken = `input-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
16086
+ session.inputWsConnecting = true;
16087
+ session.inputWsConnectToken = connectToken;
16088
+ getRemoteFleetBridgeStatusForFrames()
16089
+ .then(status => {
16090
+ if (!session.active || session.inputWsConnectToken !== connectToken) {
16091
+ return;
16092
+ }
16093
+
16094
+ const wsUrl = buildRemoteFleetInputWsUrl(status);
16095
+ if (!wsUrl) {
16096
+ throw new Error('input-ws-url-unavailable');
16097
+ }
16098
+
16099
+ const inputWs = new WebSocket(wsUrl);
16100
+ session.inputWs = inputWs;
16101
+ inputWs.onopen = () => {
16102
+ if (!session.active || session.inputWs !== inputWs) {
16103
+ return;
16104
+ }
16105
+
16106
+ session.inputWsConnecting = false;
16107
+ session.inputWsUnavailableUntil = 0;
16108
+ if (session.inputWsOpenTimer) {
16109
+ clearTimeout(session.inputWsOpenTimer);
16110
+ session.inputWsOpenTimer = null;
16111
+ }
16112
+ flushRemoteFleetControlInputQueue(session);
16113
+ };
16114
+ inputWs.onmessage = event => {
16115
+ try {
16116
+ const message = JSON.parse(String(event?.data || ''));
16117
+ if (message?.ok === false && session.active) {
16118
+ session.status.textContent = message.error || 'Input failed';
16119
+ }
16120
+ } catch {
16121
+ // Ignore input ack parse errors.
16122
+ }
16123
+ };
16124
+ inputWs.onerror = () => {
16125
+ if (session.inputWs === inputWs) {
16126
+ handleRemoteFleetControlInputSocketFailure(session);
16127
+ }
16128
+ };
16129
+ inputWs.onclose = () => {
16130
+ if (session.inputWs === inputWs) {
16131
+ handleRemoteFleetControlInputSocketFailure(session);
16132
+ }
16133
+ };
16134
+ session.inputWsOpenTimer = setTimeout(() => {
16135
+ if (session.active && session.inputWs === inputWs && inputWs.readyState === 0) {
16136
+ handleRemoteFleetControlInputSocketFailure(session);
16137
+ }
16138
+ }, 900);
16139
+ })
16140
+ .catch(() => {
16141
+ if (!session.active || session.inputWsConnectToken !== connectToken) {
16142
+ return;
16143
+ }
16144
+
16145
+ session.inputWsConnecting = false;
16146
+ session.inputWsUnavailableUntil = Date.now() + 1000;
16147
+ drainRemoteFleetControlInputQueueToFallback(session);
16148
+ });
16149
+
16150
+ return true;
16151
+ }
16152
+
16153
+ function sendRemoteFleetControlInputNow(session, payload) {
16154
+ if (!session?.active || !payload?.type) {
16155
+ return;
16156
+ }
16157
+
16158
+ const input = createRemoteFleetControlInput(session, payload);
16159
+ const envelope = {
16160
+ type: 'input',
16161
+ nodeId: session.nodeId,
16162
+ deviceId: session.deviceId,
16163
+ requestId: `input-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
16164
+ input
15929
16165
  };
15930
16166
 
16167
+ const wsReady = ensureRemoteFleetControlInputSocket(session);
16168
+ if (wsReady && sendRemoteFleetControlInputEnvelope(session, envelope)) {
16169
+ return;
16170
+ }
16171
+
16172
+ if (wsReady) {
16173
+ enqueueRemoteFleetControlInput(session, envelope);
16174
+ return;
16175
+ }
16176
+
16177
+ sendRemoteFleetControlInputFallback(session, input);
16178
+ }
16179
+
16180
+ function sendRemoteFleetControlInput(session, payload, throttleMove = false) {
16181
+ if (!session?.active || !payload?.type) {
16182
+ return;
16183
+ }
16184
+
15931
16185
  if (!throttleMove) {
15932
- send(payload);
16186
+ sendRemoteFleetControlInputNow(session, payload);
15933
16187
  return;
15934
16188
  }
15935
16189
 
15936
16190
  const now = performance.now();
15937
16191
  const elapsed = now - (session.lastMoveSentAt || 0);
15938
- if (elapsed >= 32) {
16192
+ if (elapsed >= REMOTE_FLEET_CONTROL_INPUT_MOVE_MS) {
15939
16193
  session.lastMoveSentAt = now;
15940
- send(payload);
16194
+ sendRemoteFleetControlInputNow(session, payload);
15941
16195
  return;
15942
16196
  }
15943
16197
 
@@ -15952,9 +16206,9 @@
15952
16206
  session.pendingMove = null;
15953
16207
  if (next) {
15954
16208
  session.lastMoveSentAt = performance.now();
15955
- send(next);
16209
+ sendRemoteFleetControlInputNow(session, next);
15956
16210
  }
15957
- }, Math.max(1, 32 - elapsed));
16211
+ }, Math.max(1, REMOTE_FLEET_CONTROL_INPUT_MOVE_MS - elapsed));
15958
16212
  }
15959
16213
 
15960
16214
  function bindRemoteFleetControlInput(session) {
@@ -15965,6 +16219,40 @@
15965
16219
  overlay.addEventListener(eventName, event => event.stopPropagation(), true);
15966
16220
  });
15967
16221
 
16222
+ const shouldIgnoreKeyboardEvent = event =>
16223
+ event?.target?.closest?.('[data-remote-fleet-control-close="true"]');
16224
+
16225
+ const handleKeyboardEvent = (event, type) => {
16226
+ if (!session.active || shouldIgnoreKeyboardEvent(event) || event.__remoteFleetControlHandled === true) {
16227
+ return;
16228
+ }
16229
+
16230
+ event.__remoteFleetControlHandled = true;
16231
+ event.preventDefault();
16232
+ event.stopPropagation();
16233
+ shell.focus({ preventScroll: true });
16234
+ sendRemoteFleetControlInput(session, {
16235
+ type,
16236
+ key: event.key,
16237
+ code: event.code,
16238
+ repeat: event.repeat === true
16239
+ });
16240
+ };
16241
+
16242
+ const handleWindowKeyDown = event => handleKeyboardEvent(event, 'keyDown');
16243
+ const handleWindowKeyUp = event => handleKeyboardEvent(event, 'keyUp');
16244
+ if (typeof window.addEventListener === 'function') {
16245
+ window.addEventListener('keydown', handleWindowKeyDown, true);
16246
+ window.addEventListener('keyup', handleWindowKeyUp, true);
16247
+ }
16248
+ session.inputCleanup = () => {
16249
+ if (typeof window.removeEventListener === 'function') {
16250
+ window.removeEventListener('keydown', handleWindowKeyDown, true);
16251
+ window.removeEventListener('keyup', handleWindowKeyUp, true);
16252
+ }
16253
+ session.inputCleanup = null;
16254
+ };
16255
+
15968
16256
  stage.addEventListener('pointerdown', event => {
15969
16257
  const point = getRemoteFleetControlPoint(event, canvas, false);
15970
16258
  if (!point) {
@@ -16041,35 +16329,8 @@
16041
16329
  event.stopPropagation();
16042
16330
  });
16043
16331
 
16044
- shell.addEventListener('keydown', event => {
16045
- if (event.target?.closest?.('[data-remote-fleet-control-close="true"]')) {
16046
- return;
16047
- }
16048
-
16049
- event.preventDefault();
16050
- event.stopPropagation();
16051
- sendRemoteFleetControlInput(session, {
16052
- type: 'keyDown',
16053
- key: event.key,
16054
- code: event.code,
16055
- repeat: event.repeat === true
16056
- });
16057
- });
16058
-
16059
- shell.addEventListener('keyup', event => {
16060
- if (event.target?.closest?.('[data-remote-fleet-control-close="true"]')) {
16061
- return;
16062
- }
16063
-
16064
- event.preventDefault();
16065
- event.stopPropagation();
16066
- sendRemoteFleetControlInput(session, {
16067
- type: 'keyUp',
16068
- key: event.key,
16069
- code: event.code,
16070
- repeat: event.repeat === true
16071
- });
16072
- });
16332
+ shell.addEventListener('keydown', event => handleKeyboardEvent(event, 'keyDown'));
16333
+ shell.addEventListener('keyup', event => handleKeyboardEvent(event, 'keyUp'));
16073
16334
  }
16074
16335
 
16075
16336
  function openRemoteFleetControlPopup(bodyView, nodeModel, deviceId) {
@@ -16233,10 +16494,18 @@
16233
16494
  lastMoveSentAt: 0,
16234
16495
  lastControlFrameAt: 0,
16235
16496
  lastControlFrameSeq: 0,
16236
- binaryFramePreferred: false
16497
+ binaryFramePreferred: false,
16498
+ inputWs: null,
16499
+ inputWsConnecting: false,
16500
+ inputWsConnectToken: '',
16501
+ inputWsOpenTimer: null,
16502
+ inputWsUnavailableUntil: 0,
16503
+ inputQueue: [],
16504
+ inputCleanup: null
16237
16505
  };
16238
16506
  activeRemoteFleetControlPopup = session;
16239
16507
  bindRemoteFleetControlInput(session);
16508
+ ensureRemoteFleetControlInputSocket(session);
16240
16509
  attachRemoteFleetControlBinaryFrameSession(session, bodyView);
16241
16510
 
16242
16511
  const refresh = async () => {
@@ -1,5 +1,5 @@
1
1
  self.assetsManifest = {
2
- "version": "20SB8UIp",
2
+ "version": "uSz5Xt0z",
3
3
  "assets": [
4
4
  {
5
5
  "hash": "sha256-+CSYMcqLNTsq3VnH11jgYyOCCdxvHzL74CBmo4sCmMU=",
@@ -86,7 +86,7 @@
86
86
  "url": "_content/MindExecution.Shared/js/mind-map-core.js.backup"
87
87
  },
88
88
  {
89
- "hash": "sha256-CXwos9ge49EzJij/kELHzhWSDGBSR3fbqqW7hBxARCI=",
89
+ "hash": "sha256-4BdT/RnBOnh3Mm/hTZilg3zo9az6W9oYf8h3br2aDko=",
90
90
  "url": "_content/MindExecution.Shared/js/mind-map-css3d-manager.js"
91
91
  },
92
92
  {
@@ -1,4 +1,4 @@
1
- /* Manifest version: 20SB8UIp */
1
+ /* Manifest version: uSz5Xt0z */
2
2
  // Hosted deployments should prefer the network over stale offline caches.
3
3
  // This service worker immediately clears old Blazor offline caches and unregisters itself.
4
4