@mindexec/cli 0.2.159 → 0.2.161

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.159",
3
+ "version": "0.2.161",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -1328,10 +1328,21 @@ try {
1328
1328
  const drawCountBeforeControlNode = document.canvasDrawCalls.length;
1329
1329
  manager.renderRemoteFleetDeviceForTest(deviceBody, buildDeviceNode(focusedDevice, hub.getStatus()));
1330
1330
  assert.ok(deviceBody.textContent.includes(focusedDevice.Name));
1331
+ assert.ok(deviceBody.querySelector('[data-remote-fleet-device-toolbar="true"]'));
1332
+ assert.ok(deviceBody.querySelector('[data-remote-fleet-device-actions="true"]'));
1333
+ assert.ok(deviceBody.querySelector('[data-remote-fleet-device-task-panel="true"]'));
1334
+ const deviceStatusBar = deviceBody.querySelector('[data-remote-fleet-device-status-bar="true"]')
1335
+ || deviceBody.querySelector('[data-remote-fleet-device-statusbar="true"]');
1336
+ assert.ok(deviceStatusBar);
1331
1337
  const deviceControlStage = deviceBody.querySelector('[data-remote-fleet-control-node-stage="true"]');
1332
1338
  const deviceControlCanvas = deviceBody.querySelector('[data-remote-fleet-control-node-canvas="true"]');
1333
1339
  assert.ok(deviceControlStage);
1334
1340
  assert.ok(deviceControlCanvas);
1341
+ assert.equal(
1342
+ deviceControlStage.parentElement,
1343
+ deviceBody,
1344
+ 'RemoteFleetDevice should spend its main body area on the control screen');
1345
+ assert.equal(deviceBody.querySelectorAll('[data-remote-fleet-status-card], [data-remote-fleet-stat]').length, 0);
1335
1346
  assert.equal(document.activeElement, deviceControlStage);
1336
1347
  await wait(30);
1337
1348
  const deviceControlSubscribe = diagnostics.webSocketSends
@@ -1439,7 +1450,7 @@ try {
1439
1450
  assert.ok(deviceBody.querySelector('[data-remote-fleet-action="refresh-device"]'));
1440
1451
  assert.ok(deviceBody.querySelector('[data-remote-fleet-action="task-device"]'));
1441
1452
  assert.ok(deviceBody.textContent.includes('Task timed out waiting for the agent response.'));
1442
- assert.ok(deviceBody.textContent.includes(focusedDevice.DeviceId));
1453
+ assert.ok(deviceStatusBar.title.includes(focusedDevice.DeviceId));
1443
1454
  const controlCanvasBeforeRerender = deviceControlCanvas;
1444
1455
  manager.renderRemoteFleetDeviceForTest(deviceBody, buildDeviceNode(focusedDevice, hub.getStatus()));
1445
1456
  await wait(30);
@@ -291,6 +291,8 @@ function spawnBridge({ bridgePort, remoteHubPort, workspacePath, authRoot, label
291
291
  MINDEXEC_REMOTE_REGISTRY_FOLLOWER: follower ? '1' : '0',
292
292
  MINDEXEC_REMOTE_REGISTRY_POLL_MS: '10000',
293
293
  MINDEXEC_REMOTE_REGISTRY_FAST_RETRY_MS: '250',
294
+ MINDEXEC_REMOTE_REGISTRY_INACTIVE_STOP_COUNT: '2',
295
+ MINDEXEC_REMOTE_REGISTRY_INACTIVE_GRACE_MS: '500',
294
296
  MINDEXEC_REMOTE_REGISTRY_REALTIME_RECONNECT_MS: '250',
295
297
  MINDEXEC_REMOTE_REGISTRY_REALTIME_DEBOUNCE_MS: '100',
296
298
  MINDEXEC_REMOTE_HOST_TARGET_RENEW_MS: '5000',
@@ -575,6 +577,28 @@ async function main() {
575
577
  assert.equal(secondAgent.leaseId, 'lease-b');
576
578
  await waitForConnectedDevice(hostB, 'host-b');
577
579
 
580
+ await wait(250);
581
+ currentTarget = null;
582
+ fakeSupabase.broadcastChange('DELETE');
583
+ await waitFor(async () => {
584
+ const status = await fetchJson(`${client.baseUrl}/api/status`);
585
+ const agent = await fetchJson(`${client.baseUrl}/api/remote/agent/status`);
586
+ return status.payload?.remoteRegistryFollower?.reason === 'no-active-target-agent-kept'
587
+ && agent.payload?.running === true
588
+ && String(agent.payload?.manager || '') === hostBEndpoint
589
+ ? { status: status.payload.remoteRegistryFollower, agent: agent.payload }
590
+ : null;
591
+ }, 4000, `transient no-active-target keeps managed agent\n${client.details()}`);
592
+
593
+ currentTarget = createRegistryTarget({
594
+ endpoint: hostBEndpoint,
595
+ endpointCandidates: [hostBEndpoint],
596
+ leaseId: 'lease-b'
597
+ });
598
+ fakeSupabase.broadcastChange('INSERT');
599
+ const restoredAgent = await waitForManagedAgent(client, hostBEndpoint, 'host-b after transient missing target', 9000);
600
+ assert.equal(Number(restoredAgent.pid || 0), Number(secondAgent.pid || 0), JSON.stringify(restoredAgent));
601
+
578
602
  const secondPid = Number(secondAgent.pid || 0);
579
603
  killProcess(secondPid);
580
604
  const restartedAgent = await waitForManagedAgentRestart(client, hostBEndpoint, secondPid, 'host-b', 12000);
@@ -589,6 +613,8 @@ async function main() {
589
613
  expires_at: new Date(Date.now() - 1000).toISOString()
590
614
  };
591
615
  fakeSupabase.broadcastChange('UPDATE');
616
+ await wait(700);
617
+ fakeSupabase.broadcastChange('UPDATE');
592
618
 
593
619
  await waitFor(async () => {
594
620
  const result = await fetchJson(`${client.baseUrl}/api/remote/agent/status`);
package/server.js CHANGED
@@ -3360,6 +3360,8 @@ const REMOTE_REGISTRY_FOLLOWER_ENABLED = !/^(0|false|no|off)$/i.test(String(proc
3360
3360
  const REMOTE_REGISTRY_FOLLOWER_POLL_MS = Math.max(1500, Number(process.env.MINDEXEC_REMOTE_REGISTRY_POLL_MS || 5000) || 5000);
3361
3361
  const REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS = Math.max(500, Number(process.env.MINDEXEC_REMOTE_REGISTRY_FAST_RETRY_MS || 1200) || 1200);
3362
3362
  const REMOTE_REGISTRY_FOLLOWER_MISSING_SESSION_STOP_COUNT = 3;
3363
+ const REMOTE_REGISTRY_FOLLOWER_INACTIVE_STOP_COUNT = Math.max(2, Number(process.env.MINDEXEC_REMOTE_REGISTRY_INACTIVE_STOP_COUNT || 8) || 8);
3364
+ const REMOTE_REGISTRY_FOLLOWER_INACTIVE_GRACE_MS = Math.max(500, Number(process.env.MINDEXEC_REMOTE_REGISTRY_INACTIVE_GRACE_MS || 60000) || 60000);
3363
3365
  const REMOTE_REGISTRY_REALTIME_ENABLED = REMOTE_REGISTRY_FOLLOWER_ENABLED
3364
3366
  && !/^(0|false|no|off)$/i.test(String(process.env.MINDEXEC_REMOTE_REGISTRY_REALTIME || 'true'));
3365
3367
  const REMOTE_REGISTRY_REALTIME_HEARTBEAT_MS = Math.max(10000, Number(process.env.MINDEXEC_REMOTE_REGISTRY_REALTIME_HEARTBEAT_MS || 25000) || 25000);
@@ -3391,6 +3393,8 @@ let remoteRegistryFollowerTimer = null;
3391
3393
  let remoteRegistryFollowerInFlight = false;
3392
3394
  let remoteRegistryFollowerStarted = false;
3393
3395
  let remoteRegistryFollowerConsecutiveMissingSession = 0;
3396
+ let remoteRegistryFollowerConsecutiveInactiveTarget = 0;
3397
+ let remoteRegistryFollowerInactiveTargetFirstAt = 0;
3394
3398
  let remoteRegistryRealtimeSocket = null;
3395
3399
  let remoteRegistryRealtimeSessionKey = '';
3396
3400
  let remoteRegistryRealtimeTopic = '';
@@ -3532,6 +3536,9 @@ function createRemoteAgentSyncReport(body = {}) {
3532
3536
  targetLeaseId: safeRemoteAgentField(body.targetLeaseId || body.TargetLeaseId, 128),
3533
3537
  targetNodeId: safeRemoteAgentField(body.targetNodeId || body.TargetNodeId, 128),
3534
3538
  targetExpiresAt: safeRemoteAgentField(body.targetExpiresAt || body.TargetExpiresAt, 80),
3539
+ inactiveCount: Number(body.inactiveCount ?? body.InactiveCount ?? 0) || 0,
3540
+ inactiveElapsedMs: Number(body.inactiveElapsedMs ?? body.InactiveElapsedMs ?? 0) || 0,
3541
+ agentKept: body.agentKept === true || body.AgentKept === true,
3535
3542
  updatedAt: new Date().toISOString()
3536
3543
  };
3537
3544
  }
@@ -3954,6 +3961,39 @@ function maybeRetryRemoteAgentAfterExit(state, reason = 'agent-exited') {
3954
3961
  scheduleRemoteRegistryFollower(0, reason);
3955
3962
  }
3956
3963
 
3964
+ function resetRemoteRegistryInactiveTargetGrace() {
3965
+ remoteRegistryFollowerConsecutiveInactiveTarget = 0;
3966
+ remoteRegistryFollowerInactiveTargetFirstAt = 0;
3967
+ }
3968
+
3969
+ function rememberRemoteRegistryInactiveTarget(reason = 'no-active-target') {
3970
+ const now = Date.now();
3971
+ remoteRegistryFollowerConsecutiveInactiveTarget += 1;
3972
+ if (!remoteRegistryFollowerInactiveTargetFirstAt) {
3973
+ remoteRegistryFollowerInactiveTargetFirstAt = now;
3974
+ }
3975
+
3976
+ const elapsedMs = Math.max(0, now - remoteRegistryFollowerInactiveTargetFirstAt);
3977
+ const agentRunning = isRemoteAgentRegistryOwned();
3978
+ const confirmedInactive = remoteRegistryFollowerConsecutiveInactiveTarget >= REMOTE_REGISTRY_FOLLOWER_INACTIVE_STOP_COUNT
3979
+ && elapsedMs >= REMOTE_REGISTRY_FOLLOWER_INACTIVE_GRACE_MS;
3980
+
3981
+ if (agentRunning && !confirmedInactive) {
3982
+ logEvent(
3983
+ 'remote',
3984
+ `registry target temporarily inactive; keeping managed RemoteAgent ${formatKeyValue('reason', reason)} ${formatKeyValue('count', remoteRegistryFollowerConsecutiveInactiveTarget)} ${formatKeyValue('elapsedMs', elapsedMs)}`,
3985
+ 'remote');
3986
+ }
3987
+
3988
+ return {
3989
+ count: remoteRegistryFollowerConsecutiveInactiveTarget,
3990
+ elapsedMs,
3991
+ agentRunning,
3992
+ confirmedInactive,
3993
+ agentKept: agentRunning && !confirmedInactive
3994
+ };
3995
+ }
3996
+
3957
3997
  function isLocalRemoteHostTargetActive() {
3958
3998
  const status = remoteHub.getStatus({ includeSecrets: false });
3959
3999
  return status?.hostTargetActive === true
@@ -4208,9 +4248,11 @@ function resolveNpxCliLauncher() {
4208
4248
  return '';
4209
4249
  }
4210
4250
 
4211
- async function stopRemoteAgentConnection(reason = 'stopped') {
4251
+ async function stopRemoteAgentConnection(reason = 'stopped', options = {}) {
4212
4252
  const previous = remoteAgentState;
4213
- suppressRemoteAgentAutoRetry(previous.connectionKey);
4253
+ if (options.suppressRetry !== false) {
4254
+ suppressRemoteAgentAutoRetry(previous.connectionKey);
4255
+ }
4214
4256
  if (previous.proc) {
4215
4257
  try {
4216
4258
  await terminateProcessTree(previous.proc);
@@ -4753,7 +4795,13 @@ function serializeRemoteRegistryFollowerState() {
4753
4795
  return {
4754
4796
  ...remoteRegistryFollowerState,
4755
4797
  pollMs: REMOTE_REGISTRY_FOLLOWER_POLL_MS,
4756
- fastRetryMs: REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS
4798
+ fastRetryMs: REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS,
4799
+ inactiveStopCount: REMOTE_REGISTRY_FOLLOWER_INACTIVE_STOP_COUNT,
4800
+ inactiveGraceMs: REMOTE_REGISTRY_FOLLOWER_INACTIVE_GRACE_MS,
4801
+ inactiveCount: remoteRegistryFollowerConsecutiveInactiveTarget,
4802
+ inactiveElapsedMs: remoteRegistryFollowerInactiveTargetFirstAt
4803
+ ? Math.max(0, Date.now() - remoteRegistryFollowerInactiveTargetFirstAt)
4804
+ : 0
4757
4805
  };
4758
4806
  }
4759
4807
 
@@ -5499,7 +5547,11 @@ function buildRemoteHostTargetRegistryPayload(hub) {
5499
5547
  const hostInstanceId = safeRemoteAgentField(hub?.hostTargetHostInstanceId || hub?.hostInstanceId, 128);
5500
5548
  const nodeId = safeRemoteAgentField(hub?.hostTargetNodeId, 128);
5501
5549
  const activatedAt = safeRemoteAgentField(hub?.hostTargetActivatedAt, 80) || new Date().toISOString();
5502
- const expiresAt = safeRemoteAgentField(hub?.hostTargetExpiresAt, 80) || new Date(Date.now() + REMOTE_HOST_TARGET_LEASE_MS).toISOString();
5550
+ const rawExpiresAt = safeRemoteAgentField(hub?.hostTargetExpiresAt, 80);
5551
+ const rawExpiresAtMs = Date.parse(rawExpiresAt || '');
5552
+ const expiresAt = Number.isFinite(rawExpiresAtMs) && rawExpiresAtMs > Date.now() + 5000
5553
+ ? rawExpiresAt
5554
+ : new Date(Date.now() + REMOTE_HOST_TARGET_LEASE_MS).toISOString();
5503
5555
 
5504
5556
  if (!pairToken || !leaseId || !hostInstanceId || !nodeId) {
5505
5557
  return {
@@ -5710,7 +5762,7 @@ async function reconcileRemoteAfterSystemResume(trigger = 'timer-drift', driftMs
5710
5762
  closeRemoteRegistryRealtime('system-resume');
5711
5763
 
5712
5764
  if (wasRegistryAgentRunning) {
5713
- await stopRemoteAgentConnection('system-resume-reconnect');
5765
+ await stopRemoteAgentConnection('system-resume-reconnect', { suppressRetry: false });
5714
5766
  }
5715
5767
 
5716
5768
  if (isLocalHost) {
@@ -6001,6 +6053,7 @@ async function runRemoteRegistryFollowerOnce(trigger = 'timer') {
6001
6053
  const target = await fetchRemoteRegistryTarget(config, session);
6002
6054
 
6003
6055
  if (localHub?.hostTargetActive === true) {
6056
+ resetRemoteRegistryInactiveTargetGrace();
6004
6057
  if (target?.active === true
6005
6058
  && !isRemoteRegistryTargetExpired(target)
6006
6059
  && !isRemoteRegistryTargetSameAsLocalHost(localHub, target)) {
@@ -6064,32 +6117,50 @@ async function runRemoteRegistryFollowerOnce(trigger = 'timer') {
6064
6117
  }
6065
6118
 
6066
6119
  if (!target?.active || isRemoteRegistryTargetExpired(target)) {
6067
- if (isRemoteAgentRegistryOwned()) {
6120
+ const inactiveReason = target
6121
+ ? 'registry-inactive'
6122
+ : 'no-active-target';
6123
+ const inactive = rememberRemoteRegistryInactiveTarget(inactiveReason);
6124
+ if (inactive.confirmedInactive && isRemoteAgentRegistryOwned()) {
6068
6125
  await stopRemoteAgentConnection('registry-inactive');
6069
6126
  }
6127
+ const keptManager = inactive.agentKept ? safeRemoteAgentField(remoteAgentState.manager, 160) : '';
6128
+ const keptCandidates = inactive.agentKept ? normalizeRemoteManagerEndpointList(remoteAgentState.managerCandidates, remoteAgentState.manager) : [];
6070
6129
  updateRemoteRegistryFollowerState({
6071
- status: 'idle',
6072
- reason: 'no-active-target',
6130
+ status: inactive.agentKept ? 'waiting' : 'idle',
6131
+ reason: inactive.agentKept ? `${inactiveReason}-agent-kept` : inactiveReason,
6073
6132
  authenticated: true,
6074
6133
  lastAttemptAt: attemptedAt,
6075
6134
  lastSuccessAt: attemptedAt,
6076
- targetEndpoint: '',
6077
- targetEndpointCandidates: [],
6078
- targetLeaseId: '',
6079
- targetNodeId: '',
6135
+ targetEndpoint: keptManager,
6136
+ targetEndpointCandidates: keptCandidates,
6137
+ targetLeaseId: inactive.agentKept ? safeRemoteAgentField(remoteAgentState.leaseId, 128) : '',
6138
+ targetNodeId: inactive.agentKept ? safeRemoteAgentField(remoteAgentState.nodeId, 128) : '',
6139
+ inactiveCount: inactive.count,
6140
+ inactiveElapsedMs: inactive.elapsedMs,
6141
+ agentKept: inactive.agentKept,
6080
6142
  lastError: ''
6081
6143
  });
6082
6144
  await reportRemoteRegistryFollowerSync({
6083
6145
  ok: true,
6084
6146
  skipped: true,
6085
- reason: 'no-active-target',
6147
+ reason: inactive.agentKept ? `${inactiveReason}-agent-kept` : inactiveReason,
6086
6148
  trigger,
6087
- authenticated: true
6149
+ authenticated: true,
6150
+ targetEndpoint: keptManager,
6151
+ targetEndpointCandidates: keptCandidates,
6152
+ targetLeaseId: inactive.agentKept ? remoteAgentState.leaseId : '',
6153
+ targetNodeId: inactive.agentKept ? remoteAgentState.nodeId : '',
6154
+ inactiveCount: inactive.count,
6155
+ inactiveElapsedMs: inactive.elapsedMs,
6156
+ agentKept: inactive.agentKept
6088
6157
  });
6089
6158
  scheduleRemoteRegistryFollower(REMOTE_REGISTRY_FOLLOWER_POLL_MS, 'no-target');
6090
6159
  return serializeRemoteRegistryFollowerState();
6091
6160
  }
6092
6161
 
6162
+ resetRemoteRegistryInactiveTargetGrace();
6163
+
6093
6164
  if (isRemoteRegistryTargetSameAsLocalHost(localHub, target)) {
6094
6165
  updateRemoteRegistryFollowerState({
6095
6166
  status: 'skipped',
@@ -2585,6 +2585,17 @@ body.mindcanvas-is-dense .css3d-resolution-wrapper.node-type-templatelauncher.se
2585
2585
  background: #ffffff;
2586
2586
  }
2587
2587
 
2588
+ .map-node-template-card.map-node-remote-device .template-card__shell--remote-fleet {
2589
+ gap: 0;
2590
+ padding: 8px;
2591
+ }
2592
+
2593
+ .map-node-template-card.map-node-remote-device .template-card__remote-fleet-body {
2594
+ border: 0;
2595
+ border-radius: 0;
2596
+ background: transparent;
2597
+ }
2598
+
2588
2599
  .template-card__generate:hover {
2589
2600
  background: #115e59;
2590
2601
  }
@@ -4008,7 +4008,9 @@
4008
4008
 
4009
4009
  function isTemplateLauncherNode(nodeModel) {
4010
4010
  const contentType = String(nodeModel?.contentType ?? nodeModel?.ContentType ?? '').trim().toLowerCase();
4011
- return contentType === 'templatelauncher' || isRemoteFleetMonitorNode(nodeModel);
4011
+ return contentType === 'templatelauncher'
4012
+ || isRemoteFleetMonitorNode(nodeModel)
4013
+ || isRemoteFleetDeviceNode(nodeModel);
4012
4014
  }
4013
4015
 
4014
4016
  function isLiveOverlayPortalNode(nodeModel) {
@@ -4031,8 +4033,7 @@
4031
4033
  const semanticType = getNodeSemanticType(nodeModel);
4032
4034
  return semanticType === 'MindCanvasAgent'
4033
4035
  || semanticType === 'AgentCommand'
4034
- || semanticType === BUSINESS_AUTOMATION_SEMANTIC_TYPE
4035
- || semanticType === REMOTE_FLEET_DEVICE_SEMANTIC_TYPE;
4036
+ || semanticType === BUSINESS_AUTOMATION_SEMANTIC_TYPE;
4036
4037
  }
4037
4038
 
4038
4039
  function allowsMemoExternalChrome(nodeModel) {
@@ -11977,8 +11978,7 @@
11977
11978
  const isAgentStyled = element?.classList?.contains('map-node-agent')
11978
11979
  || semanticType === 'MindCanvasAgent'
11979
11980
  || semanticType === 'AgentCommand'
11980
- || semanticType === BUSINESS_AUTOMATION_SEMANTIC_TYPE
11981
- || semanticType === REMOTE_FLEET_DEVICE_SEMANTIC_TYPE;
11981
+ || semanticType === BUSINESS_AUTOMATION_SEMANTIC_TYPE;
11982
11982
  wrapper.classList.toggle('selection-style-agent', !!isAgentStyled);
11983
11983
  const nextColorKey = MEMO_COLOR_THEMES[colorKey] ? colorKey : 'coral';
11984
11984
  const theme = MEMO_COLOR_THEMES[nextColorKey] || MEMO_COLOR_THEMES.gray;
@@ -12264,8 +12264,13 @@
12264
12264
  '[data-template-option-id]',
12265
12265
  '[data-remote-fleet-action]',
12266
12266
  '[data-remote-fleet-control-popup]',
12267
+ '[data-remote-fleet-control-node-stage]',
12268
+ '[data-remote-fleet-control-node-canvas]',
12269
+ '[data-remote-fleet-control-node-placeholder]',
12267
12270
  '[data-remote-fleet-task-input]',
12271
+ '[data-remote-fleet-device-task-input]',
12268
12272
  '[data-remote-fleet-ai-toggle]',
12273
+ '[data-remote-fleet-device-ai-toggle]',
12269
12274
  '[data-remote-fleet-search]',
12270
12275
  '.csv-table-scroll',
12271
12276
  '.csv-table-header-button',
@@ -12426,7 +12431,7 @@
12426
12431
  }
12427
12432
 
12428
12433
  function createTemplateLauncherNodeElement(nodeModel) {
12429
- if (isRemoteFleetMonitorNode(nodeModel)) {
12434
+ if (isRemoteFleetNode(nodeModel)) {
12430
12435
  return createRemoteFleetTemplateNodeElement(nodeModel);
12431
12436
  }
12432
12437
 
@@ -12532,10 +12537,11 @@
12532
12537
  const width = Number(nodeModel.width ?? nodeModel.Width ?? 960);
12533
12538
  const height = Number(nodeModel.height ?? nodeModel.Height ?? 620);
12534
12539
  const title = getRemoteFleetDisplayTitle(nodeModel.prompt ?? nodeModel.Prompt);
12540
+ const isDeviceNode = isRemoteFleetDeviceNode(nodeModel);
12535
12541
 
12536
12542
  const container = document.createElement('div');
12537
12543
  container.id = `node-${nodeModel.id}`;
12538
- container.className = 'map-node css3d-dynamic-node map-node-template-card map-node-remote-fleet';
12544
+ container.className = `map-node css3d-dynamic-node map-node-template-card map-node-remote-fleet${isDeviceNode ? ' map-node-remote-device' : ''}`;
12539
12545
  container.dataset.nodeId = nodeModel.id;
12540
12546
  container.dataset.contentType = 'templateLauncher';
12541
12547
  container.dataset.semanticType = getNodeSemanticType(nodeModel);
@@ -12550,25 +12556,31 @@
12550
12556
  const shell = document.createElement('div');
12551
12557
  shell.className = 'template-card__shell template-card__shell--remote-fleet';
12552
12558
 
12553
- const header = document.createElement('div');
12554
- header.className = 'template-card__header template-card__header--remote-fleet';
12559
+ if (!isDeviceNode) {
12560
+ const header = document.createElement('div');
12561
+ header.className = 'template-card__header template-card__header--remote-fleet';
12555
12562
 
12556
- const icon = document.createElement('div');
12557
- icon.className = 'template-card__icon template-card__icon--remote-fleet';
12558
- icon.innerHTML = '<i class="fa-solid fa-network-wired" aria-hidden="true"></i>';
12559
- header.appendChild(icon);
12563
+ const icon = document.createElement('div');
12564
+ icon.className = 'template-card__icon template-card__icon--remote-fleet';
12565
+ icon.innerHTML = '<i class="fa-solid fa-network-wired" aria-hidden="true"></i>';
12566
+ header.appendChild(icon);
12560
12567
 
12561
- const titleBlock = document.createElement('div');
12562
- titleBlock.className = 'template-card__title-block';
12563
- titleBlock.appendChild(createTemplateCardText('div', 'template-card__eyebrow', 'Remote'));
12564
- titleBlock.appendChild(createTemplateCardText('div', 'template-card__title', title));
12565
- header.appendChild(titleBlock);
12566
- shell.appendChild(header);
12568
+ const titleBlock = document.createElement('div');
12569
+ titleBlock.className = 'template-card__title-block';
12570
+ titleBlock.appendChild(createTemplateCardText('div', 'template-card__eyebrow', 'Remote'));
12571
+ titleBlock.appendChild(createTemplateCardText('div', 'template-card__title', title));
12572
+ header.appendChild(titleBlock);
12573
+ shell.appendChild(header);
12574
+ }
12567
12575
 
12568
12576
  const bodyView = document.createElement('div');
12569
12577
  bodyView.className = 'template-card__remote-fleet-body markdown-body mind-map-text-scrollable-view';
12570
12578
  bodyView.dataset.nodeId = nodeModel.id;
12571
- renderRemoteFleetMonitor(bodyView, nodeModel);
12579
+ if (isDeviceNode) {
12580
+ renderRemoteFleetDevice(bodyView, nodeModel);
12581
+ } else {
12582
+ renderRemoteFleetMonitor(bodyView, nodeModel);
12583
+ }
12572
12584
  shell.appendChild(bodyView);
12573
12585
 
12574
12586
  container.appendChild(shell);
@@ -17095,7 +17107,6 @@
17095
17107
  const refreshedAt = getRemoteFleetMetadataValue(nodeModel, 'RemoteFleetLastRefreshAtUtc', '');
17096
17108
  const lastError = getRemoteFleetMetadataValue(nodeModel, 'RemoteFleetLastError', '');
17097
17109
  const deviceId = getRemoteFleetMetadataValue(nodeModel, 'RemoteFleetPinnedDeviceId', device ? getRemoteFleetDeviceId(device) : '');
17098
- const endpoint = getRemoteFleetMetadataValue(nodeModel, 'RemoteFleetHubEndpoint', '127.0.0.1:5197');
17099
17110
 
17100
17111
  bodyView.dataset.src = `remote-device:${deviceId}:${refreshedAt}`;
17101
17112
  bodyView.classList.add('map-node-remote-fleet__body');
@@ -17104,12 +17115,13 @@
17104
17115
  flex: 1 1 auto;
17105
17116
  min-height: 0;
17106
17117
  pointer-events: auto;
17118
+ position: relative;
17107
17119
  display: flex;
17108
17120
  flex-direction: column;
17109
- gap: 10px;
17110
- padding: 12px 14px 14px;
17121
+ gap: 6px;
17122
+ padding: 6px;
17111
17123
  overflow: hidden;
17112
- background: linear-gradient(180deg, rgba(248, 250, 252, 0.96), rgba(236, 253, 245, 0.78));
17124
+ background: #ffffff;
17113
17125
  `;
17114
17126
 
17115
17127
  const feedback = document.createElement('div');
@@ -17228,42 +17240,82 @@
17228
17240
  const release = String(getRemoteFleetDeviceField(device, 'release', 'Release', ''));
17229
17241
  const latestTaskStatus = String(getRemoteFleetDeviceField(device, 'latestTaskStatus', 'LatestTaskStatus', ''));
17230
17242
  const latestTaskTitle = String(getRemoteFleetDeviceField(device, 'latestTaskTitle', 'LatestTaskTitle', ''));
17231
- const latestTaskApproval = String(getRemoteFleetDeviceField(device, 'latestTaskApprovalLevel', 'LatestTaskApprovalLevel', ''));
17232
17243
  const latestTaskUpdatedAt = String(getRemoteFleetDeviceField(device, 'latestTaskUpdatedAt', 'LatestTaskUpdatedAt', ''));
17233
17244
  const latestTaskError = String(getRemoteFleetDeviceField(device, 'latestTaskError', 'LatestTaskError', ''));
17234
17245
  const latestTaskResultModel = String(getRemoteFleetDeviceField(device, 'latestTaskResultModel', 'LatestTaskResultModel', ''));
17235
17246
  const latestTaskResultResponseId = String(getRemoteFleetDeviceField(device, 'latestTaskResultResponseId', 'LatestTaskResultResponseId', ''));
17236
17247
  const latestTaskResult = String(getRemoteFleetDeviceField(device, 'latestTaskResultSummary', 'LatestTaskResultSummary', ''));
17237
17248
 
17238
- const header = document.createElement('div');
17239
- header.style.cssText = 'display:flex;align-items:flex-start;justify-content:space-between;gap:10px;flex:0 0 auto;';
17240
- const titleBox = document.createElement('div');
17241
- titleBox.style.cssText = 'min-width:0;display:flex;flex-direction:column;gap:3px;';
17249
+ const toolbar = document.createElement('div');
17250
+ toolbar.dataset.remoteFleetDeviceToolbar = 'true';
17251
+ toolbar.style.cssText = `
17252
+ flex: 0 0 32px;
17253
+ min-height: 32px;
17254
+ display: flex;
17255
+ align-items: center;
17256
+ gap: 8px;
17257
+ min-width: 0;
17258
+ padding: 0 2px;
17259
+ `;
17260
+ const deviceTitle = document.createElement('div');
17261
+ deviceTitle.style.cssText = `
17262
+ min-width: 0;
17263
+ flex: 1 1 auto;
17264
+ display: flex;
17265
+ align-items: center;
17266
+ gap: 7px;
17267
+ overflow: hidden;
17268
+ `;
17269
+ const stateDot = document.createElement('span');
17270
+ stateDot.title = connected ? 'Online' : 'Offline';
17271
+ stateDot.style.cssText = `
17272
+ width: 8px;
17273
+ height: 8px;
17274
+ border-radius: 50%;
17275
+ flex: 0 0 auto;
17276
+ background: ${connected ? '#10b981' : '#94a3b8'};
17277
+ box-shadow: 0 0 0 2px rgba(226, 232, 240, 0.9);
17278
+ `;
17242
17279
  const title = document.createElement('strong');
17243
17280
  title.textContent = name;
17244
17281
  title.title = `${name} (${deviceId})`;
17245
- 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;';
17246
- const subtitle = document.createElement('span');
17247
- subtitle.textContent = `${connected ? 'Online' : 'Offline'} - ${release ? `${platform} ${release}` : platform}`;
17248
- subtitle.title = subtitle.textContent;
17249
- 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;`;
17250
- titleBox.appendChild(title);
17251
- titleBox.appendChild(subtitle);
17252
- const statusBadge = document.createElement('span');
17253
- statusBadge.textContent = liveActive ? 'LIVE' : (connected ? 'READY' : 'OFFLINE');
17254
- statusBadge.style.cssText = `
17282
+ title.style.cssText = `
17283
+ min-width: 0;
17284
+ color: #0f172a;
17285
+ font-size: 13px;
17286
+ font-weight: 950;
17287
+ line-height: 1;
17288
+ overflow: hidden;
17289
+ text-overflow: ellipsis;
17290
+ white-space: nowrap;
17291
+ letter-spacing: 0;
17292
+ `;
17293
+ const titleStatus = document.createElement('span');
17294
+ titleStatus.textContent = liveActive ? 'Live' : (connected ? 'Ready' : 'Offline');
17295
+ titleStatus.style.cssText = `
17255
17296
  flex: 0 0 auto;
17256
- padding: 5px 8px;
17257
- border-radius: 999px;
17258
- background: ${liveActive ? 'rgba(220, 38, 38, 0.12)' : connected ? 'rgba(16, 185, 129, 0.12)' : 'rgba(100, 116, 139, 0.12)'};
17259
- color: ${liveActive ? '#b91c1c' : connected ? '#047857' : '#475569'};
17297
+ color: ${connected ? '#047857' : '#64748b'};
17260
17298
  font-size: 10px;
17261
- font-weight: 950;
17299
+ font-weight: 900;
17300
+ line-height: 1;
17262
17301
  letter-spacing: 0;
17263
17302
  `;
17264
- header.appendChild(titleBox);
17265
- header.appendChild(statusBadge);
17266
- bodyView.appendChild(header);
17303
+ deviceTitle.appendChild(stateDot);
17304
+ deviceTitle.appendChild(title);
17305
+ deviceTitle.appendChild(titleStatus);
17306
+ const actions = document.createElement('div');
17307
+ actions.dataset.remoteFleetDeviceActions = 'true';
17308
+ actions.style.cssText = `
17309
+ flex: 0 0 auto;
17310
+ display: flex;
17311
+ align-items: center;
17312
+ justify-content: flex-end;
17313
+ gap: 5px;
17314
+ min-width: 0;
17315
+ `;
17316
+ toolbar.appendChild(deviceTitle);
17317
+ toolbar.appendChild(actions);
17318
+ bodyView.appendChild(toolbar);
17267
17319
 
17268
17320
  const controlStage = document.createElement('div');
17269
17321
  controlStage.dataset.remoteFleetControlNodeStage = 'true';
@@ -17273,12 +17325,12 @@
17273
17325
  controlStage.style.cssText = `
17274
17326
  position: relative;
17275
17327
  flex: 1 1 auto;
17276
- min-height: 260px;
17328
+ min-height: 0;
17277
17329
  width: 100%;
17278
17330
  overflow: hidden;
17279
17331
  border-radius: 0;
17280
17332
  background: #020617;
17281
- border: 1px solid rgba(15, 23, 42, 0.18);
17333
+ border: 1px solid rgba(15, 23, 42, 0.20);
17282
17334
  cursor: crosshair;
17283
17335
  touch-action: none;
17284
17336
  outline: none;
@@ -17382,103 +17434,81 @@
17382
17434
  }
17383
17435
  }
17384
17436
 
17385
- const stats = document.createElement('div');
17386
- stats.style.cssText = 'display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:7px;flex:0 0 auto;';
17387
- stats.appendChild(createRemoteFleetStat('Seen', formatRemoteFleetAge(getRemoteFleetDeviceField(device, 'lastSeenAt', 'LastSeenAt', '')), connected ? 'online' : 'default'));
17388
- stats.appendChild(createRemoteFleetStat('Uptime', formatRemoteFleetDuration(getRemoteFleetDeviceField(device, 'uptimeSec', 'UptimeSec', Number.NaN))));
17389
- stats.appendChild(createRemoteFleetStat('Mem', formatRemoteFleetPercent(getRemoteFleetDeviceField(device, 'usedMemRatio', 'UsedMemRatio', Number.NaN))));
17390
- stats.appendChild(createRemoteFleetStat('Load', formatRemoteFleetNumber(getRemoteFleetDeviceField(device, 'load1', 'Load1', Number.NaN), 2)));
17391
- bodyView.appendChild(stats);
17392
-
17393
- if (latestTaskStatus || latestTaskTitle || latestTaskResult || latestTaskError) {
17394
- const taskBox = document.createElement('div');
17395
- const taskTone = latestTaskError || latestTaskStatus === 'failed'
17396
- ? 'error'
17397
- : (latestTaskStatus === 'completed' ? 'done' : 'pending');
17398
- taskBox.style.cssText = `
17399
- display: flex;
17400
- flex-direction: column;
17401
- gap: 3px;
17402
- min-width: 0;
17403
- padding: 8px 9px;
17404
- border-radius: 8px;
17405
- background: ${taskTone === 'error' ? 'rgba(248, 113, 113, 0.12)' : taskTone === 'done' ? 'rgba(16, 185, 129, 0.10)' : 'rgba(37, 99, 235, 0.08)'};
17406
- 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)'};
17407
- flex: 0 0 auto;
17408
- `;
17409
- const taskLine = document.createElement('div');
17410
- const taskModeLabel = latestTaskApproval === 'ai-assist' ? 'AI' : 'Task';
17411
- taskLine.textContent = `${taskModeLabel} ${latestTaskStatus || 'task'}${latestTaskUpdatedAt ? ` - ${formatRemoteFleetAge(latestTaskUpdatedAt)}` : ''}${latestTaskResultModel ? ` - ${latestTaskResultModel}` : ''}`;
17412
- taskLine.title = latestTaskResultResponseId
17413
- ? `${taskLine.textContent} (${latestTaskResultResponseId})`
17414
- : taskLine.textContent;
17415
- 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;`;
17416
- const taskSummary = document.createElement('div');
17417
- taskSummary.textContent = formatRemoteFleetTaskError(latestTaskError) || latestTaskResult || latestTaskTitle || 'Task queued';
17418
- taskSummary.title = taskSummary.textContent;
17419
- 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;';
17420
- taskBox.appendChild(taskLine);
17421
- taskBox.appendChild(taskSummary);
17422
- bodyView.appendChild(taskBox);
17423
- }
17424
-
17425
- const taskInput = document.createElement('textarea');
17426
- taskInput.dataset.remoteFleetDeviceTaskInput = 'true';
17427
- taskInput.placeholder = 'Task for this computer';
17428
- taskInput.rows = 2;
17429
- taskInput.style.cssText = `
17430
- width: 100%;
17431
- min-width: 0;
17432
- height: 50px;
17433
- resize: none;
17434
- border-radius: 8px;
17435
- border: 1px solid rgba(148, 163, 184, 0.34);
17436
- background: rgba(255, 255, 255, 0.92);
17437
- color: #0f172a;
17438
- padding: 8px 10px;
17439
- font-size: 12px;
17440
- line-height: 1.35;
17441
- font-weight: 700;
17442
- letter-spacing: 0;
17443
- outline: none;
17444
- pointer-events: auto;
17445
- `;
17446
- bodyView.appendChild(taskInput);
17447
-
17448
- const actions = document.createElement('div');
17449
- actions.style.cssText = 'display:flex;align-items:center;gap:7px;flex-wrap:wrap;flex:0 0 auto;';
17450
17437
  const refreshButton = createRemoteFleetButton('Refresh', 'Refresh this remote control device', 'refresh-device');
17451
- refreshButton.style.height = '30px';
17438
+ refreshButton.style.height = '26px';
17452
17439
  actions.appendChild(refreshButton);
17453
17440
  if (connected && thumbnailEnabled) {
17454
17441
  const shotButton = createRemoteFleetButton('Shot', 'Request thumbnail', 'thumbnail-device');
17455
17442
  shotButton.dataset.deviceId = deviceId;
17456
- shotButton.style.height = '30px';
17443
+ shotButton.style.height = '26px';
17457
17444
  actions.appendChild(shotButton);
17458
17445
  }
17459
17446
  if (connected && liveStreamEnabled) {
17460
17447
  const liveButton = createRemoteFleetButton(liveActive ? 'Stop' : 'Live', liveActive ? 'Stop live stream' : 'Start live stream', liveActive ? 'live-stop' : 'live-start');
17461
17448
  liveButton.dataset.deviceId = deviceId;
17462
17449
  liveButton.dataset.streamId = liveStreamId;
17463
- liveButton.style.height = '30px';
17450
+ liveButton.style.height = '26px';
17464
17451
  if (liveActive) {
17465
17452
  liveButton.style.borderColor = 'rgba(220, 38, 38, 0.32)';
17466
17453
  liveButton.style.color = '#b91c1c';
17467
17454
  }
17468
17455
  actions.appendChild(liveButton);
17469
17456
  }
17457
+ let taskToggleButton = null;
17470
17458
  if (connected && taskEnabled) {
17471
- const taskButton = createRemoteFleetButton(aiAssistEnabled ? 'Task/AI' : 'Task', 'Dispatch task to this device', 'task-device');
17472
- taskButton.dataset.deviceId = deviceId;
17473
- taskButton.style.height = '30px';
17474
- actions.appendChild(taskButton);
17459
+ taskToggleButton = createRemoteFleetButton(aiAssistEnabled ? 'Task/AI' : 'Task', 'Open task input for this device', 'task-open');
17460
+ taskToggleButton.dataset.deviceId = deviceId;
17461
+ taskToggleButton.style.height = '26px';
17462
+ actions.appendChild(taskToggleButton);
17475
17463
  }
17476
17464
  if (connected) {
17477
17465
  const pingButton = createRemoteFleetButton('Ping', 'Ping device', 'ping-device');
17478
17466
  pingButton.dataset.deviceId = deviceId;
17479
- pingButton.style.height = '30px';
17467
+ pingButton.style.height = '26px';
17480
17468
  actions.appendChild(pingButton);
17481
17469
  }
17470
+
17471
+ const taskPanel = document.createElement('div');
17472
+ taskPanel.dataset.remoteFleetDeviceTaskPanel = 'true';
17473
+ taskPanel.style.cssText = `
17474
+ position: absolute;
17475
+ right: 8px;
17476
+ top: 42px;
17477
+ z-index: 6;
17478
+ width: min(360px, calc(100% - 16px));
17479
+ display: none;
17480
+ grid-template-columns: minmax(0, 1fr) auto auto;
17481
+ gap: 6px;
17482
+ align-items: stretch;
17483
+ padding: 7px;
17484
+ border-radius: 8px;
17485
+ border: 1px solid rgba(148, 163, 184, 0.30);
17486
+ background: rgba(255, 255, 255, 0.97);
17487
+ box-shadow: 0 16px 32px rgba(15, 23, 42, 0.18);
17488
+ pointer-events: auto;
17489
+ `;
17490
+ const taskInput = document.createElement('textarea');
17491
+ taskInput.dataset.remoteFleetDeviceTaskInput = 'true';
17492
+ taskInput.placeholder = 'Task for this computer';
17493
+ taskInput.rows = 2;
17494
+ taskInput.style.cssText = `
17495
+ width: 100%;
17496
+ min-width: 0;
17497
+ height: 42px;
17498
+ resize: none;
17499
+ border-radius: 7px;
17500
+ border: 1px solid rgba(148, 163, 184, 0.34);
17501
+ background: rgba(255, 255, 255, 0.94);
17502
+ color: #0f172a;
17503
+ padding: 6px 8px;
17504
+ font-size: 11px;
17505
+ line-height: 1.3;
17506
+ font-weight: 700;
17507
+ letter-spacing: 0;
17508
+ outline: none;
17509
+ pointer-events: auto;
17510
+ `;
17511
+ taskPanel.appendChild(taskInput);
17482
17512
  const aiToggleLabel = document.createElement('label');
17483
17513
  aiToggleLabel.title = 'Use AI assist when this agent exposes it';
17484
17514
  aiToggleLabel.style.cssText = `
@@ -17486,7 +17516,7 @@
17486
17516
  align-items: center;
17487
17517
  justify-content: center;
17488
17518
  gap: 6px;
17489
- height: 30px;
17519
+ height: 42px;
17490
17520
  padding: 0 9px;
17491
17521
  border-radius: 7px;
17492
17522
  border: 1px solid rgba(14, 165, 233, 0.28);
@@ -17507,37 +17537,73 @@
17507
17537
  aiToggleText.textContent = 'AI';
17508
17538
  aiToggleLabel.appendChild(aiToggle);
17509
17539
  aiToggleLabel.appendChild(aiToggleText);
17510
- actions.appendChild(aiToggleLabel);
17511
- bodyView.appendChild(actions);
17540
+ taskPanel.appendChild(aiToggleLabel);
17541
+ const taskSendButton = createRemoteFleetButton('Send', 'Dispatch task to this device', 'task-device');
17542
+ taskSendButton.dataset.deviceId = deviceId;
17543
+ taskSendButton.style.height = '42px';
17544
+ taskPanel.appendChild(taskSendButton);
17545
+ bodyView.appendChild(taskPanel);
17512
17546
  bodyView.appendChild(feedback);
17513
17547
 
17514
- const footer = document.createElement('div');
17515
- footer.textContent = `Endpoint ${endpoint} - ${deviceId} - refreshed ${formatRemoteFleetAge(refreshedAt)}`;
17516
- footer.title = footer.textContent;
17517
- footer.style.cssText = `
17518
- margin-top: auto;
17548
+ const statusBar = document.createElement('div');
17549
+ statusBar.dataset.remoteFleetDeviceStatusBar = 'true';
17550
+ const latestTaskText = formatRemoteFleetTaskError(latestTaskError) || latestTaskResult || latestTaskTitle || '';
17551
+ const statusItems = [
17552
+ `${connected ? 'Online' : 'Offline'}`,
17553
+ `Seen ${formatRemoteFleetAge(getRemoteFleetDeviceField(device, 'lastSeenAt', 'LastSeenAt', ''))}`,
17554
+ `Uptime ${formatRemoteFleetDuration(getRemoteFleetDeviceField(device, 'uptimeSec', 'UptimeSec', Number.NaN))}`,
17555
+ `Mem ${formatRemoteFleetPercent(getRemoteFleetDeviceField(device, 'usedMemRatio', 'UsedMemRatio', Number.NaN))}`,
17556
+ `Load ${formatRemoteFleetNumber(getRemoteFleetDeviceField(device, 'load1', 'Load1', Number.NaN), 2)}`,
17557
+ release ? `${platform} ${release}` : platform,
17558
+ latestTaskStatus ? `Task ${latestTaskStatus}${latestTaskUpdatedAt ? ` ${formatRemoteFleetAge(latestTaskUpdatedAt)}` : ''}` : '',
17559
+ latestTaskText ? latestTaskText : ''
17560
+ ].filter(text => String(text || '').trim());
17561
+ statusBar.textContent = statusItems.join(' | ');
17562
+ statusBar.title = `${name} (${deviceId}) - ${statusBar.textContent}${latestTaskResultModel ? ` - ${latestTaskResultModel}` : ''}${latestTaskResultResponseId ? ` - ${latestTaskResultResponseId}` : ''}`;
17563
+ statusBar.style.cssText = `
17519
17564
  flex: 0 0 auto;
17520
- color: #64748b;
17521
- font-size: 10px;
17565
+ min-height: 22px;
17566
+ display: flex;
17567
+ align-items: center;
17568
+ min-width: 0;
17569
+ padding: 0 6px;
17570
+ border-radius: 0;
17571
+ border: 1px solid rgba(148, 163, 184, 0.18);
17572
+ background: rgba(248, 250, 252, 0.92);
17573
+ color: #334155;
17574
+ font-size: 10.5px;
17522
17575
  font-weight: 750;
17523
- line-height: 1.2;
17576
+ line-height: 1;
17524
17577
  overflow: hidden;
17525
17578
  text-overflow: ellipsis;
17526
17579
  white-space: nowrap;
17580
+ letter-spacing: 0;
17527
17581
  `;
17528
- bodyView.appendChild(footer);
17582
+ bodyView.appendChild(statusBar);
17529
17583
 
17530
17584
  if (lastError.trim()) {
17531
17585
  setFeedback(lastError, 'error');
17532
17586
  }
17533
17587
 
17534
- [taskInput, aiToggleLabel, aiToggle, ...Array.from(actions.querySelectorAll('button'))].forEach(control => {
17588
+ [taskPanel, taskInput, aiToggleLabel, aiToggle, ...Array.from(actions.querySelectorAll('button')), taskSendButton].forEach(control => {
17535
17589
  if (!control) return;
17536
17590
  ['mousedown', 'mouseup', 'click', 'dblclick', 'keydown'].forEach(eventName => {
17537
17591
  control.addEventListener(eventName, event => event.stopPropagation());
17538
17592
  });
17539
17593
  });
17540
17594
 
17595
+ if (taskToggleButton) {
17596
+ taskToggleButton.addEventListener('click', event => {
17597
+ event.preventDefault();
17598
+ event.stopPropagation();
17599
+ const nextVisible = taskPanel.style.display === 'none';
17600
+ taskPanel.style.display = nextVisible ? 'grid' : 'none';
17601
+ if (nextVisible) {
17602
+ taskInput.focus({ preventScroll: true });
17603
+ }
17604
+ });
17605
+ }
17606
+
17541
17607
  refreshButton.addEventListener('click', async event => {
17542
17608
  event.preventDefault();
17543
17609
  event.stopPropagation();
@@ -17613,7 +17679,7 @@
17613
17679
  });
17614
17680
  });
17615
17681
 
17616
- actions.querySelectorAll('[data-remote-fleet-action="task-device"]').forEach(button => {
17682
+ bodyView.querySelectorAll('[data-remote-fleet-action="task-device"]').forEach(button => {
17617
17683
  button.addEventListener('click', async event => {
17618
17684
  event.preventDefault();
17619
17685
  event.stopPropagation();
@@ -19422,7 +19488,7 @@
19422
19488
  }
19423
19489
 
19424
19490
  function createMemoNodeElement(nodeModel) {
19425
- if (isRemoteFleetMonitorNode(nodeModel)) {
19491
+ if (isRemoteFleetNode(nodeModel)) {
19426
19492
  return createRemoteFleetTemplateNodeElement(nodeModel);
19427
19493
  }
19428
19494
 
@@ -22612,7 +22678,7 @@
22612
22678
  dividerEl.style.display = prompt ? (isLoading || hasContent ? 'block' : 'none') : 'none';
22613
22679
  }
22614
22680
 
22615
- if (isRemoteFleetMonitorNode(nodeModel)) {
22681
+ if (isRemoteFleetNode(nodeModel)) {
22616
22682
  el.classList.remove('node-type-memo', 'has-context-source-pins');
22617
22683
  el.classList.add('node-type-templatelauncher');
22618
22684
  el.dataset.contentType = 'templateLauncher';
@@ -22639,6 +22705,7 @@
22639
22705
 
22640
22706
  card.dataset.nodeId = nodeId;
22641
22707
  card.dataset.semanticType = getNodeSemanticType(nodeModel);
22708
+ card.classList.toggle('map-node-remote-device', isRemoteFleetDeviceNode(nodeModel));
22642
22709
  card.style.width = `${worldWidth}px`;
22643
22710
  card.style.height = `${worldHeight}px`;
22644
22711
  card.style.minWidth = `${worldWidth}px`;
@@ -22657,7 +22724,11 @@
22657
22724
  }
22658
22725
 
22659
22726
  const bodyView = card.querySelector('.template-card__remote-fleet-body');
22660
- renderRemoteFleetMonitor(bodyView, nodeModel);
22727
+ if (isRemoteFleetDeviceNode(nodeModel)) {
22728
+ renderRemoteFleetDevice(bodyView, nodeModel);
22729
+ } else {
22730
+ renderRemoteFleetMonitor(bodyView, nodeModel);
22731
+ }
22661
22732
  bindTemplateCardCanvasNavigation(card, nodeModel);
22662
22733
  return;
22663
22734
  }
@@ -382,7 +382,8 @@
382
382
  const contentType = getNodeContentType(model);
383
383
  return isTemplateLauncherModel(model) ||
384
384
  contentType === 'csv-table' ||
385
- isBusinessAutomationModel(model);
385
+ isBusinessAutomationModel(model) ||
386
+ isRemoteFleetNodeModel(model);
386
387
  }
387
388
 
388
389
  function isRemoteFleetNodeModel(model) {
@@ -43,7 +43,10 @@
43
43
  }
44
44
 
45
45
  function isTemplateLauncherNodeModel(nodeModel) {
46
- return getNodeContentTypeLower(nodeModel) === 'templatelauncher';
46
+ const semanticType = getNodeSemanticType(nodeModel);
47
+ return getNodeContentTypeLower(nodeModel) === 'templatelauncher'
48
+ || semanticType === REMOTE_FLEET_SEMANTIC_TYPE
49
+ || semanticType === REMOTE_FLEET_DEVICE_SEMANTIC_TYPE;
47
50
  }
48
51
 
49
52
  function getNodeMetadata(nodeModel) {
@@ -103,8 +106,7 @@
103
106
  const semanticType = getNodeSemanticType(nodeModel);
104
107
  return semanticType === 'MindCanvasAgent'
105
108
  || semanticType === 'AgentCommand'
106
- || semanticType === BUSINESS_AUTOMATION_SEMANTIC_TYPE
107
- || semanticType === REMOTE_FLEET_DEVICE_SEMANTIC_TYPE;
109
+ || semanticType === BUSINESS_AUTOMATION_SEMANTIC_TYPE;
108
110
  }
109
111
 
110
112
  function isAgentNodeModel(nodeModel) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "mainAssemblyName": "MindExecution.Web",
3
3
  "resources": {
4
- "hash": "sha256-oN9NB5XxbZ2xSDkf/iKkLdth/StnRySGcRVgEHblI6U=",
4
+ "hash": "sha256-fuZfmnPbFwdls7P4hSQJc1lN5QGs/DTxCqcNmFrIMjs=",
5
5
  "fingerprinting": {
6
6
  "Google.Protobuf.9h59ukbel7.dll": "Google.Protobuf.dll",
7
7
  "Markdig.d1j7v41cl1.dll": "Markdig.dll",
@@ -127,12 +127,12 @@
127
127
  "MindExecution.Kernel.3zom09ir6j.dll": "MindExecution.Kernel.dll",
128
128
  "MindExecution.Plugins.Admin.bk23zzy6wc.dll": "MindExecution.Plugins.Admin.dll",
129
129
  "MindExecution.Plugins.Business.zwcvc8wufe.dll": "MindExecution.Plugins.Business.dll",
130
- "MindExecution.Plugins.Concept.4ncya2e794.dll": "MindExecution.Plugins.Concept.dll",
130
+ "MindExecution.Plugins.Concept.9igh5dsuw9.dll": "MindExecution.Plugins.Concept.dll",
131
131
  "MindExecution.Plugins.Directory.jc0g7fvyzv.dll": "MindExecution.Plugins.Directory.dll",
132
- "MindExecution.Plugins.PlanMaster.0yfii1nh3p.dll": "MindExecution.Plugins.PlanMaster.dll",
133
- "MindExecution.Plugins.YouTube.y4vxx807fo.dll": "MindExecution.Plugins.YouTube.dll",
134
- "MindExecution.Shared.50ptsgk9eg.dll": "MindExecution.Shared.dll",
135
- "MindExecution.Web.hucq2w2ism.dll": "MindExecution.Web.dll",
132
+ "MindExecution.Plugins.PlanMaster.g9r2lbhihg.dll": "MindExecution.Plugins.PlanMaster.dll",
133
+ "MindExecution.Plugins.YouTube.6ni889qas8.dll": "MindExecution.Plugins.YouTube.dll",
134
+ "MindExecution.Shared.xtnh42yedc.dll": "MindExecution.Shared.dll",
135
+ "MindExecution.Web.cg3mse80dr.dll": "MindExecution.Web.dll",
136
136
  "dotnet.js": "dotnet.js",
137
137
  "dotnet.native.qc8g39g30v.js": "dotnet.native.js",
138
138
  "dotnet.native.boem75ye5i.wasm": "dotnet.native.wasm",
@@ -280,16 +280,16 @@
280
280
  "netstandard.yvr3prsx0x.dll": "sha256-EksNn8Luo4bOWqJ6X7dIe9qG9oOqwOVzjH2xYyMNi+E=",
281
281
  "MindExecution.Core.asayb4hw1o.dll": "sha256-5gla4JTR/zbvkGpneSt6S7ItE51+MjWgjW6L+B6Yevk=",
282
282
  "MindExecution.Kernel.3zom09ir6j.dll": "sha256-78vR2SNkF9FwdQv0fEItSyz4HzgyzwGUROJAnpripPA=",
283
- "MindExecution.Plugins.Concept.4ncya2e794.dll": "sha256-gKad12tQh1p7kInUiSIECobEZiD4VIuxmmskGE3ambo=",
284
- "MindExecution.Plugins.PlanMaster.0yfii1nh3p.dll": "sha256-OMcIYPKBnnJCW6QRN8nGykgCloUl1FRnFx6lma85ZGs=",
285
- "MindExecution.Shared.50ptsgk9eg.dll": "sha256-1X+o7c7jCm4MVorCrq52beiyMe+C6qFw6EyIGfZGQdo=",
286
- "MindExecution.Web.hucq2w2ism.dll": "sha256-iSV90XAYUoYq7Rfm7gf4tzJBsIy2zbfVfxadAFS8pSY="
283
+ "MindExecution.Plugins.Concept.9igh5dsuw9.dll": "sha256-L9kDRKc/l7qDyrpHv3nP2eZJFl3hiTKzobGq+1Ir1u4=",
284
+ "MindExecution.Plugins.PlanMaster.g9r2lbhihg.dll": "sha256-hMdypae7YThv34t4g0TL/iI1GF/vQsZVQgEehe1AXt8=",
285
+ "MindExecution.Shared.xtnh42yedc.dll": "sha256-GwFDvUKSi/L8U9L2QsvOVjOOZYAaQInM5CLVYhD0BFk=",
286
+ "MindExecution.Web.cg3mse80dr.dll": "sha256-BDF8hVymVem3X/dB9iKv2S6Vohz7y9CKSYMQOattgaA="
287
287
  },
288
288
  "lazyAssembly": {
289
289
  "MindExecution.Plugins.Admin.bk23zzy6wc.dll": "sha256-uz7Nz0OZ8BBESp2Nr9YocyzV0dUrn7szPuD4/m/RWL8=",
290
290
  "MindExecution.Plugins.Business.zwcvc8wufe.dll": "sha256-3YDsB2/uNI+feWqJk+cesLHgNt8s2/8hXxGPO6CRZZE=",
291
291
  "MindExecution.Plugins.Directory.jc0g7fvyzv.dll": "sha256-PSe7135JPpTBmu4j+rGLF0Dc/6w/wDJNPi8zlfTmSMk=",
292
- "MindExecution.Plugins.YouTube.y4vxx807fo.dll": "sha256-HkMC8BltV070e0Uaskpmyu6+TIs04y0G2t4E62RJdcc="
292
+ "MindExecution.Plugins.YouTube.6ni889qas8.dll": "sha256-ngO7cBhZwzW2Sx9TqcTZj/3TW3lOkcluXr76P2oH9X8="
293
293
  }
294
294
  },
295
295
  "cacheBootResources": true,
@@ -7,8 +7,8 @@
7
7
  <title>MindExec | Run your ideas as AI task graphs</title>
8
8
  <meta name="description" content="MindExec is an AI execution canvas for solo builders, researchers, developers, and creators. Start with free browser tools, then move serious work into saved MindCanvas projects." />
9
9
  <base href="/" />
10
- <link rel="stylesheet" href="_content/MindExecution.Shared/css/app.css?v=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" />
10
+ <link rel="stylesheet" href="_content/MindExecution.Shared/css/app.css?v=20260617-remote-control-screen-v593" />
11
+ <link rel="stylesheet" href="_content/MindExecution.Shared/css/mind-map-overrides.css?v=20260617-remote-control-screen-v593" />
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-remote-frame-no-flash-v592';
582
+ const scriptVersion = '20260617-remote-control-screen-v593';
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": "ikaqQxI3",
2
+ "version": "xi510uSd",
3
3
  "assets": [
4
4
  {
5
5
  "hash": "sha256-+CSYMcqLNTsq3VnH11jgYyOCCdxvHzL74CBmo4sCmMU=",
@@ -42,7 +42,7 @@
42
42
  "url": "_content/MindExecution.Shared/css/app.css"
43
43
  },
44
44
  {
45
- "hash": "sha256-FxtluumoQV2QXTCFtSR+/dc0Xp/CFDcF1cXkaeikt4s=",
45
+ "hash": "sha256-Qe8ZasFVu0/cCvryv6hkXRbNvdEQFO8yqiIOKN5w4G4=",
46
46
  "url": "_content/MindExecution.Shared/css/mind-map-overrides.css"
47
47
  },
48
48
  {
@@ -86,7 +86,7 @@
86
86
  "url": "_content/MindExecution.Shared/js/mind-map-core.js.backup"
87
87
  },
88
88
  {
89
- "hash": "sha256-yUuxzH9Fpd62pPLMU5n0HuYjsJhKvC82LO5X5Uu/1gg=",
89
+ "hash": "sha256-5NeWmkeG5tE20WMmpOO59SkGuCFv+yk/h6kOeYeOmJA=",
90
90
  "url": "_content/MindExecution.Shared/js/mind-map-css3d-manager.js"
91
91
  },
92
92
  {
@@ -114,7 +114,7 @@
114
114
  "url": "_content/MindExecution.Shared/js/mind-map-lod-plan-worker.js"
115
115
  },
116
116
  {
117
- "hash": "sha256-Lo7v8nqlOhYydM8OzuMLHllA29+Q76y/PIV+QaB1uyo=",
117
+ "hash": "sha256-pI3Kf/v2vRRR1SMDKCjMZcLtR8ACg7XAROlgwHBGg50=",
118
118
  "url": "_content/MindExecution.Shared/js/mind-map-lod-renderer.js"
119
119
  },
120
120
  {
@@ -134,7 +134,7 @@
134
134
  "url": "_content/MindExecution.Shared/js/mind-map-node-search-worker.js"
135
135
  },
136
136
  {
137
- "hash": "sha256-Du/v38HmdprkhvETqiO0mx3Ez3rYmEvjQnDMIQclta0=",
137
+ "hash": "sha256-e1IsTR6acAA6OdptYbIJWTS3km0IJGh/VYBU75rDMXU=",
138
138
  "url": "_content/MindExecution.Shared/js/mind-map-nodes.js"
139
139
  },
140
140
  {
@@ -426,28 +426,28 @@
426
426
  "url": "_framework/MindExecution.Plugins.Business.zwcvc8wufe.dll"
427
427
  },
428
428
  {
429
- "hash": "sha256-gKad12tQh1p7kInUiSIECobEZiD4VIuxmmskGE3ambo=",
430
- "url": "_framework/MindExecution.Plugins.Concept.4ncya2e794.dll"
429
+ "hash": "sha256-L9kDRKc/l7qDyrpHv3nP2eZJFl3hiTKzobGq+1Ir1u4=",
430
+ "url": "_framework/MindExecution.Plugins.Concept.9igh5dsuw9.dll"
431
431
  },
432
432
  {
433
433
  "hash": "sha256-PSe7135JPpTBmu4j+rGLF0Dc/6w/wDJNPi8zlfTmSMk=",
434
434
  "url": "_framework/MindExecution.Plugins.Directory.jc0g7fvyzv.dll"
435
435
  },
436
436
  {
437
- "hash": "sha256-OMcIYPKBnnJCW6QRN8nGykgCloUl1FRnFx6lma85ZGs=",
438
- "url": "_framework/MindExecution.Plugins.PlanMaster.0yfii1nh3p.dll"
437
+ "hash": "sha256-hMdypae7YThv34t4g0TL/iI1GF/vQsZVQgEehe1AXt8=",
438
+ "url": "_framework/MindExecution.Plugins.PlanMaster.g9r2lbhihg.dll"
439
439
  },
440
440
  {
441
- "hash": "sha256-HkMC8BltV070e0Uaskpmyu6+TIs04y0G2t4E62RJdcc=",
442
- "url": "_framework/MindExecution.Plugins.YouTube.y4vxx807fo.dll"
441
+ "hash": "sha256-ngO7cBhZwzW2Sx9TqcTZj/3TW3lOkcluXr76P2oH9X8=",
442
+ "url": "_framework/MindExecution.Plugins.YouTube.6ni889qas8.dll"
443
443
  },
444
444
  {
445
- "hash": "sha256-1X+o7c7jCm4MVorCrq52beiyMe+C6qFw6EyIGfZGQdo=",
446
- "url": "_framework/MindExecution.Shared.50ptsgk9eg.dll"
445
+ "hash": "sha256-GwFDvUKSi/L8U9L2QsvOVjOOZYAaQInM5CLVYhD0BFk=",
446
+ "url": "_framework/MindExecution.Shared.xtnh42yedc.dll"
447
447
  },
448
448
  {
449
- "hash": "sha256-iSV90XAYUoYq7Rfm7gf4tzJBsIy2zbfVfxadAFS8pSY=",
450
- "url": "_framework/MindExecution.Web.hucq2w2ism.dll"
449
+ "hash": "sha256-BDF8hVymVem3X/dB9iKv2S6Vohz7y9CKSYMQOattgaA=",
450
+ "url": "_framework/MindExecution.Web.cg3mse80dr.dll"
451
451
  },
452
452
  {
453
453
  "hash": "sha256-IsZJ91/OW+fHzNqIgEc7Y072ns8z9dGritiSyvR9Wgc=",
@@ -770,7 +770,7 @@
770
770
  "url": "_framework/Websocket.Client.vapounvmnl.dll"
771
771
  },
772
772
  {
773
- "hash": "sha256-l0xPjAbrLfbdmB69gqtRn+JMPssryiM0fh1uDxZ/NNo=",
773
+ "hash": "sha256-1T/+tduwHalERbmteG+ZOkOPvGnie3dWjxoK9p0myto=",
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-8VAYYW89mBjF45WS2vA1+Hssmo6S30PB8AW03niSE2Q=",
837
+ "hash": "sha256-tgf6hPsk0rfP/I8Wx88fu4cEJGVIRNrgAztKJfw35Ms=",
838
838
  "url": "index.html"
839
839
  },
840
840
  {
@@ -1,4 +1,4 @@
1
- /* Manifest version: ikaqQxI3 */
1
+ /* Manifest version: xi510uSd */
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