@mindexec/cli 0.2.456 → 0.2.458

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.
Files changed (21) hide show
  1. package/package.json +77 -77
  2. package/remote-fast/osx-arm64/mindexec-remote-fast.dll +0 -0
  3. package/remote-fast/osx-x64/mindexec-remote-fast.dll +0 -0
  4. package/remote-fast/win-x64/mindexec-remote-fast.dll +0 -0
  5. package/scripts/remote-fleet-render-smoke.mjs +50 -229
  6. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +301 -604
  7. package/wwwroot/_content/MindExecution.Shared/js/mind-map-pipeline.js +1326 -1323
  8. package/wwwroot/_framework/{MindExecution.Core.glgqkah960.dll → MindExecution.Core.2ch68iyy8o.dll} +0 -0
  9. package/wwwroot/_framework/{MindExecution.Kernel.1nb0zfmie1.dll → MindExecution.Kernel.dh617xfv36.dll} +0 -0
  10. package/wwwroot/_framework/{MindExecution.Plugins.Admin.4pdpv5q991.dll → MindExecution.Plugins.Admin.0sm50hbae9.dll} +0 -0
  11. package/wwwroot/_framework/{MindExecution.Plugins.Business.jpl2k4nnhh.dll → MindExecution.Plugins.Business.3wq01orrbu.dll} +0 -0
  12. package/wwwroot/_framework/{MindExecution.Plugins.Concept.rissr3n74r.dll → MindExecution.Plugins.Concept.z8yl28fa2a.dll} +0 -0
  13. package/wwwroot/_framework/{MindExecution.Plugins.Directory.tkopeqv16k.dll → MindExecution.Plugins.Directory.m7murp5oes.dll} +0 -0
  14. package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.jmjftycwo4.dll → MindExecution.Plugins.PlanMaster.yuyrbpf0vh.dll} +0 -0
  15. package/wwwroot/_framework/{MindExecution.Plugins.YouTube.drj98vf3ti.dll → MindExecution.Plugins.YouTube.rwxvn00rm2.dll} +0 -0
  16. package/wwwroot/_framework/{MindExecution.Shared.cbieupjmfp.dll → MindExecution.Shared.r9k48iyijb.dll} +0 -0
  17. package/wwwroot/_framework/{MindExecution.Web.xl8ygqgy0i.dll → MindExecution.Web.742aribkxm.dll} +0 -0
  18. package/wwwroot/_framework/blazor.boot.json +21 -21
  19. package/wwwroot/index.html +722 -722
  20. package/wwwroot/service-worker-assets.js +25 -25
  21. package/wwwroot/service-worker.js +1 -1
