@mindexec/cli 0.2.9 → 0.2.11

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.
@@ -3401,6 +3401,7 @@
3401
3401
  const MEDIA_GENERATION_STATUS_MESSAGE_METADATA_KEY = 'MediaGenerationStatusMessage';
3402
3402
  const BUSINESS_AUTOMATION_SEMANTIC_TYPE = 'BusinessAutomationNode';
3403
3403
  const REMOTE_FLEET_SEMANTIC_TYPE = 'RemoteFleetMonitor';
3404
+ const REMOTE_FLEET_DEVICE_SEMANTIC_TYPE = 'RemoteFleetDevice';
3404
3405
  const AUTOMATION_NODE_KIND_METADATA_KEY = 'AutomationNodeKind';
3405
3406
  const AUTOMATION_NODE_LABEL_METADATA_KEY = 'AutomationNodeLabel';
3406
3407
  const AUTOMATION_NODE_DESCRIPTION_METADATA_KEY = 'AutomationNodeDescription';
@@ -3574,6 +3575,16 @@
3574
3575
  return getNodeSemanticType(nodeModel) === REMOTE_FLEET_SEMANTIC_TYPE;
3575
3576
  }
3576
3577
 
3578
+ function isRemoteFleetDeviceNode(nodeModel) {
3579
+ return getNodeSemanticType(nodeModel) === REMOTE_FLEET_DEVICE_SEMANTIC_TYPE;
3580
+ }
3581
+
3582
+ function isRemoteFleetNode(nodeModel) {
3583
+ const semanticType = getNodeSemanticType(nodeModel);
3584
+ return semanticType === REMOTE_FLEET_SEMANTIC_TYPE
3585
+ || semanticType === REMOTE_FLEET_DEVICE_SEMANTIC_TYPE;
3586
+ }
3587
+
3577
3588
  function isBusinessAutomationContextSourceNode(nodeModel) {
3578
3589
  if (!nodeModel || isBusinessAutomationNode(nodeModel)) {
3579
3590
  return false;
@@ -3588,7 +3599,8 @@
3588
3599
  return semanticType === 'MindCanvasAgent'
3589
3600
  || semanticType === 'AgentCommand'
3590
3601
  || semanticType === BUSINESS_AUTOMATION_SEMANTIC_TYPE
3591
- || semanticType === REMOTE_FLEET_SEMANTIC_TYPE;
3602
+ || semanticType === REMOTE_FLEET_SEMANTIC_TYPE
3603
+ || semanticType === REMOTE_FLEET_DEVICE_SEMANTIC_TYPE;
3592
3604
  }
3593
3605
 
3594
3606
  function allowsMemoExternalChrome(nodeModel) {
@@ -11845,6 +11857,11 @@
11845
11857
  return;
11846
11858
  }
11847
11859
 
11860
+ if (isRemoteFleetDeviceNode(nodeModel)) {
11861
+ renderRemoteFleetDevice(bodyView, nodeModel);
11862
+ return;
11863
+ }
11864
+
11848
11865
  bodyView.classList.remove('map-node-remote-fleet__body');
11849
11866
  bodyView.style.cssText = `
11850
11867
  flex: 1 1 auto;
@@ -11954,6 +11971,22 @@
11954
11971
  }
11955
11972
  }
11956
11973
 
11974
+ function parseRemoteFleetPinnedDevice(nodeModel) {
11975
+ const raw = getRemoteFleetMetadataValue(nodeModel, 'RemoteFleetPinnedDeviceJson', '{}');
11976
+ if (!raw.trim()) {
11977
+ return null;
11978
+ }
11979
+
11980
+ try {
11981
+ const parsed = JSON.parse(raw);
11982
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
11983
+ ? parsed
11984
+ : null;
11985
+ } catch {
11986
+ return null;
11987
+ }
11988
+ }
11989
+
11957
11990
  function formatRemoteFleetNumber(value, digits = 0) {
11958
11991
  const number = Number(value);
11959
11992
  return Number.isFinite(number) ? number.toFixed(digits) : '-';
@@ -12300,6 +12333,501 @@
12300
12333
  }
12301
12334
  }
12302
12335
 
12336
+ function renderRemoteFleetDevice(bodyView, nodeModel) {
12337
+ if (!bodyView) return;
12338
+ clearRemoteFleetTimers(bodyView);
12339
+
12340
+ const nodeId = String(nodeModel?.id ?? nodeModel?.Id ?? '');
12341
+ const device = parseRemoteFleetPinnedDevice(nodeModel);
12342
+ const endpoint = getRemoteFleetMetadataValue(nodeModel, 'RemoteFleetHubEndpoint', '127.0.0.1:5197');
12343
+ const hubStatus = getRemoteFleetMetadataValue(nodeModel, 'RemoteFleetHubStatus', 'offline');
12344
+ const refreshedAt = getRemoteFleetMetadataValue(nodeModel, 'RemoteFleetLastRefreshAtUtc', '');
12345
+ const lastError = getRemoteFleetMetadataValue(nodeModel, 'RemoteFleetLastError', '');
12346
+ const deviceId = getRemoteFleetMetadataValue(nodeModel, 'RemoteFleetPinnedDeviceId', device ? getRemoteFleetDeviceId(device) : '');
12347
+
12348
+ bodyView.dataset.src = `remote-device:${deviceId}:${refreshedAt}`;
12349
+ bodyView.classList.add('map-node-remote-fleet__body');
12350
+ bodyView.innerHTML = '';
12351
+ bodyView.style.cssText = `
12352
+ flex: 1 1 auto;
12353
+ min-height: 0;
12354
+ pointer-events: auto;
12355
+ display: flex;
12356
+ flex-direction: column;
12357
+ gap: 10px;
12358
+ padding: 12px 14px 14px;
12359
+ overflow: hidden;
12360
+ background: linear-gradient(180deg, rgba(248, 250, 252, 0.96), rgba(236, 253, 245, 0.78));
12361
+ `;
12362
+
12363
+ const feedback = document.createElement('div');
12364
+ feedback.dataset.remoteFleetDeviceFeedback = 'true';
12365
+ feedback.style.cssText = `
12366
+ display: none;
12367
+ flex: 0 0 auto;
12368
+ min-height: 22px;
12369
+ padding: 5px 8px;
12370
+ border-radius: 7px;
12371
+ background: rgba(37, 99, 235, 0.08);
12372
+ color: #1d4ed8;
12373
+ font-size: 11px;
12374
+ font-weight: 800;
12375
+ line-height: 1.25;
12376
+ overflow: hidden;
12377
+ text-overflow: ellipsis;
12378
+ white-space: nowrap;
12379
+ `;
12380
+ const setFeedback = (message, tone = 'info') => {
12381
+ feedback.textContent = message || '';
12382
+ feedback.style.display = message ? 'block' : 'none';
12383
+ feedback.style.background = tone === 'error'
12384
+ ? 'rgba(248, 113, 113, 0.12)'
12385
+ : tone === 'success'
12386
+ ? 'rgba(16, 185, 129, 0.10)'
12387
+ : 'rgba(37, 99, 235, 0.08)';
12388
+ feedback.style.color = tone === 'error'
12389
+ ? '#991b1b'
12390
+ : tone === 'success'
12391
+ ? '#047857'
12392
+ : '#1d4ed8';
12393
+ };
12394
+
12395
+ const refreshRemoteDeviceNode = async () => {
12396
+ if (bodyView._remoteFleetRefreshInFlight === true) {
12397
+ return null;
12398
+ }
12399
+
12400
+ bodyView._remoteFleetRefreshInFlight = true;
12401
+ try {
12402
+ const result = await invokeDotNetAsync('RefreshRemoteFleetDeviceNodeFromJs', nodeId);
12403
+ await syncRemoteFleetNodeStateFromResult(result);
12404
+ return result;
12405
+ } finally {
12406
+ bodyView._remoteFleetRefreshInFlight = false;
12407
+ }
12408
+ };
12409
+
12410
+ if (!device || !deviceId) {
12411
+ const empty = document.createElement('div');
12412
+ empty.textContent = lastError || 'Pinned remote device snapshot is unavailable.';
12413
+ empty.style.cssText = `
12414
+ flex: 1 1 auto;
12415
+ display: flex;
12416
+ align-items: center;
12417
+ justify-content: center;
12418
+ min-height: 120px;
12419
+ padding: 14px;
12420
+ border-radius: 8px;
12421
+ border: 1px dashed rgba(100, 116, 139, 0.36);
12422
+ color: #475569;
12423
+ font-size: 13px;
12424
+ font-weight: 850;
12425
+ text-align: center;
12426
+ background: rgba(255, 255, 255, 0.74);
12427
+ `;
12428
+ const refreshButton = createRemoteFleetButton('Refresh', 'Refresh this pinned device', 'refresh-device');
12429
+ refreshButton.style.height = '32px';
12430
+ refreshButton.addEventListener('click', async event => {
12431
+ event.preventDefault();
12432
+ event.stopPropagation();
12433
+ refreshButton.disabled = true;
12434
+ try {
12435
+ await refreshRemoteDeviceNode();
12436
+ } finally {
12437
+ refreshButton.disabled = false;
12438
+ }
12439
+ });
12440
+ bodyView.appendChild(empty);
12441
+ bodyView.appendChild(refreshButton);
12442
+ bodyView.appendChild(feedback);
12443
+ return;
12444
+ }
12445
+
12446
+ const name = getRemoteFleetDeviceName(device);
12447
+ const connected = isRemoteFleetDeviceConnected(device);
12448
+ const taskEnabled = isRemoteFleetDeviceTaskCapable(device);
12449
+ const aiAssistEnabled = isRemoteFleetDeviceAiCapable(device);
12450
+ const thumbnailEnabled = isRemoteFleetThumbnailCapable(device);
12451
+ const liveStreamEnabled = getRemoteFleetDeviceField(device, 'liveStreamEnabled', 'LiveStreamEnabled', false) === true;
12452
+ const liveActive = isRemoteFleetLiveActive(device);
12453
+ const liveStreamId = String(getRemoteFleetDeviceField(device, 'liveStreamId', 'LiveStreamId', ''));
12454
+ const hasLiveFrame = hasRemoteFleetLiveFrame(device);
12455
+ const hasThumbnail = hasRemoteFleetThumbnail(device);
12456
+ const previewDataUrl = hasLiveFrame
12457
+ ? String(getRemoteFleetDeviceField(device, 'liveFrameDataUrl', 'LiveFrameDataUrl', ''))
12458
+ : String(getRemoteFleetDeviceField(device, 'thumbnailDataUrl', 'ThumbnailDataUrl', ''));
12459
+ const previewAt = hasLiveFrame
12460
+ ? String(getRemoteFleetDeviceField(device, 'liveFrameReceivedAt', 'LiveFrameReceivedAt', ''))
12461
+ : String(getRemoteFleetDeviceField(device, 'thumbnailReceivedAt', 'ThumbnailReceivedAt',
12462
+ getRemoteFleetDeviceField(device, 'thumbnailCapturedAt', 'ThumbnailCapturedAt', '')));
12463
+ const platform = [
12464
+ getRemoteFleetDeviceField(device, 'platform', 'Platform', 'unknown'),
12465
+ getRemoteFleetDeviceField(device, 'arch', 'Arch', '')
12466
+ ].filter(Boolean).join(' / ');
12467
+ const release = String(getRemoteFleetDeviceField(device, 'release', 'Release', ''));
12468
+ const latestTaskStatus = String(getRemoteFleetDeviceField(device, 'latestTaskStatus', 'LatestTaskStatus', ''));
12469
+ const latestTaskTitle = String(getRemoteFleetDeviceField(device, 'latestTaskTitle', 'LatestTaskTitle', ''));
12470
+ const latestTaskApproval = String(getRemoteFleetDeviceField(device, 'latestTaskApprovalLevel', 'LatestTaskApprovalLevel', ''));
12471
+ const latestTaskUpdatedAt = String(getRemoteFleetDeviceField(device, 'latestTaskUpdatedAt', 'LatestTaskUpdatedAt', ''));
12472
+ const latestTaskError = String(getRemoteFleetDeviceField(device, 'latestTaskError', 'LatestTaskError', ''));
12473
+ const latestTaskResult = String(getRemoteFleetDeviceField(device, 'latestTaskResultSummary', 'LatestTaskResultSummary', ''));
12474
+
12475
+ const header = document.createElement('div');
12476
+ header.style.cssText = 'display:flex;align-items:flex-start;justify-content:space-between;gap:10px;flex:0 0 auto;';
12477
+ const titleBox = document.createElement('div');
12478
+ titleBox.style.cssText = 'min-width:0;display:flex;flex-direction:column;gap:3px;';
12479
+ const title = document.createElement('strong');
12480
+ title.textContent = name;
12481
+ title.title = `${name} (${deviceId})`;
12482
+ title.style.cssText = 'color:#0f172a;font-size:16px;font-weight:950;line-height:1.12;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;letter-spacing:0;';
12483
+ const subtitle = document.createElement('span');
12484
+ subtitle.textContent = `${connected ? 'Online' : 'Offline'} - ${release ? `${platform} ${release}` : platform}`;
12485
+ subtitle.title = subtitle.textContent;
12486
+ subtitle.style.cssText = `color:${connected ? '#047857' : '#64748b'};font-size:11px;font-weight:850;line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;letter-spacing:0;`;
12487
+ titleBox.appendChild(title);
12488
+ titleBox.appendChild(subtitle);
12489
+ const statusBadge = document.createElement('span');
12490
+ statusBadge.textContent = liveActive ? 'LIVE' : (connected ? 'READY' : 'OFFLINE');
12491
+ statusBadge.style.cssText = `
12492
+ flex: 0 0 auto;
12493
+ padding: 5px 8px;
12494
+ border-radius: 999px;
12495
+ background: ${liveActive ? 'rgba(220, 38, 38, 0.12)' : connected ? 'rgba(16, 185, 129, 0.12)' : 'rgba(100, 116, 139, 0.12)'};
12496
+ color: ${liveActive ? '#b91c1c' : connected ? '#047857' : '#475569'};
12497
+ font-size: 10px;
12498
+ font-weight: 950;
12499
+ letter-spacing: 0;
12500
+ `;
12501
+ header.appendChild(titleBox);
12502
+ header.appendChild(statusBadge);
12503
+ bodyView.appendChild(header);
12504
+
12505
+ const preview = document.createElement('div');
12506
+ preview.style.cssText = `
12507
+ position: relative;
12508
+ flex: 0 0 auto;
12509
+ width: 100%;
12510
+ aspect-ratio: 16 / 9;
12511
+ overflow: hidden;
12512
+ border-radius: 8px;
12513
+ background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
12514
+ border: 1px solid rgba(15, 23, 42, 0.16);
12515
+ `;
12516
+ if (hasLiveFrame || hasThumbnail) {
12517
+ const image = document.createElement('img');
12518
+ image.src = previewDataUrl;
12519
+ image.alt = `${name} screen`;
12520
+ image.loading = 'lazy';
12521
+ image.decoding = 'async';
12522
+ image.style.cssText = 'width:100%;height:100%;object-fit:cover;display:block;';
12523
+ preview.appendChild(image);
12524
+ } else {
12525
+ const placeholder = document.createElement('div');
12526
+ placeholder.textContent = thumbnailEnabled ? 'No frame yet' : 'Status only';
12527
+ placeholder.style.cssText = `
12528
+ position: absolute;
12529
+ inset: 0;
12530
+ display: flex;
12531
+ align-items: center;
12532
+ justify-content: center;
12533
+ color: rgba(226, 232, 240, 0.78);
12534
+ font-size: 12px;
12535
+ font-weight: 900;
12536
+ letter-spacing: 0;
12537
+ `;
12538
+ preview.appendChild(placeholder);
12539
+ }
12540
+ const previewBadge = document.createElement('span');
12541
+ previewBadge.textContent = hasLiveFrame
12542
+ ? `LIVE ${previewAt ? formatRemoteFleetAge(previewAt) : ''}`.trim()
12543
+ : (previewAt ? formatRemoteFleetAge(previewAt) : (hubStatus === 'online' ? 'Pinned' : 'Hub offline'));
12544
+ previewBadge.style.cssText = `
12545
+ position: absolute;
12546
+ left: 7px;
12547
+ bottom: 7px;
12548
+ max-width: calc(100% - 14px);
12549
+ padding: 4px 7px;
12550
+ border-radius: 999px;
12551
+ background: ${hasLiveFrame ? 'rgba(220, 38, 38, 0.84)' : 'rgba(15, 23, 42, 0.74)'};
12552
+ color: #e2e8f0;
12553
+ font-size: 9px;
12554
+ font-weight: 950;
12555
+ line-height: 1;
12556
+ overflow: hidden;
12557
+ text-overflow: ellipsis;
12558
+ white-space: nowrap;
12559
+ letter-spacing: 0;
12560
+ `;
12561
+ preview.appendChild(previewBadge);
12562
+ bodyView.appendChild(preview);
12563
+
12564
+ const stats = document.createElement('div');
12565
+ stats.style.cssText = 'display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:7px;flex:0 0 auto;';
12566
+ stats.appendChild(createRemoteFleetStat('Seen', formatRemoteFleetAge(getRemoteFleetDeviceField(device, 'lastSeenAt', 'LastSeenAt', '')), connected ? 'online' : 'default'));
12567
+ stats.appendChild(createRemoteFleetStat('Uptime', formatRemoteFleetDuration(getRemoteFleetDeviceField(device, 'uptimeSec', 'UptimeSec', Number.NaN))));
12568
+ stats.appendChild(createRemoteFleetStat('Mem', formatRemoteFleetPercent(getRemoteFleetDeviceField(device, 'usedMemRatio', 'UsedMemRatio', Number.NaN))));
12569
+ stats.appendChild(createRemoteFleetStat('Load', formatRemoteFleetNumber(getRemoteFleetDeviceField(device, 'load1', 'Load1', Number.NaN), 2)));
12570
+ bodyView.appendChild(stats);
12571
+
12572
+ if (latestTaskStatus || latestTaskTitle || latestTaskResult || latestTaskError) {
12573
+ const taskBox = document.createElement('div');
12574
+ const taskTone = latestTaskError || latestTaskStatus === 'failed'
12575
+ ? 'error'
12576
+ : (latestTaskStatus === 'completed' ? 'done' : 'pending');
12577
+ taskBox.style.cssText = `
12578
+ display: flex;
12579
+ flex-direction: column;
12580
+ gap: 3px;
12581
+ min-width: 0;
12582
+ padding: 8px 9px;
12583
+ border-radius: 8px;
12584
+ background: ${taskTone === 'error' ? 'rgba(248, 113, 113, 0.12)' : taskTone === 'done' ? 'rgba(16, 185, 129, 0.10)' : 'rgba(37, 99, 235, 0.08)'};
12585
+ border: 1px solid ${taskTone === 'error' ? 'rgba(248, 113, 113, 0.24)' : taskTone === 'done' ? 'rgba(16, 185, 129, 0.20)' : 'rgba(37, 99, 235, 0.16)'};
12586
+ flex: 0 0 auto;
12587
+ `;
12588
+ const taskLine = document.createElement('div');
12589
+ const taskModeLabel = latestTaskApproval === 'ai-assist' ? 'AI' : 'Task';
12590
+ taskLine.textContent = `${taskModeLabel} ${latestTaskStatus || 'task'}${latestTaskUpdatedAt ? ` - ${formatRemoteFleetAge(latestTaskUpdatedAt)}` : ''}`;
12591
+ taskLine.style.cssText = `color:${taskTone === 'error' ? '#991b1b' : taskTone === 'done' ? '#047857' : '#1d4ed8'};font-size:10px;font-weight:900;line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;letter-spacing:0;`;
12592
+ const taskSummary = document.createElement('div');
12593
+ taskSummary.textContent = latestTaskError || latestTaskResult || latestTaskTitle || 'Task queued';
12594
+ taskSummary.title = taskSummary.textContent;
12595
+ taskSummary.style.cssText = 'color:#334155;font-size:11px;font-weight:750;line-height:1.3;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;letter-spacing:0;';
12596
+ taskBox.appendChild(taskLine);
12597
+ taskBox.appendChild(taskSummary);
12598
+ bodyView.appendChild(taskBox);
12599
+ }
12600
+
12601
+ const taskInput = document.createElement('textarea');
12602
+ taskInput.dataset.remoteFleetDeviceTaskInput = 'true';
12603
+ taskInput.placeholder = 'Task for this computer';
12604
+ taskInput.rows = 2;
12605
+ taskInput.style.cssText = `
12606
+ width: 100%;
12607
+ min-width: 0;
12608
+ height: 50px;
12609
+ resize: none;
12610
+ border-radius: 8px;
12611
+ border: 1px solid rgba(148, 163, 184, 0.34);
12612
+ background: rgba(255, 255, 255, 0.92);
12613
+ color: #0f172a;
12614
+ padding: 8px 10px;
12615
+ font-size: 12px;
12616
+ line-height: 1.35;
12617
+ font-weight: 700;
12618
+ letter-spacing: 0;
12619
+ outline: none;
12620
+ pointer-events: auto;
12621
+ `;
12622
+ bodyView.appendChild(taskInput);
12623
+
12624
+ const actions = document.createElement('div');
12625
+ actions.style.cssText = 'display:flex;align-items:center;gap:7px;flex-wrap:wrap;flex:0 0 auto;';
12626
+ const refreshButton = createRemoteFleetButton('Refresh', 'Refresh this pinned device', 'refresh-device');
12627
+ refreshButton.style.height = '30px';
12628
+ actions.appendChild(refreshButton);
12629
+ if (connected && thumbnailEnabled) {
12630
+ const shotButton = createRemoteFleetButton('Shot', 'Request thumbnail', 'thumbnail-device');
12631
+ shotButton.dataset.deviceId = deviceId;
12632
+ shotButton.style.height = '30px';
12633
+ actions.appendChild(shotButton);
12634
+ }
12635
+ if (connected && liveStreamEnabled) {
12636
+ const liveButton = createRemoteFleetButton(liveActive ? 'Stop' : 'Live', liveActive ? 'Stop live stream' : 'Start live stream', liveActive ? 'live-stop' : 'live-start');
12637
+ liveButton.dataset.deviceId = deviceId;
12638
+ liveButton.dataset.streamId = liveStreamId;
12639
+ liveButton.style.height = '30px';
12640
+ if (liveActive) {
12641
+ liveButton.style.borderColor = 'rgba(220, 38, 38, 0.32)';
12642
+ liveButton.style.color = '#b91c1c';
12643
+ }
12644
+ actions.appendChild(liveButton);
12645
+ }
12646
+ if (connected && taskEnabled) {
12647
+ const taskButton = createRemoteFleetButton(aiAssistEnabled ? 'Task/AI' : 'Task', 'Dispatch task to this device', 'task-device');
12648
+ taskButton.dataset.deviceId = deviceId;
12649
+ taskButton.style.height = '30px';
12650
+ actions.appendChild(taskButton);
12651
+ }
12652
+ if (connected) {
12653
+ const pingButton = createRemoteFleetButton('Ping', 'Ping device', 'ping-device');
12654
+ pingButton.dataset.deviceId = deviceId;
12655
+ pingButton.style.height = '30px';
12656
+ actions.appendChild(pingButton);
12657
+ }
12658
+ const aiToggleLabel = document.createElement('label');
12659
+ aiToggleLabel.title = 'Use AI assist when this agent exposes it';
12660
+ aiToggleLabel.style.cssText = `
12661
+ display: ${aiAssistEnabled ? 'inline-flex' : 'none'};
12662
+ align-items: center;
12663
+ justify-content: center;
12664
+ gap: 6px;
12665
+ height: 30px;
12666
+ padding: 0 9px;
12667
+ border-radius: 7px;
12668
+ border: 1px solid rgba(14, 165, 233, 0.28);
12669
+ background: rgba(240, 249, 255, 0.92);
12670
+ color: #0369a1;
12671
+ font-size: 11px;
12672
+ font-weight: 900;
12673
+ letter-spacing: 0;
12674
+ cursor: pointer;
12675
+ pointer-events: auto;
12676
+ user-select: none;
12677
+ `;
12678
+ const aiToggle = document.createElement('input');
12679
+ aiToggle.type = 'checkbox';
12680
+ aiToggle.dataset.remoteFleetDeviceAiToggle = 'true';
12681
+ aiToggle.style.cssText = 'width:14px;height:14px;margin:0;accent-color:#0284c7;';
12682
+ const aiToggleText = document.createElement('span');
12683
+ aiToggleText.textContent = 'AI';
12684
+ aiToggleLabel.appendChild(aiToggle);
12685
+ aiToggleLabel.appendChild(aiToggleText);
12686
+ actions.appendChild(aiToggleLabel);
12687
+ bodyView.appendChild(actions);
12688
+ bodyView.appendChild(feedback);
12689
+
12690
+ const footer = document.createElement('div');
12691
+ footer.textContent = `Endpoint ${endpoint} - ${deviceId} - refreshed ${formatRemoteFleetAge(refreshedAt)}`;
12692
+ footer.title = footer.textContent;
12693
+ footer.style.cssText = `
12694
+ margin-top: auto;
12695
+ flex: 0 0 auto;
12696
+ color: #64748b;
12697
+ font-size: 10px;
12698
+ font-weight: 750;
12699
+ line-height: 1.2;
12700
+ overflow: hidden;
12701
+ text-overflow: ellipsis;
12702
+ white-space: nowrap;
12703
+ `;
12704
+ bodyView.appendChild(footer);
12705
+
12706
+ if (lastError.trim()) {
12707
+ setFeedback(lastError, 'error');
12708
+ }
12709
+
12710
+ [taskInput, aiToggleLabel, aiToggle, ...Array.from(actions.querySelectorAll('button'))].forEach(control => {
12711
+ if (!control) return;
12712
+ ['mousedown', 'mouseup', 'click', 'dblclick', 'keydown'].forEach(eventName => {
12713
+ control.addEventListener(eventName, event => event.stopPropagation());
12714
+ });
12715
+ });
12716
+
12717
+ refreshButton.addEventListener('click', async event => {
12718
+ event.preventDefault();
12719
+ event.stopPropagation();
12720
+ refreshButton.disabled = true;
12721
+ try {
12722
+ await refreshRemoteDeviceNode();
12723
+ } finally {
12724
+ refreshButton.disabled = false;
12725
+ }
12726
+ });
12727
+
12728
+ actions.querySelectorAll('[data-remote-fleet-action="thumbnail-device"]').forEach(button => {
12729
+ button.addEventListener('click', async event => {
12730
+ event.preventDefault();
12731
+ event.stopPropagation();
12732
+ button.disabled = true;
12733
+ setFeedback('Requesting thumbnail...');
12734
+ try {
12735
+ const result = await invokeDotNetAsync('RequestRemoteFleetThumbnailFromJs', nodeId, deviceId);
12736
+ await syncRemoteFleetNodeStateFromResult(result);
12737
+ setFeedback(result?.success ? 'Thumbnail refreshed.' : (result?.error || 'Thumbnail failed.'), result?.success ? 'success' : 'error');
12738
+ } finally {
12739
+ button.disabled = false;
12740
+ }
12741
+ });
12742
+ });
12743
+
12744
+ actions.querySelectorAll('[data-remote-fleet-action="live-start"]').forEach(button => {
12745
+ button.addEventListener('click', async event => {
12746
+ event.preventDefault();
12747
+ event.stopPropagation();
12748
+ button.disabled = true;
12749
+ setFeedback('Starting live stream...');
12750
+ try {
12751
+ const result = await invokeDotNetAsync('StartRemoteFleetLiveStreamFromJs', nodeId, deviceId);
12752
+ await syncRemoteFleetNodeStateFromResult(result);
12753
+ setFeedback(result?.success ? 'Live stream started.' : (result?.error || 'Live stream failed.'), result?.success ? 'success' : 'error');
12754
+ } finally {
12755
+ button.disabled = false;
12756
+ }
12757
+ });
12758
+ });
12759
+
12760
+ actions.querySelectorAll('[data-remote-fleet-action="live-stop"]').forEach(button => {
12761
+ button.addEventListener('click', async event => {
12762
+ event.preventDefault();
12763
+ event.stopPropagation();
12764
+ button.disabled = true;
12765
+ setFeedback('Stopping live stream...');
12766
+ try {
12767
+ const result = await invokeDotNetAsync('StopRemoteFleetLiveStreamFromJs', nodeId, deviceId, button.dataset.streamId || '');
12768
+ await syncRemoteFleetNodeStateFromResult(result);
12769
+ setFeedback(result?.success ? 'Live stream stopped.' : (result?.error || 'Live stop failed.'), result?.success ? 'success' : 'error');
12770
+ } finally {
12771
+ button.disabled = false;
12772
+ }
12773
+ });
12774
+ });
12775
+
12776
+ actions.querySelectorAll('[data-remote-fleet-action="ping-device"]').forEach(button => {
12777
+ button.addEventListener('click', async event => {
12778
+ event.preventDefault();
12779
+ event.stopPropagation();
12780
+ button.disabled = true;
12781
+ setFeedback('Pinging device...');
12782
+ try {
12783
+ const result = await invokeDotNetAsync('PingRemoteFleetDeviceFromJs', nodeId, deviceId);
12784
+ await syncRemoteFleetNodeStateFromResult(result);
12785
+ setFeedback(result?.success ? 'Ping queued.' : (result?.error || 'Ping failed.'), result?.success ? 'success' : 'error');
12786
+ } finally {
12787
+ button.disabled = false;
12788
+ }
12789
+ });
12790
+ });
12791
+
12792
+ actions.querySelectorAll('[data-remote-fleet-action="task-device"]').forEach(button => {
12793
+ button.addEventListener('click', async event => {
12794
+ event.preventDefault();
12795
+ event.stopPropagation();
12796
+ const instruction = String(taskInput.value || '').trim();
12797
+ if (!instruction) {
12798
+ setFeedback('Write a task first.', 'error');
12799
+ taskInput.focus();
12800
+ return;
12801
+ }
12802
+
12803
+ button.disabled = true;
12804
+ setFeedback('Dispatching task...');
12805
+ try {
12806
+ const result = await invokeDotNetAsync('DispatchRemoteFleetTaskFromJs', nodeId, deviceId, instruction, aiToggle.checked === true);
12807
+ await syncRemoteFleetNodeStateFromResult(result);
12808
+ setFeedback(result?.success ? 'Task queued.' : (result?.error || 'Task dispatch failed.'), result?.success ? 'success' : 'error');
12809
+ } finally {
12810
+ button.disabled = false;
12811
+ }
12812
+ });
12813
+ });
12814
+
12815
+ if (liveActive) {
12816
+ bodyView._remoteFleetLiveRefreshTimer = setInterval(async () => {
12817
+ if (!document.body.contains(bodyView)) {
12818
+ clearRemoteFleetTimers(bodyView);
12819
+ return;
12820
+ }
12821
+
12822
+ try {
12823
+ await refreshRemoteDeviceNode();
12824
+ } catch {
12825
+ clearRemoteFleetTimers(bodyView);
12826
+ }
12827
+ }, REMOTE_FLEET_LIVE_REFRESH_MS);
12828
+ }
12829
+ }
12830
+
12303
12831
  function renderRemoteFleetMonitor(bodyView, nodeModel) {
12304
12832
  if (!bodyView) return;
12305
12833
  clearRemoteFleetTimers(bodyView);
@@ -13116,7 +13644,7 @@
13116
13644
  }
13117
13645
 
13118
13646
  const actions = document.createElement('div');
13119
- actions.style.cssText = 'display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:auto;';
13647
+ actions.style.cssText = 'display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:auto;';
13120
13648
  const status = document.createElement('span');
13121
13649
  status.textContent = connectedDevice
13122
13650
  ? (liveStreamActive ? 'Live' : (aiAssistEnabled ? `AI ${aiModel || 'ready'}` : 'Connected'))
@@ -13131,6 +13659,13 @@
13131
13659
  white-space: nowrap;
13132
13660
  `;
13133
13661
  actions.appendChild(status);
13662
+ if (deviceId) {
13663
+ const pinButton = createRemoteFleetButton('Pin', 'Pin this device as a canvas node', 'pin-device');
13664
+ pinButton.dataset.deviceId = deviceId;
13665
+ pinButton.style.height = '24px';
13666
+ pinButton.style.fontSize = '10px';
13667
+ actions.appendChild(pinButton);
13668
+ }
13134
13669
  if (connectedDevice && deviceId) {
13135
13670
  const focusButton = createRemoteFleetButton('Focus', 'Show this device in focused live panel', 'live-focus');
13136
13671
  focusButton.dataset.deviceId = deviceId;
@@ -13431,6 +13966,28 @@
13431
13966
  }
13432
13967
  });
