@mindexec/cli 0.2.157 → 0.2.159

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.157",
3
+ "version": "0.2.159",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -585,15 +585,15 @@ function encodeRemoteBinaryFrame(metadata, payload = new Uint8Array([0xff, 0xd8,
585
585
  return buffer;
586
586
  }
587
587
 
588
- function buildMonitorNode(devices, hubStatus, latestTaskBatch = null, recentTaskBatches = [], lastError = '') {
588
+ function buildMonitorNode(devices, hubStatus, latestTaskBatch = null, recentTaskBatches = [], lastError = '', nodeId = 'remote-fleet-render-smoke') {
589
589
  const connected = devices.filter(device => device.Connected).length;
590
590
  const endpoint = hubStatus.agentEndpoint || '127.0.0.1:5197';
591
591
  const pairToken = hubStatus.pairToken || 'render-smoke-token';
592
592
  const hostTargetState = hubStatus.hostTargetActive
593
- ? (hubStatus.hostTargetNodeId === 'remote-fleet-render-smoke' ? 'hosting' : 'other-monitor')
593
+ ? (hubStatus.hostTargetNodeId === nodeId ? 'hosting' : 'other-monitor')
594
594
  : 'inactive';
595
595
  return {
596
- id: 'remote-fleet-render-smoke',
596
+ id: nodeId,
597
597
  contentType: 'templateLauncher',
598
598
  metadata: {
599
599
  SemanticType: 'RemoteFleetMonitor',
@@ -625,10 +625,10 @@ function buildMonitorNode(devices, hubStatus, latestTaskBatch = null, recentTask
625
625
  };
626
626
  }
627
627
 
628
- function createRemoteFleetTemplateShell(document, focusDeviceId = '') {
628
+ function createRemoteFleetTemplateShell(document, focusDeviceId = '', nodeId = 'remote-fleet-render-smoke') {
629
629
  const nodeShell = document.createElement('div');
630
630
  nodeShell.setAttribute('class', 'map-node-template-card map-node-remote-fleet');
631
- nodeShell.dataset.nodeId = 'remote-fleet-render-smoke';
631
+ nodeShell.dataset.nodeId = nodeId;
632
632
  const shell = document.createElement('div');
633
633
  shell.setAttribute('class', 'template-card__shell template-card__shell--remote-fleet');
634
634
  const header = document.createElement('div');
@@ -642,7 +642,7 @@ function createRemoteFleetTemplateShell(document, focusDeviceId = '') {
642
642
  header.appendChild(titleWrap);
643
643
  const bodyView = document.createElement('div');
644
644
  bodyView.setAttribute('class', 'template-card__remote-fleet-body map-node-remote-fleet__body');
645
- bodyView.dataset.nodeId = 'remote-fleet-render-smoke';
645
+ bodyView.dataset.nodeId = nodeId;
646
646
  bodyView.dataset.remoteFleetAutoMonitor = 'false';
647
647
  if (focusDeviceId) {
648
648
  bodyView.dataset.remoteFleetFocusDeviceId = focusDeviceId;
@@ -1043,6 +1043,7 @@ try {
1043
1043
  binaryDrawCalls.some(call => call.op === 'clearRect' || call.op === 'fillRect'),
1044
1044
  false,
1045
1045
  'binary Blob frame paint must not clear/fill the canvas before drawing');
1046
+ const binaryCanvasBeforeRerender = binaryCanvas;
1046
1047
 
1047
1048
  const liveOverlayCard = document.createElement('div');
1048
1049
  liveOverlayCard.dataset.nodeId = 'remote-fleet-render-smoke';
@@ -1105,7 +1106,42 @@ try {
1105
1106
  ?.querySelector('[data-remote-fleet-device-preview="tile"]');
1106
1107
  assert.ok(rerenderedPatchPreview);
1107
1108
  assert.equal(rerenderedPatchPreview.dataset.remoteFleetFrameSeq, String(websocketFrameSeq));
1108
- assert.equal(rerenderedPatchPreview.querySelector('canvas[data-remote-fleet-frame-canvas="true"]')?.style.display, 'block');
1109
+ const rerenderedPatchCanvas = rerenderedPatchPreview.querySelector('canvas[data-remote-fleet-frame-canvas="true"]');
1110
+ assert.equal(rerenderedPatchCanvas?.style.display, 'block');
1111
+ assert.equal(
1112
+ rerenderedPatchCanvas,
1113
+ binaryCanvasBeforeRerender,
1114
+ 'MDM rerender should reuse the already-painted canvas instead of flashing a blank replacement');
1115
+
1116
+ const otherBoardNodeId = 'remote-fleet-render-smoke-other-board';
1117
+ const { nodeShell: otherBoardShell, bodyView: otherBoardBodyView } =
1118
+ createRemoteFleetTemplateShell(document, focusedDevice.DeviceId, otherBoardNodeId);
1119
+ document.body.appendChild(otherBoardShell);
1120
+ manager.renderRemoteFleetMonitorForTest(
1121
+ otherBoardBodyView,
1122
+ buildMonitorNode(
1123
+ devices,
1124
+ hub.getStatus({ includeSecrets: true }),
1125
+ latestTaskBatch,
1126
+ recentTaskBatches,
1127
+ '',
1128
+ otherBoardNodeId));
1129
+ await wait(30);
1130
+ const otherBoardPatchPreview = otherBoardBodyView
1131
+ .querySelector(`article[data-device-id="${patchTarget.DeviceId}"]`)
1132
+ ?.querySelector('[data-remote-fleet-device-preview="tile"]');
1133
+ assert.ok(otherBoardPatchPreview);
1134
+ assert.equal(
1135
+ otherBoardPatchPreview.dataset.remoteFleetFrameSeq,
1136
+ String(websocketFrameSeq),
1137
+ 'a different-board MDM should prime from the device-global latest binary frame cache');
1138
+ assert.equal(
1139
+ otherBoardPatchPreview.querySelector('canvas[data-remote-fleet-frame-canvas="true"]')?.style.display,
1140
+ 'block');
1141
+ assert.ok(diagnostics.runtimeTrace.some(event =>
1142
+ event.type === 'remote.frame.wsCachePrimed'
1143
+ && event.nodeId === otherBoardNodeId));
1144
+ otherBoardShell.remove();
1109
1145
 
1110
1146
  const controlCard = bodyView.querySelector(`article[data-device-id="${patchTarget.DeviceId}"]`);
1111
1147
  assert.ok(controlCard);
@@ -1404,6 +1440,18 @@ try {
1404
1440
  assert.ok(deviceBody.querySelector('[data-remote-fleet-action="task-device"]'));
1405
1441
  assert.ok(deviceBody.textContent.includes('Task timed out waiting for the agent response.'));
1406
1442
  assert.ok(deviceBody.textContent.includes(focusedDevice.DeviceId));
1443
+ const controlCanvasBeforeRerender = deviceControlCanvas;
1444
+ manager.renderRemoteFleetDeviceForTest(deviceBody, buildDeviceNode(focusedDevice, hub.getStatus()));
1445
+ await wait(30);
1446
+ const controlCanvasAfterRerender = deviceBody.querySelector('[data-remote-fleet-control-node-canvas="true"]');
1447
+ assert.equal(
1448
+ controlCanvasAfterRerender,
1449
+ controlCanvasBeforeRerender,
1450
+ 'RemoteFleetDevice rerender should reuse the already-painted control canvas instead of flashing black');
1451
+ assert.equal(
1452
+ deviceBody.querySelector('[data-remote-fleet-control-node-placeholder="true"]'),
1453
+ null,
1454
+ 'RemoteFleetDevice rerender should not cover a restored control canvas with a waiting placeholder');
1407
1455
 
1408
1456
  nodeShell.remove();
1409
1457
  emptyShell.remove();
@@ -13511,6 +13511,7 @@
13511
13511
  const REMOTE_FLEET_BINARY_FRAME_CONNECT_GRACE_MS = 1600;
13512
13512
  const REMOTE_FLEET_BINARY_FRAME_CACHE_MS = 10000;
13513
13513
  const REMOTE_FLEET_BINARY_FRAME_CACHE_LIMIT = 240;
13514
+ const REMOTE_FLEET_BINARY_FRAME_SESSION_IDLE_MS = 8000;
13514
13515
  const REMOTE_FLEET_AUTO_LIVE_START_MAX_PARALLEL = 8;
13515
13516
  const REMOTE_FLEET_AUTO_LIVE_START_COOLDOWN_MS = 3000;
13516
13517
  const REMOTE_FLEET_TASK_FOLLOW_INITIAL_MS = 250;
@@ -13522,6 +13523,7 @@
13522
13523
  const remoteFleetHostLeaseTimers = new Map();
13523
13524
  const remoteFleetLocalHostTargets = new Map();
13524
13525
  const remoteFleetBinaryFrameSessions = new Map();
13526
+ const remoteFleetLatestBinaryFrames = new Map();
13525
13527
  const remoteFleetFrameBlobCache = new Map();
13526
13528
  let remoteFleetBridgeStatusPromise = null;
13527
13529
  let activeRemoteFleetControlPopup = null;
@@ -13948,36 +13950,85 @@
13948
13950
  }
13949
13951
  }
13950
13952
 
13953
+ function pruneRemoteFleetGlobalBinaryFrameCache() {
13954
+ const now = getRemoteFleetFrameNow();
13955
+ for (const [deviceId, entry] of remoteFleetLatestBinaryFrames) {
13956
+ if (!deviceId
13957
+ || !entry
13958
+ || !entry.frame
13959
+ || now - Number(entry.seenAt || 0) > REMOTE_FLEET_BINARY_FRAME_CACHE_MS
13960
+ || remoteFleetLatestBinaryFrames.size > REMOTE_FLEET_BINARY_FRAME_CACHE_LIMIT) {
13961
+ remoteFleetLatestBinaryFrames.delete(deviceId);
13962
+ }
13963
+ }
13964
+ }
13965
+
13951
13966
  function rememberRemoteFleetBinaryFrame(session, frame) {
13952
13967
  const deviceId = String(frame?.deviceId || '').trim();
13953
- if (!session || !deviceId || !frame?._remoteFleetPayloadBlob) {
13968
+ if (!deviceId || !frame?._remoteFleetPayloadBlob) {
13954
13969
  return;
13955
13970
  }
13956
13971
 
13957
- if (!(session.latestBinaryFrames instanceof Map)) {
13958
- session.latestBinaryFrames = new Map();
13959
- }
13960
-
13961
- pruneRemoteFleetBinaryFrameCache(session);
13962
- session.latestBinaryFrames.set(deviceId, {
13972
+ const entry = {
13963
13973
  seenAt: getRemoteFleetFrameNow(),
13964
13974
  frame: cloneRemoteFleetFramePatch(frame)
13965
- });
13975
+ };
13976
+
13977
+ if (session) {
13978
+ if (!(session.latestBinaryFrames instanceof Map)) {
13979
+ session.latestBinaryFrames = new Map();
13980
+ }
13981
+
13982
+ pruneRemoteFleetBinaryFrameCache(session);
13983
+ session.latestBinaryFrames.set(deviceId, {
13984
+ seenAt: entry.seenAt,
13985
+ frame: cloneRemoteFleetFramePatch(frame)
13986
+ });
13987
+ }
13988
+
13989
+ pruneRemoteFleetGlobalBinaryFrameCache();
13990
+ remoteFleetLatestBinaryFrames.set(deviceId, entry);
13966
13991
  }
13967
13992
 
13968
13993
  function applyRemoteFleetBinaryFrameCacheToBody(bodyView, session) {
13969
- if (!bodyView || !(session?.latestBinaryFrames instanceof Map) || session.latestBinaryFrames.size === 0) {
13994
+ if (!bodyView) {
13970
13995
  return 0;
13971
13996
  }
13972
13997
 
13973
- pruneRemoteFleetBinaryFrameCache(session);
13974
- const patches = [];
13975
- for (const entry of session.latestBinaryFrames.values()) {
13976
- if (entry?.frame) {
13977
- patches.push(cloneRemoteFleetFramePatch(entry.frame));
13998
+ const patchesByDeviceId = new Map();
13999
+ const allowedDeviceIds = new Set(getRemoteFleetBinaryFrameDeviceIds(session));
14000
+ const addFrame = entry => {
14001
+ const frame = entry?.frame;
14002
+ const deviceId = String(frame?.deviceId || '').trim();
14003
+ if (!deviceId || !frame) {
14004
+ return;
14005
+ }
14006
+
14007
+ if (allowedDeviceIds.size > 0 && !allowedDeviceIds.has(deviceId)) {
14008
+ return;
14009
+ }
14010
+
14011
+ const existing = patchesByDeviceId.get(deviceId);
14012
+ const existingSeq = Number(existing?.frameSeq || 0);
14013
+ const nextSeq = Number(frame.frameSeq || 0);
14014
+ if (!existing || nextSeq >= existingSeq) {
14015
+ patchesByDeviceId.set(deviceId, cloneRemoteFleetFramePatch(frame));
14016
+ }
14017
+ };
14018
+
14019
+ if (session?.latestBinaryFrames instanceof Map && session.latestBinaryFrames.size > 0) {
14020
+ pruneRemoteFleetBinaryFrameCache(session);
14021
+ for (const entry of session.latestBinaryFrames.values()) {
14022
+ addFrame(entry);
13978
14023
  }
13979
14024
  }
13980
14025
 
14026
+ pruneRemoteFleetGlobalBinaryFrameCache();
14027
+ for (const entry of remoteFleetLatestBinaryFrames.values()) {
14028
+ addFrame(entry);
14029
+ }
14030
+
14031
+ const patches = Array.from(patchesByDeviceId.values());
13981
14032
  return patches.length > 0
13982
14033
  ? applyRemoteFleetFramePatches(bodyView, patches)
13983
14034
  : 0;
@@ -14256,6 +14307,69 @@
14256
14307
  }, REMOTE_FLEET_BINARY_FRAME_WS_RECONNECT_MS);
14257
14308
  }
14258
14309
 
14310
+ function cancelRemoteFleetBinaryFrameSessionRelease(session) {
14311
+ if (!session?.releaseTimer) {
14312
+ return;
14313
+ }
14314
+
14315
+ clearTimeout(session.releaseTimer);
14316
+ session.releaseTimer = null;
14317
+ }
14318
+
14319
+ function closeRemoteFleetBinaryFrameSession(session, reason = 'close') {
14320
+ if (!session) {
14321
+ return;
14322
+ }
14323
+
14324
+ cancelRemoteFleetBinaryFrameSessionRelease(session);
14325
+ if (session.reconnectTimer) {
14326
+ clearTimeout(session.reconnectTimer);
14327
+ session.reconnectTimer = null;
14328
+ }
14329
+ if (session.subscriptionTimer) {
14330
+ clearInterval(session.subscriptionTimer);
14331
+ session.subscriptionTimer = null;
14332
+ }
14333
+ if (session.ws) {
14334
+ try {
14335
+ session.ws.close();
14336
+ } catch {
14337
+ // Ignore WebSocket close failures.
14338
+ }
14339
+ session.ws = null;
14340
+ }
14341
+ remoteFleetBinaryFrameSessions.delete(session.nodeId);
14342
+ window.RuntimeTrace?.emit?.('remote.frame.wsSessionClosed', {
14343
+ nodeId: session.nodeId,
14344
+ reason
14345
+ });
14346
+ }
14347
+
14348
+ function scheduleRemoteFleetBinaryFrameSessionRelease(session, reason = 'idle') {
14349
+ if (!session
14350
+ || session.releaseTimer
14351
+ || session.bodies.size > 0
14352
+ || (session.controlSessions instanceof Set && session.controlSessions.size > 0)) {
14353
+ return;
14354
+ }
14355
+
14356
+ session.releaseTimer = setTimeout(() => {
14357
+ session.releaseTimer = null;
14358
+ if (session.bodies.size > 0
14359
+ || (session.controlSessions instanceof Set && session.controlSessions.size > 0)) {
14360
+ return;
14361
+ }
14362
+
14363
+ closeRemoteFleetBinaryFrameSession(session, reason);
14364
+ }, REMOTE_FLEET_BINARY_FRAME_SESSION_IDLE_MS);
14365
+ session.releaseTimer?.unref?.();
14366
+ window.RuntimeTrace?.emit?.('remote.frame.wsSessionIdle', {
14367
+ nodeId: session.nodeId,
14368
+ reason,
14369
+ ttlMs: REMOTE_FLEET_BINARY_FRAME_SESSION_IDLE_MS
14370
+ });
14371
+ }
14372
+
14259
14373
  async function openRemoteFleetBinaryFrameSocket(session) {
14260
14374
  if (!session || (session.bodies.size === 0 && (!session.controlSessions || session.controlSessions.size === 0)) || typeof WebSocket !== 'function') {
14261
14375
  return false;
@@ -14305,7 +14419,17 @@
14305
14419
 
14306
14420
  parseRemoteFleetBinaryFrameMessage(event.data)
14307
14421
  .then(frame => {
14308
- if (!frame || (session.bodies.size === 0 && (!session.controlSessions || session.controlSessions.size === 0))) {
14422
+ if (!frame) {
14423
+ return;
14424
+ }
14425
+
14426
+ const hasFrameConsumers =
14427
+ session.bodies.size > 0
14428
+ || (session.controlSessions instanceof Set && session.controlSessions.size > 0);
14429
+ if (!hasFrameConsumers) {
14430
+ const frameAt = getRemoteFleetFrameNow();
14431
+ session.lastBinaryFrameAt = frameAt;
14432
+ rememberRemoteFleetBinaryFrame(session, frame);
14309
14433
  if (frame?._remoteFleetObjectUrl && typeof URL !== 'undefined' && typeof URL.revokeObjectURL === 'function') {
14310
14434
  URL.revokeObjectURL(frame._remoteFleetObjectUrl);
14311
14435
  }
@@ -14408,6 +14532,7 @@
14408
14532
  bodies: new Set(),
14409
14533
  ws: null,
14410
14534
  reconnectTimer: null,
14535
+ releaseTimer: null,
14411
14536
  subscriptionTimer: null,
14412
14537
  subscriptionKey: '',
14413
14538
  latestBinaryFrames: new Map(),
@@ -14417,6 +14542,7 @@
14417
14542
  remoteFleetBinaryFrameSessions.set(nodeId, session);
14418
14543
  }
14419
14544
 
14545
+ cancelRemoteFleetBinaryFrameSessionRelease(session);
14420
14546
  session.bodies.add(bodyView);
14421
14547
  session.getDeviceIds = getDeviceIds;
14422
14548
  session.liveOptions = {
@@ -14428,7 +14554,14 @@
14428
14554
  mode: String(options.mode || 'remote-fast')
14429
14555
  };
14430
14556
  bodyView._remoteFleetBinaryFrameSession = session;
14431
- applyRemoteFleetBinaryFrameCacheToBody(bodyView, session);
14557
+ const primedFrames = applyRemoteFleetBinaryFrameCacheToBody(bodyView, session);
14558
+ if (primedFrames > 0) {
14559
+ bodyView._remoteFleetLastBinaryFrameAt = getRemoteFleetFrameNow();
14560
+ window.RuntimeTrace?.emit?.('remote.frame.wsCachePrimed', {
14561
+ nodeId,
14562
+ count: primedFrames
14563
+ });
14564
+ }
14432
14565
  openRemoteFleetBinaryFrameSocket(session).catch(() => undefined);
14433
14566
  return true;
14434
14567
  }
@@ -14446,23 +14579,7 @@
14446
14579
  return;
14447
14580
  }
14448
14581
 
14449
- if (session.reconnectTimer) {
14450
- clearTimeout(session.reconnectTimer);
14451
- session.reconnectTimer = null;
14452
- }
14453
- if (session.subscriptionTimer) {
14454
- clearInterval(session.subscriptionTimer);
14455
- session.subscriptionTimer = null;
14456
- }
14457
- if (session.ws) {
14458
- try {
14459
- session.ws.close();
14460
- } catch {
14461
- // Ignore WebSocket close failures.
14462
- }
14463
- session.ws = null;
14464
- }
14465
- remoteFleetBinaryFrameSessions.delete(session.nodeId);
14582
+ scheduleRemoteFleetBinaryFrameSessionRelease(session, 'body-detached');
14466
14583
  }
14467
14584
 
14468
14585
  function requestRemoteFleetFrameLoopFrame(callback) {
@@ -14725,6 +14842,104 @@
14725
14842
  return canvas;
14726
14843
  }
14727
14844
 
14845
+ function collectRemoteFleetPreviewFrameSurfaces(root) {
14846
+ const surfaces = new Map();
14847
+ if (!root?.querySelectorAll) {
14848
+ return surfaces;
14849
+ }
14850
+
14851
+ root.querySelectorAll('[data-remote-fleet-device-preview][data-device-id]').forEach(preview => {
14852
+ const deviceId = String(preview.dataset.deviceId || '').trim();
14853
+ if (!deviceId) {
14854
+ return;
14855
+ }
14856
+
14857
+ const canvas = preview.querySelector('canvas[data-remote-fleet-frame-canvas="true"]');
14858
+ const image = preview.querySelector('img[data-remote-fleet-frame-image="true"]');
14859
+ surfaces.set(deviceId, {
14860
+ canvas,
14861
+ image,
14862
+ kind: String(preview.dataset.remoteFleetFrameKind || canvas?.dataset?.remoteFleetFrameKind || image?.dataset?.remoteFleetFrameKind || '').trim().toLowerCase(),
14863
+ seq: Number(preview.dataset.remoteFleetFrameSeq || canvas?.dataset?.remoteFleetFrameSeq || image?.dataset?.remoteFleetFrameSeq || 0) || 0,
14864
+ url: String(preview.dataset.remoteFleetFrameUrl || canvas?.dataset?.remoteFleetFrameUrl || image?.dataset?.remoteFleetFrameUrl || '').trim()
14865
+ });
14866
+ });
14867
+
14868
+ return surfaces;
14869
+ }
14870
+
14871
+ function restoreRemoteFleetPreviewFrameSurface(preview, surfaces, deviceId) {
14872
+ const id = String(deviceId || preview?.dataset?.deviceId || '').trim();
14873
+ const surface = id && surfaces instanceof Map ? surfaces.get(id) : null;
14874
+ if (!preview || !surface) {
14875
+ return false;
14876
+ }
14877
+
14878
+ const existingSeq = Number(surface.seq || 0) || 0;
14879
+ const nextSeq = Number(preview.dataset.remoteFleetFrameSeq || 0) || 0;
14880
+ if (existingSeq <= 0 || (nextSeq > 0 && existingSeq < nextSeq)) {
14881
+ return false;
14882
+ }
14883
+
14884
+ const currentCanvas = preview.querySelector('canvas[data-remote-fleet-frame-canvas="true"]');
14885
+ if (surface.canvas && surface.canvas !== currentCanvas) {
14886
+ currentCanvas?.remove?.();
14887
+ surface.canvas.dataset.remoteFleetFrameKind = surface.kind || preview.dataset.remoteFleetFrameKind || 'live';
14888
+ surface.canvas.dataset.remoteFleetFrameSeq = String(existingSeq);
14889
+ surface.canvas.dataset.remoteFleetFrameUrl = surface.url || '';
14890
+ surface.canvas.style.display = 'block';
14891
+ if (typeof preview.insertBefore === 'function') {
14892
+ preview.insertBefore(surface.canvas, preview.firstChild || null);
14893
+ } else {
14894
+ preview.appendChild?.(surface.canvas);
14895
+ }
14896
+ }
14897
+
14898
+ const currentImage = preview.querySelector('img[data-remote-fleet-frame-image="true"]');
14899
+ if (surface.canvas) {
14900
+ preview.querySelector('[data-remote-fleet-screen-placeholder="true"]')?.remove?.();
14901
+ if (currentImage && currentImage !== surface.image) {
14902
+ currentImage.style.display = 'none';
14903
+ }
14904
+ if (surface.image && surface.image !== currentImage && surface.image.parentElement === preview) {
14905
+ surface.image.style.display = 'none';
14906
+ }
14907
+ } else if (surface.image && surface.image !== currentImage) {
14908
+ currentImage?.remove?.();
14909
+ surface.image.style.display = 'block';
14910
+ if (typeof preview.insertBefore === 'function') {
14911
+ preview.insertBefore(surface.image, preview.firstChild || null);
14912
+ } else {
14913
+ preview.appendChild?.(surface.image);
14914
+ }
14915
+ }
14916
+
14917
+ preview.dataset.remoteFleetFrameKind = surface.kind || preview.dataset.remoteFleetFrameKind || '';
14918
+ preview.dataset.remoteFleetFrameSeq = String(existingSeq);
14919
+ preview.dataset.remoteFleetFrameUrl = surface.url || '';
14920
+ preview.dataset.remoteFleetRestoredFrame = 'true';
14921
+ return true;
14922
+ }
14923
+
14924
+ function collectRemoteFleetControlNodeSurface(root) {
14925
+ const canvas = root?.querySelector?.('canvas[data-remote-fleet-control-node-canvas="true"]');
14926
+ if (!canvas) {
14927
+ return null;
14928
+ }
14929
+
14930
+ return {
14931
+ canvas,
14932
+ kind: String(canvas.dataset.remoteFleetFrameKind || 'live').trim().toLowerCase(),
14933
+ seq: Number(canvas.dataset.remoteFleetFrameSeq || 0) || 0,
14934
+ contentRect: canvas._remoteFleetContentRect || null
14935
+ };
14936
+ }
14937
+
14938
+ function hasRemoteFleetReusableControlSurface(surface) {
14939
+ return !!surface?.canvas
14940
+ && (Number(surface.seq || 0) > 0 || !!surface.contentRect);
14941
+ }
14942
+
14728
14943
  function syncRemoteFleetFrameCanvasSize(canvas) {
14729
14944
  if (!canvas) {
14730
14945
  return null;
@@ -14736,8 +14951,18 @@
14736
14951
  const cssWidth = Math.max(1, Math.round(rect?.width || canvas.clientWidth || 320));
14737
14952
  const cssHeight = Math.max(1, Math.round(rect?.height || canvas.clientHeight || 180));
14738
14953
  const dpr = Math.max(1, Math.min(REMOTE_FLEET_FRAME_CANVAS_MAX_DPR, Number(window.devicePixelRatio || 1) || 1));
14739
- const nextWidth = Math.max(1, Math.round(cssWidth * dpr));
14740
- const nextHeight = Math.max(1, Math.round(cssHeight * dpr));
14954
+ let nextWidth = Math.max(1, Math.round(cssWidth * dpr));
14955
+ let nextHeight = Math.max(1, Math.round(cssHeight * dpr));
14956
+ const currentWidth = Number(canvas.width || 0) || 0;
14957
+ const currentHeight = Number(canvas.height || 0) || 0;
14958
+ const widthDelta = Math.abs(currentWidth - nextWidth);
14959
+ const heightDelta = Math.abs(currentHeight - nextHeight);
14960
+ if (currentWidth > 0 && widthDelta > 0 && widthDelta <= 2) {
14961
+ nextWidth = currentWidth;
14962
+ }
14963
+ if (currentHeight > 0 && heightDelta > 0 && heightDelta <= 2) {
14964
+ nextHeight = currentHeight;
14965
+ }
14741
14966
  if (canvas.width !== nextWidth) {
14742
14967
  canvas.width = nextWidth;
14743
14968
  }
@@ -15829,23 +16054,7 @@
15829
16054
  refreshRemoteFleetBinaryFrameSubscription(binarySession, true);
15830
16055
 
15831
16056
  if (binarySession.bodies.size === 0 && binarySession.controlSessions.size === 0) {
15832
- if (binarySession.reconnectTimer) {
15833
- clearTimeout(binarySession.reconnectTimer);
15834
- binarySession.reconnectTimer = null;
15835
- }
15836
- if (binarySession.subscriptionTimer) {
15837
- clearInterval(binarySession.subscriptionTimer);
15838
- binarySession.subscriptionTimer = null;
15839
- }
15840
- if (binarySession.ws) {
15841
- try {
15842
- binarySession.ws.close();
15843
- } catch {
15844
- // Ignore WebSocket close failures.
15845
- }
15846
- binarySession.ws = null;
15847
- }
15848
- remoteFleetBinaryFrameSessions.delete(binarySession.nodeId);
16057
+ scheduleRemoteFleetBinaryFrameSessionRelease(binarySession, 'control-detached');
15849
16058
  }
15850
16059
  }
15851
16060
 
@@ -15869,7 +16078,12 @@
15869
16078
  }
15870
16079
 
15871
16080
  drawRemoteFleetFrameToCanvas(session.canvas, bitmap, 'contain');
16081
+ session.canvas.dataset.remoteFleetFrameKind = String(frame.kind || 'live').toLowerCase();
16082
+ session.canvas.dataset.remoteFleetFrameSeq = String(Number(frame.frameSeq || frame.FrameSeq || 0) || 0);
16083
+ session.canvas.dataset.remoteFleetFrameUrl = String(frame.frameUrl || '');
15872
16084
  session.canvas.style.display = 'block';
16085
+ session.lastControlFrameSeq = Number(frame.frameSeq || frame.FrameSeq || session.lastControlFrameSeq || 0) || 0;
16086
+ session.lastControlFrameAt = Date.now();
15873
16087
  session.placeholder?.remove?.();
15874
16088
  session.placeholder = null;
15875
16089
  if (typeof bitmap.close === 'function') {
@@ -15919,6 +16133,9 @@
15919
16133
  }
15920
16134
 
15921
16135
  drawRemoteFleetFrameToCanvas(session.canvas, bitmap, 'contain');
16136
+ session.canvas.dataset.remoteFleetFrameKind = String(frame.kind || 'live').toLowerCase();
16137
+ session.canvas.dataset.remoteFleetFrameSeq = String(frameSeq || 0);
16138
+ session.canvas.dataset.remoteFleetFrameUrl = String(frame.frameUrl || '');
15922
16139
  session.canvas.style.display = 'block';
15923
16140
  session.lastControlFrameAt = Date.now();
15924
16141
  session.status.textContent = `Control ${REMOTE_FLEET_CONTROL_LIVE_FPS}fps`;
@@ -16870,6 +17087,7 @@
16870
17087
  function renderRemoteFleetDevice(bodyView, nodeModel) {
16871
17088
  if (!bodyView) return;
16872
17089
  clearRemoteFleetTimers(bodyView, { keepBinaryFrameSocket: true });
17090
+ const preservedControlSurface = collectRemoteFleetControlNodeSurface(bodyView);
16873
17091
 
16874
17092
  const nodeId = String(nodeModel?.id ?? nodeModel?.Id ?? '');
16875
17093
  const device = parseRemoteFleetPinnedDevice(nodeModel);
@@ -17065,7 +17283,7 @@
17065
17283
  touch-action: none;
17066
17284
  outline: none;
17067
17285
  `;
17068
- const controlCanvas = document.createElement('canvas');
17286
+ const controlCanvas = preservedControlSurface?.canvas || document.createElement('canvas');
17069
17287
  controlCanvas.dataset.remoteFleetControlNodeCanvas = 'true';
17070
17288
  controlCanvas.style.cssText = `
17071
17289
  position: absolute;
@@ -17125,7 +17343,10 @@
17125
17343
  controlOverlay.appendChild(controlDot);
17126
17344
  controlOverlay.appendChild(controlStatus);
17127
17345
  controlStage.appendChild(controlCanvas);
17128
- controlStage.appendChild(controlPlaceholder);
17346
+ const restoredControlSurface = hasRemoteFleetReusableControlSurface(preservedControlSurface);
17347
+ if (!restoredControlSurface) {
17348
+ controlStage.appendChild(controlPlaceholder);
17349
+ }
17129
17350
  controlStage.appendChild(controlOverlay);
17130
17351
  bodyView.appendChild(controlStage);
17131
17352
 
@@ -17137,7 +17358,17 @@
17137
17358
  controlStage,
17138
17359
  controlCanvas,
17139
17360
  controlStatus,
17140
- controlPlaceholder);
17361
+ restoredControlSurface ? null : controlPlaceholder);
17362
+ if (session && restoredControlSurface) {
17363
+ session.lastControlFrameSeq = Number(preservedControlSurface?.seq || session.lastControlFrameSeq || 0) || 0;
17364
+ session.lastControlFrameAt = Date.now();
17365
+ controlStatus.textContent = `Control ${REMOTE_FLEET_CONTROL_LIVE_FPS}fps`;
17366
+ window.RuntimeTrace?.emit?.('remote.control.surfaceRestored', {
17367
+ nodeId,
17368
+ deviceId,
17369
+ seq: session.lastControlFrameSeq
17370
+ });
17371
+ }
17141
17372
  if (session && isRemoteFleetFrameSource(previewSource)) {
17142
17373
  paintRemoteFleetControlFrame(session, {
17143
17374
  deviceId,
@@ -17425,6 +17656,7 @@
17425
17656
  function renderRemoteFleetMonitor(bodyView, nodeModel) {
17426
17657
  if (!bodyView) return;
17427
17658
  clearRemoteFleetTimers(bodyView, { keepBinaryFrameSocket: true });
17659
+ const preservedFrameSurfaces = collectRemoteFleetPreviewFrameSurfaces(bodyView);
17428
17660
 
17429
17661
  const nodeId = String(nodeModel?.id ?? nodeModel?.Id ?? '');
17430
17662
  const searchState = bodyView.dataset.remoteFleetSearch || '';
@@ -17979,6 +18211,7 @@
17979
18211
  preview.appendChild(placeholder);
17980
18212
  }
17981
18213
 
18214
+ restoreRemoteFleetPreviewFrameSurface(preview, preservedFrameSurfaces, getRemoteFleetDeviceId(device));
17982
18215
  return preview;
17983
18216
  };
17984
18217
 
@@ -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=20260617-auth-mirror-v590" />
11
- <link rel="stylesheet" href="_content/MindExecution.Shared/css/mind-map-overrides.css?v=20260617-auth-mirror-v590" />
10
+ <link rel="stylesheet" href="_content/MindExecution.Shared/css/app.css?v=20260617-remote-frame-no-flash-v592" />
11
+ <link rel="stylesheet" href="_content/MindExecution.Shared/css/mind-map-overrides.css?v=20260617-remote-frame-no-flash-v592" />
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 = '20260617-auth-mirror-v590';
582
+ const scriptVersion = '20260617-remote-frame-no-flash-v592';
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": "GgmfxCXF",
2
+ "version": "ikaqQxI3",
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-1ImfUjyr1aBodcgICy6KTDGinJHMb1jdRG2MYGPdYvE=",
89
+ "hash": "sha256-yUuxzH9Fpd62pPLMU5n0HuYjsJhKvC82LO5X5Uu/1gg=",
90
90
  "url": "_content/MindExecution.Shared/js/mind-map-css3d-manager.js"
91
91
  },
92
92
  {
@@ -834,7 +834,7 @@
834
834
  "url": "image-manifest.json"
835
835
  },
836
836
  {
837
- "hash": "sha256-RLe/OcetCLrQfgXCZMRe1KAmWd4NRvzz9LOOJ0dr7p0=",
837
+ "hash": "sha256-8VAYYW89mBjF45WS2vA1+Hssmo6S30PB8AW03niSE2Q=",
838
838
  "url": "index.html"
839
839
  },
840
840
  {
@@ -1,4 +1,4 @@
1
- /* Manifest version: GgmfxCXF */
1
+ /* Manifest version: ikaqQxI3 */
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