@mindexec/cli 0.2.118 → 0.2.120

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.118",
3
+ "version": "0.2.120",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
package/remote-hub.js CHANGED
@@ -372,8 +372,8 @@ function serializeRemoteFrame(frame, deviceId, frameKind, options = {}) {
372
372
  framePath,
373
373
  frameUrl: framePath
374
374
  };
375
- if (options.includeDataUrl !== false) {
376
- serialized.dataUrl = dataUrl;
375
+ if (options.includeDataUrl === true) {
376
+ serialized.dataUrl = dataUrl || buildFrameDataUrl(payload, '', publicFrame.mimeType || publicFrame.format || 'image/jpeg');
377
377
  }
378
378
 
379
379
  return serialized;
@@ -1114,7 +1114,7 @@ export function createRemoteHub(options = {}) {
1114
1114
 
1115
1115
  function listDevices(options = {}) {
1116
1116
  const serializeOptions = {
1117
- includeDataUrl: options.includeDataUrl !== false
1117
+ includeDataUrl: options.includeDataUrl === true
1118
1118
  };
1119
1119
  return [...devices.values()]
1120
1120
  .map(device => serializeDevice(device, serializeOptions))
@@ -1969,7 +1969,6 @@ export function createRemoteHub(options = {}) {
1969
1969
  contentHash,
1970
1970
  captureMs: readFrameCaptureMs(message),
1971
1971
  sameContentStreak,
1972
- dataUrl: buildFrameDataUrl(framePayload, frameData, mimeType),
1973
1972
  payload,
1974
1973
  accessToken: createFrameAccessToken()
1975
1974
  };
@@ -2040,7 +2039,6 @@ export function createRemoteHub(options = {}) {
2040
2039
  contentHash,
2041
2040
  captureMs: readFrameCaptureMs(message),
2042
2041
  sameContentStreak,
2043
- dataUrl: buildFrameDataUrl(framePayload, frameData, mimeType),
2044
2042
  payload,
2045
2043
  accessToken: createFrameAccessToken()
2046
2044
  };
@@ -2961,14 +2959,14 @@ export function createRemoteHub(options = {}) {
2961
2959
  function getDeviceLiveFrame(deviceId, options = {}) {
2962
2960
  const device = devices.get(String(deviceId || ''));
2963
2961
  return serializeRemoteFrame(device?.latestLiveFrame, device?.deviceId, 'live', {
2964
- includeDataUrl: options.includeDataUrl !== false
2962
+ includeDataUrl: options.includeDataUrl === true
2965
2963
  });
2966
2964
  }
2967
2965
 
2968
2966
  function getDeviceThumbnail(deviceId, options = {}) {
2969
2967
  const device = devices.get(String(deviceId || ''));
2970
2968
  return serializeRemoteFrame(device?.latestThumbnail, device?.deviceId, 'thumbnail', {
2971
- includeDataUrl: options.includeDataUrl !== false
2969
+ includeDataUrl: options.includeDataUrl === true
2972
2970
  });
2973
2971
  }
2974
2972
 
@@ -373,10 +373,16 @@ async function runSyntheticEnabledSmoke() {
373
373
  });
374
374
  assert.equal(thumbnail.ok, true, JSON.stringify(thumbnail.payload));
375
375
  assert.equal(thumbnail.payload?.thumbnail?.streamId, 'http-smoke-thumb');
376
- assert.ok(String(thumbnail.payload?.thumbnail?.dataUrl || '').startsWith('data:image/png;base64,'));
376
+ assert.ok(!('dataUrl' in thumbnail.payload.thumbnail));
377
377
  assert.ok(String(thumbnail.payload?.thumbnail?.framePath || '').includes(`/api/remote/devices/${encodeURIComponent(thumbnailTarget.deviceId)}/thumbnail?`));
378
378
  assert.equal(thumbnail.payload?.thumbnail?.frameUrl, thumbnail.payload?.thumbnail?.framePath);
379
379
 
380
+ const thumbnailWithData = await fetchJson(`${baseUrl}/api/remote/devices/${encodeURIComponent(thumbnailTarget.deviceId)}/thumbnail?includeDataUrl=true`, {
381
+ token: BRIDGE_TOKEN
382
+ });
383
+ assert.equal(thumbnailWithData.ok, true, JSON.stringify(thumbnailWithData.payload));
384
+ assert.ok(String(thumbnailWithData.payload?.thumbnail?.dataUrl || '').startsWith('data:image/png;base64,'));
385
+
380
386
  const thumbnailBinary = await fetchBinary(`${baseUrl}${thumbnail.payload.thumbnail.framePath}`);
381
387
  assert.equal(thumbnailBinary.ok, true, `${thumbnailBinary.status} ${thumbnailBinary.contentType}`);
382
388
  assert.equal(thumbnailBinary.contentType, 'image/png');
@@ -433,9 +439,15 @@ async function runSyntheticEnabledSmoke() {
433
439
  assert.equal(liveFrame.payload?.frame?.streamId, 'http-smoke-live');
434
440
  assert.equal(liveFrame.payload?.frame?.mode, 'remote-fast');
435
441
  assert.equal(liveFrame.payload?.frame?.fps, 20);
436
- assert.ok(String(liveFrame.payload?.frame?.dataUrl || '').startsWith('data:image/png;base64,'));
442
+ assert.ok(!('dataUrl' in liveFrame.payload.frame));
437
443
  assert.ok(String(liveFrame.payload?.frame?.framePath || '').includes(`/api/remote/devices/${encodeURIComponent(liveTarget.deviceId)}/live/frame?`));
438
444
 
445
+ const liveFrameWithData = await fetchJson(`${baseUrl}/api/remote/devices/${encodeURIComponent(liveTarget.deviceId)}/live/frame?includeDataUrl=true`, {
446
+ token: BRIDGE_TOKEN
447
+ });
448
+ assert.equal(liveFrameWithData.ok, true, JSON.stringify(liveFrameWithData.payload));
449
+ assert.ok(String(liveFrameWithData.payload?.frame?.dataUrl || '').startsWith('data:image/png;base64,'));
450
+
439
451
  const liveBinary = await fetchBinary(`${baseUrl}${liveFrame.payload.frame.framePath}`);
440
452
  assert.equal(liveBinary.ok, true, `${liveBinary.status} ${liveBinary.contentType}`);
441
453
  assert.equal(liveBinary.contentType, 'image/png');
@@ -37,9 +37,12 @@ try {
37
37
  assert.equal(devices.length, SYNTHETIC_COUNT);
38
38
  assert.equal(devices.filter(device => device.synthetic === true).length, SYNTHETIC_COUNT);
39
39
  assert.equal(devices.some(device => device.connected === false), true);
40
- assert.equal(devices.some(device => device.latestThumbnail?.dataUrl), true);
40
+ assert.equal(devices.some(device => device.latestThumbnail && 'dataUrl' in device.latestThumbnail), false);
41
41
  assert.equal(devices.some(device => device.activeLiveStream?.active === true), true);
42
42
 
43
+ const devicesWithDataUrl = hub.listDevices({ includeDataUrl: true });
44
+ assert.equal(devicesWithDataUrl.some(device => device.latestThumbnail?.dataUrl), true);
45
+
43
46
  const deviceListPayload = {
44
47
  total: devices.length,
45
48
  pagination: 'none',
@@ -58,6 +61,8 @@ try {
58
61
  });
59
62
  assert.equal(thumbnailResult.ok, true);
60
63
  assert.equal(hub.getDeviceThumbnail(thumbnailTarget.deviceId).streamId, 'scale-thumb');
64
+ assert.equal('dataUrl' in hub.getDeviceThumbnail(thumbnailTarget.deviceId), false);
65
+ assert.ok(hub.getDeviceThumbnail(thumbnailTarget.deviceId, { includeDataUrl: true }).dataUrl);
61
66
 
62
67
  const liveTarget = devices.find(device => device.connected && device.capabilities?.liveStream);
63
68
  assert.ok(liveTarget);
package/server.js CHANGED
@@ -8,9 +8,9 @@
8
8
 
9
9
  import express from 'express';
10
10
  import cors from 'cors';
11
- import { promises as fs, readFileSync, statSync, createReadStream } from 'fs';
11
+ import { promises as fs, readFileSync, statSync, createReadStream, chmodSync } from 'fs';
12
12
  import path from 'path';
13
- import { exec, spawn, execFile } from 'child_process';
13
+ import { exec, spawn, spawnSync, execFile } from 'child_process';
14
14
  import { promisify } from 'util';
15
15
  import os from 'os';
16
16
  import multer from 'multer';
@@ -2907,10 +2907,14 @@ const REMOTE_AGENT_EARLY_EXIT_MS = 1200;
2907
2907
  const REMOTE_AGENT_READY_TIMEOUT_MS = 5000;
2908
2908
  const REMOTE_AGENT_DEFAULT_ENGINE = 'auto';
2909
2909
  const REMOTE_AGENT_SYNC_REPORT_LOG_REPEAT_MS = 30000;
2910
+ const REMOTE_AGENT_RACE_START_STAGGER_MS = 120;
2911
+ const REMOTE_AGENT_MAX_PARALLEL_CANDIDATES = 6;
2910
2912
  let remoteAgentState = createRemoteAgentIdleState();
2911
2913
  let remoteAgentSyncReportState = null;
2912
2914
  let remoteAgentSyncReportLogKey = '';
2913
2915
  let remoteAgentSyncReportLogAt = 0;
2916
+ let remoteAgentConnectPromise = null;
2917
+ const remoteAgentRecentSuccessfulManagers = new Map();
2914
2918
 
2915
2919
  function createRemoteAgentIdleState(overrides = {}) {
2916
2920
  return {
@@ -3211,6 +3215,46 @@ function createRemoteAgentConnectionKey(manager, leaseId) {
3211
3215
  .slice(0, 24);
3212
3216
  }
3213
3217
 
3218
+ function getRemoteAgentRecentManagerKey(leaseId = '') {
3219
+ return safeRemoteAgentField(leaseId || 'default', 128) || 'default';
3220
+ }
3221
+
3222
+ function rememberRemoteAgentSuccessfulManager(manager, leaseId = '') {
3223
+ const endpoint = normalizeRemoteManagerEndpoint(manager);
3224
+ if (!endpoint) {
3225
+ return;
3226
+ }
3227
+
3228
+ remoteAgentRecentSuccessfulManagers.set(getRemoteAgentRecentManagerKey(leaseId), {
3229
+ endpoint,
3230
+ updatedAt: Date.now()
3231
+ });
3232
+ }
3233
+
3234
+ function prioritizeRemoteAgentManagers(managers, leaseId = '') {
3235
+ const normalized = normalizeRemoteManagerEndpointList(managers);
3236
+ if (normalized.length <= 1) {
3237
+ return normalized;
3238
+ }
3239
+
3240
+ const recent = remoteAgentRecentSuccessfulManagers.get(getRemoteAgentRecentManagerKey(leaseId));
3241
+ const recentEndpoint = normalizeRemoteManagerEndpoint(recent?.endpoint);
3242
+ if (!recentEndpoint) {
3243
+ return normalized;
3244
+ }
3245
+
3246
+ const recentIndex = normalized.findIndex(item => item.toLowerCase() === recentEndpoint.toLowerCase());
3247
+ if (recentIndex <= 0) {
3248
+ return normalized;
3249
+ }
3250
+
3251
+ return [
3252
+ normalized[recentIndex],
3253
+ ...normalized.slice(0, recentIndex),
3254
+ ...normalized.slice(recentIndex + 1)
3255
+ ];
3256
+ }
3257
+
3214
3258
  function isRemoteAgentProcessRunning() {
3215
3259
  const proc = remoteAgentState.proc;
3216
3260
  return remoteAgentState.status === 'running'
@@ -3225,19 +3269,183 @@ function isLocalRemoteHostTargetActive() {
3225
3269
  && (!status.hostTargetHostInstanceId || status.hostTargetHostInstanceId === BRIDGE_INSTANCE_ID);
3226
3270
  }
3227
3271
 
3228
- function resolveRemoteAgentLauncher() {
3229
- const localLauncher = path.join(BRIDGE_ROOT, 'node_modules', '@mindexec', 'remote', 'bin', 'mindexec-remote.js');
3230
- try {
3231
- if (statSync(localLauncher).isFile()) {
3272
+ function getRemoteFastRuntimeId() {
3273
+ const platform = process.platform;
3274
+ const arch = process.arch;
3275
+ if (platform === 'win32' && arch === 'x64') {
3276
+ return 'win-x64';
3277
+ }
3278
+ if (platform === 'darwin' && arch === 'arm64') {
3279
+ return 'osx-arm64';
3280
+ }
3281
+ if (platform === 'darwin' && arch === 'x64') {
3282
+ return 'osx-x64';
3283
+ }
3284
+ return '';
3285
+ }
3286
+
3287
+ function getRemoteFastRuntimeExecutableName() {
3288
+ return process.platform === 'win32'
3289
+ ? 'mindexec-remote-fast.exe'
3290
+ : 'mindexec-remote-fast';
3291
+ }
3292
+
3293
+ function getBundledRemoteFastRuntimeDirs() {
3294
+ const rid = getRemoteFastRuntimeId();
3295
+ if (!rid) {
3296
+ return [];
3297
+ }
3298
+
3299
+ return [
3300
+ path.join(BRIDGE_ROOT, 'remote-fast', rid),
3301
+ path.join(BRIDGE_ROOT, 'remote', 'fast', rid),
3302
+ path.join(BRIDGE_ROOT, 'node_modules', '@mindexec', 'remote', 'fast', rid),
3303
+ path.resolve(BRIDGE_ROOT, '..', 'remote', 'fast', rid),
3304
+ path.resolve(BRIDGE_ROOT, '..', 'RemoteAgent', 'fast', rid)
3305
+ ];
3306
+ }
3307
+
3308
+ function resolveRemoteFastLauncher() {
3309
+ const rid = getRemoteFastRuntimeId();
3310
+ if (!rid) {
3311
+ return {
3312
+ ok: false,
3313
+ error: `remote-fast-not-packaged:${process.platform}-${process.arch}`
3314
+ };
3315
+ }
3316
+
3317
+ const failures = [];
3318
+ const seen = new Set();
3319
+ for (const dir of getBundledRemoteFastRuntimeDirs()) {
3320
+ const resolvedDir = path.resolve(dir);
3321
+ const key = resolvedDir.toLowerCase();
3322
+ if (seen.has(key)) {
3323
+ continue;
3324
+ }
3325
+ seen.add(key);
3326
+
3327
+ const executable = path.join(resolvedDir, getRemoteFastRuntimeExecutableName());
3328
+ try {
3329
+ if (statSync(executable).isFile()) {
3330
+ if (process.platform !== 'win32') {
3331
+ try {
3332
+ const currentMode = statSync(executable).mode;
3333
+ chmodSync(executable, currentMode | 0o755);
3334
+ } catch {
3335
+ // best effort only
3336
+ }
3337
+ }
3338
+
3339
+ const preflight = spawnSync(executable, ['--version'], {
3340
+ encoding: 'utf8',
3341
+ windowsHide: true,
3342
+ timeout: 2500
3343
+ });
3344
+ if (preflight.status === 0) {
3345
+ return {
3346
+ ok: true,
3347
+ command: executable,
3348
+ argsPrefix: [],
3349
+ launcher: executable,
3350
+ usingNpx: false,
3351
+ directFast: true,
3352
+ rid
3353
+ };
3354
+ }
3355
+ failures.push(`${executable}:exit:${preflight.status}`);
3356
+ }
3357
+ } catch {
3358
+ // Try DLL or next directory.
3359
+ }
3360
+
3361
+ const dll = path.join(resolvedDir, 'mindexec-remote-fast.dll');
3362
+ try {
3363
+ if (statSync(dll).isFile()) {
3364
+ const preflight = spawnSync('dotnet', [dll, '--version'], {
3365
+ encoding: 'utf8',
3366
+ windowsHide: true,
3367
+ timeout: 2500
3368
+ });
3369
+ if (preflight.status === 0) {
3370
+ return {
3371
+ ok: true,
3372
+ command: 'dotnet',
3373
+ argsPrefix: [dll],
3374
+ launcher: dll,
3375
+ usingNpx: false,
3376
+ directFast: true,
3377
+ rid
3378
+ };
3379
+ }
3380
+ failures.push(`${dll}:exit:${preflight.status}`);
3381
+ }
3382
+ } catch {
3383
+ // Try next directory.
3384
+ }
3385
+ }
3386
+
3387
+ return {
3388
+ ok: false,
3389
+ error: failures.length > 0
3390
+ ? failures.join('; ')
3391
+ : `remote-fast-runtime-missing:${rid}`
3392
+ };
3393
+ }
3394
+
3395
+ function resolveLocalRemotePackageLaunchers() {
3396
+ return [
3397
+ path.join(BRIDGE_ROOT, 'remote', 'bin', 'mindexec-remote.js'),
3398
+ path.join(BRIDGE_ROOT, 'node_modules', '@mindexec', 'remote', 'bin', 'mindexec-remote.js'),
3399
+ path.resolve(BRIDGE_ROOT, '..', 'remote', 'bin', 'mindexec-remote.js'),
3400
+ path.resolve(BRIDGE_ROOT, '..', 'RemoteAgent', 'bin', 'mindexec-remote.js')
3401
+ ];
3402
+ }
3403
+
3404
+ function resolveRemotePackageLauncher() {
3405
+ const seen = new Set();
3406
+ for (const candidate of resolveLocalRemotePackageLaunchers()) {
3407
+ const resolved = path.resolve(candidate);
3408
+ const key = resolved.toLowerCase();
3409
+ if (seen.has(key)) {
3410
+ continue;
3411
+ }
3412
+ seen.add(key);
3413
+ try {
3414
+ if (statSync(resolved).isFile()) {
3415
+ return {
3416
+ command: process.execPath,
3417
+ argsPrefix: [resolved],
3418
+ launcher: resolved,
3419
+ usingNpx: false,
3420
+ directFast: false
3421
+ };
3422
+ }
3423
+ } catch {
3424
+ // Try next package layout.
3425
+ }
3426
+ }
3427
+ return null;
3428
+ }
3429
+
3430
+ function resolveRemoteAgentLauncher(engine = REMOTE_AGENT_DEFAULT_ENGINE) {
3431
+ const normalizedEngine = normalizeRemoteAgentEngine(engine);
3432
+ if (normalizedEngine !== 'node') {
3433
+ const fast = resolveRemoteFastLauncher();
3434
+ if (fast.ok) {
3435
+ return fast;
3436
+ }
3437
+
3438
+ if (normalizedEngine === 'fast') {
3232
3439
  return {
3233
- command: process.execPath,
3234
- argsPrefix: [localLauncher],
3235
- launcher: localLauncher,
3236
- usingNpx: false
3440
+ ok: false,
3441
+ error: fast.error || 'remote-fast-unavailable'
3237
3442
  };
3238
3443
  }
3239
- } catch {
3240
- // Fall back to npx for source checkouts or older package installs.
3444
+ }
3445
+
3446
+ const localPackage = resolveRemotePackageLauncher();
3447
+ if (localPackage) {
3448
+ return localPackage;
3241
3449
  }
3242
3450
 
3243
3451
  const npxCli = resolveNpxCliLauncher();
@@ -3246,7 +3454,8 @@ function resolveRemoteAgentLauncher() {
3246
3454
  command: process.execPath,
3247
3455
  argsPrefix: [npxCli, '-y', '@mindexec/remote@latest'],
3248
3456
  launcher: npxCli,
3249
- usingNpx: true
3457
+ usingNpx: true,
3458
+ directFast: false
3250
3459
  };
3251
3460
  }
3252
3461
 
@@ -3255,7 +3464,8 @@ function resolveRemoteAgentLauncher() {
3255
3464
  command: process.env.ComSpec || 'cmd.exe',
3256
3465
  argsPrefix: ['/d', '/s', '/c', 'npx.cmd', '-y', '@mindexec/remote@latest'],
3257
3466
  launcher: 'cmd.exe /c npx.cmd',
3258
- usingNpx: true
3467
+ usingNpx: true,
3468
+ directFast: false
3259
3469
  };
3260
3470
  }
3261
3471
 
@@ -3263,7 +3473,8 @@ function resolveRemoteAgentLauncher() {
3263
3473
  command: 'npx',
3264
3474
  argsPrefix: ['-y', '@mindexec/remote@latest'],
3265
3475
  launcher: 'npx',
3266
- usingNpx: true
3476
+ usingNpx: true,
3477
+ directFast: false
3267
3478
  };
3268
3479
  }
3269
3480
 
@@ -3343,11 +3554,11 @@ async function startRemoteAgentConnection(options = {}) {
3343
3554
  const nodeId = safeRemoteAgentField(options.nodeId, 128);
3344
3555
  const source = safeRemoteAgentField(options.source || 'registry', 64);
3345
3556
  const engine = normalizeRemoteAgentEngine(options.engine);
3346
- const managers = normalizeRemoteManagerEndpointList(
3557
+ const managers = prioritizeRemoteAgentManagers(normalizeRemoteManagerEndpointList(
3347
3558
  options.manager || options.managerEndpoint || options.endpoint,
3348
3559
  options.managerCandidates,
3349
3560
  options.endpointCandidates,
3350
- options.candidates);
3561
+ options.candidates), leaseId);
3351
3562
 
3352
3563
  if (managers.length === 0) {
3353
3564
  logWarn('remote', 'managed RemoteAgent connect rejected: invalid manager endpoint');
@@ -3385,45 +3596,321 @@ async function startRemoteAgentConnection(options = {}) {
3385
3596
  await stopRemoteAgentConnection('replaced-by-new-target');
3386
3597
  }
3387
3598
 
3388
- let lastResult = null;
3599
+ if (remoteAgentConnectPromise) {
3600
+ return await remoteAgentConnectPromise;
3601
+ }
3602
+
3603
+ remoteAgentConnectPromise = startRemoteAgentConnectionRace({
3604
+ managers,
3605
+ pairToken,
3606
+ leaseId,
3607
+ nodeId,
3608
+ source,
3609
+ engine
3610
+ }).finally(() => {
3611
+ remoteAgentConnectPromise = null;
3612
+ });
3613
+ return await remoteAgentConnectPromise;
3614
+ }
3615
+
3616
+ function buildRemoteAgentLaunchArgs(launcher, manager, pairToken, engine) {
3617
+ const args = [
3618
+ ...launcher.argsPrefix,
3619
+ 'connect',
3620
+ '--manager',
3621
+ manager,
3622
+ '--pair',
3623
+ pairToken
3624
+ ];
3625
+
3626
+ if (launcher.directFast === true) {
3627
+ args.push('--transport', 'ws');
3628
+ return args;
3629
+ }
3630
+
3631
+ args.push('--engine', engine);
3632
+ return args;
3633
+ }
3634
+
3635
+ function appendRemoteAgentAttemptOutput(attempt, stream, chunk) {
3636
+ const text = chunk.toString();
3637
+ const key = stream === 'stderr' ? 'stderrTail' : 'stdoutTail';
3638
+ attempt.state[key] = String((attempt.state[key] || '') + text).slice(-REMOTE_AGENT_STDIO_TAIL_CHARS);
3639
+ if (/Connected to RemoteHub/i.test(text)) {
3640
+ attempt.state.ready = true;
3641
+ attempt.state.connectedAt = new Date().toISOString();
3642
+ }
3643
+ attempt.state.updatedAt = new Date().toISOString();
3644
+ }
3645
+
3646
+ function summarizeRemoteAgentAttempt(attempt) {
3647
+ return formatRemoteAgentFailureSummary(attempt?.state || {});
3648
+ }
3649
+
3650
+ function createRemoteAgentAttempt(options = {}) {
3651
+ const manager = normalizeRemoteManagerEndpoint(options.manager);
3652
+ const pairToken = safeRemoteAgentField(options.pairToken || options.pair, 512);
3653
+ const leaseId = safeRemoteAgentField(options.leaseId, 128);
3654
+ const nodeId = safeRemoteAgentField(options.nodeId, 128);
3655
+ const source = safeRemoteAgentField(options.source || 'registry', 64);
3656
+ const engine = normalizeRemoteAgentEngine(options.engine);
3657
+ const managerCandidates = normalizeRemoteManagerEndpointList(options.managerCandidates, manager);
3658
+
3659
+ const connectionKey = createRemoteAgentConnectionKey(manager, leaseId);
3660
+ const launcher = options.launcher || resolveRemoteAgentLauncher(engine);
3661
+ if (launcher?.ok === false) {
3662
+ return {
3663
+ manager,
3664
+ failed: true,
3665
+ error: launcher.error || 'remote-agent-launcher-unavailable',
3666
+ state: createRemoteAgentIdleState({
3667
+ status: 'failed',
3668
+ manager,
3669
+ managerCandidates,
3670
+ leaseId,
3671
+ nodeId,
3672
+ engine,
3673
+ source,
3674
+ connectionKey,
3675
+ lastError: launcher.error || 'remote-agent-launcher-unavailable'
3676
+ })
3677
+ };
3678
+ }
3679
+
3680
+ const state = createRemoteAgentIdleState({
3681
+ status: 'starting',
3682
+ manager,
3683
+ managerCandidates,
3684
+ leaseId,
3685
+ nodeId,
3686
+ engine,
3687
+ source,
3688
+ connectionKey,
3689
+ startedAt: new Date().toISOString(),
3690
+ ready: false,
3691
+ connectedAt: '',
3692
+ launcher: launcher.launcher,
3693
+ usingNpx: launcher.usingNpx
3694
+ });
3695
+
3696
+ const args = buildRemoteAgentLaunchArgs(launcher, manager, pairToken, engine);
3697
+ const attempt = {
3698
+ manager,
3699
+ pairToken,
3700
+ leaseId,
3701
+ nodeId,
3702
+ source,
3703
+ engine,
3704
+ launcher,
3705
+ args,
3706
+ state,
3707
+ child: null,
3708
+ failed: false,
3709
+ error: ''
3710
+ };
3711
+
3712
+ try {
3713
+ const child = spawn(launcher.command, args, {
3714
+ cwd: BRIDGE_ROOT,
3715
+ stdio: ['ignore', 'pipe', 'pipe'],
3716
+ windowsHide: true,
3717
+ env: {
3718
+ ...process.env,
3719
+ MINDEXEC_REMOTE_AGENT_MANAGED: '1'
3720
+ }
3721
+ });
3722
+
3723
+ attempt.child = child;
3724
+ state.proc = child;
3725
+ state.pid = Number(child.pid || 0);
3726
+ state.status = 'running';
3727
+ state.updatedAt = new Date().toISOString();
3728
+
3729
+ child.stdout.on('data', chunk => appendRemoteAgentAttemptOutput(attempt, 'stdout', chunk));
3730
+ child.stderr.on('data', chunk => appendRemoteAgentAttemptOutput(attempt, 'stderr', chunk));
3731
+ child.once('error', err => {
3732
+ attempt.failed = true;
3733
+ attempt.error = err?.message || String(err);
3734
+ state.status = 'failed';
3735
+ state.lastError = attempt.error;
3736
+ state.updatedAt = new Date().toISOString();
3737
+ });
3738
+ child.once('exit', (code, signal) => {
3739
+ if (state.status === 'running' && state.ready === true) {
3740
+ state.status = code === 0 ? 'exited' : 'failed';
3741
+ } else {
3742
+ attempt.failed = true;
3743
+ state.status = code === 0 ? 'exited' : 'failed';
3744
+ }
3745
+ state.exitCode = Number.isFinite(code) ? code : null;
3746
+ state.signal = signal || '';
3747
+ state.exitedAt = new Date().toISOString();
3748
+ state.updatedAt = state.exitedAt;
3749
+ if (code !== 0 && !state.lastError) {
3750
+ state.lastError = signal ? `signal:${signal}` : `exit:${code}`;
3751
+ }
3752
+ attempt.error = attempt.error || state.lastError || `exit:${code ?? 'unknown'}`;
3753
+ });
3754
+ } catch (err) {
3755
+ attempt.failed = true;
3756
+ attempt.error = err?.message || String(err);
3757
+ state.status = 'failed';
3758
+ state.lastError = attempt.error;
3759
+ state.updatedAt = new Date().toISOString();
3760
+ }
3761
+
3762
+ return attempt;
3763
+ }
3764
+
3765
+ async function stopRemoteAgentAttempt(attempt, reason = 'race-lost') {
3766
+ if (!attempt?.child) {
3767
+ return;
3768
+ }
3769
+
3770
+ if (attempt.child.exitCode !== null || attempt.child.killed) {
3771
+ return;
3772
+ }
3773
+
3774
+ try {
3775
+ await terminateProcessTree(attempt.child);
3776
+ } catch (err) {
3777
+ logWarn('remote', `managed RemoteAgent cleanup failed ${formatKeyValue('manager', attempt.manager || '-')} ${formatKeyValue('reason', reason)} ${formatKeyValue('error', err?.message || err)}`);
3778
+ }
3779
+ }
3780
+
3781
+ async function startRemoteAgentConnectionRace(options = {}) {
3782
+ const managers = normalizeRemoteManagerEndpointList(options.managers).slice(0, REMOTE_AGENT_MAX_PARALLEL_CANDIDATES);
3783
+ const pairToken = safeRemoteAgentField(options.pairToken || options.pair, 512);
3784
+ const leaseId = safeRemoteAgentField(options.leaseId, 128);
3785
+ const nodeId = safeRemoteAgentField(options.nodeId, 128);
3786
+ const source = safeRemoteAgentField(options.source || 'registry', 64);
3787
+ const engine = normalizeRemoteAgentEngine(options.engine);
3788
+ const launcher = resolveRemoteAgentLauncher(engine);
3789
+ if (launcher?.ok === false) {
3790
+ const error = launcher.error || 'remote-agent-launcher-unavailable';
3791
+ remoteAgentState = createRemoteAgentIdleState({
3792
+ status: 'failed',
3793
+ manager: managers[0] || '',
3794
+ managerCandidates: managers,
3795
+ leaseId,
3796
+ nodeId,
3797
+ engine,
3798
+ source,
3799
+ lastError: error,
3800
+ updatedAt: new Date().toISOString()
3801
+ });
3802
+ emitBridgeEvent('RemoteAgentFailed', serializeRemoteAgentState());
3803
+ return { ok: false, error, agent: serializeRemoteAgentState() };
3804
+ }
3805
+
3806
+ remoteAgentState = createRemoteAgentIdleState({
3807
+ status: 'starting',
3808
+ manager: managers[0] || '',
3809
+ managerCandidates: managers,
3810
+ leaseId,
3811
+ nodeId,
3812
+ engine,
3813
+ source,
3814
+ startedAt: new Date().toISOString(),
3815
+ launcher: 'race',
3816
+ usingNpx: false
3817
+ });
3818
+
3819
+ const attempts = [];
3389
3820
  for (let index = 0; index < managers.length; index += 1) {
3390
3821
  const manager = managers[index];
3391
- if (managers.length > 1) {
3392
- logEvent(
3393
- 'remote',
3394
- `managed RemoteAgent candidate ${index + 1}/${managers.length} ${formatKeyValue('manager', manager)}`,
3395
- 'remote');
3396
- }
3822
+ logEvent(
3823
+ 'remote',
3824
+ `managed RemoteAgent race ${index + 1}/${managers.length} ${formatKeyValue('manager', manager)}`,
3825
+ 'remote');
3397
3826
 
3398
- const result = await startRemoteAgentConnectionAttempt({
3827
+ const attempt = createRemoteAgentAttempt({
3399
3828
  manager,
3400
3829
  managerCandidates: managers,
3401
3830
  pairToken,
3402
3831
  leaseId,
3403
3832
  nodeId,
3404
3833
  source,
3405
- engine
3834
+ engine,
3835
+ launcher
3406
3836
  });
3407
- if (result?.ok === true) {
3408
- return result;
3837
+ attempts.push(attempt);
3838
+
3839
+ if (index + 1 < managers.length && REMOTE_AGENT_RACE_START_STAGGER_MS > 0) {
3840
+ await new Promise(resolve => setTimeout(resolve, REMOTE_AGENT_RACE_START_STAGGER_MS));
3409
3841
  }
3842
+ }
3410
3843
 
3411
- lastResult = result;
3412
- if (isRemoteAgentProcessRunning()) {
3413
- await stopRemoteAgentConnection(`candidate-failed:${manager}`);
3844
+ const startedAt = Date.now();
3845
+ let winner = null;
3846
+ while (Date.now() - startedAt < REMOTE_AGENT_READY_TIMEOUT_MS) {
3847
+ winner = attempts.find(attempt =>
3848
+ attempt?.state?.ready === true
3849
+ && attempt?.child
3850
+ && attempt.child.exitCode === null
3851
+ && !attempt.child.killed);
3852
+ if (winner) {
3853
+ break;
3414
3854
  }
3415
3855
 
3416
- if (index + 1 < managers.length) {
3417
- logWarn(
3418
- 'remote',
3419
- `managed RemoteAgent candidate failed; trying next ${formatKeyValue('manager', manager)} ${formatKeyValue('error', result?.error || 'unknown')}`);
3856
+ const allFinished = attempts.every(attempt =>
3857
+ attempt.failed === true
3858
+ || !attempt.child
3859
+ || attempt.child.exitCode !== null
3860
+ || attempt.child.killed);
3861
+ if (allFinished) {
3862
+ break;
3420
3863
  }
3864
+
3865
+ await new Promise(resolve => setTimeout(resolve, 60));
3421
3866
  }
3422
3867
 
3868
+ if (winner) {
3869
+ for (const attempt of attempts) {
3870
+ if (attempt !== winner) {
3871
+ await stopRemoteAgentAttempt(attempt, 'race-lost');
3872
+ }
3873
+ }
3874
+
3875
+ remoteAgentState = winner.state;
3876
+ remoteAgentState.status = 'running';
3877
+ remoteAgentState.proc = winner.child;
3878
+ remoteAgentState.updatedAt = new Date().toISOString();
3879
+ rememberRemoteAgentSuccessfulManager(winner.manager, leaseId);
3880
+ emitBridgeEvent('RemoteAgentStarted', serializeRemoteAgentState());
3881
+ logEvent(
3882
+ 'remote',
3883
+ `managed RemoteAgent connected ${formatKeyValue('manager', winner.manager)} ${formatKeyValue('launcher', winner.launcher?.launcher || '-')}`,
3884
+ 'remote');
3885
+ return { ok: true, alreadyRunning: false, agent: serializeRemoteAgentState() };
3886
+ }
3887
+
3888
+ const lastAttempt = attempts.findLast?.(attempt => attempt?.error || attempt?.state?.lastError)
3889
+ || attempts[attempts.length - 1]
3890
+ || null;
3891
+ for (const attempt of attempts) {
3892
+ await stopRemoteAgentAttempt(attempt, 'race-failed');
3893
+ }
3894
+
3895
+ const error = lastAttempt
3896
+ ? summarizeRemoteAgentAttempt(lastAttempt)
3897
+ : 'all-manager-candidates-failed';
3898
+ remoteAgentState = createRemoteAgentIdleState({
3899
+ status: 'failed',
3900
+ manager: managers[0] || '',
3901
+ managerCandidates: managers,
3902
+ leaseId,
3903
+ nodeId,
3904
+ engine,
3905
+ source,
3906
+ lastError: error,
3907
+ updatedAt: new Date().toISOString()
3908
+ });
3909
+ emitBridgeEvent('RemoteAgentFailed', serializeRemoteAgentState());
3423
3910
  return {
3424
3911
  ok: false,
3425
- error: lastResult?.error || 'all-manager-candidates-failed',
3426
- agent: lastResult?.agent || serializeRemoteAgentState()
3912
+ error,
3913
+ agent: serializeRemoteAgentState()
3427
3914
  };
3428
3915
  }
3429
3916
 
@@ -3445,17 +3932,11 @@ async function startRemoteAgentConnectionAttempt(options = {}) {
3445
3932
  }
3446
3933
 
3447
3934
  const connectionKey = createRemoteAgentConnectionKey(manager, leaseId);
3448
- const launcher = resolveRemoteAgentLauncher();
3449
- const args = [
3450
- ...launcher.argsPrefix,
3451
- 'connect',
3452
- '--manager',
3453
- manager,
3454
- '--pair',
3455
- pairToken,
3456
- '--engine',
3457
- engine
3458
- ];
3935
+ const launcher = resolveRemoteAgentLauncher(engine);
3936
+ if (launcher?.ok === false) {
3937
+ return { ok: false, error: launcher.error || 'remote-agent-launcher-unavailable', agent: serializeRemoteAgentState() };
3938
+ }
3939
+ const args = buildRemoteAgentLaunchArgs(launcher, manager, pairToken, engine);
3459
3940
 
3460
3941
  remoteAgentState = createRemoteAgentIdleState({
3461
3942
  status: 'starting',
@@ -8065,7 +8546,7 @@ app.get('/api/remote/status', (req, res) => {
8065
8546
 
8066
8547
  app.get('/api/remote/devices', (req, res) => {
8067
8548
  res.setHeader('Cache-Control', 'no-store');
8068
- const includeDataUrl = !/^(0|false|no|url|metadata)$/i.test(String(req.query?.includeDataUrl ?? req.query?.frameData ?? ''));
8549
+ const includeDataUrl = /^(1|true|yes|on|data|base64)$/i.test(String(req.query?.includeDataUrl ?? req.query?.frameData ?? ''));
8069
8550
  const devices = remoteHub.listDevices({ includeDataUrl });
8070
8551
  res.json({
8071
8552
  total: devices.length,
@@ -8285,7 +8766,7 @@ function isRemoteCapabilityEnabled(device, key) {
8285
8766
  }
8286
8767
 
8287
8768
  function includeRemoteFrameDataUrl(req) {
8288
- return !/^(0|false|no|url|metadata)$/i.test(String(req.query?.includeDataUrl ?? req.query?.frameData ?? ''));
8769
+ return /^(1|true|yes|on|data|base64)$/i.test(String(req.query?.includeDataUrl ?? req.query?.frameData ?? ''));
8289
8770
  }
8290
8771
 
8291
8772
  function sendRemoteFrameBinary(req, res, frameKind) {
@@ -12221,11 +12221,119 @@
12221
12221
  }
12222
12222
  }
12223
12223
 
12224
+ function isTemplateCardCanvasControlTarget(target) {
12225
+ return !!target?.closest?.([
12226
+ 'button',
12227
+ 'a',
12228
+ 'input',
12229
+ 'textarea',
12230
+ 'select',
12231
+ '[contenteditable="true"]',
12232
+ '[data-template-card-interactive="true"]',
12233
+ '[data-template-card-action]',
12234
+ '[data-template-option-id]',
12235
+ '[data-remote-fleet-action]',
12236
+ '[data-remote-fleet-control-popup]',
12237
+ '[data-remote-fleet-task-input]',
12238
+ '[data-remote-fleet-ai-toggle]',
12239
+ '[data-remote-fleet-search]',
12240
+ '.csv-table-scroll',
12241
+ '.csv-table-header-button',
12242
+ '.csv-table-cell'
12243
+ ].join(', '));
12244
+ }
12245
+
12246
+ function routeTemplateCardWheelToCanvas(event) {
12247
+ if (!_module || !event) {
12248
+ return false;
12249
+ }
12250
+
12251
+ if (typeof window.MindMapInteractions?.routeCanvasWheelEvent === 'function') {
12252
+ window.MindMapInteractions.routeCanvasWheelEvent(_module, event);
12253
+ return true;
12254
+ }
12255
+
12256
+ if (window.MindMapInteractions?.shouldTreatWheelAsZoom?.(_module, event) === true) {
12257
+ window.MindMapInteractions?.routeWheelEventToZoom?.(_module, event);
12258
+ } else {
12259
+ window.MindMapInteractions?.routeCtrlWheelEventToVerticalScroll?.(_module, event);
12260
+ }
12261
+ return true;
12262
+ }
12263
+
12264
+ function bindTemplateCardCanvasNavigation(container, nodeModel) {
12265
+ const card = container?.classList?.contains?.('map-node-template-card')
12266
+ ? container
12267
+ : container?.querySelector?.('.map-node-template-card');
12268
+ if (!card || typeof card.addEventListener !== 'function') {
12269
+ return;
12270
+ }
12271
+
12272
+ if (card._templateCardCanvasNavigationModule === _module
12273
+ && card._templateCardWheelNavigationHandler
12274
+ && card._templateCardPanNavigationHandler) {
12275
+ return;
12276
+ }
12277
+
12278
+ if (card._templateCardWheelNavigationHandler) {
12279
+ card.removeEventListener('wheel', card._templateCardWheelNavigationHandler, true);
12280
+ }
12281
+ if (card._templateCardPanNavigationHandler) {
12282
+ card.removeEventListener('mousedown', card._templateCardPanNavigationHandler, true);
12283
+ }
12284
+
12285
+ const nodeId = String(getNodeId(nodeModel) || card.dataset?.nodeId || '').trim();
12286
+ const isRemoteFleetCard = isRemoteFleetMonitorNode(nodeModel);
12287
+
12288
+ const wheelHandler = event => {
12289
+ routeTemplateCardWheelToCanvas(event);
12290
+ };
12291
+
12292
+ const panHandler = event => {
12293
+ const button = Number(event?.button);
12294
+ if (button === 1 || button === 2) {
12295
+ const started = window.MindMapInteractions?.beginPanFromMouseEvent?.(
12296
+ _module,
12297
+ event,
12298
+ card
12299
+ ) === true;
12300
+ if (started) {
12301
+ event.stopImmediatePropagation?.();
12302
+ }
12303
+ return;
12304
+ }
12305
+
12306
+ if (button !== 0 || isRemoteFleetCard || !nodeId || isTemplateCardCanvasControlTarget(event.target)) {
12307
+ return;
12308
+ }
12309
+
12310
+ const started = window.MindMapInteractions?.startNodeDragFromDom?.(
12311
+ _module,
12312
+ event,
12313
+ nodeId,
12314
+ { source: 'template-card' }
12315
+ ) === true;
12316
+ if (started) {
12317
+ event.stopImmediatePropagation?.();
12318
+ }
12319
+ };
12320
+
12321
+ card._templateCardCanvasNavigationModule = _module;
12322
+ card._templateCardWheelNavigationHandler = wheelHandler;
12323
+ card._templateCardPanNavigationHandler = panHandler;
12324
+ card.addEventListener('wheel', wheelHandler, { passive: false, capture: true });
12325
+ card.addEventListener('mousedown', panHandler, { passive: false, capture: true });
12326
+ }
12327
+
12224
12328
  function bindTemplateLauncherNodeEvents(container, nodeModel) {
12225
12329
  const card = container?.classList?.contains?.('map-node-template-card')
12226
12330
  ? container
12227
12331
  : container?.querySelector?.('.map-node-template-card');
12228
- if (!card || card.dataset.templateCardEventsBound === 'true') return;
12332
+ if (!card) return;
12333
+
12334
+ bindTemplateCardCanvasNavigation(card, nodeModel);
12335
+
12336
+ if (card.dataset.templateCardEventsBound === 'true') return;
12229
12337
 
12230
12338
  card.dataset.templateCardEventsBound = 'true';
12231
12339
  card._templateCardConfig = getTemplateCardConfig(nodeModel);
@@ -13995,6 +14103,7 @@
13995
14103
  function isRemoteFleetFrameSource(value) {
13996
14104
  const source = String(value || '').trim();
13997
14105
  if (/^data:image\/(png|jpe?g|webp|svg\+xml);base64,/i.test(source)
14106
+ || /^blob:/i.test(source)
13998
14107
  || /^\/api\/remote\//i.test(source)) {
13999
14108
  return true;
14000
14109
  }
@@ -14266,7 +14375,18 @@
14266
14375
  return null;
14267
14376
  }
14268
14377
 
14269
- if (typeof fetch !== 'function' || typeof createImageBitmap !== 'function') {
14378
+ if (typeof createImageBitmap !== 'function') {
14379
+ return null;
14380
+ }
14381
+
14382
+ const timeoutMs = getRemoteFleetFrameDecodeTimeoutMs(frame);
14383
+ if (frame._remoteFleetPayloadBlob) {
14384
+ return await withRemoteFleetFrameTimeout(
14385
+ createImageBitmap(frame._remoteFleetPayloadBlob),
14386
+ timeoutMs);
14387
+ }
14388
+
14389
+ if (typeof fetch !== 'function') {
14270
14390
  return null;
14271
14391
  }
14272
14392
 
@@ -14304,7 +14424,6 @@
14304
14424
  entry.expiresAt = now + REMOTE_FLEET_FRAME_BLOB_CACHE_MS;
14305
14425
  }
14306
14426
 
14307
- const timeoutMs = getRemoteFleetFrameDecodeTimeoutMs(frame);
14308
14427
  const blob = await withRemoteFleetFrameTimeout(
14309
14428
  entry.promise,
14310
14429
  timeoutMs,
@@ -14323,7 +14442,7 @@
14323
14442
  }
14324
14443
 
14325
14444
  function cloneRemoteFleetFramePatch(frame) {
14326
- return {
14445
+ const patch = {
14327
14446
  deviceId: String(frame?.deviceId || '').trim(),
14328
14447
  kind: String(frame?.kind || 'thumbnail').trim().toLowerCase(),
14329
14448
  frameSeq: Number.isFinite(Number(frame?.frameSeq)) ? Math.floor(Number(frame.frameSeq)) : 0,
@@ -14335,6 +14454,16 @@
14335
14454
  captureMs: Number(frame?.captureMs || 0) || 0,
14336
14455
  sameContentStreak: Number(frame?.sameContentStreak || 0) || 0
14337
14456
  };
14457
+ if (frame?._remoteFleetPayloadBlob) {
14458
+ patch._remoteFleetPayloadBlob = frame._remoteFleetPayloadBlob;
14459
+ }
14460
+ if (frame?._remoteFleetObjectUrl) {
14461
+ patch._remoteFleetObjectUrl = frame._remoteFleetObjectUrl;
14462
+ }
14463
+ if (frame?._remoteFleetBinaryFrame === true) {
14464
+ patch._remoteFleetBinaryFrame = true;
14465
+ }
14466
+ return patch;
14338
14467
  }
14339
14468
 
14340
14469
  function isRemoteFleetFrameNewerForPreview(preview, frame, comparePending = false) {
@@ -14435,23 +14564,7 @@
14435
14564
  placeholder.remove();
14436
14565
  }
14437
14566
 
14438
- const previewMode = String(preview?.dataset?.remoteFleetDevicePreview || '');
14439
- if (previewMode === 'tile') {
14440
- preview.querySelector?.('[data-remote-fleet-frame-badge="true"]')?.remove();
14441
- return true;
14442
- }
14443
-
14444
- const badge = ensureRemoteFleetFrameBadge(preview, frame);
14445
- if (badge) {
14446
- const at = frame.receivedAt || frame.capturedAt || '';
14447
- badge.textContent = frame.kind === 'live'
14448
- ? `LIVE ${at ? formatRemoteFleetAge(at) : ''}`.trim()
14449
- : (at ? formatRemoteFleetAge(at) : 'Screen');
14450
- badge.style.display = 'inline-flex';
14451
- badge.style.background = frame.kind === 'live'
14452
- ? 'rgba(220, 38, 38, 0.84)'
14453
- : 'rgba(15, 23, 42, 0.72)';
14454
- }
14567
+ preview.querySelector?.('[data-remote-fleet-frame-badge="true"]')?.remove();
14455
14568
 
14456
14569
  return true;
14457
14570
  }
@@ -14772,11 +14885,10 @@
14772
14885
 
14773
14886
  function applyRemoteFleetFramePatchToPreview(preview, frame) {
14774
14887
  if (String(frame?.kind || '').toLowerCase() === 'live') {
14775
- preview._remoteFleetPendingFrame = null;
14776
- delete preview.dataset.remoteFleetPendingFrameKind;
14777
- delete preview.dataset.remoteFleetPendingFrameSeq;
14778
- delete preview.dataset.remoteFleetPendingFrameUrl;
14779
- return queueRemoteFleetImageSwap(preview, frame);
14888
+ preview._remoteFleetPendingImageFrame = null;
14889
+ delete preview.dataset.remoteFleetPendingImageKind;
14890
+ delete preview.dataset.remoteFleetPendingImageSeq;
14891
+ delete preview.dataset.remoteFleetPendingImageUrl;
14780
14892
  }
14781
14893
 
14782
14894
  return queueRemoteFleetFramePaint(preview, frame);
@@ -14856,6 +14968,16 @@
14856
14968
  return;
14857
14969
  }
14858
14970
 
14971
+ const binarySession = bodyView._remoteFleetBinaryFrameSession;
14972
+ const binarySocket = binarySession?.ws || null;
14973
+ if (binarySocket
14974
+ && typeof WebSocket !== 'undefined'
14975
+ && (binarySocket.readyState === WebSocket.OPEN || binarySocket.readyState === WebSocket.CONNECTING)) {
14976
+ lastTick = timestamp;
14977
+ bodyView._remoteFleetFrameLoopRaf = requestRemoteFleetFrameLoopFrame(loop);
14978
+ return;
14979
+ }
14980
+
14859
14981
  if (timestamp - lastTick >= intervalMs && bodyView._remoteFleetFrameRefreshInFlight !== true) {
14860
14982
  lastTick = timestamp;
14861
14983
  bodyView._remoteFleetFrameRefreshInFlight = true;
@@ -15885,29 +16007,6 @@
15885
16007
  `;
15886
16008
  preview.appendChild(placeholder);
15887
16009
  }
15888
- const previewBadge = document.createElement('span');
15889
- previewBadge.dataset.remoteFleetFrameBadge = 'true';
15890
- previewBadge.textContent = hasLiveFrame
15891
- ? `LIVE ${previewAt ? formatRemoteFleetAge(previewAt) : ''}`.trim()
15892
- : (previewAt ? formatRemoteFleetAge(previewAt) : (hubStatus === 'online' ? 'Pinned' : 'Hub offline'));
15893
- previewBadge.style.cssText = `
15894
- position: absolute;
15895
- left: 7px;
15896
- bottom: 7px;
15897
- max-width: calc(100% - 14px);
15898
- padding: 4px 7px;
15899
- border-radius: 999px;
15900
- background: ${hasLiveFrame ? 'rgba(220, 38, 38, 0.84)' : 'rgba(15, 23, 42, 0.74)'};
15901
- color: #e2e8f0;
15902
- font-size: 9px;
15903
- font-weight: 950;
15904
- line-height: 1;
15905
- overflow: hidden;
15906
- text-overflow: ellipsis;
15907
- white-space: nowrap;
15908
- letter-spacing: 0;
15909
- `;
15910
- preview.appendChild(previewBadge);
15911
16010
  bodyView.appendChild(preview);
15912
16011
 
15913
16012
  const stats = document.createElement('div');
@@ -16256,6 +16355,15 @@
16256
16355
  if (bodyView._remoteFleetNodeSelectionBound !== true) {
16257
16356
  bodyView._remoteFleetNodeSelectionBound = true;
16258
16357
  bodyView.addEventListener('mousedown', event => {
16358
+ if (event.button === 1 || event.button === 2) {
16359
+ window.MindMapInteractions?.beginPanFromMouseEvent?.(_module, event, bodyView);
16360
+ return;
16361
+ }
16362
+
16363
+ if (event.button !== 0) {
16364
+ return;
16365
+ }
16366
+
16259
16367
  const controlTarget = event.target?.closest?.('button, input, textarea, select, [contenteditable="true"]');
16260
16368
  if (controlTarget) {
16261
16369
  return;
@@ -17297,7 +17405,16 @@
17297
17405
  });
17298
17406
  getDeviceCards().forEach(card => {
17299
17407
  ['mousedown', 'mouseup', 'dblclick'].forEach(eventName => {
17300
- card.addEventListener(eventName, event => event.stopPropagation());
17408
+ card.addEventListener(eventName, event => {
17409
+ if (event.button === 1 || event.button === 2) {
17410
+ if (eventName === 'mousedown') {
17411
+ window.MindMapInteractions?.beginPanFromMouseEvent?.(_module, event, card);
17412
+ }
17413
+ return;
17414
+ }
17415
+
17416
+ event.stopPropagation();
17417
+ });
17301
17418
  });
17302
17419
  card.addEventListener('dblclick', event => {
17303
17420
  event.preventDefault();
@@ -20882,6 +20999,9 @@
20882
20999
  && !isRemoteFleetMonitorNode(nodeModel)) {
20883
21000
  bindTemplateLauncherNodeEvents(clonedElement, nodeModel);
20884
21001
  }
21002
+ if (String(nodeModel.contentType ?? nodeModel.ContentType ?? '').toLowerCase() === 'templatelauncher') {
21003
+ bindTemplateCardCanvasNavigation(clonedElement, nodeModel);
21004
+ }
20885
21005
  wrapper.appendChild(clonedElement);
20886
21006
  if (String(nodeModel.contentType ?? nodeModel.ContentType ?? '').toLowerCase() !== 'templatelauncher'
20887
21007
  || isRemoteFleetMonitorNode(nodeModel)) {
@@ -21115,6 +21235,7 @@
21115
21235
 
21116
21236
  const bodyView = card.querySelector('.template-card__remote-fleet-body');
21117
21237
  renderRemoteFleetMonitor(bodyView, nodeModel);
21238
+ bindTemplateCardCanvasNavigation(card, nodeModel);
21118
21239
  return;
21119
21240
  }
21120
21241
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "mainAssemblyName": "MindExecution.Web",
3
3
  "resources": {
4
- "hash": "sha256-Sm5LEcaIEbOQMLRdM7FmN29gN1AP1yTcQOVH+E/ZgGY=",
4
+ "hash": "sha256-o8W1U0lBmrbs5unXcwG+/IEh2KogSA7Rk3MqjQnNfGU=",
5
5
  "fingerprinting": {
6
6
  "Google.Protobuf.9h59ukbel7.dll": "Google.Protobuf.dll",
7
7
  "Markdig.d1j7v41cl1.dll": "Markdig.dll",
@@ -131,7 +131,7 @@
131
131
  "MindExecution.Plugins.Directory.jc0g7fvyzv.dll": "MindExecution.Plugins.Directory.dll",
132
132
  "MindExecution.Plugins.PlanMaster.2sugb66h95.dll": "MindExecution.Plugins.PlanMaster.dll",
133
133
  "MindExecution.Plugins.YouTube.vy0nsbcoo4.dll": "MindExecution.Plugins.YouTube.dll",
134
- "MindExecution.Shared.e8lipb57xv.dll": "MindExecution.Shared.dll",
134
+ "MindExecution.Shared.uwkrc41o75.dll": "MindExecution.Shared.dll",
135
135
  "MindExecution.Web.994sb3nzuy.dll": "MindExecution.Web.dll",
136
136
  "dotnet.js": "dotnet.js",
137
137
  "dotnet.native.qc8g39g30v.js": "dotnet.native.js",
@@ -282,7 +282,7 @@
282
282
  "MindExecution.Kernel.3zom09ir6j.dll": "sha256-78vR2SNkF9FwdQv0fEItSyz4HzgyzwGUROJAnpripPA=",
283
283
  "MindExecution.Plugins.Concept.lxunwcdjgq.dll": "sha256-WVyvjaxW6UoGQvGW0icChLsXwYVCcdn0BLTftzlkCKc=",
284
284
  "MindExecution.Plugins.PlanMaster.2sugb66h95.dll": "sha256-Lp0AieRHDo2H4Yl0Il1pPruy9nFUHJsX9t1AblXGD6Y=",
285
- "MindExecution.Shared.e8lipb57xv.dll": "sha256-Au9QMwnm9MaP2+xzdldeQYtj4tRXzwOBWYOxmYR8spM=",
285
+ "MindExecution.Shared.uwkrc41o75.dll": "sha256-YjF+7VcGo3+nB0fsa2nh3AiaRyrHS1AL6d6Mq4RWKqo=",
286
286
  "MindExecution.Web.994sb3nzuy.dll": "sha256-jQshtLmhEC4xenzdD9PqHjaCuKvJf/frzaKBi2weDAg="
287
287
  },
288
288
  "lazyAssembly": {
@@ -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-ws-control-frames-v573" />
11
- <link rel="stylesheet" href="_content/MindExecution.Shared/css/mind-map-overrides.css?v=20260616-remote-ws-control-frames-v573" />
10
+ <link rel="stylesheet" href="_content/MindExecution.Shared/css/app.css?v=20260616-mdm-ws-fast-v575" />
11
+ <link rel="stylesheet" href="_content/MindExecution.Shared/css/mind-map-overrides.css?v=20260616-mdm-ws-fast-v575" />
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-ws-control-frames-v573';
582
+ const scriptVersion = '20260616-mdm-ws-fast-v575';
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": "31UVI5Fx",
2
+ "version": "vnLn5hRl",
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-vpLVZnaAN2JQkbnKVrjBOCGkbHU0uveNB4vqR1sMyHg=",
89
+ "hash": "sha256-eGHfAJwh8N3LmDZTr3/anW3CVrYCEYowGfK4N8KPzik=",
90
90
  "url": "_content/MindExecution.Shared/js/mind-map-css3d-manager.js"
91
91
  },
92
92
  {
@@ -442,8 +442,8 @@
442
442
  "url": "_framework/MindExecution.Plugins.YouTube.vy0nsbcoo4.dll"
443
443
  },
444
444
  {
445
- "hash": "sha256-Au9QMwnm9MaP2+xzdldeQYtj4tRXzwOBWYOxmYR8spM=",
446
- "url": "_framework/MindExecution.Shared.e8lipb57xv.dll"
445
+ "hash": "sha256-YjF+7VcGo3+nB0fsa2nh3AiaRyrHS1AL6d6Mq4RWKqo=",
446
+ "url": "_framework/MindExecution.Shared.uwkrc41o75.dll"
447
447
  },
448
448
  {
449
449
  "hash": "sha256-jQshtLmhEC4xenzdD9PqHjaCuKvJf/frzaKBi2weDAg=",
@@ -770,7 +770,7 @@
770
770
  "url": "_framework/Websocket.Client.vapounvmnl.dll"
771
771
  },
772
772
  {
773
- "hash": "sha256-XSS0rv+S2JAW90BVDbFUd23REh87EAC1P5aFjXSMAGo=",
773
+ "hash": "sha256-lL/HCjpHZ3+EWJkTvNj72sH8NLl/eSU2UZDTr6bP99c=",
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-VJOyHChPIe+gqQk3r5g4qTjsKYTrLXoP6I5L+o4ZYzE=",
837
+ "hash": "sha256-IYebw41Eytj+5xl0lRltZ9R7wOwuHg0mlcgar2MiudE=",
838
838
  "url": "index.html"
839
839
  },
840
840
  {
@@ -1,4 +1,4 @@
1
- /* Manifest version: 31UVI5Fx */
1
+ /* Manifest version: vnLn5hRl */
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