@@ -1,9 +1,9 @@
1
- // File: mind-map-css3d-manager.js
2
- // [Refactor] Owns CSS3DObject creation, DOM cloning, and event binding.
3
- (function () {
4
- 'use strict';
5
-
6
- // ▼▼▼ [Perf] Debug logging (disable in production) ▼▼▼
1
+ // File: mind-map-css3d-manager.js
2
+ // [Refactor] Owns CSS3DObject creation, DOM cloning, and event binding.
3
+ (function () {
4
+ 'use strict';
5
+
6
+ // ▼▼▼ [Perf] Debug logging (disable in production) ▼▼▼
7
7
  const DEBUG = false;
8
8
  const log = DEBUG ? console.log.bind(console) : () => { };
9
9
  const warn = DEBUG ? console.warn.bind(console) : () => { };
@@ -46,8 +46,9 @@
46
46
  const CSS3D_ONLY_NODE_RENDERING = true;
47
47
  const CSV_TABLE_MAX_RENDER_ROWS = 500;
48
48
  const CSV_TABLE_MAX_RENDER_COLUMNS = 80;
49
- const REMOTE_FLEET_DISPLAY_NAME = 'Multi Desktop Monitor';
50
- const REMOTE_FLEET_LEGACY_DISPLAY_NAME = 'Remote Fleet Monitor';
49
+ const REMOTE_FLEET_DISPLAY_NAME = 'Remote Work Console';
50
+ const REMOTE_FLEET_LEGACY_DISPLAY_NAME = 'Multi Desktop Monitor';
51
+ const REMOTE_FLEET_LEGACY_DISPLAY_NAME_V0 = 'Remote Fleet Monitor';
51
52
  const _cssVideoProxyStateByVideo = new WeakMap();
52
53
 
53
54
  function syncMindCanvasPlatformRenderClasses() {
@@ -355,12 +356,12 @@
355
356
  renderCsvTableContent
356
357
  };
357
358
  // ▲▲▲ [Perf] ▲▲▲
358
-
359
- // ▼▼▼ [New] 기본 마크다운 스타일 주입 ▼▼▼
359
+
360
+ // ▼▼▼ [New] 기본 마크다운 스타일 주입 ▼▼▼
360
361
  function injectMarkdownStyles() {
361
- // ▼▼▼ [FIX] 기존 스타일 제거 후 새로 주입 (업데이트 보장) ▼▼▼
362
- const existingStyle = document.getElementById('mind-map-markdown-styles');
363
- if (existingStyle) existingStyle.remove();
362
+ // ▼▼▼ [FIX] 기존 스타일 제거 후 새로 주입 (업데이트 보장) ▼▼▼
363
+ const existingStyle = document.getElementById('mind-map-markdown-styles');
364
+ if (existingStyle) existingStyle.remove();
364
365
  const style = document.createElement('style');
365
366
  style.id = 'mind-map-markdown-styles';
366
367
  style.textContent = `
@@ -388,17 +389,17 @@
388
389
  .markdown-body h2 { font-size: 1.4em; font-weight: bold; margin-top: 1em; margin-bottom: 0.5em; border-bottom: 1px solid #eaecef; padding-bottom: 0.3em; }
389
390
  .markdown-body h3 { font-size: 1.2em; font-weight: bold; margin-top: 1em; margin-bottom: 0.5em; }
390
391
  .markdown-body p { margin-bottom: 0.8em; white-space: normal; }
391
- .markdown-body ul { padding-left: 1.5em; margin-bottom: 1em; list-style-type: disc; }
392
- .markdown-body ol { padding-left: 1.5em; margin-bottom: 1em; list-style-type: decimal; }
393
- .markdown-body li { margin-bottom: 0.25em; }
394
- .markdown-body blockquote { border-left: 4px solid #dfe2e5; padding: 0 1em; color: #6a737d; margin: 0 0 1em 0; }
395
- .markdown-body code { font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; background-color: rgba(27,31,35,0.05); padding: 0.2em 0.4em; border-radius: 3px; font-size: 85%; }
396
- .markdown-body pre { background-color: #f6f8fa; padding: 12px; overflow: auto; border-radius: 3px; margin-bottom: 1em; }
397
- .markdown-body pre code { background-color: transparent; padding: 0; font-size: 100%; }
398
- .markdown-body a { color: #0366d6; text-decoration: none; }
399
- .markdown-body a:hover { text-decoration: underline; }
400
- .markdown-body img { max-width: 100%; box-sizing: content-box; background-color: #fff; }
401
- .markdown-body table { border-spacing: 0; border-collapse: collapse; width: 100%; margin-bottom: 1em; }
392
+ .markdown-body ul { padding-left: 1.5em; margin-bottom: 1em; list-style-type: disc; }
393
+ .markdown-body ol { padding-left: 1.5em; margin-bottom: 1em; list-style-type: decimal; }
394
+ .markdown-body li { margin-bottom: 0.25em; }
395
+ .markdown-body blockquote { border-left: 4px solid #dfe2e5; padding: 0 1em; color: #6a737d; margin: 0 0 1em 0; }
396
+ .markdown-body code { font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; background-color: rgba(27,31,35,0.05); padding: 0.2em 0.4em; border-radius: 3px; font-size: 85%; }
397
+ .markdown-body pre { background-color: #f6f8fa; padding: 12px; overflow: auto; border-radius: 3px; margin-bottom: 1em; }
398
+ .markdown-body pre code { background-color: transparent; padding: 0; font-size: 100%; }
399
+ .markdown-body a { color: #0366d6; text-decoration: none; }
400
+ .markdown-body a:hover { text-decoration: underline; }
401
+ .markdown-body img { max-width: 100%; box-sizing: content-box; background-color: #fff; }
402
+ .markdown-body table { border-spacing: 0; border-collapse: collapse; width: 100%; margin-bottom: 1em; }
402
403
  .markdown-body table th, .markdown-body table td { padding: 6px 13px; border: 1px solid #dfe2e5; }
403
404
  .markdown-body table th { font-weight: 600; background-color: #f6f8fa; }
404
405
  .markdown-body hr { height: 0.25em; padding: 0; margin: 24px 0; background-color: #e1e4e8; border: 0; }
@@ -597,10 +598,10 @@
597
598
  }
598
599
  /* Force list markers to be visible */
599
600
  .markdown-body ul li { list-style-type: disc; }
600
- .markdown-body ol li { list-style-type: decimal; }
601
-
602
- /* ▼▼▼ [NEW] 슬림 스크롤바 스타일 - 모든 텍스트/노트/코드 영역 (Razor + JS 동적노드 전체) ▼▼▼ */
603
- /* 1. Firefox 대응 + 여백 최소화 (속성 선택자로 모든 노드 잡기) */
601
+ .markdown-body ol li { list-style-type: decimal; }
602
+
603
+ /* ▼▼▼ [NEW] 슬림 스크롤바 스타일 - 모든 텍스트/노트/코드 영역 (Razor + JS 동적노드 전체) ▼▼▼ */
604
+ /* 1. Firefox 대응 + 여백 최소화 (속성 선택자로 모든 노드 잡기) */
604
605
  .note-textarea,
605
606
  .note-content,
606
607
  .markdown-body,
@@ -659,7 +660,7 @@
659
660
  width: 8px !important;
660
661
  height: 8px !important;
661
662
  }
662
-
663
+
663
664
  .note-textarea::-webkit-scrollbar-thumb,
664
665
  .note-content::-webkit-scrollbar-thumb,
665
666
  .markdown-body::-webkit-scrollbar-thumb,
@@ -750,7 +751,7 @@
750
751
  .map-node-code .code-body::-webkit-scrollbar-thumb:active {
751
752
  background: rgba(0, 0, 0, 0.45) !important;
752
753
  }
753
-
754
+
754
755
  /* 3. 노트 편집기는 렌더링 markdown과 같은 타이포그래피/박스 모델을 유지 */
755
756
  .map-node-note .note-textarea,
756
757
  .map-node-note textarea.note-textarea {
@@ -788,7 +789,7 @@
788
789
  color: transparent !important;
789
790
  text-shadow: 0 0 0 #ffffff !important;
790
791
  }
791
-
792
+
792
793
  .prose, [id^="node-response-"]:not(.image-content):not(.video-content):not(.embed-content) {
793
794
  padding-right: 14px !important;
794
795
  }
@@ -809,8 +810,8 @@
809
810
  overflow-wrap: inherit !important;
810
811
  }
811
812
  /* ▲▲▲ [NEW] ▲▲▲ */
812
-
813
- /* ▼▼▼ [NEW] CSS3D 텍스트 앨리어싱 개선 ▼▼▼ */
813
+
814
+ /* ▼▼▼ [NEW] CSS3D 텍스트 앨리어싱 개선 ▼▼▼ */
814
815
  .css3d-resolution-wrapper,
815
816
  .css3d-dynamic-node,
816
817
  [id^="css3d-node-"],
@@ -13458,7 +13459,7 @@
13458
13459
 
13459
13460
  function getRemoteFleetDisplayTitle(value) {
13460
13461
  const title = String(value ?? '').trim();
13461
- if (!title || title === REMOTE_FLEET_LEGACY_DISPLAY_NAME) {
13462
+ if (!title || title === REMOTE_FLEET_LEGACY_DISPLAY_NAME || title === REMOTE_FLEET_LEGACY_DISPLAY_NAME_V0) {
13462
13463
  return REMOTE_FLEET_DISPLAY_NAME;
13463
13464
  }
13464
13465
  return title;
@@ -13552,7 +13553,7 @@
13552
13553
 
13553
13554
  const titleBlock = document.createElement('div');
13554
13555
  titleBlock.className = 'template-card__title-block';
13555
- titleBlock.appendChild(createTemplateCardText('div', 'template-card__eyebrow', 'Remote'));
13556
+ titleBlock.appendChild(createTemplateCardText('div', 'template-card__eyebrow', 'Remote Work'));
13556
13557
  titleBlock.appendChild(createTemplateCardText('div', 'template-card__title', title));
13557
13558
  header.appendChild(titleBlock);
13558
13559
  shell.appendChild(header);
@@ -14605,7 +14606,7 @@
14605
14606
  const REMOTE_FLEET_MONITOR_REFRESH_MS = 10000;
14606
14607
  const REMOTE_FLEET_EMPTY_MONITOR_REFRESH_MS = 2500;
14607
14608
  const REMOTE_FLEET_LIVE_REFRESH_MS = 30000;
14608
- const REMOTE_FLEET_MONITOR_LIVE_FPS = 20;
14609
+ const REMOTE_FLEET_MONITOR_LIVE_FPS = 2;
14609
14610
  const REMOTE_FLEET_LIVE_FRAME_REFRESH_MS = Math.round(1000 / REMOTE_FLEET_MONITOR_LIVE_FPS);
14610
14611
  const REMOTE_FLEET_CONTROL_LIVE_FPS = 30;
14611
14612
  const REMOTE_FLEET_CONTROL_FRAME_REFRESH_MS = Math.round(1000 / REMOTE_FLEET_CONTROL_LIVE_FPS);
@@ -14626,8 +14627,6 @@
14626
14627
  const REMOTE_FLEET_BINARY_FRAME_CACHE_MS = 10000;
14627
14628
  const REMOTE_FLEET_BINARY_FRAME_CACHE_LIMIT = 240;
14628
14629
  const REMOTE_FLEET_BINARY_FRAME_SESSION_IDLE_MS = 8000;
14629
- const REMOTE_FLEET_AUTO_LIVE_START_MAX_PARALLEL = 8;
14630
- const REMOTE_FLEET_AUTO_LIVE_START_COOLDOWN_MS = 3000;
14631
14630
  const REMOTE_FLEET_TASK_FOLLOW_INITIAL_MS = 250;
14632
14631
  const REMOTE_FLEET_TASK_FOLLOW_REFRESH_MS = 2000;
14633
14632
  const REMOTE_FLEET_TASK_FOLLOW_MAX_TICKS = 60;
@@ -15690,12 +15689,13 @@
15690
15689
  }
15691
15690
  session.bodies.add(bodyView);
15692
15691
  session.getDeviceIds = getDeviceIds;
15693
- session.options = { fps: 20, width: Number(options.width || 1920) || 1920, height: Number(options.height || 1080) || 1080, tileWidth: Number(options.tileWidth || 320) || 320, tileHeight: Number(options.tileHeight || 180) || 180 };
15692
+ const fps = Math.max(1, Math.min(30, Number(options.fps || REMOTE_FLEET_MONITOR_LIVE_FPS) || REMOTE_FLEET_MONITOR_LIVE_FPS));
15693
+ session.options = { fps, width: Number(options.width || 1920) || 1920, height: Number(options.height || 1080) || 1080, tileWidth: Number(options.tileWidth || 320) || 320, tileHeight: Number(options.tileHeight || 180) || 180 };
15694
15694
  const fallback = () => {
15695
15695
  if (session.fallbackStarted) return;
15696
15696
  session.fallbackStarted = true;
15697
15697
  try { session.ws?.close?.(); } catch { /* encoder failure fallback */ }
15698
- session.bodies.forEach(body => startRemoteFleetBinaryFrameSocket(body, session.getDeviceIds, { autoStartLive: true, fps: 20, maxWidth: 960, maxHeight: 540, quality: 60, mode: 'mode2-lzo', streamPurpose: 'wall' }));
15698
+ session.bodies.forEach(body => startRemoteFleetBinaryFrameSocket(body, session.getDeviceIds, { autoStartLive: true, fps: session.options.fps, maxWidth: 960, maxHeight: 540, quality: 60, mode: 'mode2-lzo', streamPurpose: 'wall' }));
15699
15699
  window.RuntimeTrace?.emit?.('remote.atlas.fallback', { nodeId: session.nodeId, mode: 'mode2-lzo' });
15700
15700
  };
15701
15701
  session.fallback = fallback;
@@ -15709,7 +15709,7 @@
15709
15709
  ws.binaryType = 'arraybuffer';
15710
15710
  ws.onopen = () => {
15711
15711
  session.wsOpening = false;
15712
- ws.send(JSON.stringify({ type: 'configure', deviceIds: getRemoteFleetBinaryFrameDeviceIds(session), fps: 20, width: session.options.width, height: session.options.height, tileWidth: session.options.tileWidth, tileHeight: session.options.tileHeight }));
15712
+ ws.send(JSON.stringify({ type: 'configure', deviceIds: getRemoteFleetBinaryFrameDeviceIds(session), fps: session.options.fps, width: session.options.width, height: session.options.height, tileWidth: session.options.tileWidth, tileHeight: session.options.tileHeight }));
15713
15713
  window.RuntimeTrace?.emit?.('remote.atlas.wsOpen', { nodeId: session.nodeId });
15714
15714
  };
15715
15715
  ws.onmessage = event => {
@@ -17273,16 +17273,6 @@
17273
17273
  }
17274
17274
 
17275
17275
  binarySession.controlSessions.add(controlSession);
17276
- binarySession.liveOptions = {
17277
- ...(binarySession.liveOptions || {}),
17278
- autoStartLive: true,
17279
- fps: REMOTE_FLEET_CONTROL_LIVE_FPS,
17280
- maxWidth: 1280,
17281
- maxHeight: 720,
17282
- quality: 65,
17283
- mode: 'mode3-h264-hw',
17284
- streamPurpose: 'control'
17285
- };
17286
17276
  controlSession.binaryFrameSession = binarySession;
17287
17277
  controlSession.binaryFramePreferred = true;
17288
17278
  refreshRemoteFleetBinaryFrameSubscription(binarySession, true);
@@ -17368,7 +17358,7 @@
17368
17358
  session.canvas.dataset.remoteFleetFrameSeq = String(frameSeq || 0);
17369
17359
  session.canvas.style.display = 'block';
17370
17360
  session.lastControlFrameAt = Date.now();
17371
- session.status.textContent = `Control ${REMOTE_FLEET_CONTROL_LIVE_FPS}fps`;
17361
+ session.status.textContent = `${session.statusLabel || 'Control'} ${Number(session.fps || REMOTE_FLEET_CONTROL_LIVE_FPS)}fps`;
17372
17362
  session.placeholder?.remove?.();
17373
17363
  session.placeholder = null;
17374
17364
  }
@@ -17403,7 +17393,7 @@
17403
17393
  session.canvas.dataset.remoteFleetFrameUrl = String(frame.frameUrl || '');
17404
17394
  session.canvas.style.display = 'block';
17405
17395
  session.lastControlFrameAt = Date.now();
17406
- session.status.textContent = `Control ${REMOTE_FLEET_CONTROL_LIVE_FPS}fps`;
17396
+ session.status.textContent = `${session.statusLabel || 'Control'} ${Number(session.fps || REMOTE_FLEET_CONTROL_LIVE_FPS)}fps`;
17407
17397
  session.placeholder?.remove?.();
17408
17398
  session.placeholder = null;
17409
17399
  if (typeof bitmap.close === 'function') {
@@ -18152,7 +18142,9 @@
18152
18142
  wasLiveActive: false,
18153
18143
  startedStream: false,
18154
18144
  streamId: '',
18155
- controlLeaseId: `control-node-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
18145
+ controlLeaseId: `observe-node-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
18146
+ statusLabel: 'Observe',
18147
+ fps: REMOTE_FLEET_MONITOR_LIVE_FPS,
18156
18148
  timer: null,
18157
18149
  moveTimer: null,
18158
18150
  pendingMove: null,
@@ -18170,25 +18162,21 @@
18170
18162
  };
18171
18163
 
18172
18164
  bodyView._remoteFleetControlNodeSession = session;
18173
- bindRemoteFleetControlInput(session, {
18174
- captureOverlayEvents: false,
18175
- windowKeyboardCapture: false
18176
- });
18177
- ensureRemoteFleetControlInputSocket(session);
18178
18165
  startRemoteFleetBinaryFrameSocket(bodyView, () => [normalizedDeviceId], {
18179
18166
  autoStartLive: true,
18180
- fps: REMOTE_FLEET_CONTROL_LIVE_FPS,
18181
- maxWidth: 1280,
18182
- maxHeight: 720,
18183
- quality: 65,
18184
- mode: 'mode3-h264-hw'
18167
+ fps: REMOTE_FLEET_MONITOR_LIVE_FPS,
18168
+ maxWidth: 960,
18169
+ maxHeight: 540,
18170
+ quality: 60,
18171
+ mode: 'mode2-lzo',
18172
+ streamPurpose: 'wall'
18185
18173
  });
18186
18174
  attachRemoteFleetControlBinaryFrameSession(session, bodyView);
18187
18175
  stage.focus?.({ preventScroll: true });
18188
18176
  if (status) {
18189
- status.textContent = `Control ${REMOTE_FLEET_CONTROL_LIVE_FPS}fps`;
18177
+ status.textContent = `Observe ${REMOTE_FLEET_MONITOR_LIVE_FPS}fps`;
18190
18178
  }
18191
- window.RuntimeTrace?.emit?.('remote.control.nodeOpened', {
18179
+ window.RuntimeTrace?.emit?.('remote.observe.nodeOpened', {
18192
18180
  nodeId: normalizedNodeId,
18193
18181
  deviceId: normalizedDeviceId
18194
18182
  });
@@ -18440,7 +18428,7 @@
18440
18428
 
18441
18429
  if (!device || !deviceId) {
18442
18430
  const empty = document.createElement('div');
18443
- empty.textContent = lastError || 'Remote control device snapshot is unavailable.';
18431
+ empty.textContent = lastError || 'Remote workstation snapshot is unavailable.';
18444
18432
  empty.style.cssText = `
18445
18433
  flex: 1 1 auto;
18446
18434
  display: flex;
@@ -18456,7 +18444,7 @@
18456
18444
  text-align: center;
18457
18445
  background: rgba(255, 255, 255, 0.74);
18458
18446
  `;
18459
- const refreshButton = createRemoteFleetButton('Refresh', 'Refresh this remote control device', 'refresh-device');
18447
+ const refreshButton = createRemoteFleetButton('Refresh', 'Refresh this remote workstation', 'refresh-device');
18460
18448
  refreshButton.style.height = '32px';
18461
18449
  refreshButton.addEventListener('click', async event => {
18462
18450
  event.preventDefault();
@@ -18579,7 +18567,7 @@
18579
18567
  controlStage.dataset.remoteFleetControlNodeStage = 'true';
18580
18568
  controlStage.dataset.deviceId = deviceId;
18581
18569
  controlStage.tabIndex = 0;
18582
- controlStage.title = `${name} remote control`;
18570
+ controlStage.title = `${name} observation and remote work`;
18583
18571
  controlStage.style.cssText = `
18584
18572
  position: relative;
18585
18573
  flex: 1 1 auto;
@@ -18589,7 +18577,7 @@
18589
18577
  border-radius: 0;
18590
18578
  background: #020617;
18591
18579
  border: 1px solid rgba(15, 23, 42, 0.20);
18592
- cursor: crosshair;
18580
+ cursor: default;
18593
18581
  touch-action: none;
18594
18582
  outline: none;
18595
18583
  `;
@@ -18637,7 +18625,7 @@
18637
18625
  `;
18638
18626
  const controlPlaceholder = document.createElement('div');
18639
18627
  controlPlaceholder.dataset.remoteFleetControlNodePlaceholder = 'true';
18640
- controlPlaceholder.textContent = connected ? 'Waiting for screen' : 'Offline';
18628
+ controlPlaceholder.textContent = connected ? 'Waiting for observation frame' : 'Offline';
18641
18629
  controlPlaceholder.style.cssText = `
18642
18630
  position: absolute;
18643
18631
  inset: 0;
@@ -18672,7 +18660,7 @@
18672
18660
  if (session && restoredControlSurface) {
18673
18661
  session.lastControlFrameSeq = Number(preservedControlSurface?.seq || session.lastControlFrameSeq || 0) || 0;
18674
18662
  session.lastControlFrameAt = Date.now();
18675
- controlStatus.textContent = `Control ${REMOTE_FLEET_CONTROL_LIVE_FPS}fps`;
18663
+ controlStatus.textContent = `Observe ${REMOTE_FLEET_MONITOR_LIVE_FPS}fps`;
18676
18664
  window.RuntimeTrace?.emit?.('remote.control.surfaceRestored', {
18677
18665
  nodeId,
18678
18666
  deviceId,
@@ -18692,7 +18680,7 @@
18692
18680
  }
18693
18681
  }
18694
18682
 
18695
- const refreshButton = createRemoteFleetButton('Refresh', 'Refresh this remote control device', 'refresh-device');
18683
+ const refreshButton = createRemoteFleetButton('Refresh', 'Refresh this remote workstation', 'refresh-device');
18696
18684
  refreshButton.style.height = '26px';
18697
18685
  actions.appendChild(refreshButton);
18698
18686
  if (connected && thumbnailEnabled) {
@@ -18733,8 +18721,8 @@
18733
18721
  right: 8px;
18734
18722
  top: 42px;
18735
18723
  z-index: 6;
18736
- width: min(360px, calc(100% - 16px));
18737
- display: none;
18724
+ width: min(520px, calc(100% - 16px));
18725
+ display: grid;
18738
18726
  grid-template-columns: minmax(0, 1fr) auto auto;
18739
18727
  gap: 6px;
18740
18728
  align-items: stretch;
@@ -18979,8 +18967,7 @@
18979
18967
 
18980
18968
  function renderRemoteFleetMonitor(bodyView, nodeModel) {
18981
18969
  if (!bodyView) return;
18982
- clearRemoteFleetTimers(bodyView, { keepBinaryFrameSocket: true });
18983
- const preservedFrameSurfaces = collectRemoteFleetPreviewFrameSurfaces(bodyView);
18970
+ clearRemoteFleetTimers(bodyView);
18984
18971
 
18985
18972
  const nodeId = String(nodeModel?.id ?? nodeModel?.Id ?? '');
18986
18973
  const searchState = bodyView.dataset.remoteFleetSearch || '';
@@ -18990,12 +18977,11 @@
18990
18977
  const densityState = normalizeRemoteFleetMonitorSize(bodyView.dataset.remoteFleetDensity || 'cards');
18991
18978
  const aiAssistState = bodyView.dataset.remoteFleetAiAssist === 'true';
18992
18979
  const autoMonitorState = bodyView.dataset.remoteFleetAutoMonitor !== 'false';
18993
- const focusState = bodyView.dataset.remoteFleetFocusDeviceId || '';
18994
18980
  const latestTaskBatch = parseRemoteFleetLatestTaskBatch(nodeModel);
18995
18981
  const recentTaskBatches = parseRemoteFleetRecentTaskBatches(nodeModel);
18996
18982
  const latestTaskBatchKey = getRemoteFleetTaskBatchKey(latestTaskBatch);
18997
18983
  const latestTaskBatchActive = isRemoteFleetTaskBatchActive(latestTaskBatch);
18998
- const showTaskSummarySurfaces = false;
18984
+ const showTaskSummarySurfaces = true;
18999
18985
  if (latestTaskBatchKey && bodyView.dataset.remoteFleetTaskFollowKey !== latestTaskBatchKey) {
19000
18986
  bodyView.dataset.remoteFleetTaskFollowKey = latestTaskBatchKey;
19001
18987
  bodyView.dataset.remoteFleetTaskFollowTicks = '0';
@@ -19009,18 +18995,10 @@
19009
18995
  : [...sortedDevices].sort((left, right) =>
19010
18996
  compareRemoteFleetGroups(left, right, groupState)
19011
18997
  || compareRemoteFleetDevices(left, right, sortState));
19012
- const hasActiveLiveStream = devices.some(isRemoteFleetLiveActive);
19013
18998
  const hasMonitorTargets = devices.some(device =>
19014
18999
  isRemoteFleetDeviceConnected(device) && isRemoteFleetThumbnailCapable(device));
19015
- const focusedDevice = devices.find(device => getRemoteFleetDeviceId(device) === focusState)
19016
- || devices.find(device => isRemoteFleetLiveActive(device))
19017
- || null;
19018
- if (focusedDevice) {
19019
- bodyView.dataset.remoteFleetFocusDeviceId = getRemoteFleetDeviceId(focusedDevice);
19020
- }
19021
- const selectedState = bodyView.dataset.remoteFleetSelectedDeviceId || focusState || '';
19000
+ const selectedState = bodyView.dataset.remoteFleetSelectedDeviceId || '';
19022
19001
  const selectedDevice = devices.find(device => getRemoteFleetDeviceId(device) === selectedState)
19023
- || focusedDevice
19024
19002
  || devices[0]
19025
19003
  || null;
19026
19004
  if (selectedDevice) {
@@ -19147,7 +19125,7 @@
19147
19125
  const searchInput = document.createElement('input');
19148
19126
  searchInput.type = 'search';
19149
19127
  searchInput.value = searchState;
19150
- searchInput.placeholder = '';
19128
+ searchInput.placeholder = 'Find a computer or task';
19151
19129
  searchInput.dataset.remoteFleetSearch = 'true';
19152
19130
  searchInput.autocomplete = 'off';
19153
19131
  searchInput.spellcheck = false;
@@ -19172,8 +19150,7 @@
19172
19150
  { value: 'offline', label: 'Offline' },
19173
19151
  { value: 'task', label: 'Task' },
19174
19152
  { value: 'ai', label: 'AI' },
19175
- { value: 'live', label: 'Live' },
19176
- { value: 'frames', label: 'Frames' },
19153
+ { value: 'frames', label: 'Thumbnails' },
19177
19154
  { value: 'issues', label: 'Issues' }
19178
19155
  ], filterState, 'Device filter');
19179
19156
 
@@ -19220,11 +19197,11 @@
19220
19197
  filterRow.appendChild(groupSelect);
19221
19198
  filterRow.appendChild(densitySelect);
19222
19199
  filterRow.appendChild(matchCount);
19223
- filterRow.dataset.remoteFleetAdvancedFilters = 'hidden';
19200
+ filterRow.dataset.remoteFleetAdvancedFilters = 'visible';
19224
19201
 
19225
19202
  const taskRow = document.createElement('div');
19226
19203
  taskRow.style.cssText = `
19227
- display: none;
19204
+ display: grid;
19228
19205
  grid-template-columns: minmax(0, 1fr) auto auto auto;
19229
19206
  gap: 8px;
19230
19207
  align-items: stretch;
@@ -19232,7 +19209,7 @@
19232
19209
  `;
19233
19210
  const taskInput = document.createElement('textarea');
19234
19211
  taskInput.dataset.remoteFleetTaskInput = 'true';
19235
- taskInput.placeholder = '';
19212
+ taskInput.placeholder = 'Describe the work: inspect Git, run tests, build, deploy, or summarize results';
19236
19213
  taskInput.rows = 2;
19237
19214
  taskInput.style.cssText = `
19238
19215
  width: 100%;
@@ -19286,7 +19263,7 @@
19286
19263
  aiToggleText.textContent = 'AI';
19287
19264
  aiToggleLabel.appendChild(aiToggle);
19288
19265
  aiToggleLabel.appendChild(aiToggleText);
19289
- const sendVisibleButton = createRemoteFleetButton('Send visible', 'Dispatch task to visible connected task-capable devices', 'task-visible');
19266
+ const sendVisibleButton = createRemoteFleetButton('Send selected', 'Dispatch task to the selected agent', 'task-selected');
19290
19267
  sendVisibleButton.style.height = '54px';
19291
19268
  const sendConnectedButton = createRemoteFleetButton('Send all', 'Dispatch task to all connected task-capable devices', 'task-connected');
19292
19269
  sendConnectedButton.style.height = '54px';
@@ -19294,7 +19271,7 @@
19294
19271
  taskRow.appendChild(aiToggleLabel);
19295
19272
  taskRow.appendChild(sendVisibleButton);
19296
19273
  taskRow.appendChild(sendConnectedButton);
19297
- taskRow.dataset.remoteFleetTaskComposer = 'hidden';
19274
+ taskRow.dataset.remoteFleetTaskComposer = 'visible';
19298
19275
 
19299
19276
  const taskFeedback = document.createElement('div');
19300
19277
  taskFeedback.dataset.remoteFleetTaskFeedback = 'true';
@@ -19313,6 +19290,8 @@
19313
19290
  text-overflow: ellipsis;
19314
19291
  white-space: nowrap;
19315
19292
  `;
19293
+ bodyView.appendChild(filterRow);
19294
+ bodyView.appendChild(taskRow);
19316
19295
  bodyView.appendChild(taskFeedback);
19317
19296
 
19318
19297
  if (showTaskSummarySurfaces && latestTaskBatch) {
@@ -19484,23 +19463,16 @@
19484
19463
  const createDevicePreview = (device, mode = 'tile') => {
19485
19464
  const name = getRemoteFleetDeviceName(device);
19486
19465
  const connectedDevice = isRemoteFleetDeviceConnected(device);
19487
- const thumbnailCapturedAt = String(device?.thumbnailCapturedAt || device?.ThumbnailCapturedAt || '');
19488
- const liveFrameReceivedAt = String(device?.liveFrameReceivedAt || device?.LiveFrameReceivedAt || '');
19489
19466
  const hasThumbnail = hasRemoteFleetThumbnail(device);
19490
- const hasLiveFrame = hasRemoteFleetLiveFrame(device);
19491
- const previewSource = hasLiveFrame
19492
- ? getRemoteFleetFrameSource(device, 'live')
19493
- : getRemoteFleetFrameSource(device, 'thumbnail');
19467
+ const previewSource = getRemoteFleetFrameSource(device, 'thumbnail');
19494
19468
  const hasPreviewSource = isRemoteFleetFrameSource(previewSource);
19495
- const isDetail = mode === 'detail';
19496
19469
 
19497
19470
  const preview = document.createElement('div');
19498
19471
  preview.dataset.remoteFleetDevicePreview = mode;
19499
19472
  preview.dataset.deviceId = getRemoteFleetDeviceId(device);
19500
- preview.dataset.remoteFleetFrameKind = hasLiveFrame ? 'live' : (hasThumbnail ? 'thumbnail' : '');
19501
- preview.dataset.remoteFleetFrameSeq = String(hasLiveFrame
19502
- ? getRemoteFleetDeviceField(device, 'liveFrameSeq', 'LiveFrameSeq', 0)
19503
- : getRemoteFleetDeviceField(device, 'thumbnailFrameSeq', 'ThumbnailFrameSeq', 0));
19473
+ preview.dataset.remoteFleetFrameKind = hasThumbnail ? 'thumbnail' : '';
19474
+ preview.dataset.remoteFleetFrameSeq = String(
19475
+ getRemoteFleetDeviceField(device, 'thumbnailFrameSeq', 'ThumbnailFrameSeq', 0));
19504
19476
  preview.title = getRemoteFleetDeviceTooltip(device);
19505
19477
  preview.style.cssText = `
19506
19478
  position: relative;
@@ -19516,13 +19488,13 @@
19516
19488
  `;
19517
19489
  ensureRemoteFleetFrameCanvas(preview);
19518
19490
 
19519
- if ((hasLiveFrame || hasThumbnail) && hasPreviewSource) {
19491
+ if (hasThumbnail && hasPreviewSource) {
19520
19492
  const image = document.createElement('img');
19521
19493
  image.dataset.remoteFleetFrameImage = 'true';
19522
- image.dataset.remoteFleetFrameKind = hasLiveFrame ? 'live' : 'thumbnail';
19494
+ image.dataset.remoteFleetFrameKind = 'thumbnail';
19523
19495
  image.dataset.remoteFleetFrameSeq = preview.dataset.remoteFleetFrameSeq;
19524
19496
  image.src = previewSource;
19525
- image.alt = hasLiveFrame ? `${name} live frame` : `${name} thumbnail`;
19497
+ image.alt = `${name} thumbnail`;
19526
19498
  image.loading = 'lazy';
19527
19499
  image.decoding = 'async';
19528
19500
  image.style.cssText = `
@@ -19543,7 +19515,6 @@
19543
19515
  preview.appendChild(placeholder);
19544
19516
  }
19545
19517
 
19546
- restoreRemoteFleetPreviewFrameSurface(preview, preservedFrameSurfaces, getRemoteFleetDeviceId(device));
19547
19518
  return preview;
19548
19519
  };
19549
19520
 
@@ -19611,10 +19582,7 @@
19611
19582
  const connectedDevice = isRemoteFleetDeviceConnected(device);
19612
19583
  const name = getRemoteFleetDeviceName(device);
19613
19584
  const deviceId = getRemoteFleetDeviceId(device);
19614
- const liveStreamEnabled = isRemoteFleetLiveCapable(device);
19615
- const liveStreamActive = isRemoteFleetLiveActive(device);
19616
19585
  const hasThumbnail = hasRemoteFleetThumbnail(device);
19617
- const hasLiveFrame = hasRemoteFleetLiveFrame(device);
19618
19586
  const taskEnabled = isRemoteFleetDeviceTaskCapable(device);
19619
19587
  const aiAssistEnabled = isRemoteFleetDeviceAiCapable(device);
19620
19588
  const latestTaskStatus = String(device?.latestTaskStatus || device?.LatestTaskStatus || '');
@@ -19662,15 +19630,13 @@
19662
19630
  card.dataset.remoteFleetConnected = connectedDevice ? 'true' : 'false';
19663
19631
  card.dataset.remoteFleetTaskCapable = taskEnabled ? 'true' : 'false';
19664
19632
  card.dataset.remoteFleetAiCapable = aiAssistEnabled ? 'true' : 'false';
19665
- card.dataset.remoteFleetHasFrame = (hasThumbnail || hasLiveFrame) ? 'true' : 'false';
19666
- card.dataset.remoteFleetLiveCapable = liveStreamEnabled ? 'true' : 'false';
19667
- card.dataset.remoteFleetLiveActive = liveStreamActive ? 'true' : 'false';
19633
+ card.dataset.remoteFleetHasFrame = hasThumbnail ? 'true' : 'false';
19668
19634
  card.dataset.remoteFleetIssue = (!connectedDevice || latestTaskStatus === 'failed' || !!latestTaskError) ? 'true' : 'false';
19669
19635
  card.dataset.remoteFleetSelected = isSelected ? 'true' : 'false';
19670
19636
  card.dataset.remoteFleetAction = 'select-device';
19671
19637
  card.setAttribute('role', 'button');
19672
19638
  card.setAttribute('aria-pressed', isSelected ? 'true' : 'false');
19673
- card.setAttribute('aria-label', `Open remote control for ${name}`);
19639
+ card.setAttribute('aria-label', `Select ${name} as the agent task target`);
19674
19640
  card.tabIndex = 0;
19675
19641
  card.title = getRemoteFleetDeviceTooltip(device);
19676
19642
  card.style.cssText = `
@@ -19778,121 +19744,12 @@
19778
19744
  const readTaskInstruction = () => String(taskInput.value || '').trim();
19779
19745
  const useAiAssist = () => aiToggle.checked === true;
19780
19746
  const getDeviceCards = () => Array.from(grid.querySelectorAll('article[data-device-id]'));
19781
- const pushUniqueFrameDeviceId = (ids, device) => {
19782
- const id = getRemoteFleetDeviceId(device);
19783
- if (id && !ids.includes(id)) {
19784
- ids.push(id);
19785
- }
19786
- };
19787
- const getVisibleFrameDeviceIds = () => {
19788
- const ids = getDeviceCards()
19747
+ const getVisibleFrameDeviceIds = () => Array.from(new Set(
19748
+ getDeviceCards()
19789
19749
  .filter(card => card.style.display !== 'none')
19790
19750
  .map(card => String(card.dataset.deviceId || '').trim())
19791
- .filter(Boolean);
19792
- return Array.from(new Set(ids)).slice(0, 120);
19793
- };
19794
- const getVisibleLiveDevices = () => {
19795
- const visibleIds = new Set(getVisibleFrameDeviceIds());
19796
- return devices
19797
- .filter(device => {
19798
- const id = getRemoteFleetDeviceId(device);
19799
- return id
19800
- && visibleIds.has(id)
19801
- && isRemoteFleetDeviceConnected(device)
19802
- && isRemoteFleetLiveCapable(device);
19803
- })
19804
- .slice(0, 120);
19805
- };
19806
- const getVisibleLiveFrameDeviceIds = () => {
19807
- const ids = [];
19808
- getVisibleLiveDevices().forEach(device => pushUniqueFrameDeviceId(ids, device));
19809
- devices
19810
- .filter(device => isRemoteFleetDeviceConnected(device) && isRemoteFleetLiveActive(device))
19811
- .forEach(device => pushUniqueFrameDeviceId(ids, device));
19812
- return ids.slice(0, 120);
19813
- };
19814
- const ensureVisibleRemoteFleetLiveStreams = async () => {
19815
- if (!(bodyView._remoteFleetAutoLiveStartInFlight instanceof Set)) {
19816
- bodyView._remoteFleetAutoLiveStartInFlight = new Set();
19817
- }
19818
- if (!(bodyView._remoteFleetAutoLiveStartAttemptAt instanceof Map)) {
19819
- bodyView._remoteFleetAutoLiveStartAttemptAt = new Map();
19820
- }
19821
- const now = Date.now();
19822
- const targets = getVisibleLiveDevices()
19823
- .filter(device => {
19824
- const deviceId = getRemoteFleetDeviceId(device);
19825
- const lastAttemptAt = Number(bodyView._remoteFleetAutoLiveStartAttemptAt.get(deviceId) || 0);
19826
- return deviceId
19827
- && !isRemoteFleetLiveActive(device)
19828
- && !bodyView._remoteFleetAutoLiveStartInFlight.has(deviceId)
19829
- && (now - lastAttemptAt >= REMOTE_FLEET_AUTO_LIVE_START_COOLDOWN_MS);
19830
- });
19831
- if (targets.length === 0) {
19832
- return null;
19833
- }
19834
-
19835
- let started = 0;
19836
- let lastResult = null;
19837
- const queue = targets.slice(0, 120);
19838
- const startOne = async target => {
19839
- const deviceId = getRemoteFleetDeviceId(target);
19840
- if (!deviceId || bodyView._remoteFleetAutoLiveStartInFlight.has(deviceId)) {
19841
- return;
19842
- }
19843
-
19844
- bodyView._remoteFleetAutoLiveStartInFlight.add(deviceId);
19845
- bodyView._remoteFleetAutoLiveStartAttemptAt.set(deviceId, Date.now());
19846
- try {
19847
- const result = await invokeDotNetAsync('StartRemoteFleetLiveStreamFromJs', nodeId, deviceId, REMOTE_FLEET_MONITOR_LIVE_FPS);
19848
- lastResult = result;
19849
- const success = isRemoteFleetResultSuccess(result);
19850
- window.RuntimeTrace?.emit?.('remote.live.autoStart', {
19851
- nodeId,
19852
- deviceId,
19853
- success,
19854
- fps: result?.fps || result?.Fps || 0,
19855
- mode: 'visible-grid',
19856
- error: result?.error || result?.Error || ''
19857
- });
19858
- if (success) {
19859
- started += 1;
19860
- }
19861
- } finally {
19862
- bodyView._remoteFleetAutoLiveStartInFlight.delete(deviceId);
19863
- }
19864
- };
19865
-
19866
- const workerCount = Math.min(REMOTE_FLEET_AUTO_LIVE_START_MAX_PARALLEL, queue.length);
19867
- await Promise.all(Array.from({ length: workerCount }, async () => {
19868
- while (queue.length > 0) {
19869
- const target = queue.shift();
19870
- if (target) {
19871
- await startOne(target);
19872
- }
19873
- }
19874
- }));
19875
-
19876
- if (started > 0) {
19877
- try {
19878
- await refreshRemoteFleetNode();
19879
- } catch (error) {
19880
- window.RuntimeTrace?.emit?.('remote.live.autoRefreshFailed', {
19881
- nodeId,
19882
- started,
19883
- error: error?.message || String(error || '')
19884
- });
19885
- }
19886
-
19887
- window.RuntimeTrace?.emit?.('remote.live.awaitingBinaryFrames', {
19888
- nodeId,
19889
- started,
19890
- transport: 'ws-binary'
19891
- });
19892
- }
19893
-
19894
- return lastResult || { success: started > 0, started };
19895
- };
19751
+ .filter(Boolean)))
19752
+ .slice(0, 120);
19896
19753
  const refreshRemoteFleetNode = async () => {
19897
19754
  if (bodyView._remoteFleetRefreshInFlight === true) {
19898
19755
  return null;
@@ -19909,7 +19766,9 @@
19909
19766
  };
19910
19767
  const refreshRemoteFleetVisibleFrames = async (requestThumbnails = false) => {
19911
19768
  const result = await invokeDotNetAsync('RefreshRemoteFleetFramesFromJs', nodeId, getVisibleFrameDeviceIds(), requestThumbnails === true);
19912
- applyRemoteFleetFramePatches(bodyView, normalizeRemoteFleetDeviceFramePatches(result));
19769
+ const thumbnailPatches = normalizeRemoteFleetDeviceFramePatches(result)
19770
+ .filter(frame => String(frame?.kind || '').toLowerCase() === 'thumbnail');
19771
+ applyRemoteFleetFramePatches(bodyView, thumbnailPatches);
19913
19772
  return result;
19914
19773
  };
19915
19774
  const startRemoteFleetMonitorRefreshTimer = (intervalMs, reason = 'monitor') => {
@@ -20011,15 +19870,18 @@
20011
19870
  scheduleRemoteFleetTaskFollow();
20012
19871
  }
20013
19872
  };
20014
- const getVisibleEligibleDeviceIds = () => {
19873
+ const getSelectedEligibleDeviceId = () => {
19874
+ const selectedId = String(bodyView.dataset.remoteFleetSelectedDeviceId || '').trim();
19875
+ if (!selectedId) return '';
19876
+ const card = getDeviceCards().find(item => String(item.dataset.deviceId || '').trim() === selectedId);
20015
19877
  const wantsAi = useAiAssist();
20016
- return getDeviceCards()
20017
- .filter(card => card.style.display !== 'none'
20018
- && card.dataset.remoteFleetConnected === 'true'
20019
- && card.dataset.remoteFleetTaskCapable === 'true'
20020
- && (!wantsAi || card.dataset.remoteFleetAiCapable === 'true'))
20021
- .map(card => String(card.dataset.deviceId || '').trim())
20022
- .filter(Boolean);
19878
+ return card
19879
+ && card.style.display !== 'none'
19880
+ && card.dataset.remoteFleetConnected === 'true'
19881
+ && card.dataset.remoteFleetTaskCapable === 'true'
19882
+ && (!wantsAi || card.dataset.remoteFleetAiCapable === 'true')
19883
+ ? selectedId
19884
+ : '';
20023
19885
  };
20024
19886
 
20025
19887
  const applyRemoteFleetFilters = () => {
@@ -20038,7 +19900,6 @@
20038
19900
  || (filterMode === 'offline' && card.dataset.remoteFleetConnected !== 'true')
20039
19901
  || (filterMode === 'task' && card.dataset.remoteFleetConnected === 'true' && card.dataset.remoteFleetTaskCapable === 'true')
20040
19902
  || (filterMode === 'ai' && card.dataset.remoteFleetConnected === 'true' && card.dataset.remoteFleetAiCapable === 'true')
20041
- || (filterMode === 'live' && card.dataset.remoteFleetLiveActive === 'true')
20042
19903
  || (filterMode === 'frames' && card.dataset.remoteFleetHasFrame === 'true')
20043
19904
  || (filterMode === 'issues' && card.dataset.remoteFleetIssue === 'true');
20044
19905
  const show = matchesSearch && matchesMode;
@@ -20072,10 +19933,11 @@
20072
19933
 
20073
19934
  matchCount.textContent = `${visible}/${devices.length}`;
20074
19935
  matchCount.title = `${visible} visible, ${eligible} targetable`;
20075
- sendVisibleButton.disabled = eligible <= 0;
20076
- sendVisibleButton.title = eligible > 0
20077
- ? `Dispatch to ${eligible} visible target(s)`
20078
- : 'No visible targetable devices';
19936
+ const selectedTargetId = getSelectedEligibleDeviceId();
19937
+ sendVisibleButton.disabled = !selectedTargetId;
19938
+ sendVisibleButton.title = selectedTargetId
19939
+ ? `Dispatch to selected agent ${selectedTargetId}`
19940
+ : 'Select an online task-capable agent';
20079
19941
  const allTargetCount = wantsAi ? aiCapableCount : taskCapableCount;
20080
19942
  sendConnectedButton.disabled = allTargetCount <= 0;
20081
19943
  sendConnectedButton.title = allTargetCount > 0
@@ -20149,6 +20011,7 @@
20149
20011
  deviceId: normalizedSelectedId,
20150
20012
  mode: 'dom-patch'
20151
20013
  });
20014
+ applyRemoteFleetFilters();
20152
20015
  };
20153
20016
  getDeviceCards().forEach(card => {
20154
20017
  ['pointerdown', 'mousedown', 'mouseup'].forEach(eventName => {
@@ -20164,40 +20027,6 @@
20164
20027
  event.stopImmediatePropagation?.();
20165
20028
  });
20166
20029
  });
20167
- card.addEventListener('dblclick', event => {
20168
- event.preventDefault();
20169
- event.stopPropagation();
20170
- event.stopImmediatePropagation?.();
20171
- const deviceId = String(card.dataset.deviceId || '').trim();
20172
- if (!deviceId) return;
20173
- applySelectedDeviceVisualState(deviceId);
20174
- if (card.dataset.remoteFleetOpeningControlNode === 'true') {
20175
- return;
20176
- }
20177
-
20178
- card.dataset.remoteFleetOpeningControlNode = 'true';
20179
- invokeDotNetAsync('AddRemoteFleetDeviceNodeFromJs', nodeId, deviceId)
20180
- .then(async result => {
20181
- await syncRemoteFleetNodeStateFromResult(result);
20182
- if (result?.success) {
20183
- const controlNodeId = String(result.nodeId || result.NodeId || '').trim();
20184
- if (controlNodeId) {
20185
- window.mindMap?.focusNode?.(controlNodeId, { selectNode: true });
20186
- window.mindMap?.selectNode?.(controlNodeId);
20187
- invokeDotNetAsync('SelectNodeInBlazor', controlNodeId).catch(() => undefined);
20188
- }
20189
- setTaskFeedback('');
20190
- } else {
20191
- setTaskFeedback(result?.error || result?.Error || 'Remote control node failed.', 'error');
20192
- }
20193
- })
20194
- .catch(error => {
20195
- setTaskFeedback(error?.message || 'Remote control node failed.', 'error');
20196
- })
20197
- .finally(() => {
20198
- delete card.dataset.remoteFleetOpeningControlNode;
20199
- });
20200
- });
20201
20030
  const selectCard = event => {
20202
20031
  event.preventDefault();
20203
20032
  event.stopPropagation();
@@ -20233,8 +20062,6 @@
20233
20062
  scheduleRemoteFleetTaskFollow();
20234
20063
  }
20235
20064
 
20236
- primeRemoteFleetRenderedFrameSurfaces(bodyView, 'monitor-render');
20237
-
20238
20065
  sendVisibleButton.addEventListener('click', async event => {
20239
20066
  event.preventDefault();
20240
20067
  event.stopPropagation();
@@ -20245,19 +20072,19 @@
20245
20072
  return;
20246
20073
  }
20247
20074
 
20248
- const targetIds = getVisibleEligibleDeviceIds();
20249
- if (targetIds.length === 0) {
20250
- setTaskFeedback('No visible targetable devices.', 'error');
20075
+ const targetId = getSelectedEligibleDeviceId();
20076
+ if (!targetId) {
20077
+ setTaskFeedback('Select an online task-capable agent.', 'error');
20251
20078
  return;
20252
20079
  }
20253
20080
 
20254
20081
  sendVisibleButton.disabled = true;
20255
- setTaskFeedback('Dispatching task to visible devices...');
20082
+ setTaskFeedback('Dispatching task to selected agent...');
20256
20083
  try {
20257
- const result = await invokeDotNetAsync('DispatchRemoteFleetTaskBatchFromJs', nodeId, targetIds, instruction, useAiAssist());
20084
+ const result = await invokeDotNetAsync('DispatchRemoteFleetTaskFromJs', nodeId, targetId, instruction, useAiAssist());
20258
20085
  await syncRemoteFleetNodeStateFromResult(result);
20259
- const mode = useAiAssist() ? 'visible AI task' : 'visible remote task';
20260
- const feedback = formatRemoteFleetDispatchFeedback(result, targetIds.length, mode);
20086
+ const mode = useAiAssist() ? 'selected AI task' : 'selected agent task';
20087
+ const feedback = formatRemoteFleetDispatchFeedback(result, 1, mode);
20261
20088
  setTaskFeedback(feedback.text, feedback.tone);
20262
20089
  startRemoteFleetTaskFollowFromResult(result);
20263
20090
  } finally {
@@ -20381,84 +20208,6 @@
20381
20208
  });
20382
20209
  });
20383
20210
 
20384
- bodyView.querySelectorAll('[data-remote-fleet-action="pin-device"]').forEach(button => {
20385
- button.addEventListener('click', async event => {
20386
- event.preventDefault();
20387
- event.stopPropagation();
20388
- const deviceId = String(button.dataset.deviceId || '').trim();
20389
- if (!deviceId) return;
20390
- button.disabled = true;
20391
- setTaskFeedback('Pinning device to canvas...');
20392
- try {
20393
- const result = await invokeDotNetAsync('AddRemoteFleetDeviceNodeFromJs', nodeId, deviceId);
20394
- await syncRemoteFleetNodeStateFromResult(result);
20395
- if (result?.success) {
20396
- setTaskFeedback('Pinned device node.', 'success');
20397
- } else {
20398
- setTaskFeedback(result?.error || 'Pin failed.', 'error');
20399
- }
20400
- } finally {
20401
- button.disabled = false;
20402
- }
20403
- });
20404
- });
20405
-
20406
- bodyView.querySelectorAll('[data-remote-fleet-action="live-focus"]').forEach(button => {
20407
- button.addEventListener('click', event => {
20408
- event.preventDefault();
20409
- event.stopPropagation();
20410
- const deviceId = String(button.dataset.deviceId || '').trim();
20411
- if (!deviceId) return;
20412
- bodyView.dataset.remoteFleetFocusDeviceId = deviceId;
20413
- renderRemoteFleetMonitor(bodyView, nodeModel);
20414
- });
20415
- });
20416
-
20417
- bodyView.querySelectorAll('[data-remote-fleet-action="live-start"]').forEach(button => {
20418
- button.addEventListener('click', async event => {
20419
- event.preventDefault();
20420
- event.stopPropagation();
20421
- const deviceId = String(button.dataset.deviceId || '').trim();
20422
- if (!deviceId) return;
20423
- bodyView.dataset.remoteFleetFocusDeviceId = deviceId;
20424
- button.disabled = true;
20425
- setTaskFeedback('Starting focused live stream...');
20426
- try {
20427
- const result = await invokeDotNetAsync('StartRemoteFleetLiveStreamFromJs', nodeId, deviceId);
20428
- await syncRemoteFleetNodeStateFromResult(result);
20429
- if (result?.success) {
20430
- setTaskFeedback('Focused live stream started.', 'success');
20431
- } else {
20432
- setTaskFeedback(result?.error || 'Live stream start failed.', 'error');
20433
- }
20434
- } finally {
20435
- button.disabled = false;
20436
- }
20437
- });
20438
- });
20439
-
20440
- bodyView.querySelectorAll('[data-remote-fleet-action="live-stop"]').forEach(button => {
20441
- button.addEventListener('click', async event => {
20442
- event.preventDefault();
20443
- event.stopPropagation();
20444
- const deviceId = String(button.dataset.deviceId || '').trim();
20445
- if (!deviceId) return;
20446
- button.disabled = true;
20447
- setTaskFeedback('Stopping focused live stream...');
20448
- try {
20449
- const result = await invokeDotNetAsync('StopRemoteFleetLiveStreamFromJs', nodeId, deviceId, button.dataset.streamId || '');
20450
- await syncRemoteFleetNodeStateFromResult(result);
20451
- if (result?.success) {
20452
- setTaskFeedback('Focused live stream stopped.', 'success');
20453
- } else {
20454
- setTaskFeedback(result?.error || 'Live stream stop failed.', 'error');
20455
- }
20456
- } finally {
20457
- button.disabled = false;
20458
- }
20459
- });
20460
- });
20461
-
20462
20211
  bodyView.querySelectorAll('[data-remote-fleet-action="ping-device"]').forEach(button => {
20463
20212
  button.addEventListener('click', async event => {
20464
20213
  event.preventDefault();
@@ -20525,65 +20274,13 @@
20525
20274
  }, 250);
20526
20275
  }
20527
20276
 
20528
- const visibleFrameDeviceIds = getVisibleFrameDeviceIds();
20529
- const hasVisibleFrameTarget = visibleFrameDeviceIds.length > 0;
20530
- const hasVisibleLiveTarget = getVisibleLiveFrameDeviceIds().length > 0
20531
- || devices.some(device => isRemoteFleetDeviceConnected(device) && isRemoteFleetLiveCapable(device));
20532
- const shouldUseBinaryFrameSocket = hasVisibleLiveTarget
20533
- || hasActiveLiveStream
20534
- || (autoMonitorState && hasVisibleFrameTarget);
20535
- if (shouldUseBinaryFrameSocket) {
20536
- const atlasSocketStarted = startRemoteFleetAtlasSocket(bodyView, () => {
20537
- const liveIds = getVisibleLiveFrameDeviceIds();
20538
- return liveIds.length > 0 ? liveIds : getVisibleFrameDeviceIds();
20539
- }, {
20540
- fps: REMOTE_FLEET_MONITOR_LIVE_FPS,
20541
- width: 1920,
20542
- height: 1080,
20543
- tileWidth: 320,
20544
- tileHeight: 180
20545
- });
20546
- if (!atlasSocketStarted) {
20547
- window.RuntimeTrace?.emit?.('remote.frame.wsUnavailable', {
20548
- nodeId,
20549
- reason: 'atlas-frame-socket-not-started'
20550
- });
20551
- ensureVisibleRemoteFleetLiveStreams().catch(error => {
20552
- window.RuntimeTrace?.emit?.('remote.live.autoStartFailed', {
20553
- nodeId,
20554
- error: error?.message || String(error || '')
20555
- });
20556
- });
20557
- } else {
20558
- window.RuntimeTrace?.emit?.('remote.live.wsAutoStartRequested', {
20559
- nodeId,
20560
- transport: 'ws-atlas-h264',
20561
- fps: REMOTE_FLEET_MONITOR_LIVE_FPS,
20562
- visibleFrameTargets: visibleFrameDeviceIds.length,
20563
- visibleLiveTargets: getVisibleLiveFrameDeviceIds().length
20564
- });
20565
- }
20566
- bodyView._remoteFleetLiveRefreshTimer = setInterval(async () => {
20567
- if (!document.body.contains(bodyView)) {
20568
- clearRemoteFleetTimers(bodyView);
20569
- return;
20570
- }
20571
-
20572
- try {
20573
- await refreshRemoteFleetNode();
20574
- } catch {
20575
- clearRemoteFleetTimers(bodyView);
20576
- }
20577
- }, REMOTE_FLEET_LIVE_REFRESH_MS);
20578
- } else {
20579
- releaseRemoteFleetBinaryFrameSocket(bodyView);
20580
- releaseRemoteFleetAtlasSocket(bodyView);
20581
- if (autoMonitorState && hasMonitorTargets) {
20582
- startRemoteFleetFrameLoop(bodyView, REMOTE_FLEET_THUMBNAIL_FRAME_REFRESH_MS, () => refreshRemoteFleetVisibleFrames(true));
20583
- startRemoteFleetMonitorRefreshTimer(REMOTE_FLEET_MONITOR_REFRESH_MS, 'visible-devices');
20584
- } else if (autoMonitorState) {
20585
- startRemoteFleetMonitorRefreshTimer(REMOTE_FLEET_EMPTY_MONITOR_REFRESH_MS, 'empty-discovery');
20586
- }
20277
+ releaseRemoteFleetBinaryFrameSocket(bodyView);
20278
+ releaseRemoteFleetAtlasSocket(bodyView);
20279
+ if (autoMonitorState && hasMonitorTargets) {
20280
+ startRemoteFleetFrameLoop(bodyView, REMOTE_FLEET_THUMBNAIL_FRAME_REFRESH_MS, () => refreshRemoteFleetVisibleFrames(true));
20281
+ startRemoteFleetMonitorRefreshTimer(REMOTE_FLEET_MONITOR_REFRESH_MS, 'thumbnail-only');
20282
+ } else if (autoMonitorState) {
20283
+ startRemoteFleetMonitorRefreshTimer(REMOTE_FLEET_EMPTY_MONITOR_REFRESH_MS, 'empty-discovery');
20587
20284
  }
20588
20285
  }
20589
20286
 
@@ -21112,7 +20809,7 @@
21112
20809
  * ▼▼▼ [New] text/markdown 노드를 위한 동적 DOM 요소 생성 ▼▼▼
21113
20810
  * Blazor 템플릿이 없는 노드 타입을 위해 JS에서 직접 DOM 생성
21114
20811
  */
21115
- // Expose helper to create markdown content consistently
20812
+ // Expose helper to create markdown content consistently
21116
20813
  function createMarkdownContent(content, isCodeNode = false) {
21117
20814
  const wrapper = document.createElement('div');
21118
20815
  const normalizedContent = typeof content === 'string' ? content : String(content || '');
@@ -21169,22 +20866,22 @@
21169
20866
  contentEl.textContent = nextText;
21170
20867
  }
21171
20868
  }
21172
-
21173
- function getCachedMarkdownHtml(nodeModel, content) {
21174
- if (!nodeModel) return null;
21175
- if (nodeModel._cachedMarkdownSource === content && nodeModel._cachedMarkdownHtml) {
21176
- return nodeModel._cachedMarkdownHtml;
21177
- }
21178
- return null;
21179
- }
21180
-
21181
- function setCachedMarkdownHtml(nodeModel, content, html) {
21182
- if (!nodeModel) return;
21183
- nodeModel._cachedMarkdownSource = content;
21184
- nodeModel._cachedMarkdownHtml = html;
21185
- }
21186
-
21187
- // HTML 이스케이프 헬퍼 - kept for compatibility
20869
+
20870
+ function getCachedMarkdownHtml(nodeModel, content) {
20871
+ if (!nodeModel) return null;
20872
+ if (nodeModel._cachedMarkdownSource === content && nodeModel._cachedMarkdownHtml) {
20873
+ return nodeModel._cachedMarkdownHtml;
20874
+ }
20875
+ return null;
20876
+ }
20877
+
20878
+ function setCachedMarkdownHtml(nodeModel, content, html) {
20879
+ if (!nodeModel) return;
20880
+ nodeModel._cachedMarkdownSource = content;
20881
+ nodeModel._cachedMarkdownHtml = html;
20882
+ }
20883
+
20884
+ // HTML 이스케이프 헬퍼 - kept for compatibility
21188
20885
  function escapeHtml(text) {
21189
20886
  const div = document.createElement('div');
21190
20887
  div.textContent = text;
@@ -23193,7 +22890,7 @@
23193
22890
  ensurePromptCopyDelegation();
23194
22891
 
23195
22892
  // ... (logging)
23196
-
22893
+
23197
22894
  // 컨테이너 생성
23198
22895
  const container = document.createElement('div');
23199
22896
  container.id = `node-${nodeModel.id}`;
@@ -23217,7 +22914,7 @@
23217
22914
  color: #1f2937;
23218
22915
  box-shadow: none;
23219
22916
  `;
23220
-
22917
+
23221
22918
  // 콘텐츠 래퍼 생성
23222
22919
  const contentWrapper = document.createElement('div');
23223
22920
  contentWrapper.className = isCsvTableNode
@@ -23316,12 +23013,12 @@
23316
23013
  // 응답 영역 생성
23317
23014
  const responseDiv = document.createElement('div');
23318
23015
  responseDiv.id = `node-response-${nodeModel.id}`;
23319
-
23320
- // [FIX] 'note' type should also render markdown
23016
+
23017
+ // [FIX] 'note' type should also render markdown
23321
23018
  const isCodeNode = contentTypeLower === 'code';
23322
23019
  // Variable isNoteNode declared above
23323
23020
  const isMarkdownType = ['text', 'markdown'].includes(contentTypeLower); // Removed 'note'
23324
-
23021
+
23325
23022
  if (isCsvTableNode) {
23326
23023
  responseDiv.className = 'node-response csv-table-scroll';
23327
23024
  responseDiv.style.cssText = `
@@ -23387,44 +23084,44 @@
23387
23084
  padding-right: 14px;
23388
23085
  box-sizing: border-box;
23389
23086
  `;
23390
-
23391
- // 1. Save Metadata
23392
- responseDiv.dataset.src = content;
23393
- responseDiv.dataset.nodeId = nodeModel.id;
23394
-
23395
- // 2. Render Markdown or Text
23396
- const cachedHtml = getCachedMarkdownHtml(nodeModel, content);
23397
- if (cachedHtml) {
23398
- responseDiv.innerHTML = cachedHtml;
23399
- } else if (typeof marked !== 'undefined' && content) {
23400
- const options = { breaks: true, gfm: true };
23401
- try {
23402
- if (typeof marked.parse === 'function') {
23403
- responseDiv.innerHTML = marked.parse(content, options);
23404
- } else if (typeof marked === 'function') {
23405
- responseDiv.innerHTML = marked(content, options);
23406
- } else {
23407
- responseDiv.innerText = content;
23408
- }
23409
- setCachedMarkdownHtml(nodeModel, content, responseDiv.innerHTML);
23410
- } catch {
23411
- responseDiv.innerText = content;
23412
- }
23413
- } else {
23414
- responseDiv.innerText = content;
23415
- }
23416
-
23417
- // 3. (Removed) Click Event Listener
23418
- // Centralized interaction logic in mind-map-interactions.js handles selection and editing.
23419
- // Also pointer-events are none by default, so this wouldn't fire.
23420
-
23421
- // Prevent drag interference (optional, but harmless)
23422
- responseDiv.ondragstart = (e) => {
23423
- if (e.target.tagName !== 'A' && !e.target.closest('a')) {
23424
- e.preventDefault();
23425
- }
23426
- };
23427
-
23087
+
23088
+ // 1. Save Metadata
23089
+ responseDiv.dataset.src = content;
23090
+ responseDiv.dataset.nodeId = nodeModel.id;
23091
+
23092
+ // 2. Render Markdown or Text
23093
+ const cachedHtml = getCachedMarkdownHtml(nodeModel, content);
23094
+ if (cachedHtml) {
23095
+ responseDiv.innerHTML = cachedHtml;
23096
+ } else if (typeof marked !== 'undefined' && content) {
23097
+ const options = { breaks: true, gfm: true };
23098
+ try {
23099
+ if (typeof marked.parse === 'function') {
23100
+ responseDiv.innerHTML = marked.parse(content, options);
23101
+ } else if (typeof marked === 'function') {
23102
+ responseDiv.innerHTML = marked(content, options);
23103
+ } else {
23104
+ responseDiv.innerText = content;
23105
+ }
23106
+ setCachedMarkdownHtml(nodeModel, content, responseDiv.innerHTML);
23107
+ } catch {
23108
+ responseDiv.innerText = content;
23109
+ }
23110
+ } else {
23111
+ responseDiv.innerText = content;
23112
+ }
23113
+
23114
+ // 3. (Removed) Click Event Listener
23115
+ // Centralized interaction logic in mind-map-interactions.js handles selection and editing.
23116
+ // Also pointer-events are none by default, so this wouldn't fire.
23117
+
23118
+ // Prevent drag interference (optional, but harmless)
23119
+ responseDiv.ondragstart = (e) => {
23120
+ if (e.target.tagName !== 'A' && !e.target.closest('a')) {
23121
+ e.preventDefault();
23122
+ }
23123
+ };
23124
+
23428
23125
  } else if (isMarkdownType) {
23429
23126
  responseDiv.className = 'node-response markdown-body mind-map-text-scrollable-view';
23430
23127
  // ▼▼▼ [FIX] 텍스트 선택을 위한 스타일 설정 ▼▼▼
@@ -23442,15 +23139,15 @@
23442
23139
  user-select: text;
23443
23140
  -webkit-user-select: text;
23444
23141
  `;
23445
- // ▲▲▲ [FIX] ▲▲▲
23446
- const cachedHtml = getCachedMarkdownHtml(nodeModel, content);
23447
- if (cachedHtml) {
23448
- responseDiv.innerHTML = cachedHtml;
23449
- } else {
23450
- const html = createMarkdownContent(content);
23451
- responseDiv.innerHTML = html;
23452
- setCachedMarkdownHtml(nodeModel, content, html);
23453
- }
23142
+ // ▲▲▲ [FIX] ▲▲▲
23143
+ const cachedHtml = getCachedMarkdownHtml(nodeModel, content);
23144
+ if (cachedHtml) {
23145
+ responseDiv.innerHTML = cachedHtml;
23146
+ } else {
23147
+ const html = createMarkdownContent(content);
23148
+ responseDiv.innerHTML = html;
23149
+ setCachedMarkdownHtml(nodeModel, content, html);
23150
+ }
23454
23151
  } else if (contentTypeLower === 'image') {
23455
23152
  responseDiv.className = 'node-response image-content';
23456
23153
  responseDiv.style.cssText = `width: 100%; height: 100%; display: ${hasContent ? 'block' : 'none'}; position: relative; overflow: hidden; pointer-events: none; padding: 0 !important; margin: 0 !important; background: transparent; border: 0 !important; scrollbar-width: none;`;
@@ -23534,15 +23231,15 @@
23534
23231
 
23535
23232
  return container;
23536
23233
  }
23537
-
23538
- // HTML 이스케이프 헬퍼
23539
- function escapeHtml(text) {
23540
- const div = document.createElement('div');
23541
- div.textContent = text;
23542
- return div.innerHTML;
23543
- }
23544
- // ▲▲▲ [New] ▲▲▲
23545
-
23234
+
23235
+ // HTML 이스케이프 헬퍼
23236
+ function escapeHtml(text) {
23237
+ const div = document.createElement('div');
23238
+ div.textContent = text;
23239
+ return div.innerHTML;
23240
+ }
23241
+ // ▲▲▲ [New] ▲▲▲
23242
+
23546
23243
  function getNodeViewContentInfo(nodeModel) {
23547
23244
  const contentTypeLower = String(nodeModel?.contentType ?? nodeModel?.ContentType ?? '').toLowerCase();
23548
23245
  const remoteFleetMonitor = isRemoteFleetMonitorNode(nodeModel);
@@ -23800,7 +23497,7 @@
23800
23497
  console.error('[MindMapCss3DManager] CSS3DObject not available');
23801
23498
  return null;
23802
23499
  }
23803
-
23500
+
23804
23501
  const resolvedNodeId = getNodeId(nodeModel);
23805
23502
  const templateId = `node-${resolvedNodeId}`;
23806
23503
  let templateElement = document.getElementById(templateId);
@@ -23822,60 +23519,60 @@
23822
23519
  || contentTypeLower === 'embed') {
23823
23520
  templateElement = null;
23824
23521
  }
23825
-
23826
- let isDynamicallyCreated = false;
23827
- // text/markdown 타입은 Blazor 템플릿이 없으므로 동적 생성
23828
- if (!templateElement) {
23829
- // text/markdown type has no Blazor template, so create dynamically.
23830
- // [FIX] 'note' type also falls back to dynamic creation if not found (or if we force dynamic for consistent MD)
23831
- // But usually 'note' comes from Razor. If missing, we create it.
23522
+
23523
+ let isDynamicallyCreated = false;
23524
+ // text/markdown 타입은 Blazor 템플릿이 없으므로 동적 생성
23525
+ if (!templateElement) {
23526
+ // text/markdown type has no Blazor template, so create dynamically.
23527
+ // [FIX] 'note' type also falls back to dynamic creation if not found (or if we force dynamic for consistent MD)
23528
+ // But usually 'note' comes from Razor. If missing, we create it.
23832
23529
  const dynamicTypes = ['text', 'markdown', 'code', 'note', 'memo', CSV_TABLE_CONTENT_TYPE, 'templatelauncher', 'image', 'video', 'embed'];
23833
23530
 
23834
23531
  if (remoteFleetMonitor || dynamicTypes.includes(contentTypeLower)) {
23835
23532
  log(`[MindMapCss3DManager] Deferring dynamic DOM element for ${visualContentTypeLower || contentTypeLower} node ${resolvedNodeId} to shared NodeView`);
23836
23533
  templateElement = document.createElement('div');
23837
23534
  isDynamicallyCreated = true;
23838
- } else {
23839
- // 경고 로그 제한 (초기 로딩 시 템플릿이 아직 렌더링되지 않은 경우 스팸 방지)
23840
- if (!window._templateWarningCount) window._templateWarningCount = 0;
23841
- if (window._templateWarningCount < 5) {
23535
+ } else {
23536
+ // 경고 로그 제한 (초기 로딩 시 템플릿이 아직 렌더링되지 않은 경우 스팸 방지)
23537
+ if (!window._templateWarningCount) window._templateWarningCount = 0;
23538
+ if (window._templateWarningCount < 5) {
23842
23539
  warn(`[MindMapCss3DManager] Template not found for node ${resolvedNodeId} (async templates pending). Template ID: ${templateId}`);
23843
- window._templateWarningCount++;
23844
- if (window._templateWarningCount === 5) {
23845
- warn(`[MindMapCss3DManager] ... suppressing further template warnings. Templates will be available after async render.`);
23846
- }
23847
- }
23848
- return null;
23849
- }
23850
- }
23851
- // ▲▲▲ [New] ▲▲▲
23852
-
23540
+ window._templateWarningCount++;
23541
+ if (window._templateWarningCount === 5) {
23542
+ warn(`[MindMapCss3DManager] ... suppressing further template warnings. Templates will be available after async render.`);
23543
+ }
23544
+ }
23545
+ return null;
23546
+ }
23547
+ }
23548
+ // ▲▲▲ [New] ▲▲▲
23549
+
23853
23550
  const existingCss3dId = `css3d-node-${resolvedNodeId}`;
23854
23551
  const nodeEntry = module.nodeObjectsById.get(resolvedNodeId);
23855
-
23856
- if (nodeEntry && nodeEntry.cssObject) {
23552
+
23553
+ if (nodeEntry && nodeEntry.cssObject) {
23857
23554
  warn(`[MindMapCss3DManager] CSS3D object already exists in memory for node ${resolvedNodeId}. Returning existing object.`);
23858
- return nodeEntry.cssObject;
23859
- }
23860
-
23861
- const existingCss3dElement = document.getElementById(existingCss3dId);
23862
- if (existingCss3dElement) {
23863
- warn(`[MindMapCss3DManager] Removing duplicate CSS3D element from DOM: ${existingCss3dId}`);
23864
- if (existingCss3dElement.parentElement) {
23865
- existingCss3dElement.parentElement.removeChild(existingCss3dElement);
23866
- }
23867
- }
23868
-
23869
- // (It lives in a hidden div, so it is not visible.)
23870
- /*
23871
- const razorMapNode = document.querySelector(`#${templateId}`);
23872
- if (razorMapNode && razorMapNode.parentElement) {
23873
- console.warn(`[MindMapCss3DManager] Removing Razor-generated MapNode from DOM: ${templateId}`);
23874
- razorMapNode.parentElement.removeChild(razorMapNode);
23875
- }
23876
- */
23877
- // ▲▲▲ [Critical fix] ▲▲▲
23878
-
23555
+ return nodeEntry.cssObject;
23556
+ }
23557
+
23558
+ const existingCss3dElement = document.getElementById(existingCss3dId);
23559
+ if (existingCss3dElement) {
23560
+ warn(`[MindMapCss3DManager] Removing duplicate CSS3D element from DOM: ${existingCss3dId}`);
23561
+ if (existingCss3dElement.parentElement) {
23562
+ existingCss3dElement.parentElement.removeChild(existingCss3dElement);
23563
+ }
23564
+ }
23565
+
23566
+ // (It lives in a hidden div, so it is not visible.)
23567
+ /*
23568
+ const razorMapNode = document.querySelector(`#${templateId}`);
23569
+ if (razorMapNode && razorMapNode.parentElement) {
23570
+ console.warn(`[MindMapCss3DManager] Removing Razor-generated MapNode from DOM: ${templateId}`);
23571
+ razorMapNode.parentElement.removeChild(razorMapNode);
23572
+ }
23573
+ */
23574
+ // ▲▲▲ [Critical fix] ▲▲▲
23575
+
23879
23576
  const nodeView = createNodeViewElement(module, nodeModel, {
23880
23577
  hostKind: NODE_VIEW_HOST_CSS3D,
23881
23578
  idPrefix: 'css3d-node'
@@ -23900,31 +23597,31 @@
23900
23597
  }
23901
23598
  clonedElement.style.display = '';
23902
23599
  clonedElement.style.transformOrigin = '0% 0%';
23903
-
23904
- // ▼▼▼ [Clarity] Scale factor for high-resolution rendering ▼▼▼
23600
+
23601
+ // ▼▼▼ [Clarity] Scale factor for high-resolution rendering ▼▼▼
23905
23602
  const resolutionScale = nodeView.resolutionScale;
23906
23603
  applyCss3dResolutionLayout(clonedElement, width, height, resolutionScale, {
23907
23604
  resetLayoutTransform: true
23908
23605
  });
23909
23606
  clonedElement.style.borderRadius = '0px';
23910
-
23911
- // to the inner element, the node shifts down-right.
23912
- // Remove it so the element sits at the wrapper origin (0,0).
23913
- clonedElement.style.position = 'absolute';
23914
- clonedElement.style.left = '0px';
23915
- clonedElement.style.top = '0px';
23916
- // ▲▲▲ [Changed] ▲▲▲
23917
-
23607
+
23608
+ // to the inner element, the node shifts down-right.
23609
+ // Remove it so the element sits at the wrapper origin (0,0).
23610
+ clonedElement.style.position = 'absolute';
23611
+ clonedElement.style.left = '0px';
23612
+ clonedElement.style.top = '0px';
23613
+ // ▲▲▲ [Changed] ▲▲▲
23614
+
23918
23615
  clonedElement.style.transition = 'none';
23919
23616
  clonedElement.style.webkitTransition = 'none';
23920
-
23921
- // ▼▼▼ [Perf] GPU acceleration for smooth zoom/pan ▼▼▼
23922
- // 초기에는 꺼둠 (auto). 카메라 이동 시 mind-map-core.js가 setWillChange(true) 호출
23617
+
23618
+ // ▼▼▼ [Perf] GPU acceleration for smooth zoom/pan ▼▼▼
23619
+ // 초기에는 꺼둠 (auto). 카메라 이동 시 mind-map-core.js가 setWillChange(true) 호출
23923
23620
  clonedElement.style.willChange = 'auto'; // 'transform';
23924
23621
  clonedElement.style.backfaceVisibility = isMediaNode ? 'visible' : 'hidden';
23925
23622
  clonedElement.style.webkitBackfaceVisibility = isMediaNode ? 'visible' : 'hidden';
23926
23623
  // ▲▲▲ [Perf] ▲▲▲
23927
-
23624
+
23928
23625
  const wrapper = document.createElement('div');
23929
23626
  const wrapperTypeClass = `node-type-${visualContentTypeLower || 'unknown'}`;
23930
23627
  wrapper.className = `css3d-resolution-wrapper lod-high ${wrapperTypeClass}`;
@@ -23937,10 +23634,10 @@
23937
23634
  wrapper.dataset.nodeId = resolvedNodeId;
23938
23635
  wrapper.style.transition = CSS3D_WRAPPER_TRANSITION;
23939
23636
  wrapper.style.webkitTransition = CSS3D_WRAPPER_WEBKIT_TRANSITION;
23940
-
23941
- wrapper.style.transformOrigin = '0 0';
23942
- // ▲▲▲ [Critical fix] ▲▲▲
23943
-
23637
+
23638
+ wrapper.style.transformOrigin = '0 0';
23639
+ // ▲▲▲ [Critical fix] ▲▲▲
23640
+
23944
23641
  // ▼▼▼ [Fix] 외부 스크롤바 방지 - 내부 콘텐츠 스크롤바만 표시 ▼▼▼
23945
23642
  wrapper.style.overflow = allowsExternalNodeChrome ? 'visible' : 'hidden';
23946
23643
  if (allowsExternalNodeChrome) {
@@ -23949,14 +23646,14 @@
23949
23646
  wrapper.style.containIntrinsicSize = 'auto';
23950
23647
  }
23951
23648
  // ▲▲▲ [Fix] ▲▲▲
23952
-
23953
- // ▼▼▼ [Perf] GPU acceleration for smooth zoom/pan ▼▼▼
23649
+
23650
+ // ▼▼▼ [Perf] GPU acceleration for smooth zoom/pan ▼▼▼
23954
23651
  wrapper.style.willChange = 'auto'; // 'transform';
23955
23652
  wrapper.style.backfaceVisibility = isMediaNode ? 'visible' : 'hidden';
23956
23653
  wrapper.style.webkitBackfaceVisibility = isMediaNode ? 'visible' : 'hidden';
23957
23654
  wrapper.style.transformStyle = isMediaNode ? 'flat' : '';
23958
23655
  // ▲▲▲ [Perf] ▲▲▲
23959
-
23656
+
23960
23657
  if (contentTypeLower === 'image' || contentTypeLower === 'video') {
23961
23658
  wrapper.style.pointerEvents = 'auto'; // [Fix] Changed from none to auto for resize handle detection
23962
23659
  clonedElement.style.pointerEvents = 'none';
@@ -24033,10 +23730,10 @@
24033
23730
  }
24034
23731
 
24035
23732
  // ... (event listeners) ...
24036
-
24037
- const css3dObj = new CSS3DObjectCtor(wrapper);
24038
- css3dObj.scale.set(1 / resolutionScale, 1 / resolutionScale, 1 / resolutionScale);
24039
- css3dObj.visible = false;
23733
+
23734
+ const css3dObj = new CSS3DObjectCtor(wrapper);
23735
+ css3dObj.scale.set(1 / resolutionScale, 1 / resolutionScale, 1 / resolutionScale);
23736
+ css3dObj.visible = false;
24040
23737
  css3dObj.userData.nodeId = resolvedNodeId;
24041
23738
  css3dObj.userData.worldWidth = width;
24042
23739
  css3dObj.userData.worldHeight = height;
@@ -24046,10 +23743,10 @@
24046
23743
  }
24047
23744
 
24048
23745
  // ▼▼▼ [Fix] nodeModel의 위치 정보를 사용하여 초기 위치 설정 ▼▼▼
24049
- // 호출자가 위치를 따로 설정하지 않아도 올바른 위치에 생성됩니다.
24050
- // Z 오프셋 0: 그리드(Z=0)와 동일한 평면에 배치하여 원근/시차 완전 제거
24051
- const posX = nodeModel.positionX ?? nodeModel.PositionX ?? 0;
24052
- const posY = nodeModel.positionY ?? nodeModel.PositionY ?? 0;
23746
+ // 호출자가 위치를 따로 설정하지 않아도 올바른 위치에 생성됩니다.
23747
+ // Z 오프셋 0: 그리드(Z=0)와 동일한 평면에 배치하여 원근/시차 완전 제거
23748
+ const posX = nodeModel.positionX ?? nodeModel.PositionX ?? 0;
23749
+ const posY = nodeModel.positionY ?? nodeModel.PositionY ?? 0;
24053
23750
  const posZ = (nodeModel.positionZ ?? nodeModel.PositionZ ?? 0); // [Fix] Z Offset 제거 (0으로 통일)
24054
23751
  css3dObj.position.set(posX, posY, posZ);
24055
23752
  // ▲▲▲ [Fix] ▲▲▲
@@ -24062,7 +23759,7 @@
24062
23759
 
24063
23760
  return css3dObj;
24064
23761
  }
24065
-
23762
+
24066
23763
  function syncCss3dContent(nodeModel, cssObject) {
24067
23764
  if (!nodeModel || !cssObject || !cssObject.element) return;
24068
23765
  const nodeId = String(getNodeId(nodeModel) || '').trim();
@@ -24147,7 +23844,7 @@
24147
23844
  });
24148
23845
  }