13433
13968
 
13969
+ grid.querySelectorAll('[data-remote-fleet-action="pin-device"]').forEach(button => {
13970
+ button.addEventListener('click', async event => {
13971
+ event.preventDefault();
13972
+ event.stopPropagation();
13973
+ const deviceId = String(button.dataset.deviceId || '').trim();
13974
+ if (!deviceId) return;
13975
+ button.disabled = true;
13976
+ setTaskFeedback('Pinning device to canvas...');
13977
+ try {
13978
+ const result = await invokeDotNetAsync('AddRemoteFleetDeviceNodeFromJs', nodeId, deviceId);
13979
+ await syncRemoteFleetNodeStateFromResult(result);
13980
+ if (result?.success) {
13981
+ setTaskFeedback('Pinned device node.', 'success');
13982
+ } else {
13983
+ setTaskFeedback(result?.error || 'Pin failed.', 'error');
13984
+ }
13985
+ } finally {
13986
+ button.disabled = false;
13987
+ }
13988
+ });
13989
+ });
13990
+
13434
13991
  bodyView.querySelectorAll('[data-remote-fleet-action="live-focus"]').forEach(button => {
13435
13992
  button.addEventListener('click', event => {
13436
13993
  event.preventDefault();
@@ -13797,7 +14354,7 @@
13797
14354
  const agentStyledMemo = isAgentStyledMemoNode(nodeModel);
13798
14355
  const mindCanvasAgent = isMindCanvasAgentNode(nodeModel);
13799
14356
  const businessAutomation = isBusinessAutomationNode(nodeModel);
13800
- const remoteFleet = isRemoteFleetMonitorNode(nodeModel);
14357
+ const remoteFleet = isRemoteFleetNode(nodeModel);
13801
14358
  const externalChrome = allowsMemoExternalChrome(nodeModel);
13802
14359
 
13803
14360
  const container = document.createElement('div');
@@ -13874,7 +14431,7 @@
13874
14431
 
13875
14432
  header.appendChild(iconWrap);
13876
14433
  header.appendChild(titleWrap);
13877
- if (agentStyledMemo && !businessAutomation) {
14434
+ if (agentStyledMemo && !businessAutomation && !remoteFleet) {
13878
14435
  const roleSelect = createMindCanvasAgentRoleSelect(nodeModel);
13879
14436
  header.appendChild(roleSelect);
13880
14437
  }
@@ -16941,7 +17498,7 @@
16941
17498
  const iconKey = getMemoIconKey(nodeModel);
16942
17499
  const agentStyledMemo = isAgentStyledMemoNode(nodeModel);
16943
17500
  const businessAutomation = isBusinessAutomationNode(nodeModel);
16944
- const remoteFleet = isRemoteFleetMonitorNode(nodeModel);
17501
+ const remoteFleet = isRemoteFleetNode(nodeModel);
16945
17502
  const externalChrome = allowsMemoExternalChrome(nodeModel);
16946
17503
 
16947
17504
  memoEl.classList.toggle('map-node-agent', agentStyledMemo);
@@ -16965,7 +17522,7 @@
16965
17522
  titleInput.value = nextTitle;
16966
17523
  }
16967
17524
  disableEditorAssist(titleInput);
16968
- if (businessAutomation) {
17525
+ if (businessAutomation || remoteFleet) {
16969
17526
  memoEl.querySelector('.map-node-memo__agent-role-select')?.remove();
16970
17527
  } else {
16971
17528
  updateMindCanvasAgentRoleSelect(memoEl, nodeModel);
@@ -57,6 +57,7 @@
57
57
 
58
58
  const BUSINESS_AUTOMATION_SEMANTIC_TYPE = 'BusinessAutomationNode';
59
59
  const REMOTE_FLEET_SEMANTIC_TYPE = 'RemoteFleetMonitor';
60
+ const REMOTE_FLEET_DEVICE_SEMANTIC_TYPE = 'RemoteFleetDevice';
60
61
  const AUTOMATION_INPUT_PINS_METADATA_KEY = 'AutomationInputPins';
61
62
  const AUTOMATION_OUTPUT_PINS_METADATA_KEY = 'AutomationOutputPins';
62
63
  const AUTOMATION_PIN_RAIL_WIDTH = 118;
@@ -103,7 +104,8 @@
103
104
  return semanticType === 'MindCanvasAgent'
104
105
  || semanticType === 'AgentCommand'
105
106
  || semanticType === BUSINESS_AUTOMATION_SEMANTIC_TYPE
106
- || semanticType === REMOTE_FLEET_SEMANTIC_TYPE;
107
+ || semanticType === REMOTE_FLEET_SEMANTIC_TYPE
108
+ || semanticType === REMOTE_FLEET_DEVICE_SEMANTIC_TYPE;
107
109
  }
108
110
 
109
111
  function isAgentNodeModel(nodeModel) {