@mindexec/cli 0.2.114 → 0.2.116

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.114",
3
+ "version": "0.2.116",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -20,7 +20,7 @@
20
20
  "scripts": {
21
21
  "start": "node launch-bridge.cjs",
22
22
  "dev": "node launch-bridge.cjs --watch",
23
- "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",
23
+ "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",
24
24
  "test:auth": "node scripts/auth-session-smoke.mjs",
25
25
  "test:remote": "node scripts/remote-hub-smoke.mjs",
26
26
  "test:remote:scale": "node scripts/remote-hub-scale-smoke.mjs",
package/remote-hub.js CHANGED
@@ -601,6 +601,7 @@ export function createRemoteHub(options = {}) {
601
601
  const logEvent = options.logEvent || (() => {});
602
602
  const logWarn = options.logWarn || (() => {});
603
603
  const emitEvent = options.emitEvent || (() => {});
604
+ const emitFrame = typeof options.emitFrame === 'function' ? options.emitFrame : (() => {});
604
605
 
605
606
  const enabled = isEnabledValue(env.MINDEXEC_REMOTE_HUB ?? env.REMOTE_HUB_ENABLED, true)
606
607
  && !isDisabledValue(env.REMOTE_HUB_DISABLED);
@@ -985,6 +986,14 @@ export function createRemoteHub(options = {}) {
985
986
  });
986
987
  device.latestLiveFrame = frame;
987
988
  rememberRecentFramePayload(device, 'live', frame);
989
+ emitFrame({
990
+ kind: 'live',
991
+ deviceId: device.deviceId,
992
+ frame: serializeRemoteFrame(frame, device.deviceId, 'live', { includeDataUrl: false }),
993
+ payload: frame.payload,
994
+ mimeType: frame.mimeType,
995
+ byteLength: frame.byteLength
996
+ });
988
997
  device.activeLiveStream.lastFrameAt = frame.receivedAt;
989
998
  device.activeLiveStream.lastFrameSeq = frame.frameSeq;
990
999
  device.activeLiveStream.framesReceived = (device.activeLiveStream.framesReceived || 0) + 1;
@@ -1780,6 +1789,14 @@ export function createRemoteHub(options = {}) {
1780
1789
  accessToken: createFrameAccessToken()
1781
1790
  };
1782
1791
  rememberRecentFramePayload(device, 'live', device.latestLiveFrame);
1792
+ emitFrame({
1793
+ kind: 'live',
1794
+ deviceId: device.deviceId,
1795
+ frame: serializeRemoteFrame(device.latestLiveFrame, device.deviceId, 'live', { includeDataUrl: false }),
1796
+ payload: device.latestLiveFrame.payload,
1797
+ mimeType: device.latestLiveFrame.mimeType,
1798
+ byteLength: device.latestLiveFrame.byteLength
1799
+ });
1783
1800
  device.activeLiveStream.lastFrameAt = device.lastSeenAt;
1784
1801
  device.activeLiveStream.lastFrameSeq = frameSeq;
1785
1802
  device.activeLiveStream.framesReceived = (device.activeLiveStream.framesReceived || 0) + 1;
@@ -0,0 +1,196 @@
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-frame-ws-smoke-token';
11
+ const PAIR_TOKEN = 'remote-frame-ws-pair-token';
12
+
13
+ function wait(ms) {
14
+ return new Promise(resolve => setTimeout(resolve, ms));
15
+ }
16
+
17
+ async function findFreePort() {
18
+ return await new Promise((resolve, reject) => {
19
+ const server = net.createServer();
20
+ server.unref();
21
+ server.once('error', reject);
22
+ server.listen(0, '127.0.0.1', () => {
23
+ const address = server.address();
24
+ const port = typeof address === 'object' && address ? address.port : 0;
25
+ server.close(() => resolve(port));
26
+ });
27
+ });
28
+ }
29
+
30
+ async function fetchJson(url, options = {}) {
31
+ const response = await fetch(url, {
32
+ ...options,
33
+ headers: {
34
+ ...(options.body ? { 'Content-Type': 'application/json' } : {}),
35
+ 'X-Bridge-Token': BRIDGE_TOKEN,
36
+ ...(options.headers || {})
37
+ }
38
+ });
39
+ const payload = await response.json().catch(() => null);
40
+ return { ok: response.ok, status: response.status, payload };
41
+ }
42
+
43
+ async function waitForBridge(baseUrl, details) {
44
+ const startedAt = Date.now();
45
+ while (Date.now() - startedAt < 30000) {
46
+ try {
47
+ const result = await fetchJson(`${baseUrl}/api/remote/status`);
48
+ if (result.ok && result.payload?.started === true) {
49
+ return result.payload;
50
+ }
51
+ } catch {
52
+ // Server is still starting.
53
+ }
54
+ await wait(100);
55
+ }
56
+
57
+ throw new Error(`Timed out waiting for LocalBridge.\n${details()}`);
58
+ }
59
+
60
+ function spawnBridge({ bridgePort, remoteHubPort }) {
61
+ const bridgeRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
62
+ const env = {
63
+ ...process.env,
64
+ BRIDGE_PORT: String(bridgePort),
65
+ BRIDGE_TOKEN,
66
+ BRIDGE_REQUIRE_TOKEN: '1',
67
+ MINDEXEC_REMOTE_HUB: '1',
68
+ MINDEXEC_REMOTE_SYNTHETIC_FLEET: '1',
69
+ REMOTE_HUB_HOST: '127.0.0.1',
70
+ REMOTE_HUB_PORT: String(remoteHubPort),
71
+ REMOTE_HUB_PAIR_TOKEN: PAIR_TOKEN,
72
+ NO_COLOR: '1'
73
+ };
74
+
75
+ const child = spawn(process.execPath, ['server.js'], {
76
+ cwd: bridgeRoot,
77
+ stdio: ['ignore', 'pipe', 'pipe'],
78
+ windowsHide: true,
79
+ env
80
+ });
81
+
82
+ let stdout = '';
83
+ let stderr = '';
84
+ child.stdout.on('data', chunk => {
85
+ stdout += chunk.toString();
86
+ });
87
+ child.stderr.on('data', chunk => {
88
+ stderr += chunk.toString();
89
+ });
90
+
91
+ const exitPromise = new Promise(resolve => child.once('exit', resolve));
92
+ const stop = async () => {
93
+ if (child.exitCode === null && !child.killed) {
94
+ child.kill('SIGTERM');
95
+ await Promise.race([exitPromise, wait(5000)]);
96
+ if (child.exitCode === null && !child.killed) {
97
+ child.kill('SIGKILL');
98
+ }
99
+ }
100
+ };
101
+
102
+ return {
103
+ baseUrl: `http://127.0.0.1:${bridgePort}`,
104
+ details: () => `stdout=${stdout}\nstderr=${stderr}`,
105
+ stop
106
+ };
107
+ }
108
+
109
+ function parseFramePacket(data) {
110
+ const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
111
+ assert.ok(buffer.length > 8, 'binary frame packet should include header, metadata, and payload');
112
+ const metaLength = buffer.readUInt32BE(0);
113
+ assert.ok(metaLength > 0 && metaLength < 64 * 1024, 'metadata length should be bounded');
114
+ assert.ok(4 + metaLength < buffer.length, 'packet should include binary payload after metadata');
115
+ const metadata = JSON.parse(buffer.subarray(4, 4 + metaLength).toString('utf8'));
116
+ const payload = buffer.subarray(4 + metaLength);
117
+ return { metadata, payload };
118
+ }
119
+
120
+ async function waitForBinaryFrame(ws) {
121
+ return await new Promise((resolve, reject) => {
122
+ const timeout = setTimeout(() => reject(new Error('timed out waiting for binary frame')), 10000);
123
+ ws.on('message', data => {
124
+ if (typeof data === 'string') {
125
+ return;
126
+ }
127
+ const raw = Buffer.isBuffer(data) ? data : Buffer.from(data);
128
+ if (raw[0] === 0x7b || raw[0] === 0x5b) {
129
+ return;
130
+ }
131
+ clearTimeout(timeout);
132
+ try {
133
+ resolve(parseFramePacket(raw));
134
+ } catch (error) {
135
+ reject(error);
136
+ }
137
+ });
138
+ ws.on('error', reject);
139
+ ws.on('close', () => reject(new Error('websocket closed before binary frame')));
140
+ });
141
+ }
142
+
143
+ async function main() {
144
+ const bridgePort = await findFreePort();
145
+ const remoteHubPort = await findFreePort();
146
+ const bridge = spawnBridge({ bridgePort, remoteHubPort });
147
+
148
+ try {
149
+ const status = await waitForBridge(bridge.baseUrl, bridge.details);
150
+ assert.equal(status.started, true);
151
+
152
+ const seed = await fetchJson(`${bridge.baseUrl}/api/remote/synthetic/seed`, {
153
+ method: 'POST',
154
+ body: JSON.stringify({ count: 4, connectedRatio: 1, liveRatio: 1 })
155
+ });
156
+ assert.equal(seed.ok, true);
157
+
158
+ const devicesResult = await fetchJson(`${bridge.baseUrl}/api/remote/devices`);
159
+ assert.equal(devicesResult.ok, true);
160
+ const device = devicesResult.payload?.devices?.find(item => item.connected === true);
161
+ assert.ok(device?.deviceId, 'expected connected synthetic device');
162
+
163
+ const ws = new WebSocket(`${bridge.baseUrl.replace(/^http:/, 'ws:')}/api/remote/frames/ws?token=${encodeURIComponent(BRIDGE_TOKEN)}`);
164
+ await new Promise((resolve, reject) => {
165
+ ws.once('open', resolve);
166
+ ws.once('error', reject);
167
+ });
168
+ ws.send(JSON.stringify({ type: 'subscribe', deviceIds: [device.deviceId] }));
169
+
170
+ const framePromise = waitForBinaryFrame(ws);
171
+ const live = await fetchJson(`${bridge.baseUrl}/api/remote/devices/${encodeURIComponent(device.deviceId)}/live/start`, {
172
+ method: 'POST',
173
+ body: JSON.stringify({ fps: 20 })
174
+ });
175
+ assert.equal(live.ok, true);
176
+
177
+ const frame = await framePromise;
178
+ assert.equal(frame.metadata.type, 'remote.frame.binary');
179
+ assert.equal(frame.metadata.kind, 'live');
180
+ assert.equal(frame.metadata.deviceId, device.deviceId);
181
+ assert.equal(frame.metadata.fps, 20);
182
+ assert.ok(frame.metadata.frameSeq > 0, 'frameSeq should be positive');
183
+ assert.equal(frame.metadata.mimeType, 'image/png');
184
+ assert.ok(frame.payload.length > 0, 'payload should be non-empty');
185
+ ws.close();
186
+
187
+ console.log('Remote frame WebSocket smoke OK');
188
+ } finally {
189
+ await bridge.stop();
190
+ }
191
+ }
192
+
193
+ main().catch(error => {
194
+ console.error(error);
195
+ process.exitCode = 1;
196
+ });
package/server.js CHANGED
@@ -2120,28 +2120,41 @@ app.get('/api/local-files/:id', async (req, res) => {
2120
2120
 
2121
2121
  const httpServer = createServer(app);
2122
2122
  const wss = new WebSocketServer({ noServer: true });
2123
+ const remoteFrameWss = new WebSocketServer({ noServer: true });
2123
2124
  const wsClients = new Set();
2124
- const shellJobs = new Map();
2125
-
2126
- httpServer.on('upgrade', (req, socket, head) => {
2127
- try {
2128
- const parsed = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
2129
- const token = parsed.searchParams.get('token') || '';
2130
-
2131
- if (parsed.pathname !== '/events' || token !== wsToken) {
2132
- socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
2133
- socket.destroy();
2134
- return;
2135
- }
2136
-
2137
- wss.handleUpgrade(req, socket, head, (ws) => {
2138
- wss.emit('connection', ws, req);
2139
- });
2140
- } catch {
2141
- socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
2142
- socket.destroy();
2143
- }
2144
- });
2125
+ const remoteFrameClients = new Set();
2126
+ const shellJobs = new Map();
2127
+
2128
+ httpServer.on('upgrade', (req, socket, head) => {
2129
+ try {
2130
+ const parsed = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
2131
+ const token = parsed.searchParams.get('token') || '';
2132
+
2133
+ if (parsed.pathname === '/events' && token === wsToken) {
2134
+ wss.handleUpgrade(req, socket, head, (ws) => {
2135
+ wss.emit('connection', ws, req);
2136
+ });
2137
+ return;
2138
+ }
2139
+
2140
+ if (parsed.pathname === '/api/remote/frames/ws'
2141
+ && (!bridgeAuthRequired || token === bridgeToken)) {
2142
+ remoteFrameWss.handleUpgrade(req, socket, head, (ws) => {
2143
+ remoteFrameWss.emit('connection', ws, req);
2144
+ });
2145
+ return;
2146
+ }
2147
+
2148
+ {
2149
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
2150
+ socket.destroy();
2151
+ return;
2152
+ }
2153
+ } catch {
2154
+ socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
2155
+ socket.destroy();
2156
+ }
2157
+ });
2145
2158
 
2146
2159
  wss.on('connection', (ws) => {
2147
2160
  wsClients.add(ws);
@@ -2154,11 +2167,168 @@ wss.on('connection', (ws) => {
2154
2167
  projectRoot: projectSession.rootPath,
2155
2168
  fileCount: projectSession.symbolsByFile.size
2156
2169
  }
2157
- }));
2158
- });
2159
-
2160
- // Helper: Validate path is within workspace
2161
- function validatePath(requestedPath) {
2170
+ }));
2171
+ });
2172
+
2173
+ function normalizeRemoteFrameWsDeviceIds(value) {
2174
+ const raw = [];
2175
+ if (Array.isArray(value)) {
2176
+ raw.push(...value);
2177
+ } else if (typeof value === 'string') {
2178
+ raw.push(...value.split(','));
2179
+ }
2180
+
2181
+ const ids = [];
2182
+ const seen = new Set();
2183
+ for (const entry of raw) {
2184
+ const id = String(entry || '').trim();
2185
+ if (!id || seen.has(id)) {
2186
+ continue;
2187
+ }
2188
+
2189
+ seen.add(id);
2190
+ ids.push(id);
2191
+ if (ids.length >= 240) {
2192
+ break;
2193
+ }
2194
+ }
2195
+
2196
+ return ids;
2197
+ }
2198
+
2199
+ function sendRemoteFrameClientJson(ws, payload) {
2200
+ if (!ws || ws.readyState !== 1) {
2201
+ return false;
2202
+ }
2203
+
2204
+ try {
2205
+ ws.send(JSON.stringify(payload));
2206
+ return true;
2207
+ } catch {
2208
+ return false;
2209
+ }
2210
+ }
2211
+
2212
+ function updateRemoteFrameClientSubscription(ws, payload = {}) {
2213
+ const deviceIds = normalizeRemoteFrameWsDeviceIds(payload.deviceIds ?? payload.devices ?? payload.deviceId);
2214
+ ws.remoteFrameDeviceIds = new Set(deviceIds);
2215
+ ws.remoteFrameSubscriptionUpdatedAt = new Date().toISOString();
2216
+ sendRemoteFrameClientJson(ws, {
2217
+ type: 'RemoteFrameSubscription',
2218
+ timestamp: ws.remoteFrameSubscriptionUpdatedAt,
2219
+ deviceIds,
2220
+ mode: deviceIds.length > 0 ? 'selected-devices' : 'all-devices'
2221
+ });
2222
+ }
2223
+
2224
+ function buildRemoteFrameBinaryPacket(frameEvent) {
2225
+ const payload = frameEvent?.payload;
2226
+ if (!Buffer.isBuffer(payload) || payload.length === 0) {
2227
+ return null;
2228
+ }
2229
+
2230
+ const frame = frameEvent.frame || {};
2231
+ const metadata = {
2232
+ type: 'remote.frame.binary',
2233
+ kind: frameEvent.kind || 'live',
2234
+ deviceId: frameEvent.deviceId || frame.deviceId || '',
2235
+ frameSeq: Number(frame.frameSeq || 0) || 0,
2236
+ streamId: frame.streamId || '',
2237
+ width: Number(frame.width || 0) || 0,
2238
+ height: Number(frame.height || 0) || 0,
2239
+ mimeType: frameEvent.mimeType || frame.mimeType || frame.format || 'image/jpeg',
2240
+ mode: frame.mode || '',
2241
+ fps: Number(frame.fps || 0) || 0,
2242
+ capturedAt: frame.capturedAt || '',
2243
+ receivedAt: frame.receivedAt || '',
2244
+ byteLength: Number(frameEvent.byteLength || payload.length) || payload.length,
2245
+ transport: frame.transport || '',
2246
+ contentHash: frame.contentHash || '',
2247
+ captureMs: Number(frame.captureMs || 0) || 0,
2248
+ sameContentStreak: Number(frame.sameContentStreak || 0) || 0
2249
+ };
2250
+ const metaBuffer = Buffer.from(JSON.stringify(metadata), 'utf8');
2251
+ const header = Buffer.allocUnsafe(4);
2252
+ header.writeUInt32BE(metaBuffer.length, 0);
2253
+ return Buffer.concat([header, metaBuffer, payload], header.length + metaBuffer.length + payload.length);
2254
+ }
2255
+
2256
+ function broadcastRemoteBinaryFrame(frameEvent) {
2257
+ if (remoteFrameClients.size === 0) {
2258
+ return;
2259
+ }
2260
+
2261
+ const deviceId = String(frameEvent?.deviceId || frameEvent?.frame?.deviceId || '').trim();
2262
+ if (!deviceId) {
2263
+ return;
2264
+ }
2265
+
2266
+ const packet = buildRemoteFrameBinaryPacket(frameEvent);
2267
+ if (!packet) {
2268
+ return;
2269
+ }
2270
+
2271
+ for (const client of remoteFrameClients) {
2272
+ if (client.readyState !== 1) {
2273
+ continue;
2274
+ }
2275
+
2276
+ const ids = client.remoteFrameDeviceIds;
2277
+ if (ids instanceof Set && ids.size > 0 && !ids.has(deviceId)) {
2278
+ continue;
2279
+ }
2280
+
2281
+ if (client.bufferedAmount > 8 * 1024 * 1024) {
2282
+ client.remoteFrameDroppedCount = (client.remoteFrameDroppedCount || 0) + 1;
2283
+ continue;
2284
+ }
2285
+
2286
+ try {
2287
+ client.send(packet, { binary: true });
2288
+ client.remoteFrameSentCount = (client.remoteFrameSentCount || 0) + 1;
2289
+ } catch {
2290
+ client.remoteFrameDroppedCount = (client.remoteFrameDroppedCount || 0) + 1;
2291
+ }
2292
+ }
2293
+ }
2294
+
2295
+ remoteFrameWss.on('connection', (ws, req) => {
2296
+ remoteFrameClients.add(ws);
2297
+ ws.binaryType = 'arraybuffer';
2298
+ try {
2299
+ const parsed = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
2300
+ updateRemoteFrameClientSubscription(ws, {
2301
+ deviceIds: parsed.searchParams.get('devices') || ''
2302
+ });
2303
+ } catch {
2304
+ updateRemoteFrameClientSubscription(ws, {});
2305
+ }
2306
+
2307
+ ws.on('message', data => {
2308
+ try {
2309
+ const text = Buffer.isBuffer(data) ? data.toString('utf8') : String(data || '');
2310
+ const payload = JSON.parse(text);
2311
+ if (payload?.type === 'subscribe') {
2312
+ updateRemoteFrameClientSubscription(ws, payload);
2313
+ }
2314
+ } catch {
2315
+ sendRemoteFrameClientJson(ws, {
2316
+ type: 'RemoteFrameSubscriptionError',
2317
+ error: 'invalid-message'
2318
+ });
2319
+ }
2320
+ });
2321
+ ws.on('close', () => remoteFrameClients.delete(ws));
2322
+ ws.on('error', () => remoteFrameClients.delete(ws));
2323
+ sendRemoteFrameClientJson(ws, {
2324
+ type: 'RemoteFrameSocketReady',
2325
+ timestamp: new Date().toISOString(),
2326
+ protocol: 'mindexec.remote.frames.binary.v1'
2327
+ });
2328
+ });
2329
+
2330
+ // Helper: Validate path is within workspace
2331
+ function validatePath(requestedPath) {
2162
2332
  const fullPath = resolveWorkspaceBoundPath(requestedPath);
2163
2333
  if (!isPathWithin(workspacePath, fullPath)) {
2164
2334
  throw new Error('Path must be within workspace');
@@ -2726,6 +2896,7 @@ const remoteHub = createRemoteHub({
2726
2896
  logWarn,
2727
2897
  logError,
2728
2898
  emitEvent: emitBridgeEvent,
2899
+ emitFrame: broadcastRemoteBinaryFrame,
2729
2900
  managerPackage: BRIDGE_PACKAGE_NAME,
2730
2901
  managerVersion: BRIDGE_VERSION,
2731
2902
  hostInstanceId: BRIDGE_INSTANCE_ID
@@ -7772,8 +7943,9 @@ app.get('/api/status', async (req, res) => {
7772
7943
  status: 'ok',
7773
7944
  version: BRIDGE_VERSION,
7774
7945
  workspace: workspacePath,
7775
- wsToken,
7776
- wsPath: '/events',
7946
+ wsToken,
7947
+ wsPath: '/events',
7948
+ remoteFrameWsPath: '/api/remote/frames/ws',
7777
7949
  bridgeToken,
7778
7950
  bridgeTokenHeader,
7779
7951
  bridgeAuthRequired,
@@ -5,7 +5,7 @@
5
5
  const DEBUG = false;
6
6
  const FPS_DEBUG = false;
7
7
  const FRAME_PERF_DEBUG = false;
8
- const MINDMAP_CORE_BUILD_ID = '20260616-remote-agent-sticky-overlay-frames-v571';
8
+ const MINDMAP_CORE_BUILD_ID = '20260616-remote-ws-control-frames-v573';
9
9
  const CanvasPhase = Object.freeze({
10
10
  Booting: 'booting',
11
11
  BoardFileLoading: 'board-file-loading',
@@ -13345,19 +13345,25 @@
13345
13345
  const REMOTE_FLEET_LIVE_REFRESH_MS = 30000;
13346
13346
  const REMOTE_FLEET_MONITOR_LIVE_FPS = 10;
13347
13347
  const REMOTE_FLEET_LIVE_FRAME_REFRESH_MS = Math.round(1000 / REMOTE_FLEET_MONITOR_LIVE_FPS);
13348
+ const REMOTE_FLEET_CONTROL_LIVE_FPS = 20;
13349
+ const REMOTE_FLEET_CONTROL_FRAME_REFRESH_MS = Math.round(1000 / REMOTE_FLEET_CONTROL_LIVE_FPS);
13348
13350
  const REMOTE_FLEET_THUMBNAIL_FRAME_REFRESH_MS = 2000;
13349
13351
  const REMOTE_FLEET_FRAME_CANVAS_MAX_DPR = 2;
13350
13352
  const REMOTE_FLEET_FRAME_BLOB_CACHE_MS = 10000;
13351
13353
  const REMOTE_FLEET_FRAME_BLOB_CACHE_LIMIT = 96;
13352
13354
  const REMOTE_FLEET_LIVE_FRAME_DECODE_TIMEOUT_MS = 180;
13353
13355
  const REMOTE_FLEET_THUMBNAIL_FRAME_DECODE_TIMEOUT_MS = 1200;
13356
+ const REMOTE_FLEET_BINARY_FRAME_WS_RECONNECT_MS = 1500;
13357
+ const REMOTE_FLEET_BINARY_FRAME_SUBSCRIBE_MS = 1000;
13354
13358
  const REMOTE_FLEET_TASK_FOLLOW_INITIAL_MS = 250;
13355
13359
  const REMOTE_FLEET_TASK_FOLLOW_REFRESH_MS = 2000;
13356
13360
  const REMOTE_FLEET_TASK_FOLLOW_MAX_TICKS = 60;
13357
13361
  const REMOTE_FLEET_HOST_LEASE_REFRESH_MS = 10000;
13358
13362
  const remoteFleetHostLeaseTimers = new Map();
13359
13363
  const remoteFleetLocalHostTargets = new Map();
13364
+ const remoteFleetBinaryFrameSessions = new Map();
13360
13365
  const remoteFleetFrameBlobCache = new Map();
13366
+ let remoteFleetBridgeStatusPromise = null;
13361
13367
  let activeRemoteFleetControlPopup = null;
13362
13368
 
13363
13369
  function findRemoteFleetBodyByNodeId(nodeId) {
@@ -13495,6 +13501,7 @@
13495
13501
 
13496
13502
  function clearRemoteFleetTimers(bodyView) {
13497
13503
  if (!bodyView) return;
13504
+ releaseRemoteFleetBinaryFrameSocket(bodyView);
13498
13505
  if (bodyView._remoteFleetLiveRefreshTimer) {
13499
13506
  clearInterval(bodyView._remoteFleetLiveRefreshTimer);
13500
13507
  bodyView._remoteFleetLiveRefreshTimer = null;
@@ -13519,6 +13526,372 @@
13519
13526
  }
13520
13527
  }
13521
13528
 
13529
+ function getRemoteFleetBridgeBaseUrlCandidates() {
13530
+ const candidates = [];
13531
+ const push = value => {
13532
+ const text = String(value || '').trim().replace(/\/+$/, '');
13533
+ if (text && !candidates.includes(text)) {
13534
+ candidates.push(text);
13535
+ }
13536
+ };
13537
+
13538
+ push(window.MindExecLocalBridge?.activeBridgeUrl);
13539
+ push(window.location?.origin);
13540
+ return candidates;
13541
+ }
13542
+
13543
+ async function getRemoteFleetBridgeStatusForFrames() {
13544
+ if (remoteFleetBridgeStatusPromise) {
13545
+ return remoteFleetBridgeStatusPromise;
13546
+ }
13547
+
13548
+ remoteFleetBridgeStatusPromise = (async () => {
13549
+ const nonce = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
13550
+ let lastError = null;
13551
+ for (const baseUrl of getRemoteFleetBridgeBaseUrlCandidates()) {
13552
+ try {
13553
+ const response = await fetch(`${baseUrl}/api/status?remoteFrames=ws&cb=${encodeURIComponent(nonce)}`, {
13554
+ cache: 'no-store',
13555
+ credentials: 'omit'
13556
+ });
13557
+ if (!response.ok) {
13558
+ lastError = new Error(`status:${response.status}`);
13559
+ continue;
13560
+ }
13561
+
13562
+ const status = await response.json();
13563
+ status.__remoteFleetBridgeBaseUrl = baseUrl;
13564
+ return status;
13565
+ } catch (error) {
13566
+ lastError = error;
13567
+ }
13568
+ }
13569
+
13570
+ throw lastError || new Error('bridge-status-unavailable');
13571
+ })();
13572
+
13573
+ try {
13574
+ return await remoteFleetBridgeStatusPromise;
13575
+ } catch (error) {
13576
+ remoteFleetBridgeStatusPromise = null;
13577
+ throw error;
13578
+ }
13579
+ }
13580
+
13581
+ function buildRemoteFleetBinaryFrameWsUrl(status) {
13582
+ const baseUrl = String(status?.__remoteFleetBridgeBaseUrl || window.location?.origin || '').trim();
13583
+ const path = String(status?.remoteFrameWsPath || '/api/remote/frames/ws').trim() || '/api/remote/frames/ws';
13584
+ if (!baseUrl) {
13585
+ return '';
13586
+ }
13587
+
13588
+ const url = new URL(path, baseUrl);
13589
+ url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
13590
+ const bridgeToken = String(status?.bridgeToken || '').trim();
13591
+ if (bridgeToken) {
13592
+ url.searchParams.set('token', bridgeToken);
13593
+ }
13594
+ return url.toString();
13595
+ }
13596
+
13597
+ async function parseRemoteFleetBinaryFrameMessage(data) {
13598
+ let buffer = null;
13599
+ if (data instanceof ArrayBuffer) {
13600
+ buffer = data;
13601
+ } else if (data?.arrayBuffer && typeof data.arrayBuffer === 'function') {
13602
+ buffer = await data.arrayBuffer();
13603
+ }
13604
+
13605
+ if (!buffer || buffer.byteLength < 5) {
13606
+ return null;
13607
+ }
13608
+
13609
+ const view = new DataView(buffer);
13610
+ const metaLength = view.getUint32(0);
13611
+ if (!Number.isFinite(metaLength)
13612
+ || metaLength <= 0
13613
+ || metaLength > 64 * 1024
13614
+ || 4 + metaLength >= buffer.byteLength) {
13615
+ return null;
13616
+ }
13617
+
13618
+ const metadataBytes = new Uint8Array(buffer, 4, metaLength);
13619
+ const metadataText = new TextDecoder().decode(metadataBytes);
13620
+ const metadata = JSON.parse(metadataText);
13621
+ if (String(metadata?.type || '').toLowerCase() !== 'remote.frame.binary') {
13622
+ return null;
13623
+ }
13624
+
13625
+ const payload = buffer.slice(4 + metaLength);
13626
+ const mimeType = String(metadata.mimeType || metadata.format || 'image/jpeg').trim() || 'image/jpeg';
13627
+ const payloadBlob = new Blob([payload], { type: mimeType });
13628
+ const objectUrl = URL.createObjectURL(payloadBlob);
13629
+ return {
13630
+ ...metadata,
13631
+ kind: String(metadata.kind || 'live').toLowerCase() === 'thumbnail' ? 'thumbnail' : 'live',
13632
+ mimeType,
13633
+ frameUrl: objectUrl,
13634
+ framePath: objectUrl,
13635
+ dataUrl: '',
13636
+ _remoteFleetObjectUrl: objectUrl,
13637
+ _remoteFleetPayloadBlob: payloadBlob,
13638
+ _remoteFleetBinaryFrame: true
13639
+ };
13640
+ }
13641
+
13642
+ function releaseRemoteFleetFrameObjectUrl(preview, nextObjectUrl = '') {
13643
+ const previous = preview?._remoteFleetObjectUrl || '';
13644
+ if (previous && previous !== nextObjectUrl && typeof URL !== 'undefined' && typeof URL.revokeObjectURL === 'function') {
13645
+ try {
13646
+ URL.revokeObjectURL(previous);
13647
+ } catch {
13648
+ // Best-effort browser memory cleanup.
13649
+ }
13650
+ }
13651
+
13652
+ if (preview) {
13653
+ preview._remoteFleetObjectUrl = nextObjectUrl || '';
13654
+ }
13655
+ }
13656
+
13657
+ function getRemoteFleetBinaryFrameDeviceIds(sessionOrGetter) {
13658
+ const getDeviceIds = typeof sessionOrGetter === 'function'
13659
+ ? sessionOrGetter
13660
+ : sessionOrGetter?.getDeviceIds;
13661
+ const ids = typeof getDeviceIds === 'function'
13662
+ ? getDeviceIds()
13663
+ : [];
13664
+ const normalized = (Array.isArray(ids) ? ids : [])
13665
+ .map(value => String(value || '').trim())
13666
+ .filter(Boolean);
13667
+
13668
+ if (sessionOrGetter?.controlSessions instanceof Set) {
13669
+ sessionOrGetter.controlSessions.forEach(controlSession => {
13670
+ const deviceId = String(controlSession?.deviceId || '').trim();
13671
+ if (deviceId) {
13672
+ normalized.push(deviceId);
13673
+ }
13674
+ });
13675
+ }
13676
+
13677
+ return Array.from(new Set(normalized))
13678
+ .slice(0, 240);
13679
+ }
13680
+
13681
+ function refreshRemoteFleetBinaryFrameSubscription(session, force = false) {
13682
+ if (!session?.ws || session.ws.readyState !== WebSocket.OPEN) {
13683
+ return false;
13684
+ }
13685
+
13686
+ const deviceIds = getRemoteFleetBinaryFrameDeviceIds(session);
13687
+ const key = deviceIds.join('\n');
13688
+ if (!force && session.subscriptionKey === key) {
13689
+ return true;
13690
+ }
13691
+
13692
+ session.subscriptionKey = key;
13693
+ session.ws.send(JSON.stringify({
13694
+ type: 'subscribe',
13695
+ nodeId: session.nodeId,
13696
+ deviceIds
13697
+ }));
13698
+ return true;
13699
+ }
13700
+
13701
+ function scheduleRemoteFleetBinaryFrameReconnect(session) {
13702
+ if (!session || session.reconnectTimer || (session.bodies.size === 0 && (!session.controlSessions || session.controlSessions.size === 0))) {
13703
+ return;
13704
+ }
13705
+
13706
+ session.reconnectTimer = setTimeout(() => {
13707
+ session.reconnectTimer = null;
13708
+ openRemoteFleetBinaryFrameSocket(session);
13709
+ }, REMOTE_FLEET_BINARY_FRAME_WS_RECONNECT_MS);
13710
+ }
13711
+
13712
+ async function openRemoteFleetBinaryFrameSocket(session) {
13713
+ if (!session || (session.bodies.size === 0 && (!session.controlSessions || session.controlSessions.size === 0)) || typeof WebSocket !== 'function') {
13714
+ return false;
13715
+ }
13716
+
13717
+ if (session.ws && (session.ws.readyState === WebSocket.OPEN || session.ws.readyState === WebSocket.CONNECTING)) {
13718
+ refreshRemoteFleetBinaryFrameSubscription(session);
13719
+ return true;
13720
+ }
13721
+
13722
+ try {
13723
+ const status = await getRemoteFleetBridgeStatusForFrames();
13724
+ const wsUrl = buildRemoteFleetBinaryFrameWsUrl(status);
13725
+ if (!wsUrl) {
13726
+ throw new Error('remote-frame-ws-url-unavailable');
13727
+ }
13728
+
13729
+ const ws = new WebSocket(wsUrl);
13730
+ session.ws = ws;
13731
+ session.subscriptionKey = '';
13732
+ ws.binaryType = 'arraybuffer';
13733
+ ws.onopen = () => {
13734
+ window.RuntimeTrace?.emit?.('remote.frame.wsOpen', { nodeId: session.nodeId });
13735
+ refreshRemoteFleetBinaryFrameSubscription(session, true);
13736
+ if (!session.subscriptionTimer) {
13737
+ session.subscriptionTimer = setInterval(
13738
+ () => refreshRemoteFleetBinaryFrameSubscription(session),
13739
+ REMOTE_FLEET_BINARY_FRAME_SUBSCRIBE_MS
13740
+ );
13741
+ }
13742
+ };
13743
+ ws.onmessage = event => {
13744
+ if (typeof event.data === 'string') {
13745
+ window.RuntimeTrace?.emit?.('remote.frame.wsMeta', {
13746
+ nodeId: session.nodeId,
13747
+ message: event.data.slice(0, 120)
13748
+ });
13749
+ return;
13750
+ }
13751
+
13752
+ parseRemoteFleetBinaryFrameMessage(event.data)
13753
+ .then(frame => {
13754
+ if (!frame || (session.bodies.size === 0 && (!session.controlSessions || session.controlSessions.size === 0))) {
13755
+ if (frame?._remoteFleetObjectUrl && typeof URL !== 'undefined' && typeof URL.revokeObjectURL === 'function') {
13756
+ URL.revokeObjectURL(frame._remoteFleetObjectUrl);
13757
+ }
13758
+ return;
13759
+ }
13760
+
13761
+ let applied = 0;
13762
+ session.bodies.forEach(body => {
13763
+ if (document.body.contains(body)) {
13764
+ applied += applyRemoteFleetFramePatches(body, [frame]);
13765
+ }
13766
+ });
13767
+
13768
+ let controlPaintQueued = 0;
13769
+ if (session.controlSessions instanceof Set && session.controlSessions.size > 0) {
13770
+ session.controlSessions.forEach(controlSession => {
13771
+ if (!controlSession?.active) {
13772
+ session.controlSessions.delete(controlSession);
13773
+ return;
13774
+ }
13775
+
13776
+ if (String(controlSession.deviceId || '').trim() !== String(frame.deviceId || '').trim()) {
13777
+ return;
13778
+ }
13779
+
13780
+ controlPaintQueued += 1;
13781
+ paintRemoteFleetControlBinaryFrame(controlSession, frame)
13782
+ .catch(error => {
13783
+ window.RuntimeTrace?.emit?.('remote.control.binaryPaintFailed', {
13784
+ nodeId: session.nodeId,
13785
+ deviceId: controlSession.deviceId,
13786
+ error: error?.message || String(error || '')
13787
+ });
13788
+ });
13789
+ });
13790
+ }
13791
+
13792
+ const controlUsesPayloadBlob = controlPaintQueued > 0
13793
+ && !!frame._remoteFleetPayloadBlob
13794
+ && typeof createImageBitmap === 'function';
13795
+ if (applied <= 0
13796
+ && (controlPaintQueued <= 0 || controlUsesPayloadBlob)
13797
+ && frame._remoteFleetObjectUrl
13798
+ && typeof URL !== 'undefined'
13799
+ && typeof URL.revokeObjectURL === 'function') {
13800
+ URL.revokeObjectURL(frame._remoteFleetObjectUrl);
13801
+ }
13802
+ })
13803
+ .catch(error => {
13804
+ window.RuntimeTrace?.emit?.('remote.frame.wsParseFailed', {
13805
+ nodeId: session.nodeId,
13806
+ error: error?.message || String(error || '')
13807
+ });
13808
+ });
13809
+ };
13810
+ ws.onclose = () => {
13811
+ if (session.ws === ws) {
13812
+ session.ws = null;
13813
+ }
13814
+ window.RuntimeTrace?.emit?.('remote.frame.wsClosed', { nodeId: session.nodeId });
13815
+ scheduleRemoteFleetBinaryFrameReconnect(session);
13816
+ };
13817
+ ws.onerror = () => {
13818
+ try {
13819
+ ws.close();
13820
+ } catch {
13821
+ // Ignore WebSocket close failures.
13822
+ }
13823
+ };
13824
+ return true;
13825
+ } catch (error) {
13826
+ window.RuntimeTrace?.emit?.('remote.frame.wsOpenFailed', {
13827
+ nodeId: session.nodeId,
13828
+ error: error?.message || String(error || '')
13829
+ });
13830
+ scheduleRemoteFleetBinaryFrameReconnect(session);
13831
+ return false;
13832
+ }
13833
+ }
13834
+
13835
+ function startRemoteFleetBinaryFrameSocket(bodyView, getDeviceIds) {
13836
+ const nodeId = String(bodyView?.dataset?.nodeId || '').trim();
13837
+ if (!nodeId || typeof WebSocket !== 'function') {
13838
+ return false;
13839
+ }
13840
+
13841
+ let session = remoteFleetBinaryFrameSessions.get(nodeId);
13842
+ if (!session) {
13843
+ session = {
13844
+ nodeId,
13845
+ bodies: new Set(),
13846
+ ws: null,
13847
+ reconnectTimer: null,
13848
+ subscriptionTimer: null,
13849
+ subscriptionKey: '',
13850
+ controlSessions: new Set(),
13851
+ getDeviceIds: null
13852
+ };
13853
+ remoteFleetBinaryFrameSessions.set(nodeId, session);
13854
+ }
13855
+
13856
+ session.bodies.add(bodyView);
13857
+ session.getDeviceIds = getDeviceIds;
13858
+ bodyView._remoteFleetBinaryFrameSession = session;
13859
+ openRemoteFleetBinaryFrameSocket(session).catch(() => undefined);
13860
+ return true;
13861
+ }
13862
+
13863
+ function releaseRemoteFleetBinaryFrameSocket(bodyView) {
13864
+ const session = bodyView?._remoteFleetBinaryFrameSession;
13865
+ if (!session) {
13866
+ return;
13867
+ }
13868
+
13869
+ delete bodyView._remoteFleetBinaryFrameSession;
13870
+ session.bodies.delete(bodyView);
13871
+ if (session.bodies.size > 0 || (session.controlSessions instanceof Set && session.controlSessions.size > 0)) {
13872
+ refreshRemoteFleetBinaryFrameSubscription(session, true);
13873
+ return;
13874
+ }
13875
+
13876
+ if (session.reconnectTimer) {
13877
+ clearTimeout(session.reconnectTimer);
13878
+ session.reconnectTimer = null;
13879
+ }
13880
+ if (session.subscriptionTimer) {
13881
+ clearInterval(session.subscriptionTimer);
13882
+ session.subscriptionTimer = null;
13883
+ }
13884
+ if (session.ws) {
13885
+ try {
13886
+ session.ws.close();
13887
+ } catch {
13888
+ // Ignore WebSocket close failures.
13889
+ }
13890
+ session.ws = null;
13891
+ }
13892
+ remoteFleetBinaryFrameSessions.delete(session.nodeId);
13893
+ }
13894
+
13522
13895
  function requestRemoteFleetFrameLoopFrame(callback) {
13523
13896
  if (typeof requestAnimationFrame === 'function') {
13524
13897
  return {
@@ -14021,6 +14394,7 @@
14021
14394
  }
14022
14395
 
14023
14396
  const nextSeq = Number(frame.frameSeq || 0);
14397
+ releaseRemoteFleetFrameObjectUrl(preview, frame._remoteFleetObjectUrl || '');
14024
14398
  preview.dataset.remoteFleetFrameKind = frame.kind;
14025
14399
  preview.dataset.remoteFleetFrameSeq = String(nextSeq || 0);
14026
14400
  preview.dataset.remoteFleetFrameUrl = frame.frameUrl;
@@ -14529,6 +14903,7 @@
14529
14903
 
14530
14904
  activeRemoteFleetControlPopup = null;
14531
14905
  session.active = false;
14906
+ detachRemoteFleetControlBinaryFrameSession(session);
14532
14907
  if (session.timer) {
14533
14908
  clearTimeout(session.timer);
14534
14909
  session.timer = null;
@@ -14550,6 +14925,66 @@
14550
14925
  }
14551
14926
  }
14552
14927
 
14928
+ function attachRemoteFleetControlBinaryFrameSession(controlSession, bodyView) {
14929
+ if (!controlSession?.active || !bodyView || typeof WebSocket !== 'function') {
14930
+ return false;
14931
+ }
14932
+
14933
+ let binarySession = bodyView._remoteFleetBinaryFrameSession || null;
14934
+ if (!binarySession) {
14935
+ if (!startRemoteFleetBinaryFrameSocket(bodyView, () => [])) {
14936
+ return false;
14937
+ }
14938
+ binarySession = bodyView._remoteFleetBinaryFrameSession || null;
14939
+ }
14940
+
14941
+ if (!binarySession) {
14942
+ return false;
14943
+ }
14944
+
14945
+ if (!(binarySession.controlSessions instanceof Set)) {
14946
+ binarySession.controlSessions = new Set();
14947
+ }
14948
+
14949
+ binarySession.controlSessions.add(controlSession);
14950
+ controlSession.binaryFrameSession = binarySession;
14951
+ controlSession.binaryFramePreferred = true;
14952
+ refreshRemoteFleetBinaryFrameSubscription(binarySession, true);
14953
+ openRemoteFleetBinaryFrameSocket(binarySession).catch(() => undefined);
14954
+ return true;
14955
+ }
14956
+
14957
+ function detachRemoteFleetControlBinaryFrameSession(controlSession) {
14958
+ const binarySession = controlSession?.binaryFrameSession;
14959
+ if (!binarySession?.controlSessions) {
14960
+ return;
14961
+ }
14962
+
14963
+ binarySession.controlSessions.delete(controlSession);
14964
+ delete controlSession.binaryFrameSession;
14965
+ refreshRemoteFleetBinaryFrameSubscription(binarySession, true);
14966
+
14967
+ if (binarySession.bodies.size === 0 && binarySession.controlSessions.size === 0) {
14968
+ if (binarySession.reconnectTimer) {
14969
+ clearTimeout(binarySession.reconnectTimer);
14970
+ binarySession.reconnectTimer = null;
14971
+ }
14972
+ if (binarySession.subscriptionTimer) {
14973
+ clearInterval(binarySession.subscriptionTimer);
14974
+ binarySession.subscriptionTimer = null;
14975
+ }
14976
+ if (binarySession.ws) {
14977
+ try {
14978
+ binarySession.ws.close();
14979
+ } catch {
14980
+ // Ignore WebSocket close failures.
14981
+ }
14982
+ binarySession.ws = null;
14983
+ }
14984
+ remoteFleetBinaryFrameSessions.delete(binarySession.nodeId);
14985
+ }
14986
+ }
14987
+
14553
14988
  async function paintRemoteFleetControlFrame(session, frame) {
14554
14989
  if (!session?.active || !frame || !isRemoteFleetFrameSource(frame.frameUrl)) {
14555
14990
  return false;
@@ -14581,6 +15016,59 @@
14581
15016
  }
14582
15017
  }
14583
15018
 
15019
+ async function paintRemoteFleetControlBinaryFrame(session, frame) {
15020
+ if (!session?.active || !frame || String(frame.kind || '').toLowerCase() !== 'live') {
15021
+ return false;
15022
+ }
15023
+
15024
+ const frameSeq = Number(frame.frameSeq || frame.FrameSeq || 0) || 0;
15025
+ if (frameSeq > 0 && frameSeq < (session.lastControlFrameSeq || 0)) {
15026
+ return false;
15027
+ }
15028
+
15029
+ if (frameSeq > 0) {
15030
+ session.lastControlFrameSeq = frameSeq;
15031
+ }
15032
+
15033
+ let bitmap = null;
15034
+ try {
15035
+ if (frame._remoteFleetPayloadBlob && typeof createImageBitmap === 'function') {
15036
+ bitmap = await createImageBitmap(frame._remoteFleetPayloadBlob);
15037
+ } else if (isRemoteFleetFrameSource(frame.frameUrl)) {
15038
+ bitmap = await loadRemoteFleetFrameBitmap(frame);
15039
+ }
15040
+
15041
+ if (!bitmap || !session.active) {
15042
+ return false;
15043
+ }
15044
+
15045
+ requestRemoteFleetFrameLoopFrame(() => {
15046
+ if (!session.active
15047
+ || !document.body.contains(session.overlay)
15048
+ || (frameSeq > 0 && frameSeq < (session.lastControlFrameSeq || 0))) {
15049
+ if (typeof bitmap.close === 'function') {
15050
+ bitmap.close();
15051
+ }
15052
+ return;
15053
+ }
15054
+
15055
+ drawRemoteFleetFrameToCanvas(session.canvas, bitmap, 'contain');
15056
+ session.canvas.style.display = 'block';
15057
+ session.lastControlFrameAt = Date.now();
15058
+ session.status.textContent = `Control ${REMOTE_FLEET_CONTROL_LIVE_FPS}fps`;
15059
+ if (typeof bitmap.close === 'function') {
15060
+ bitmap.close();
15061
+ }
15062
+ });
15063
+ return true;
15064
+ } catch {
15065
+ if (bitmap && typeof bitmap.close === 'function') {
15066
+ bitmap.close();
15067
+ }
15068
+ return false;
15069
+ }
15070
+ }
15071
+
14584
15072
  function sendRemoteFleetControlInput(session, payload, throttleMove = false) {
14585
15073
  if (!session?.active || !payload?.type) {
14586
15074
  return;
@@ -14901,16 +15389,29 @@
14901
15389
  timer: null,
14902
15390
  moveTimer: null,
14903
15391
  pendingMove: null,
14904
- lastMoveSentAt: 0
15392
+ lastMoveSentAt: 0,
15393
+ lastControlFrameAt: 0,
15394
+ lastControlFrameSeq: 0,
15395
+ binaryFramePreferred: false
14905
15396
  };
14906
15397
  activeRemoteFleetControlPopup = session;
14907
15398
  bindRemoteFleetControlInput(session);
15399
+ attachRemoteFleetControlBinaryFrameSession(session, bodyView);
14908
15400
 
14909
15401
  const refresh = async () => {
14910
15402
  if (!session.active) {
14911
15403
  return;
14912
15404
  }
14913
15405
 
15406
+ const now = Date.now();
15407
+ const hasFreshBinaryFrame = session.binaryFramePreferred === true
15408
+ && session.lastControlFrameAt > 0
15409
+ && now - session.lastControlFrameAt < 1200;
15410
+ if (hasFreshBinaryFrame) {
15411
+ session.timer = setTimeout(refresh, 500);
15412
+ return;
15413
+ }
15414
+
14914
15415
  try {
14915
15416
  const result = await invokeDotNetAsync('GetRemoteFleetFrameFromJs', nodeId, targetId, 'live');
14916
15417
  const frame = normalizeRemoteFleetFramePayload(result?.frame || result?.Frame, 'live');
@@ -14918,7 +15419,8 @@
14918
15419
  frame.deviceId = frame.deviceId || targetId;
14919
15420
  await paintRemoteFleetControlFrame(session, frame);
14920
15421
  applyRemoteFleetFramePatches(bodyView, [frame]);
14921
- status.textContent = 'Control';
15422
+ session.lastControlFrameAt = Date.now();
15423
+ status.textContent = session.binaryFramePreferred ? 'Control fallback' : `Control ${REMOTE_FLEET_CONTROL_LIVE_FPS}fps`;
14922
15424
  } else if (result?.error || result?.Error) {
14923
15425
  status.textContent = result.error || result.Error;
14924
15426
  }
@@ -14926,7 +15428,7 @@
14926
15428
  status.textContent = error?.message || 'Frame failed';
14927
15429
  } finally {
14928
15430
  if (session.active) {
14929
- session.timer = setTimeout(refresh, REMOTE_FLEET_LIVE_FRAME_REFRESH_MS);
15431
+ session.timer = setTimeout(refresh, REMOTE_FLEET_CONTROL_FRAME_REFRESH_MS);
14930
15432
  }
14931
15433
  }
14932
15434
  };
@@ -14950,7 +15452,7 @@
14950
15452
  deviceId: targetId
14951
15453
  });
14952
15454
 
14953
- invokeDotNetAsync('StartRemoteFleetLiveStreamFromJs', nodeId, targetId)
15455
+ invokeDotNetAsync('StartRemoteFleetLiveStreamFromJs', nodeId, targetId, REMOTE_FLEET_CONTROL_LIVE_FPS)
14954
15456
  .then(async result => {
14955
15457
  if (!session.active) {
14956
15458
  return;
@@ -14959,7 +15461,7 @@
14959
15461
  await syncRemoteFleetNodeStateFromResult(result);
14960
15462
  session.startedStream = result?.success === true || result?.Success === true;
14961
15463
  session.streamId = String(result?.streamId || result?.StreamId || '');
14962
- status.textContent = session.startedStream ? 'Control' : (result?.error || result?.Error || 'View');
15464
+ status.textContent = session.startedStream ? `Control ${REMOTE_FLEET_CONTROL_LIVE_FPS}fps` : (result?.error || result?.Error || 'View');
14963
15465
  refresh();
14964
15466
  })
14965
15467
  .catch(error => {
@@ -17086,7 +17588,13 @@
17086
17588
  error: error?.message || String(error || '')
17087
17589
  });
17088
17590
  });
17089
- startRemoteFleetFrameLoop(bodyView, REMOTE_FLEET_LIVE_FRAME_REFRESH_MS, () => refreshRemoteFleetVisibleLiveFrames());
17591
+ const binaryFrameSocketStarted = startRemoteFleetBinaryFrameSocket(bodyView, () => {
17592
+ const liveIds = getVisibleLiveFrameDeviceIds();
17593
+ return liveIds.length > 0 ? liveIds : getVisibleFrameDeviceIds();
17594
+ });
17595
+ if (!binaryFrameSocketStarted) {
17596
+ startRemoteFleetFrameLoop(bodyView, REMOTE_FLEET_LIVE_FRAME_REFRESH_MS, () => refreshRemoteFleetVisibleLiveFrames());
17597
+ }
17090
17598
  bodyView._remoteFleetLiveRefreshTimer = setInterval(async () => {
17091
17599
  if (!document.body.contains(bodyView)) {
17092
17600
  clearRemoteFleetTimers(bodyView);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "mainAssemblyName": "MindExecution.Web",
3
3
  "resources": {
4
- "hash": "sha256-8Fvmr2gwPca8frlDucd8gkb1BGymLVRCbTINI76yI5s=",
4
+ "hash": "sha256-JxTp+hXYDhd47gICFMzpCDWm8b4z1JGet67SEEI1F3c=",
5
5
  "fingerprinting": {
6
6
  "Google.Protobuf.9h59ukbel7.dll": "Google.Protobuf.dll",
7
7
  "Markdig.d1j7v41cl1.dll": "Markdig.dll",
@@ -127,12 +127,12 @@
127
127
  "MindExecution.Kernel.pbzp3jfync.dll": "MindExecution.Kernel.dll",
128
128
  "MindExecution.Plugins.Admin.gxzwlji1cf.dll": "MindExecution.Plugins.Admin.dll",
129
129
  "MindExecution.Plugins.Business.vtaey8c59y.dll": "MindExecution.Plugins.Business.dll",
130
- "MindExecution.Plugins.Concept.e1j4pkp0uf.dll": "MindExecution.Plugins.Concept.dll",
130
+ "MindExecution.Plugins.Concept.85y7un1ks6.dll": "MindExecution.Plugins.Concept.dll",
131
131
  "MindExecution.Plugins.Directory.zc8ffaoknd.dll": "MindExecution.Plugins.Directory.dll",
132
- "MindExecution.Plugins.PlanMaster.yfeqppy3kf.dll": "MindExecution.Plugins.PlanMaster.dll",
133
- "MindExecution.Plugins.YouTube.vzjd4jwanl.dll": "MindExecution.Plugins.YouTube.dll",
134
- "MindExecution.Shared.6xjhkrpfwd.dll": "MindExecution.Shared.dll",
135
- "MindExecution.Web.03g8r8265y.dll": "MindExecution.Web.dll",
132
+ "MindExecution.Plugins.PlanMaster.ylooagumgh.dll": "MindExecution.Plugins.PlanMaster.dll",
133
+ "MindExecution.Plugins.YouTube.azfozpumhv.dll": "MindExecution.Plugins.YouTube.dll",
134
+ "MindExecution.Shared.mppf8quyau.dll": "MindExecution.Shared.dll",
135
+ "MindExecution.Web.tln762tijf.dll": "MindExecution.Web.dll",
136
136
  "dotnet.js": "dotnet.js",
137
137
  "dotnet.native.qc8g39g30v.js": "dotnet.native.js",
138
138
  "dotnet.native.boem75ye5i.wasm": "dotnet.native.wasm",
@@ -280,16 +280,16 @@
280
280
  "netstandard.yvr3prsx0x.dll": "sha256-EksNn8Luo4bOWqJ6X7dIe9qG9oOqwOVzjH2xYyMNi+E=",
281
281
  "MindExecution.Core.fc9cjbjplq.dll": "sha256-TymUFwryFLdbCYy2U5qdgE2ZV1yHaLR9bgyrmx6Tcp0=",
282
282
  "MindExecution.Kernel.pbzp3jfync.dll": "sha256-Ff6WHyLD39V0fEjUiQlxX9Y+edssyTH+7T4zfBdj3s0=",
283
- "MindExecution.Plugins.Concept.e1j4pkp0uf.dll": "sha256-nq1Uv/H+JSyGBzGym6AKbVMSa53sktNACog3+JqITI4=",
284
- "MindExecution.Plugins.PlanMaster.yfeqppy3kf.dll": "sha256-YpcXMAmctAWAbfzFrfcYmk3sOo7j8hZGGKsrv4VgqUs=",
285
- "MindExecution.Shared.6xjhkrpfwd.dll": "sha256-5lCRRlgeky/uSmxPrO5acuFLOjfRphWQwSyVY5E1Tq8=",
286
- "MindExecution.Web.03g8r8265y.dll": "sha256-lEHwMVSR1YHsNoejZYt53SRBrYrN/4Ak7dqMqNEnXrg="
283
+ "MindExecution.Plugins.Concept.85y7un1ks6.dll": "sha256-ZK10a6NfAZTCKX0BpiH6e+pBatMwTeXMCtA79A+zGQg=",
284
+ "MindExecution.Plugins.PlanMaster.ylooagumgh.dll": "sha256-aoNSkErjCAqRgET/An00bm2GP3Jsnnu4hz8e/jpaLcI=",
285
+ "MindExecution.Shared.mppf8quyau.dll": "sha256-FsB415v0PlO8/788VKC99rE/BhViR61KcM9+Crxo9Ok=",
286
+ "MindExecution.Web.tln762tijf.dll": "sha256-vy73kyPmRU7hqhUFOFs9fdsn5AWmO7cNTRDoWigkc2M="
287
287
  },
288
288
  "lazyAssembly": {
289
289
  "MindExecution.Plugins.Admin.gxzwlji1cf.dll": "sha256-5D5B2ZuUMj46zBMgH2dRA7CbQf++uukMPrEliFLuZWs=",
290
290
  "MindExecution.Plugins.Business.vtaey8c59y.dll": "sha256-rU9MzRRmHuj0/IluV1dvE6x3aRCK/DA4DWKy1DO4VVg=",
291
291
  "MindExecution.Plugins.Directory.zc8ffaoknd.dll": "sha256-Ey/HajaVuBxB/Ou4qsM70nhAZBoODeCENzG1J/uEsxs=",
292
- "MindExecution.Plugins.YouTube.vzjd4jwanl.dll": "sha256-e9Z1x+BJvcFG38RiQW4jiGg3wFvgH6TBjYIDXPBxOPk="
292
+ "MindExecution.Plugins.YouTube.azfozpumhv.dll": "sha256-GjL5SM7+D8SKJBE528Y5Py29kQrTXPmG5OFr6+E0L4s="
293
293
  }
294
294
  },
295
295
  "cacheBootResources": true,
@@ -7,8 +7,8 @@
7
7
  <title>MindExec | Run your ideas as AI task graphs</title>
8
8
  <meta name="description" content="MindExec is an AI execution canvas for solo builders, researchers, developers, and creators. Start with free browser tools, then move serious work into saved MindCanvas projects." />
9
9
  <base href="/" />
10
- <link rel="stylesheet" href="_content/MindExecution.Shared/css/app.css?v=20260616-remote-agent-sticky-overlay-frames-v571" />
11
- <link rel="stylesheet" href="_content/MindExecution.Shared/css/mind-map-overrides.css?v=20260616-remote-agent-sticky-overlay-frames-v571" />
10
+ <link rel="stylesheet" href="_content/MindExecution.Shared/css/app.css?v=20260616-remote-ws-control-frames-v573" />
11
+ <link rel="stylesheet" href="_content/MindExecution.Shared/css/mind-map-overrides.css?v=20260616-remote-ws-control-frames-v573" />
12
12
  <!-- ?쇄뼹??Font Awesome (local) ?쇄뼹??-->
13
13
  <link rel="stylesheet" href="_content/MindExecution.Shared/lib/font-awesome/css/all.min.css" />
14
14
  <!-- ?꿎뼯??-->
@@ -579,7 +579,7 @@
579
579
  }
580
580
 
581
581
  const base = '_content/MindExecution.Shared/js/';
582
- const scriptVersion = '20260616-remote-agent-sticky-overlay-frames-v571';
582
+ const scriptVersion = '20260616-remote-ws-control-frames-v573';
583
583
  const scriptUrl = (script) => `${base}${script}?v=${scriptVersion}`;
584
584
  console.log(`[Script Loader] Shared JS version: ${scriptVersion}`);
585
585
  const criticalScripts = [
@@ -1,5 +1,5 @@
1
1
  self.assetsManifest = {
2
- "version": "WOO5ml3v",
2
+ "version": "pV7f33KI",
3
3
  "assets": [
4
4
  {
5
5
  "hash": "sha256-+CSYMcqLNTsq3VnH11jgYyOCCdxvHzL74CBmo4sCmMU=",
@@ -78,7 +78,7 @@
78
78
  "url": "_content/MindExecution.Shared/js/marked.min.js"
79
79
  },
80
80
  {
81
- "hash": "sha256-UtXpHLTnYeP2BPz7IAicclCMMdLjzYd29I+u5rMi270=",
81
+ "hash": "sha256-oDfBxq3aqmSU1z5MGGpE9ZeTdsBtrykHEBnygOVFJtE=",
82
82
  "url": "_content/MindExecution.Shared/js/mind-map-core.js"
83
83
  },
84
84
  {
@@ -86,7 +86,7 @@
86
86
  "url": "_content/MindExecution.Shared/js/mind-map-core.js.backup"
87
87
  },
88
88
  {
89
- "hash": "sha256-itUEN+HfFS2lC/aHq02RNF70gDWhggsJok0F3uOLHT0=",
89
+ "hash": "sha256-8uQGgnoT3SXKM89hTz49ThSpGgT88GRP7ipe8xlR3z0=",
90
90
  "url": "_content/MindExecution.Shared/js/mind-map-css3d-manager.js"
91
91
  },
92
92
  {
@@ -426,28 +426,28 @@
426
426
  "url": "_framework/MindExecution.Plugins.Business.vtaey8c59y.dll"
427
427
  },
428
428
  {
429
- "hash": "sha256-nq1Uv/H+JSyGBzGym6AKbVMSa53sktNACog3+JqITI4=",
430
- "url": "_framework/MindExecution.Plugins.Concept.e1j4pkp0uf.dll"
429
+ "hash": "sha256-ZK10a6NfAZTCKX0BpiH6e+pBatMwTeXMCtA79A+zGQg=",
430
+ "url": "_framework/MindExecution.Plugins.Concept.85y7un1ks6.dll"
431
431
  },
432
432
  {
433
433
  "hash": "sha256-Ey/HajaVuBxB/Ou4qsM70nhAZBoODeCENzG1J/uEsxs=",
434
434
  "url": "_framework/MindExecution.Plugins.Directory.zc8ffaoknd.dll"
435
435
  },
436
436
  {
437
- "hash": "sha256-YpcXMAmctAWAbfzFrfcYmk3sOo7j8hZGGKsrv4VgqUs=",
438
- "url": "_framework/MindExecution.Plugins.PlanMaster.yfeqppy3kf.dll"
437
+ "hash": "sha256-aoNSkErjCAqRgET/An00bm2GP3Jsnnu4hz8e/jpaLcI=",
438
+ "url": "_framework/MindExecution.Plugins.PlanMaster.ylooagumgh.dll"
439
439
  },
440
440
  {
441
- "hash": "sha256-e9Z1x+BJvcFG38RiQW4jiGg3wFvgH6TBjYIDXPBxOPk=",
442
- "url": "_framework/MindExecution.Plugins.YouTube.vzjd4jwanl.dll"
441
+ "hash": "sha256-GjL5SM7+D8SKJBE528Y5Py29kQrTXPmG5OFr6+E0L4s=",
442
+ "url": "_framework/MindExecution.Plugins.YouTube.azfozpumhv.dll"
443
443
  },
444
444
  {
445
- "hash": "sha256-5lCRRlgeky/uSmxPrO5acuFLOjfRphWQwSyVY5E1Tq8=",
446
- "url": "_framework/MindExecution.Shared.6xjhkrpfwd.dll"
445
+ "hash": "sha256-FsB415v0PlO8/788VKC99rE/BhViR61KcM9+Crxo9Ok=",
446
+ "url": "_framework/MindExecution.Shared.mppf8quyau.dll"
447
447
  },
448
448
  {
449
- "hash": "sha256-lEHwMVSR1YHsNoejZYt53SRBrYrN/4Ak7dqMqNEnXrg=",
450
- "url": "_framework/MindExecution.Web.03g8r8265y.dll"
449
+ "hash": "sha256-vy73kyPmRU7hqhUFOFs9fdsn5AWmO7cNTRDoWigkc2M=",
450
+ "url": "_framework/MindExecution.Web.tln762tijf.dll"
451
451
  },
452
452
  {
453
453
  "hash": "sha256-IsZJ91/OW+fHzNqIgEc7Y072ns8z9dGritiSyvR9Wgc=",
@@ -770,7 +770,7 @@
770
770
  "url": "_framework/Websocket.Client.vapounvmnl.dll"
771
771
  },
772
772
  {
773
- "hash": "sha256-pATnlidoWPOV2BpRonMlb3bDjtALaA8sXI+DiPFY4/0=",
773
+ "hash": "sha256-HDeRZ1juous1GiTsZw+QLukbDxOOYjh72G4Dta2w5Fc=",
774
774
  "url": "_framework/blazor.boot.json"
775
775
  },
776
776
  {
@@ -834,7 +834,7 @@
834
834
  "url": "image-manifest.json"
835
835
  },
836
836
  {
837
- "hash": "sha256-fojDZj+Dr+k8SI1esa0xdrCRBfXVHxr+cLNb3bvD7t8=",
837
+ "hash": "sha256-VJOyHChPIe+gqQk3r5g4qTjsKYTrLXoP6I5L+o4ZYzE=",
838
838
  "url": "index.html"
839
839
  },
840
840
  {
@@ -1,4 +1,4 @@
1
- /* Manifest version: WOO5ml3v */
1
+ /* Manifest version: pV7f33KI */
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