24149
23846
  }
24150
-
23847
+
24151
23848
  const prompt = nodeModel.prompt ?? nodeModel.Prompt ?? '';
24152
23849
  if (contentTypeLower === 'image' || contentTypeLower === 'video' || contentTypeLower === 'embed') {
24153
23850
  restoreCss3dMediaOverlayState(nodeModel, cssObject);
@@ -24506,19 +24203,19 @@
24506
24203
  renderBusinessAutomationChrome(resolveBusinessAutomationChromeContainer(el), nodeModel);
24507
24204
  }
24508
24205
  // Reusable objects for Frustum Culling (avoid GC)
24509
- const _frustum = new THREE.Frustum();
24510
- const _projScreenMatrix = new THREE.Matrix4();
24511
- const _worldPos = new THREE.Vector3();
24512
- const _worldQuat = new THREE.Quaternion();
24513
- const _worldScale = new THREE.Vector3();
24514
- const _nodeBox = new THREE.Box3(); // 바운딩 박스용
24515
-
24516
-
24517
- // ▼▼▼ [New] Minimal update loop - only syncs positions when dirty ▼▼▼
24518
- // 그리드와 동일한 Z 평면(Z=0)에 배치하여 원근/시차 완전 제거
24519
- // [Fix] CSS3D 노드도 Z=0으로 완전 평탄화하여 카메라 이동 시 그리드와 정렬 유지
24520
- const CSS3D_Z_OFFSET = 0;
24521
-
24206
+ const _frustum = new THREE.Frustum();
24207
+ const _projScreenMatrix = new THREE.Matrix4();
24208
+ const _worldPos = new THREE.Vector3();
24209
+ const _worldQuat = new THREE.Quaternion();
24210
+ const _worldScale = new THREE.Vector3();
24211
+ const _nodeBox = new THREE.Box3(); // 바운딩 박스용
24212
+
24213
+
24214
+ // ▼▼▼ [New] Minimal update loop - only syncs positions when dirty ▼▼▼
24215
+ // 그리드와 동일한 Z 평면(Z=0)에 배치하여 원근/시차 완전 제거
24216
+ // [Fix] CSS3D 노드도 Z=0으로 완전 평탄화하여 카메라 이동 시 그리드와 정렬 유지
24217
+ const CSS3D_Z_OFFSET = 0;
24218
+
24522
24219
  function syncCssPositionFromGl(module, nodeEntry, targetZ) {
24523
24220
  const glObject = nodeEntry.glObject;
24524
24221
  const cssPos = nodeEntry.cssObject.position;
@@ -24622,8 +24319,8 @@
24622
24319
  dirtyIds?.clear?.();
24623
24320
  module._cssParentRepairNeeded = false;
24624
24321
  }
24625
- // ▲▲▲ [New] ▲▲▲
24626
-
24322
+ // ▲▲▲ [New] ▲▲▲
24323
+
24627
24324
  // ▼▼▼ [New] 일괄 will-change 제어 (성능 최적화) ▼▼▼
24628
24325
  let _willChangeState = null; // null = unknown, true = enabled, false = disabled
24629
24326
  let _willChangeTouchedIds = new Set();
@@ -24818,10 +24515,10 @@
24818
24515
  }
24819
24516
  if (el.style.willChange !== value) el.style.willChange = value;
24820
24517
  });
24821
-
24822
- // Razor 템플릿 기반 요소 (id가 css3d-node-로 시작하는 것들)
24823
- // (위의 createCss3dObject에서 clonedElement에 해당)
24824
- // querySelectorAll로 잡을 수 있음
24518
+
24519
+ // Razor 템플릿 기반 요소 (id가 css3d-node-로 시작하는 것들)
24520
+ // (위의 createCss3dObject에서 clonedElement에 해당)
24521
+ // querySelectorAll로 잡을 수 있음
24825
24522
  const clonedElements = document.querySelectorAll('[id^="css3d-node-"]');
24826
24523
  clonedElements.forEach(el => {
24827
24524
  if (effectiveEnabled && shouldPreserveAgentMotionCssElement(el)) {
@@ -24831,9 +24528,9 @@
24831
24528
  if (el.style.willChange !== value) el.style.willChange = value;
24832
24529
  });
24833
24530
  }
24834
- // ▲▲▲ [New] ▲▲▲
24835
-
24836
- // Expose MindMapCss3DManager globally
24531
+ // ▲▲▲ [New] ▲▲▲
24532
+
24533
+ // Expose MindMapCss3DManager globally
24837
24534
  window.MindMapCss3DManager = {
24838
24535
  activateSelectableTextOverlay: activateSelectableTextOverlay,
24839
24536
  activateNativeTextSelectionSource: activateNativeTextSelectionSource,
@@ -24912,6 +24609,6 @@
24912
24609
  hideBusinessAutomationFloatingTooltipForNode: hideBusinessAutomationFloatingTooltipForNode,
24913
24610
  update: update // Export
24914
24611
  };
24915
-
24916
- console.log('✓ mind-map-css3d-manager.js loaded (Note editing fixed)');
24917
- })();
24612
+
24613
+ console.log('✓ mind-map-css3d-manager.js loaded (Note editing fixed)');
24614
+ })();